You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

2902 lines
128 KiB

# -*- coding: utf-8 -*-
"""
Copyright 2020, 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 <http://www.gnu.org/licenses/>.
"""
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
#Third Party Modules
from calfbox import cbox
#Template Modules
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,
"tuplets" : self.duration.tuplets,
"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):
"""Called by chord.lilypond(), 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()
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
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.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.tuplets = [(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.tuplets = [(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"])
self.tuplets = serializedObject["tuplets"] #TODO: make sure the types are correct?
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["tuplets"] = self.tuplets
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.tuplets = self.tuplets.copy()
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
for numerator, denominator in self.tuplets:
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.tuplets == [(2,3)]:
self.tuplets = []
else:
self.tuplets = [(2,3)]
def lilypond(self):
"""Called by note.lilypond(), See Item.lilypond for the general docstring.
returns a number as string."""
if self.durationKeyword == D_TIE:
append = "~"
else:
append = ""
n = self.genericNumber
if n == 0:
return "\\breve" + append
elif n == -1:
return "\\longa" + append
elif n == -2:
return "\\maxima" + 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.tuplets for note in self.chord.notelist)
@property
def baseDuration(self):
return self.minimumNote.duration.baseDuration
@property
def genericNumber(self):
return duration.baseDurationToTraditionalNumber[self.baseDuration]
##############################################
## 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):
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):
if self.lilypondParameters["override"]:
return self.lilypondParameters["override"]
else:
return self._lilypond()
def _lilypond(self):
"""called by block.lilypond(), 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):
"""called by block.lilypond(), 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:
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._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, tupletListForDuration):
note = self.getNearestNote(pitch)
oldValueForUndo = note.duration.copy()
note.duration.tuplets = tupletListForDuration
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))
#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.
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)
self.durationGroup.cacheMinimumNote()
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, durationTupletListForEachNote):
"""
parameter format:
[
[(2,3)], #note 1
[(4,5), (2,3)], #note 2
[(3,4)], #note 3
]
"""
gen = self._createParameterGenerator(self.notelist, durationTupletListForEachNote)
oldValues = []
for note in self.notelist:
oldValues.append(note.duration.copy()) #see _setDurationlist
note.duration.tuplets = 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 stepUp(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.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 structures export. Has the same structure as a stem.
}
def _lilypond(self):
"""Called by block.lilypond(), returns a string.
See Item.lilypond for the general docstring."""
pitches, durations = zip(*(note.lilypond() for note in self.notelist))
onlyOneDuration = durations.count(durations[0]) == len(durations)
if onlyOneDuration:
return "<" + " ".join(pitches) + ">" + durations[0]
else:
#TODO: Cache this
# < \\ { <g''>4 } \\ { <e'' a'>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 << { <chord> } \\ { <chord> } \\ 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()
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
if lilydur.endswith("~"):
lilydur = lilydur[:-1] #without ~ #TODO: not supported yet.
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 + " }" )
result = "<<" + " ".join(substrings) + ">>"
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, tupletListForDuration):
oldValue = self.duration.copy()
self.duration.tuplets = tupletListForDuration
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, durationTupletListForEachNote):
"""
parameter format compatible with Chord
[
[(2,3)], #note 1
[(4,5), (2,3)], #note 2
[(3,4)], #note 3
]
"""
oldValue = self.duration.copy()
self.duration.tuplets = durationTupletListForEachNote[0]
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,
"tuplets" : self.duration.tuplets,
"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,
}
def _lilypond(self):
"""Called by block.lilypond(), returns a string.
See Item.lilypond for the general docstring."""
if self.lilypondParameters["visible"]:
return "r{}".format(self.duration.lilypond())
else:
return "s{}".format(self.duration.lilypond())
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,
}
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):
"""Called by block.lilypond(), returns a string.
See Item.lilypond for the general docstring."""
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 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 #really a plain note?
assert pitchmath.plain(root) == root
self.root = 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" : [],
}
def _lilypond(self):
def build(step, alteration): #generate a Scheme pair for Lilyponds GUILE interpreter
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)"
elif self.lilypondParameters["explicit"] and alteration == 0:
return "(" + str(step) + " . ," + "NATURAL)"
else:
return ""
#G Major ((3, 10), (0, 0), (4, 0), (1, 0), (5, 0), (2, 0), (6, 0))
#`((0 . ,NATURAL) (1 . ,NATURAL) (2 . ,NATURAL) (3 . ,SHARP) (4 . ,NATURAL) (5 . ,NATURAL) (6 . ,NATURAL))
schemepairs = " ".join(build(x[0], x[1]) for x in self.keysigList)
schemepairs = " ".join(schemepairs.split()) # split and join again to reduce all whitespaces to single ones.
subtextRoot = "\\once \\override Staff.KeySignature #'stencil = #(lambda (grob) (ly:stencil-combine-at-edge (ly:key-signature-interface::print grob) Y DOWN (grob-interpret-markup grob (markup #:small \"" + pitchmath.pitch2ly[self.root].strip(",").title() + "\")) 3)) " #3 is the space between keysig and text
lyKeysignature = subtextRoot + "\\set Staff.keyAlterations = #`( {} )".format(schemepairs)
return lyKeysignature
#Alternative
schemepairs = " ".join(build(num, alt) for num, alt in enumerate(self.deviationFromMajorScale))
schemepairs = " ".join(schemepairs.split()) # split and join again to reduce all whitespaces to single ones.
subtextRoot = "\\once \\override Staff.KeySignature #'stencil = #(lambda (grob) (ly:stencil-combine-at-edge (ly:key-signature-interface::print grob) Y DOWN (grob-interpret-markup grob (markup #:small \"" + pitchmath.pitch2ly[self.root].strip(",").title() + "\")) 3)) " #3 is the space between keysig and text
if schemepairs:
lyKeysignature = subtextRoot + f"\\key {pitchmath.pitch2ly[self.root].strip(',')} #`( {schemepairs} )"
else: #no explicit accidental
lyKeysignature = subtextRoot + "\\set Staff.keyAlterations = #`(( 0 . ,NATURAL))" #TODO: subtextRoot position is broken
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 = {
#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
"percussion" : 1, #Same as Alto
}
centerPitch = { clefString:offset*50+1720 for clefString, offset in offset.items() }
def __init__(self, clefString):
super().__init__()
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" : [],
#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):
return "\\clef \"{}\"".format(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" : [],
}
class MetricalInstruction(Item):
def __init__(self, treeOfInstructions, isMetrical = True):
"""Don't edit. Delete and create a new one"""
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 #if you skipped the docstring. 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):