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.
3094 lines
137 KiB
3094 lines
137 KiB
# -*- 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 <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
|
|
|
|
#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
|
|
|
|
# < \\ { <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(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 <e'>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.
|
|
}
|
|
|