Browse Source

still adapting

master
Nils 6 years ago
parent
commit
0b1d55c615
  1. 118
      engine/api.py
  2. 85
      engine/main.py
  3. 242
      engine/pattern.py
  4. 79
      engine/track.py
  5. 306
      qtgui/mainwindow.py
  6. 4
      qtgui/pattern_grid.py
  7. 10
      qtgui/songeditor.py

118
engine/api.py

@ -1,12 +1,21 @@
#! /usr/bin/env python3
# -*- coding: utf-8 -*-
#Standard Library Modules
from typing import List, Set, Dict, Tuple
#Third Party Modules
from calfbox import cbox
#Template Modules
import template.engine.api #we need direct access to the module to inject data in the provided structures. but we also need the functions directly. next line:
from template.engine.api import *
DEFAULT_VELOCITY = 90
DEFAULT_FACTOR = 1
NUMBER_OF_STEPS = 8 #for exchange with the GUI: one octave of range per pattern. This is unlikely to change and then will not work immediately, but better to keep it as a named "constant".
#Our modules
from .pattern import NUMBER_OF_STEPS
DEFAULT_FACTOR = 1 #for the GUI.
#New callbacks
class ClientCallbacks(Callbacks): #inherits from the templates api callbacks
@ -16,7 +25,7 @@ class ClientCallbacks(Callbacks): #inherits from the templates api callbacks
self.timeSignatureChanged = []
self.scoreChanged = []
self.numberOfMeasuresChanged = []
self.numberOfTracksChanged = []
self.trackStructureChanged = []
self.trackMetaDataChanged = []
self.patternChanged = []
self.stepChanged = []
@ -30,16 +39,16 @@ class ClientCallbacks(Callbacks): #inherits from the templates api callbacks
"""There is one tempo for the entire song in quarter notes per mintue.
score.isTransportMaster to False means we do not create our own changes
and leave everything to the default. Negative values are not possible"""
if session.data.isTransportMaster:
export = session.data.quarterNotesPerMinute
if session.data.tempoMap.isTransportMaster:
export = session.data.tempoMap.getQuarterNotesPerMinute()
else:
export = None
for func in self.quarterNotesPerMinuteChanged:
func(export)
def _setPlaybackTicks(self):
def _setPlaybackTicks(self): #Differs from the template because it has subdivisions.
ppqn = cbox.Transport.status().pos_ppqn * session.data.subdivisions
status = _playbackStatus()
status = playbackStatus()
for func in self.setPlaybackTicks:
func(ppqn, status)
@ -117,6 +126,13 @@ class ClientCallbacks(Callbacks): #inherits from the templates api callbacks
for func in self.stepChanged:
func(index, pitch)
def _trackStructureChanged(self, track):
"""update one track structure. Does not export cbox.
Also includes transposition """
export = track.export()
for func in self.trackStructureChanged:
func(export)
def _numberOfTracksChanged(self):
"""sent the current track order as list of ids, combined with their structure.
This is also used when tracks get created or deleted, also on initial load.
@ -138,6 +154,9 @@ class ClientCallbacks(Callbacks): #inherits from the templates api callbacks
for func in self.numberOfMeasuresChanged:
func(export)
#Inject our derived Callbacks into the parent module
template.engine.api.callbacks = ClientCallbacks()
from template.engine.api import callbacks
@ -162,7 +181,8 @@ def startEngine(nsmClient):
callbacks._trackMetaDataChanged(track) #for colors, scale and noteNames
session.data.buildAllTracks()
_updatePlayback()
updatePlayback()
def toggleLoop():
@ -170,17 +190,17 @@ def toggleLoop():
Current measure is where the playback cursor is"""
if session.inLoopMode:
session.data.buildSongDuration() #no parameter removes the loop
_updatePlayback()
updatePlayback()
session.inLoopMode = None
callbacks._loopChanged(None, None, None)
else:
now = loopMeasureAroundPpqn=cbox.Transport.status().pos_ppqn
loopStart, loopEnd = session.data.buildSongDuration(now)
_updatePlayback()
updatePlayback()
session.inLoopMode = (loopStart, loopEnd)
assert loopStart <= now < loopEnd
if not _playbackStatus():
if not playbackStatus():
cbox.Transport.play()
oneMeasureInTicks = (session.data.howManyUnits * session.data.whatTypeOfUnit) / session.data.subdivisions
@ -204,17 +224,17 @@ def toStart():
##Score
def set_quarterNotesPerMinute(value):
if value is None:
session.data.isTransportMaster = False #triggers rebuild
session.data.tempoMap.isTransportMaster = False #triggers rebuild
elif value == "on":
assert not session.data.isTransportMaster
assert not session.data.tempoMap.isTransportMaster
#keep old bpm value
session.data.isTransportMaster = True #triggers rebuild
session.data.tempoMap.isTransportMaster = True #triggers rebuild
else:
assert value > 0
session.data.quarterNotesPerMinute = value #triggers rebuild
session.data.isTransportMaster = True #triggers rebuild
session.data.tempoMap.setQuarterNotesPerMinute(value)
session.data.tempoMap.isTransportMaster = True #triggers rebuild
#Does not need track rebuilding
_updatePlayback()
updatePlayback()
callbacks._quarterNotesPerMinuteChanged()
@ -222,21 +242,21 @@ def set_whatTypeOfUnit(ticks):
if session.data.whatTypeOfUnit == ticks: return
session.data.whatTypeOfUnit = ticks
session.data.buildAllTracks()
_updatePlayback()
updatePlayback()
callbacks._timeSignatureChanged()
def set_howManyUnits(value):
if session.data.howManyUnits == value: return
session.data.howManyUnits = value
session.data.buildAllTracks()
_updatePlayback()
updatePlayback()
callbacks._timeSignatureChanged()
def set_subdivisions(value):
if session.data.subdivisions == value: return
session.data.subdivisions = value
session.data.buildAllTracks()
_updatePlayback()
updatePlayback()
callbacks._subdivisionsChanged()
def convert_subdivisions(value, errorHandling):
@ -245,7 +265,7 @@ def convert_subdivisions(value, errorHandling):
result = session.data.convertSubdivisions(value, errorHandling)
if result:
session.data.buildAllTracks()
_updatePlayback()
updatePlayback()
callbacks._timeSignatureChanged() #includes subdivisions
for tr in session.data.tracks:
callbacks._patternChanged(tr)
@ -257,7 +277,7 @@ def set_numberOfMeasures(value):
if session.data.numberOfMeasures == value: return
session.data.numberOfMeasures = value
session.data.buildSongDuration()
_updatePlayback()
updatePlayback()
callbacks._numberOfMeasuresChanged()
callbacks._scoreChanged() #redundant but cheap and convenient
@ -269,7 +289,7 @@ def set_measuresPerGroup(value):
def changeTrackName(trackId, name):
track = session.data.trackById(trackId)
track.name = " ".join(name.split())
track.sequencerInterface.name = " ".join(name.split())
callbacks._trackMetaDataChanged(track)
def changeTrackColor(trackId, colorInHex):
@ -291,11 +311,11 @@ def createSiblingTrack(trackId):
(if any)"""
track = session.data.trackById(trackId)
assert type(track.pattern.scale) == tuple
newTrack = session.data.addTrack(name=track.name, scale=track.pattern.scale, color=track.color, noteNames=track.pattern.noteNames) #track name increments itself from "Track A" to "Track B" or "Track 1" -> "Track 2"
newTrack = session.data.addTrack(name=track.sequencerInterface.name, scale=track.pattern.scale, color=track.color, noteNames=track.pattern.noteNames) #track name increments itself from "Track A" to "Track B" or "Track 1" -> "Track 2"
newTrack.pattern.averageVelocity = track.pattern.averageVelocity
jackConnections = cbox.JackIO.get_connected_ports(track.cboxPortName())
jackConnections = cbox.JackIO.get_connected_ports(track.sequencerInterface.cboxPortName())
for port in jackConnections:
cbox.JackIO.port_connect(newTrack.cboxPortName(), port)
cbox.JackIO.port_connect(newTrack.sequencerInterface.cboxPortName(), port)
#Move new track to neighbour the old one.
oldIndex = session.data.tracks.index(track)
newIndex = session.data.tracks.index(newTrack)
@ -310,7 +330,7 @@ def deleteTrack(trackId):
session.data.deleteTrack(track)
if not session.data.tracks: #always keep at least one track
session.data.addTrack()
_updatePlayback()
updatePlayback()
callbacks._numberOfTracksChanged()
def moveTrack(trackId, newIndex):
@ -331,7 +351,7 @@ def setSwitches(trackId, setOfPositions, newBool):
else:
track.structure = track.structure.difference(setOfPositions) #remove everything from setOfPositions that is in the existing one, ignore entries from setOfPositions not in existing.
track.buildTrack()
_updatePlayback()
updatePlayback()
callbacks._trackStructureChanged(track)
def setSwitch(trackId, position, newBool):
@ -343,7 +363,7 @@ def setSwitch(trackId, position, newBool):
if not position in track.structure: return
track.structure.remove(position)
track.buildTrack()
_updatePlayback()
updatePlayback()
callbacks._trackStructureChanged(track)
return True
@ -359,21 +379,21 @@ def trackInvertSwitches(trackId):
new = set(i for i in range(session.data.numberOfMeasures))
track.structure = new.difference(track.structure)
track.buildTrack()
_updatePlayback()
updatePlayback()
callbacks._trackStructureChanged(track)
def trackOffAllSwitches(trackId):
track = session.data.trackById(trackId)
track.structure = set()
track.buildTrack()
_updatePlayback()
updatePlayback()
callbacks._trackStructureChanged(track)
def trackOnAllSwitches(trackId):
track = session.data.trackById(trackId)
track.structure = set(i for i in range(session.data.numberOfMeasures))
track.buildTrack()
_updatePlayback()
updatePlayback()
callbacks._trackStructureChanged(track)
def trackMergeCopyFrom(sourceTrackId, targetTrackId):
@ -384,7 +404,7 @@ def trackMergeCopyFrom(sourceTrackId, targetTrackId):
targetTrack.whichPatternsAreScaleTransposed.update(sourceTrack.whichPatternsAreScaleTransposed)
targetTrack.whichPatternsAreHalftoneTransposed.update(sourceTrack.whichPatternsAreHalftoneTransposed)
targetTrack.buildTrack()
_updatePlayback()
updatePlayback()
callbacks._trackStructureChanged(targetTrack)
def setSwitchScaleTranspose(trackId, position, transpose):
@ -392,7 +412,7 @@ def setSwitchScaleTranspose(trackId, position, transpose):
track = session.data.trackById(trackId)
track.whichPatternsAreScaleTransposed[position] = transpose
track.buildTrack()
_updatePlayback()
updatePlayback()
callbacks._trackStructureChanged(track)
return True
@ -401,7 +421,7 @@ def setSwitchHalftoneTranspose(trackId, position, transpose):
track = session.data.trackById(trackId)
track.whichPatternsAreHalftoneTransposed[position] = transpose
track.buildTrack()
_updatePlayback()
updatePlayback()
callbacks._trackStructureChanged(track)
return True
@ -413,7 +433,7 @@ def insertSilence(howMany, beforeMeasureNumber):
track.whichPatternsAreHalftoneTransposed = { (k+howMany if k >= beforeMeasureNumber else k):v for k,v in track.whichPatternsAreHalftoneTransposed.items() }
callbacks._trackStructureChanged(track)
session.data.buildAllTracks()
_updatePlayback()
updatePlayback()
def deleteSwitches(howMany, fromMeasureNumber):
for track in session.data.tracks:
@ -447,7 +467,7 @@ def deleteSwitches(howMany, fromMeasureNumber):
callbacks._trackStructureChanged(track)
session.data.buildAllTracks()
_updatePlayback()
updatePlayback()
#Pattern Steps
def setPattern(trackId, patternList):
@ -479,7 +499,7 @@ def setStep(trackId, stepExportDict):
track.pattern.buildExportCache()
track.buildTrack()
_updatePlayback()
updatePlayback()
callbacks._stepChanged(track, stepExportDict)
def removeStep(trackId, index, pitch):
@ -489,7 +509,7 @@ def removeStep(trackId, index, pitch):
track.pattern.data.remove(oldNote)
track.pattern.buildExportCache()
track.buildTrack()
_updatePlayback()
updatePlayback()
callbacks._removeStep(track, index, pitch)
def setScale(trackId, scale):
@ -499,7 +519,7 @@ def setScale(trackId, scale):
track.pattern.scale = scale
track.pattern.buildExportCache()
track.buildTrack()
_updatePlayback()
updatePlayback()
callbacks._trackMetaDataChanged(track)
def setNoteNames(trackId, noteNames):
@ -514,7 +534,7 @@ def transposeHalftoneSteps(trackId, steps):
track.pattern.scale = [midipitch+steps for midipitch in track.pattern.scale]
track.pattern.buildExportCache()
track.buildTrack()
_updatePlayback()
updatePlayback()
callbacks._trackMetaDataChanged(track)
def patternInvertSteps(trackId):
@ -522,7 +542,7 @@ def patternInvertSteps(trackId):
track.pattern.invert()
track.pattern.buildExportCache()
track.buildTrack()
_updatePlayback()
updatePlayback()
callbacks._patternChanged(track)
def patternOnAllSteps(trackId):
@ -530,7 +550,7 @@ def patternOnAllSteps(trackId):
track.pattern.fill()
track.pattern.buildExportCache()
track.buildTrack()
_updatePlayback()
updatePlayback()
callbacks._patternChanged(track)
def patternOffAllSteps(trackId):
@ -538,7 +558,7 @@ def patternOffAllSteps(trackId):
track.pattern.empty()
track.pattern.buildExportCache()
track.buildTrack()
_updatePlayback()
updatePlayback()
callbacks._patternChanged(track)
@ -583,7 +603,7 @@ def setScaleToKeyword(trackId, keyword):
track.pattern.scale = result
track.pattern.buildExportCache()
track.buildTrack()
_updatePlayback()
updatePlayback()
callbacks._trackMetaDataChanged(track)
def changePatternVelocity(trackId, steps):
@ -594,7 +614,7 @@ def changePatternVelocity(trackId, steps):
track.pattern.buildExportCache()
track.buildTrack()
_updatePlayback()
updatePlayback()
callbacks._patternChanged(track)
@ -603,11 +623,11 @@ def changePatternVelocity(trackId, steps):
def noteOn(trackId, row):
track = session.data.trackById(trackId)
midipitch = track.pattern.scale[row]
cbox.send_midi_event(0x90, midipitch, track.pattern.averageVelocity, output=track.cboxMidiOutUuid)
cbox.send_midi_event(0x90, midipitch, track.pattern.averageVelocity, output=track.sequencerInterface.cboxMidiOutUuid)
def noteOff(trackId, row):
track = session.data.trackById(trackId)
midipitch = track.pattern.scale[row]
cbox.send_midi_event(0x80, midipitch, track.pattern.averageVelocity, output=track.cboxMidiOutUuid)
cbox.send_midi_event(0x80, midipitch, track.pattern.averageVelocity, output=track.sequencerInterface.cboxMidiOutUuid)

85
engine/main.py

@ -30,6 +30,8 @@ from calfbox import cbox
#Template Modules
from template.engine.data import Data as TemplateData
import template.engine.sequencer
from template.engine.duration import D4
from template.engine.pitch import noteNames
#Our modules
from .track import Track
@ -47,36 +49,31 @@ class Data(template.engine.sequencer.Score):
traditional time signatures.
"""
def __init__(self, parentSession, howManyUnits=8, whatTypeOfUnit=D4, tracks = None, numberOfMeasures = 64, measuresPerGroup=8, subdivisions=1, isTransportMaster=False, quarterNotesPerMinute=120.0):
super().__init__(parentSession, tracks, tempoMap=None)
self.howManyUnits = howManyUnits
self.whatTypeOfUnit = whatTypeOfUnit
self.numberOfMeasures = numberOfMeasures
self.measuresPerGroup = measuresPerGroup # meta data, has no effect on playback.
self.subdivisions = subdivisions
def __init__(self, parentSession):
super().__init__(parentSession)
self.howManyUnits = 8
self.whatTypeOfUnit = D4
self.numberOfMeasures = 64
self.measuresPerGroup = 8 # meta data, has no effect on playback.
self.subdivisions = 1
self.lastUsedNotenames = noteNames["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.
if not tracks: #Empty / New project
self.tracks = []
self.addTrack(name="Melody A", color="#ffff00")
self.tracks[0].structure=set((0,)) #Already have the first pattern activated, so 'play' after startup already produces sounding notes. This is less confusing for a new user.
self.addTrack(name="Bass A", color="#00ff00")
self.addTrack(name="Drums A", color="#ff5500")
def trackById(self, trackId):
for track in self.tracks:
if trackId == id(track):
return track
raise ValueError(f"Track {trackId} not found. Current Tracks: {[id(tr) for tr in self.tracks]}")
self.addTrack(name="Melody A", color="#ffff00")
self.tracks[0].structure=set((0,)) #Already have the first pattern activated, so 'play' after startup already produces sounding notes. This is less confusing for a new user.
self.addTrack(name="Bass A", color="#00ff00")
self.addTrack(name="Drums A", color="#ff5500")
self._processAfterInit()
def _processAfterInit(self):
pass
def addTrack(self, name="", scale=None, color=None, noteNames=None):
"""Overrides the simpler template version"""
track = Track(parentScore=self, name=name, scale=scale, color=color, noteNames=noteNames)
self.tracks.append(track)
return track
def deleteTrack(self, track):
track.prepareForDeletion()
self.tracks.remove(track)
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."""
@ -177,43 +174,24 @@ class Data(template.engine.sequencer.Score):
"numberOfMeasures" : self.numberOfMeasures,
"measuresPerGroup" : self.measuresPerGroup,
"subdivisions" : self.subdivisions,
"lastUsedNotenames" : self.lastUsedNotenames,
})
return dictionary
@classmethod
def instanceFromSerializedData(cls, parentSession, serializedData):
self = cls(parentSession=parentSession,
howManyUnits=serializedData["howManyUnits"],
whatTypeOfUnit=serializedData["whatTypeOfUnit"],
tracks=True,
numberOfMeasures=serializedData["numberOfMeasures"],
measuresPerGroup=serializedData["measuresPerGroup"],
subdivisions=serializedData["subdivisions"],
isTransportMaster=serializedData["isTransportMaster"],
quarterNotesPerMinute=serializedData["quarterNotesPerMinute"],
)
self.tracks = []
for trackSrzData in serializedData["tracks"]:
track = Track(
parentScore=self,
name=trackSrzData["name"],
structure=set(trackSrzData["structure"]),
pattern= True, #fake. Filled in right after Track got created
color=trackSrzData["color"],
whichPatternsAreScaleTransposed=trackSrzData["whichPatternsAreScaleTransposed"],
whichPatternsAreHalftoneTransposed=trackSrzData["whichPatternsAreHalftoneTransposed"],
)
track.pattern = Pattern(parentTrack=track, data=trackSrzData["data"], scale=trackSrzData["scale"], noteNames=trackSrzData["noteNames"],)
self.tracks.append(track)
return self
@classmethod
def instanceFromSerializedData(cls, parentSession, serializedData):
self = super().instanceFromSerializedData(cls, parentSession, serializedData)
self = cls.__new__(cls)
self.howManyUnits = serializedData["howManyUnits"]
self.whatTypeOfUnit = serializedData["whatTypeOfUnit"]
self.numberOfMeasures = serializedData["numberOfMeasures"]
self.measuresPerGroup = serializedData["measuresPerGroup"]
self.subdivisions = serializedData["subdivisions"]
self.lastUsedNotenames = serializedData["lastUsedNotenames"]
#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
return self
def export(self):
return {
@ -222,6 +200,7 @@ class Data(template.engine.sequencer.Score):
"whatTypeOfUnit" : self.whatTypeOfUnit,
"numberOfMeasures" : self.numberOfMeasures,
"measuresPerGroup" : self.measuresPerGroup,
"subdivisions" : self.subdivisions,
"isTransportMaster" : self.tempoMap.export()["isTransportMaster"],
}

242
engine/pattern.py

@ -0,0 +1,242 @@
#! /usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Copyright 2018, Nils Hilbricht, Germany ( https://www.hilbricht.net )
This file is part of Patroneo ( https://www.laborejo.org )
Laborejo is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
#Standard Library Modules
from typing import List, Set, Dict, Tuple
from warnings import warn
#Third Party Modules
from calfbox import cbox
NUMBER_OF_STEPS = 8 #for exchange with the GUI: one octave of range per pattern. This is unlikely to change and then will not work immediately, but better to keep it as a named "constant".
DEFAULT_VELOCITY = 90
class Pattern(object):
"""A pattern can be in only one track.
In fact having it as its own object is only for code readability
A pattern is an unordered list of dicts. Each dicts is an step, or a note.
{"index":int 0-timesigLength, "factor": float, "pitch", int 0-7, "velocity":int 0-127}
The pitch is determined is by a scale, which is a list of len 7 of midi pitches. Our "pitch" is
an index in this list.
The scale works in screen coordinates(rows and columns). So usually the highest values comes first.
It is not possible to create overlapping sounds with different pitches.
We do force the correct order of on and off for the same pitch by cutting off the previous
note.
The pattern has always its maximum length.
"""
def __init__(self, parentTrack, data:List[dict]=None, scale:Tuple[int]=None, noteNames:List[str]=None):
self._prepareBeforeInit()
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.noteNames = noteNames if noteNames else self.parentTrack.parentScore.lastUsedNotenames #This is mostly for the GUI or other kinds of representation instead midi notes
self._processAfterInit()
def _prepareBeforeInit(self):
self._cachedTransposedScale = {}
def _processAfterInit(self):
self._tonalRange = range(-1*NUMBER_OF_STEPS+1, NUMBER_OF_STEPS)
self.averageVelocity = DEFAULT_VELOCITY # cached on each build
self._exportCacheVersion = 0 # increased each time the cache is renewed. Can be used to check for changes in the pattern itself.
self.exportCache = [] #filled in by self.buildExportCache. Used by parentTrack.export() to send to the GUI
self.buildExportCache()
self._builtPatternCache = {} #holds a ready cbox pattern for a clip as value. Key is a tuple of hashable parameters. see self.buildPattern
@property
def scale(self):
return self._scale
@scale.setter
def scale(self, value):
"""The scale can never be modified in place! Only replace it with a different list.
For that reason we keep scale an immutable tuple instead of a list"""
self._scale = tuple(value)
self.createCachedTonalRange()
@property
def noteNames(self):
return self._noteNames
@noteNames.setter
def noteNames(self, value):
self._noteNames = tuple(value) #we keep it immutable, this is safer to avoid accidental linked structures when creating a clone.
self.parentTrack.parentScore.lastUsedNotenames = self._noteNames #new default for new tracks
def fill(self):
"""Create a 2 dimensional array"""
l = len(self.scale)
lst = []
vel = self.averageVelocity
for index in range(self.parentTrack.parentScore.howManyUnits):
for pitchindex in range(l):
lst.append({"index":index, "factor": 1, "pitch": pitchindex, "velocity":vel})
self.data = lst
def empty(self):
self.data = []
def invert(self):
l = len(self.scale)
lst = []
existing = [(d["index"], d["pitch"]) for d in self.data]
vel = self.averageVelocity
for index in range(self.parentTrack.parentScore.howManyUnits):
for pitchindex in range(l):
if not (index, pitchindex) in existing:
lst.append({"index":index, "factor": 1, "pitch": pitchindex, "velocity":vel})
self.data = lst
def stepByIndexAndPitch(self, index, pitch):
for d in self.data:
if d["index"] == index and d["pitch"] == pitch:
return d
return None
def createCachedTonalRange(self):
"""Take the existing scale and generate all possible notes for it. This way we don't
need to regenerate them each time the pattern change.
This is meant to return a midi pitch. However, not all 128 are possible."""
#We create three full octaves because the code is much easier to write and read. no modulo, no divmod.
#We may not present all of them to the user.
self._cachedTransposedScale.clear()
for step in range(NUMBER_OF_STEPS):
self._cachedTransposedScale[step] = self.scale[step]
if not step+7 in self._cachedTransposedScale:
self._cachedTransposedScale[step+7] = self.scale[step] - 12 #yes, that is correct. We do top-bottom for our steps.
if not step-7 in self._cachedTransposedScale:
self._cachedTransposedScale[step-7] = self.scale[step] + 12
def buildExportCache(self):
"""Called by the api directly and once on init/load"""
self.exportCache = [] #only used by parentTrack.export()
assert self.scale and len(self.scale) == NUMBER_OF_STEPS #from constants
for pattern in (p for p in self.data if p["index"] < self.parentTrack.parentScore.howManyUnits): # < and not <= because index counts from 0 but howManyUnits counts from 1
note = {}
note["pitch"] = pattern["pitch"]
note["index"] = pattern["index"]
note["factor"] = pattern["factor"]
note["velocity"] = pattern["velocity"]
note["midipitch"] = self.scale[pattern["pitch"]] #we always report the untransposed note. For a GUI the pattern gets transposed, not the note.
note["exceedsPlayback"] = False #This is set by buildPattern.
self.exportCache.append(note)
self.averageVelocity = int( sum((n["velocity"] for n in self.data)) / len(self.data) ) if self.data else DEFAULT_VELOCITY
self._exportCacheVersion += 1 #used by build pattern for its cache hash
def buildPattern(self, scaleTransposition, halftoneTransposition, howManyUnits, whatTypeOfUnit, subdivisions):
"""return a cbox pattern ready to insert into a cbox clip.
This is the function to communicate with the outside, e.g. the track.
All ticks are relativ to the pattern start
We cache internally, but for the outside it looks like we generate a new pattern each time
on each little note update or transposition change.
The cache restores the cbox-pattern, not the note configuration in our python data. It is
used for placing the same pattern into multiple measures. But _not_ to return to a note
configuration we already had once. If you change the pattern itself the cache is cleared.
For that we keep the _exportCacheVersion around.
Actually we do not clear the cache, we simply let it sit.
#TODO: since exportCacheVersion is basically a very basic hash of the grid status we could use it to also cache buildExportCache. Profile on a slow system first!
Relevant for caching:
scaleTransposition
halftoneTransposition
howManyUnits
whatTypeOfUnit
subdivisions
scale (as tuple so it is hashable)
self._exportCacheVersion
_cachedTransposedScale is updated with self.scale changes and therefore already covered.
"""
cacheHash = (scaleTransposition, halftoneTransposition, howManyUnits, whatTypeOfUnit, subdivisions, tuple(self.scale), self._exportCacheVersion)
try:
return self._builtPatternCache[cacheHash]
except KeyError:
pass
oneMeasureInTicks = howManyUnits * whatTypeOfUnit
oneMeasureInTicks /= subdivisions #subdivisions is 1 by default. bigger values mean shorter values, which is compensated by the user setting bigger howManyUnits manually.
oneMeasureInTicks = int(oneMeasureInTicks)
exportPattern = bytes()
for noteDict in self.exportCache:
index = noteDict["index"]
startTick = index * whatTypeOfUnit
endTick = startTick + noteDict["factor"] * whatTypeOfUnit
startTick /= subdivisions
endTick /= subdivisions
startTick = int(startTick)
endTick = int(endTick)
if endTick > oneMeasureInTicks:
endTick = oneMeasureInTicks #all note off must end at the end of the pattern
noteDict["exceedsPlayback"] = True
else:
noteDict["exceedsPlayback"] = False
velocity = noteDict["velocity"]
pitch = self._cachedTransposedScale[noteDict["pitch"] + scaleTransposition] + halftoneTransposition
exportPattern += cbox.Pattern.serialize_event(startTick, 0x90, pitch, velocity) # note on
exportPattern += cbox.Pattern.serialize_event(endTick-1, 0x80, pitch, velocity-1) # note off #-1 ticks to create a small logical gap. Does not affect next note on.
pattern = cbox.Document.get_song().pattern_from_blob(exportPattern, oneMeasureInTicks)
self._builtPatternCache[cacheHash] = pattern
return pattern
#Save / Load / Export
def serialize(self)->dict:
return {
"scale": self.scale,
"data": self.data,
"noteNames": self.noteNames,
}
@classmethod
def instanceFromSerializedData(cls, parentTrack, serializedData):
self = cls.__new__(cls)
self._prepareBeforeInit()
self.parentTrack = parentTrack
#Use setters to trigger side effects like createCachedTonalRange()
self.scale = serializedData["scale"] #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 = serializedData["data"] #For content see docstring.
self.noteNames = serializedData["noteNames"] #This is mostly for the GUI or other kinds of representation instead midi notes
self._processAfterInit()
return self
#No export. Track uses pattern data directly in its own export.

79
engine/track.py

@ -21,6 +21,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
#Standard Library Modules
from typing import List, Set, Dict, Tuple
#Third Party Modules
@ -28,33 +29,35 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import template.engine.sequencer
#Our modules
from .pattern import Pattern
class Track(object): #injection at the bottom of this file!
"""The pattern is same as the track, even if the GUI does not represent it that way"""
"""The pattern is same as the track, even if the GUI does not represent it that way
def __repr__(self) -> str:
return f"Patroneo Track: {self.name}"
Init parameters are for cloned and copied tracks, but not for loading from json.
"""
#def __init__(self, parentScore, name="", structure=None, pattern=None, scale=None, color=None, whichPatternsAreScaleTransposed=None, whichPatternsAreHalftoneTransposed=None, noteNames=None):
def __init__(self, parentScore, name="", structure=None, pattern=None, scale=None, color=None, whichPatternsAreScaleTransposed=None, whichPatternsAreHalftoneTransposed=None, noteNames=None):
def __init__(self, parentScore,
name:str="",
structure:Set[int]=None,
scale:Tuple[int]=None,
color:str=None,
whichPatternsAreScaleTransposed:Dict[int,int]=None,
whichPatternsAreHalftoneTransposed:Dict[int,int]=None,
noteNames:List[str]=None):
print (self)
self.parentScore = parentScore
self.sequencerInterface = template.engine.sequencer.SequencerInterface(parentTrack=self, name=name) #needs parentScore
self.color = color if color else "#00FFFF" # "#rrggbb" in hex. no alpha. a convenience slot for the GUI to save a color.
#User data:
self.pattern = pattern if pattern else Pattern(parentTrack = self, scale=scale, noteNames=noteNames)
self.pattern = Pattern(parentTrack=self, scale=scale, noteNames=noteNames)
self.structure = structure if structure else set() #see buildTrack(). This is the main track data structure besides the pattern. Just integers (starts at 0) as switches which are positions where to play the patterns. In between are automatic rests.
self.whichPatternsAreScaleTransposed = whichPatternsAreScaleTransposed if whichPatternsAreScaleTransposed else {} #position:integers between -7 and 7. Reversed pitch, row based: -7 is higher than 7!!
self.whichPatternsAreHalftoneTransposed = whichPatternsAreHalftoneTransposed if whichPatternsAreHalftoneTransposed else {} #position:integers between -7 and 7. Reversed pitch, row based: -7 is higher than 7!!
self._processAfterInit()
if whichPatternsAreScaleTransposed:
self.whichPatternsAreScaleTransposed = {int(k):int(v) for k,v in whichPatternsAreScaleTransposed.items()} #json saves dict keys as strings
else:
self.whichPatternsAreScaleTransposed = {} #position:integers between -7 and 7. Reversed pitch, row based: -7 is higher than 7!!
if whichPatternsAreHalftoneTransposed:
self.whichPatternsAreHalftoneTransposed = {int(k):int(v) for k,v in whichPatternsAreHalftoneTransposed.items()} #json saves dict keys as strings
else:
self.whichPatternsAreHalftoneTransposed = {} #position:integers between -7 and 7. Reversed pitch, row based: -7 is higher than 7!!
def _processAfterInit(self):
pass
def buildTrack(self):
"""The goal is to create a cbox-track, consisting of cbox-clips which hold cbox-pattern,
@ -68,7 +71,7 @@ class Track(object): #injection at the bottom of this file!
oneMeasureInTicks = int(oneMeasureInTicks)
filteredStructure = [index for index in sorted(self.structure) if index < self.parentScore.numberOfMeasures] #not <= because we compare count with range
cboxclips = [o.clip for o in self.calfboxTrack.status().clips]
cboxclips = [o.clip for o in self.sequencerInterface.calfboxTrack.status().clips]
for cboxclip in cboxclips:
cboxclip.delete() #removes itself from the track
@ -76,42 +79,45 @@ class Track(object): #injection at the bottom of this file!
scaleTransposition = self.whichPatternsAreScaleTransposed[index] if index in self.whichPatternsAreScaleTransposed else 0
halftoneTransposition = self.whichPatternsAreHalftoneTransposed[index] if index in self.whichPatternsAreHalftoneTransposed else 0
cboxPattern = self.pattern.buildPattern(scaleTransposition, halftoneTransposition, self.parentScore.howManyUnits, self.parentScore.whatTypeOfUnit, self.parentScore.subdivisions)
r = self.calfboxTrack.add_clip(index*oneMeasureInTicks, 0, oneMeasureInTicks, cboxPattern) #pos, pattern-internal offset, length, pattern.
r = self.sequencerInterface.calfboxTrack.add_clip(index*oneMeasureInTicks, 0, oneMeasureInTicks, cboxPattern) #pos, pattern-internal offset, length, pattern.
######Old optimisations. Keep for later####
##########################################
#if changeClipsInPlace: #no need for track.buildTrack. Very cheap pattern exchange.
# cboxclips = [o.clip for o in self.parentTrack.calfboxTrack.status().clips]
# cboxclips = [o.clip for o in self.parentTrack.sequencerInterface.calfboxTrack.status().clips]
# for cboxclip in cboxclips:
# cboxclip.set_pattern(self.cboxPattern[cboxclip.patroneoScaleTransposed])
#Save / Load / Export
def serialize(self)->dict:
dictionary = super().serialize()
dictionary.update( { #update in place
return {
"sequencerInterface" : self.sequencerInterface.serialize(),
"color" : self.color,
"structure" : list(self.structure),
"data" : self.pattern.data,
"scale" : self.pattern.scale, #The scale is part of the track meta callback.
"noteNames" : self.pattern.noteNames, #The noteNames are part of the track meta callback.
"pattern" : self.pattern.serialize(),
"whichPatternsAreScaleTransposed" : self.whichPatternsAreScaleTransposed,
"whichPatternsAreHalftoneTransposed" : self.whichPatternsAreHalftoneTransposed,
})
return dictionary
}
@classmethod
def instanceFromSerializedData(cls, parentTrack, serializedData):
def instanceFromSerializedData(cls, parentScore, serializedData):
self = cls.__new__(cls)
self._name = serializedData["name"]
self.parentTrack = parentTrack
self.parentScore = parentTrack.parentScore
self.parentScore = parentScore
self.sequencerInterface = template.engine.sequencer.SequencerInterface.instanceFromSerializedData(self, serializedData["sequencerInterface"])
self.color = serializedData["color"]
self.structure = set(serializedData["structure"])
self.whichPatternsAreHalftoneTransposed = {int(k):int(v) for k,v in serializedData["whichPatternsAreHalftoneTransposed"].items()} #json saves dict keys as strings
self.whichPatternsAreScaleTransposed = {int(k):int(v) for k,v in serializedData["whichPatternsAreScaleTransposed"].items()} #json saves dict keys as strings
self.pattern = Pattern.instanceFromSerializedData(parentTrack=self, serializedData=serializedData["pattern"] )
self._processAfterInit()
return self
def export(self)->dict:
dictionary = super().export()
dictionary.update({
return {
"id" : id(self),
"sequencerInterface" : self.sequencerInterface.export(),
"color" : self.color,
"structure" : sorted(self.structure),
"pattern": self.pattern.exportCache,
@ -120,8 +126,7 @@ class Track(object): #injection at the bottom of this file!
"numberOfMeasures": self.parentScore.numberOfMeasures,
"whichPatternsAreScaleTransposed": self.whichPatternsAreScaleTransposed,
"whichPatternsAreHalftoneTransposed": self.whichPatternsAreHalftoneTransposed,
})
}
#Dependency Injections.
template.engine.sequencer.Score.TrackClass = Track #Score will look for Track in its module.

306
qtgui/mainwindow.py

@ -23,8 +23,10 @@ import logging; logging.info("import {}".format(__file__))
#Standard Library Modules
import os.path
#Third Party Modules
from PyQt5 import QtWidgets, QtCore, QtGui
#Template Modules
from template.qtgui.mainwindow import MainWindow as TemplateMainWindow
from template.qtgui.menu import Menu
@ -126,15 +128,317 @@ class MainWindow(TemplateMainWindow):
self.patternGrid = PatternGrid(parentView=self.ui.gridView)
self.ui.gridView.setScene(self.patternGrid)
#Toolbar, which needs the widgets above already established
self._populateToolbar()
#MainWindow Callbacks
api.callbacks.numberOfTracksChanged.append(self.callback_numberOfTracksChanged)
self.currentTrackId = None #this is purely a GUI construct. the engine does not know a current track. On startup there is no active track
self.start() #This shows the GUI, or not, depends on the NSM gui save setting. We need to call that after the menu, otherwise the about dialog will block and then we get new menu entries, which looks strange.
#There is always a track. Forcing that to be active is better than having to hide all the pattern widgets, or to disable them.
#However, we need the engine to be ready.
self.chooseCurrentTrack(api.session.data.tracks[0].export()) #By Grabthar's hammer, by the suns of Worvan, what a hack! #TODO: Access to the sessions data structure directly instead of api. Not good. Getter function or api @property is cleaner.
self.start() #This shows the GUI, or not, depends on the NSM gui save setting. We need to call that after the menu, otherwise the about dialog will block and then we get new menu entries, which looks strange.
def callback_numberOfTracksChanged(self, exportDictList):
"""We need to find out of the current track was the deleted one or if a new track got added
automatically."""
#if self.programStarted and len(exportDictList) == 1:
if len(exportDictList) == 1:
self.chooseCurrentTrack(exportDictList[0])
def chooseCurrentTrack(self, exportDict):
"""This is in mainWindow because we need access to different sections of the program.
newCurrentTrack is a backend track ID
This is not triggered by the engine but by our GUI functions. exportDict is not the current
engine data but a cached version. Careful! For example the pattern redraws when the
current track changes, but there was no callback that informed the songeditor of a changed
pattern because it doesn't deal with patterns. So it didn't receive the new exportDict and
sent its old cached version to the patternGrid via this function. So the grid never
got the new information and "forgot" all settings.
"""
newCurrentTrackId = exportDict["id"]
if self.currentTrackId == newCurrentTrackId:
return True
participantsDicts = (self.trackLabelEditor.tracks, self.songEditor.tracks) #all structures with dicts with key trackId that need active/inactive marking
for d in participantsDicts:
try: #First mark old one inactive
d[self.currentTrackId].mark(False)
except KeyError: #track was deleted or never existed (load empty file)
pass
d[newCurrentTrackId].mark(True) #New one as active
self.patternGrid.guicallback_chooseCurrentTrack(exportDict)
#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!
self.currentTrackId = newCurrentTrackId
def addTrack(self):
"""Add a new track and initialize it with some data from the current one"""
scale = api.session.data.trackById(self.currentTrackId).pattern.scale #TODO: Access to the sessions data structure directly instead of api. Not good. Getter function or api @property is cleaner.
api.addTrack(scale)
def cloneSelectedTrack(self):
"""Add a new track via the template option. This is the best track add function"""
newTrackExporDict = api.createSiblingTrack(self.currentTrackId)
self.chooseCurrentTrack(newTrackExporDict)
def _populateToolbar(self):
self.ui.toolBar.contextMenuEvent = api.nothing #remove that annoying Checkbox context menu
#Designer Buttons
self.ui.actionAddTrack.triggered.connect(self.addTrack)
self.ui.actionClone_Selected_Track.triggered.connect(self.cloneSelectedTrack)
#New Widgets
#BPM. Always in quarter notes to keep it simple
beatsPerMinuteBlock = QtWidgets.QWidget()
bpmLayout = QtWidgets.QHBoxLayout()
bpmLayout.setSpacing(0)
bpmLayout.setContentsMargins(0,0,0,0)
beatsPerMinuteBlock.setLayout(bpmLayout)
bpmCheckbox = QtWidgets.QCheckBox(QtCore.QCoreApplication.translate("Toolbar", "BPM/Tempo: "))
bpmCheckbox.setToolTip(QtCore.QCoreApplication.translate("Toolbar", "Deactivate to beccome JACK Transport Slave. Activate for Master."))
bpmLayout.addWidget(bpmCheckbox)
beatsPerMinute = QtWidgets.QSpinBox()
beatsPerMinute.setToolTip(bpmCheckbox.toolTip())
beatsPerMinute.setMinimum(0) #0 means off
beatsPerMinute.setMaximum(999)
beatsPerMinute.setSpecialValueText("JACK")
bpmLayout.addWidget(beatsPerMinute)
def quarterNotesPerMinuteChanged():
beatsPerMinute.blockSignals(True)
assert bpmCheckbox.isChecked()
value = beatsPerMinute.value()
api.set_quarterNotesPerMinute(max(1, value))
beatsPerMinute.blockSignals(False)
beatsPerMinute.editingFinished.connect(quarterNotesPerMinuteChanged)
def bpmCheckboxChanged(state):
bpmCheckbox.blockSignals(True)
if state:
api.set_quarterNotesPerMinute("on")
else:
api.set_quarterNotesPerMinute(None)
bpmCheckbox.blockSignals(False)
bpmCheckbox.stateChanged.connect(bpmCheckboxChanged)
def callback_quarterNotesPerMinuteChanged(newValue):
#We just receive an int, not a dict.
beatsPerMinute.blockSignals(True)
bpmCheckbox.blockSignals(True)
if newValue:
bpmCheckbox.setChecked(True)
beatsPerMinute.setEnabled(True)
beatsPerMinute.setValue(newValue)
else:
beatsPerMinute.setEnabled(False)
bpmCheckbox.setChecked(False)
beatsPerMinute.setValue(0)
beatsPerMinute.blockSignals(False)
bpmCheckbox.blockSignals(False)
api.callbacks.quarterNotesPerMinuteChanged.append(callback_quarterNotesPerMinuteChanged)
#Number of Measures
numberOfMeasures = QtWidgets.QSpinBox()
numberOfMeasures.setMinimum(1)
numberOfMeasures.setMaximum(9999)
numberOfMeasures.setToolTip(QtCore.QCoreApplication.translate("Toolbar", "Overall length of the song"))
def numberOfMeasuresChanged():
numberOfMeasures.blockSignals(True)
api.set_numberOfMeasures(int(numberOfMeasures.value()))
numberOfMeasures.blockSignals(False)
numberOfMeasures.editingFinished.connect(numberOfMeasuresChanged)
def callback_setnumberOfMeasures(exportDictScore):
numberOfMeasures.blockSignals(True)
numberOfMeasures.setValue(exportDictScore["numberOfMeasures"])
numberOfMeasures.blockSignals(False)
api.callbacks.numberOfMeasuresChanged.append(callback_setnumberOfMeasures)
#Subdivisions
#See manual
numberOfSubdivisions = QtWidgets.QSpinBox()
numberOfSubdivisions.setToolTip(QtCore.QCoreApplication.translate("Toolbar", "Please read the manual!"))
self.numberOfSubdivisions = numberOfSubdivisions #access from the convert dialog
numberOfSubdivisions.setMinimum(1)
numberOfSubdivisions.setMaximum(4)
def numberOfSubdivisionsChanged():
api.set_subdivisions(numberOfSubdivisions.value())
numberOfSubdivisions.editingFinished.connect(numberOfSubdivisionsChanged)
def callback_subdivisionsChanged(newValue):
numberOfSubdivisions.blockSignals(True)
numberOfSubdivisions.setValue(newValue)
numberOfSubdivisions.blockSignals(False)
api.callbacks.subdivisionsChanged.append(callback_subdivisionsChanged)
#Time Signature
unitTypes = [
QtCore.QCoreApplication.translate("TimeSignature", "Whole"),
QtCore.QCoreApplication.translate("TimeSignature", "Half"),
QtCore.QCoreApplication.translate("TimeSignature", "Quarter"),
QtCore.QCoreApplication.translate("TimeSignature", "Eigth"),
QtCore.QCoreApplication.translate("TimeSignature", "Sixteenth")
]
units = [api.D1, api.D2, api.D4, api.D8, api.D16]
howManyUnits = QtWidgets.QSpinBox()
howManyUnits.setMinimum(1)
howManyUnits.setMaximum(999)
howManyUnits.setToolTip(QtCore.QCoreApplication.translate("Toolbar", "Length of the pattern (bottom part of the program)"))
whatTypeOfUnit = QtWidgets.QComboBox()
whatTypeOfUnit.addItems(unitTypes)
#whatTypeOfUnit.setStyleSheet("QComboBox { background-color: transparent; } QComboBox QAbstractItemView { selection-background-color: transparent; } ");
whatTypeOfUnit.setToolTip(QtCore.QCoreApplication.translate("Toolbar", "How long is each main step"))
def typeChanged(index):
whatTypeOfUnit.blockSignals(True)
api.set_whatTypeOfUnit(units[index])
whatTypeOfUnit.blockSignals(False)
whatTypeOfUnit.currentIndexChanged.connect(typeChanged)
def numberChanged():
howManyUnits.blockSignals(True)
api.set_howManyUnits(howManyUnits.value())
howManyUnits.blockSignals(False)
howManyUnits.editingFinished.connect(numberChanged)
def callback_setTimeSignature(nr, typ):
howManyUnits.blockSignals(True)
whatTypeOfUnit.blockSignals(True)
idx = units.index(typ)
whatTypeOfUnit.setCurrentIndex(idx)
howManyUnits.setValue(nr)
howManyUnits.blockSignals(False)
whatTypeOfUnit.blockSignals(False)
api.callbacks.timeSignatureChanged.append(callback_setTimeSignature)
#Subdivisions Convert Button
convertSubdivisions = QtWidgets.QToolButton()
convertSubdivisions.setText(QtCore.QCoreApplication.translate("Toolbar", "Convert Grouping"))
convertSubdivisions.clicked.connect(self.convertSubdivisionsSubMenu)
convertSubdivisions.setToolTip(QtCore.QCoreApplication.translate("Toolbar", "Change step grouping but keep your music the same"))
#Add all to Toolbar
def spacer():
"""Insert a spacing widget. positions depends on execution order"""
spacer = QtWidgets.QWidget()
spacer.setFixedWidth(30)
self.ui.toolBar.addWidget(spacer)
#Clone Track and AddTrack button is added through Designer but we change the text here to get a translation
self.ui.actionClone_Selected_Track.setText(QtCore.QCoreApplication.translate("Toolbar", "Clone Selected Track"))
self.ui.actionClone_Selected_Track.setToolTip(QtCore.QCoreApplication.translate("Toolbar", "Use this! Create a new track that inherits everything but the content from the original. Already jack connected!"))
self.ui.actionAddTrack.setText(QtCore.QCoreApplication.translate("Toolbar", "Add Track"))
self.ui.actionAddTrack.setToolTip(QtCore.QCoreApplication.translate("Toolbar", "Add a complete empty track that needs to be connected to an instrument manually."))
spacer()
self.ui.toolBar.addWidget(beatsPerMinuteBlock) # combined widget with its label and translation included
spacer()
l = QtWidgets.QLabel(QtCore.QCoreApplication.translate("Toolbar", "Measures per Track: "))
l.setToolTip(numberOfMeasures.toolTip())
self.ui.toolBar.addWidget(l)
self.ui.toolBar.addWidget(numberOfMeasures)
spacer()
l = QtWidgets.QLabel(QtCore.QCoreApplication.translate("Toolbar", "Steps per Pattern:"))
l.setToolTip(howManyUnits.toolTip())
self.ui.toolBar.addWidget(l)
self.ui.toolBar.addWidget(howManyUnits)
l = QtWidgets.QLabel(QtCore.QCoreApplication.translate("Toolbar", " in groups of: "))
l.setToolTip(howManyUnits.toolTip())
self.ui.toolBar.addWidget(l)
self.ui.toolBar.addWidget(numberOfSubdivisions)
l = QtWidgets.QLabel(QtCore.QCoreApplication.translate("Toolbar", " so that each group produces a:"))
l.setToolTip(whatTypeOfUnit.toolTip())
self.ui.toolBar.addWidget(l)
self.ui.toolBar.addWidget(whatTypeOfUnit)
spacer()
self.ui.toolBar.addWidget(convertSubdivisions)
def zoomUpperHalf(self, delta):
"""This is called from within the parts of the combined song editor.
The Song Editor consists of three graphic scenes and their views.
But only the part where you can switch measures on and off calls this."""
try: self.zoomFactor
except: self.zoomFactor = 1 # no save. We don't keep a qt config.
if delta > 0: #zoom in
self.zoomFactor = min(5, round(self.zoomFactor + 0.25, 2))
else: #zoom out
self.zoomFactor = max(1, round(self.zoomFactor - 0.25, 2))
for view in (self.ui.songEditorView, self.ui.trackEditorView, self.ui.timelineView):
view.resetTransform()
self.ui.songEditorView.scale(self.zoomFactor, self.zoomFactor)
self.ui.trackEditorView.scale(1, self.zoomFactor)
self.ui.timelineView.scale(self.zoomFactor, 1)
#view.centerOn(event.scenePos())
def convertSubdivisionsSubMenu(self):
class Submenu(QtWidgets.QDialog):
def __init__(self, mainWindow):
super().__init__(mainWindow) #if you don't set the parent to the main window the whole screen will be the root and the dialog pops up in the middle of it.
#self.setModal(True) #we don't need this when called with self.exec() instead of self.show()
self.layout = QtWidgets.QFormLayout()
self.setLayout(self.layout)
self.numberOfSubdivisions = QtWidgets.QSpinBox()
self.numberOfSubdivisions.setMinimum(1)
self.numberOfSubdivisions.setMaximum(4)#
self.numberOfSubdivisions.setValue(mainWindow.numberOfSubdivisions.value())
self.layout.addRow(QtCore.QCoreApplication.translate("convertSubdivisionsSubMenu", "New Grouping"), self.numberOfSubdivisions)
self.errorHandling = QtWidgets.QComboBox()
self.errorHandling.addItems([
QtCore.QCoreApplication.translate("convertSubdivisionsSubMenu", "Do nothing"),
QtCore.QCoreApplication.translate("convertSubdivisionsSubMenu", "Delete wrong steps"),
QtCore.QCoreApplication.translate("convertSubdivisionsSubMenu", "Merge wrong steps"),
])
self.layout.addRow(QtCore.QCoreApplication.translate("convertSubdivisionsSubMenu", "If not possible"), self.errorHandling)
#self.setFocus(); #self.grabKeyboard(); #redundant for a proper modal dialog. Leave here for documentation reasons.
buttonBox = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel)
buttonBox.accepted.connect(self.accept)
buttonBox.rejected.connect(self.reject)
self.layout.addWidget(buttonBox)
def __call__(self):
"""This instance can be called like a function"""
self.exec() #blocks until the dialog gets closed
s = Submenu(self)
s()
if s.result():
value = s.numberOfSubdivisions.value()
error= ("fail", "delete", "merge")[s.errorHandling.currentIndex()]
api.convert_subdivisions(value, error)

4
qtgui/pattern_grid.py

@ -136,8 +136,6 @@ class PatternGrid(QtWidgets.QGraphicsScene):
"""
assert not exportDict["id"] == self.parentView.parentMainWindow.currentTrackId #this is still the old track.
#self.trackName.setText(exportDict["name"])
updateMode = self.parentView.viewportUpdateMode() #prevent iteration flickering from color changes.
self.parentView.setViewportUpdateMode(self.parentView.NoViewportUpdate)
@ -195,7 +193,7 @@ class PatternGrid(QtWidgets.QGraphicsScene):
"""
if force or self.parentView.parentMainWindow.currentTrackId == exportDict["id"]:
self.trackName.setText(exportDict["name"])
self.trackName.setText(exportDict["sequencerInterface"]["name"])
self.trackName.show()
c = QtGui.QColor(exportDict["color"])
self.currentColor = c

10
qtgui/songeditor.py

@ -116,7 +116,7 @@ class SongEditor(QtWidgets.QGraphicsScene):
self.removeItem(trackStructure) #remove from scene
del trackStructure
assert all(track.exportDict["index"] == self.trackOrder.index(track) for track in self.tracks.values())
assert all(track.exportDict["sequencerInterface"]["index"] == self.trackOrder.index(track) for track in self.tracks.values())
self.cachedCombinedTrackHeight = len(self.tracks) * SIZE_UNIT
self.setSceneRect(0,0,exportDict["numberOfMeasures"]*SIZE_UNIT,self.cachedCombinedTrackHeight + 3*SIZE_TOP_OFFSET) #no empty space on top without a scene rect. Also a bit of leniance.
self.playhead.setLine(0, 0, 0, self.cachedCombinedTrackHeight) #(x1, y1, x2, y2)
@ -571,7 +571,7 @@ class TrackLabelEditor(QtWidgets.QGraphicsScene):
del item #yes, manual memory management in Python. We need to get rid of anything we want to delete later or Qt will crash because we have a python wrapper without a qt object
listOfLabelsAndFunctions = [
(exportDict["name"], None),
(exportDict["sequencerInterface"]["name"], None),
(QtCore.QCoreApplication.translate("TrackLabelContext", "Invert Measures"), lambda: api.trackInvertSwitches(exportDict["id"])),
(QtCore.QCoreApplication.translate("TrackLabelContext", "All Measures On"), lambda: api.trackOnAllSwitches(exportDict["id"])),
(QtCore.QCoreApplication.translate("TrackLabelContext", "All Measures Off"), lambda: api.trackOffAllSwitches(exportDict["id"])),
@ -599,7 +599,7 @@ class TrackLabelEditor(QtWidgets.QGraphicsScene):
for track in self.tracks.values():
sourceDict = track.exportDict
a = QtWidgets.QAction(sourceDict["name"], mergeMenu)
a = QtWidgets.QAction(sourceDict["sequencerInterface"]["name"], mergeMenu)
mergeMenu.addAction(a)
mergeCommand = createCopyMergeLambda(sourceDict["id"])
if sourceDict["id"] == exportDict["id"]:
@ -734,7 +734,7 @@ class TrackLabel(QtWidgets.QGraphicsRectItem):
def sendToEngine(self):
self.setReadOnly(True)
new = self.text()
if not new == self.parentTrackLabel.exportDict["name"]:
if not new == self.parentTrackLabel.exportDict["sequencerInterface"]["name"]:
self.blockSignals(True)
api.changeTrackName(self.parentTrackLabel.exportDict["id"], new)
self.blockSignals(False)
@ -749,7 +749,7 @@ class TrackLabel(QtWidgets.QGraphicsRectItem):
def update(self, exportDict):
self.lineEdit.setText(exportDict["name"])
self.lineEdit.setText(exportDict["sequencerInterface"]["name"])
self.exportDict = exportDict
self.colorButton.setBrush(QtGui.QColor(exportDict["color"]))

Loading…
Cancel
Save