# -*- coding: utf-8 -*- """ Copyright 2022, Nils Hilbricht, Germany ( https://www.hilbricht.net ) This file is part of Laborejo ( 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 . """ import logging; logger = logging.getLogger(__name__); logger.info("import") #Standard Library Modules from collections import deque from fractions import Fraction from weakref import WeakSet, WeakValueDictionary from warnings import warn #Template Modules from template.calfbox import cbox import template.engine.pitch as pitchmath import template.engine.duration as duration from template.engine.duration import DM, DB, DL, D1, D2, D4, D8, D16, D32, D64, D128, D256, D512, D1024, D_DEFAULT, D_STACCATO, D_TENUTO, D_TIE from template.helper import EndlessGenerator, flatList ########################## ## Part of other items ## ########################## class Note(object): """A note cannot be inserted in a track directly. It relies on a Chord structure. A note is not the level of operation. All note operations go through the notes parent Chord item. This is not only a design but is actually needed for undo. Only the Chord methods return their reverse operation for the undo system to register. for example note.octaveUp just sets self.pitch an octave up and returns None.""" allNotes = WeakValueDictionary() #key is the noteId, value is the weak reference to the Note. AND NOT CHORDS def __init__(self, parentChord, duration, pitch): self.pitch = pitch #a value from constants.py with octave modifier self.duration = duration #a Duration instance self.dynamic = Dynamic() self._secondInit(parentChord) def _secondInit(self, parentChord): """see Item._secondInit""" self.parentChord = parentChord self.rememberNote() @property def pitch(self): if self._pitch < 20: warn("Pitch {} lower than the lowest note detected. Exported normalized non-destructively to lowest possible note. Please raise pitch again manually before further editing.".format(self._pitch)) return 20 elif self._pitch > 3140: warn("Pitch {} higher than the highest note detected. Exported normalized non-destructively to highest possible note. Please raise pitch again manually before further editing.".format(self._pitch)) return 3140 else: return self._pitch @pitch.setter def pitch(self, value): self._pitch = value @classmethod def instanceFromSerializedData(cls, serializedObject, parentObject): """see Score.instanceFromSerializedData""" assert cls.__name__ == serializedObject["class"] self = cls.__new__(cls) self._pitch = int(serializedObject["pitch"]) self.duration = Duration.instanceFromSerializedData(serializedObject["duration"], parentObject = self) self.dynamic = Dynamic.instanceFromSerializedData(serializedObject["dynamic"], parentObject = self) self._secondInit(parentChord = parentObject) return self def serialize(self): result = {} result["class"] = "Note" result["duration"] = self.duration.serialize() result["pitch"] = self._pitch result["dynamic"] = self.dynamic.serialize() return result def rememberNote(self): oid = id(self) Note.allNotes[oid] = self #weakref_finalize(self, print, "deleted note "+str(oid)) return oid def copy(self, newParentChord): new = Note(newParentChord, self.duration.copy(), self._pitch) new.dynamic = self.dynamic.copy() return new def __lt__(self, other): # self < other return self.pitch < other.pitch def __le__(self, other): # self <= other return self.pitch <= other.pitch def __eq__(self, other): # self == other return self.pitch == other.pitch def __ne__(self, other): # self != other return self.pitch != other.pitch def __gt__(self, other): # self > other return self.pitch > other.pitch def __ge__(self, other): # self >= other return self.pitch >= other.pitch def asDotOnLine(self, clef): return pitchmath.distanceInDiatonicStepsFrom1720[self.pitch] + clef.asDotOnLineOffset def accidental(self, keysig): """-20, -10, 0, +10, +20 and just 1, which is Natural""" diff = pitchmath.diffToKey(self.pitch, keysig) #If there is a variation to the keysig. We need to find out if a white note has to get an accidental or an already altered noted (from the keysig) gets a natural sign. if diff and self.pitch == pitchmath.toWhite[self.pitch]: return 1 #natural elif diff == 0: #This just means it is the same as the keysig. return diff else: #it is different to the keysig. Traditionally the accidental shows the difference to white, not to the keysig. eis in C-minor is #e not Xe/##e return self.pitch - pitchmath.toWhite[self.pitch] #Inplace pitch modifications def sharpen(self): self.pitch = pitchmath.sharpen(self.pitch) def flatten(self): self.pitch = pitchmath.flatten(self.pitch) def intervalAutomatic(self, rootPitch, targetPitch): """Change the notepitch with the same interval that is between rootPitch and targetPitch""" self.pitch = pitchmath.intervalAutomatic(self.pitch, rootPitch, targetPitch) def mirrorAroundCursor(self, cursorPitch): self.pitch = pitchmath.mirror(self.pitch, axis=cursorPitch) #def toWhite(self): # self.pitch = pitch.toWhite[self.pitch] #def mirror(self, keysig, axis): # """Wants two pitches""" # self.pitch = pitch.mirror(self.pitch, axis) # self.toScale(keysig) def octaveUp(self): self.pitch = self.pitch + 350 def octaveDown(self): self.pitch = self.pitch - 350 def toScale(self, keysig): self.pitch = pitchmath.toScale(self.pitch, keysig) def stepUpInScale(self, keysig): self.pitch += 50 self.toScale(keysig) #adjust to keysig. def stepDownInScale(self, keysig): self.pitch -= 50 self.toScale(keysig) #adjust to keysig. def exportObject(self, trackState): dur = self.duration.completeDuration() on, off = self.duration.noteOnAndOff(trackState, dur) result = { "type" : "note", "id" : id(self), "accidental" : self.accidental(trackState.keySignature()), "dotOnLine" : self.asDotOnLine(trackState.clef()), "dots" : self.duration.dots, "tuplet" : self.duration.tuplet, "notehead" : self.duration.notehead, "completeDuration" : dur, #"leftModInTicks" : self.duration.startModInTicks(trackState), #only for gui. midi is in self.noteOnAndOff "leftModInTicks" : on, #"rightModInTicks" : self.duration.endModInTicks(trackState), #only for gui. midi is in self.noteOnAndOff "rightModInTicks" : off - dur, "tieDistanceInTicks" : dur, #because technically that is right. This note sounds until this value. "tie" : "", #empty, first, notFirst . notFirst is the end of a tie-sequence as well as a middle tie: c ~ [c] ~ c "manualOverride" : bool(self.duration.shiftStart or (self.duration.shiftEnd and not self.duration.durationKeyword == D_TIE)), "durationKeyword" : self.duration.durationKeyword, #an int. We internally use constants through module constants.D_KEYWORD "velocity" : self.dynamic.velocity(trackState), #int "velocityManualOverride" : bool(self.dynamic.velocityModification), "endTick" : trackState.tickindex, #for ties. "dynamicKeyword" : self.dynamic.dynamicKeyword, #int/"enum", defined in constants. One-time Dynamic Signature for sfz, fp and accents. } #Handle tied notes. if self.pitch in trackState.EXPORTtiedNoteExportObjectsWaitingForClosing: result["tie"] = "notFirst" if not self.duration.durationKeyword == D_TIE: #this was the last tied note, not a middle. trackState.EXPORTtiedNoteExportObjectsWaitingForClosing[self.pitch]["rightModInTicks"] = trackState.tickindex - trackState.EXPORTtiedNoteExportObjectsWaitingForClosing[self.pitch]["endTick"] + result["rightModInTicks"] trackState.EXPORTtiedNoteExportObjectsWaitingForClosing[self.pitch]["tieDistanceInTicks"] = trackState.tickindex - dur - trackState.EXPORTtiedNoteExportObjectsWaitingForClosing[self.pitch]["endTick"] + trackState.EXPORTtiedNoteExportObjectsWaitingForClosing[self.pitch]["completeDuration"] del trackState.EXPORTtiedNoteExportObjectsWaitingForClosing[self.pitch] #delete this pitch from the dict. Obviously this does not delete the exportObject. elif self.duration.durationKeyword == D_TIE: #since we already made sure this pitch is not waiting for a closing/middle tie but we are a tie we can be sure to be the first in a sequence. result["tie"] = "first" trackState.EXPORTtiedNoteExportObjectsWaitingForClosing[self.pitch] = result self._cachedExportObject = result return result def lilypond(self, carryLilypondRanges): """Called by chord.lilypond(carryLilypondRanges), See Item.lilypond for the general docstring. Returns two strings, pitch and duration. The duration is per-chord or needs special polyphony.""" return pitchmath.pitch2ly[self.pitch], self.duration.lilypond(carryLilypondRanges) class Dynamic(object): """dynamic means velocity midi terms. (but not volume and not expression). Absolute velocity can never go over 128 or below 0. 0 means mute. This is not a Dynamic Item which can be inserted into a track. Only notes have a working dynamic attribute. """ def __init__(self): self.velocityModification = 0 #signed int which will simply get added to the current dynamic signatures value. self.dynamicKeyword = 0 #TODO One-time Dynamic Signature for sfz, fp and accents. def _secondInit(self, parentNote): """see Item._secondInit""" pass @classmethod def instanceFromSerializedData(cls, serializedObject, parentObject): """see Score.instanceFromSerializedData""" assert cls.__name__ == serializedObject["class"] self = cls.__new__(cls) self.velocityModification = int(serializedObject["velocityModification"]) self.dynamicKeyword = int(serializedObject["dynamicKeyword"]) self._secondInit(parentNote = parentObject) return self def serialize(self): result = {} result["class"] = "Dynamic" result["velocityModification"] = self.velocityModification result["dynamicKeyword"] = self.dynamicKeyword return result def copy(self): new = Dynamic() new.velocityModification = self.velocityModification #int, needs no special copy new.dynamicKeyword = self.dynamicKeyword #int, needs no special copy return new def velocity(self, trackState): #todo: if keyword. see init base = trackState.dynamicSignature().baseVelocity(trackState) if trackState.dynamicRamp: #maybe override the baseVelocity. vel = trackState.dynamicRamp.getVelocity(trackState.tickindex - trackState.track.previousItem().logicalDuration() ) #TODO: The tickindex is already ahead of the current item. Somehow in the whole program that doesn't matter except for DynamicRamps. this is really strange. I don't know if this is a design error or one of the few exceptions. Actually, a rather prominent "exception" is the exportObject for every item which is state.tickindex - item.logicalDuration() and even has a comment. So I guess it is alright. if not vel is None: #0 is good. None means there is a DynamicRamp but not a target velocity. So we ignore it for now. base = vel value = int(base + self.velocityModification) if value < 0: return 0 elif value > 127: return 127 else: return value class Duration(object): """Don't be confused by the amount of durations in the score. Each item has a duration. A clef has a duration, including the default one in TrackState. A chord has a null-duration itself, because it is an Item, and also each note in the chord has its own duration. These are the real durations. There are multiple concepts of "duration" in the program: baseDuration: The note without dots, tuplets or other modifications. This is the basic note on a sheet of paper. completeDuration: baseDuration with dots and tuplets. But no shifting(staccato, manual mod etc.) noteOnAndOff: the final tick value for midi export. complete duration with all expression modifications like staccato, slurs or tenuto and user fine control logicalDuration: Value used to calculate the order and spacing of items. aka. determines how many ticks the cursor advances when parsing the item or how many pixels space are needed in a GUI before the next item. For normal chords this is the same as completeDuration. But this also includes values for MultiMeasure Rests and other special cases. Note that this is not the sounding duration from noteOnAndOff but the "original" duration without any interpretation or expression based modifications. The duration of a chord is called "durationGroup", which is it's own class. Why the name mismatch? Because all the data that matters to the outside gets exported to a static dict. For chords the logicalDuration is the shortest logical note. Example: A chord consists of a half note with staccato and a quarter note which is extended by quite a bit through manual shifting. The sounding quarter note is longer than the sounding half note. noteOnAndOff(half note) < noteOnAndOff(quarter). But the logical duration is still the completeDuration where a half note is always longer than a quarter note. """ def __init__(self, baseDuration): self._baseDuration = baseDuration #base value. Without dots, tuplets/times , overrides etc. this is the duration for all representations #DEPRECTATED. WILL BREAK SAVE FILES. self.tuplets = [] # a list of tuplets [(numerator, denominator), (,)...]. normal triplet is (2,3). Numerator is the level of the notehead. 2^n Each is one level of tuplets, arbitrary nesting depth. [2,3] for triplet # Tuplet multiplication is used for all representations and gets auto-merged on lilypond output to tuplet groups (if its in a chord). This is a list and not tuple() because json load will make it a list anyway. self.tuplet = None #a single tuple (numerator, denominator). There is no nesting in Laborejo. Numerator is the level of the notehead. 2^n Each is one level of tuplets, arbitrary nesting depth. [2,3] for triplet # Tuplet multiplication is used for all representations and gets auto-merged on lilypond output to tuplet groups (if its in a chord). self.dots = 0 #number of dots. self.durationKeyword = D_DEFAULT #The offsets shift the start and ending to the left and right or rather: earlier and later. positive values mean later, negative values mean earlier. self.shiftStart = 0 # in ticks self.shiftEnd = 0 # in ticks self._secondInit(parentItem = None) #see Item._secondInit def _secondInit(self, parentItem): """see Score._secondInit""" #discard parentItem self.genericNumber = self.calcGenericNumber() self.notehead = self.calcNotehead() #depends on self.genericNumber #self.cachedCompleteDuration = None def __setattr__(self, name, value): #print ("change!", self, name, value) super().__setattr__(name, value) super().__setattr__("cachedCompleteDuration", None) @classmethod def createByGuessing(cls, completeDuration): #class method. no self. """Return a new Duration. A constructor class method. Take one absolute tick value and guess what is the best duration combination (base, tuplets, dots...) for it. createByGuessing exists to distinguish between dotted notes and fractions. This means that the duration must be somehow musical precise and without fuzzyness. You can't shove arbitrary live-recorded duration values in here. It is not a quantizer""" def _closest(num, datalist): """only for positive numbers""" if num in datalist: return num else: positions = len(datalist) tmplist = [0] + sorted(datalist) tmplist.reverse() #Loop over all positions except the last 0. for x in range(positions): if tmplist[x+1] < num < tmplist[x]: return tmplist[x] else: #the highest value in datalist was smaller return None if completeDuration == int(completeDuration): completeDuration = int(completeDuration) durList = [D1024, D512, D256, D128, D64, D32, D16, D8, D4, D2, D1, DB, DL, DM] guessedBase = _closest(completeDuration, durList) if guessedBase: if completeDuration in durList: #no need for guessing new = cls(completeDuration) elif completeDuration/1.5 in durList: #single dot new = cls(completeDuration/1.5) new.dots = 1 elif completeDuration/1.5/1.5 in durList: #double dots new = cls(completeDuration/1.5/1.5) new.dots = 2 elif completeDuration *3/2 in durList: #triplets are so a common we take care of them instead of brute forcing with fractions in the else branch new = cls(guessedBase) new.tuplet = (2,3) else: #tuplet. That means the value is below a standard "notehead" duration. We need to find how much much lower. new = cls(guessedBase) #ratio = completeDuration / guessedBase #0.666~ for triplet newRatio = Fraction(int(completeDuration), guessedBase).limit_denominator(100000) #protects 6 or 7 decimal positions new.tuplet = (newRatio.numerator, newRatio.denominator) else: #the base could not be guessed. In this case we just create a non-standard note. warn("non-standard duration generated") new = cls(int(completeDuration)) #210, minimum base duration value. return new @classmethod def instanceFromSerializedData(cls, serializedObject, parentObject): """see Score.instanceFromSerializedData""" assert cls.__name__ == serializedObject["class"] self = cls.__new__(cls) self._baseDuration = int(serializedObject["baseDuration"]) if "tuplets" in serializedObject and serializedObject["tuplets"]: #old file format, which allowed nested tuplets. Was never used by anyone. #convert to new single-tuplet format and this will be gone next save. self.tuplet = serializedObject["tuplets"][0] #just take the first element. There was never a nested tuple, the gui never supported it. elif "tuplet" in serializedObject and serializedObject["tuplet"]: #Current file format. self.tuplet = tuple(serializedObject["tuplet"]) else: self.tuplet = None self.dots = int(serializedObject["dots"]) self.durationKeyword = int(serializedObject["durationKeyword"]) self.shiftStart = int(serializedObject["shiftStart"]) self.shiftEnd = int(serializedObject["shiftEnd"]) self._secondInit(parentItem = parentObject) return self def serialize(self): result = {} result["class"] = "Duration" result["baseDuration"] = self._baseDuration result["tuplet"] = self.tuplet result["dots"] = self.dots result["shiftStart"] = self.shiftStart result["shiftEnd"] = self.shiftEnd result["durationKeyword"] = self.durationKeyword return result @property def baseDuration(self): return self._baseDuration @baseDuration.setter def baseDuration(self, value): """implement limits""" if value < D1024: value = D1024 elif value > DM: value = DM self._baseDuration = value def copy(self): new = Duration(self.baseDuration) new.tuplet = self.tuplet new.dots = self.dots new.genericNumber = new.calcGenericNumber() new.notehead = new.calcNotehead() new.durationKeyword = self.durationKeyword new.shiftStart = self.shiftStart new.shiftEnd = self.shiftEnd return new def augment(self): self.baseDuration *= 2 self.genericNumber = self.calcGenericNumber() self.notehead = self.calcNotehead() def diminish(self): self.baseDuration /= 2 self.genericNumber = self.calcGenericNumber() self.notehead = self.calcNotehead() def calcGenericNumber(self): """A number for humans and lilypond. 4 = quarter, 2 = half""" return duration.baseDurationToTraditionalNumber[self.baseDuration] def calcNotehead(self): """What classical notehead would be used to express baseDuration""" if self.genericNumber >= 4: return 4 else: return self.genericNumber def completeDuration(self): """Ticks with dots and tuplets. But not leftMod rightMod x = basic note value , n = number of dots: 2x - x/2^n This function doesn't seem to be much. Why all the optimisation? Because it is called very very often. many times or so for a single note. """ if not self.baseDuration: return 0 if self.cachedCompleteDuration: return self.cachedCompleteDuration else: value = 2 * self.baseDuration - self.baseDuration / 2**self.dots if self.tuplet: numerator, denominator = self.tuplet value = value * numerator / denominator if not value == int(value): raise ValueError("Only integers are allowed as durations. {} is not {}.".format(value, int(value))) #TODO: leave this in until real world testing. We *could* live with floats for very complex tuplets, but let's see where this leads us. value = int(value) super().__setattr__("cachedCompleteDuration", value) return value def noteOnAndOff(self, trackState, completeDuration): """Midi only. return a tick value, the offset from an imaginary starting point on the tick/time axis, which needs to be added later. Think t=0 during this function. We do not export the actual duration since this function is intended for midi which only needs start and end points and calculates the duration itself.""" on = self.startModInTicks(trackState)(completeDuration) off = completeDuration + self.endModInTicks(trackState)(completeDuration) if on > off: warn("Your calculation resulted in a note off before note on. Please change it manually. Forced to standard note on/off.\n{}\n{}".format(self.startModInTicks(trackState), self.endModInTicks(trackState))) return 0, completeDuration else: return (on, off) def startModInTicks(self, trackState): """Modifications have different priority. A user created mod is always highest priority. Contexts like slurs or other instructions are next. Then follow individual duration keywords. There can only be one keyword at a time.""" if self.shiftStart: return lambda x: self.shiftStart elif trackState.duringLegatoSlur: return trackState.track.durationSettingsSignature.legatoOn elif self.durationKeyword == D_DEFAULT or self.durationKeyword == D_TIE: return trackState.track.durationSettingsSignature.defaultOn elif self.durationKeyword == D_STACCATO: return trackState.track.durationSettingsSignature.staccatoOn elif self.durationKeyword == D_TENUTO: return trackState.track.durationSettingsSignature.tenutoOn else: raise ValueError("Duration Keyword {} unknown".format(self.durationKeyword)) def endModInTicks(self, trackState): """see Duration.startMod""" if self.shiftEnd: return lambda x: self.shiftEnd elif trackState.duringLegatoSlur: if self.durationKeyword == D_STACCATO: #slur plus staccato cancels each other out return trackState.track.durationSettingsSignature.defaultOff else: return trackState.track.durationSettingsSignature.legatoOff elif self.durationKeyword == D_DEFAULT or self.durationKeyword == D_TIE: return trackState.track.durationSettingsSignature.defaultOff elif self.durationKeyword == D_STACCATO: return trackState.track.durationSettingsSignature.staccatoOff elif self.durationKeyword == D_TENUTO: return trackState.track.durationSettingsSignature.tenutoOff else: raise ValueError("Duration Keyword {} unknown".format(self.durationKeyword)) def toggleDurationKeyword(self, keywordConstant): """Activate a keyword for a note. If the note already has one, overwrite it. Except it already has the same keyword, then remove it""" if self.durationKeyword == keywordConstant: self.durationKeyword = D_DEFAULT else: self.durationKeyword = keywordConstant def toggleDot(self): if self.dots == 1: self.dots = 2 elif self.dots == 0: self.dots = 1 else: self.dots = 0 def toggleTriplet(self): if self.tuplet == (2,3): self.tuplet = None else: self.tuplet = (2,3) lilypondDurationKeywords = { D_TIE : "~", D_STACCATO : "-.", #shorthand for \staccato D_TENUTO : "--", } def lilypond(self, carryLilypondRanges): """Called by note.lilypond(carryLilypondRanges), See Item.lilypond for the general docstring. returns a number as string.""" if self.durationKeyword in Duration.lilypondDurationKeywords: append = Duration.lilypondDurationKeywords[self.durationKeyword] else: append = "" n = self.genericNumber if n == 0: return "\\breve" + self.dots*"." + append elif n == -1: return "\\longa" + self.dots*"." + append elif n == -2: return "\\maxima" + self.dots*"." + append else: return str(n) + self.dots*"." + append class DurationGroup(object): """Holds several durations and returns values meant for chords. Always up to date since notelist is a mutable data type. Is compatible to Duration() methods and parameters. Midi will send the actual note durations, per-note. For the representation (GUI) and logical calculations always the minimum note duration counts for the whole chord. """ def __init__(self, chord): self.chord = chord self.group = True self.minimumNote = None self.cacheMinimumNote() #the shortest note. set inplace. def cacheMinimumNote(self): work = [] for note in self.chord.notelist: work.append((note.duration.completeDuration(), note)) self.minimumNote = min(work)[1] #a list of tuplets (duration, note). The minimum is the one with the lowest duration, [1] is the note def templateDuration(self): """for chord addNote we need some template""" return self.minimumNote.duration.copy() def completeDuration(self): """Complete Musical Duration of this item.""" return self.minimumNote.duration.completeDuration() def hasTuplet(self): """Return true if there is any tuplet in any note""" return any(note.duration.tuplet for note in self.chord.notelist) @property def baseDuration(self): return self.minimumNote.duration.baseDuration @property def genericNumber(self): return duration.baseDurationToTraditionalNumber[self.baseDuration] @property def tuplet(self): """Return the tuplet of the shortest duration. Does *not* use self.hasTuplet() because that returns if any note has tuplet. We just want the logical shortest duration, not even the actual absolute shortes duration. In other words: the smallest notehead, does that have a tuplet?""" return self.minimumNote.duration.tuplet ############################################## ## Items ## ############################################## def createChordOrRest(completeDuration, pitchlist, velocityModification): """Return a Chord or Rest instance. The complete duration is the absolute duration after dots, tuplets etc. e.g. generated by midi in. Pitchlist can also be empty then it generates rests. But you have to explicitely set it to empty. This is used by the pattern generator which supports both rests and chords and works by absolute durations. """ durationObject = Duration.createByGuessing(completeDuration) dynamic = Dynamic() dynamic.velocityModification = velocityModification if pitchlist: #chord new = Chord(firstDuration=durationObject, firstPitch=pitchlist[0]) for pitch in pitchlist[1:]: new.addNote(pitch) for note in new.notelist: note.dynamic = dynamic.copy() else: new = Rest(durationObject) return new class Item(object): pseudoDuration = Duration(0) #any item has a duration. But we don't need a new pseudo duration instance for every clef and keysig. def __init__(self): self.duration = self.pseudoDuration self.notelist = [] #only in Chord. Here for compatibility reasons self.parentBlocks = WeakSet() #this is filled and manipulated by the Block Class through copy, split, contentLink etc. this takes also care of Item.copy(). Don't look in this file for anything parentBlocks related. #self._secondInit(parentBlock = None) #Items don't know their parent object. They are in a mutable list which can be in several containers. self.lilypondParameters = { # Only use basic data types! Not all lilypond parameters are used by every item. For clarity and editing we keep them all in one place. "visible" : True, "override" : "", "explicit" : False, } def _secondInit(self, parentBlock): """see Score._secondInit""" def deserializeDurationAndNotelistInPlace(self, serializedObject): """Part of instanceFromSerializedData. Any Item has a notelist and a duration, even if they are empty or never read. Needs a self, so the instance needs to be created. Call after self = cls.__new__(cls)""" self.parentBlocks = WeakSet() #inserted by the parent load method self.duration = Duration.instanceFromSerializedData(serializedObject["duration"], parentObject = self) #needs to be implemented in every childclass because we do not call Item.instanceFromSerializedData self.notelist = [Note.instanceFromSerializedData(note, parentObject = self) for note in serializedObject["notelist"]] self.lilypondParameters = serializedObject["lilypondParameters"] @classmethod def instanceFromSerializedData(cls, serializedObject, parentObject): """see Score.instanceFromSerializedData. This does not get called by a child class so you need to reimplement self.duration and other common parameters in every child class!""" assert cls.__name__ == serializedObject["class"] self = cls.__new__(cls) self.deserializeDurationAndNotelistInPlace(serializedObject) #creates parentBlocks self._secondInit(parentTrack = parentObject) return self def serialize(self): """Return a serialized data from this instance. Used for save, load and undo/redo. Can be called in a chain by subclasses with super().serialize The main difference between serialize and exportObject is that serialize does not compute anything. I just saves the state without calulating note on and off or stem directions, for example. """ #result = super().serialize() #call this in child classes result = {} result["class"] = self.__class__.__name__ result["duration"] = self.duration.serialize() result["notelist"] = [note.serialize() for note in self.notelist] result["lilypondParameters"] = self.lilypondParameters return result def _exportObject(self, trackState): """Implement this in every child class, return a dict. Base class function to return a dict which is a good layout export basis. For example for GUI frontends. They don't have to parse and calculate their own values in slow pure Python then midiBytes is a list with patternBlob/binary data generated by cbox.Pattern.serialize_event(position, midibyte1 (noteon), midibyte2(pitch), midibyte3(velocity)) Items with duration, like chords, will most like need to create at least two patternBlobs, one for the start (e.g. note on) and one for the stop (e.g. note off) """ raise NotImplementedError("Implement _exportObject() for this item type!") #And for documentation and education here a template exportObject duration = 0 return { "type" : "", "completeDuration" : duration, "tickindex" : trackState.tickindex - duration, #we parse the tickindex after we stepped over the item. "midiBytes" : [], "UIstring" : "", #this is for a UI, possibly a text UI, maybe for simple items of a GUI. Make it as short and unambigious as possible. } def exportObject(self, trackState): exportDict = { "id" : id(self), "lilypondParameters" : self.lilypondParameters, } exportDict.update(self._exportObject(trackState)) return exportDict @property def parentTracks(self): #return (block.parentTrack for block in self.parentBlocks[0]) return (block.parentTrack for block in list(self.parentBlocks)[0].linkedContentBlocksInScore()) @property def parentTrackIds(self): return (id(tr) for tr in self.parentTracks) def _copy(self): """return an independent copy of self""" raise NotImplementedError("Implement _copy() for this item type!") def copy(self): new = self._copy() new.lilypondParameters = self.lilypondParameters.copy() return new def copyParentBlocks(self, oldItem): """Move the oldItems parentBlocks to the new item""" #self.parentBlocks = oldItem.parentBlocks #TODO. We took that out when pasting after deleting a track and recreating failed. Why do we to copy the parentBlocks when a parentBlock is added during block.insert anyway? Wild guess: we don't. def logicalDuration(self): """Used for positioning items on the tickindex and subsequently midi export and GUI spacing. A chord can have logicalDuration D4 but one of it's notes can have a dot or a user duration mod. It will still use the logicalDuration of D4 to figure out where the next note is. From a midi POV: logicalDuration determines the next NoteOn. NoteOffs can be modified and are not used for positioning on the timeline """ return 0 def deserializeInplace(self, serializedObject): """Change this instance values to the values in the serialized object. Part of the undo/redo system""" return #Empty methods that allow for apply to selection and other iterators to work with items that cannot respond to a command. e.g. stepUp on a rest. #Api functions must react to these function returning None. For example Undo will not register anything when no undo-function is returned by an item-method. #We don't use functions(self, *args) and then a bunch of function1 = function2 = function3 because this way it is easier to see what methods are available (and what arguments they need) when creating new classes from Item. def sharpenNoteNearPitch(self, pitch): pass def flattenNoteNearPitch(self, pitch): pass def stepUpNoteNearPitch(self, pitch, keysig): pass def stepDownNoteNearPitch(self, pitch, keysig): pass def stepUpOctaveNoteNearPitch(self, pitch): pass def stepDownOctaveNoteNearPitch(self, pitch): pass def augmentNoteNearPitch(self, pitch): pass def diminishNoteNearPitch(self, pitch): pass def toggleDotNearPitch(self, pitch): pass def toggleTripletNearPitch(self, pitch): pass def setTupletNearPitch(self, pitch, tupletListForDuration): pass def toggleDurationKeywordNearPitch(self, pitch, keywordConstant): pass def moreVelocityNearPitch(self, pitch): pass def lessVelocityNearPitch(self, pitch): pass def moreDurationNearPitch(self, pitch): pass def lessDurationNearPitch(self, pitch): pass def resetVelocityAndDurationModsNearPitch(self, pitch): pass def sharpen(self): pass def flatten(self): pass def augment(self): pass def diminish(self): pass def toggleDot(self): pass def toggleTriplet(self): pass def setTuplet(self, durationTupletListForEachNote): pass def toggleDurationKeyword(self, listOfKeywordConstants): pass def stepUp(self, keysigList): pass def stepDown(self, keysigList): pass def stepUpOctave(self): pass def stepDownOctave(self): pass def intervalAutomatic(self, rootPitch, targetPitch): pass def split(self, newparts): pass def addNote(self, pitch): pass def deleteNote(self, note): pass def removeNoteNearPitch(self, pitch): pass def moreVelocity(self): pass def lessVelocity(self): pass def moreDuration(self): pass def lessDuration(self): pass def resetVelocityAndDurationMods(self): pass def toggleBeam(self): pass def removeBeam(self): pass def midiRelativeChannelPlus(self): pass def midiRelativeChannelMinus(self): pass def midiRelativeChannelReset(self): pass def mirrorAroundCursor(self, cursorPitch): pass def lilypond(self, carryLilypondRanges): if self.lilypondParameters["override"]: return self.lilypondParameters["override"] else: return self._lilypond(carryLilypondRanges) def _lilypond(self, carryLilypondRanges): """called by block.lilypond(carryLilypondRanges), returns a string. Don't create white-spaces yourself, this is done by the structures. When in doubt prefer functionality and robustness over 'beautiful' lilypond syntax.""" return "" class TemplateItem(Item): """Use this with copy and paste to create a new item class""" def __init__(self, itemData): """Either init gets called, by creation during runtime, or instanceFromSerializedData. That means everything in init must be matched by a loading call in instanceFromSerializedData.""" super().__init__() raise NotImplementedError("Implement __init__() for this item type!") self.itemData = itemData #matches instanceFromSerializedData and serialize self._secondInit(parentBlock = None) #see Item._secondInit. def _secondInit(self, parentBlock): """After creating a new item during runtime or after loading from a file this gets called. Put every calcuation that depends on values that could be changed by the user. see Item._secondInit""" super()._secondInit(parentBlock) #Item._secondInit @classmethod def instanceFromSerializedData(cls, serializedObject, parentObject): """see Score.instanceFromSerializedData""" raise NotImplementedError("Implement instanceFromSerializedData() for this item type!") assert cls.__name__ == serializedObject["class"] self = cls.__new__(cls) self.itemData = serializedObject["itemData"] self.deserializeDurationAndNotelistInPlace(serializedObject) self._secondInit(parentBlock = parentObject) return self def serialize(self): raise NotImplementedError("Implement serialize() for this item type!") result = super().serialize() #call this in child classes result["itemData"] = self.itemData return result def _copy(self): """return an independent copy of self""" raise NotImplementedError("Implement _copy() for this item type!") new = TemplateItem(self.itemData) return new def _exportObject(self, trackState): """Implement this in every child class, return a dict. Base class function to return a dict which is a good layout export basis. For example for GUI frontends. They don't have to parse and calculate their own values in slow pure Python then midiBytes is a list with patternBlob/binary data generated by cbox.Pattern.serialize_event(position, midibyte1 (noteon), midibyte2(pitch), midibyte3(velocity)) Items with duration, like chords, will most like need to create at least two patternBlobs, one for the start (e.g. note on) and one for the stop (e.g. note off) """ raise NotImplementedError("Implement _exportObject() for this item type!") #And for documentation and education here a template exportObject duration = 0 return { "type" : "", "completeDuration" : duration, "tickindex" : trackState.tickindex - duration, #we parse the tickindex after we stepped over the item. "midiBytes" : [], "UIstring" : "", #this is for a UI, possibly a text UI, maybe for simple items of a GUI. Make it as short and unambigious as possible. } def _lilypond(self, carryLilypondRanges): """called by block.lilypond(carryLilypondRanges), returns a string. Don't create white-spaces yourself, this is done by the structures. When in doubt prefer functionality and robustness over 'beautiful' lilypond syntax.""" return "" class Chord(Item): """Duration and firstPitch are for the first note. You must only modify the notelist through Chord methods. The chord methods keep the list sorted and also keep track what is the shortest and longest note, and other meta-data. Replacing .notelist or using list methods like .append directly will break the whole program. Especially the lowest and shortest notes are used multiple times on every item export! """ def __init__(self, firstDuration, firstPitch): super().__init__() firstNote = Note(self, firstDuration, firstPitch) self.notelist = [firstNote] #from lowest to highest self._beamGroup = False # Works a bit like legato slurs, only this is a property of a note, not an item. self.beamGroup is a @property that self-corrects against errors. self._secondInit(parentBlock = None) #see Item._secondInit. self.midiChannelOffset = 0 #from -15 to 15. In reality much less. Expected values are -3 to +3 def _secondInit(self, parentBlock): """see Item._secondInit""" super()._secondInit(parentBlock) #Item._secondInit self.durationGroup = DurationGroup(self) #durationGroup is a dynamic data type and therefore does not need serialize. The serialized durations are in the notelist. self._cachedClefForLedgerLines = None @classmethod def instanceFromSerializedData(cls, serializedObject, parentObject): """see Score.instanceFromSerializedData""" assert cls.__name__ == serializedObject["class"] self = cls.__new__(cls) self.deserializeDurationAndNotelistInPlace(serializedObject) self._beamGroup = bool(serializedObject["beamGroup"]) self.midiChannelOffset = serializedObject["midiChannelOffset"] self._secondInit(parentBlock = parentObject) return self def serialize(self): result = super().serialize() #call this in child classes result["beamGroup"] = self._beamGroup result["midiChannelOffset"] = self.midiChannelOffset #durationGroup is a dynamic data type and gets its values from its parent chord: self. return result def logicalDuration(self): """Return the logical duration that is used to calculate the space and subsequent position of items.""" #TODO: this gets called pretty often. 6 times per note insert on a previously empty track. Why?! return self.durationGroup.completeDuration() def _copy(self): new = Chord(Duration(0), 0) new.copyParentBlocks(self) new._beamGroup = self._beamGroup new.notelist = [] for note in self.notelist: new.notelist.append(note.copy(new)) new.durationGroup.cacheMinimumNote() return new def pitchlist(self): return [note.pitch for note in self.notelist] #needs to be subscriptable def whitePitchlist(self): result = [] for note in self.notelist: result.append(pitchmath.toWhite[note.pitch]) return result def _setNotelist(self, notelist): oldNotelist = self.notelist.copy() self.notelist = notelist self.durationGroup.cacheMinimumNote() return lambda: self._setNotelist(oldNotelist) def replaceNoteList(self, notelist): """Wants an actual notelist, not pitches. This is its own function because the private _setNotelist was created years before this here """ return self._setNotelist(notelist) def addNote(self, pitch): """The notelist gets sorted after insert. Each white note is only allowed once in each chord. That means gis and ges are not allowed in the same chord. And not two times the same note. We use the template duration from the group. That means the new note has the same main-properties as the existing notes. """ note = Note(self, self.durationGroup.templateDuration(), pitch) if pitchmath.toWhite[pitch] in self.whitePitchlist(): return False #only one of each base note in a chord. no cis, ces and c in the same chord. else: self._cachedClefForLedgerLines = None #this resets the actual ledger cache, otherwise added notes get no ledger lines. oldNotelist = self.notelist.copy() #only copies the order, not the items itself. Which is what we need. self.notelist.append(note) self.notelist.sort() self.durationGroup.cacheMinimumNote() return lambda: self._setNotelist(oldNotelist) def deleteNote(self, note): """Delete the exact note, an instance. No search for the pitch or crap like "highest first". Notelist remains sorted after delete.""" if len(self.notelist) > 1: oldNotelist = self.notelist.copy() self.notelist.remove(note) self.durationGroup.cacheMinimumNote() #No need for sorting. return lambda: self._setNotelist(oldNotelist) def removeNoteNearPitch(self, pitch): note = self.getNearestNote(pitch) return self.deleteNote(note) def getNearestNote(self, pitch): """Return the note which is nearest. The behaviour if two notes of the same pitch are in this chord is undefined. Whatever cython/python finds first will get returned. It really shouldn't matter.""" listOfTuples = [] for note in self.notelist: listOfTuples.append((abs(pitch - note.pitch), note)) note = min(listOfTuples)[1] #[1] of the tuple (pitch,noteinstance) return note #def Note getHighestNote(self): #def Note getLowestNotes(self): #Direct functions. Do not need a context, don't need a state. #Meant for undo. def _setNotePitch(self, note, targetPitch): """A direct setter for an exact note, not just near a pitch. Mostly for undo""" oldValueForUndo = note.pitch note.pitch = targetPitch self._cachedClefForLedgerLines = None return lambda: self._setNotePitch(note, oldValueForUndo) def _setNoteDuration(self, note, durationInstance): """see setNotePitch.""" assert not note.duration is durationInstance oldValueForUndo = note.duration.copy() note.duration = durationInstance self.durationGroup.cacheMinimumNote() return lambda: self._setNoteDuration(note, oldValueForUndo) def _setNoteDynamic(self, note, dynamicInstance): """see _setNoteDuration.""" assert not note.dynamic is dynamicInstance oldValueForUndo = note.dynamic.copy() note.dynamic = dynamicInstance return lambda: self._setNoteDynamic(note, oldValueForUndo) def _setNoteDynamicAndDuration(self, note, durationInstance, dynamicInstance): assert not note.duration is durationInstance oldDurationValueForUndo = note.duration.copy() note.duration = durationInstance self._cachedClefForLedgerLines = None assert not note.dynamic is dynamicInstance oldDynamicValueForUndo = note.dynamic.copy() note.dynamic = dynamicInstance return lambda: self._setNoteDynamicAndDuration(note, oldDurationValueForUndo, oldDynamicValueForUndo) #Nearest Note: #Pitch modifications def sharpenNoteNearPitch(self, pitch): note = self.getNearestNote(pitch) oldValueForUndo = note.pitch self.notelist.sort() note.sharpen() return lambda: self._setNotePitch(note, oldValueForUndo) def flattenNoteNearPitch(self, pitch): note = self.getNearestNote(pitch) oldValueForUndo = note.pitch self.notelist.sort() note.flatten() return lambda: self._setNotePitch(note, oldValueForUndo) def stepUpNoteNearPitch(self, pitch, keysig): note = self.getNearestNote(pitch) oldValueForUndo = note.pitch note.stepUpInScale(keysig) self.notelist.sort() self._cachedClefForLedgerLines = None return lambda: self._setNotePitch(note, oldValueForUndo) def stepDownNoteNearPitch(self, pitch, keysig): note = self.getNearestNote(pitch) oldValueForUndo = note.pitch note.stepDownInScale(keysig) self.notelist.sort() self._cachedClefForLedgerLines = None return lambda: self._setNotePitch(note, oldValueForUndo) def stepUpOctaveNoteNearPitch(self, pitch): note = self.getNearestNote(pitch) oldValueForUndo = note.pitch note.octaveUp() self.notelist.sort() self._cachedClefForLedgerLines = None return lambda: self._setNotePitch(note, oldValueForUndo) def stepDownOctaveNoteNearPitch(self, pitch): note = self.getNearestNote(pitch) oldValueForUndo = note.pitch note.octaveDown() self.notelist.sort() self._cachedClefForLedgerLines = None return lambda: self._setNotePitch(note, oldValueForUndo) #Duration modifications def augmentNoteNearPitch(self, pitch): note = self.getNearestNote(pitch) oldValueForUndo = note.duration.copy() note.duration.augment() self.durationGroup.cacheMinimumNote() return lambda: self._setNoteDuration(note, oldValueForUndo) def diminishNoteNearPitch(self, pitch): note = self.getNearestNote(pitch) oldValueForUndo = note.duration.copy() note.duration.diminish() self.durationGroup.cacheMinimumNote() return lambda: self._setNoteDuration(note, oldValueForUndo) def toggleDotNearPitch(self, pitch): note = self.getNearestNote(pitch) oldValueForUndo = note.duration.copy() note.duration.toggleDot() self.durationGroup.cacheMinimumNote() return lambda: self._setNoteDuration(note, oldValueForUndo) def toggleTripletNearPitch(self, pitch): note = self.getNearestNote(pitch) oldValueForUndo = note.duration.copy() note.duration.toggleTriplet() self.durationGroup.cacheMinimumNote() return lambda: self._setNoteDuration(note, oldValueForUndo) def setTupletNearPitch(self, pitch, tupletForDuration): note = self.getNearestNote(pitch) oldValueForUndo = note.duration.copy() note.duration.tuplet = tupletForDuration self.durationGroup.cacheMinimumNote() return lambda: self._setNoteDuration(note, oldValueForUndo) def toggleDurationKeywordNearPitch(self, pitch, keywordConstant): note = self.getNearestNote(pitch) oldValueForUndo = note.duration.copy() note.duration.toggleDurationKeyword(keywordConstant) return lambda: self._setNoteDuration(note, oldValueForUndo) def moreDurationNearPitch(self, pitch): """If you change the function don't forget the chord version""" note = self.getNearestNote(pitch) oldValueForUndo = note.duration.copy() note.duration.shiftEnd += D64 if not note.duration.shiftEnd: #to avoid weird behaviour where you add duration and the note off actually jumps back because shiftEnd got 0 and the default note off kicked in (which is less than 0). You can only reset to truly "0" by using the reset command. note.duration.shiftEnd += D64 return lambda: self._setNoteDuration(note, oldValueForUndo) def lessDurationNearPitch(self, pitch): note = self.getNearestNote(pitch) oldValueForUndo = note.duration.copy() if not note.duration.shiftEnd: #only happens when never touched by human hands. prevents a jump in the wrong direction because the user expects a change from the current value, not from 0. note.duration.shiftEnd = note._cachedExportObject["rightModInTicks"] note.duration.shiftEnd -= D64 if not note.duration.shiftEnd: #see moreDurationNearPitch. never return to 0 to let the default keyword kick in again. note.duration.shiftEnd -= D64 return lambda: self._setNoteDuration(note, oldValueForUndo) def moreVelocityNearPitch(self, pitch): note = self.getNearestNote(pitch) oldValueForUndo = note.dynamic.copy() note.dynamic.velocityModification += 1 return lambda: self._setNoteDynamic(note, oldValueForUndo) def lessVelocityNearPitch(self, pitch): note = self.getNearestNote(pitch) oldValueForUndo = note.dynamic.copy() note.dynamic.velocityModification -= 1 return lambda: self._setNoteDynamic(note, oldValueForUndo) def resetVelocityAndDurationModsNearPitch(self, pitch): note = self.getNearestNote(pitch) if note.dynamic.velocityModification == 0 and note.duration.shiftStart == 0 and note.duration.shiftEnd == 0: return None oldDurationValueForUndo = note.duration.copy() oldDynamicValueForUndo = note.dynamic.copy() note.dynamic.velocityModification = 0 note.duration.shiftStart = 0 note.duration.shiftEnd = 0 return lambda: self._setNoteDynamicAndDuration(note, oldDurationValueForUndo, oldDynamicValueForUndo) #Whole Chord/All Notes. Typically used with selections #REMEMBER that function-parameters are lists because it is possible #to give one parameter for each note in the chord. def _createParameterGenerator(self, notelist, parameterForEachNote): """Check and choose if there is a correct parameter list with enough values for each note in the notelist or choose the only given parameter for each note. Most of the time we want all parameters set to the same value so we receive only one argument in the listForEachNote. However, Undo from the api really sends us the original values, which could be a different one for each note. So we expect a list with len()==1 or equal length to notelist. If we only have one note it does not matter at all.""" if len(parameterForEachNote) == 1: gen = EndlessGenerator(parameterForEachNote[0]) else: assert len(self.notelist) == len(parameterForEachNote) gen = (param for param in parameterForEachNote) return gen def _setDurationlist(self, durationlist): """see _setPitchlist. Only for durations. The durations are standalone versions, most likely created through duration.copy() (in case of undo). Since this is never called twice (score.selectedItems() filters out content-links) on content-linked data no links get broken.""" assert len(self.notelist) == len(durationlist) oldValues = [] for note, newDuration in zip(self.notelist, durationlist): oldValues.append(note.duration.copy()) #no content-links get broken this way because score.selectedItems() only gets one instance of every item, no matter how many links exist. note.duration = newDuration self.durationGroup.cacheMinimumNote() return lambda: self._setDurationlist(oldValues) def _setDynamiclist(self, dynamiclist): """see _setDurationslist. Only for dynamics.""" assert len(self.notelist) == len(dynamiclist) oldValues = [] for note, newDynamic in zip(self.notelist, dynamiclist): oldValues.append(note.dynamic.copy()) #no content-links get broken this way because score.selectedItems() only gets one instance of every item, no matter how many links exist. note.dynamic = newDynamic return lambda: self._setDynamiclist(oldValues) def _setDynamicAndDuration(self, omnilist): """omnilist is a list of tuples (duration, dynamic) Function written for clearVelocityAndDurationMods""" assert len(self.notelist) == len(omnilist) oldValues = [] for note, (newDuration, newDynamic) in zip(self.notelist, omnilist): oldValues.append((note.duration.copy(), note.dynamic.copy())) #no content-links get broken this way because score.selectedItems() only gets one instance of every item, no matter how many links exist. note.dynamic = newDynamic note.duration = newDuration return lambda: self._setDynamicAndDuration(oldValues) def _setPitchlist(self, pitchlist): """This is for undo. We rely on the fact that undo is linear and the pitches we set match exactly the order of notes we already have. We don't need tests for valid values or a matching number of notes""" assert len(self.notelist) == len(pitchlist) oldValues = [] for note, newPitch in zip(self.notelist, pitchlist): oldValues.append(note.pitch) note.pitch = newPitch self._cachedClefForLedgerLines = None return lambda: self._setPitchlist(oldValues) def sharpen(self): oldValues = [] for note in self.notelist: oldValues.append(note.pitch) note.sharpen() return lambda: self._setPitchlist(oldValues) def flatten(self): oldValues = [] for note in self.notelist: oldValues.append(note.pitch) note.flatten() return lambda: self._setPitchlist(oldValues) def split(self, newparts, _existingCopies = []): """ This is only for chords. Technically this works with rests as well. However, there is little musical meaning in splitted rests. And it does not make sense for further editing. Undo works by remembering which original note spawned which child notes. There is an optional parameter _existingCopies. This is used by the internal undo/redo system. The api does not need to call it. It will be used instead of making new copies. Reason: If you split a note and split one of the newly spawned notes again it works fine. Two times Undo will delete those notes and keep only the original note with its original duration. If you now do redo with a normal split it will work the first time and split the original note again. But the resulting notes are NOT the ones from the first round. So we can't find use them in our undo philosophy, which works with specific instances. In other words: only redo needs _existingCopies. """ assert newparts >= 2 #First change the current duration in place so we only have to make copies later. oldValues = [] for note in self.notelist: oldValues.append(note.duration.copy()) #we don't actually need a copy since we create a new one. But better safe than sorry. This matches also the behaviour of the other duration change functions. complDur = note.duration.completeDuration() #newTicks = complDur / newparts note.duration = Duration.createByGuessing(Fraction(complDur, newparts)) #Cache it now, so it is correct for all copies created below self.durationGroup.cacheMinimumNote() #Now we have one note with the duration the user wants to have. Make n-1 copies- spawnedNotes = [] for block in self.parentBlocks: #weakrefs are not iterators and not iterable. So next() does not work and [x] does not work. for loop has to do. currentIndexInBlock = block.data.index(self) for i in range(newparts-1): if _existingCopies: #this is redo. c = _existingCopies[i] else: c = self.copy() spawnedNotes.append(c) block.data.insert(currentIndexInBlock+1, c) #add each new item after the original one. c.parentBlocks.add(block) break #we only need one block since the data is the same in all blocks. def _undoSplit(): #Reverse order. First remove the spawned notes for spawn in spawnedNotes: block.data.remove(spawn) #Then restore the original note durations self._setDurationlist(oldValues) return lambda: self.split(newparts, _existingCopies = spawnedNotes) return _undoSplit def augment(self): oldValues = [] for note in self.notelist: oldValues.append(note.duration.copy()) #see _setDurationlist note.duration.augment() self.durationGroup.cacheMinimumNote() return lambda: self._setDurationlist(oldValues) def diminish(self): oldValues = [] for note in self.notelist: oldValues.append(note.duration.copy()) #see _setDurationlist note.duration.diminish() self.durationGroup.cacheMinimumNote() return lambda: self._setDurationlist(oldValues) def _setRelativeMidiChannel(self, value): oldValue = self.midiChannelOffset self.midiChannelOffset = value return lambda: self._setRelativeMidiChannel(oldValue) def midiRelativeChannelPlus(self): return self._setRelativeMidiChannel(self.midiChannelOffset+1) def midiRelativeChannelMinus(self): return self._setRelativeMidiChannel(self.midiChannelOffset-1) def midiRelativeChannelReset(self): return self._setRelativeMidiChannel(0) def toggleDot(self): oldValues = [] for note in self.notelist: oldValues.append(note.duration.copy()) #see _setDurationlist note.duration.toggleDot() self.durationGroup.cacheMinimumNote() return lambda: self._setDurationlist(oldValues) def toggleTriplet(self): oldValues = [] for note in self.notelist: oldValues.append(note.duration.copy()) #see _setDurationlist note.duration.toggleTriplet() self.durationGroup.cacheMinimumNote() return lambda: self._setDurationlist(oldValues) def setTuplet(self, durationTupletForEachNote:list): """ parameter format: [ (2,3), #note 1 (4,5), #note 2 (3,4), #note 3 ] """ gen = self._createParameterGenerator(self.notelist, durationTupletForEachNote) oldValues = [] for note in self.notelist: oldValues.append(note.duration.copy()) #see _setDurationlist note.duration.tuplet = next(gen) self.durationGroup.cacheMinimumNote() return lambda: self._setDurationlist(oldValues) def toggleDurationKeyword(self, listOfKeywordConstants): gen = self._createParameterGenerator(self.notelist, listOfKeywordConstants) oldValues = [] for note in self.notelist: oldValues.append(note.duration.copy()) #see _setDurationlist note.duration.toggleDurationKeyword(next(gen)) return lambda: self._setDurationlist(oldValues) def adjustToKeySignature(self, keysigList): """We get a list as parameter but we already know it is not possible to get more than one keysig for a chord. We play along for compatibility""" gen = self._createParameterGenerator(self.notelist, keysigList) oldValues = [] for note in self.notelist: oldValues.append(note.pitch) note.toScale(next(gen)) self._cachedClefForLedgerLines = None return lambda: self._setPitchlist(oldValues) def stepUp(self, keysigList): gen = self._createParameterGenerator(self.notelist, keysigList) oldValues = [] for note in self.notelist: oldValues.append(note.pitch) note.stepUpInScale(next(gen)) self._cachedClefForLedgerLines = None return lambda: self._setPitchlist(oldValues) def stepDown(self, keysigList): gen = self._createParameterGenerator(self.notelist, keysigList) oldValues = [] for note in self.notelist: oldValues.append(note.pitch) note.stepDownInScale(next(gen)) self._cachedClefForLedgerLines = None return lambda: self._setPitchlist(oldValues) def stepUpOctave(self): oldValues = [] for note in self.notelist: oldValues.append(note.pitch) note.octaveUp() self._cachedClefForLedgerLines = None return lambda: self._setPitchlist(oldValues) def stepDownOctave(self): oldValues = [] for note in self.notelist: oldValues.append(note.pitch) note.octaveDown() self._cachedClefForLedgerLines = None return lambda: self._setPitchlist(oldValues) def intervalAutomatic(self, rootPitch, targetPitch): oldValues = [] for note in self.notelist: oldValues.append(note.pitch) note.intervalAutomatic(rootPitch, targetPitch) #TODO: possible optimisation is to calculate the interval once instead of every time. self._cachedClefForLedgerLines = None return lambda: self._setPitchlist(oldValues) def mirrorAroundCursor(self, cursorPitch): oldValues = [] for note in self.notelist: oldValues.append(note.pitch) note.mirrorAroundCursor(cursorPitch) self.notelist.sort() self._cachedClefForLedgerLines = None return lambda: self._setPitchlist(oldValues) def moreDuration(self): oldValues = [] for note in self.notelist: oldValues.append(note.duration.copy()) #see _setDurationlist note.duration.shiftEnd += D64 if not note.duration.shiftEnd: #see moreDurationNearPitch note.duration.shiftEnd += D64 return lambda: self._setDurationlist(oldValues) def lessDuration(self): oldValues = [] for note in self.notelist: oldValues.append(note.duration.copy()) #see _setDurationlist if not note.duration.shiftEnd: #only happens when never touched by human hands. prevents a jump in the wrong direction because the user expects a change from the current value, not from 0. note.duration.shiftEnd = note._cachedExportObject["rightModInTicks"] note.duration.shiftEnd -= D64 if not note.duration.shiftEnd: #see moreDurationNearPitch note.duration.shiftEnd -= D64 return lambda: self._setDurationlist(oldValues) def moreVelocity(self): oldValues = [] for note in self.notelist: oldValues.append(note.dynamic.copy()) note.dynamic.velocityModification += 1 return lambda: self._setDynamiclist(oldValues) def lessVelocity(self): oldValues = [] for note in self.notelist: oldValues.append(note.dynamic.copy()) note.dynamic.velocityModification -= 1 return lambda: self._setDynamiclist(oldValues) def resetVelocityAndDurationMods(self): oldValues = [] unmodified = [] for note in self.notelist: oldValues.append((note.duration.copy(), note.dynamic.copy())) #no content-links get broken this way because score.selectedItems() only gets one instance of every item, no matter how many links exist. unmodified.append(note.dynamic.velocityModification == 0 and note.duration.shiftStart == 0 and note.duration.shiftEnd == 0) note.dynamic.velocityModification = 0 note.duration.shiftStart = 0 note.duration.shiftEnd = 0 if all(unmodified): return None else: return lambda: self._setDynamicAndDuration(oldValues) @property def beamGroup(self): """toggleBeam only works on D8 or smaller. But a D8 can get augmented and then still have the beam flag. So every time the beamStatus gets accessed we take this as a chance to test if this Chord still has the rights for a beam instruction. This happens at least every Gui or Midi static export. So very very often and always when it matters. TODO: test performance and maybe put this test in the duration itself""" if self.durationGroup.baseDuration > D8: self._beamGroup = False return self._beamGroup def _setBeam(self, beamBool): """for undo""" oldValue = self._beamGroup #it is not guaranteed that this is the opposite of beamBool. It doesn't need do be. self._beamGroup = beamBool return lambda: self._setBeam(oldValue) def toggleBeam(self): if self.durationGroup.baseDuration > D8: assert self._beamGroup == False return None else: undoFunction = self._setBeam(not self._beamGroup) return undoFunction def removeBeam(self): return self._setBeam(False) #return undo function #Export functions def calcFlag(self): """What classical flag would be used to express baseDuration""" if self.durationGroup.genericNumber <= 4: #no flag. Quarter, Half or bigger duration. return 0 else: return self.durationGroup.genericNumber def calculateLedgerLinesForLayoutExport(self, clef): if self._cachedClefForLedgerLines and clef == self._cachedClefForLedgerLines: return self._cachedLedgerLines else: self._cachedClefForLedgerLines = clef lowestNote = self.notelist[0] highestNote = self.notelist[-1] below = lowestNote.asDotOnLine(clef) above = highestNote.asDotOnLine(clef) if below >= 6: below = int(below/2) - 2 else: below = 0 if above <= -6: above = int(abs(above)/2) - 2 else: above = 0 self._cachedLedgerLines = (below, above) return (below, above) def _exportObject(self, trackState): clef = trackState.clef() dotOnLineList = [] for note in self.notelist: dotOnLineList.append(note.asDotOnLine(clef)) highestPitchAsDotOnLine = min(dotOnLineList) #it is min because the higher pitch go in the minus direction. Qt Convention. Think Rows/Columns of a table/pixels on screen lowestPitchAsDotOnLine = max(dotOnLineList) #it is max because the higher pitch go in the minus direction. Qt Convention. Think Rows/Columns of a table/pixels on screen baseDur = self.durationGroup.baseDuration #duration is a DurationGroup so we get the minimmal note value from the notelist dur = self.logicalDuration() #duration is a DurationGroup so we get the minimmal note value from the notelist #Stems. in staffline coordinates (dots on lines): [starting point, length, 1|-1] 1 stem is on the right or -1 left side of the note. if baseDur < D1: if trackState.track.double and len(self.notelist) == 1 and 7 <= highestPitchAsDotOnLine <= 11 : #The break is between the h and c' (treble-clef) forceDownStem = True else: forceDownStem = False x = abs(highestPitchAsDotOnLine - lowestPitchAsDotOnLine) #difference between the two numbers if (not forceDownStem) and sum(dotOnLineList) >= 0: #stem goes up. stem = (lowestPitchAsDotOnLine - 1, -1 * (x+5), 1) #-1 excludes the note height/gap itself else: #stem goes down stem = (highestPitchAsDotOnLine + 1, x+5, -1) #+1 excludes the note height/gap itself else: stem = tuple() flagResult = self.calcFlag() if flagResult: assert stem flag = flagResult * stem[2] #1 or -1 for up or down. else: flag = 0 #Calfbox midiBytesList = [] exportNoteList = [] logicalStartingTick = trackState.tickindex - dur #minus duration because we are RIGHT of an item after parsing it in structures.staticRepresentation for note in self.notelist: velocity = note.dynamic.velocity(trackState) midipitch = pitchmath.toMidi[note.pitch] onOffset, offOffset = note.duration.noteOnAndOff(trackState, note.duration.completeDuration()) if note.pitch in trackState.EXPORTtiedNoteExportObjectsWaitingForClosing: #this is the last or the middle in a tied note sequence (but not the first). exportNoteList.append(note.exportObject(trackState)) if not note.pitch in trackState.EXPORTtiedNoteExportObjectsWaitingForClosing: #AGAIN! if this changed after exportObject it means this was the last note in a tied sequence #Export the missing note off (see below in this loop) midiBytesList.append(cbox.Pattern.serialize_event(logicalStartingTick + offOffset - 1, 0x80+pitchmath.midiChannelLimiter(trackState.midiChannel()+self.midiChannelOffset), pitchmath.midiPitchLimiter(midipitch, trackState.midiTranspose), velocity)) #-1 to create a small logical gap. This is nothing compared to our tick value dimensions, but it is enough for the midi protocol to treat two notes as separate ones. Imporant to say that this does NOT affect the next note on. This will be mathematically correct anyway. continue else: #this is a real note with its own note-on. Set it. midiBytesList.append(cbox.Pattern.serialize_event(logicalStartingTick + onOffset, 0x90+pitchmath.midiChannelLimiter(trackState.midiChannel()+self.midiChannelOffset), pitchmath.midiPitchLimiter(midipitch, trackState.midiTranspose), velocity)) if not note.duration.durationKeyword == D_TIE: #First tied note in a row: no note off. these are set by the last in a sequence tied note (see above) midiBytesList.append(cbox.Pattern.serialize_event(logicalStartingTick + offOffset - 1, 0x80+pitchmath.midiChannelLimiter(trackState.midiChannel()+self.midiChannelOffset), pitchmath.midiPitchLimiter(midipitch, trackState.midiTranspose), velocity)) #-1 to create a small logical gap. This is nothing compared to our tick value dimensions, but it is enough for the midi protocol to treat two notes as separate ones. Imporant to say that this does NOT affect the next note on. This will be mathematically correct anyway. #Export Dict needs to be the last step because ties depend on the first/notFirst flags not set. exportNoteList.append(note.exportObject(trackState)) #Ledger Lines ledgerLines = self.calculateLedgerLinesForLayoutExport(clef) if trackState.track.double and not 6 in dotOnLineList and ledgerLines[0] <= 6 : #The "middle" Ledger line in a double stuff is not needed if there is no note on it. ledgerLines = (ledgerLines[0] -6, ledgerLines[1]) assert dur == int(dur) assert trackState.tickindex == int(trackState.tickindex) return { "type" : "Chord", "notelist" : exportNoteList, #a list of dicts with notehead paramters like accidentals, noteheads and dots on lines. "dotOnLineList" : dotOnLineList, "highestPitchAsDotOnLine" : highestPitchAsDotOnLine, "lowestPitchAsDotOnLine" : lowestPitchAsDotOnLine, "completeDuration" : dur, "baseDuration" : baseDur, #this is from a durationGroup, so we get the minimal one. Duplicated in NoteList but handy for calculating beams. "ledgerLines" : ledgerLines, "tickindex" : trackState.tickindex - dur, #because we parse the tickindex after we stepped over the item. "stem" : stem, #stem means in staffline coordinates (dots on lines): [starting point, length, 1|-1] 1 stem is on the right or -1 left side of the note. "flag" : flag, #0 os no flag, 8 is eighth flag upwards, -8 is downwards. 16 is 16th note etc. "beamGroup" : self.beamGroup, #bool "midiBytes" : midiBytesList, "midiChannelOffset" : self.midiChannelOffset, "beam" : tuple(), #decided later in track export. Has the same structure as a stem. "UIstring" : self.lilypond(carryLilypondRanges={}), } def _lilypond(self, carryLilypondRanges): """Called by block.lilypond(carryLilypondRanges), returns a string. See Item.lilypond for the general docstring.""" def _createLilypondTuplet(duration, carryLilypondRanges:dict)->str: pre = "" # \tuplet 3/2 { } if duration.tuplet: #item has a tuplet... if "tuplet" in carryLilypondRanges: #... and is part of a group (2nd or more) if duration.tuplet == carryLilypondRanges["tuplet"]: pass #this is nothing and the entire reason why we check for group membership. Tuplet-members in ly are just inside a {...} music section. else: #it just happened that two different tuplet types followed next to each other. end one, start a new tuplet group. carryLilypondRanges["tuplet"] = duration.tuplet pre = f" }} \\tuplet {duration.tuplet[1]}/{duration.tuplet[0]} {{ " else: # ... and is the first of a group carryLilypondRanges["tuplet"] = duration.tuplet pre = f"\\tuplet {duration.tuplet[1]}/{duration.tuplet[0]} {{ " else: if "tuplet" in carryLilypondRanges: #... and the previous item was a tuplet. Group has ended. pre = " } " #hopefully this just ends the tuplet :) . Pre is correct, not post. Because we end the tuplet of the previous item. del carryLilypondRanges["tuplet"] else: #... and the previous item was also not a tuplet. Most common case. pass return pre #Do we have a dynamic \f signature waiting? (convert laborejo prefix to lilypond postfix) #They belong after the duration. if "DynamicSignature" in carryLilypondRanges: dynsig = carryLilypondRanges["DynamicSignature"] del carryLilypondRanges["DynamicSignature"] else: dynsig = "" #Do we have a slur ( or ) signature waiting? (convert laborejo prefix to lilypond postfix) if "LegatoSlur" in carryLilypondRanges: slur = carryLilypondRanges["LegatoSlur"] del carryLilypondRanges["LegatoSlur"] else: slur = "" # Check and create lilypond durations # A staccato note counts as different than a non-staccato. If the whole chord is staccato -> True, mixed -> False pitches, durations = zip(*(note.lilypond(carryLilypondRanges) for note in self.notelist)) onlyOneDuration = durations.count(durations[0]) == len(durations) if onlyOneDuration: tupletSubstring = _createLilypondTuplet(self.notelist[0].duration, carryLilypondRanges) if len(pitches) == 1: # The most common case: Just a single note return tupletSubstring + pitches[0] + durations[0] + dynsig + slur #this is a string. else: # A simple chord with mulitple pitches but just one duration return tupletSubstring + "<" + " ".join(pitches) + ">" + durations[0] + dynsig + slur else: #TODO: Cache this # < \\ { 4 } \\ { 2 } >> #Here is how it's done: We group the same duration together and create normal chords which are placed each in its own automatic voice through the lilypond syntax << { } \\ { } \\ etc. >> #Lilypond default is to take the longest note as overall duration. However, Laborejo uses the shortest duration as logical duration. So we scale all other to logical shortest duration #See http://lilypond.org/doc/v2.18/Documentation/notation/writing-rhythms#scaling-durations table = {} for note in self.notelist: pitch, duration = note.lilypond(carryLilypondRanges) ticks = note.duration.completeDuration() if not (ticks, duration) in table: table[(ticks, duration)] = list() table[(ticks, duration)].append(pitch) substrings = [] minimumTicksInChord = self.durationGroup.completeDuration() for (ticks, lilydur), lst in table.items(): #factor = Fraction(ticks, minimumDurInChord).limit_denominator(100000) #protects 6 or 7 decimal positions #dur = lilydur + " * {}/{}".format(factor.denominator, factor.numerator) #lilypond requires the 1/x syntax assert int(ticks) == ticks, ticks assert int(minimumTicksInChord) == minimumTicksInChord, minimumTicksInChord assert minimumTicksInChord <= ticks #Ties in pseudo polyphone will not lead to a ly-error. It will simply produce no tie output. if lilydur.endswith("~"): logger.warning("We do not support ties in pseudo-polyphony. Lilypond will run, but not show the tie. Please use multiple tracks for such complex polyphony or let lilypond split and tie longer notes for you: " + " ".join(pitches) + " ".join(durations) ) lilydur = lilydur[:-1] #without ~ and Duration Keywords. #TODO: not supported yet in mixed duration chords. if int(minimumTicksInChord) == int(ticks): #This is correct and nicer, because it removes 4 * 53760/53760 . dur = lilydur #the test for 'onlyOneDuration' above failed because we have mixed tenuto, staccato etc. and not because the actual durations were different. else: dur = lilydur + " * {}/{}".format(int(minimumTicksInChord), int(ticks)) #lilypond requires the x/y syntax, even if it is 1/y #TODO: this is very ugly substrings.append("\\\\ { <" + " ".join(lst) + ">" + dur + " }" ) tupletSubstring = _createLilypondTuplet(self.durationGroup, carryLilypondRanges) result = tupletSubstring + "<<" + " ".join(substrings) + dynsig + ">>" if slur: logging.error("Slurs in single-track polyphonic chords are not supported. Please use multiple tracks for real polyphony. This is the problematic Chord: " + result) return result class Rest(Item): def __init__(self, duration): super().__init__() self.duration = duration self._secondInit(parentBlock = None) #see Item._secondInit. def _secondInit(self, parentBlock): """see Item._secondInit""" super()._secondInit(parentBlock) #Item._secondInit @property def durationGroup(self): """For compatibility with Chords""" return self.duration @classmethod def instanceFromSerializedData(cls, serializedObject, parentObject): """see Score.instanceFromSerializedData""" assert cls.__name__ == serializedObject["class"] self = cls.__new__(cls) self.deserializeDurationAndNotelistInPlace(serializedObject) self.lilypondParameters = serializedObject["lilypondParameters"] self._secondInit(parentBlock = parentObject) return self def _copy(self): new = Rest(self.duration.copy()) new.copyParentBlocks(self) new.lilypondParameters = self.lilypondParameters.copy() return new def logicalDuration(self): """Return the logical duration that is used to calculate the space and subsequent position of items. Does include the base, dots and tuplets but not modifications for finetuning or phrasing.""" return self.duration.completeDuration() # Commands issued by the user. Support undo. # They get the same parameters, like pitch, as a Chord. This is only for compatibility def _setDuration(self, newDuration): """A setter which supports undo""" assert type(newDuration) is Duration oldValue = self.duration.copy() self.duration = newDuration return lambda: self._setDuration(oldValue) def augmentNoteNearPitch(self, pitch = None): oldValue = self.duration.copy() self.duration.augment() return lambda: self._setDuration(oldValue) def diminishNoteNearPitch(self, pitch = None): oldValue = self.duration.copy() self.duration.diminish() return lambda: self._setDuration(oldValue) def toggleDotNearPitch(self, pitch = None): oldValue = self.duration.copy() self.duration.toggleDot() return lambda: self._setDuration(oldValue) def toggleTripletNearPitch(self, pitch = None): oldValue = self.duration.copy() self.duration.toggleTriplet() return lambda: self._setDuration(oldValue) def setTupletNearPitch(self, pitch, tuplet): oldValue = self.duration.copy() self.duration.tuplet = tuplet return lambda: self._setDuration(oldValue) #Apply to selection gets called by different methods. We can just create aliases for Rest augment = augmentNoteNearPitch diminish = diminishNoteNearPitch toggleDot = toggleDotNearPitch toggleTriplet = toggleTripletNearPitch def setTuplet(self, durationTupletForEachNote:list): """ parameter format compatible with Chord [ (2,3), #note 1 (4,5), #note 2 (3,4), #note 3 ] """ oldValue = self.duration.copy() self.duration.tuplet = durationTupletForEachNote[0] #TODO: ? reasoning to only take the first? I think this is right, I just forgot why. return lambda: self._setDuration(oldValue) def calcFlag(self): """What classical flag would be used to express baseDuration, even if this only a rest""" if self.duration.genericNumber <= 4: #no flag. Quarter, Half or bigger duration. return 0 else: return self.duration.genericNumber def _exportObject(self, trackState): dur = self.logicalDuration() return { "type": "Rest", "rest": self.duration.genericNumber, "baseDuration": self.duration.baseDuration, "completeDuration" : dur, "dots" : self.duration.dots, "tuplet" : self.duration.tuplet, "tickindex" : trackState.tickindex - dur, #because we parse the tickindex after we stepped over the item. "midiBytes" : [], #for compatibility with beaming groups: "flag" : self.calcFlag(), "stem" : (0,0,0), "lowestPitchAsDotOnLine" : 0, "highestPitchAsDotOnLine" : 0, "UIstring" : self.lilypond(carryLilypondRanges={}), } def _lilypond(self, carryLilypondRanges): """Called by block.lilypond(carryLilypondRanges), returns a string. See Item.lilypond for the general docstring.""" if self.lilypondParameters["visible"]: return "r{}".format(self.duration.lilypond(carryLilypondRanges)) else: return "s{}".format(self.duration.lilypond(carryLilypondRanges)) class MultiMeasureRest(Item): def __init__(self, numberOfMeasures): super().__init__() self.numberOfMeasures = numberOfMeasures self._secondInit(parentBlock = None) #None is filled in later by item insert. #see Item._secondInit. def _secondInit(self, parentBlock): """see Item._secondInit""" super()._secondInit(parentBlock) #Item._secondInit self._cachedOneMeasureInTicks = 0 @property def numberOfMeasures(self): return self._numberOfMeasures @numberOfMeasures.setter def numberOfMeasures(self, newValue): if newValue <= 0: #this allows 0.5 and 0.25 newValue = 1 self._numberOfMeasures = newValue @classmethod def instanceFromSerializedData(cls, serializedObject, parentObject): """see Score.instanceFromSerializedData""" assert cls.__name__ == serializedObject["class"] self = cls.__new__(cls) self.deserializeDurationAndNotelistInPlace(serializedObject) #creates parent blocks. self._secondInit(parentBlock = parentObject) self.numberOfMeasures = int(serializedObject["numberOfMeasures"]) self.lilypondParameters = serializedObject["lilypondParameters"] return self def serialize(self): result = super().serialize() #call this in child classes result["numberOfMeasures"] = self.numberOfMeasures return result def _copy(self): new = MultiMeasureRest(self.numberOfMeasures) new.lilypondParameters = self.lilypondParameters.copy() new.copyParentBlocks(self) return new def logicalDuration(self): """Return the logical duration that is used to calculate the space and subsequent position of items. Cursor movement uses this. This gets called before exportObject gets called. """ if not self._cachedOneMeasureInTicks: for block in self.parentBlocks: self._cachedOneMeasureInTicks = block.parentTrack.state.metricalInstruction().oneMeasureInTicks break return self._cachedOneMeasureInTicks * self.numberOfMeasures def _exportObject(self, trackState): self._cachedOneMeasureInTicks = trackState.metricalInstruction().oneMeasureInTicks self._cachedMetricalInstruction = trackState.metricalInstruction() #for lilypond export dur = self.logicalDuration() return { "type": "MultiMeasureRest", "numberOfMeasures": self.numberOfMeasures, "completeDuration" : dur, "tickindex" : trackState.tickindex - dur, #because we parse the tickindex after we stepped over this item. "midiBytes" : [], "lilypondParameters" : self.lilypondParameters, "UIstring" : self.lilypond(carryLilypondRanges={}), } def _setNumberOfMeasures(self, numberOfMeasures): """For undo and redo""" oldValue = self.numberOfMeasures self.numberOfMeasures = numberOfMeasures return lambda: self._setNumberOfMeasures(oldValue) def augmentNoteNearPitch(self, pitch): """Don't double the duration but simply add one more measure""" oldValue = self.numberOfMeasures self.numberOfMeasures += 1 return lambda: self._setNumberOfMeasures(oldValue) def diminishNoteNearPitch(self, pitch): """Don't half the duration but simply add one more measure""" oldValue = self.numberOfMeasures self.numberOfMeasures -= 1 return lambda: self._setNumberOfMeasures(oldValue) #Selected MM rests do *2 and /2 like other durations. def augment(self): oldValue = self.numberOfMeasures self.numberOfMeasures = self.numberOfMeasures * 2 return lambda: self._setNumberOfMeasures(oldValue) def diminish(self): oldValue = self.numberOfMeasures self.numberOfMeasures = self.numberOfMeasures / 2 return lambda: self._setNumberOfMeasures(oldValue) def _lilypond(self, carryLilypondRanges): """Called by block.lilypond(carryLilypondRanges), returns a string. See Item.lilypond for the general docstring.""" if not self._cachedOneMeasureInTicks: #it is possible to end up with MM-Rests in a track without any metrical instruction. Linked blocks, deleting the metrical after placing MMRests etc. #This would crash the next lookup because _cachedMetricalInstruction is None return "" lyduration = "{}/{}".format(self._cachedMetricalInstruction.nominator, duration.baseDurationToTraditionalNumber[self._cachedMetricalInstruction.denominator]) if self.lilypondParameters["visible"]: return "R1*{}*{}".format(self.numberOfMeasures, lyduration) else: return "\\skilp1*{}*{}".format(self.numberOfMeasures, lyduration) class KeySignature(Item): """Examples: C Major: KeySignature(20, [0, 0, 0, 0, 0, 0, 0]) #c, d, e, f, g, a, b/h F Major: KeySignature(170, [0, 0, 0, 0, 0, 0, -10]) G Major: KeySignature(220, [0, 0, 0, 10, 0, 0, 0]) Indian Scale ?: KeySignature(220, [0, -10, -20, 0, 0, -10, -20, 0]) #c, des, eeses, f, g, aes, beses, c The input deviationFromMajorScale is a relative scale and transformed to an absolute keysig, compatible with Lilypond, in init. The result is saved permanently in self.keysigList in the following form: [(0, c), (1, d), (2, e), (3, f), (4, g), (5, a), (6, b)]) where each note can be , -20, -10, 0, +10, +20 for double flat to double sharp. Attention! the order of items is important. Example 2. G Major with correct order of accidentals: [(3,10), (0,0), (4,0), (1,0), (5,0), (2,0), (6,0)]) Example 3. F Major with correct order. [(6,-10), (2,0), (5,0), (1,0), (4,0), (0,0), (3,0)]) So it is possible to rewrite the keysignature manually. It is a public parameter. Eventhough a dict might seem convenient for this kind of data structure the order is important. """ def __init__(self, root, deviationFromMajorScale): super().__init__() assert root < 350 #is it really a plain note? assert pitchmath.plain(root) == root self.root:int = root self.deviationFromMajorScale = deviationFromMajorScale self._secondInit(parentBlock = None) #see Item._secondInit. self.lilypondParameters["explicit"] = False def _secondInit(self, parentBlock): """see Item._secondInit""" super()._secondInit(parentBlock) #Item._secondInit rootIndex = pitchmath.pillarOfFifth.index(self.root) #which position in the pillar has the root note? distance = rootIndex - 15 #distance from C. This is not the same as pitchmatch.tonalDistanceFromC! It is the index distance and not the tonal distance as interval. transposedMajorScale = self.transposeMajorScale([(3,0), (0,0), (4,0), (1,0), (5,0), (2,0), (6,0)], distance) #the number list is the major scale. transposedMajorScale = self.rotateToRoot(sorted(transposedMajorScale), pitchmath.tonalDistanceFromC[self.root]) #Ordered by pitch, rotated to have the root note in front #Modify the transposed major scale with our modificator pattern. self.keysigList = [] for maj, mod in zip(transposedMajorScale, self.deviationFromMajorScale): self.keysigList.append((maj[0], maj[1] + mod)) self.keysigList = self.sortKeysig(self.keysigList) #Now fill a list with offsets from the middle line in treble clef h', which is 0. c'' is -1, a' is +1 self.keysigListAsDistanceInDiatonicStepsFrom1720 = [] #the basis for exportObject for GUIs. Need to be modified dynamically with a clef, see exportObject() for index, accidentalMod in self.keysigList: if accidentalMod: if index == 0: #c index = -1 elif index == 1: #d index = -2 elif index == 2: #e index = -3 elif index == 3: #f index = -4 elif index == 4: #g index = -5 elif index == 5: #a index = 1 elif index == 6: #h/b index = 0 else: raise ValueError("Accidental indices for KeySignatures must be 0-6") self.keysigListAsDistanceInDiatonicStepsFrom1720.append((index, accidentalMod)) self.keysigList = tuple(self.keysigList) #from list to tuple for hashing and caching def hash(self): return self.keysigList def _set(self, root, deviationFromMajorScale): """Circular for undo""" oldValues = (self.root, self.deviationFromMajorScale) self.root = root self.deviationFromMajorScale = deviationFromMajorScale self._secondInit(parentBlock = None) #see Item._secondInit. return lambda ov=oldValues: self._set(*ov) @classmethod def instanceFromSerializedData(cls, serializedObject, parentObject): """see Score.instanceFromSerializedData""" assert cls.__name__ == serializedObject["class"] self = cls.__new__(cls) self.root = int(serializedObject["root"]) self.deviationFromMajorScale = serializedObject["deviationFromMajorScale"] self.deserializeDurationAndNotelistInPlace(serializedObject) self._secondInit(parentBlock = parentObject) return self def serialize(self): result = super().serialize() #call this in child classes result["root"] = self.root result["deviationFromMajorScale"] = self.deviationFromMajorScale #Only the two init values are saved. That means that any manual changes to the keysig cannot be saved. Keysigs are damned to get deleted and recreated. It is the same for copy. return result def _copy(self): new = KeySignature(root = self.root, deviationFromMajorScale = self.deviationFromMajorScale) return new def intervalAutomatic(self, rootPitch, targetPitch): undo = self._set(root = pitchmath.intervalAutomatic(self.root, rootPitch, targetPitch), deviationFromMajorScale = self.deviationFromMajorScale) return undo def transposeMajorScale(self, cMajorScale, stepsInPillar): """Transpose a list of positions/modifications like a keysig Pillar Of Fifth Steps. Sadly the method only works for the Major Keysig""" counter = 0 if stepsInPillar > 0: modificator = 10 else: modificator = -10 cMajorScale.reverse() transpose = [] for pos, accidental in cMajorScale: if counter == abs(stepsInPillar): transpose.extend(cMajorScale[counter:7]) #append the missing notes to the return list. break else: transpose.append((pos, accidental + modificator)) counter += 1 if stepsInPillar <= 0: #Return as it came in. Most likely the return list will be sorted anyway later, but this here is more correct. transpose.reverse() return transpose def sortKeysig(self, keysig): wip = sorted(keysig) new = [wip[3], wip[0], wip[4], wip[1], wip[5], wip[2], wip[6],] if wip[6][1] == -10: #Bes included? Then use flat order. new.reverse() return new def rotateToRoot(self, transposedMajorScale, pos): """pos is the first number of a keysig tupet. Use to rotate a root note to the front""" r = deque(transposedMajorScale) while not r[0][0] == pos: r.rotate(1) return list(r) #this is not simply creating a list but converting to a list. don't replace with [r] def asAccidentalsOnLines(self, clef): result = [] for onLineIndex, accidentalMod in self.keysigListAsDistanceInDiatonicStepsFrom1720: result.append((onLineIndex + clef.inComparisonToTrebleClefOffset, accidentalMod)) return result def _exportObject(self, trackState): """Usual dotsOnLine syntax. 0 for middle line, -n to go higher, +n to go lower. accidentalsOnLines is a dict with key = linePosition and value can be filled with bb, b, natural, #, ## which is: -20, -10, 0, 10, 20 """ return { "type": "KeySignature", "completeDuration" : 0, "tickindex" : trackState.tickindex, "root" : self.root, "accidentalsOnLines" : self.asAccidentalsOnLines(trackState.clef()), "midiBytes" : [], "UIstring" : self.lilypond(carryLilypondRanges={}), } def _lilypond(self, carryLilypondRanges): """Lilyponds custom key signatures are also the difference to the C Major scale. The \key X in front does not affect the scale building, it is a seconds step done by lilypond So, it is actually the same as our system, we just need to build a SCHEME string. This would be our hollywood-scale: \key c #`( (0 . ,NATURAL) (1 . ,NATURAL) (2 . ,NATURAL) (3 . ,NATURAL) (4 . ,NATURAL) (5 . ,FLAT) (6 . ,FLAT) ) https://lilypond.org/doc/v2.18/Documentation/notation/displaying-pitches#key-signature """ def build(step, alteration): #generate a Scheme pair for Lilyponds GUILE interpreter assert 0 <= step <= 6, step if alteration == -10: return "(" + str(step) + " . ," + "FLAT)" elif alteration == 10: return "(" + str(step) + " . ," + "SHARP)" elif alteration == 20: return "(" + str(step) + " . ," + "DOUBLE-SHARP)" elif alteration == -20: return "(" + str(step) + " . ," + "DOUBLE-FLAT)" else: assert alteration == 0, alteration return "(" + str(step) + " . ," + "NATURAL)" schemepairs = " ".join(build(i, dev) for i, dev in enumerate(self.deviationFromMajorScale)) schemepairs = " ".join(schemepairs.split()) # split and join again to reduce all whitespaces to single ones. assert schemepairs #For Transposition in lilypond we need to explicitly export all scheme pairs, not only the one that differ from major. #TODO: Root note text was removed with commit 7c76afa600cb7a4fca6800401d8e152e189112cd because they cannot get transposed. Maybe in the futureā€¦ lyKeysignature = f"\n\\key {pitchmath.pitch2ly[self.root].strip(',')} #`( {schemepairs} )\n" return lyKeysignature class Clef(Item): """A clef has no direct musical logical meaning. But it gives some hints: Voice range. A typical musical voice in a polyphonic setup has between one and two octaves range so seeing the clef hints at what voice it is. Therefore we can use a clef a basis for random note generation or even range checking and warnings. """ offset = { #This is for notes, not for the clef! #the offset of the middle line. Basically: After the clef, how many diatonic steps (gaps/line) is the middle line away from b'/h' (1720)? #7 steps is one octave #negative goes up, positive goes down. #5 lines as basis for the comments. "treble" : 0, #Standard G Clef "treble^8" : 7, "treble_8" : -7, "bass" : -12, #F Clef on the second highest line "bass^8" : -5, "bass_8" : -19, "french" : 2, #G Clef on the lowest line "baritone" : -10, #C Clef on the highest line "tenor" : -8, #C Clef on the second highest line "alto" : -6, #C Clef on the middle line "mezzosoprano" : -4, #C clef on the second lowest line "soprano" : -2, #C clef on the lowest line "varbaritone" :-10, #F Clef on the middle line "subbass" : -14, #F Clef on the highest line "midiDrum" : -12, #Percussion clef in the middle for layout, but bass clef in reality. Used for Midi. "percussion" : -6, #Percussion clef in the middle, for lilypond. Needs midi transposition, a special case checked for when doing midi export. } #regardles of the actual octave, what is the easiest way to display a pitch in another clef. for example F in treble is two lines down in bass. #This offset method is used for keysignature inComparisonToTrebleClef = { "treble" : 0, #Standard G Clef "treble^8" : 0, "treble_8" : 0, "alto" : 1, #C on the middle line. "bass" : 2, #F Clef on the second highest line "bass^8" : 2, "bass_8" : 2, "midiDrum" : 2, #Same as Bass. It's in this list so the api can return it. "percussion" : 1, #Same as Alto } centerPitch = { clefString:offset*50+1720 for clefString, offset in offset.items() } def __init__(self, clefString): super().__init__() assert clefString in Clef.inComparisonToTrebleClef self.clefString = clefString self._secondInit(parentBlock = None) #see Item._secondInit. def _secondInit(self, parentBlock): """see Item._secondInit""" super()._secondInit(parentBlock) #Item._secondInit self.asDotOnLineOffset = Clef.offset[self.clefString] self.inComparisonToTrebleClefOffset = Clef.inComparisonToTrebleClef[self.clefString] self.rangeCenter = Clef.centerPitch[self.clefString] self.rangeLowest = self.rangeCenter - 6*50 #the center is always the pitch on "middle line" self.rangeHighest = self.rangeCenter + 5*50 def __hash__(self): """Since we implemented __eq__ we need to make Clef hashable again""" return id(self) def __eq__(self, other): # self == other if not type(other) is Clef: return False return self.clefString == other.clefString def __ne__(self, other): # self != other if not type(other) is Clef: return True return self.clefString != other.clefString @classmethod def instanceFromSerializedData(cls, serializedObject, parentObject): """see Score.instanceFromSerializedData""" assert cls.__name__ == serializedObject["class"] self = cls.__new__(cls) self.clefString = serializedObject["clefString"] self.deserializeDurationAndNotelistInPlace(serializedObject) self._secondInit(parentBlock = parentObject) return self def serialize(self): result = super().serialize() #call this in child classes result["clefString"] = self.clefString #The other values are generated from clefString return result def _copy(self): new = Clef(self.clefString) return new def _exportObject(self, trackState): return { "type": "Clef", "completeDuration" : 0, "tickindex" : trackState.tickindex, "clef" : self.clefString, "midiBytes" : [], "UIstring" : self.lilypond(carryLilypondRanges={}), #Thats it. A GUI does not need to know anything about the clef except its look because we already deliever note pitches as dots on lines, calculated with the clef. } def _lilypond(self, carryLilypondRanges): return f'\\clef "{self.clefString}"' class TimeSignature(Item): #Deprecated since 1750 """Meter information. Nominator is how many notes in one measure and denominator is notes of which type in ticks. Eventhough Time Signatures are logically only valid at the beginning of a measure it is still possible to add them anywhere because Lilypond allows it as well. And it is much easier to program this way. Dumb users will always find a way to put stuff in the wrong place. If they want timesigs in the middle of a bar, let them have it... idiots. Do not edit a timesig. Delete and recreate.""" def __init__(self, nominator, denominator): raise RuntimeError("Use items.MetricalInstruction") super().__init__() #examples for 4/4 or 4/(210*2**8) measures self.nominator = nominator #example: 4. Upper number self.denominator = denominator #example: 210*2**8. Lower number self._secondInit(parentBlock = None) #see Item._secondInit. def _secondInit(self, parentBlock): """see Item._secondInit""" super()._secondInit(parentBlock) #Item._secondInit self.oneMeasureInTicks = self.nominator * self.denominator @classmethod def instanceFromSerializedData(cls, serializedObject, parentObject): """see Score.instanceFromSerializedData""" assert cls.__name__ == serializedObject["class"] self = cls.__new__(cls) self.nominator = serializedObject["nominator"] self.denominator = serializedObject["denominator"] self.deserializeDurationAndNotelistInPlace(serializedObject) self._secondInit(parentBlock = parentObject) return self def serialize(self): result = super().serialize() #call this in child classes result["nominator"] = self.nominator result["denominator"] = self.denominator return result def _copy(self): new = TimeSignature(self.nominator, self.denominator) return new def _exportObject(self, trackState): return { "type": "TimeSignature", "completeDuration" : 0, "tickindex" : trackState.tickindex, "nominator" : self.nominator, "denominator" : duration.baseDurationToTraditionalNumber[self.denominator], "midiBytes" : [], "UIstring" : self.lilypond(carryLilypondRanges={}), } class MetricalInstruction(Item): def __init__(self, treeOfInstructions, isMetrical = True): """Don't edit. Delete and create a new one A few examples, but this has a complete explanation in the manual 5/4 as 3+2 ((D4, D4, D4), (D4, D4)) 5/4 broken as 3 + 2 unstressed notes ((D4, D4, D4), D4, D4) this should have been (D4, D4, D4, D4, D4) """ super().__init__() for value in flatList(treeOfInstructions): if not type(value) is int: raise ValueError("Only integers are allowed in a metrical instruction. You used {}. Cast to int if you are sure your float is x.0, e.g. in a dotted quarter note created by D4*1.5".format(value)) self.treeOfInstructions = treeOfInstructions #this is a 4/4: ((D4, D4), (D4, D4)) . If empty no barlines will get drawn self.isMetrical = isMetrical #If False all dynamic playback modifications based on the metre will be deactivated but barlines will still be created. This is forced to become false when the treeOfInstructions is 0 ticks. self._secondInit(parentBlock = None) #see Item._secondInit. def _secondInit(self, parentBlock): """see Item._secondInit""" super()._secondInit(parentBlock) #Item._secondInit i = sum(flatList(self.treeOfInstructions)) assert i >= 0 #stop your shennenigans! self.oneMeasureInTicks = i #even if i is 0. if i == 0: self.isMetrical = False #protection against a wrong combination self.nominator = None self.denominator = None else: self.nominator = len(list(flatList(self.treeOfInstructions))) #for compatibility with lilypond etc. self.denominator = self.oneMeasureInTicks / self.nominator #for compatibility with lilypond etc. @classmethod def instanceFromSerializedData(cls, serializedObject, parentObject): """see Score.instanceFromSerializedData""" assert cls.__name__ == serializedObject["class"] self = cls.__new__(cls) self.treeOfInstructions = eval(serializedObject["treeOfInstructions"]) #we really want to keep the tuples and not mutable lists. self.isMetrical = serializedObject["isMetrical"] self.deserializeDurationAndNotelistInPlace(serializedObject) self._secondInit(parentBlock = parentObject) return self def serialize(self): result = super().serialize() #call this in child classes result["treeOfInstructions"] = self.treeOfInstructions.__repr__() #we really want to keep the tuples and not mutable lists. result["isMetrical"] = self.isMetrical return result def _copy(self): new = MetricalInstruction(self.treeOfInstructions, self.isMetrical) return new def asText(self): if not self.lilypondParameters["override"]: return "MetricalInstruction" if self.lilypondParameters["override"] == "\\mark \"X\" \\cadenzaOn": return "\\time X" elif self.lilypondParameters["override"].startswith("\\cadenzaOff"): return self.lilypondParameters["override"][12:] elif self.lilypondParameters["override"]: return self.lilypondParameters["override"] else: return "MetricalInstruction" def _exportObject(self, trackState): return { "type": "MetricalInstruction", "completeDuration" : 0, #it has no duration of its own. "tickindex" : trackState.tickindex, #"instruction" : #TODO, "isMetrical" : self.isMetrical, "oneMeasureInTicks" : self.oneMeasureInTicks, "midiBytes" : [], "treeOfInstructions" : self.treeOfInstructions.__repr__(), "UIstring" : self.asText(), } def _lilypond(self, carryLilypondRanges): """Since metrical instruction can get very complex we rely entirely on the lilypond override functionality. The common api signatures already include these.""" raise RuntimeError("This metrical instruction should have a lilypond-override") class BlockEndMarker(Item): """Cannot be inserted into the real blocks. It is added dynamically and automatically as exportObject only during track. static representation""" def __init__(self): super().__init__() #does not need a serialize or copy method since it gets dynamically created only for export def _exportObject(self, trackState): return { "type": "BlockEndMarker", "completeDuration" : 0, "tickindex" : trackState.tickindex, "midiBytes" : [], "UIstring" : f"== {trackState.blockName()}", } class DynamicSignature(Item): """The class for "piano", "forte" and so on""" def __init__(self, keyword): assert keyword in ["ppppp", "pppp", "ppp", "pp", "p", "mp", "mf", "f", "ff", "fff", "ffff", "tacet", "custom"] super().__init__() self.keyword = keyword self._secondInit(parentBlock = None) #see Item._secondInit. def _secondInit(self, parentBlock): """see Item._secondInit""" super()._secondInit(parentBlock) #Item._secondInit @classmethod def instanceFromSerializedData(cls, serializedObject, parentObject): """see Score.instanceFromSerializedData""" assert cls.__name__ == serializedObject["class"] self = cls.__new__(cls) self.keyword = serializedObject["keyword"] self.deserializeDurationAndNotelistInPlace(serializedObject) self._secondInit(parentBlock = parentObject) return self def serialize(self): result = super().serialize() #call this in child classes result["keyword"] = self.keyword return result def _copy(self): new = DynamicSignature(keyword = self.keyword) return new def baseVelocity(self, trackState): return trackState.track.dynamicSettingsSignature.dynamics[self.keyword] def _exportObject(self, trackState): return { "type": "DynamicSignature", "completeDuration" : 0, "tickindex" : trackState.tickindex, "keyword" : self.keyword, "midiBytes" : [], "UIstring" : "Dyn: " + self.keyword, } def _lilypond(self, carryLilypondRanges): """ Lilypond Dynamics are postfix. Don't export anything directly, but add the dynamic keyword to the lilypond ranges dict so the next chord can use it as postfix. """ carryLilypondRanges["DynamicSignature"] = f"\\{self.keyword}" return "" class DynamicRamp(Item): """The class for "cresc", "decresc" and so on. It takes the current trackState as starting point and interpolates to the next DynamicSignature. If there is no DynamicSignature right of this item then the ramp goes to 127 or 0. The latter is probably useful for a fadeout. Setting the interpolation type to "standalone" effectively switches the interpolation off for this item. This is useful if you want the score marker (e.g. for Lilypond) but not the midi, for example because you do dynamics with the expression CC. Cresc or Decresc is set automatically. A note gets its velocity through note.dynamic.velocity(trackState) """ def __init__(self): super().__init__() self.graphType = "linear" #options: linear, standalone self._secondInit(parentBlock = None) #see Item._secondInit. def _secondInit(self, parentBlock): """see Item._secondInit""" super()._secondInit(parentBlock) #Item._secondInit #The cached valueas are set during export. self._cachedTargetVelocity = -1 #0 is valid as well. Test for >0 self._cachedTargetTick = -1 #0 is valid as well. Test for >0 self._cachedTickIndex = -1 #0 is valid as well. Test for >0 self._cachedVelocity = -1 #0 is valid as well. Test for >0 @classmethod def instanceFromSerializedData(cls, serializedObject, parentObject): """see Score.instanceFromSerializedData""" assert cls.__name__ == serializedObject["class"] self = cls.__new__(cls) self.graphType = serializedObject["graphType"] self.deserializeDurationAndNotelistInPlace(serializedObject) self._secondInit(parentBlock = parentObject) return self def serialize(self): result = super().serialize() #call this in child classes result["graphType"] = self.graphType return result def _copy(self): new = DynamicRamp() new.graphType = self.graphType return new def _linearVelocityValue(self, tickindex): """ the variable is taken from the standard formula: f(x) = m*x + n We want to know the velocity value for a tickindex x. f(x) is that velocity. m is the gradient (steigung) """ x = tickindex - self._cachedTickIndex n = self._cachedVelocity divider = (self._cachedTargetTick - self._cachedTickIndex) if not divider: #there is a chance that this gets 0 but only while editing is in an intermediate sate. divider = 1 m = (self._cachedTargetVelocity - self._cachedVelocity) / divider return int(m*x+n) def getVelocity(self, tickindex): """Get the complete velocity for a tickindex, without the base velocity from the dynamic Signature""" if self._cachedVelocity >= 0 and self._cachedTargetVelocity >= 0 and self._cachedTargetTick >= 0 and self._cachedTickIndex >= 0: #assert tickindex <= self._cachedTargetTick #not correct. #assert tickindex >= self._cachedTickIndex #this creates problems with patterns which modify the tickindex while exporting. if self.graphType == "linear": return self._linearVelocityValue(tickindex) elif self.graphType == "standalone": return self._cachedVelocity else: raise ValueError("GraphType unknown:", self.graphType) else: #this is the case when a dynamic ramp has no final dynamic signature. note.dynamic.velocity checks for None and chooses the normal velocity instead. return None def _exportObject(self, trackState): """The keyword is dyn-ramp if there is either no follow-up Dynamic Signature or if the same dynsig follows again""" keyword = "dyn-ramp" if self._cachedVelocity >= 0 and self._cachedTargetVelocity >= 0 and self._cachedTargetTick >= 0 and self._cachedTickIndex >= 0: #print ("from {} to {} in {} ticks".format(self._cachedVelocity, self._cachedTargetVelocity, self._cachedTargetTick - self._cachedTickIndex )) if self._cachedVelocity > self._cachedTargetVelocity: keyword = "decresc" elif self._cachedVelocity < self._cachedTargetVelocity: keyword = "cresc" #else it stays dyn-ramp. #We export this so many times.. let's bake the variant into a variable for later lilypond export. self._cacheKeywordForLilypond = "\\<" if keyword == "cresc" else "\\>" return { "type": "DynamicSignature", "graphType" : self.graphType, "keyword" : keyword, "completeDuration" : 0, "tickindex" : trackState.tickindex, "midiBytes" : [], "UIstring" : keyword, } def _lilypond(self, carryLilypondRanges): """ Lilypond Dynamics are postfix. Don't export anything directly, but add the dynamic keyword to the lilypond ranges dict so the next chord can use it as postfix. """ carryLilypondRanges["DynamicSignature"] = self._cacheKeywordForLilypond return "" class LegatoSlur(Item): def __init__(self): super().__init__() self._secondInit(parentBlock = None) #see Item._secondInit. def _secondInit(self, parentBlock): """see Item._secondInit""" super()._secondInit(parentBlock) #Item._secondInit @classmethod def instanceFromSerializedData(cls, serializedObject, parentObject): """see Score.instanceFromSerializedData""" assert cls.__name__ == serializedObject["class"] self = cls.__new__(cls) self.deserializeDurationAndNotelistInPlace(serializedObject) self._secondInit(parentBlock = parentObject) return self def _copy(self): new = LegatoSlur() return new def openOrClose(self, trackState): """This function gets called by exportObject where the trackState is already after the current item, which is LegatoSlur. This means if trackState.duringLegatoSlur is False we just closed a slur. """ if trackState.duringLegatoSlur: return "open" else: return "close" def asText(self, trackState): if trackState.duringLegatoSlur: return "(" else: return ")" def _exportObject(self, trackState): #We export so many times, the chances are > 99.9% that this cache for lilypond export will be correct self._cacheKeywordForLilypond = self.asText(trackState) return { "type" : "LegatoSlur", "slur" : self.openOrClose(trackState), "completeDuration" : 0, "tickindex" : trackState.tickindex, "midiBytes" : [], "UIstring" : self.asText(trackState), } def _lilypond(self, carryLilypondRanges): """ Lilypond Slurs are postfix. Don't export anything directly, but add the keyword to the lilypond ranges dict so the next chord can use it as postfix. """ carryLilypondRanges["LegatoSlur"] = self._cacheKeywordForLilypond return "" class InstrumentChange(Item): """Includes program change, both bank changes and the lilypond short name change. Lilypond does not support a full name change, so we don't offer that here.""" def __init__(self, program, msb, lsb, shortInstrumentName): """Either init gets called, by creation during runtime, or instanceFromSerializedData. That means everything in init must be matched by a loading call in instanceFromSerializedData.""" super().__init__() assert -1 <= program <= 127, program #-1 means Laborejo will not send the program change, if this is the initial change. assert 0 <= msb <= 127, msb #a CC value assert 0 <= lsb <= 127, lsb #a CC value self.program = program self.msb = msb self.lsb = lsb self.shortInstrumentName = shortInstrumentName self._secondInit(parentBlock = None) #see Item._secondInit. def _secondInit(self, parentBlock): """After creating a new item during runtime or after loading from a file this gets called. Put every calcuation that depends on values that could be changed by the user. see Item._secondInit""" super()._secondInit(parentBlock) #Item._secondInit @classmethod def instanceFromSerializedData(cls, serializedObject, parentObject): """see Score.instanceFromSerializedData""" assert cls.__name__ == serializedObject["class"] self = cls.__new__(cls) self.program = serializedObject["program"] self.msb = serializedObject["msb"] self.lsb = serializedObject["lsb"] self.shortInstrumentName = serializedObject["shortInstrumentName"] self.deserializeDurationAndNotelistInPlace(serializedObject) self._secondInit(parentBlock = parentObject) return self def serialize(self): result = super().serialize() #call this in child classes result["program"] = self.program result["msb"] = self.msb result["lsb"] = self.lsb result["shortInstrumentName"] = self.shortInstrumentName return result def _copy(self): """return an independent copy of self""" new = type(self)(self.program, self.msb, self.lsb, self.shortInstrumentName) #all init parameters are immutable types and copied implicitly return new def _exportObject(self, trackState): duration = 0 tickPosition = trackState.tickindex - duration #minus duration because we are RIGHT of an item after parsing it in structures.staticRepresentation #change this trackState.midiProgram = self.value assert tickPosition == 0 or self.program >= 0 _msbStr = " msb" + str(self.msb) if self.msb > 0 else "" _lsbStr = " lsb" + str(self.lsb) if self.lsb > 0 else "" _nameStr = self.shortInstrumentName _nameStr = _nameStr + " " if self.shortInstrumentName else "" return { "type" : "InstrumentChange", "completeDuration" : duration, "tickindex" : trackState.tickindex - duration, #we parse the tickindex after we stepped over the item. "midiBytes" : [cbox.Pattern.serialize_event(tickPosition, 0xC0 + trackState.midiChannel(), self.program, 0), cbox.Pattern.serialize_event(tickPosition, 0xB0 + trackState.midiChannel(), 0, self.msb), #position, status byte+channel, controller number, controller value cbox.Pattern.serialize_event(tickPosition, 0xB0 + trackState.midiChannel(), 32, self.lsb), #position, status byte+channel, controller number, controller value ], "shortInstrumentName" : self.shortInstrumentName, "UIstring" : "{}[pr{}{}{}]".format(_nameStr, self.program, _msbStr, _lsbStr), #this is for a UI, possibly a text UI, maybe for simple items of a GUI. Make it as short and unambigious as possible. } def _lilypond(self, carryLilypondRanges): """called by block.lilypond(carryLilypondRanges), returns a string. Don't create white-spaces yourself, this is done by the structures. When in doubt prefer functionality and robustness over 'beautiful' lilypond syntax.""" if self.shortInstrumentName: return f'\\mark \\markup {{\\small "{self.shortInstrumentName}"}}' + "\n" + f'\\set Staff.shortInstrumentName = #"{self.shortInstrumentName}"' else: return "" class ChannelChange(Item): def __init__(self, value, text): """Either init gets called, by creation during runtime, or instanceFromSerializedData. That means everything in init must be matched by a loading call in instanceFromSerializedData.""" super().__init__() #self.itemData #matches instanceFromSerializedData and serialize assert 0 <= value <= 16, value self.value = value self.text = text self._secondInit(parentBlock = None) #see Item._secondInit. def _secondInit(self, parentBlock): """After creating a new item during runtime or after loading from a file this gets called. Put every calcuation that depends on values that could be changed by the user. see Item._secondInit""" super()._secondInit(parentBlock) #Item._secondInit @classmethod def instanceFromSerializedData(cls, serializedObject, parentObject): """see Score.instanceFromSerializedData""" assert cls.__name__ == serializedObject["class"] self = cls.__new__(cls) self.value = serializedObject["value"] self.text = serializedObject["text"] self.deserializeDurationAndNotelistInPlace(serializedObject) self._secondInit(parentBlock = parentObject) return self def serialize(self): result = super().serialize() #call this in child classes result["value"] = self.value result["text"] = self.text return result def _copy(self): """return an independent copy of self""" new = type(self)(self.value, self.text) #both init parameters are immutable types and copied implicitly return new def _exportObject(self, trackState): duration = 0 tickPosition = trackState.tickindex - duration #minus duration because we are RIGHT of an item after parsing it in structures.staticRepresentation #the new midi channel for the cursor, and all following export items, is handled by the track state and left/right parser return { "type" : "ChannelChange", "completeDuration" : duration, "tickindex" : trackState.tickindex - duration, #we parse the tickindex after we stepped over the item. "midiBytes" : [], "text" : self.text, #TODO: UIs work with 1-16. The rest of the laborejo engine works with 0-15 but this is dedicated for our own UI, read-only. Export the GUI value directly, even if it is a responsibility mismatch. "UIstring" : f"{self.text}(ch{self.value+1})" if self.text else f"ch{self.value+1}", #this is for a UI, possibly a text UI, maybe for simple items of a GUI. Make it as short and unambigious as possible. } def _lilypond(self, carryLilypondRanges): """called by block.lilypond(carryLilypondRanges), returns a string. Don't create white-spaces yourself, this is done by the structures. When in doubt prefer functionality and robustness over 'beautiful' lilypond syntax.""" if self.text: return f'\\mark \\markup {{\\small "{self.text}"}}' else: return "" #Note in use. class RecordedNote(Item): """An intermediate item that gets created by the live recording process. It has no Duration instance but a simple tick duration instead. Also it has a fixed position in tick-time and cannot be edited with the cursor and the other methods. It lives in a special per-track buffer. Is is saved and gets loaded. """ def __init__(self, itemData): """Either init gets called, by creation during runtime, or instanceFromSerializedData. That means everything in init must be matched by a loading call in instanceFromSerializedData.""" super().__init__(positionInTicks, durationInTicks ) #TODO: that is not super._init of item. Item has no parameters! self.positionInTicks = positionInTicks self.durationInTicks = durationInTicks self._secondInit(parentBlock = None) #see Item._secondInit. def _secondInit(self, parentBlock): """After creating a new item during runtime or after loading from a file this gets called. Put every calcuation that depends on values that could be changed by the user. see Item._secondInit""" super()._secondInit(parentBlock) #Item._secondInit @classmethod def instanceFromSerializedData(cls, serializedObject, parentObject): """see Score.instanceFromSerializedData""" raise NotImplementedError("Implement instanceFromSerializedData() for this item type!") assert cls.__name__ == serializedObject["class"] self = cls.__new__(cls) self.itemData = serializedObject["itemData"] self.deserializeDurationAndNotelistInPlace(serializedObject) self._secondInit(parentBlock = parentObject) return self def serialize(self): raise NotImplementedError("Implement serialize() for this item type!") result = super().serialize() #call this in child classes result["itemData"] = self.itemData return result def _copy(self): """return an independent copy of self""" raise NotImplementedError("Implement _copy() for this item type!") new = TemplateItem(self.itemData) return new def _exportObject(self, trackState): """Implement this in every child class, return a dict. Base class function to return a dict which is a good layout export basis. For example for GUI frontends. They don't have to parse and calculate their own values in slow pure Python then midiBytes is a list with patternBlob/binary data generated by cbox.Pattern.serialize_event(position, midibyte1 (noteon), midibyte2(pitch), midibyte3(velocity)) Items with duration, like chords, will most like need to create at least two patternBlobs, one for the start (e.g. note on) and one for the stop (e.g. note off) """ raise NotImplementedError("Implement _exportObject() for this item type!") #And for documentation and education here a template exportObject duration = 0 return { "type" : "", "completeDuration" : duration, "tickindex" : trackState.tickindex - duration, #we parse the tickindex after we stepped over the item. "midiBytes" : [], "UIstring" : "", #this is for a UI, possibly a text UI, maybe for simple items of a GUI. Make it as short and unambigious as possible. } def _lilypond(self, carryLilypondRanges): """absolutely not""" return "" class LilypondText(Item): """lilypond text as a last resort""" def __init__(self, text): super().__init__() self.text = text self._secondInit(parentBlock = None) #On item creation there is no parentBlock. The block adds itself to the item during insert. def _secondInit(self, parentBlock): """see Item._secondInit""" super()._secondInit(parentBlock) #Item._secondInit def _lilypond(self, carryLilypondRanges): """called by block.lilypond(carryLilypondRanges), returns a string. Don't create white-spaces yourself, this is done by the structures. When in doubt prefer functionality and robustness over 'beautiful' lilypond syntax.""" return self.text @classmethod def instanceFromSerializedData(cls, serializedObject, parentObject): """see Score.instanceFromSerializedData""" assert cls.__name__ == serializedObject["class"] self = cls.__new__(cls) self.text = serializedObject["text"] self.deserializeDurationAndNotelistInPlace(serializedObject) #creates parentBlocks self._secondInit(parentBlock = parentObject) return self def serialize(self): result = super().serialize() #call this in child classes result["text"] = self.text return result def _copy(self): """return an independent copy of self""" new = LilypondText(self.text) return new def _exportObject(self, trackState): return { "type" : "LilypondText", "completeDuration" : 0, "tickindex" : trackState.tickindex, #we parse the tickindex after we stepped over the item. "midiBytes" : [], "text" : self.text, "UIstring" : self.text, #this is for a UI, possibly a text UI, maybe for simple items of a GUI. Make it as short and unambigious as possible. }