ifccEnd==self.ccStart:#there is no interpolation. It is just one value.
result.append((self.ccStart,tickStart))
returnresult
m=(tickEnd-tickStart)/(ccEnd-self.ccStart)#we need to calculate this after making sure that ccend and start are not the same. Else we get /0
#From here on: We actually need interpolation. Let the math begin.
ifccEnd>self.ccStart:#upward slope
iteratorList=list(range(self.ccStart,ccEnd))#20 to 70 results in 20....69
else:#downward slope
iteratorList=list(range(ccEnd+1,self.ccStart+1))#70 to 20 results ins 70...21
iteratorList.reverse()
#Calculate at which tick a given ccValue will happen. value = m * (i - y1)
forccValueiniteratorList:
result.append((ccValue,tickStart+int(m*(ccValue-self.ccStart))))# int(m * (ccValue - self.ccStart)) results in 0 for the first item (set by the user). so the first item is just tickStart. that does not mean tickStart for the first item is 0. We just normalize to tickStart as virtual 0 here.
firstBlockWithNewContentDuringDeserializeToObject=dict()#this is not resetted anywhere since each load is a program start.
allBlocks={}#key is the blockId, value is the Block. This is a one way dict. It gets never deleted so undo can recover old blocks. Since blocks are unique this is no problem.
GraphBlock.allBlocks[oid]=self#This is on the score level, or a global level. That means we don't need to change this, even if the track gets moved to a new track by the api.
firstBlock=GraphBlock.firstBlockWithNewContentDuringDeserializeToObject[serializedObject["contentLinkGroup"]]#first block with the same contentGroup. This is the one with the real data.
self.data=firstBlock.data
self._duration=firstBlock._duration
self.linkedContentBlocks=firstBlock.linkedContentBlocks#add self to this is in _secondInit
else:#Standalone or First occurence of a content linked block
self._duration=[int(serializedObject["duration"])]#this is just an int, minimumBlockInTicks or so. Not Item.Duration(). For save and load we drop the list as mutable value.
self.name=serializedObject["name"]
self._secondInit(parentObject)
returnself
defserialize(self):
result={}
result["class"]=self.__class__.__name__
result["name"]=self.name
result["duration"]=self.duration#this is just an int, minimumBlockInTicks or so. Not Item.Duration(). For save and load we drop the list as mutable value.
#We only save the data if this is the first content-linked block in a sequence.
contentLinkGroupId=id(self.data)
result["contentLinkGroup"]=contentLinkGroupId
forblockinself.parentGraphTrack.blocks:
blockId=id(block)
dataId=id(block.data)
ifdataId==contentLinkGroupId:
ifblockId==id(self):#first block with this dataId found.
returnnewData,linkedContentBlocks,self._duration[:]#mutable. the api uses the list directly as well because we want to undo/restore the old original list, which may be content linked.
returnTrue#cannot fail, for now. the api still waits for a positive return code
deffind(self,graphItem):
"""find a relative tick position by graphItem.
WealreadyknowthatthegraphItemisinthisblock.Nowwe
needtofindtheposition."""
assertgraphIteminself.data.values()
forposition,iteminself.data.items():
ifitemisgraphItem:
returnposition
else:
raiseValueError("graphItem not found in this block",graphItem,self)
defremove(self,tickPositionRelativeToBlockStart):
ifnottickPositionRelativeToBlockStart==0:#don't allow the first item in a block to be deleted. Since the 0 item can't be moved this will always be the 0 item.
asserttickPositionRelativeToBlockStart!=min(self.data.keys())#this can't be the first item in this block.
"""we don't allow the first item to be moved. Makes
thingseasierandclearerfortheuser"""
iftickPositionRelativeToBlockStart>0andnottickPositionRelativeToBlockStart==newPosition:#otherwise it would just delete the complete item because old and new are the same and there would be no duplicate item to delete.
toDelete=[posforposinself.data.keys()ifpos>self.duration]#must be list, not generator, because we need the positions in advance to delete them later. A generator would delete from the dict while it is still generated.
fordelPosintoDelete:
delself.data[delPos]
defexportsAllItems(self):
"""Does the current self.duration prevent GraphItems from
gettingexported?"""
ifmax(self.data)>self.duration:
returnFalse
else:
returnTrue
defgetMaxContentPosition(self):
value=max(sorted(self.data.keys()))
returnvalue
defstaticRepresentation(self):
"""list of (tickPositionRelativeToBlockStart, GraphItem) tuples.
#Since every block comes with a value at position 0 we need to check if this was already replaced or if we need to adjust it to the real tempo at this point in time.
ifnothasPointAtZero:
realStartCCPoint=block.data[block.getMaxContentPosition()]#only the remaining items.
modelNewBlock.data[0]=realStartCCPoint.copy()
#Now the original block and all its content links have fewer items than before
#For every of the content-linked blocks in the tempo track we need to create a new block and insert it right next to it.
forblinblock.linkedContentBlocksInScore():
index=self.blocks.index(bl)
link=modelNewBlock.contentLink()
self.blocks.insert(index+1,link)
#duration is mutable. All content links change duration as well.
iflen(block.linkedContentBlocksInScore())==1andlen(nextBlock.linkedContentBlocksInScore())==1:#both blocks are standalone. no content-links. This also prevents that both blocks are content links of each other.
eliflen(block.linkedContentBlocksInScore())==len(nextBlock.linkedContentBlocksInScore()):#It is maybe possible to build pairs from the current block and all its content links with the follow up block and all its content links.
elifnotself.blocks.index(firstBlock)+1==self.blocks.index(secondBlock):#all first blocks must be followed directly by a content link of the second block. linkedContentBlocksInScore() returns a blocklist in order so we can compare.
firstBlock._duration=[newFirstBlockDuration,]#Duration is content linked. if we use the setter it will create the wrong sum. Force the new duration instead.
patternBlob=bytes()# Create a binary blob that contains the MIDI events for CC tracks
self.cleanBlockEdges()
forblockIndexinrange(len(self.blocks)):
block=self.blocks[blockIndex]
assertlen(block.data)>0
l=block.staticRepresentation()#this takes care that only user items which fit in the block.duration are exported.
foritemIndexinrange(len(l)):#range starts from 0 so we can use this as index.
#l has tuples (tickPositionFromBlockStart, GraphItem)
thisPosition=l[itemIndex][0]
thisGraphItem=l[itemIndex][1]
#Check if we reached the last item in this block.
ifitemIndexislen(l)-1:#len counts from 1, itemIndex from 0.
assertthisGraphItemisl[-1][1]#l[-1] is a tuple (tickPositionFromBlockStart, GraphItem)
#Is there another block after this one?
ifblockisself.blocks[-1]:#this was the last block?
#there is no nextGraphItem and subsequently no interpolation.
typeString="lastInTrack"# also a switch for later.
nextPosition=-1#doesn't matter
#this is checked later in the loop, before we create the exportDict
else:#there is still a block left after the current
#The nextGraphItem can be found in the next block.
nextBlock=self.blocks[blockIndex+1]
nextBlockL=nextBlock.staticRepresentation()
nextPosition=nextBlockL[0][0]+block.duration#Instead of itemIndex we just need [0] for the first in the next block.
nextGraphItem=nextBlockL[0][1]#Instead of itemIndex we just need [0] for the first
else:#default case. Next item is still in the same block.
nextPosition=l[itemIndex+1][0]
nextGraphItem=l[itemIndex+1][1]
#We now generate a chain of items from the current position to the next,
#at least one, depending on the interpolation type (like linear, none etc.)
iftypeString=="lastInTrack":
userItemAndInterpolatedItemsPositions=thisGraphItem.staticRepresentation(-1,thisPosition,-1)#-1 are magic markers that indicate a forced standalone mode. no interpolation, we just get one item back.
forccValue,generatedTickPositioninuserItemAndInterpolatedItemsPositions:#generatedTickPosition is relative to the block beginning.
iftypeString=="user"ortypeString=="lastInTrack":#interpolated items can be anywhere. We don't care much about them.
assertgeneratedTickPosition<=block.duration
assert127>=ccValue>=0
assertgeneratedTickPosition>=0
exportDictItem={
"type":typeString,
"value":-1*ccValue,#minus goes up because it reduces the line position, which starts from top of the screen. For a tempo this is a bpm value for quarter notes.
"position":sumOfBlocksDurationsWithoutCurrent+generatedTickPosition,#generatedTickPosition is relative to the block beginning.
"id":id(thisGraphItem),
"blockId":id(block),
"minPossibleAbsolutePosition":sumOfBlocksDurationsWithoutCurrent,#If you want to move this item to the left or right, this is only possible within the current block.
"maxPossibleAbsolutePosition":sumOfBlocksDurationsWithoutCurrent+block.duration,#If you want to move this item to the left or right, this is only possible within the current block.
"lastInBlock":len(block.data)==1,
}
result.append(exportDictItem)
typeString="interpolated"#The next items in userItemAndInterpolatedItemsPositions are interpolated items. Reset once we leave the local forLoop.
blob=cbox.Pattern.serialize_event(exportDictItem["position"],0xB0+channel,self.cc,ccValue)#position, status byte+channel, controller number, controller value #TODO use channel of the parent note track at that moment in time.
patternBlob+=blob
else:#If empty then CC uses the tracks initial midi channel.
blob=cbox.Pattern.serialize_event(exportDictItem["position"],0xB0+self.parentTrack.initialMidiChannel,self.cc,ccValue)#position, status byte+channel, controller number, controller value #TODO use channel of the parent note track at that moment in time.
patternBlob+=blob
#Prepare data for the next block.
sumOfBlocksDurationsWithoutCurrent+=block.duration#the naming is only true until the last iteration. Then all blocks, even the current one, are in the sum and can be used below.
assertlen(result)<=len(self.parentTrack.parentData.tracks)#For now we assume that blocks cannot be linked across CCs. This can be removed after we tested it for a while and the time comes for cross-CC links