Browse Source

prevent delete-track-undo from crashing if previously connected jack connection quit in the meantime

master
Nils 4 years ago
parent
commit
3d0308feee
  1. 317
      template/engine/sequencer.py

317
template/engine/sequencer.py

@ -41,29 +41,29 @@ class Score(Data):
Order ist reflected in JACK through metadata. UIs should adopt it as well. 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 be injected with your Track class.
Score.TrackClass needs to have a SequencerInterface of type SequencerInterface Score.TrackClass needs to have a SequencerInterface of type SequencerInterface
self.tracks holds only active tracks. Which means tracks that produce sound 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. 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 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 yourself before calling score.delete. For example Laborejo registers the Track instance
in our history module which keeps the instance alive. 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. Special Tracks do not need to be created here. E.g. a metronome can be just a track.
""" """
TrackClass = None TrackClass = None
def __init__(self, parentSession): def __init__(self, parentSession):
assert Score.TrackClass assert Score.TrackClass
super().__init__(parentSession) super().__init__(parentSession)
self.tracks = [] #see docstring self.tracks = [] #see docstring
self.tempoMap = TempoMap(parentData = self) self.tempoMap = TempoMap(parentData = self)
self._template_processAfterInit() self._template_processAfterInit()
def _template_processAfterInit(self): #needs a different name because there is an inherited class with the same method. 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""" """Call this after either init or instanceFromSerializedData"""
if METADATA["metronome"]: if METADATA["metronome"]:
self.metronome = Metronome(parentData=self) #Purely dynamic structure. No save/load. No undo/redo 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. 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. The cbox way of doing that is to set the loop range to zero at the end of the track.
Zero length is stop. Zero length is stop.
""" """
if startEndTuple is None: if startEndTuple is None:
longestTrackDuration = max(track.sequencerInterface.cachedDuration for track in self.tracks) longestTrackDuration = max(track.sequencerInterface.cachedDuration for track in self.tracks)
start = longestTrackDuration start = longestTrackDuration
end = longestTrackDuration end = longestTrackDuration
else: else:
start, end = startEndTuple start, end = startEndTuple
cbox.Document.get_song().set_loop(start, end) cbox.Document.get_song().set_loop(start, end)
#Tracks #Tracks
def addTrack(self, name:str=""): def addTrack(self, name:str=""):
track = Score.TrackClass(parentData=self, name=name) track = Score.TrackClass(parentData=self, name=name)
assert track.sequencerInterface assert track.sequencerInterface
self.tracks.append(track) self.tracks.append(track)
return track return track
def deleteTrack(self, track): def deleteTrack(self, track):
track.sequencerInterface.prepareForDeletion() track.sequencerInterface.prepareForDeletion()
self.tracks.remove(track) self.tracks.remove(track)
@ -107,12 +107,12 @@ class Score(Data):
sent to the UI. sent to the UI.
""" """
order = {portName:index for index, portName in enumerate(track.sequencerInterface.cboxPortName() for track in self.tracks)} order = {portName:index for index, portName in enumerate(track.sequencerInterface.cboxPortName() for track in self.tracks)}
try: try:
cbox.JackIO.Metadata.set_all_port_order(order) cbox.JackIO.Metadata.set_all_port_order(order)
except Exception as e: #No Jack Meta Data except Exception as e: #No Jack Meta Data
logger.error(e) logger.error(e)
def trackById(self, trackId:int): def trackById(self, trackId:int):
for track in self.tracks: for track in self.tracks:
if trackId == id(track): if trackId == id(track):
@ -156,7 +156,7 @@ class Score(Data):
childObject.tracks=loadedTracks childObject.tracks=loadedTracks
childObject.tempoMap=TempoMap.instanceFromSerializedData(parentData=childObject, serializedData=serializedData["tempoMap"]) childObject.tempoMap=TempoMap.instanceFromSerializedData(parentData=childObject, serializedData=serializedData["tempoMap"])
childObject._template_processAfterInit() childObject._template_processAfterInit()
def export(self)->dict: def export(self)->dict:
return { return {
@ -165,32 +165,32 @@ class Score(Data):
} }
class _Interface(object): class _Interface(object):
#no load or save. Do that in the child classes. #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.parentTrack = parentTrack
self.parentData = parentTrack.parentData self.parentData = parentTrack.parentData
self._name = self._isNameAvailable(name) if name else str(id(self)) self._name = self._isNameAvailable(name) if name else str(id(self))
self._enabled = True self._enabled = True
self._processAfterInit() self._processAfterInit()
def _processAfterInit(self): def _processAfterInit(self):
self._cachedPatterns = [] #makes undo after delete possible self._cachedPatterns = [] #makes undo after delete possible
self.calfboxTrack = cbox.Document.get_song().add_track() 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 #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 @property
def name(self): def name(self):
return self._name return self._name
@property @property
def enabled(self)->bool: def enabled(self)->bool:
return self._enabled return self._enabled
def _isNameAvailable(self, name:str): def _isNameAvailable(self, name:str):
@ -212,60 +212,60 @@ class _Interface(object):
def setTrack(self, blobs:Iterable): #(bytes-blob, position, length) def setTrack(self, blobs:Iterable): #(bytes-blob, position, length)
"""Converts an Iterable of (bytes-blob, position, length) to cbox patterns, clips and adds """Converts an Iterable of (bytes-blob, position, length) to cbox patterns, clips and adds
them to an empty track, which replaces the current one. them to an empty track, which replaces the current one.
Simplest version is to send one blob at position 0 with its length.""" Simplest version is to send one blob at position 0 with its length."""
#self.calfboxTrack.delete() #cbox clear data, not python structure #self.calfboxTrack.delete() #cbox clear data, not python structure
#self.calfboxTrack = cbox.Document.get_song().add_track() #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.calfboxTrack.clear_clips()
self._cachedPatterns = [] #makes undo after delete possible self._cachedPatterns = [] #makes undo after delete possible
pos = 0 pos = 0
for blob, pos, leng in blobs: for blob, pos, leng in blobs:
if leng > 0: if leng > 0:
pat = cbox.Document.get_song().pattern_from_blob(blob, leng) pat = cbox.Document.get_song().pattern_from_blob(blob, leng)
t = (pat, pos, leng) t = (pat, pos, leng)
self._cachedPatterns.append(t) self._cachedPatterns.append(t)
length = 0 length = 0
for pattern, position, length in self._cachedPatterns: for pattern, position, length in self._cachedPatterns:
if length > 0: if length > 0:
self.calfboxTrack.add_clip(position, 0, length, pattern) #pos, offset, length, pattern. 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() self._updatePlayback()
def insertEmptyClip(self): def insertEmptyClip(self):
"""Convenience function to make recording into an empty song possible. """Convenience function to make recording into an empty song possible.
Will be removed by self.setTrack.""" Will be removed by self.setTrack."""
blob = bytes() blob = bytes()
#We do not need any content #blob += cbox.Pattern.serialize_event(0, 0x80, 60, 64) # note off #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 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.cachedDuration = MAXIMUM_TICK_DURATION
self._updatePlayback() self._updatePlayback()
class _Subtrack(object): class _Subtrack(object):
"""Generates its own midi data and caches the resulting track but does not have a name """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. 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. 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. 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). 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 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?""" and not use SubTracks. But where is the fun in that?"""
def __init__(self, parentSequencerInterface): def __init__(self, parentSequencerInterface):
self._cachedPatterns = [] #makes undo after delete possible self._cachedPatterns = [] #makes undo after delete possible
self.parentSequencerInterface = parentSequencerInterface self.parentSequencerInterface = parentSequencerInterface
self.calfboxSubTrack = cbox.Document.get_song().add_track() self.calfboxSubTrack = cbox.Document.get_song().add_track()
self.calfboxSubTrack.set_external_output(parentSequencerInterface.cboxMidiOutUuid) self.calfboxSubTrack.set_external_output(parentSequencerInterface.cboxMidiOutUuid)
def prepareForDeletion(self): def prepareForDeletion(self):
self.calfboxSubTrack.delete() #in place self deletion. self.calfboxSubTrack.delete() #in place self deletion.
self.calfboxSubTrack = None self.calfboxSubTrack = None
@ -274,31 +274,31 @@ class _Subtrack(object):
def recreateThroughUndo(self): def recreateThroughUndo(self):
assert self.calfboxSubTrack is None, self.calfboxSubTrack assert self.calfboxSubTrack is None, self.calfboxSubTrack
self.calfboxSubTrack = cbox.Document.get_song().add_track() 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. self.calfboxSubTrack.add_clip(position, 0, length, pattern) #pos, offset, length, pattern.
cbox.Document.get_song().update_playback() cbox.Document.get_song().update_playback()
def setSubtrack(self, blobs:Iterable): #(bytes-blob, position, length) def setSubtrack(self, blobs:Iterable): #(bytes-blob, position, length)
"""Does not add to the parents cached duration. Therefore it will not send data beyond """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.""" its parent track length, except if another track pushes the overall duration beyond."""
self.calfboxSubTrack.clear_clips() self.calfboxSubTrack.clear_clips()
self._cachedPatterns = [] #makes undo after delete possible self._cachedPatterns = [] #makes undo after delete possible
pos = 0 pos = 0
for blob, pos, leng in blobs: for blob, pos, leng in blobs:
if leng > 0: if leng > 0:
pat = cbox.Document.get_song().pattern_from_blob(blob, leng) pat = cbox.Document.get_song().pattern_from_blob(blob, leng)
t = (pat, pos, leng) t = (pat, pos, leng)
self._cachedPatterns.append(t) self._cachedPatterns.append(t)
length = 0 length = 0
for pattern, position, length in self._cachedPatterns: for pattern, position, length in self._cachedPatterns:
if length > 0: if length > 0:
self.calfboxSubTrack.add_clip(position, 0, length, pattern) #pos, offset, length, pattern. self.calfboxSubTrack.add_clip(position, 0, length, pattern) #pos, offset, length, pattern.
cbox.Document.get_song().update_playback() 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. """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 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. self.cachedDuration = the maximum track length. Used to determine the song playback duration.
""" """
def _processAfterInit(self): def _processAfterInit(self):
#Create midi out and cbox track #Create midi out and cbox track
logger.info("Creating empty SequencerInterface instance") logger.info("Creating empty SequencerInterface instance")
super()._processAfterInit() super()._processAfterInit()
self.cboxMidiOutUuid = cbox.JackIO.create_midi_output(self._name) self.cboxMidiOutUuid = cbox.JackIO.create_midi_output(self._name)
self.calfboxTrack.set_external_output(self.cboxMidiOutUuid) self.calfboxTrack.set_external_output(self.cboxMidiOutUuid)
cbox.JackIO.rename_midi_output(self.cboxMidiOutUuid, self._name) 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._subtracks = {} #arbitrary key: _Subtrack(). This is not in Interface itself because Subtracks assume a jack midi out.
self.enable(self._enabled) 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 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: if enabled:
#self.calfboxTrack.set_external_output(self.cboxMidiOutUuid) #Old version. Does not prevent hanging notes. #self.calfboxTrack.set_external_output(self.cboxMidiOutUuid) #Old version. Does not prevent hanging notes.
self.calfboxTrack.set_mute(0) self.calfboxTrack.set_mute(0)
else: else:
#self.calfboxTrack.set_external_output("") #Old version. Does not prevent hanging notes. #self.calfboxTrack.set_external_output("") #Old version. Does not prevent hanging notes.
self.calfboxTrack.set_mute(1) self.calfboxTrack.set_mute(1)
self._enabled = bool(enabled) self._enabled = bool(enabled)
cbox.Document.get_song().update_playback() cbox.Document.get_song().update_playback()
@_Interface.name.setter @_Interface.name.setter
def name(self, value): def name(self, value):
@ -350,19 +350,19 @@ class SequencerInterface(_Interface): #Basically the midi part of a track.
def cboxPortName(self)->str: def cboxPortName(self)->str:
"""Return the complete jack portname: OurName:PortName""" """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 return portname
def prepareForDeletion(self): def prepareForDeletion(self):
"""Called by score right before this track gets deleted. """Called by score right before this track gets deleted.
This does not mean the track is gone. It can be recovered by This does not mean the track is gone. It can be recovered by
undo. That is why we bother setting calfboxTrack to None undo. That is why we bother setting calfboxTrack to None
again. again.
""" """
portlist = cbox.JackIO.get_connected_ports(self.cboxPortName()) portlist = cbox.JackIO.get_connected_ports(self.cboxPortName())
self._beforeDeleteThisJackMidiWasConnectedTo = portlist self._beforeDeleteThisJackMidiWasConnectedTo = portlist
self.calfboxTrack.set_external_output("") self.calfboxTrack.set_external_output("")
cbox.JackIO.delete_midi_output(self.cboxMidiOutUuid) cbox.JackIO.delete_midi_output(self.cboxMidiOutUuid)
self.calfboxTrack.delete() #in place self deletion. self.calfboxTrack.delete() #in place self deletion.
@ -370,7 +370,7 @@ class SequencerInterface(_Interface): #Basically the midi part of a track.
self.cboxMidiOutUuid = None self.cboxMidiOutUuid = None
#we leave cachedDuration untouched #we leave cachedDuration untouched
self._updatePlayback() self._updatePlayback()
def recreateThroughUndo(self): def recreateThroughUndo(self):
"""Brings this track back from the dead, in-place. """Brings this track back from the dead, in-place.
Assumes this track instance was not in the score but 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 assert self.calfboxTrack is None, self.calfboxTrack
self.calfboxTrack = cbox.Document.get_song().add_track() 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.
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.calfboxTrack.add_clip(position, 0, length, pattern) #pos, offset, length, pattern.
#self.cachedDuration is still valid #self.cachedDuration is still valid
#Create MIDI and reconnect Jack #Create MIDI and reconnect Jack
self.cboxMidiOutUuid = cbox.JackIO.create_midi_output(self.name) self.cboxMidiOutUuid = cbox.JackIO.create_midi_output(self.name)
cbox.JackIO.rename_midi_output(self.cboxMidiOutUuid, self._name) cbox.JackIO.rename_midi_output(self.cboxMidiOutUuid, self._name)
self.calfboxTrack.set_external_output(self.cboxMidiOutUuid) self.calfboxTrack.set_external_output(self.cboxMidiOutUuid)
for port in self._beforeDeleteThisJackMidiWasConnectedTo: 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 #Make it official
self._updatePlayback() self._updatePlayback()
def setSubtrack(self, key, blobs:Iterable): #(bytes-blob, position, length) def setSubtrack(self, key, blobs:Iterable): #(bytes-blob, position, length)
"""Creates a new subtrack if key is unknown """Creates a new subtrack if key is unknown
Forward data to the real function Forward data to the real function
Simplest version is to send one blob at position 0 with its length""" 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) self._subtracks[key] = _Subtrack(parentSequencerInterface=self)
assert self._subtracks[key], key assert self._subtracks[key], key
assert isinstance(self._subtracks[key], _Subtrack), type(self._subtracks[key]) assert isinstance(self._subtracks[key], _Subtrack), type(self._subtracks[key])
self._subtracks[key].setSubtrack(blobs) self._subtracks[key].setSubtrack(blobs)
#Save / Load / Export #Save / Load / Export
@ -413,15 +416,15 @@ class SequencerInterface(_Interface): #Basically the midi part of a track.
"""Generate Data to save as json""" """Generate Data to save as json"""
return { return {
"name" : self.name, "name" : self.name,
"enabled" : self._enabled, "enabled" : self._enabled,
} }
@classmethod @classmethod
def instanceFromSerializedData(cls, parentTrack, serializedData): def instanceFromSerializedData(cls, parentTrack, serializedData):
self = cls.__new__(cls) self = cls.__new__(cls)
self._name = serializedData["name"] self._name = serializedData["name"]
self._enabled = serializedData["enabled"] self._enabled = serializedData["enabled"]
self.parentTrack = parentTrack self.parentTrack = parentTrack
self.parentData = parentTrack.parentData self.parentData = parentTrack.parentData
self._processAfterInit() 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. """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 This is not a pure sfz sampler, but rather a track that ends in an instrument instead of a
jack midi output.""" jack midi output."""
def __init__(self, parentTrack, name:str, absoluteSfzPath:str): 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 = cbox.Document.get_engine().new_scene()
self.scene.clear() self.scene.clear()
self.scene.add_new_instrument_layer(name, "sampler") #"sampler" is the cbox sfz engine 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.scene.set_enable_default_song_input(True)
self.instrumentLayer = self.scene.status().layers[0].get_instrument() 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) 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. 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("") self.calfboxTrack.set_external_output("")
#Metadata #Metadata
portnameL = f"{cbox.JackIO.status().client_name}:out_1" 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(portnameL, name.title() + "-L")
cbox.JackIO.Metadata.set_pretty_name(portnameR, name.title() + "-R") cbox.JackIO.Metadata.set_pretty_name(portnameR, name.title() + "-R")
def enable(self, enabled): def enable(self, enabled):
if enabled: if enabled:
self.scene.status().layers[0].set_enable(True) self.scene.status().layers[0].set_enable(True)
else: else:
self.scene.status().layers[0].set_enable(False) 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. 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 @property
def enabled(self)->bool: def enabled(self)->bool:
return self._enabled return self._enabled
class TempoMap(object): class TempoMap(object):
""" """
This is a singleton instance in Score. Don't subclass. This is a singleton instance in Score. Don't subclass.
Main data structure is self._tempoMap = {positionInTicks:(bpmAsFloat, timesigUpper, timesigLower)} 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). The tempo map is only active if the whole program is JACK Transport Master (via Cbox).
If not we simply follow jack sync. 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 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. 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 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 then don't worry about save and load. Treat it as a cache that conveniently restores the last
setting after program startup. """ setting after program startup. """
def __init__(self, parentData): def __init__(self, parentData):
logger.info("Creating empty TempoMap instance") logger.info("Creating empty TempoMap instance")
self.parentData = parentData 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._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._isTransportMaster = False
self._processAfterInit() self._processAfterInit()
assert not cbox.Document.get_song().status().mtis assert not cbox.Document.get_song().status().mtis
def _processAfterInit(self): def _processAfterInit(self):
self.factor = 1.0 # not saved 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() self._sanitize()
def _updatePlayback(self): def _updatePlayback(self):
"""A wrapper that not only calls update playback but forces JACK to call its BBT callback, """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. 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() cbox.Document.get_song().update_playback()
pos = cbox.Transport.status().pos #can be None on program start 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 self.isTransportMaster and not cbox.Transport.status().playing: #pos can be 0
if pos is None: if pos is None:
#Yes, we destroy the current playback position. But we ARE timebase master, so that is fine. #Yes, we destroy the current playback position. But we ARE timebase master, so that is fine.
cbox.Transport.seek_samples(0) cbox.Transport.seek_samples(0)
else: #default case else: #default case
cbox.Transport.seek_samples(pos) cbox.Transport.seek_samples(pos)
@property @property
def isTransportMaster(self) -> bool: def isTransportMaster(self) -> bool:
return self._isTransportMaster return self._isTransportMaster
@ -555,74 +558,74 @@ class TempoMap(object):
self._isTransportMaster = value self._isTransportMaster = value
if value: if value:
self._sendToCbox() #reactivate existing tempo map self._sendToCbox() #reactivate existing tempo map
cbox.JackIO.external_tempo(False) 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) cbox.JackIO.transport_mode(master = True, conditional = False) #conditional = only attempt to become a master (will fail if there is one already)
else: else:
self._clearCboxTempoMap() #clear cbox map but don't touch our own data. self._clearCboxTempoMap() #clear cbox map but don't touch our own data.
cbox.JackIO.external_tempo(True) cbox.JackIO.external_tempo(True)
try: try:
cbox.JackIO.transport_mode(master = False) cbox.JackIO.transport_mode(master = False)
except Exception: #"Not a current timebase master" except Exception: #"Not a current timebase master"
pass pass
self._updatePlayback() self._updatePlayback()
def _sanitize(self): def _sanitize(self):
"""Inplace modification of self.tempoMap. Remove zeros and convert to float values. """ """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} 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 #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") #logger.warning("Found invalid tempo map. Forcing to 120 bpm. Please correct manually")
#self._tempoMap = {0, 120.0} #self._tempoMap = {0, 120.0}
def _clearCboxTempoMap(self): def _clearCboxTempoMap(self):
"""Remove all cbox tempo values by iterating over all of them and set them to None, which is """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. the secret cbox handshake to delete a tempo change on a specific position.
Keep our own local data intact.""" 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. 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) song.delete_mti(mti.pos)
self._updatePlayback() self._updatePlayback()
assert not song.status().mtis, song.status().mtis assert not song.status().mtis, song.status().mtis
def _sendToCbox(self): def _sendToCbox(self):
"""Send to cbox""" """Send to cbox"""
assert self.isTransportMaster assert self.isTransportMaster
assert self._tempoMap assert self._tempoMap
song = cbox.Document.get_song() song = cbox.Document.get_song()
for pos, (value, timesigNum, timesigDenom) in self._tempoMap.items(): 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, 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) #Tempo changes are fine to happen on the same tick as note on.
self._updatePlayback() self._updatePlayback()
def setTempoMap(self, tempoMap:dict): def setTempoMap(self, tempoMap:dict):
"""All-in-one function for outside access""" """All-in-one function for outside access"""
if self._tempoMap != tempoMap: if self._tempoMap != tempoMap:
self._tempoMap = tempoMap self._tempoMap = tempoMap
self._sanitize() self._sanitize()
if self.isTransportMaster: #if not the data will be used later. if self.isTransportMaster: #if not the data will be used later.
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() self._sendToCbox()
def setFactor(self, factor:float): 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.factor = round(factor, 4)
self._sanitize() 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 self._sendToCbox() #uses the factor
def setQuarterNotesPerMinute(self, quarterNotesPerMinute:float): def setQuarterNotesPerMinute(self, quarterNotesPerMinute:float):
"""Simple tempo setter. Overrides all other tempo data. """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] currentValue, timesigNum, timesigDenom = self._tempoMap[0]
self.setTempoMap({0:(quarterNotesPerMinute, timesigNum, timesigDenom)}) self.setTempoMap({0:(quarterNotesPerMinute, timesigNum, timesigDenom)})
def setTimeSignature(self, timesigNum:int, timesigDenom:int): def setTimeSignature(self, timesigNum:int, timesigDenom:int):
"""Simple traditional timesig setter. Overrides all other timesig data. """Simple traditional timesig setter. Overrides all other timesig data.
Works in tandem with self.setTimeSignature. Works in tandem with self.setTimeSignature.
""" """
assert timesigNum > 0, timesigNum 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" #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] currentValue, OLD_timesigNum, OLD_timesigDenom = self._tempoMap[0]

Loading…
Cancel
Save