self.dynamics={#This is a dict instead of direct values so that dynamicSignature can use a string keyword to get their value and don't have to use getattr().
warn("Syntax Warning: duration processing not possible. Only 'x' and numbers are allowed for calculations. Please check your input: {}".format(string))
self.string=string
self.evaluated=lambdax:0
def__call__(self,parameter):
returnself.evaluated(parameter)
def__str__(self):
returnself.string
def__init__(self):
self.data=WeakKeyDictionary()
def__get__(self,instance,owner):
# we get here when someone calls instance.attribute, and attribute is a DurationMod instance
#return self.data.get(instance)
returnself.data[instance]
def__set__(self,instance,string):
# we get here when someone calls instance.attribute = val, and attribute is a DurationMod instance
self.dynamicRamp=None#Not for cursor movement so it is not a stack.
self.EXPORTtiedNoteExportObjectsWaitingForClosing={}#pitch:noteExportObject . Empty during cursor movement, filled during export.
self.duringLegatoSlur=False#there are no nested legato slurs
self.duringBeamGroup=False#no nested beam groups.
self.midiChannels=[track.initialMidiChannel]#these are just integers, not items. items.ChannelChange parsing changes this and items automatically get the new value. A stack of midi channels allows the cursor to know the current channel for immediate playback/feedback.
# TODO: When a change item for the following functions is introduced they need to get parsed in track.parseLeft and track.parseRight
defposition(self):
"""The position in the track"""
#[0, 1, 2, 3][:3] means slice item 0, 1, 2 but not include 3.
result=0
forblockinself.track.blocks[:self.blockindex]:#all blocks without the current one.
#TODO: the following assert fails. somewhere in the code the local cursor index gets not updated properly. most likely in insert, delete or track.tail, track.head
#This is the reason why inserting in contend linket blocks results in the wrong cursor position if we rely on keeping track of the track position manually or using the local block positions. We can only rely on the current one.
#assert len(block.data) == block.localCursorIndex #does not hold for the current block because it was not completely traversed yet.
result+=len(block.data)+1#+1 for the appending position
result+=self.track.currentBlock().localCursorIndex#finally add the position in the current block which is somewhere in the middle or so. at least not the same as len(currentBlock)
"""Return if the cursor is on the end of a block (which is
alsotheendofthetrack)"""
returnself.track.currentBlock().isAppending()
#You cannot cache the trackState, especially not keysignatures. Leave this in as a reminder.
#An item can be in two blocks which places it in two different contexts/states. If you want to know the context of an item you really have to put the cursor on it.
defcreateCached(self):
"""Create a static version of the current state which can be
#TODO: for now we only cache values that we know we need. This should keep this function under control and prevent misuse from lazyness, like explained in the docstring.
#return {
# "keySignature" : self.keySignatures[-1],
#}
pass
classTrack(object):
allTracks=WeakValueDictionary()#key is the trackId, value is the weak reference to the Track. Deleted tracks (from the current session) and hidden tracks are in here as well.
self.ccChannels=tuple()#unsorted numbers from 0-15 which represent the midi channels all CCs are sent to. Only replaced by a new tuple by the user directly. From json this becomes a list, so don't test for type. If empty then CC uses the initial midi channel.
self.double=False#if true add 5 stafflines extra below. Clef will always stay in the upper staff. This is a representation-hint. E.g. a gui can offer a bigger staff system for the user.
#Since version 2.1.0 the initial signature can be set by the user, mostly as a GUI convenience.
#These are used by the track-state init/head
self.initialClefKeyword:str="treble"#it is much easier to handle the keyword with save/load and setting this value, so we are not using the item.Clef class
#block.parentTrack = None We keep the parent track for undo. Through the linear nature of undo it is guaranteed that the track instance will still exists when this delete attempts undo.
self.blocks.remove(block)#eventhough this must be undone we cannot keep the deleted block in self.blocks. Self.blocks is designed for only active blocks.
block.parentTrack=None
self.toPosition(originalPosition,strict=False)#has head() in it. strict=False just leaves the cursor at head if we can't return to the position because it got deleted.
elifself.state.blockindex+1<len(self.blocks):#block counts from 0, len from 1. If block+1 is smaller than the amount of blocks this means this is not the final block.
#block end. This is already the previous state. right now we are in the next block already.
self.state.blockindex+=1
self.currentBlock().head()#in case we have a block multiple times in the track this resets it from the last time we traversed it
#instead of parsing right:
lastBlock=self.blocks[self.state.blockindex-1]
self.state.tickindex+=lastBlock.staticExportEndMarkerDuration()#why -1? see comment above
return2#moving succeeded, but no Item was returned. This is a gap between two blocks.
else:
return0
#track end
defmeasureLeft(self):
"""We don't use goToTickIndex for this because that
ifcurItemandcurItem.logicalDuration()>0ornotself.state.ticksSinceLastMeasureStartLive==0:#we found the boundary between this measure and the one before it
"""A very good way to find a position, while looking the other
wayiftheitematthispositionisindeedtheitemweare
lookingforormaybejustacopy.
Remember:Blocksareunique!Itemsareareonly
uniquewithinablock"""
self.head()
whileTrue:
ifself.state.blockindex==blockindexandself.currentBlock().localCursorIndex==localCursorIndex:#self.currentBlock() is only executed if the blockindex is already correct
returnTrue
else:
ifnotself.right():#end of track
raiseRuntimeError("Position or block not found. Either the block has been deleted or is too short now.",(self.state.blockindex,self.currentBlock().localCursorIndex))
deftoItemInBlock(self,targetItem,targetBlock):
"""A block is the only unique element in a Laborejo score.
self.state.duringLegatoSlur=notself.state.duringLegatoSlur#there is only level of slurs and it is not allowed to insert a legatoSlurOpen when self.state.duringLegatoSlur is True or ..close if the state is false. This makes just toggling the value possible,
elifisinstance(item,MultiMeasureRest):
pass
elifisinstance(item,Clef):
self.state.clefs.pop()
elifisinstance(item,KeySignature):
self.state.keySignatures.pop()
elifisinstance(item,MetricalInstruction):
self.state.metricalInstructions.pop()
elifisinstance(item,DynamicSignature):
self.state.dynamicSignatures.pop()
elifisinstance(item,InstrumentChange):
self.state.instrumentChanges.pop()
#else:
#items.Item
defparseRight(self):
item=self.previousItem()
dur=item.logicalDuration()
self.state.tickindex+=dur#Anything that is right of the cursor (including the current item, which is technically right of the cursor as well) does not matter for the tickindex and is not saved in here.
iftickRest>=0:#Measure Number calculation does not protect against human error. A user can add a MetricalInstruction at the beginning of a measure which will create problems here and there but is allowed nevertheless as temporary state while editing
self.state.ticksSinceLastMeasureStartLive=tickRest#If one measure was overfull the rest ist chosen as next startpoint.
ifisinstance(item,Chord):
pass
elifisinstance(item,Rest):
pass
elifisinstance(item,LegatoSlur):
self.state.duringLegatoSlur=notself.state.duringLegatoSlur#there is only level of slurs and it is not allowed to insert a legatoSlurOpen when self.state.duringLegatoSlur is True or ..close if the state is false. This makes just toggling the value possible,
elifisinstance(item,DynamicRamp):#This is only for a complete parse, not for cursor movement. That is why this is not stack and there is nothing in parseLeft() to unset this. head() is enough to unset.
graphTrackObject.parentTrack=self#in case of undo this is redundant. In other cases it might be neccessary
graphTrackObject.cc=cc#this makes this function the basis for "change cc" or even moving to other tracks
self.ccGraphTracks[cc]=graphTrackObject
defdeleteGraphTrackCC(self,cc):
result=self.ccGraphTracks[cc]
self.ccGraphTracks[cc].prepareForDeletion()#this only deletes the current midi track. not the midi out. If we add this track again through undo we don't need to do anything. it will be regenerated automatically.
#Find sequential content-linked blocks to convert them into lilypond voltas
last=set()
currentFirstBlockInARow=None
repeatCounter=0
blockRepeatIndex={}#block : int
blockRepeatTotal={}#first block : int. This is not the same as len(set(block.linkedContentBlocksInScore())) because the latter is all links in total, while we only want the consecutive count. Only the first block of a row is in here.
#TODO: Contentlink -> Volta conversion is currently deactivated for multi-track. Too many lilypond problems
#As long as blockRepeatTotal exists and is empty export will treat each block as standalone, so everything works
#The commented-out code does not show the number of repeats, and more importantly, places the same repeats in all staffs, even if they do not exist there.
#This is not compatible with real-life music where one instrument plays the same part twice, but the second one has a different version the 2nd time.
#If we ever find a lilypond way to only set synchronized repeats this can get activated for more tracks.
codeActivated=len(self.parentData.tracks)==1
codeActivated=False#TODO: NO! Too fragile
forblockinself.blocks:#in order
links=set(block.linkedContentBlocksInScore())#it's a generator
iflinks:
ifcodeActivatedandlinks==last:
#This is not the first one in a row.
repeatCounter+=1
blockRepeatTotal[currentFirstBlockInARow]+=1
else:
#This is the first one in a row. Reset.
repeatCounter=0
currentFirstBlockInARow=block
blockRepeatTotal[currentFirstBlockInARow]=1
last=links
blockRepeatIndex[block]=repeatCounter#0 for standalone blocks. Each block one entry
#Another round through the blocks to generate data
lyData=""
forblockinself.blocks:
ifblockinblockRepeatTotal:
l=blockRepeatTotal[block]
ifl>1:
assertblockRepeatIndex[block]==0
lyData+=f"\n\\repeat volta {l}"+block.lilypond(carryLilypondRanges)#the lilypond block includes music expression { }
else:
#No voltas. We could do \repeat volta 1 but that would be ugly lilypond.
lyData+=block.lilypond(carryLilypondRanges)
else:
# A linked block in a row.
# do NOT export any content linked block that is not the first in a row.