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.durationSettingsSignature=DurationSettingsSignature()#only one per track
self.dynamicSettingsSignature=DynamicSettingsSignature()#only one per track
self.upbeatInTicks=0#playback does not care about upbeats. But exporting does. for example barlines.
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.
#A set of starting values. They are user editable and represent the tracks playback state on head(). Therefore they get saved.
self.initialMidiChannel=0# 0-15
self.initialMidiProgram=-1# 0-127. -1 is "don't send"
self.initialMidiBankMsb=0# 0-127. Depends on program >= 0
self.initialMidiBankLsb=0# 0-127. Depends on program >= 0
self.midiTranspose=0# -127 to +127 but the result is never outside of 0-127.1 Cannot change during the track.
#The instrument names are also handled by the trackState so that the cursor knows about instrument changes
self.initialInstrumentName=""#different than the track name. e.g. "Violin"
"""Return the duration of the whole track, in ticks"""
result=0
forblockinself.blocks:
result+=block.duration()
returnresult
deflistOfCCsInThisTrack(self):
returnlist(self.ccGraphTracks.keys())
defappendBlock(self):
new=Block(self)
self.blocks.append(new)
returnnew
defappendExistingBlock(self,block):
block.parentTrack=self
self.blocks.append(block)
defdeleteBlock(self,block):
"""Undo of deleteBlock is rearrange blocks."""
iflen(self.blocks)>1:
originalPosition=self.state.position()
#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.
returnself,block#self is parent Track
defcurrentBlock(self):
block=self.blocks[self.state.blockindex]
returnblock
defduplicateBlock(self,block):
originalPosition=self.state.position()
index=self.blocks.index(block)
copy=block.copy(newParentTrack=self)
self.blocks.insert(index+1,copy)
self.toPosition(originalPosition)#has head() in it
defduplicateContentLinkBlock(self,block):
originalPosition=self.state.position()
index=self.blocks.index(block)
linked=block.contentLink()
self.blocks.insert(index+1,linked)
self.toPosition(originalPosition)#has head() in it
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
whileself.left():#stops automatically at the beginning of the track
ifself.currentItem().logicalDuration()>0ornotself.state.ticksSinceLastMeasureStartLive==0:#we found the boundary between this measure and the one before it
self.right()
break
else:
self.head()
defstartOfBlock(self):
currentBlock=self.currentBlock()#we stay in this block so this doesn't need to change.
whilenotcurrentBlock.localCursorIndex==0:
self.left()
defendOfBlock(self):
"""Go to the end of this block"""
currentBlock=self.currentBlock()#we stay in this block so this doesn't need to change.
"""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.
delself.ccGraphTracks[cc]
returnresult
#we don't need to unregister anything from cbox.
#Save / Load / Export
deflilypond(self):
"""Called by score.lilypond(), returns a string."""
#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.
elifr==2:#block end. Again, this is already the previous state. right now we are in the next block already.
lastBlock=self.blocks[self.state.blockindex-1]#why -1? see comment above
dur=lastBlock.staticExportEndMarkerDuration()
resultAppend(BlockEndMarker().exportObject(self.state))#this instance of BlockEndMarker does only exist during this export.
else:
break
#Check if any new Barlines need to get created. It is possible that since the last item more than one barline needs to get created (Multi Measure Rests)
#Achtung! Barlines are calculated by completed measure. That means it counts until a measure is full, according to the current metrical instruction.
#But metrical instructions themselves need to be calculated at the beginning of the measure. To put them in sync we need to keep track. Therefore we use an ordered dict which provides pairing as well as order.
#Additional benefit is that multiple metrical instructions on the same tick can be handled, eventhough we consider them a user error (according to the users manual)
iftype(previousItem)isMetricalInstructionandnot(self.state.tickindex==0andself.upbeatInTicks):#always start a new metrical section with a barline. Otherwise we don't see a barlines after a long section without barlines.
#There is a distinction between metrical and non-metrical instructions. However, that doesn't matter for barlines. Both produce barlines.
#Empty Metrical Instructions (the default for a new track) disable barlines until a metrical instruction appears which then (re)starts the metrical cycles.
whiletickRest>=self.upbeatInTicks:#we crossed into the next measure. Or even more measures, for MMRests. By definition the metricalInstruction cannot have changed in the meantime since every tick was occupied by a sinle tick.
#In the end add one final barline if the last measure was complete. This will create the effect, for the GUI and the user, that a measure "closes" once it is complete. There is no chance for a double barline because we use a dict.
ifexpChordislastExpChordorexpChord["baseDuration"]>D8:#invalid beaming. An open beaming group encountering either the end of the track or >D8 is invalid and not counted.
currentlyDuringBeaming=False
beamGroups.pop()#the group so far was invalid because it was not closed properly.
else:
beamGroups[-1].append(expChord)
resultBeamGroups=[]#will be added to the exported meta-data
forbeamGroupinbeamGroups:
ifbeamGroup:#with content
#stem means in staffline coordinates (dots on lines): (starting point, length, 1|-)] 1 stem is on the right or -1 left side of the note.
#We don't deal with the length, stem[1], here. By the time writing this function it was just constant "5" anyway
#min and max below are "reversed" (min for the highest position in upward stems) because we set coordinates in lines and rows with the middle line as origin/0. Important: Stems do not begin at the same line/space as their note. Upstems begin notehead-1, downstems notehead+1
stems=[o["stem"]foroinbeamGroup]#this cannot be a generator because we are using it multiple times.
assertstems
minimum=min(s[0]forsinstems)
maximum=max(s[0]forsinstems)
ifsum(s[2]forsinstems)>=0:#beam upwards direction
direction=1
length=-5
beamPosition=minimum+length
startDotOnLineKeyword="highestPitchAsDotOnLine"
#assert beamPosition <= 0
else:#beam down direction
length=5
beamPosition=maximum+length
direction=-1
startDotOnLineKeyword="lowestPitchAsDotOnLine"
#assert beamPosition > 0
#beams have the same syntax and structure as a stem.
#Until now we figured out the position of the beams as well as their length. However: Mixed duration (e.g. 8th + 16th) groups are allowed and also common. We need to split these groups into sub-groups
#These groups are different than the real groups because they share the same baseline for the beams and also have at least one uninterrupted beam connecting all sub-groups.
subBeamGroups=[[]]
currentSubGroupFlag=beamGroup[0]["flag"]
forobjinbeamGroup:
ifobj["flag"]==currentSubGroupFlag:
subBeamGroups[-1].append(obj)
else:
subBeamGroups.append(list())
currentSubGroupFlag=obj["flag"]
subBeamGroups[-1].append(obj)
forsubBeamGroupinsubBeamGroups:
forobjinsubBeamGroup:
startStaffLine=obj[startDotOnLineKeyword]
length=beamPosition-startStaffLine
obj["beam"]=(startStaffLine,length,direction)#(starting point, length, 1|-)] 1 stem is on the right or -1 left side of the note.
resultBeamGroups.append((firstItem["tickindex"],lastItem["tickindex"]+connectorLength,abs(firstItem["flag"]),beamPosition,direction))#tick-start, tick-end, duration-type, position as staffline. Extra simple for the GUI.
#Beams finished
resultAppend(metaData)
metaData["barlines"]=barlines.keys()
metaData["duration"]=self.state.tickindex#tickindex is now at the end, so this is the end duration. This includes Blocks minimumDuration as well since it is included in left/right
self.sequencerInterface.setTrack([t])#(bytes-blob, position, length) #tickindex is still on the last position, which means the second parameter is the length