Browse Source

guard against a rare crash that happens when a qt widget tries to access a just deleted track, because the widget itself got deleted which triggered an out-of-focus-send-to-engine event

tags/v2.1.0
Nils 2 months ago
parent
commit
b24394e681
2 changed files with 48 additions and 3 deletions
  1. +36
    -2
      engine/api.py
  2. +12
    -1
      template/engine/sequencer.py

+ 36
- 2
engine/api.py View File

@@ -475,6 +475,7 @@ def set_measuresPerGroup(value):
def changeTrackName(trackId, name):
"""The template gurantees a unique, sanitized name across tracks and groups"""
track = session.data.trackById(trackId)
if not track: return
if not name.lower() in (gr.lower() for gr in getGroups()):
session.history.register(lambda trId=trackId, v=track.sequencerInterface.name: changeTrackName(trId,v), descriptionString="Track Name")
track.sequencerInterface.name = name #sanitizes on its own. Checks for duplicate tracks but not groups
@@ -483,6 +484,7 @@ def changeTrackName(trackId, name):
def changeTrackColor(trackId, colorInHex):
"""Expects "#rrggbb"""
track = session.data.trackById(trackId)
if not track: return
assert len(colorInHex) == 7, colorInHex
session.history.register(lambda trId=trackId, v=track.color: changeTrackColor(trId,v), descriptionString="Track Color")
track.color = colorInHex
@@ -495,6 +497,7 @@ def changeTrackMidiChannel(trackId, newChannel:int):
logger.warning(f"Midi Channel must be between 1-16 for this function, was: {newChannel}. Doing nothing.")
return
track = session.data.trackById(trackId)
if not track: return
session.history.register(lambda trId=trackId, v=track.midiChannel: changeTrackMidiChannel(trId,v), descriptionString="Track Midi Channel")
track.midiChannel = newChannel-1
track.pattern.buildExportCache()
@@ -517,6 +520,7 @@ def createSiblingTrack(trackId): #aka clone track
The jack midi out will be independent after creation, but connected to the same instrument
(if any)"""
track = session.data.trackById(trackId)
if not track: return
assert type(track.pattern.scale) == tuple
newTrack = session.data.addTrack(name=track.sequencerInterface.name, scale=track.pattern.scale, color=track.color, simpleNoteNames=track.pattern.simpleNoteNames) #track name increments itself from "Track A" to "Track B" or "Track 1" -> "Track 2"
newTrack.pattern.averageVelocity = track.pattern.averageVelocity
@@ -551,6 +555,7 @@ def _reinsertDeletedTrack(track, trackIndex):

def deleteTrack(trackId):
track = session.data.trackById(trackId)
if not track: return
oldIndex = session.data.tracks.index(track)
with session.history.sequence("Delete Track"):
setTrackGroup(trackId, "") #has it's own undo
@@ -562,8 +567,6 @@ def deleteTrack(trackId):
else:
session.history.register(lambda tr=deletedTrack, pos=oldIndex: _reinsertDeletedTrack(tr, pos), descriptionString="Delete Track")

print (session.data.groups)

updatePlayback()
callbacks._numberOfTracksChanged()

@@ -574,6 +577,7 @@ def moveTrack(trackId, newIndex):
in session.data.tracks , so that jack metadata port order works, groups or not.
"""
track = session.data.trackById(trackId)
if not track: return
oldIndex = session.data.tracks.index(track)
if not oldIndex == newIndex:
session.history.register(lambda tr=trackId, pos=oldIndex: moveTrack(trackId, pos), descriptionString="Move Track")
@@ -587,6 +591,7 @@ def setTrackPatternLengthMultiplicator(trackId, newMultiplicator:int):
return #Invalid input

track = session.data.trackById(trackId)
if not track: return
session.history.register(lambda tr=trackId, v=track.patternLengthMultiplicator: setTrackPatternLengthMultiplicator(trackId, v), descriptionString="Pattern Multiplier")
track.patternLengthMultiplicator = newMultiplicator
track.pattern.buildExportCache()
@@ -610,6 +615,7 @@ def setTrackGroup(trackId, groupName:str):
"""A not yet existing groupName will create that.
Set to empty string to create a standalone track"""
track = session.data.trackById(trackId)
if not track: return
groupName = ''.join(ch for ch in groupName if ch.isalnum()) #sanitize
groupName = " ".join(groupName.split()) #remove double spaces
if not track.group == groupName:
@@ -675,6 +681,7 @@ def _setTrackStructure(trackId, structure, undoMessage):
"""For undo. Used by all functions as entry point, then calls itself for undo/redo.
structure is a set of integers which we can copy with .copy()"""
track = session.data.trackById(trackId)
if not track: return

session.history.register(lambda tr=trackId, v=track.structure.copy(), msg=undoMessage: _setTrackStructure(trackId, v, msg), descriptionString=undoMessage)

@@ -687,6 +694,7 @@ def _setTrackStructure(trackId, structure, undoMessage):
def setSwitches(trackId, setOfPositions, newBool):
"""Used in the GUI to select multiple switches in a row by dragging the mouse"""
track = session.data.trackById(trackId)
if not track: return
session.history.register(lambda tr=trackId, v=track.structure.copy(): _setTrackStructure(trackId, v, "Set Measures"), descriptionString="Set Measures")
if newBool:
track.structure = track.structure.union(setOfPositions) #merge: add setOfPositions to the existing one
@@ -698,6 +706,7 @@ def setSwitches(trackId, setOfPositions, newBool):

def setSwitch(trackId, position, newBool):
track = session.data.trackById(trackId)
if not track: return
session.history.register(lambda trId=trackId, v=track.structure.copy(): _setTrackStructure(trId, v, "Set Measures"), descriptionString="Set Measures")
if newBool:
if position in track.structure: return
@@ -712,6 +721,7 @@ def setSwitch(trackId, position, newBool):

def trackInvertSwitches(trackId):
track = session.data.trackById(trackId)
if not track: return
session.history.register(lambda trId=trackId, v=track.structure.copy(): _setTrackStructure(trId, v, "Invert Measures"), descriptionString="Invert Measures")
"""
if track.structure:
@@ -728,6 +738,7 @@ def trackInvertSwitches(trackId):

def trackOffAllSwitches(trackId):
track = session.data.trackById(trackId)
if not track: return
session.history.register(lambda trId=trackId, v=track.structure.copy(): _setTrackStructure(trId, v, "Track Measures Off"), descriptionString="Track Measures Off")
track.structure = set()
track.buildTrack()
@@ -736,6 +747,7 @@ def trackOffAllSwitches(trackId):

def trackOnAllSwitches(trackId):
track = session.data.trackById(trackId)
if not track: return
session.history.register(lambda trId=trackId, v=track.structure.copy(): _setTrackStructure(trId, v, "Track Measures On"), descriptionString="Track Measures On")
track.structure = set(i for i in range(session.data.numberOfMeasures))
track.buildTrack()
@@ -773,6 +785,7 @@ def _setSwitchesScaleTranspose(trackId, whichPatternsAreScaleTransposed):
"""For undo. Used by all functions as entry point, then calls itself for undo/redo.
whichPatternsAreScaleTransposed is a dicts of int:int which we can copy with .copy()"""
track = session.data.trackById(trackId)
if not track: return

session.history.register(lambda tr=trackId, v=track.whichPatternsAreScaleTransposed.copy(): _setSwitchesScaleTranspose(trackId, v), descriptionString="Set Modal Shift")

@@ -785,6 +798,7 @@ def _setSwitchesScaleTranspose(trackId, whichPatternsAreScaleTransposed):
def setSwitchScaleTranspose(trackId, position, transpose):
"""Scale transposition is flipped. lower value means higher pitch"""
track = session.data.trackById(trackId)
if not track: return
session.history.register(lambda tr=trackId, v=track.whichPatternsAreScaleTransposed.copy(): _setSwitchesScaleTranspose(trackId, v), descriptionString="Set Modal Shift")
track.whichPatternsAreScaleTransposed[position] = transpose
track.buildTrack()
@@ -797,6 +811,7 @@ def _setSwitchHalftoneTranspose(trackId, whichPatternsAreHalftoneTransposed):
"""For undo. Used by all functions as entry point, then calls itself for undo/redo.
whichPatternsAreScaleTransposed is a dicts of int:int which we can copy with .copy()"""
track = session.data.trackById(trackId)
if not track: return

session.history.register(lambda tr=trackId, v=track.whichPatternsAreHalftoneTransposed.copy(): _setSwitchHalftoneTranspose(trackId, v), descriptionString="Set Half Tone Shift")

@@ -808,6 +823,7 @@ def _setSwitchHalftoneTranspose(trackId, whichPatternsAreHalftoneTransposed):
def setSwitchHalftoneTranspose(trackId, position, transpose):
"""Halftone transposition is not flipped. Higher value means higher pitch"""
track = session.data.trackById(trackId)
if not track: return
session.history.register(lambda tr=trackId, v=track.whichPatternsAreHalftoneTransposed.copy(): _setSwitchHalftoneTranspose(trackId, v), descriptionString="Set Half Tone Shift")
track.whichPatternsAreHalftoneTransposed[position] = transpose
track.buildTrack()
@@ -1015,6 +1031,7 @@ def setPattern(trackId, patternList, undoMessage):
It is rarely used directly by the GUI, if at all. Normal changes are atomic.
"""
track = session.data.trackById(trackId)
if not track: return

session.history.register(lambda trId=trackId, v=track.pattern.copyData(), msg=undoMessage: setPattern(trId, v, msg), descriptionString=undoMessage)

@@ -1037,6 +1054,7 @@ def setStep(trackId, stepExportDict):
This is also for velocity!
"""
track = session.data.trackById(trackId)
if not track: return
session.history.register(lambda trId=trackId, v=track.pattern.copyData(): setPattern(trId, v, "Change Step"), descriptionString="Change Step")
oldNote = track.pattern.stepByIndexAndPitch(index=stepExportDict["index"], pitch=stepExportDict["pitch"])
if oldNote: #modify existing note
@@ -1054,6 +1072,7 @@ def setStep(trackId, stepExportDict):
def removeStep(trackId, index, pitch):
"""Reverse of setStep"""
track = session.data.trackById(trackId)
if not track: return
session.history.register(lambda trId=trackId, v=track.pattern.copyData(): setPattern(trId, v, "Remove Step"), descriptionString="Remove Step")
oldNote = track.pattern.stepByIndexAndPitch(index, pitch)
track.pattern.data.remove(oldNote)
@@ -1066,6 +1085,7 @@ def setScale(trackId, scale, callback = True):
"""Expects a scale list or tuple from lowest index to highest.
Actual pitches don't matter."""
track = session.data.trackById(trackId)
if not track: return
session.history.register(lambda trId=trackId, v=track.pattern.scale[:]: setScale(trId, v, callback=True), descriptionString="Set Scale")
track.pattern.scale = scale #tuple, or list if oversight in json loading :)
track.pattern.buildExportCache()
@@ -1078,12 +1098,14 @@ def setSimpleNoteNames(trackId, simpleNoteNames):
"""note names is a list of strings with length 128. One name for each midi note.
It is saved to file"""
track = session.data.trackById(trackId)
if not track: return
session.history.register(lambda trId=trackId, v=track.pattern.simpleNoteNames[:]: setSimpleNoteNames(trId, v), descriptionString="Note Names")
track.pattern.simpleNoteNames = simpleNoteNames #list of strings
callbacks._trackMetaDataChanged(track)

def transposeHalftoneSteps(trackId, steps):
track = session.data.trackById(trackId)
if not track: return
session.history.register(lambda trId=trackId, v=track.pattern.scale[:]: setScale(trId, v, callback=True), descriptionString="Transpose Scale")
track.pattern.scale = [midipitch+steps for midipitch in track.pattern.scale]
track.pattern.buildExportCache()
@@ -1093,6 +1115,7 @@ def transposeHalftoneSteps(trackId, steps):

def patternInvertSteps(trackId):
track = session.data.trackById(trackId)
if not track: return
session.history.register(lambda trId=trackId, v=track.pattern.copyData(): setPattern(trId, v, "Invert Steps"), descriptionString="Invert Steps")
track.pattern.invert()
track.pattern.buildExportCache()
@@ -1102,6 +1125,7 @@ def patternInvertSteps(trackId):

def patternOnAllSteps(trackId):
track = session.data.trackById(trackId)
if not track: return
session.history.register(lambda trId=trackId, v=track.pattern.copyData(): setPattern(trId, v, "All Steps On"), descriptionString="All Steps On")
track.pattern.fill()
track.pattern.buildExportCache()
@@ -1111,6 +1135,7 @@ def patternOnAllSteps(trackId):

def patternOffAllSteps(trackId):
track = session.data.trackById(trackId)
if not track: return
session.history.register(lambda trId=trackId, v=track.pattern.copyData(): setPattern(trId, v, "All Steps Off"), descriptionString="All Steps Off")
track.pattern.empty()
track.pattern.buildExportCache()
@@ -1121,6 +1146,7 @@ def patternOffAllSteps(trackId):
def patternInvertRow(trackId, pitchindex):
"""Pitchindex is the row"""
track = session.data.trackById(trackId)
if not track: return
session.history.register(lambda trId=trackId, v=track.pattern.copyData(): setPattern(trId, v, "Invert Row"), descriptionString="Invert Row")
track.pattern.invertRow(pitchindex)
track.pattern.buildExportCache()
@@ -1132,6 +1158,7 @@ def patternClearRow(trackId, pitchindex):
"""Pitchindex is the row.
Index is the column"""
track = session.data.trackById(trackId)
if not track: return
session.history.register(lambda trId=trackId, v=track.pattern.copyData(): setPattern(trId, v, "Clear Row"), descriptionString="Clear Row")
track.pattern.clearRow(pitchindex)
track.pattern.buildExportCache()
@@ -1143,6 +1170,7 @@ def patternRowRepeatFromStep(trackId, pitchindex, index):
"""Pitchindex is the row.
Index is the column"""
track = session.data.trackById(trackId)
if not track: return
session.history.register(lambda trId=trackId, v=track.pattern.copyData(): setPattern(trId, v, "Fill Row with Repeat"), descriptionString="Fill Row with Repeat")
track.pattern.repeatFromStep(pitchindex, index)
track.pattern.buildExportCache()
@@ -1152,6 +1180,7 @@ def patternRowRepeatFromStep(trackId, pitchindex, index):

def patternRowChangeVelocity(trackId, pitchindex, delta):
track = session.data.trackById(trackId)
if not track: return
session.history.register(lambda trId=trackId, v=track.pattern.copyData(): setPattern(trId, v, "Change Row Velocity"), descriptionString="Change Row Velocity")
for note in track.pattern.getRow(pitchindex):
new = note["velocity"] + delta
@@ -1204,6 +1233,7 @@ def setScaleToKeyword(trackId, keyword):
This function is called not often and does not need to be performant.
"""
track = session.data.trackById(trackId)
if not track: return
session.history.register(lambda trId=trackId, v=track.pattern.scale[:]: setScale(trId, v, callback=True), descriptionString="Set Scale")

rememberRootNote = track.pattern.scale[-1] #The last note has a special role by convention. No matter if this is the lowest midi-pitch or not. Most of the time it is the lowest though.
@@ -1248,6 +1278,7 @@ def setScaleToKeyword(trackId, keyword):

def changePatternVelocity(trackId, steps):
track = session.data.trackById(trackId)
if not track: return
session.history.register(lambda trId=trackId, v=track.pattern.copyData(): setPattern(trId, v, "Change Pattern Velocity"), descriptionString="Change Pattern Velocity")
for note in track.pattern.data:
new = note["velocity"] + steps
@@ -1272,6 +1303,7 @@ def resizePatternWithoutScale(trackId, steps):
return

track = session.data.trackById(trackId)
if not track: return

#We could use setScale for undo. But this requires a different set of callbacks. We use our own function, eventhough some of the calculations are not needed for undo.

@@ -1305,11 +1337,13 @@ def resizePatternWithoutScale(trackId, steps):
#Other functions. These can't be template functions because they use a specific track and Patroneos row and scale system.
def noteOn(trackId, row):
track = session.data.trackById(trackId)
if not track: return
midipitch = track.pattern.scale[row]
cbox.send_midi_event(0x90+track.midiChannel, midipitch, track.pattern.averageVelocity, output=track.cboxMidiOutAbstraction)

def noteOff(trackId, row):
track = session.data.trackById(trackId)
if not track: return
midipitch = track.pattern.scale[row]
cbox.send_midi_event(0x80+track.midiChannel, midipitch, track.pattern.averageVelocity, output=track.cboxMidiOutAbstraction)


+ 12
- 1
template/engine/sequencer.py View File

@@ -61,6 +61,7 @@ class Score(Data):
self.tracks = [] #see docstring
self.tempoMap = TempoMap(parentData = self)
self._template_processAfterInit()
self._tracksFailedLookup = []

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"""
@@ -119,11 +120,21 @@ class Score(Data):
except Exception as e: #No Jack Meta Data or Error with ports.
logger.error(e)


def trackById(self, trackId:int):
"""Returns a track or None, if not found"""
for track in self.tracks:
if trackId == id(track):
return track
raise ValueError(f"Track {trackId} not found. Current Tracks: {[id(tr) for tr in self.tracks]}")
else:
#Previously this crashed with a ValueError. However, after a rare bug that a gui widget focussed out because a track was deleted and then tried to send its value to the engine we realize that this lookup can gracefully return None.
#Nothing will break: Functions that are not aware yet, that None is an option will crash when they try to access None as a track object. For this case we present the following logger error:
if not trackId in self._tracksFailedLookup:
logger.error(f"Track {trackId} not found. Current Tracks: {[id(tr) for tr in self.tracks]}")
self._tracksFailedLookup.append(trackId) #prevent multiple error messages for the same track in a row.
return None
#raise ValueError(f"Track {trackId} not found. Current Tracks: {[id(tr) for tr in self.tracks]}")



#Save / Load / Export

Loading…
Cancel
Save