diff --git a/CHANGELOG b/CHANGELOG index 507df52..e6562b1 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,7 @@ 2019-10-15 Version 1.4 +New context option for track labels: Replace pattern with one from other track +Streamline existing context menu options for measure groups +New context options for measure group: duplicate and clean transpositions Activate the first pattern for all three tracks on a new project to ease up starting Set default drums track for new projects to use GM Drum note names and start with a good scale Use Median Velocity for new notes, not average. diff --git a/engine/api.py b/engine/api.py index 2f98a74..43783eb 100644 --- a/engine/api.py +++ b/engine/api.py @@ -171,7 +171,7 @@ from template.engine.api import callbacks _templateStartEngine = startEngine def updatePlayback(): - #TODO: use template.sequencer.py internal updates instead + #TODO: use template.sequencer.py internal updates instead cbox.Document.get_song().update_playback() def startEngine(nsmClient): @@ -232,7 +232,7 @@ def seek(value): cbox.Transport.seek_ppqn(value) ##Score -def set_quarterNotesPerMinute(value:int): +def set_quarterNotesPerMinute(value): if value is None: session.data.tempoMap.isTransportMaster = False #triggers rebuild elif value == "on": @@ -242,7 +242,7 @@ def set_quarterNotesPerMinute(value:int): else: assert value > 0 session.data.tempoMap.setQuarterNotesPerMinute(value) - session.data.tempoMap.isTransportMaster = True #triggers rebuild + session.data.tempoMap.isTransportMaster = True #triggers rebuild #Does not need track rebuilding updatePlayback() callbacks._quarterNotesPerMinuteChanged() @@ -419,6 +419,18 @@ def trackMergeCopyFrom(sourceTrackId, targetTrackId): targetTrack.buildTrack() updatePlayback() callbacks._trackStructureChanged(targetTrack) + +def trackPatternReplaceFrom(sourceTrackId, targetTrackId): + if not sourceTrackId == targetTrackId: + sourceTrack = session.data.trackById(sourceTrackId) + targetTrack = session.data.trackById(targetTrackId) + + copyPattern = sourceTrack.pattern.copy(newParentTrack = targetTrack) + targetTrack.pattern = copyPattern + + targetTrack.buildTrack() + updatePlayback() + callbacks._patternChanged(targetTrack) def setSwitchScaleTranspose(trackId, position, transpose): """Scale transposition is flipped. lower value means higher pitch""" @@ -448,6 +460,33 @@ def insertSilence(howMany, beforeMeasureNumber): session.data.buildAllTracks() updatePlayback() +def duplicateSwitchGroup(startMeasureForGroup:int, endMeasureExclusive:int): + groupSize = endMeasureExclusive-startMeasureForGroup + insertSilence(groupSize, endMeasureExclusive) + + for track in session.data.tracks: + for switch in range(startMeasureForGroup+groupSize, endMeasureExclusive+groupSize): #One group after the given one. + if switch-groupSize in track.structure: + track.structure.add(switch) + if switch-groupSize in track.whichPatternsAreScaleTransposed: + track.whichPatternsAreScaleTransposed[switch] = track.whichPatternsAreScaleTransposed[switch-groupSize] + if switch-groupSize in track.whichPatternsAreHalftoneTransposed: + track.whichPatternsAreHalftoneTransposed[switch] = track.whichPatternsAreHalftoneTransposed[switch-groupSize] + callbacks._trackStructureChanged(track) + session.data.buildAllTracks() + updatePlayback() + +def clearSwitchGroupTranspositions(startMeasureForGroup:int, endMeasureExclusive:int): + for track in session.data.tracks: + for switch in range(startMeasureForGroup, endMeasureExclusive): + if switch in track.whichPatternsAreScaleTransposed: + del track.whichPatternsAreScaleTransposed[switch] + if switch in track.whichPatternsAreHalftoneTransposed: + del track.whichPatternsAreHalftoneTransposed[switch] + callbacks._trackStructureChanged(track) + session.data.buildAllTracks() + updatePlayback() + def deleteSwitches(howMany, fromMeasureNumber): for track in session.data.tracks: new_structure = set() diff --git a/engine/pattern.py b/engine/pattern.py index 1e8ba5d..df8d75a 100644 --- a/engine/pattern.py +++ b/engine/pattern.py @@ -59,7 +59,7 @@ class Pattern(object): self.parentTrack = parentTrack self.scale = scale if scale else (72, 71, 69, 67, 65, 64, 62, 60) #Scale needs to be set first because on init/load data already depends on it, at least the default scale. The scale is part of the track meta callback. self.data = data if data else list() #For content see docstring. this cannot be the default parameter because we would set the same list for all instances. - self.simpleNoteNames = simpleNoteNames if simpleNoteNames else self.parentTrack.parentData.lastUsedNotenames #This is mostly for the GUI or other kinds of representation instead midi notes + self.simpleNoteNames = simpleNoteNames if simpleNoteNames else self.parentTrack.parentData.lastUsedNotenames[:] #This is mostly for the GUI or other kinds of representation instead midi notes self._processAfterInit() def _prepareBeforeInit(self): @@ -75,6 +75,14 @@ class Pattern(object): self._builtPatternCache = {} #holds a ready cbox pattern for a clip as value. Key is a tuple of hashable parameters. see self.buildPattern + def copy(self, newParentTrack): + """Return an independent copy of this pattern""" + data = [note.copy() for note in self.data] #list of mutable dicts. Dicts have only primitve data types inside + scale = self.scale #it is immutable so there is no risk of changing it in place for both patterns at once + simpleNoteNames = self.simpleNoteNames[:] #this mutable list always gets replaced completely by setting a new list, but we don't want to take any chances and create a slice copy. + result = Pattern(newParentTrack, data, scale, simpleNoteNames) + return result + @property def scale(self): return self._scale diff --git a/qtgui/resources/translations/de.qm b/qtgui/resources/translations/de.qm index 2bd3ed0..b83eae6 100644 Binary files a/qtgui/resources/translations/de.qm and b/qtgui/resources/translations/de.qm differ diff --git a/qtgui/resources/translations/de.ts b/qtgui/resources/translations/de.ts index 848d9cc..6902f52 100644 --- a/qtgui/resources/translations/de.ts +++ b/qtgui/resources/translations/de.ts @@ -194,12 +194,32 @@ Insert {} empty measures before no. {} - {} leere Takte vor Takt {} einfügen + {} leere Takte vor Takt {} einfügen Delete {} measures from no. {} on - Lösche {} Takte von Takt {} beginnend + Lösche {} Takte von Takt {} beginnend + + + + Insert empty group before this one + Leere Taktgruppe vor dieser einfügen + + + + Delete whole group + Lösche diese Taktgruppe + + + + Duplicate whole group including measures + Verdopple diese Taktgruppe inkl. Struktur + + + + Clear all group transpositions + Setze alle Transpositionen dieser Taktgruppe zurück @@ -324,12 +344,12 @@ TrackLabel - + grab and move to reorder tracks mit der maus halten und ziehen um Spuren anzuordnen - + change track color setze Farbe der Spur @@ -337,34 +357,44 @@ TrackLabelContext - + Invert Measures Taktauswahl umdrehen - + All Measures On Alle Takte anschalten - + All Measures Off Alle Takte ausschalten - + Clone this Track Spur klonen - + Delete Track Spur löschen Merge/Copy from - Übernimm Struktur von + Übernimm Struktur von + + + + Merge/Copy Measure-Structure from + Übernimm und ergänze Struktur von + + + + Replace Pattern with + Ersetze Noten des Taktes durch diff --git a/qtgui/songeditor.py b/qtgui/songeditor.py index 514b949..c3093bc 100644 --- a/qtgui/songeditor.py +++ b/qtgui/songeditor.py @@ -172,7 +172,7 @@ class SongEditor(QtWidgets.QGraphicsScene): barline.setPen(self.normalPen) class TrackStructure(QtWidgets.QGraphicsRectItem): - """From left to right. Holds two lines to show the "stafflinen" and a number of switches, + """From left to right. Holds two lines to show the "staffline" and a number of switches, colored rectangles to indicate where a pattern is activated on the timeline""" def __init__(self, parentScene): @@ -320,10 +320,15 @@ class TrackStructure(QtWidgets.QGraphicsRectItem): position = self.scenePos2switchPosition(event.scenePos().x()) #measure number 0 based measuresPerGroup = self.parentScene.measuresPerGroupCache + offset = position % measuresPerGroup + startMeasureForGroup = position - offset + endMeasureExclusive = startMeasureForGroup + measuresPerGroup + listOfLabelsAndFunctions = [ - - (QtCore.QCoreApplication.translate("SongStructure", "Insert {} empty measures before no. {}").format(measuresPerGroup, position+1), lambda: api.insertSilence(howMany=measuresPerGroup, beforeMeasureNumber=position)), - (QtCore.QCoreApplication.translate("SongStructure", "Delete {} measures from no. {} on").format(measuresPerGroup, position+1), lambda: api.deleteSwitches(howMany=measuresPerGroup, fromMeasureNumber=position)), + (QtCore.QCoreApplication.translate("SongStructure", "Insert empty group before this one").format(measuresPerGroup), lambda: api.insertSilence(howMany=measuresPerGroup, beforeMeasureNumber=startMeasureForGroup)), + (QtCore.QCoreApplication.translate("SongStructure", "Delete whole group").format(measuresPerGroup), lambda: api.deleteSwitches(howMany=measuresPerGroup, fromMeasureNumber=startMeasureForGroup)), + (QtCore.QCoreApplication.translate("SongStructure", "Duplicate whole group including measures"), lambda: api.duplicateSwitchGroup(startMeasureForGroup, endMeasureExclusive)), + (QtCore.QCoreApplication.translate("SongStructure", "Clear all group transpositions"), lambda: api.clearSwitchGroupTranspositions(startMeasureForGroup, endMeasureExclusive)), ] for text, function in listOfLabelsAndFunctions: @@ -592,8 +597,8 @@ class TrackLabelEditor(QtWidgets.QGraphicsScene): a.triggered.connect(function) #Add a submenu for merge/copy - mergeMenu = menu.addMenu(QtCore.QCoreApplication.translate("TrackLabelContext", "Merge/Copy from")) - + mergeMenu = menu.addMenu(QtCore.QCoreApplication.translate("TrackLabelContext", "Merge/Copy Measure-Structure from")) + def createCopyMergeLambda(srcId): return lambda: api.trackMergeCopyFrom(srcId, exportDict["id"]) @@ -606,6 +611,21 @@ class TrackLabelEditor(QtWidgets.QGraphicsScene): a.setEnabled(False) a.triggered.connect(mergeCommand) + #Add a submenu for pattern merge/copy + copyMenu = menu.addMenu(QtCore.QCoreApplication.translate("TrackLabelContext", "Replace Pattern with")) + + def replacePatternWithLambda(srcId): + return lambda: api.trackPatternReplaceFrom(srcId, exportDict["id"]) + + for track in self.tracks.values(): + sourceDict = track.exportDict + a = QtWidgets.QAction(sourceDict["sequencerInterface"]["name"], copyMenu) + copyMenu.addAction(a) + mergeCommand = replacePatternWithLambda(sourceDict["id"]) + if sourceDict["id"] == exportDict["id"]: + a.setEnabled(False) + a.triggered.connect(mergeCommand) + pos = QtGui.QCursor.pos() pos.setY(pos.y() + 5) self.parentView.parentMainWindow.setFocus()