diff --git a/engine/api.py b/engine/api.py index 0eaa1d4..9d50c78 100644 --- a/engine/api.py +++ b/engine/api.py @@ -37,6 +37,7 @@ class ClientCallbacks(Callbacks): #inherits from the templates api callbacks self.subdivisionsChanged = [] self.quarterNotesPerMinuteChanged = [] self.loopChanged = [] + self.loopMeasureFactorChanged = [] self.patternLengthMultiplicatorChanged = [] def _quarterNotesPerMinuteChanged(self): @@ -62,6 +63,12 @@ class ClientCallbacks(Callbacks): #inherits from the templates api callbacks for func in self.loopChanged: func(export) + def _loopMeasureFactorChanged(self): + """Very atomic callback. Used only for one value: how many measures are in one loop""" + export = session.data.loopMeasureFactor + for func in self.loopMeasureFactorChanged: + func(export) + def _timeSignatureChanged(self): nr = session.data.howManyUnits typ = session.data.whatTypeOfUnit @@ -205,6 +212,7 @@ def startEngine(nsmClient): callbacks._numberOfMeasuresChanged() callbacks._subdivisionsChanged() callbacks._quarterNotesPerMinuteChanged() + callbacks._loopMeasureFactorChanged() for track in session.data.tracks: callbacks._trackMetaDataChanged(track) #for colors, scale and simpleNoteNames @@ -227,12 +235,15 @@ def _loopNow(): now = cbox.Transport.status().pos_ppqn _setLoop(now) -def _setLoop(loopMeasureAroundPpqn): +def _setLoop(loopMeasureAroundPpqn:int): + """This function is used with context. + The loopFactor, how many measures are looped, is saved value """ if loopMeasureAroundPpqn < 0: _loopOff() return loopStart, loopEnd = session.data.buildSongDuration(loopMeasureAroundPpqn) + session.data._lastLoopStart = loopStart updatePlayback() session.inLoopMode = (loopStart, loopEnd) @@ -244,8 +255,18 @@ def _setLoop(loopMeasureAroundPpqn): oneMeasureInTicks = (session.data.howManyUnits * session.data.whatTypeOfUnit) / session.data.subdivisions measurenumber, rest = divmod(loopStart, oneMeasureInTicks) + callbacks._loopChanged(int(measurenumber), loopStart, loopEnd) +def setLoopMeasureFactor(newValue:int): + """How many measures are looped at once.""" + if newValue < 1: + newValue = 1 + session.data.loopMeasureFactor = newValue + callbacks._loopMeasureFactorChanged() + if session.inLoopMode: + _setLoop(session.data._lastLoopStart) + def toggleLoop(): """Plays the current measure as loop. Current measure is where the playback cursor is diff --git a/engine/main.py b/engine/main.py index 68eb267..d717605 100644 --- a/engine/main.py +++ b/engine/main.py @@ -56,22 +56,23 @@ class Data(template.engine.sequencer.Score): self.measuresPerGroup = 8 # meta data, has no effect on playback. self.subdivisions = 1 self.lastUsedNotenames = simpleNoteNames["English"] #The default value for new tracks/patterns. Changed each time the user picks a new representation via api.setNoteNames . noteNames are saved with the patterns. + self.loopMeasureFactor = 1 #when looping how many at once? #Create three tracks with their first pattern activated, so 'play' after startup already produces sounding notes. This is less confusing for a new user. - self.addTrack(name="Melody A", color="#ffff00") - self.addTrack(name="Bass A", color="#00ff00") + self.addTrack(name="Melody A", color="#ffff00") + self.addTrack(name="Bass A", color="#00ff00") self.addTrack(name="Drums A", color="#ff5500") self.tracks[0].structure=set((0,)) - self.tracks[1].structure=set((0,)) + self.tracks[1].structure=set((0,)) self.tracks[1].pattern.scale = (48, 47, 45, 43, 41, 40, 38, 36) #Low base notes, C-Major self.tracks[2].structure=set((0,)) self.tracks[2].pattern.simpleNoteNames = simpleNoteNames["Drums GM"] - self.tracks[2].pattern.scale = (49, 53, 50, 45, 42, 39, 38, 36) #A pretty good starter drum set + self.tracks[2].pattern.scale = (49, 53, 50, 45, 42, 39, 38, 36) #A pretty good starter drum set self._processAfterInit() def _processAfterInit(self): - pass + self._lastLoopStart = 0 #ticks. not saved def addTrack(self, name="", scale=None, color=None, simpleNoteNames=None): """Overrides the simpler template version""" @@ -149,7 +150,7 @@ class Data(template.engine.sequencer.Score): def buildAllTracks(self, buildSongDuration=False): - """Includes all patterns. + """Includes all patterns. buildSongDuration is True at least once in the programs life time, on startup. If True it will reset the loop. The api calls buildSongDuration directly when it sets @@ -163,7 +164,11 @@ class Data(template.engine.sequencer.Score): def buildSongDuration(self, loopMeasureAroundPpqn=None): """Loop does not reset automatically. We keep it until explicitely changed. - If we do not have a loop the song duration is already maxTrackDuration, no update needed.""" + If we do not have a loop the song duration is already maxTrackDuration, no update needed. + + An optional loopMeasureFactor can loop more than one measure. This is especially useful is + track multiplicators are used. This is a saved context value in self. + """ oneMeasureInTicks = (self.howManyUnits * self.whatTypeOfUnit) / self.subdivisions oneMeasureInTicks = int(oneMeasureInTicks) maxTrackDuration = self.numberOfMeasures * oneMeasureInTicks @@ -172,7 +177,7 @@ class Data(template.engine.sequencer.Score): else: loopMeasure = int(loopMeasureAroundPpqn / oneMeasureInTicks) #0 based start = loopMeasure * oneMeasureInTicks - end = start + oneMeasureInTicks + end = start + oneMeasureInTicks * self.loopMeasureFactor cbox.Document.get_song().set_loop(start, end) #set playback length for the entire score. Why is the first value not zero? That would create an actual loop from the start to end. 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. Zero length is stop. return start, end @@ -187,6 +192,7 @@ class Data(template.engine.sequencer.Score): "measuresPerGroup" : self.measuresPerGroup, "subdivisions" : self.subdivisions, "lastUsedNotenames" : self.lastUsedNotenames, + "loopMeasureFactor" : self.loopMeasureFactor, }) return dictionary @@ -199,6 +205,10 @@ class Data(template.engine.sequencer.Score): self.measuresPerGroup = serializedData["measuresPerGroup"] self.subdivisions = serializedData["subdivisions"] self.lastUsedNotenames = serializedData["lastUsedNotenames"] + if "loopMeasureFactor" in serializedData: #1.8 + self.loopMeasureFactor = serializedData["loopMeasureFactor"] + else: + self.loopMeasureFactor = 1 #Tracks depend on the rest of the data already in place because they create a cache on creation. super().copyFromSerializedData(parentSession, serializedData, self) #Tracks, parentSession and tempoMap @@ -213,6 +223,7 @@ class Data(template.engine.sequencer.Score): "numberOfMeasures" : self.numberOfMeasures, "measuresPerGroup" : self.measuresPerGroup, "subdivisions" : self.subdivisions, + "loopMeasureFactor" : self.loopMeasureFactor, "isTransportMaster" : self.tempoMap.export()["isTransportMaster"], } diff --git a/qtgui/designer/mainwindow.py b/qtgui/designer/mainwindow.py index 8449fa7..865b678 100644 --- a/qtgui/designer/mainwindow.py +++ b/qtgui/designer/mainwindow.py @@ -81,6 +81,11 @@ class Ui_MainWindow(object): self.loopButton.setShortcut("") self.loopButton.setObjectName("loopButton") self.horizontalLayout_2.addWidget(self.loopButton) + self.loopMeasureFactorSpinBox = QtWidgets.QSpinBox(self.widget_3) + self.loopMeasureFactorSpinBox.setMinimum(1) + self.loopMeasureFactorSpinBox.setMaximum(4096) + self.loopMeasureFactorSpinBox.setObjectName("loopMeasureFactorSpinBox") + self.horizontalLayout_2.addWidget(self.loopMeasureFactorSpinBox) self.toStartButton = QtWidgets.QPushButton(self.widget_3) self.toStartButton.setText("first") self.toStartButton.setShortcut("") diff --git a/qtgui/designer/mainwindow.ui b/qtgui/designer/mainwindow.ui index 174cca8..6123bfc 100644 --- a/qtgui/designer/mainwindow.ui +++ b/qtgui/designer/mainwindow.ui @@ -168,6 +168,16 @@ + + + + 1 + + + 4096 + + + diff --git a/qtgui/mainwindow.py b/qtgui/mainwindow.py index c333db2..cfc8690 100644 --- a/qtgui/mainwindow.py +++ b/qtgui/mainwindow.py @@ -106,6 +106,10 @@ class MainWindow(TemplateMainWindow): self.ui.loopButton.setText("") api.callbacks.loopChanged.append(callback_loopButtonText) + self.ui.loopMeasureFactorSpinBox.setFixedWidth(width) + self.ui.loopMeasureFactorSpinBox.setToolTip(QtCore.QCoreApplication.translate("PlaybackControls", "Number of measures in the loop")) + self.ui.loopMeasureFactorSpinBox.valueChanged.connect(self._sendLoopMeasureFactor) + api.callbacks.loopMeasureFactorChanged.append(self.ui.loopMeasureFactorSpinBox.setValue) #at least on load self.ui.toStartButton.setFixedWidth(width) self.ui.toStartButton.setText("") @@ -170,6 +174,10 @@ class MainWindow(TemplateMainWindow): #Here is the crowbar-method. self.nsmClient.announceSaveStatus(isClean = True) + def _sendLoopMeasureFactor(self, *args): + self.ui.loopMeasureFactorSpinBox.blockSignals(True) + api.setLoopMeasureFactor(self.ui.loopMeasureFactorSpinBox.value()) + self.ui.loopMeasureFactorSpinBox.blockSignals(False) def chooseCurrentTrack(self, exportDict): """This is in mainWindow because we need access to different sections of the program.