Browse Source

Fix all regressions and make old features compatible with new extended scales

master
Nils 3 years ago
parent
commit
66b69bce8a
  1. 46
      engine/api.py
  2. 6
      engine/main.py
  3. 18
      engine/pattern.py
  4. 2
      engine/track.py
  5. 2
      qtgui/mainwindow.py
  6. 57
      qtgui/pattern_grid.py

46
engine/api.py

@ -772,10 +772,10 @@ schemesDict = {
"Lydian": [0,0,0,+1,0,0,0], "Lydian": [0,0,0,+1,0,0,0],
"Mixolydian": [0,0,0,0,0,0,-1], "Mixolydian": [0,0,0,0,0,0,-1],
"Locrian": [0,-1,-1,0,-1,-1,-1], "Locrian": [0,-1,-1,0,-1,-1,-1],
"Blues": [0,-2,-1,0,-1,-2,-1], #blues is a special case. It has less notes than we offer. Set a full scale and another script will mute/hide those extra steps. #"Blues": [0,-2,-1,0,-1,-2,-1], #blues is a special case. It has less notes than we offer.
"Blues": [0, +1, +1, +1, 0, +1], "Blues": [0, +1, +1, +1, 0, +2, +1], #broden. Needs double octave in the middle. better than completely wrong.
"Hollywood": [0,0,0,0,0,-1,-1], #The "Hollywood"-Scale. Stargate, Lord of the Rings etc. "Hollywood": [0,0,0,0,0,-1,-1], #The "Hollywood"-Scale. Stargate, Lord of the Rings etc.
"Chromatic": [0,-1,-2,-2,-3,-4,-5,-5,-6, -7, -7, -8, -9, -10, -10], #crude... "Chromatic": [0,-1,-2,-2,-3,-4,-5,-5,-6, -7, -7, -8, -9, -10, -10], #crude... also broken > 2 octaves
} }
major.reverse() major.reverse()
for l in schemesDict.values(): for l in schemesDict.values():
@ -830,7 +830,7 @@ def setScaleToKeyword(trackId, keyword):
result = [] result = []
oldPitch = 0 oldPitch = 0
for p in r: for p in r:
while p <= oldPitch: while p < oldPitch:
p += 12 p += 12
result.append(p) result.append(p)
oldPitch = p oldPitch = p
@ -854,6 +854,44 @@ def changePatternVelocity(trackId, steps):
updatePlayback() updatePlayback()
callbacks._patternChanged(track) callbacks._patternChanged(track)
def resizePatternWithoutScale(trackId, steps):
"""Resize a patterns number of steps without changing the scale.
Can't go below 1 step.
Our editing end is the bottom one, where new steps are removed or added.
We also cannot know if there the user set a scale through an api scheme. At the very least
this needs analyzing and taking an educated guess. For now we just add notes a semitone below.
"""
if steps < 1 or steps > 128:
logger.warning(f"Pattern must have >= 1 and <= 127 steps but {steps} was requested. Doing nothing.")
return
track = session.data.trackById(trackId)
currentNr = track.pattern.numberOfSteps
oldid = id(track.pattern.scale)
s = track.pattern.scale #GUI view: from top to bottom. Usually from higher pitches to lower. (49, 53, 50, 45, 42, 39, 38, 36)
if steps == currentNr:
return
if steps < currentNr: #just reduce
track.pattern.scale = tuple(s[:steps])
else: #new
currentLowest = s[-1] #int
result = list(s) #can be edited.
for i in range(steps-currentNr):
currentLowest -= 1
result.append(currentLowest)
track.pattern.scale = tuple(result)
assert track.pattern.numberOfSteps == steps, (track.pattern.numberOfSteps, steps)
assert not oldid == id(track.pattern.scale)
track.pattern.buildExportCache()
track.buildTrack()
updatePlayback()
callbacks._patternChanged(track)
#Other functions. These can't be template functions because they use a specific track and Patroneos row and scale system. #Other functions. These can't be template functions because they use a specific track and Patroneos row and scale system.
def noteOn(trackId, row): def noteOn(trackId, row):

6
engine/main.py

@ -82,7 +82,7 @@ class Data(template.engine.sequencer.Score):
def convertSubdivisions(self, value, errorHandling): def convertSubdivisions(self, value, errorHandling):
"""Not only setting the subdivisions but also trying to scale existing notes up or down """Not only setting the subdivisions but also trying to scale existing notes up or down
proportinally. But only if possible.""" proportionally. But only if possible."""
assert errorHandling in ("fail", "delete", "merge") assert errorHandling in ("fail", "delete", "merge")
@ -92,7 +92,7 @@ class Data(template.engine.sequencer.Score):
#the easiest case. New value is bigger and a multiple of the old one. 1->everything, 2->4. #the easiest case. New value is bigger and a multiple of the old one. 1->everything, 2->4.
#We do need not check if the old notes have a place in the new grid because there are more new places than before #We do need not check if the old notes have a place in the new grid because there are more new places than before
if int(scaleFactor) == scaleFactor: if int(scaleFactor) == scaleFactor:
assert int(scaleFactor * self.howManyUnits) == scaleFactor * self.howManyUnits assert int(scaleFactor * self.howManyUnits) == scaleFactor * self.howManyUnits, (scaleFactor, self.howManyUnits)
self.howManyUnits = int(scaleFactor * self.howManyUnits) self.howManyUnits = int(scaleFactor * self.howManyUnits)
for track in self.tracks: for track in self.tracks:
for step in track.pattern.data: for step in track.pattern.data:
@ -101,7 +101,7 @@ class Data(template.engine.sequencer.Score):
#Possible case, but needs checking. #Possible case, but needs checking.
elif int(inverseScaleFactor) == inverseScaleFactor: elif int(inverseScaleFactor) == inverseScaleFactor:
assert int(scaleFactor * self.howManyUnits) == scaleFactor * self.howManyUnits assert int(scaleFactor * self.howManyUnits) == scaleFactor * self.howManyUnits, (scaleFactor, self.howManyUnits)
#Test, if in "fail" mode #Test, if in "fail" mode
if errorHandling == "fail": if errorHandling == "fail":

18
engine/pattern.py

@ -59,22 +59,11 @@ class Pattern(object):
def __init__(self, parentTrack, data:List[dict]=None, scale:Tuple[int]=None, simpleNoteNames:List[str]=None): def __init__(self, parentTrack, data:List[dict]=None, scale:Tuple[int]=None, simpleNoteNames:List[str]=None):
self._prepareBeforeInit() self._prepareBeforeInit()
self.parentTrack = parentTrack self.parentTrack = parentTrack
#self.scale = scale if scale else (72+ 12, 71+12, 69+12, 67+12, 65+12, 64+12, 62+12, 60+12, 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.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.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.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
assert self.simpleNoteNames assert self.simpleNoteNames
#Extended Pitch
#Explanation: Why don't we just do 12 steps per ocatve and then leave steps blank to create a scale?
#We still want the "User set steps, not pitch" idiom.
#self.mutedSteps = set()
#self.stepsPerOctave = 7 # This is not supposed to go below 7! e.g. Pentatonic scales are done by leaving steps out with self.blankSteps.
#self.nrOfSteps = 8 # Needs to be >= stepsPerOctave. stepsPerOctave+1 will, by default, result in a full scale plus its octave. That can of course later be changed.
#self.blankSteps = [] # Finally, some of the steps, relative to the octave, will be left blank and mute. This should create a visible gap in the GUI. Use this for pentatonic.
self._processAfterInit() self._processAfterInit()
def _prepareBeforeInit(self): def _prepareBeforeInit(self):
@ -120,11 +109,11 @@ class Pattern(object):
self._simpleNoteNames = tuple(value) #we keep it immutable, this is safer to avoid accidental linked structures when creating a clone. self._simpleNoteNames = tuple(value) #we keep it immutable, this is safer to avoid accidental linked structures when creating a clone.
self.parentTrack.parentData.lastUsedNotenames = self._simpleNoteNames #new default for new tracks self.parentTrack.parentData.lastUsedNotenames = self._simpleNoteNames #new default for new tracks
@property @property
def numberOfSteps(self): def numberOfSteps(self):
return len(self.scale) return len(self.scale)
def fill(self): def fill(self):
"""Create a 2 dimensional array""" """Create a 2 dimensional array"""
l = len(self.scale) l = len(self.scale)
@ -258,7 +247,10 @@ class Pattern(object):
"""Called by the api directly and once on init/load""" """Called by the api directly and once on init/load"""
self.exportCache = [] #only used by parentTrack.export() self.exportCache = [] #only used by parentTrack.export()
for pattern in (p for p in self.data if p["index"] < self.parentTrack.parentData.howManyUnits * self.parentTrack.patternLengthMultiplicator): # < and not <= because index counts from 0 but howManyUnits counts from 1 patternOnlyCurrentHowManyUnits = (p for p in self.data if p["index"] < self.parentTrack.parentData.howManyUnits * self.parentTrack.patternLengthMultiplicator) # < and not <= because index counts from 0 but howManyUnits counts from 1
patternOnlyCurrentNumberOfSteps = (p for p in patternOnlyCurrentHowManyUnits if p["pitch"] < self.numberOfSteps)
for pattern in patternOnlyCurrentNumberOfSteps:
note = {} note = {}
note["pitch"] = pattern["pitch"] note["pitch"] = pattern["pitch"]
note["index"] = pattern["index"] note["index"] = pattern["index"]

2
engine/track.py

@ -139,7 +139,7 @@ class Track(object): #injection at the bottom of this file!
"patternLengthMultiplicator" : self.patternLengthMultiplicator, #int "patternLengthMultiplicator" : self.patternLengthMultiplicator, #int
"pattern": self.pattern.exportCache, "pattern": self.pattern.exportCache,
"scale": self.pattern.scale, "scale": self.pattern.scale,
"numberOfSteps": self.pattern.numberOfSteps, "numberOfSteps": self.pattern.numberOfSteps, #pitches. Convenience for len(scale)
"simpleNoteNames": self.pattern.simpleNoteNames, "simpleNoteNames": self.pattern.simpleNoteNames,
"numberOfMeasures": self.parentData.numberOfMeasures, "numberOfMeasures": self.parentData.numberOfMeasures,
"whichPatternsAreScaleTransposed": self.whichPatternsAreScaleTransposed, "whichPatternsAreScaleTransposed": self.whichPatternsAreScaleTransposed,

2
qtgui/mainwindow.py

@ -209,6 +209,7 @@ class MainWindow(TemplateMainWindow):
d[newCurrentTrackId].mark(True) #New one as active d[newCurrentTrackId].mark(True) #New one as active
self.patternGrid.guicallback_chooseCurrentTrack(exportDict, newCurrentTrackId) self.patternGrid.guicallback_chooseCurrentTrack(exportDict, newCurrentTrackId)
self.transposeControls.guicallback_chooseCurrentTrack(exportDict, newCurrentTrackId)
#Remember current one for next round and for other functions #Remember current one for next round and for other functions
#Functions depend on getting set after getting called. They need to know the old track! #Functions depend on getting set after getting called. They need to know the old track!
@ -240,6 +241,7 @@ class MainWindow(TemplateMainWindow):
self.patternToolbar.addWidget(velocityControls) self.patternToolbar.addWidget(velocityControls)
transposeControls = TransposeControls(parentScene=self.patternGrid) transposeControls = TransposeControls(parentScene=self.patternGrid)
self.transposeControls = transposeControls
self.patternToolbar.addWidget(transposeControls) self.patternToolbar.addWidget(transposeControls)
#Finally add a spacer to center all widgets #Finally add a spacer to center all widgets

57
qtgui/pattern_grid.py

@ -66,7 +66,7 @@ class PatternGrid(QtWidgets.QGraphicsScene):
self._tracks = {} #tr-id:exportDict #kept up to date by various callbacks. self._tracks = {} #tr-id:exportDict #kept up to date by various callbacks.
self._zoomFactor = 1 # no save. We don't keep a qt config. self._zoomFactor = 1 # no save.
#Set color, otherwise it will be transparent in window managers or wayland that want that. #Set color, otherwise it will be transparent in window managers or wayland that want that.
self.backColor = QtGui.QColor(55, 61, 69) self.backColor = QtGui.QColor(55, 61, 69)
@ -176,6 +176,7 @@ class PatternGrid(QtWidgets.QGraphicsScene):
self._fullRedraw(newCurrentTrackId) self._fullRedraw(newCurrentTrackId)
def callback_patternChanged(self, exportDict, force=False): def callback_patternChanged(self, exportDict, force=False):
"""We receive the whole track as exportDict. """We receive the whole track as exportDict.
exportDict["pattern"] is the data structure example in the class docstring. exportDict["pattern"] is the data structure example in the class docstring.
@ -187,12 +188,24 @@ class PatternGrid(QtWidgets.QGraphicsScene):
to trigger a redraw even during the track change. to trigger a redraw even during the track change.
""" """
#Check if the number of steps is different before overwriting the old cached value
if exportDict["id"] == self.parentView.parentMainWindow.currentTrackId and not exportDict["numberOfSteps"] == self._tracks[exportDict["id"]]["numberOfSteps"]:
#print ("new steps", exportDict["numberOfSteps"], len(self._steps))
redrawStepsNeeded = True
else:
redrawStepsNeeded = False
#print ("same steps", exportDict["numberOfSteps"], len(self._steps))
#Cache anyway.
self._tracks[exportDict["id"]] = exportDict self._tracks[exportDict["id"]] = exportDict
if force or exportDict["id"] == self.parentView.parentMainWindow.currentTrackId: if force or exportDict["id"] == self.parentView.parentMainWindow.currentTrackId:
updateMode = self.parentView.viewportUpdateMode() #prevent iteration flickering from color changes. updateMode = self.parentView.viewportUpdateMode() #prevent iteration flickering from color changes.
self.parentView.setViewportUpdateMode(self.parentView.NoViewportUpdate) self.parentView.setViewportUpdateMode(self.parentView.NoViewportUpdate)
if redrawStepsNeeded:
self._redrawSteps(exportDict["patternBaseLength"], forceId=exportDict["id"]) #first parameter howMany is rhtyhm, not pitches. redraw fetches pitches itself.
for step in self._steps.values(): for step in self._steps.values():
step.off() step.off()
@ -307,7 +320,10 @@ class PatternGrid(QtWidgets.QGraphicsScene):
x = event.scenePos().x() x = event.scenePos().x()
inside = x > SIZE_RIGHT_OFFSET and x < self.sceneRect().width() - SIZE_RIGHT_OFFSET inside = x > SIZE_RIGHT_OFFSET and x < self.sceneRect().width() - SIZE_RIGHT_OFFSET
if ( row < 0 or row > 7 ) or not inside :
maxRow = self._tracks[self.parentView.parentMainWindow.currentTrackId]["numberOfSteps"]
if ( row < 0 or row >= maxRow ) or not inside :
row = None row = None
if not row == self._lastRow: if not row == self._lastRow:
@ -673,10 +689,13 @@ class Scale(QtWidgets.QGraphicsRectItem):
self.setNoteNames(exportDict["simpleNoteNames"]) self.setNoteNames(exportDict["simpleNoteNames"])
self.setScale(exportDict["scale"]) self.setScale(exportDict["scale"])
def buildScale(self, numberOfSteps): def buildScale(self, numberOfSteps):
"""This is used for building the GUI as well as switching the active track. """This is used for building the GUI as well as switching the active track.
We only add, show and hide. Never delete steps.""" We only add, show and hide. Never delete steps.
Before using self.pitchWidgets to derive a scale for the engine we need to filter out
hidden ones.
"""
stepsSoFar = len(self.pitchWidgets) stepsSoFar = len(self.pitchWidgets)
@ -718,7 +737,7 @@ class Scale(QtWidgets.QGraphicsRectItem):
pitchWidget.spinBoxValueChanged() #change all current pitchWidgets pitchWidget.spinBoxValueChanged() #change all current pitchWidgets
def sendToEngine(self, callback=True): def sendToEngine(self, callback=True):
result = [widget.spinBox.value() for widget in self.pitchWidgets] result = [widget.spinBox.value() for widget in self.pitchWidgets if widget.isVisible()] #hidden pitchwidgets are old ones that are not present in the current pattenrns scale.
#result.reverse() #result.reverse()
trackId = self.parentScene.parentView.parentMainWindow.currentTrackId trackId = self.parentScene.parentView.parentMainWindow.currentTrackId
if trackId: #startup check if trackId: #startup check
@ -726,8 +745,12 @@ class Scale(QtWidgets.QGraphicsRectItem):
class TransposeControls(QtWidgets.QWidget): class TransposeControls(QtWidgets.QWidget):
""" """
Created in mainwindow.py _populatePatternToolbar()
The row of widgets between the track structures and the patterns step grid The row of widgets between the track structures and the patterns step grid
Communication with the scale spinBoxes is done via api callbacks. We just fire and forget""" Communication with the scale spinBoxes is done via api callbacks. We just fire and forget
"""
#Not working. the translation generate works statically. translatedScales = [QtCore.QT_TRANSLATE_NOOP("Scale", scale) for scale in api.schemes] #Not working. the translation generate works statically. translatedScales = [QtCore.QT_TRANSLATE_NOOP("Scale", scale) for scale in api.schemes]
#No choice but to prepare the translations manually here. At least we do not need to watch for the order. #No choice but to prepare the translations manually here. At least we do not need to watch for the order.
@ -752,6 +775,8 @@ class TransposeControls(QtWidgets.QWidget):
self.parentScene = parentScene self.parentScene = parentScene
super().__init__() super().__init__()
self.exportDict = None
layout = QtWidgets.QHBoxLayout() layout = QtWidgets.QHBoxLayout()
layout.setSpacing(0) layout.setSpacing(0)
layout.setContentsMargins(0,0,0,0) layout.setContentsMargins(0,0,0,0)
@ -794,6 +819,26 @@ class TransposeControls(QtWidgets.QWidget):
self._comboBoxNoteNames.setToolTip(QtCore.QCoreApplication.translate("TransposeControls", "Use this scheme as note names.")) self._comboBoxNoteNames.setToolTip(QtCore.QCoreApplication.translate("TransposeControls", "Use this scheme as note names."))
layout.addWidget(self._comboBoxNoteNames) layout.addWidget(self._comboBoxNoteNames)
self._numberOfStepsChooser = QtWidgets.QSpinBox()
self._numberOfStepsChooser.setToolTip(QtCore.QCoreApplication.translate("TransposeControls", "Choose how many different notes does this pattern should have."))
self._numberOfStepsChooser.setMinimum(1)
self._numberOfStepsChooser.setMaximum(127)
self._numberOfStepsChooser.setSuffix(QtCore.QCoreApplication.translate("TransposeControls", " Notes"))
self._numberOfStepsChooser.valueChanged.connect(self._reactToNumberOfStepsChooser)
layout.addWidget(self._numberOfStepsChooser)
def guicallback_chooseCurrentTrack(self, exportDict, newCurrentTrackId):
"""Called by mainwindow chooseCurrentTrack()"""
self.exportDict = exportDict
self._numberOfStepsChooser.blockSignals(True)
self._numberOfStepsChooser.setValue(exportDict["numberOfSteps"])
self._numberOfStepsChooser.blockSignals(False)
def _reactToNumberOfStepsChooser(self):
self._numberOfStepsChooser.blockSignals(True)
api.resizePatternWithoutScale(self.exportDict["id"], self._numberOfStepsChooser.value())
self._numberOfStepsChooser.blockSignals(False)
def _changeNoteNamesByDropdown(self, index): def _changeNoteNamesByDropdown(self, index):
if index > 0: if index > 0:
index -= 1 # we inserted our "Set NoteNames to" at index 0 shifting the real values +1. index -= 1 # we inserted our "Set NoteNames to" at index 0 shifting the real values +1.

Loading…
Cancel
Save