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.

2995 lines
133 KiB

4 years ago
# -*- coding: utf-8 -*-
"""
Copyright 2022, Nils Hilbricht, Germany ( https://www.hilbricht.net )
4 years ago
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")
4 years ago
#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
4 years ago
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,
4 years ago
"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.
4 years ago
Returns two strings, pitch and duration.
The duration is per-chord or needs special polyphony."""
return pitchmath.pitch2ly[self.pitch], self.duration.lilypond(carryLilypondRanges)
4 years ago
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).
4 years ago
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)
4 years ago
durList = [D1024, D512, D256, D128, D64, D32, D16, D8, D4, D2, D1, DB, DL, DM]
guessedBase = _closest(completeDuration, durList)
4 years ago
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)
4 years ago
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)
4 years ago
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
4 years ago
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
4 years ago
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
4 years ago
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
4 years ago
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.
4 years ago
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
4 years ago
else:
self.tuplet = (2,3)
4 years ago
def lilypond(self, carryLilypondRanges):
"""Called by note.lilypond(carryLilypondRanges), See Item.lilypond for the general docstring.
4 years ago
returns a number as string."""
if self.durationKeyword == D_TIE:
append = "~"
else:
append = ""
n = self.genericNumber
4 years ago
if n == 0:
return "\\breve" + append
4 years ago
elif n == -1:
return "\\longa" + append
4 years ago
elif n == -2:
return "\\maxima" + append
4 years ago
else:
return str(n) + self.dots*"." + append
4 years ago
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.
"""
4 years ago
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)
4 years ago
@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
4 years ago
##############################################
## 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.
"""
4 years ago
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)
4 years ago
return new
4 years ago
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
4 years ago
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"""
4 years ago
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
4 years ago
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
4 years ago
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
4 years ago
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):
4 years ago
#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)
4 years ago
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.
4 years ago
def logicalDuration(self):
8 months ago
"""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
"""
4 years ago
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):
4 years ago
if self.lilypondParameters["override"]:
return self.lilypondParameters["override"]
else:
return self._lilypond(carryLilypondRanges)
4 years ago
def _lilypond(self, carryLilypondRanges):
"""called by block.lilypond(carryLilypondRanges), returns a string.
4 years ago
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
4 years ago
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
4 years ago
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.
4 years ago
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
4 years ago
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.