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],
"Mixolydian": [0,0,0,0,0,0,-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, +1, +1, +1, 0, +1],
#"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, +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.
"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()
for l in schemesDict.values():
@ -830,7 +830,7 @@ def setScaleToKeyword(trackId, keyword):
result = []
oldPitch = 0
for p in r:
while p <= oldPitch:
while p < oldPitch:
p += 12
result.append(p)
oldPitch = p
@ -854,6 +854,44 @@ def changePatternVelocity(trackId, steps):
updatePlayback()
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.
def noteOn(trackId, row):

6
engine/main.py

@ -82,7 +82,7 @@ class Data(template.engine.sequencer.Score):
def convertSubdivisions(self, value, errorHandling):
"""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")
@ -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.
#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:
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)
for track in self.tracks:
for step in track.pattern.data:
@ -101,7 +101,7 @@ class Data(template.engine.sequencer.Score):
#Possible case, but needs checking.
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
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):
self._prepareBeforeInit()
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.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
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()
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.parentTrack.parentData.lastUsedNotenames = self._simpleNoteNames #new default for new tracks
@property
def numberOfSteps(self):
return len(self.scale)
def fill(self):
"""Create a 2 dimensional array"""
l = len(self.scale)
@ -258,7 +247,10 @@ class Pattern(object):
"""Called by the api directly and once on init/load"""
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["pitch"] = pattern["pitch"]
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
"pattern": self.pattern.exportCache,
"scale": self.pattern.scale,
"numberOfSteps": self.pattern.numberOfSteps,
"numberOfSteps": self.pattern.numberOfSteps, #pitches. Convenience for len(scale)
"simpleNoteNames": self.pattern.simpleNoteNames,
"numberOfMeasures": self.parentData.numberOfMeasures,
"whichPatternsAreScaleTransposed": self.whichPatternsAreScaleTransposed,

2
qtgui/mainwindow.py

@ -209,6 +209,7 @@ class MainWindow(TemplateMainWindow):
d[newCurrentTrackId].mark(True) #New one as active
self.patternGrid.guicallback_chooseCurrentTrack(exportDict, newCurrentTrackId)
self.transposeControls.guicallback_chooseCurrentTrack(exportDict, newCurrentTrackId)
#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!
@ -240,6 +241,7 @@ class MainWindow(TemplateMainWindow):
self.patternToolbar.addWidget(velocityControls)
transposeControls = TransposeControls(parentScene=self.patternGrid)
self.transposeControls = transposeControls
self.patternToolbar.addWidget(transposeControls)
#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._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.
self.backColor = QtGui.QColor(55, 61, 69)
@ -176,6 +176,7 @@ class PatternGrid(QtWidgets.QGraphicsScene):
self._fullRedraw(newCurrentTrackId)
def callback_patternChanged(self, exportDict, force=False):
"""We receive the whole track as exportDict.
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.
"""
#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
if force or exportDict["id"] == self.parentView.parentMainWindow.currentTrackId:
updateMode = self.parentView.viewportUpdateMode() #prevent iteration flickering from color changes.
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():
step.off()
@ -307,7 +320,10 @@ class PatternGrid(QtWidgets.QGraphicsScene):
x = event.scenePos().x()
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
if not row == self._lastRow:
@ -673,10 +689,13 @@ class Scale(QtWidgets.QGraphicsRectItem):
self.setNoteNames(exportDict["simpleNoteNames"])
self.setScale(exportDict["scale"])
def buildScale(self, numberOfSteps):
"""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)
@ -718,7 +737,7 @@ class Scale(QtWidgets.QGraphicsRectItem):
pitchWidget.spinBoxValueChanged() #change all current pitchWidgets
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()
trackId = self.parentScene.parentView.parentMainWindow.currentTrackId
if trackId: #startup check
@ -726,8 +745,12 @@ class Scale(QtWidgets.QGraphicsRectItem):
class TransposeControls(QtWidgets.QWidget):
"""
Created in mainwindow.py _populatePatternToolbar()
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]
#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
super().__init__()
self.exportDict = None
layout = QtWidgets.QHBoxLayout()
layout.setSpacing(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."))
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):
if index > 0:
index -= 1 # we inserted our "Set NoteNames to" at index 0 shifting the real values +1.

Loading…
Cancel
Save