Browse Source

Add new feature to choose multiple measures to loop. Independent of track multiplicators

master
Nils 4 years ago
parent
commit
de49d42532
  1. 23
      engine/api.py
  2. 27
      engine/main.py
  3. 5
      qtgui/designer/mainwindow.py
  4. 10
      qtgui/designer/mainwindow.ui
  5. 8
      qtgui/mainwindow.py

23
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

27
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"],
}

5
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("")

10
qtgui/designer/mainwindow.ui

@ -168,6 +168,16 @@
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="loopMeasureFactorSpinBox">
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>4096</number>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="toStartButton">
<property name="text">

8
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.

Loading…
Cancel
Save