diff --git a/template/engine/api.py b/template/engine/api.py index 015eb76..e280d75 100644 --- a/template/engine/api.py +++ b/template/engine/api.py @@ -78,7 +78,7 @@ class Callbacks(object): def _dataChanged(self): - """Only called from within the callbacks. + """Only called from within the callbacks or template api. This is about data the user cares about. In other words this is the indicator if you need to save again. @@ -130,7 +130,8 @@ class Callbacks(object): def _setPlaybackTicks(self): - """This gets called very very often. Any connected function needs to watch closely + """This gets called very very often (~60 times per second). + Any connected function needs to watch closely for performance issues""" ppqn = cbox.Transport.status().pos_ppqn status = playbackStatus() @@ -237,12 +238,13 @@ class Callbacks(object): """New track, delete track, reorder Sent the current track order as list of ids, combined with their structure. This is also used when tracks get created or deleted, also on initial load. - This also includes the pattern. """ session.data.updateJackMetadataSorting() lst = [track.export() for track in session.data.tracks] for func in self.numberOfTracksChanged: func(lst) + self._dataChanged() #includes _historyChanged + def _metronomeChanged(self): """returns a dictionary with meta data such as the mute-state and the track name""" diff --git a/template/engine/sequencer.py b/template/engine/sequencer.py index b0a3c57..2541da2 100644 --- a/template/engine/sequencer.py +++ b/template/engine/sequencer.py @@ -88,6 +88,7 @@ class Score(Data): #Tracks def addTrack(self, name:str=""): + """Create and add a new track. Not an existing one""" track = Score.TrackClass(parentData=self, name=name) assert track.sequencerInterface self.tracks.append(track) @@ -96,6 +97,7 @@ class Score(Data): def deleteTrack(self, track): track.sequencerInterface.prepareForDeletion() self.tracks.remove(track) + return track #for undo def updateJackMetadataSorting(self): """Add this to you "tracksChanged" or "numberOfTracksChanged" callback. @@ -105,12 +107,16 @@ class Score(Data): self.tracks changed and subsequent sorting. Multiple track changes in a row are common, therefore the place to update jack order is in the API, where the new track order is also sent to the UI. + + We also check if the track is 'deactivated' by probing track.cboxMidiOutUuid. + Patroneo uses prepareForDeletion to deactive the tracks standalone track but keeps the + interface around for later use. """ - 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 if track.sequencerInterface.cboxMidiOutUuid)} try: cbox.JackIO.Metadata.set_all_port_order(order) - except Exception as e: #No Jack Meta Data + except Exception as e: #No Jack Meta Data or Error with ports. logger.error(e) def trackById(self, trackId:int): @@ -195,6 +201,8 @@ class _Interface(object): def _isNameAvailable(self, name:str): """Check if the name is free. If not increment""" + name = ''.join(ch for ch in name if ch.isalnum() or ch in (" ", "_", "-")) #sanitize + name = " ".join(name.split()) #remove double spaces while name in [tr.sequencerInterface.name for tr in self.parentData.tracks]: beforeLastChar = name[-2] lastChar = name[-1] @@ -319,7 +327,7 @@ class SequencerInterface(_Interface): #Basically the midi part of a track. def _processAfterInit(self): #Create midi out and cbox track - logger.info("Creating empty SequencerInterface instance") + logger.info(f"Creating empty SequencerInterface instance for {self._name}") super()._processAfterInit() self.cboxMidiOutUuid = cbox.JackIO.create_midi_output(self._name) self.calfboxTrack.set_external_output(self.cboxMidiOutUuid) @@ -346,7 +354,8 @@ class SequencerInterface(_Interface): #Basically the midi part of a track. def name(self, value): if not value in (track.sequencerInterface.name for track in self.parentData.tracks): self._name = self._isNameAvailable(value) - cbox.JackIO.rename_midi_output(self.cboxMidiOutUuid, self._name) + if self.cboxMidiOutUuid: #we could be deactivated + cbox.JackIO.rename_midi_output(self.cboxMidiOutUuid, self._name) def cboxPortName(self)->str: """Return the complete jack portname: OurName:PortName""" @@ -360,7 +369,14 @@ class SequencerInterface(_Interface): #Basically the midi part of a track. undo. That is why we bother setting calfboxTrack to None again. """ - portlist = cbox.JackIO.get_connected_ports(self.cboxPortName()) + if not self.calfboxTrack: #maybe non-template part deactivated it, like Patroneo groups + return + + try: + portlist = cbox.JackIO.get_connected_ports(self.cboxPortName()) + except: #port not found. + portlist = [] + self._beforeDeleteThisJackMidiWasConnectedTo = portlist self.calfboxTrack.set_external_output("") @@ -411,6 +427,15 @@ class SequencerInterface(_Interface): #Basically the midi part of a track. self._subtracks[key].setSubtrack(blobs) + def deleteSubtrack(self, key): + """Remove a subtrack. + Return for a potential undo""" + self._subtracks[key].prepareForDeletion() + toDelete = self._subtracks[key] + del self._subtracks[key] + return toDelete + + #Save / Load / Export def serialize(self)->dict: """Generate Data to save as json"""