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.
#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.
return2#moving succeeded, but no Item was returned. This is a gap between two blocks.
else:
return0
#track beginning
defright(self):
ifself.currentBlock().right():#side effect: actual moving
self.parseRight()
return1
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,MultiMeasureRest):
pass
elifisinstance(item,Clef):
self.state.clefs.append(item)
elifisinstance(item,KeySignature):
self.state.keySignatures.append(item)
elifisinstance(item,MetricalInstruction):
self.state.metricalInstructions.append(item)
elifisinstance(item,DynamicSignature):
self.state.dynamicSignatures.append(item)
self.state.dynamicRamp=None#reset
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.
#TODO: this is a hack. This makes Laborejo dependent on the api beeing used. This is only the case with GUI or Midi but not as a script. However, we need to expand the tempo track constantly.
#TODO: However, it is even less elegant to put this call in all rhythm editing methods and functions. inserts, block duplicate, content links, augment, tuplets undo etc.
#Taken out an placed in tempo Export. #self.score.tempoTrack.expandLastBlockToScoreDuration() #we guarantee that the tempo track is always at least as long as the music tracks.
barlines=OrderedDict()#tick:metricalInstruction . We do not use selft.state.barlines, which is simply barlines for the live cursor. This is to send Barlines to a UI and also to generate the Metronome by associating a metricalInstruction with each barline.
result=[]
metaData={}#this will be the last item in the representation and can be popped by a GUI. It holds information like the position of barlines.
#if self.hasContentLinks():
self.head()#reset the state
#At this point there is NOTHING in the track state except the init data. You can't look up anything in the state until the cursor moved at least once right. That means a metrical instruction is impossible to detect through the trackstate right now.
instrumentChangesBinaryCboxData+=cbox.Pattern.serialize_event(0,0xB0+self.initialMidiChannel,0,self.initialMidiBankMsb)#position, status byte+channel, controller number, controller value
instrumentChangesBinaryCboxData+=cbox.Pattern.serialize_event(0,0xB0+self.initialMidiChannel,32,self.initialMidiBankLsb)#position, status byte+channel, controller number, controller value
#Process the items, most likely notes. Remember that the item we are interested in is the previous item because it is left of our cursor.
previousBarlineTickIndex=self.upbeatInTicks
previousItem=None
whileTrue:
r=localRight()
ifr==1:#Exporting the items and adding them to the result is completely done in this block. expObj is not used anywhere else in this function.
previousItem=self.previousItem()
expObj=previousItem.exportObject(self.state)#yes, it is correct that the state in the parameter is ahead by one position. Why is it that way? Because everything that matters, like new dynamics will only be parsed afterwards. The trackState is always correct except for the tickindex when exporting after parsing. Thats why exportObject sometimes substracts its own duration for finding its starting tick.