From 0e0c6361c68571c2b99d013b2bbe5f783b330541 Mon Sep 17 00:00:00 2001 From: Nils <> Date: Sat, 3 Jul 2021 22:08:32 +0200 Subject: [PATCH] Add new track options to control step delay and augment behaviour to track context menu --- CHANGELOG | 1 + engine/api.py | 52 +++++++++++++++++++++++++++++++++++++-------- engine/pattern.py | 1 - engine/track.py | 10 +++++---- qtgui/mainwindow.py | 1 - qtgui/songeditor.py | 11 ++++++++++ 6 files changed, 61 insertions(+), 15 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 0657144..d8d582a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -6,6 +6,7 @@ Add track-switch to choose step-delay wrap-around behaviour, if delayed notes en Add non-destructiv pattern augmentation/scaling. Make an individual measure shorter or longer overall, with the same GUI as transpositions. Add track-switch to choose repeat-to-fill behaviour if scaling a pattern shorter, which creates empty space. Change user input to choose above measure modifications. Everything is now accesible by shortcuts, removing the functionality from the mousewheel. +Fix missing undo for measure modifications after delete. Fix wrong playback cursor speed. Fix small GUI drawing issues diff --git a/engine/api.py b/engine/api.py index a6099bf..2485bab 100644 --- a/engine/api.py +++ b/engine/api.py @@ -523,6 +523,31 @@ def changeTrackMidiChannel(trackId, newChannel:int): track.buildTrack() updatePlayback() callbacks._trackMetaDataChanged(track) + #But not the actual callback because nothing in a gui needs redrawing. Everything looks the same. + +def changeTrackStepDelayWrapAround(trackId, newState:bool): + track = session.data.trackById(trackId) + if not track: return + session.history.register(lambda trId=trackId, v=track.stepDelayWrapAround: changeTrackStepDelayWrapAround(trId,v), descriptionString="Track Step Delay Wrap-Around") + track.stepDelayWrapAround = newState + track.pattern.buildExportCache() + track.buildTrack() + updatePlayback() + callbacks._trackMetaDataChanged(track) + #But not the actual callback because nothing in a gui needs redrawing. Everything looks the same. + +def changeTrackRepeatDiminishedPatternInItself(trackId, newState:bool): + track = session.data.trackById(trackId) + if not track: return + session.history.register(lambda trId=trackId, v=track.repeatDiminishedPatternInItself: changeTrackRepeatDiminishedPatternInItself(trId,v), descriptionString="Track Repeat Diminished Pattern in itself") + track.repeatDiminishedPatternInItself = newState + track.pattern.buildExportCache() + track.buildTrack() + updatePlayback() + callbacks._trackMetaDataChanged(track) + #But not the actual callback because nothing in a gui needs redrawing. Everything looks the same. + + def addTrack(scale=None): if scale: @@ -716,7 +741,9 @@ 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 else: @@ -728,7 +755,9 @@ 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 track.structure.add(position) @@ -841,10 +870,12 @@ def _setSwitchHalftoneTranspose(trackId, whichPatternsAreHalftoneTransposed): session.history.register(lambda tr=trackId, v=track.whichPatternsAreHalftoneTransposed.copy(): _setSwitchHalftoneTranspose(trackId, v), descriptionString="Set Half Tone Shift") track.whichPatternsAreHalftoneTransposed = whichPatternsAreHalftoneTransposed #restore data + track.buildTrack() updatePlayback() callbacks._trackStructureChanged(track) + def setSwitchHalftoneTranspose(trackId, position:int, transpose:int): """Halftone transposition is not flipped. Higher value means higher pitch Default value is 0. @@ -884,7 +915,7 @@ def setSwitchStepDelay(trackId, position:int, delay:int): return True -def _setSwitchAugmentationsFactor(trackId, whichPatternsAreStepDelayed): +def _setSwitchAugmentationsFactor(trackId, whichPatternsHaveAugmentationFactor): """For undo. Used by all functions as entry point, then calls itself for undo/redo. whichPatternsHaveAugmentationFactor is a dicts of int:float which we can copy with .copy()""" track = session.data.trackById(trackId) @@ -910,12 +941,14 @@ def setSwitchAugmentationsFactor(trackId, position:int, factor:float): return True - def _registerHistoryWholeTrackSwitches(track): - """This is used by insertSilence, clearSwitchGroupModifications etc. - It assumes to run inside the context: - with session.history.sequence(""): + """This is used by insertSilence, clearSwitchGroupModifications, + exchangeSwitchGroupWithGroupToTheRight etc. + + It assumes that it runs inside this context: + with session.history.sequence("asdasd"): """ + print ("reg all", track) trackId = id(track) session.history.register(lambda trId=trackId, v=track.patternLengthMultiplicator: setTrackPatternLengthMultiplicator(trId, v), descriptionString="Pattern Multiplier") session.history.register(lambda trId=trackId, v=track.structure.copy(): _setTrackStructure(trId, v, "Change Group"), descriptionString="Change Group") @@ -923,7 +956,7 @@ def _registerHistoryWholeTrackSwitches(track): session.history.register(lambda trId=trackId, v=track.whichPatternsAreHalftoneTransposed.copy(): _setSwitchHalftoneTranspose(trId, v), descriptionString="Set Half Tone Shift") session.history.register(lambda trId=trackId, v=track.whichPatternsAreStepDelayed.copy(): _setSwitchStepDelay(trId, v), descriptionString="Set Step Delay") session.history.register(lambda trId=trackId, v=track.whichPatternsHaveAugmentationFactor.copy(): _setSwitchAugmentationsFactor(trId, v), descriptionString="Set Augmentation Factor") - + print ("reg done") def insertSilence(howMany:int, beforeMeasureNumber:int): """Insert empty measures into all tracks. @@ -943,6 +976,7 @@ def insertSilence(howMany:int, beforeMeasureNumber:int): track.whichPatternsAreHalftoneTransposed = { (k+thisTrackHowMany if k >= thisTrackWhere else k):v for k,v in track.whichPatternsAreHalftoneTransposed.items() } track.whichPatternsAreStepDelayed = { (k+thisTrackHowMany if k >= thisTrackWhere else k):v for k,v in track.whichPatternsAreStepDelayed.items() } track.whichPatternsHaveAugmentationFactor = { (k+thisTrackHowMany if k >= thisTrackWhere else k):v for k,v in track.whichPatternsHaveAugmentationFactor.items() } + callbacks._trackStructureChanged(track) callbacks._dataChanged() #register undo @@ -1003,6 +1037,7 @@ def exchangeSwitchGroupWithGroupToTheRight(startMeasureForGroup:int, endMeasureE #Remember for later testing lenStructure = len(track.structure) + lenHalfToneTransposed = len(track.whichPatternsAreHalftoneTransposed.keys()) lenScaleTransposed = len(track.whichPatternsAreScaleTransposed.keys()) lenStepDelayed = len(track.whichPatternsAreStepDelayed.keys()) lenAugmentedFactor = len(track.whichPatternsHaveAugmentationFactor.keys()) @@ -1050,8 +1085,8 @@ def exchangeSwitchGroupWithGroupToTheRight(startMeasureForGroup:int, endMeasureE track.structure.add(sw-groupSize) for stPos, stVal in tempScaleTransposed.items(): track.whichPatternsAreScaleTransposed[stPos-groupSize] = stVal - for htPos, htVal in tempScaleTransposed.items(): - track.whichPatternsAreHalftoneTransposed[hPos-groupSize] = htVal + for htPos, htVal in tempHalfToneTransposed.items(): + track.whichPatternsAreHalftoneTransposed[htPos-groupSize] = htVal for sDPos, sDVal in tempStepDelayed.items(): track.whichPatternsAreStepDelayed[sDPos-groupSize] = sDVal for aFPos, aFVal in tempAugmentedFactor.items(): @@ -1070,7 +1105,6 @@ def exchangeSwitchGroupWithGroupToTheRight(startMeasureForGroup:int, endMeasureE session.data.buildAllTracks() updatePlayback() - def clearSwitchGroupModifications(startMeasureForGroup:int, endMeasureExclusive:int): """startMeasureForGroup and endMeasureExclusive are in the global, un-multiplied measure counting format.""" diff --git a/engine/pattern.py b/engine/pattern.py index 16cf198..cdc1cc9 100644 --- a/engine/pattern.py +++ b/engine/pattern.py @@ -343,7 +343,6 @@ class Pattern(object): shuffle = int(self.parentTrack.parentData.swing * whatTypeOfUnit) #If we diminished and want to repeat the sub-pattern, create virtual extra notes. - self.parentTrack.repeatDiminishedPatternInItself = True virtualNotes = [] inverseAugmentFactor = round(0.5 + 1 / augmentationFactor) if self.parentTrack.repeatDiminishedPatternInItself and augmentationFactor < 1: diff --git a/engine/track.py b/engine/track.py index 6fe184b..a70f504 100644 --- a/engine/track.py +++ b/engine/track.py @@ -94,6 +94,7 @@ class Track(object): #injection at the bottom of this file! This is called after every small change. """ + if self.group: #We still have an inactive sequencerinterface but instead we use the group ones as subtrack. ourCalfboxTrack = self.parentData.groups[self.group]._subtracks[id(self)].calfboxSubTrack @@ -113,10 +114,11 @@ class Track(object): #injection at the bottom of this file! #First clean all modifications from the default value. #We test for k in self.structure to not have modifications for measures that will be switched on later #TODO: Can this possibly lead to a race condition where we load modifications first and then load the structure, which results in the mods getting perma-deleted here. e.g. we could not set default value in init, even for testing purposes. - self.whichPatternsAreScaleTransposed = {k:v for k,v in self.whichPatternsAreScaleTransposed.items() if v!=0 and k in self.structure} - self.whichPatternsAreHalftoneTransposed = {k:v for k,v in self.whichPatternsAreHalftoneTransposed.items() if v!=0 and k in self.structure} - self.whichPatternsAreStepDelayed = {k:v for k,v in self.whichPatternsAreStepDelayed.items() if v!=0 and k in self.structure} - self.whichPatternsHaveAugmentationFactor = {k:v for k,v in self.whichPatternsHaveAugmentationFactor.items() if v!=1.0 and k in self.structure} #default is 1.0 + #TODO: Yes, this happened with undo. every {comprehension} had a "and if k in self.structure" at the end. We took that out without remembering what that was for. + self.whichPatternsAreScaleTransposed = {k:v for k,v in self.whichPatternsAreScaleTransposed.items() if v!=0} + self.whichPatternsAreHalftoneTransposed = {k:v for k,v in self.whichPatternsAreHalftoneTransposed.items() if v!=0} + self.whichPatternsAreStepDelayed = {k:v for k,v in self.whichPatternsAreStepDelayed.items() if v!=0} + self.whichPatternsHaveAugmentationFactor = {k:v for k,v in self.whichPatternsHaveAugmentationFactor.items() if v!=1.0} #default is 1.0 oneMeasureInTicks = (self.parentData.howManyUnits * self.parentData.whatTypeOfUnit) / self.parentData.subdivisions #subdivisions is 1 by default. bigger values mean shorter values, which is compensated by the user setting bigger howManyUnits manually. oneMeasureInTicks = int(oneMeasureInTicks) * self.patternLengthMultiplicator diff --git a/qtgui/mainwindow.py b/qtgui/mainwindow.py index a1685ab..5499f8d 100644 --- a/qtgui/mainwindow.py +++ b/qtgui/mainwindow.py @@ -214,7 +214,6 @@ class MainWindow(TemplateMainWindow): #self.statusBar().showMessage(QtCore.QCoreApplication.translate("Statusbar", "")) self.statusBar().showMessage("") - api.session.data.setLanguageForEmptyFile(language = QtCore.QLocale().languageToString(QtCore.QLocale().language())) #TODO: this is a hack because we access the session directly. But this is also a function tied to Qts language string. Two wrongs... self.start() #This shows the GUI, or not, depends on the NSM gui save setting. We need to call that after the menu, otherwise the about dialog will block and then we get new menu entries, which looks strange. diff --git a/qtgui/songeditor.py b/qtgui/songeditor.py index 27e6b83..a70596a 100644 --- a/qtgui/songeditor.py +++ b/qtgui/songeditor.py @@ -985,11 +985,22 @@ class TrackLabelEditor(QtWidgets.QGraphicsScene): del item #yes, manual memory management in Python. We need to get rid of anything we want to delete later or Qt will crash because we have a python wrapper without a qt object + #Preare both on and off variants so we have a static string translation + stepDelayOnEntry = (QtCore.QCoreApplication.translate("TrackLabelContext", "Step Delay Wrap-Around: turn on"), lambda: api.changeTrackStepDelayWrapAround(exportDict["id"], True)) + stepDelayOffEntry = (QtCore.QCoreApplication.translate("TrackLabelContext", "Step Delay Wrap-Around: turn off"), lambda: api.changeTrackStepDelayWrapAround(exportDict["id"], False)) + stepDelayUse = stepDelayOffEntry if exportDict["stepDelayWrapAround"] else stepDelayOnEntry + + repeatDiminishedEntryOn = (QtCore.QCoreApplication.translate("TrackLabelContext", "Repeat Diminished Pattern in itself: turn on"), lambda: api.changeTrackRepeatDiminishedPatternInItself(exportDict["id"], True)) + repeatDiminishedEntryOff = (QtCore.QCoreApplication.translate("TrackLabelContext", "Repeat Diminished Pattern in itself: turn off"), lambda: api.changeTrackRepeatDiminishedPatternInItself(exportDict["id"], False)) + repeatDiminishedEntryUse = repeatDiminishedEntryOff if exportDict["repeatDiminishedPatternInItself"] else repeatDiminishedEntryOn + listOfLabelsAndFunctions = [ (exportDict["sequencerInterface"]["name"], None), (QtCore.QCoreApplication.translate("TrackLabelContext", "Invert Measures"), lambda: api.trackInvertSwitches(exportDict["id"])), (QtCore.QCoreApplication.translate("TrackLabelContext", "All Measures On"), lambda: api.trackOnAllSwitches(exportDict["id"])), (QtCore.QCoreApplication.translate("TrackLabelContext", "All Measures Off"), lambda: api.trackOffAllSwitches(exportDict["id"])), + stepDelayUse, + repeatDiminishedEntryUse, (QtCore.QCoreApplication.translate("TrackLabelContext", "Clone this Track"), lambda: api.createSiblingTrack(exportDict["id"])), (QtCore.QCoreApplication.translate("TrackLabelContext", "Delete Track"), lambda: api.deleteTrack(exportDict["id"])), ]