@ -41,29 +41,29 @@ class Score(Data):
Order ist reflected in JACK through metadata . UIs should adopt it as well .
Score . TrackClass needs to be injected with your Track class .
Score . TrackClass needs to have a SequencerInterface of type SequencerInterface
self . tracks holds only active tracks . Which means tracks that produce sound
That does not mean that they are visible or editable for the user .
Does NOT hold deleted tracks in the undo storage . You need to hold these tracks in memory
yourself before calling score . delete . For example Laborejo registers the Track instance
That does not mean that they are visible or editable for the user .
Does NOT hold deleted tracks in the undo storage . You need to hold these tracks in memory
yourself before calling score . delete . For example Laborejo registers the Track instance
in our history module which keeps the instance alive .
Special Tracks do not need to be created here . E . g . a metronome can be just a track .
"""
TrackClass = None
def __init__ ( self , parentSession ) :
def __init__ ( self , parentSession ) :
assert Score . TrackClass
super ( ) . __init__ ( parentSession )
self . tracks = [ ] #see docstring
self . tempoMap = TempoMap ( parentData = self )
self . _template_processAfterInit ( )
self . tracks = [ ] #see docstring
self . tempoMap = TempoMap ( parentData = self )
self . _template_processAfterInit ( )
def _template_processAfterInit ( self ) : #needs a different name because there is an inherited class with the same method.
""" Call this after either init or instanceFromSerializedData """
def _template_processAfterInit ( self ) : #needs a different name because there is an inherited class with the same method.
""" Call this after either init or instanceFromSerializedData """
if METADATA [ " metronome " ] :
self . metronome = Metronome ( parentData = self ) #Purely dynamic structure. No save/load. No undo/redo
@ -76,24 +76,24 @@ class Score(Data):
We want the song to play only once .
The cbox way of doing that is to set the loop range to zero at the end of the track .
Zero length is stop .
"""
if startEndTuple is None :
"""
if startEndTuple is None :
longestTrackDuration = max ( track . sequencerInterface . cachedDuration for track in self . tracks )
start = longestTrackDuration
end = longestTrackDuration
else :
start , end = startEndTuple
start , end = startEndTuple
cbox . Document . get_song ( ) . set_loop ( start , end )
#Tracks
def addTrack ( self , name : str = " " ) :
def addTrack ( self , name : str = " " ) :
track = Score . TrackClass ( parentData = self , name = name )
assert track . sequencerInterface
self . tracks . append ( track )
self . tracks . append ( track )
return track
def deleteTrack ( self , track ) :
def deleteTrack ( self , track ) :
track . sequencerInterface . prepareForDeletion ( )
self . tracks . remove ( track )
@ -107,12 +107,12 @@ class Score(Data):
sent to the UI .
"""
order = { portName : index for index , portName in enumerate ( track . sequencerInterface . cboxPortName ( ) for track in self . tracks ) }
try :
cbox . JackIO . Metadata . set_all_port_order ( order )
except Exception as e : #No Jack Meta Data
logger . error ( e )
def trackById ( self , trackId : int ) :
for track in self . tracks :
if trackId == id ( track ) :
@ -156,7 +156,7 @@ class Score(Data):
childObject . tracks = loadedTracks
childObject . tempoMap = TempoMap . instanceFromSerializedData ( parentData = childObject , serializedData = serializedData [ " tempoMap " ] )
childObject . _template_processAfterInit ( )
childObject . _template_processAfterInit ( )
def export ( self ) - > dict :
return {
@ -165,32 +165,32 @@ class Score(Data):
}
class _Interface ( object ) :
class _Interface ( object ) :
#no load or save. Do that in the child classes.
def __init__ ( self , parentTrack , name = None ) :
def __init__ ( self , parentTrack , name = None ) :
self . parentTrack = parentTrack
self . parentData = parentTrack . parentData
self . _name = self . _isNameAvailable ( name ) if name else str ( id ( self ) )
self . _enabled = True
self . _enabled = True
self . _processAfterInit ( )
def _processAfterInit ( self ) :
self . _cachedPatterns = [ ] #makes undo after delete possible
self . calfboxTrack = cbox . Document . get_song ( ) . add_track ( )
self . calfboxTrack . set_name ( self . name ) #only cosmetic and cbox internal. Useful for debugging, not used in jack.
self . calfboxTrack . set_name ( self . name ) #only cosmetic and cbox internal. Useful for debugging, not used in jack.
#Caches and other Non-Saved attributes
self . cachedDuration = 0 #used by parentData.buildSongDuration to calculate the overall length of the song by checking all tracks.
self . cachedDuration = 0 #used by parentData.buildSongDuration to calculate the overall length of the song by checking all tracks.
@property
def name ( self ) :
return self . _name
@property
def enabled ( self ) - > bool :
def enabled ( self ) - > bool :
return self . _enabled
def _isNameAvailable ( self , name : str ) :
@ -212,60 +212,60 @@ class _Interface(object):
def setTrack ( self , blobs : Iterable ) : #(bytes-blob, position, length)
""" Converts an Iterable of (bytes-blob, position, length) to cbox patterns, clips and adds
them to an empty track , which replaces the current one .
Simplest version is to send one blob at position 0 with its length . """
#self.calfboxTrack.delete() #cbox clear data, not python structure
Simplest version is to send one blob at position 0 with its length . """
#self.calfboxTrack.delete() #cbox clear data, not python structure
#self.calfboxTrack = cbox.Document.get_song().add_track()
#self.calfboxTrack.set_external_output(self.cboxMidiOutUuid)
#self.calfboxTrack.set_external_output(self.cboxMidiOutUuid)
self . calfboxTrack . clear_clips ( )
self . _cachedPatterns = [ ] #makes undo after delete possible
pos = 0
for blob , pos , leng in blobs :
for blob , pos , leng in blobs :
if leng > 0 :
pat = cbox . Document . get_song ( ) . pattern_from_blob ( blob , leng )
t = ( pat , pos , leng )
t = ( pat , pos , leng )
self . _cachedPatterns . append ( t )
length = 0
for pattern , position , length in self . _cachedPatterns :
for pattern , position , length in self . _cachedPatterns :
if length > 0 :
self . calfboxTrack . add_clip ( position , 0 , length , pattern ) #pos, offset, length, pattern.
self . cachedDuration = pos + length #use the last set values
self . cachedDuration = pos + length #use the last set values
self . _updatePlayback ( )
def insertEmptyClip ( self ) :
""" Convenience function to make recording into an empty song possible.
Will be removed by self . setTrack . """
Will be removed by self . setTrack . """
blob = bytes ( )
#We do not need any content #blob += cbox.Pattern.serialize_event(0, 0x80, 60, 64) # note off
pattern = cbox . Document . get_song ( ) . pattern_from_blob ( blob , MAXIMUM_TICK_DURATION ) #blog, length
self . calfboxTrack . add_clip ( 0 , 0 , MAXIMUM_TICK_DURATION , pattern ) #pos, offset, length, pattern.
self . calfboxTrack . add_clip ( 0 , 0 , MAXIMUM_TICK_DURATION , pattern ) #pos, offset, length, pattern.
self . cachedDuration = MAXIMUM_TICK_DURATION
self . _updatePlayback ( )
class _Subtrack ( object ) :
""" Generates its own midi data and caches the resulting track but does not have a name
nor its own jack midi port . Instead it is attached to an SequencerInterface .
It is SequencerInterface because that has a jack midi port , and not Interface itself .
Only used by SequencerInterface internally . Creation is done by its methods .
Only used by SequencerInterface internally . Creation is done by its methods .
This is not a child class because a top level class ' code is easier to read.
Intended usecase is to add CC messages as one subtrack per CC number ( e . g . CC7 = Volume ) .
Of course you could just put CCs together with notes in the main Interface
and not use SubTracks . But where is the fun in that ? """
def __init__ ( self , parentSequencerInterface ) :
self . _cachedPatterns = [ ] #makes undo after delete possible
self . parentSequencerInterface = parentSequencerInterface
self . calfboxSubTrack = cbox . Document . get_song ( ) . add_track ( )
self . calfboxSubTrack . set_external_output ( parentSequencerInterface . cboxMidiOutUuid )
self . calfboxSubTrack = cbox . Document . get_song ( ) . add_track ( )
self . calfboxSubTrack . set_external_output ( parentSequencerInterface . cboxMidiOutUuid )
def prepareForDeletion ( self ) :
self . calfboxSubTrack . delete ( ) #in place self deletion.
self . calfboxSubTrack = None
@ -274,31 +274,31 @@ class _Subtrack(object):
def recreateThroughUndo ( self ) :
assert self . calfboxSubTrack is None , self . calfboxSubTrack
self . calfboxSubTrack = cbox . Document . get_song ( ) . add_track ( )
for pattern , position , length in self . _cachedPatterns :
for pattern , position , length in self . _cachedPatterns :
self . calfboxSubTrack . add_clip ( position , 0 , length , pattern ) #pos, offset, length, pattern.
cbox . Document . get_song ( ) . update_playback ( )
def setSubtrack ( self , blobs : Iterable ) : #(bytes-blob, position, length)
""" Does not add to the parents cached duration. Therefore it will not send data beyond
its parent track length , except if another track pushes the overall duration beyond . """
self . calfboxSubTrack . clear_clips ( )
self . _cachedPatterns = [ ] #makes undo after delete possible
pos = 0
for blob , pos , leng in blobs :
pos = 0
for blob , pos , leng in blobs :
if leng > 0 :
pat = cbox . Document . get_song ( ) . pattern_from_blob ( blob , leng )
t = ( pat , pos , leng )
t = ( pat , pos , leng )
self . _cachedPatterns . append ( t )
length = 0
for pattern , position , length in self . _cachedPatterns :
for pattern , position , length in self . _cachedPatterns :
if length > 0 :
self . calfboxSubTrack . add_clip ( position , 0 , length , pattern ) #pos, offset, length, pattern.
cbox . Document . get_song ( ) . update_playback ( )
class SequencerInterface ( _Interface ) : #Basically the midi part of a track.
class SequencerInterface ( _Interface ) : #Basically the midi part of a track.
""" A tracks name is the same as the jack midi-out ports name.
The main purpose of the child class is to manage its musical data and regulary
@ -316,31 +316,31 @@ class SequencerInterface(_Interface): #Basically the midi part of a track.
self . cachedDuration = the maximum track length . Used to determine the song playback duration .
"""
def _processAfterInit ( self ) :
#Create midi out and cbox track
logger . info ( " Creating empty SequencerInterface instance " )
super ( ) . _processAfterInit ( )
self . cboxMidiOutUuid = cbox . JackIO . create_midi_output ( self . _name )
self . calfboxTrack . set_external_output ( self . cboxMidiOutUuid )
cbox . JackIO . rename_midi_output ( self . cboxMidiOutUuid , self . _name )
self . _subtracks = { } #arbitrary key: _Subtrack(). This is not in Interface itself because Subtracks assume a jack midi out.
self . cboxMidiOutUuid = cbox . JackIO . create_midi_output ( self . _name )
self . calfboxTrack . set_external_output ( self . cboxMidiOutUuid )
cbox . JackIO . rename_midi_output ( self . cboxMidiOutUuid , self . _name )
self . _subtracks = { } #arbitrary key: _Subtrack(). This is not in Interface itself because Subtracks assume a jack midi out.
self . enable ( self . _enabled )
def enable ( self , enabled ) :
def enable ( self , enabled ) :
""" This is " mute " , more or less. It only disables the note parts, not CCs or other subtracks.
This means if you switch this on again during playback you will have the correct context . """
This means if you switch this on again during playback you will have the correct context . """
if enabled :
#self.calfboxTrack.set_external_output(self.cboxMidiOutUuid) #Old version. Does not prevent hanging notes.
self . calfboxTrack . set_mute ( 0 )
else :
#self.calfboxTrack.set_external_output("") #Old version. Does not prevent hanging notes.
self . calfboxTrack . set_mute ( 1 )
self . _enabled = bool ( enabled )
cbox . Document . get_song ( ) . update_playback ( )
self . _enabled = bool ( enabled )
cbox . Document . get_song ( ) . update_playback ( )
@_Interface . name . setter
def name ( self , value ) :
@ -350,19 +350,19 @@ class SequencerInterface(_Interface): #Basically the midi part of a track.
def cboxPortName ( self ) - > str :
""" Return the complete jack portname: OurName:PortName """
portname = cbox . JackIO . status ( ) . client_name + " : " + self . name
portname = cbox . JackIO . status ( ) . client_name + " : " + self . name
return portname
def prepareForDeletion ( self ) :
""" Called by score right before this track gets deleted.
This does not mean the track is gone . It can be recovered by
undo . That is why we bother setting calfboxTrack to None
again .
"""
"""
portlist = cbox . JackIO . get_connected_ports ( self . cboxPortName ( ) )
self . _beforeDeleteThisJackMidiWasConnectedTo = portlist
self . _beforeDeleteThisJackMidiWasConnectedTo = portlist
self . calfboxTrack . set_external_output ( " " )
cbox . JackIO . delete_midi_output ( self . cboxMidiOutUuid )
self . calfboxTrack . delete ( ) #in place self deletion.
@ -370,7 +370,7 @@ class SequencerInterface(_Interface): #Basically the midi part of a track.
self . cboxMidiOutUuid = None
#we leave cachedDuration untouched
self . _updatePlayback ( )
def recreateThroughUndo ( self ) :
""" Brings this track back from the dead, in-place.
Assumes this track instance was not in the score but
@ -381,31 +381,34 @@ class SequencerInterface(_Interface): #Basically the midi part of a track.
assert self . calfboxTrack is None , self . calfboxTrack
self . calfboxTrack = cbox . Document . get_song ( ) . add_track ( )
self . calfboxTrack . set_name ( self . name ) #only cosmetic and cbox internal. Useful for debugging, not used in jack.
for pattern , position , length in self . _cachedPatterns :
for pattern , position , length in self . _cachedPatterns :
self . calfboxTrack . add_clip ( position , 0 , length , pattern ) #pos, offset, length, pattern.
#self.cachedDuration is still valid
#Create MIDI and reconnect Jack
self . cboxMidiOutUuid = cbox . JackIO . create_midi_output ( self . name )
cbox . JackIO . rename_midi_output ( self . cboxMidiOutUuid , self . _name )
self . calfboxTrack . set_external_output ( self . cboxMidiOutUuid )
for port in self . _beforeDeleteThisJackMidiWasConnectedTo :
cbox . JackIO . port_connect ( self . cboxPortName ( ) , port )
try :
cbox . JackIO . port_connect ( self . cboxPortName ( ) , port )
except : #external connected synth is maybe gone. Prevent crash.
logger . warning ( f " Previously external connection { port } is gone. Can ' t connect anymore. " )
#Make it official
self . _updatePlayback ( )
self . _updatePlayback ( )
def setSubtrack ( self , key , blobs : Iterable ) : #(bytes-blob, position, length)
""" Creates a new subtrack if key is unknown
Forward data to the real function
Simplest version is to send one blob at position 0 with its length """
if not key in self . _subtracks :
if not key in self . _subtracks :
self . _subtracks [ key ] = _Subtrack ( parentSequencerInterface = self )
assert self . _subtracks [ key ] , key
assert isinstance ( self . _subtracks [ key ] , _Subtrack ) , type ( self . _subtracks [ key ] )
self . _subtracks [ key ] . setSubtrack ( blobs )
#Save / Load / Export
@ -413,15 +416,15 @@ class SequencerInterface(_Interface): #Basically the midi part of a track.
""" Generate Data to save as json """
return {
" name " : self . name ,
" enabled " : self . _enabled ,
" enabled " : self . _enabled ,
}
@classmethod
def instanceFromSerializedData ( cls , parentTrack , serializedData ) :
self = cls . __new__ ( cls )
self . _name = serializedData [ " name " ]
self . _enabled = serializedData [ " enabled " ]
self . _enabled = serializedData [ " enabled " ]
self . parentTrack = parentTrack
self . parentData = parentTrack . parentData
self . _processAfterInit ( )
@ -438,55 +441,55 @@ class SequencerInterface(_Interface): #Basically the midi part of a track.
}
class SfzInstrumentSequencerInterface ( _Interface ) :
class SfzInstrumentSequencerInterface ( _Interface ) :
""" Like a midi output, only routes to an internal instrument.
This is not a pure sfz sampler , but rather a track that ends in an instrument instead of a
jack midi output . """
This is not a pure sfz sampler , but rather a track that ends in an instrument instead of a
jack midi output . """
def __init__ ( self , parentTrack , name : str , absoluteSfzPath : str ) :
super ( ) . __init__ ( parentTrack , name ) #includes processAfterInit
super ( ) . __init__ ( parentTrack , name ) #includes processAfterInit
self . scene = cbox . Document . get_engine ( ) . new_scene ( )
self . scene . clear ( )
self . scene . add_new_instrument_layer ( name , " sampler " ) #"sampler" is the cbox sfz engine
self . scene . status ( ) . layers [ 0 ] . get_instrument ( ) . engine . load_patch_from_string ( 0 , " " , " " , " " ) #fill with null instruments
self . scene . status ( ) . layers [ 0 ] . get_instrument ( ) . engine . load_patch_from_string ( 0 , " " , " " , " " ) #fill with null instruments
self . scene . set_enable_default_song_input ( True )
self . instrumentLayer = self . scene . status ( ) . layers [ 0 ] . get_instrument ( )
self . scene . status ( ) . layers [ 0 ] . set_ignore_program_changes ( 1 ) #TODO: ignore different channels. We only want one channel per scene/instrument/port.
newProgramNumber = 1
self . scene . status ( ) . layers [ 0 ] . set_ignore_program_changes ( 1 ) #TODO: ignore different channels. We only want one channel per scene/instrument/port.
newProgramNumber = 1
program = self . instrumentLayer . engine . load_patch_from_file ( newProgramNumber , absoluteSfzPath , name )
self . instrumentLayer . engine . set_patch ( 10 , newProgramNumber ) #from 1. 10 is the channel #TODO: we want this to be on all channels.
#TODO: Metronome is not compatible with current cbox. we need to route midi data from our cbox track explicitely to self.scene, which is not possible right now.
#TODO: Metronome is not compatible with current cbox. we need to route midi data from our cbox track explicitely to self.scene, which is not possible right now.
self . calfboxTrack . set_external_output ( " " )
#Metadata
portnameL = f " { cbox . JackIO . status ( ) . client_name } :out_1 "
portnameR = f " { cbox . JackIO . status ( ) . client_name } :out_2 "
portnameR = f " { cbox . JackIO . status ( ) . client_name } :out_2 "
cbox . JackIO . Metadata . set_pretty_name ( portnameL , name . title ( ) + " -L " )
cbox . JackIO . Metadata . set_pretty_name ( portnameR , name . title ( ) + " -R " )
def enable ( self , enabled ) :
if enabled :
def enable ( self , enabled ) :
if enabled :
self . scene . status ( ) . layers [ 0 ] . set_enable ( True )
else :
else :
self . scene . status ( ) . layers [ 0 ] . set_enable ( False )
self . _enabled = bool ( enabled ) #this is redundant in the SfzInstrument, but the normal midi outs need this. So we stick to the convention.
cbox . Document . get_song ( ) . update_playback ( )
cbox . Document . get_song ( ) . update_playback ( )
@property
def enabled ( self ) - > bool :
def enabled ( self ) - > bool :
return self . _enabled
class TempoMap ( object ) :
"""
This is a singleton instance in Score . Don ' t subclass.
Main data structure is self . _tempoMap = { positionInTicks : ( bpmAsFloat , timesigUpper , timesigLower ) }
The tempo map is only active if the whole program is JACK Transport Master ( via Cbox ) .
If not we simply follow jack sync .
@ -510,41 +513,41 @@ class TempoMap(object):
If you want to incrementally change the tempo map , which is really not necessary because
changing it completely is a very cheap operation , you can edit the dict _tempoMap directly .
In case you have a complex tempo management yourself , like Laborejo , use it to setTempoMap and
then don ' t worry about save and load. Treat it as a cache that conveniently restores the last
setting after program startup . """
def __init__ ( self , parentData ) :
logger . info ( " Creating empty TempoMap instance " )
self . parentData = parentData
self . _tempoMap = { 0 : ( 120.0 , 4 , 4 ) } # 4/4, 120bpm. will not be used on startup, but is needed if transportMaster is switched on
self . _isTransportMaster = False
self . parentData = parentData
self . _tempoMap = { 0 : ( 120.0 , 4 , 4 ) } # 4/4, 120bpm. will not be used on startup, but is needed if transportMaster is switched on
self . _isTransportMaster = False
self . _processAfterInit ( )
assert not cbox . Document . get_song ( ) . status ( ) . mtis
assert not cbox . Document . get_song ( ) . status ( ) . mtis
def _processAfterInit ( self ) :
self . factor = 1.0 # not saved
self . isTransportMaster = self . _isTransportMaster #already triggers cbox settings through @setter.
self . isTransportMaster = self . _isTransportMaster #already triggers cbox settings through @setter.
self . _sanitize ( )
def _updatePlayback ( self ) :
""" A wrapper that not only calls update playback but forces JACK to call its BBT callback,
so it gets the new tempo info even without transport running .
That is a bit of a hack , but it works without disturbing anything too much . """
That is a bit of a hack , but it works without disturbing anything too much . """
cbox . Document . get_song ( ) . update_playback ( )
pos = cbox . Transport . status ( ) . pos #can be None on program start
if self . isTransportMaster and not cbox . Transport . status ( ) . playing : #pos can be 0
pos = cbox . Transport . status ( ) . pos #can be None on program start
if self . isTransportMaster and not cbox . Transport . status ( ) . playing : #pos can be 0
if pos is None :
#Yes, we destroy the current playback position. But we ARE timebase master, so that is fine.
cbox . Transport . seek_samples ( 0 )
else : #default case
cbox . Transport . seek_samples ( 0 )
else : #default case
cbox . Transport . seek_samples ( pos )
@property
def isTransportMaster ( self ) - > bool :
return self . _isTransportMaster
@ -555,74 +558,74 @@ class TempoMap(object):
self . _isTransportMaster = value
if value :
self . _sendToCbox ( ) #reactivate existing tempo map
cbox . JackIO . external_tempo ( False )
cbox . JackIO . transport_mode ( master = True , conditional = False ) #conditional = only attempt to become a master (will fail if there is one already)
else :
cbox . JackIO . external_tempo ( False )
cbox . JackIO . transport_mode ( master = True , conditional = False ) #conditional = only attempt to become a master (will fail if there is one already)
else :
self . _clearCboxTempoMap ( ) #clear cbox map but don't touch our own data.
cbox . JackIO . external_tempo ( True )
cbox . JackIO . external_tempo ( True )
try :
cbox . JackIO . transport_mode ( master = False )
except Exception : #"Not a current timebase master"
pass
self . _updatePlayback ( )
def _sanitize ( self ) :
""" Inplace modification of self.tempoMap. Remove zeros and convert to float values. """
self . _tempoMap = { int ( key ) : ( float ( value ) , timesigNum , timesigDenom ) for key , ( value , timesigNum , timesigDenom ) in self . _tempoMap . items ( ) if value > 0.0 }
#Don't use the following. Empty tempo maps are allowed, especially in jack transport slave mode. #Instead set a default tempo 120 on init explicitly
#if not self._tempoMap:
#if not self._tempoMap:
#logger.warning("Found invalid tempo map. Forcing to 120 bpm. Please correct manually")
#self._tempoMap = {0, 120.0}
def _clearCboxTempoMap ( self ) :
""" Remove all cbox tempo values by iterating over all of them and set them to None, which is
the secret cbox handshake to delete a tempo change on a specific position .
Keep our own local data intact . """
song = cbox . Document . get_song ( )
song = cbox . Document . get_song ( )
for mti in song . status ( ) . mtis : #Creates a new temporary list with newly created objects, safe to iterate and delete.
song . delete_mti ( mti . pos )
self . _updatePlayback ( )
assert not song . status ( ) . mtis , song . status ( ) . mtis
def _sendToCbox ( self ) :
""" Send to cbox """
""" Send to cbox """
assert self . isTransportMaster
assert self . _tempoMap
song = cbox . Document . get_song ( )
for pos , ( value , timesigNum , timesigDenom ) in self . _tempoMap . items ( ) :
song . set_mti ( pos = pos , tempo = value * self . factor , timesig_denom = timesigDenom , timesig_num = timesigNum ) #Tempo changes are fine to happen on the same tick as note on.
#song.set_mti(pos=pos, tempo=value * self.factor) #Tempo changes are fine to happen on the same tick as note on.
song . set_mti ( pos = pos , tempo = value * self . factor , timesig_denom = timesigDenom , timesig_num = timesigNum ) #Tempo changes are fine to happen on the same tick as note on.
#song.set_mti(pos=pos, tempo=value * self.factor) #Tempo changes are fine to happen on the same tick as note on.
self . _updatePlayback ( )
def setTempoMap ( self , tempoMap : dict ) :
""" All-in-one function for outside access """
if self . _tempoMap != tempoMap :
self . _tempoMap = tempoMap
""" All-in-one function for outside access """
if self . _tempoMap != tempoMap :
self . _tempoMap = tempoMap
self . _sanitize ( )
if self . isTransportMaster : #if not the data will be used later.
self . _clearCboxTempoMap ( ) #keeps our own data so it can be send again.
self . _sendToCbox ( )
self . _clearCboxTempoMap ( ) #keeps our own data so it can be send again.
self . _sendToCbox ( )
def setFactor ( self , factor : float ) :
""" Factor is from 1, not from the current one. """
""" Factor is from 1, not from the current one. """
self . factor = round ( factor , 4 )
self . _sanitize ( )
self . _clearCboxTempoMap ( ) #keeps our own data so it can be send again.
self . _clearCboxTempoMap ( ) #keeps our own data so it can be send again.
self . _sendToCbox ( ) #uses the factor
def setQuarterNotesPerMinute ( self , quarterNotesPerMinute : float ) :
""" Simple tempo setter. Overrides all other tempo data.
Works in tandem with self . setTimeSignature """
Works in tandem with self . setTimeSignature """
currentValue , timesigNum , timesigDenom = self . _tempoMap [ 0 ]
self . setTempoMap ( { 0 : ( quarterNotesPerMinute , timesigNum , timesigDenom ) } )
def setTimeSignature ( self , timesigNum : int , timesigDenom : int ) :
""" Simple traditional timesig setter. Overrides all other timesig data.
Works in tandem with self . setTimeSignature .
"""
"""
assert timesigNum > 0 , timesigNum
#assert timesigDenom in traditionalNumberToBaseDuration, (timesigDenom, traditionalNumberToBaseDuration) For Laborejo this makes sense, but Patroneo has a fallback option with irregular timesigs: 8 steps in groups of 3 to make a quarter is valid. Results in "8/12"
currentValue , OLD_timesigNum , OLD_timesigDenom = self . _tempoMap [ 0 ]