#! /usr/bin/env python3 # -*- coding: utf-8 -*- """ Copyright 2020, Nils Hilbricht, Germany ( https://www.hilbricht.net ) This file is part of the Laborejo Software Suite ( https://www.laborejo.org ), more specifically its template base application. The Template Base Application is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ import logging; logger = logging.getLogger(__name__); logger.info("import") #Standard Library Modules from typing import Tuple import os.path #Third Party Modules from calfbox import cbox #Our template modules from ..start import PATHS from . import sequencer from ..helper import cache_unlimited, flatList from .duration import D1024 #to add a note off spacer. class Metronome(object): """ A metronome uses calfbox to generate a click track. All calfbox handling and midi generation are internally. The metronome has multiple components, each can be switched on and off on creation: -stereo audio out (stereo for cbox reasons) -midi out -midi in You can configure the midi notes representing a stressed and normal tick. There is no half-stressed signal. Stressing can be switched off. The metronome is a real midi track, not generated on the fly. Therefore it needs to be set with new data when music changes, which happens quite often. In general you want you metronome to be as long as your song. You can choose to loop the last measure, which makes simple "give me 4/4 at 120" possible. """ def __init__(self, parentData, normalMidiNote=77, stressedMidiNote=76, midiChannel=9): self.parentData = parentData #self.sequencerInterface = sequencer.SequencerInterface(parentTrack=self, name="metronome") self.sfzInstrumentSequencerInterface = sequencer.SfzInstrumentSequencerInterface(parentTrack=self, name="metronome", absoluteSfzPath=os.path.join(PATHS["templateShare"], "metronome", "metronome.sfz")) #testing: self.sfzInstrumentSequencerInterface = sequencer.SequencerInterface(parentTrack=self, name="metronome"); self.sfzInstrumentSequencerInterface.setEnabled(True) needs activating below!!! self._soundStresses = True #Change through soundStresses function self._normalTickMidiNote = normalMidiNote #no changing after instance got created self._stressedTickMidiNote = stressedMidiNote #no changing after instance got created self._midiChannel = midiChannel #GM drums #1-16 #no changing after instance got created self._cachedData = None #once we have a track it gets saved here so the midi output can be regenerated in place. self.label = "" #E.g. current Track Name, but can be anything. self.setEnabled(False) #TODO: save load def soundStresses(self, value:bool): self._soundStresses = value self.generate(self._cachedData) def generate(self, data, label:str): """Data is ordered: Iterable of (positionInTicks, isMetrical, treeOfMetricalInstructions) as tuple. Does not check if we truly need an update Label typically is the track name""" assert not data is None self.label = label self._cachedData = data result = [] for position, isMetrical, treeOfMetricalInstructions in data: isMetrical = False if not self._soundStresses else True blob, length = self.instructionToCboxMeasure(isMetrical, treeOfMetricalInstructions) result.append((blob, position, length)) self.sfzInstrumentSequencerInterface.setTrack(result) #we skip over instructions which have no proto-measure, metrical or not. This basically creates a zone without a metronome. #This might be difficult to use when thinking of Laborejo alone but in combination with a real time audio recording in another program this becomes very useful. #TODO: repeat last instruction as loop @cache_unlimited def instructionToCboxMeasure(self, isMetrical, metricalInstruction:tuple)->Tuple[bytes,int]: """Convert a metrical instruction to a metronome measure""" measureBlob = bytes() workingTicks = 0 ranOnce = False for duration in flatList(metricalInstruction): if ranOnce or not isMetrical: #normal tick. Always normal if this measure has no stressed positions, in other words it is not metrical measureBlob += cbox.Pattern.serialize_event(workingTicks, 0x90+9, 77, 127) #full velocity measureBlob += cbox.Pattern.serialize_event(workingTicks+D1024, 0x80+9, 77, 127) #note off is the shortest value possible in this program. else: #Measure "one" measureBlob += cbox.Pattern.serialize_event(workingTicks, 0x90+9, 76, 127) #different pitch measureBlob += cbox.Pattern.serialize_event(workingTicks+D1024, 0x80+9, 76, 127) ranOnce = True workingTicks += duration return (measureBlob, workingTicks) @property def enabled(self)->bool: return self.sfzInstrumentSequencerInterface.enabled def setEnabled(self, value:bool): self.sfzInstrumentSequencerInterface.enable(value) def export(self)->dict: return { # "sequencerInterface" : self.sequencerInterface.export(), "enabled" : self.enabled, "label" : self.label, }