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.
 
 

1354 lines
66 KiB

#! /usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Copyright 2017, Nils Hilbricht, Germany ( https://www.hilbricht.net )
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; logging.info("import {}".format(__file__))
#Standard Library Modules
from weakref import WeakValueDictionary, WeakSet
#Third Party Modules
#Template Modules
import template.engine.duration as duration
from template.engine.duration import D1, D4, D1024
from template.helper import pairwise
#Our modules
class GraphItem(object):
"""We use 'CC' here as synonym for whatever int value between 0
and 127, typical 7bit midi values"""
def __init__(self, ccStart):
self.ccStart = ccStart #int
self.graphType = "linear" #options: linear, standalone
self._secondInit(parentBlock = None)
self.lilypondParameters = {}
def _secondInit(self, parentBlock):
"""see Score._secondInit"""
#ignore parentBlock
pass
@classmethod
def instanceFromSerializedData(cls, serializedObject, parentObject):
"""see Score.instanceFromSerializedData"""
assert cls.__name__ == serializedObject["class"]
self = cls.__new__(cls)
self.ccStart = int(serializedObject["ccStart"])
self.graphType = serializedObject["graphType"]
self.lilypondParameters = serializedObject["lilypondParameters"]
self._secondInit(parentBlock = parentObject)
return self
def serialize(self):
result = {}
result["class"] = self.__class__.__name__
result["ccStart"] = self.ccStart
result["graphType"] = self.graphType
result["lilypondParameters"] = self.lilypondParameters
return result
def copy(self):
new = GraphItem(self.ccStart)
new.graphType = self.graphType
new.lilypondParameters = self.lilypondParameters.copy()
return new
def linearRepresentation(self, ccEnd, tickStart, tickEnd):
"""
the variable is taken from the standard formula: f(x) = m*x + n
m = (x2-x1) / (y2-y1)
x1 = tick start
y1 = CC start
x2 = tick end
y2 = CC end
tickStart and tickEnd are absolute values for the complete track
This means we can directly export them to calfbox.
result is tuples (ccValue, tickPOsitionOfThatCCValue)
"""
assert 0 <= self.ccStart < 128
assert 0 <= ccEnd < 128
result = []
if ccEnd == self.ccStart: #there is no interpolation. It is just one value.
result.append((self.ccStart, tickStart))
return result
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.
if ccEnd > 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)
for ccValue in iteratorList:
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.
assert result
assert result[0][1] == tickStart
return result
def staticRepresentation(self, ccEnd, tickStart, tickEnd):
if self.graphType == "standalone" or tickEnd < 0 or ccEnd < 0:
result = [(self.ccStart, tickStart)]
elif self.graphType == "linear":
result = self.linearRepresentation(ccEnd, tickStart, tickEnd)
else:
raise ValueError("Graph Type unknown:", self.graphType)
assert result
return result
class GraphBlock(object):
"""Basically a variant of structures.Block, but not for Item type,
but for GraphItem.
Relative to the blocks beginning tick position, handled by the
GraphTrack, in self.data there are:
key = tick position
value = GraphItem
GraphBlocks have a fixed duration. The user has to align them
manually.
"""
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.
def __init__(self, parentGraphTrack):
self.data = {0:GraphItem(0)} #content linked, mutable.
self.name = str(id(self))
self._duration = [D1*4] #len is always 1. Value is always >= D1*4. Duration is content linked, thats why it is a mutable list of len==1.
#self.duration = 0 #triggers the setter. in reality this is set to a standard minimum value in def duration
self.linkedContentBlocks = WeakSet() #only new standalone blocks use this empty WeakSet. once you contentLink a block it will be overwritten.
self._secondInit(parentGraphTrack)
def _secondInit(self, parentGraphTrack):
"""see Score._secondInit"""
self.linkedContentBlocks.add(self)
self.rememberBlock()
self.parentGraphTrack = parentGraphTrack
@property
def duration(self):
return self._duration[0]
@duration.setter
def duration(self, newValue):
"""Keep the mutable list at all cost"""
if newValue > 0 and newValue < D1*4:
newValue = D1*4
listId = id(self._duration)
self._duration.pop()
self._duration.append(newValue)
assert len(self._duration) == 1
assert listId == id(self._duration)
def rememberBlock(self):
oid = id(self)
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.
#weakref_finalize(self, print, "deleted block "+str(oid))
return oid
@classmethod
def instanceFromSerializedData(cls, serializedObject, parentObject):
"""see Score.instanceFromSerializedData"""
assert cls.__name__ == serializedObject["class"]
self = cls.__new__(cls)
self.parentGraphTrack = parentObject
if serializedObject["data"] is None:
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.data = {int(position):GraphItem.instanceFromSerializedData(item, parentObject = self) for position, item in serializedObject["data"].items()}
GraphBlock.firstBlockWithNewContentDuringDeserializeToObject[serializedObject["contentLinkGroup"]] = self
self.linkedContentBlocks = WeakSet()
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)
return self
def serialize(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
for block in self.parentGraphTrack.blocks:
blockId = id(block)
dataId = id(block.data)
if dataId == contentLinkGroupId:
if blockId == id(self): #first block with this dataId found.
result["data"] = {int(itemTickPosition):item.serialize() for itemTickPosition, item in self.data.items()}
else: #content linked, but not the first. Block already serialized.
result["data"] = None #we don't need to do anything more. The rest is handled by load and instanceFromSerializedData
break
else:
raise StopIteration("self block not found in graphTrack.blocks")
#loop ran through. This never happens.
return result
def getDataAsDict(self):
return { "name" : self.name,
"duration" : self.duration,
}
def putDataFromDict(self, dataDict):
"""modify inplace. Useful for a gui function. Compatible with
the data from getDataAsDict"""
self.name = dataDict["name"]
self.duration = dataDict["duration"]
def copy(self):
"""Return an independet copy of this block."""
new = type(self)(parentGraphTrack = self.parentGraphTrack)
assert len(new.linkedContentBlocks) == 1 #even a copy of a linked block becomes a stand-alone copy
for itemTickPosition, item in self.data.items():
new.data[itemTickPosition] = item.copy()
if self.name.endswith("-copy"):
new.name = self.name
else:
new.name = self.name + "-copy"
new._duration = self._duration[:] #mutable
return new
def contentLink(self):
"""Return a copy where only certain parameters
like Content are linked. Others can be changed"""
new = type(self)(parentGraphTrack = self.parentGraphTrack)
new.linkedContentBlocks = self.linkedContentBlocks
new.linkedContentBlocks.add(new)
new.data = self.data
new.name = self.name
new._duration = self._duration #mutable
return new
def getUnlinkedData(self):
"""Set and handled for undo/redo by the api"""
newData = {}
linkedContentBlocks = WeakSet()
linkedContentBlocks.add(self)
for itemTickPosition, item in self.data.items():
copy = item.copy()
newData[itemTickPosition] = copy
copy.linkedContentBlocks = linkedContentBlocks
return newData, 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.
def linkedContentBlocksInScore(self):
"""Named for compatibility with structures block.
Added a safety net to make sure only blocks which are actually in the timeline will be
returned. Not those in the undo buffer.
The return is sorted to be in the actual track order.
"""
assert len(self.linkedContentBlocks) >= 1
blocksInTrack = [block for block in self.linkedContentBlocks if block.parentGraphTrack]
ordered = sorted(blocksInTrack, key = lambda block: self.parentGraphTrack.blocks.index(block))
return ordered
def insert(self, graphItem, tickPositionRelativeToBlockStart):
self.data[tickPositionRelativeToBlockStart] = graphItem
return True #cannot fail, for now. the api still waits for a positive return code
def find(self, graphItem):
"""find a relative tick position by graphItem.
We already know that the graphItem is in this block. Now we
need to find the position."""
assert graphItem in self.data.values()
for position, item in self.data.items():
if item is graphItem:
return position
else:
raise ValueError("graphItem not found in this block", graphItem, self)
def remove(self, tickPositionRelativeToBlockStart):
if not tickPositionRelativeToBlockStart == 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.
assert tickPositionRelativeToBlockStart != min(self.data.keys()) #this can't be the first item in this block.
del self.data[tickPositionRelativeToBlockStart]
return True
return False
def move(self, tickPositionRelativeToBlockStart, newPosition):
"""we don't allow the first item to be moved. Makes
things easier and clearer for the user"""
if tickPositionRelativeToBlockStart > 0 and not tickPositionRelativeToBlockStart == 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.
self.data[newPosition] = self.data[tickPositionRelativeToBlockStart]
del self.data[tickPositionRelativeToBlockStart]
def deleteHiddenItems(self):
if self.getMaxContentPosition() > self.duration:
toDelete = [pos for pos in self.data.keys() if pos > 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.
for delPos in toDelete:
del self.data[delPos]
def exportsAllItems(self):
"""Does the current self.duration prevent GraphItems from
getting exported?"""
if max(self.data) > self.duration:
return False
else:
return True
def getMaxContentPosition(self):
value = max(sorted(self.data.keys()))
return value
def staticRepresentation(self):
"""list of (tickPositionRelativeToBlockStart, GraphItem) tuples.
Only exports items which fit into this blocks duration."""
sortedListOfTuples = []
for tickPosition, graphItem in self.data.items():
if tickPosition <= self.duration:
sortedListOfTuples.append((tickPosition, graphItem))
sortedListOfTuples.sort()
return sortedListOfTuples
def extendToTrackLength(self, myParentCCTrack):
"""This is a user-called command intended for the last block
in a track
Why not extend the last block automatically?
First these are too many updates so performance goes down.
Second it confuses the user when he/she attempts to split
or append. Especially append.
"""
assert self is myParentCCTrack.blocks[-1]
plainScoreDuration = myParentCCTrack.score.duration()
durationWithoutLastBlock = myParentCCTrack.durationWithoutLastBlock()
#print ("score:", plainScoreDuration, "allOther", TempoBlock.tempoTrack.durationWithoutLastBlock())
if plainScoreDuration > 0 and plainScoreDuration > durationWithoutLastBlock:
self.duration = plainScoreDuration - durationWithoutLastBlock
class GraphTrackCC(object):
"""A track for midi Control Changes.
There is no cursor and no state. Just a sequence of blocks.
The actual CC value comes from the track."""
def __init__(self, cc, parentTrack):
firstBlock = GraphBlock(parentGraphTrack = self)
self.blocks = [firstBlock] #there is always at least one block.
self.cc = cc
self._secondInit(parentTrack = parentTrack)
def _secondInit(self, parentTrack):
"""see Score._secondInit"""
self.parentTrack = parentTrack
self.score = parentTrack.parentData
@classmethod
def instanceFromSerializedData(cls, serializedObject, parentObject):
"""see Score.instanceFromSerializedData"""
assert cls.__name__ == serializedObject["class"]
self = cls.__new__(cls)
self.cc = int(serializedObject["cc"])
self.blocks = [GraphBlock.instanceFromSerializedData(block, parentObject = self) for block in serializedObject["blocks"]]
self._secondInit(parentTrack = parentObject)
return self
def serialize(self):
result = {}
result["class"] = self.__class__.__name__
result["cc"] = self.cc
result["blocks"] = [block.serialize() for block in self.blocks]
return result
def durationWithoutLastBlock(self):
result = 0
for block in self.blocks[:-1]: #returns empty list when only one block in self.blocks. So no loop will happen and this function returns 0
result += block.duration
return result
def asListOfBlockIds(self):
"""Return an ordered list of block ids"""
result = []
for block in self.blocks:
result.append(id(block))
return result
def appendGraphBlock(self):
"""A simple method to add a new GraphBlock at the end of the
current track. Basically you can do the same with split and
resize, but this is much more easier."""
new = GraphBlock(parentGraphTrack = self)
return self.appendExistingGraphBlock(new)
def appendExistingGraphBlock(self, graphBlock):
self.blocks.append(graphBlock)
return graphBlock
def splitGraphBlock(self, graphBlock, positionInTicksRelativeToBlock):
"""The new block will be right of the original content. If the new block will be empty
or has no real start value the last prevailing value will be used
as the blocks start-point"""
#TODO: refactoring. This is bascially a copy of splitTempoBlock except the block is not an id here. And the name.
block = graphBlock
assert block.duration > positionInTicksRelativeToBlock
hasPointAtZero = False
toDelete = []
modelNewBlock = GraphBlock(parentGraphTrack = self) #will not be used directly, but instead content links will be created.
for pos, item in block.data.items():
#Determine if a ccPoint gets to move to the new block
if pos >= positionInTicksRelativeToBlock:
if pos == positionInTicksRelativeToBlock:
hasPointAtZero = True
modelNewBlock.data[pos - positionInTicksRelativeToBlock] = item
toDelete.append(pos)
#else: ccPoint stays in the old block
for pos in toDelete:
del block.data[pos]
#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.
if not hasPointAtZero:
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.
for bl in block.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.
link.duration = block.duration - positionInTicksRelativeToBlock
block.duration = positionInTicksRelativeToBlock
del GraphBlock.allBlocks[id(modelNewBlock)] #Clean up the model block since it was never intended to go into the TempoTrack
return True
def mergeWithNextGraphBlock(self, graphBlock):
"""see structures block (the music block) for an explanation about merging content linked
blocks.
Hidden items (after the current duration value) of the original block
will be deleted. Hidden items of the follow-up block will be merged, but stay hidden.
"""
#TODO: refactoring. This is bascially a copy of splitTempoBlock except the block is not an id here. And the name.
block = graphBlock
if len(self.blocks) == 1:
#print ("only one block in the track")
return False
blockIndex = self.blocks.index(block)
if blockIndex+1 == len(self.blocks):
#print ("not for the last block")
return False
nextIndex = blockIndex + 1
nextBlock = self.blocks[nextIndex]
firstBlock_endingValue = block.data[block.getMaxContentPosition()].ccStart
nextBlock_startingValue = nextBlock.data[0].ccStart
startDurationToReturnForUndo = block.duration
if len(block.linkedContentBlocksInScore()) == 1 and len(nextBlock.linkedContentBlocksInScore()) == 1: #both blocks are standalone. no content-links. This also prevents that both blocks are content links of each other.
block.deleteHiddenItems()
if firstBlock_endingValue == nextBlock_startingValue: #remove redundancy
del nextBlock.data[0]
for pos, item in nextBlock.data.items():
assert not pos + block.duration in block.data
block.data[pos + block.duration] = item
block.duration += nextBlock.duration
self.deleteBlock(nextBlock)
return startDurationToReturnForUndo
elif len(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.
#Further testing required:
for firstBlock, secondBlock in zip(block.linkedContentBlocksInScore(), nextBlock.linkedContentBlocksInScore()):
if firstBlock.data is secondBlock.data: #content link of itself in succession. Very common usecase, but not compatible with merging.
#print ("content link follows itself")
return False
elif not self.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.
#print ("not all blocks-pairs are next to each other")
return False
#Test complete without exit. All blocks can be paired up.
#print ("Test complete. Merging begins")
block.deleteHiddenItems()
if firstBlock_endingValue == nextBlock_startingValue: #remove redundancy
del nextBlock.data[0]
for pos, item in nextBlock.data.items():
assert not pos + block.duration in block.data #this includes pos==0, if not deleted above.
block.data[pos + block.duration] = item
def deleteBlock(self, graphBlock):
"""at least one block. If you want to delete the track
use api.deleteGraphTrackCC"""
if len(self.blocks) > 1:
graphBlock.parentGraphTrack = None
self.blocks.remove(graphBlock)
return graphBlock
def duplicateBlock(self, graphBlock):
index = self.blocks.index(graphBlock)
copy = graphBlock.copy()
self.blocks.insert(index +1, copy)
def duplicateContentLinkBlock(self, graphBlock):
index = self.blocks.index(graphBlock)
linked = graphBlock.contentLink()
self.blocks.insert(index +1, linked)
def blocksAsDict(self):
"""Key is the block id, value the block instance"""
result = {}
for block in self.blocks:
result[id(block)] = block
return result
def rearrangeBlocks(self, listOfBlockIds):
"""Reorder the blocks in this track.
Achtung! Not including a block will delete this block
This is not allowed so we check for it."""
#blocksDict = self.blocksAsDict()
newBlockArrangement = []
for idLong in listOfBlockIds:
#newBlockArrangement.append(blocksDict[idLong])
newBlockArrangement.append(GraphBlock.allBlocks[idLong]) #all blocks includes deleted blocks in the undo memory.
#a block is a unique unit in Laborejo2. Let's make sure there are no duplicates.
#http://stackoverflow.com/questions/480214/how-do-you-remove-duplicates-from-a-list-in-python-whilst-preserving-order
seen = set()
seen_add = seen.add
self.blocks = [ x for x in newBlockArrangement if x not in seen and not seen_add(x)]
assert self.blocks
def staticRepresentation(self):
typeString = ""
sumOfBlocksDurationsWithoutCurrent = 0
result = []
pblob = bytes() # Create a binary blob that contains the MIDI events for CC tracks
self.cleanBlockEdges()
for blockIndex in range(len(self.blocks)):
block = self.blocks[blockIndex]
assert len(block.data) > 0
l = block.staticRepresentation() #this takes care that only user items which fit in the block.duration are exported.
for itemIndex in range(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.
if itemIndex is len(l)-1: #len counts from 1, itemIndex from 0.
assert thisGraphItem is l[-1][1] #l[-1] is a tuple (tickPositionFromBlockStart, GraphItem)
#Is there another block after this one?
if block is self.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.)
if typeString == "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.
else:
assert thisPosition >= 0
assert nextPosition >= 0
typeString = "user" #interpolated, lastInTrack
userItemAndInterpolatedItemsPositions = thisGraphItem.staticRepresentation(nextGraphItem.ccStart, thisPosition, nextPosition)
for ccValue, generatedTickPosition in userItemAndInterpolatedItemsPositions: #generatedTickPosition is relative to the block beginning.
if typeString == "user" or typeString == "lastInTrack": #interpolated items can be anywhere. We don't care much about them.
assert generatedTickPosition <= block.duration
assert 127 >= ccValue >= 0
assert generatedTickPosition >= 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.
if self.parentTrack.ccChannels:
for channel in self.parentTrack.ccChannels:
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.
pblob += blob
else:
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.
pblob += 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.
if sumOfBlocksDurationsWithoutCurrent > 0:
if self.calfboxTheWholeTrackAsPattern:
self.calfboxTheWholeTrackAsPattern[0].delete()
self.calfboxTheWholeTrackAsPattern = None
self.calfboxTheWholeTrackAsPattern = (cbox.Document.get_song().pattern_from_blob(pblob, sumOfBlocksDurationsWithoutCurrent), sumOfBlocksDurationsWithoutCurrent) #sumOfBlocksDurationsWithoutCurrent now even includes the current one. See above.
assert self.calfboxTheWholeTrackAsPattern
self.clearCalfboxTrack() #does NOT clear calfboxTheWholeTrackAsPattern
trackAsMidiData, trackPlainLengthInTicks = self.calfboxTheWholeTrackAsPattern
self.calfboxTrack.add_clip(0, 0, trackPlainLengthInTicks, trackAsMidiData) #pos, offset, length(and not end-position, but is the same), pattern
self.score.session.playbackNeedsUpdate = True
#Before calfbox converts this python data into C data we need to call cbox.Document.get_song().update_playback(). This is done when playback starts and checks if self.score.session.playbackNeedsUpdate.
return result
def staticGraphBlocksRepresentation(self):
"""Return a sorted list"""
result = []
tickCounter = 0
for block in self.blocks:
result.append({"type" : "GraphBlock", "id":id(block), "name":block.name, "duration":block.duration, "position":tickCounter, "exportsAllItems":block.exportsAllItems()})
tickCounter += block.duration
return result
def staticTrackRepresentation(self):
result = {}
if type(self) is TempoTrack:
mini, maxi = self.getMinimumAndMaximumValues()
result["minimumAbsoluteTempoValue"] = mini
result["maximumAbsoluteTempoValue"] = maxi
return result
def graphItemById(self, graphItemId):
for graphBlock in self.blocks:
for tickPosition, graphItem in graphBlock.staticRepresentation(): # a list of tuples
if id(graphItem) == graphItemId:
return graphBlock, graphItem
else:
raise ValueError("graphItemId not found in this track", graphItemId)
def cleanBlockEdges(self):
"""If a first block has an item at tick 300 and a second block is resized to begin at
tick 300 as well then the last item and the start-item are in the same spot.
The last item needs to go away."""
for block in self.blocks:
if block.getMaxContentPosition() == block.duration:
block.remove(block.duration)
class TempoItem(object):
"""A tempo Item exports to Lilypond.
It also interpolates to the next tempo item, but the steps
in between show only up in the playback representation,
not in lilypond"""
allTempoItems = WeakValueDictionary() #key is the id, value is the weak reference to the tempo item
#tempoTrack = the only tempo track. set by tempo track init, which happens only once in the program.
def __init__(self, unitsPerMinute, referenceTicks): # 120 quarter notes per minute is unitsPerMinute=120, referenceTicks=D4
self.unitsPerMinute = round(unitsPerMinute)
self.referenceTicks = round(referenceTicks)
self.graphType = "standalone" #options: linear, standalone
self.lilypondParameters = {"tempo":""} #for example 'Allegro' or 'Ein bischen schneller'. Only strings are allowed.
self._secondInit(parentBlock = None)
def _secondInit(self, parentBlock):
"""see Score._secondInit"""
#ignore parentBlock
self.rememberTempoItem()
@classmethod
def instanceFromSerializedData(cls, serializedObject, parentObject):
"""see Score.instanceFromSerializedData"""
assert cls.__name__ == serializedObject["class"]
self = cls.__new__(cls)
self.unitsPerMinute = int(serializedObject["unitsPerMinute"])
self.referenceTicks = int(serializedObject["referenceTicks"])
self.graphType = serializedObject["graphType"]
self.lilypondParameters = serializedObject["lilypondParameters"]
self._secondInit(parentBlock = parentObject)
return self
def serialize(self):
result = {}
result["class"] = self.__class__.__name__
result["unitsPerMinute"] = self.unitsPerMinute
result["referenceTicks"] = self.referenceTicks
result["graphType"] = self.graphType
result["lilypondParameters"] = self.lilypondParameters
return result
def rememberTempoItem(self):
oid = id(self)
TempoItem.allTempoItems[oid] = self
#weakref_finalize(self, print, "deleted tempoItem "+str(oid))
return oid
def copy(self):
new = TempoItem(unitsPerMinute = self.unitsPerMinute, referenceTicks = self.referenceTicks)
new.graphType = self.graphType
new.lilypondParameters = self.lilypondParameters
return new
def asAbsoluteTempo(self):
"""Return an absolute tempo value which can be compared.
quarter = 120 will be lower/slower than dotted-quarter=120.
It is quite simple: the more ticks per minute, the faster.
We can just multiplicate the unitsPerMinute with the
referenceTicks.
"""
return round(self.referenceTicks * self.unitsPerMinute / D4) #normalize for a quarter note
def __lt__(self, other): # self < other
return self.asAbsoluteTempo() < other.asAbsoluteTempo()
def __le__(self, other): # self <= other
return self.asAbsoluteTempo() <= other.asAbsoluteTempo()
#defining __eq__ makes the class unhashable and membership tests like if tempoItem in dict.values() will fail!
#def __eq__(self, other): # self == other
# return self.asAbsoluteTempo() == other.asAbsoluteTempo()
def __ne__(self, other): # self != other
return self.asAbsoluteTempo() != other.asAbsoluteTempo()
def __gt__(self, other): # self > other
return self.asAbsoluteTempo() > other.asAbsoluteTempo()
def __ge__(self, other): # self >= other
return self.asAbsoluteTempo() >= other.asAbsoluteTempo()
def linearRepresentation(self, endAbsoluteTempoFromTheNextItem, tickStart, tickEnd):
"""
the variable is taken from the standard formula: f(x) = m*x + n
m = (x2-x1) / (y2-y1)
x1 = tick start
y1 = AbsoluteTempo start
x2 = tick end
y2 = AbsoluteTempo end
tickStart and tickEnd are absolute values for the complete track
This means we can directly export them to calfbox.
result is tuples (asAbsoluteTempo, tickPositionOfThatTempoValue)
"""
result = []
startTempo = self.asAbsoluteTempo()
assert 0 < startTempo
assert 0 < endAbsoluteTempoFromTheNextItem
if endAbsoluteTempoFromTheNextItem == startTempo: #there is no interpolation. It is just one value, the one of the current TempoItem. Basically the same as graphType == "standalone"
result.append(((self.asAbsoluteTempo(), self.unitsPerMinute, self.referenceTicks), tickStart))
return result
m = (tickEnd - tickStart) / (endAbsoluteTempoFromTheNextItem - startTempo) #we need to calculate this after making sure that endAbsoluteTempoFromTheNextItem and start are not the same. Else we get /0
#From here on: We actually need interpolation. Let the math begin.
if endAbsoluteTempoFromTheNextItem > startTempo: #upward slope
iteratorList = list(range(startTempo, endAbsoluteTempoFromTheNextItem)) #20 to 70 results in 20....69
else: #downward slope
iteratorList = list(range(endAbsoluteTempoFromTheNextItem+1, startTempo+1)) #70 to 20 results ins 70...21
iteratorList.reverse()
#Calculate at which tick a given tempoValue will happen. value = m * (i - y1)
for tempoValue in iteratorList:
result.append((tempoValue, tickStart + int(m * (tempoValue - startTempo)))) # int(m * (tempoValue - startTempo)) 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.
assert result
assert result[0][1] == tickStart
result[0] = ((result[0][0], self.unitsPerMinute, self.referenceTicks), result[0][1]) #this will be used later in the track static generation to inform the receiver what the original tempo data was.
return result
def staticRepresentation(self, endAbsoluteTempoFromTheNextItem, tickStart, tickEnd):
if self.graphType == "standalone" or tickEnd < 0 or endAbsoluteTempoFromTheNextItem < 0:
result = [((self.asAbsoluteTempo(), self.unitsPerMinute, self.referenceTicks), tickStart)]
elif self.graphType == "linear":
result = self.linearRepresentation(endAbsoluteTempoFromTheNextItem, tickStart, tickEnd)
else:
raise ValueError("Graph Type unknown:", self.graphType)
assert result
return result
class TempoBlock(GraphBlock):
"""A tempo block holds many different things.
First of all the tempo changes and its interpolations.
The duration of a tempo block is fixed and does not depend
on the content
"""
#allTempoBlocks = WeakValueDictionary() #key is the blockId, value is the weak reference to the Block
allTempoBlocks = {}
firstBlockWithNewContentDuringDeserializeToObject = dict() #this is not resetted anywhere since each load is a program start.
#tempoTrack = the only tempo track. set by tempo track init, which happens only once in the program.
def __init__(self, parentGraphTrack):
super().__init__(self)
#self.parentGraphTrack = parentGraphTrack
self.data = {0:TempoItem(120, D4)} #default is quarter = 120 bmp.
self.name = str(id(self))
self._secondInit(parentGraphTrack)
def _secondInit(self, parentGraphTrack):
"""see Score._secondInit"""
self.linkedContentBlocks.add(self)
self.rememberTempoBlock()
self.parentGraphTrack = parentGraphTrack
@classmethod
def instanceFromSerializedData(cls, serializedObject, parentGraphTrack):
"""see Score.instanceFromSerializedData."""
#TODO: this is nearly the same as the GraphBlock method with the same name. During development it made sense to write to different functions. But this can be simplified by calling super().instanceFromSerializedData
assert cls.__name__ == serializedObject["class"]
self = cls.__new__(cls)
if serializedObject["data"] is None:
firstBlock = TempoBlock.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.data = {int(position):eval(item["class"]).instanceFromSerializedData(item, parentObject = self) for position, item in serializedObject["data"].items()}
TempoBlock.firstBlockWithNewContentDuringDeserializeToObject[serializedObject["contentLinkGroup"]] = self
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.linkedContentBlocks = WeakSet()
#No super() deserialize call here so we need to create all objects directly:
self.name = serializedObject["name"]
self._secondInit(parentGraphTrack)
return self
def serialize(self):
result = super().serialize()
return result
def getDataAsDict(self):
return { "name" : self.name,
"duration" : self.duration, #For save and load we drop the list as mutable value.
}
def putDataFromDict(self, dataDict):
"""modify inplace. Useful for a gui function. Compatible with
the data from getDataAsDict"""
for parameter, value in dataDict.items():
if parameter == "name":
self.name = value
elif parameter == "duration":
self.duration = value
else:
raise ValueError("Block does not have this property", parameter, value)
def getMinimumAndMaximumValues(self):
sort = sorted(self.data.values(), key=lambda x: x.asAbsoluteTempo())
return sort[0].asAbsoluteTempo(), sort[-1].asAbsoluteTempo()
def rememberTempoBlock(self):
oid = id(self)
TempoBlock.allTempoBlocks[oid] = self
#weakref_finalize(self, print, "deleted tempoBlock "+str(oid))
return oid
class TempoTrack(GraphTrackCC):
"""The Tempo Track is Laborejos own independent structure that sends data to
template.sequencer.TempoMap which handles the actual midi/cbox tempo map """
def __init__(self, parentData):
firstBlock = TempoBlock(parentGraphTrack = self)
firstBlock.name = "Default Tempo"
self.blocks = [firstBlock]
self.parentData = parentData
self._secondInit()
def _secondInit(self):
"""see Score._secondInit"""
pass
@classmethod
def instanceFromSerializedData(cls, serializedObject, parentData): #parentData is the score, not a track, like for CCTracks
"""see Score.instanceFromSerializedData"""
assert cls.__name__ == serializedObject["class"]
self = cls.__new__(cls)
self.parentData = parentData
self.blocks = [TempoBlock.instanceFromSerializedData(block, parentGraphTrack = self) for block in serializedObject["blocks"]]
self._secondInit()
return self
def serialize(self):
result = {}
result["class"] = self.__class__.__name__
result["blocks"] = [block.serialize() for block in self.blocks]
return result
def getMinimumAndMaximumValues(self):
allValues = (bl.getMinimumAndMaximumValues() for bl in self.blocks)
mini, maxi = zip(*allValues)
return min(mini), max(maxi)
def appendTempoBlock(self):
"""A simple method to add a new GraphBlock at the end of the
current track. Basically you can do the same with split and
resize, but this is much more easier."""
new = TempoBlock(parentGraphTrack = self)
self.blocks.append(new)
return new
def duplicateBlock(self, tempoBlock):
index = self.blocks.index(tempoBlock)
copy = tempoBlock.copy()
self.blocks.insert(index +1, copy)
def duplicateContentLinkBlock(self, tempoBlock):
index = self.blocks.index(tempoBlock)
linked = tempoBlock.contentLink()
self.blocks.insert(index +1, linked)
def splitTempoBlock(self, tempoBlockId, positionInTicksRelativeToBlock):
"""The new block will be right of the original content. If the new block will be empty
or has no real start value the last prevailing tempo will be used
as the blocks start-point"""
block = self.tempoBlockById(tempoBlockId)
if not block.duration > positionInTicksRelativeToBlock:
return False
hasPointAtZero = False
toDelete = []
modelNewBlock = TempoBlock(parentGraphTrack = self) #will not be used directly, but instead content links will be created.
for pos, item in block.data.items():
#Determine if a tempo point gets to move to the new block
if pos >= positionInTicksRelativeToBlock:
if pos == positionInTicksRelativeToBlock:
hasPointAtZero = True
modelNewBlock.data[pos - positionInTicksRelativeToBlock] = item
toDelete.append(pos)
#else: tempoPoint stays in the old block
for pos in toDelete:
del block.data[pos]
#Since every tempo block comes with a default D4=120 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.
if not hasPointAtZero:
realStartTempoPoint = block.data[block.getMaxContentPosition()] #only the remaining items.
modelNewBlock.data[0] = realStartTempoPoint.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.
for bl in block.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.
link.duration = block.duration - positionInTicksRelativeToBlock
block.duration = positionInTicksRelativeToBlock
del TempoBlock.allTempoBlocks[id(modelNewBlock)] #Clean up the model block since it was never intended to go into the TempoTrack
return True
def mergeWithNextTempoBlock(self, tempoBlockId):
"""see structures block (the music block) for an explanation about merging content linked
blocks.
Hidden items (after the current duration value) of the original block
will be deleted. Hidden items of the follow-up block will be merged, but stay hidden.
"""
self.cleanBlockEdges() #only one tempoItem per tick position, even if they are in different blocks. #TODO: remember this when creating undo for merge!
block = self.tempoBlockById(tempoBlockId)
if len(self.blocks) == 1:
#print ("only one block in the track")
return False
blockIndex = self.blocks.index(block)
if blockIndex+1 == len(self.blocks):
#print ("not for the last block")
return False
nextIndex = blockIndex + 1
nextBlock = self.blocks[nextIndex]
firstBlock_endingTempo = block.data[block.getMaxContentPosition()].asAbsoluteTempo()
nextBlock_startingTempo = nextBlock.data[0].asAbsoluteTempo()
startDurationToReturnForUndo = block.duration
if len(block.linkedContentBlocksInScore()) == 1 and len(nextBlock.linkedContentBlocksInScore()) == 1: #both blocks are standalone. no content-links. This also prevents that both blocks are content links of each other.
block.deleteHiddenItems()
if firstBlock_endingTempo == nextBlock_startingTempo: #remove redundancy
del nextBlock.data[0]
for pos, item in nextBlock.data.items():
assert not pos + block.duration in block.data
block.data[pos + block.duration] = item
block.duration += nextBlock.duration
self.deleteBlock(nextBlock)
return startDurationToReturnForUndo
elif len(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.
#Further testing required:
for firstBlock, secondBlock in zip(block.linkedContentBlocksInScore(), nextBlock.linkedContentBlocksInScore()):
if firstBlock.data is secondBlock.data: #content link of itself in succession. Very common usecase, but not compatible with merging.
#print ("content link follows itself")
return False
elif not self.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.
#print ("not all blocks-pairs are next to each other")
return False
#Test complete without exit. All blocks can be paired up.
#print ("Test complete. Merging begins")
block.deleteHiddenItems()
if firstBlock_endingTempo == nextBlock_startingTempo: #remove redundancy
del nextBlock.data[0]
for pos, item in nextBlock.data.items():
assert not pos + block.duration in block.data #this includes pos==0, if not deleted above.
block.data[pos + block.duration] = item
block.duration += nextBlock.duration
for bl in nextBlock.linkedContentBlocksInScore():
self.deleteBlock(bl)
return startDurationToReturnForUndo
else: #The blocks across the track can't be paired up, this cannot possibly work cleanly.
#print ("blocks can't be paired up")
return False
def tempoBlockById(self, tempoBlockId):
return TempoBlock.allTempoBlocks[tempoBlockId]
def expandLastBlockToScoreDuration(self):
"""This can lead to unexpected behaviour if the last block is content-linked and will resize
automatically when the score-duration gets longer. All the other content links will resize
as well.
This is expected behaviour and there is a note in the users manual to prevent that confusion"""
block = self.blocks[-1]
plainScoreDuration = self.parentData.duration()
durationWithoutLastBlock = self.durationWithoutLastBlock()
if plainScoreDuration > 0:# and plainScoreDuration - durationWithoutLastBlock > block.duration:
block.duration = plainScoreDuration - durationWithoutLastBlock
return block.duration
else:
block.duration = D1*4#plainScoreDuration - durationWithoutLastBlock
return block.duration
def tempoBlocKByAbsolutePosition(self, tickPositionAbsolute):
"""The staticBlockRepresentation is in correct, ascending, order.
We reverse it for testing >= so we can check directly in a for-loop for the current value.
Each tempo-block start position has a higher tickindex except the block we are searching for
Without reversing we would have to test if the for-loop tempo block is bigger than our
given position and then return the _previous_ block.
(Alternative: loop forward but use position+duration for the test. But why...)
"""
#Remember: this function gets called by the cursor at every change.
for dictExportItem in reversed(self.staticGraphBlocksRepresentation()):
if tickPositionAbsolute >= dictExportItem["position"]: #block start
if not dictExportItem["position"] <= tickPositionAbsolute <= dictExportItem["position"] + dictExportItem["duration"]: #make sure the point is really in this block. It is guaranteed that the tempo track is always at least as long as the music tracks.
raise ValueError("position {} is not within the tempo tracks boundaries".format(tickPositionAbsolute))
return dictExportItem["id"], dictExportItem["position"]
else:
raise ValueError("no block found")
def tempoItemById(self, tempoItemId):
tempoItem = TempoItem.allTempoItems[tempoItemId]
assert tempoItemId == id(tempoItem)
for block in self.blocks:
if tempoItem in block.data.values():
return block, tempoItem
else:
raise ValueError("Block not in the TempoTrack")
def tempoAtTickPosition(self, tickPositionAbsolute):
"""Return unitsPerMinute and referenceTick of the tempo at the given tick positon.
if there is no tempo item at this position search to the left until you find one.
We don't use self.staticRepresentation because that also gives us all interpolated points.
Interpolated points have no units * and reference factors but just a combined value for
midi or a GUI.
"""
blockId, blockStartPos = self.tempoBlocKByAbsolutePosition(tickPositionAbsolute)
block = self.tempoBlockById(blockId)
tickPositions = sorted(block.data.keys()) # tick:tempoItem
tickPositions = reversed(tickPositions) #for an easier search. see tempoBlocKByAbsolutePosition docstring
x = tickPositionAbsolute - blockStartPos
for pos in tickPositions:
if x >= pos: #pos is the tempoItem left of our position. in other words: the tempo currently active at our position.
tempoItem = block.data[pos]
return tempoItem #tempoItem.unitsPerMinute, tempoItem.referenceTicks
def staticGraphBlocksRepresentation(self):
"""Return a sorted list"""
result = []
tickCounter = 0
for block in self.blocks:
result.append({"type" : "GraphBlock", "id":id(block), "name":block.name, "duration":block.duration, "position":tickCounter, "exportsAllItems":block.exportsAllItems(), })
tickCounter += block.duration
return result
def staticRepresentation(self):
"""see structures.Track.staticRepresentation"""
self.expandLastBlockToScoreDuration()
typeString = ""
sumOfBlocksDurationsWithoutCurrent = 0
result = []
pblob = bytes() # Create a binary blob that contains the MIDI events for CC tracks
for blockIndex in range(len(self.blocks)):
block = self.blocks[blockIndex]
assert len(block.data) > 0
l = block.staticRepresentation() #this takes care that only user items which fit in the block.duration are exported.
for itemIndex in range(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.
if itemIndex is len(l)-1: #len counts from 1, itemIndex from 0.
assert thisGraphItem is l[-1][1] #l[-1] is a tuple (tickPositionFromBlockStart, GraphItem)
#Is there another block after this one?
if block is self.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.)
if typeString == "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.
else:
assert thisPosition >= 0
assert nextPosition >= 0
typeString = "user" #interpolated, lastInTrack
userItemAndInterpolatedItemsPositions = thisGraphItem.staticRepresentation(nextGraphItem.asAbsoluteTempo(), thisPosition, nextPosition)
for ccValue, generatedTickPosition in userItemAndInterpolatedItemsPositions: #generatedTickPosition is relative to the block beginning.
additionalData = {}
if typeString == "user" or typeString == "lastInTrack": #interpolated items can be anywhere. We don't care much about them.
assert generatedTickPosition <= block.duration
ccValue, additionalData["unitsPerMinute"], additionalData["referenceTicks"] = ccValue #additional data is not for interpolated items
assert generatedTickPosition >= 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. This is meant for a gui which needs only one combined value to order all items graphically on an axis.
"position" : sumOfBlocksDurationsWithoutCurrent + generatedTickPosition, #generatedTickPosition is relative to the block beginning.
"positionInBlock": generatedTickPosition,
"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,
"graphType" : thisGraphItem.graphType,
"lilypondParameters" : thisGraphItem.lilypondParameters,
}
if additionalData: #additional data is not for interpolated items
exportDictItem.update(additionalData)
result.append(exportDictItem)
typeString = "interpolated" #The next items in userItemAndInterpolatedItemsPositions are interpolated items. Reset once we leave the local forLoop.
#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.
#We duplicate the last exportItem and set it as interpolated tempo point on the last tick.
#This is after the one marked as "lastInTrack".
virtualLast = {
"type" : "interpolated",
"value": exportDictItem["value"],
"position" : self.parentData.duration(),
"positionInBlock": self.parentData.duration() - sumOfBlocksDurationsWithoutCurrent,
"id" : exportDictItem["id"],
"blockId": exportDictItem["blockId"],
"minPossibleAbsolutePosition" : exportDictItem["minPossibleAbsolutePosition"],
"maxPossibleAbsolutePosition" : exportDictItem["maxPossibleAbsolutePosition"],
"lastInBlock" : exportDictItem["lastInBlock"],
"graphType" : exportDictItem["graphType"],
"lilypondParameters" : exportDictItem["lilypondParameters"],
}
virtualLast.update(additionalData)
#Add this to the result AFTER we calculate the midi data. Otherwise it will result in conflicts.
#send tempo information to cbox.
blocksAsDict = self.blocksAsDict()
offsetForBlock = {}
offsetCounter = 0
for block in self.blocks:
offsetForBlock[block] = offsetCounter
offsetCounter += block.duration #after this block is over how many ticks have gone by?
sendToSequencerTempoMap = {} #pos:value
for staticItem in result: #static item is a tempo change as exportDictItem from above.
value = float(abs(staticItem["value"])) #exportItems have negative values. calfbox wants a float.
tempoBlock = blocksAsDict[staticItem["blockId"]]
absoluteBlockStartPosition = offsetForBlock[tempoBlock]
pos = staticItem["positionInBlock"] + absoluteBlockStartPosition
sendToSequencerTempoMap[pos] = value
self.parentData.tempoMap.setTempoMap(sendToSequencerTempoMap)
result.append(virtualLast)
return result
#not in use.
def groupItems(self, ranges, exportItems):
"""this is kind of a waste. All we are doing is
reverse engineering which item was in which tempo-block
... or do we? Alternate Endings give us ranges which
are not block based."""
ranges = list(set(ranges)); ranges.sort() #make unique # ranges = [(start, end), ...]
result = {}
for tup in ranges:
result[tup] = []
for item in exportItems:
for start, end in ranges:
#print (start, end, item["position"])
if start <= item["position"] < end:
result[(start,end)].append(item["position"])
break
else:
raise ValueError("tempo item does not match any range, which should be impossible")
return result
def rearrangeBlocks(self, listOfBlockIds):
"""Reorder the blocks in this track.
Achtung! Not including a block will delete this block."""
#blocksDict = self.blocksAsDict()
newBlockArrangement = []
for idLong in listOfBlockIds:
#newBlockArrangement.append(blocksDict[idLong])
newBlockArrangement.append(TempoBlock.allTempoBlocks[idLong]) #all blocks includes deleted blocks in the undo memory.
#a block is a unique unit in Laborejo2. Let's make sure there are no duplicates.
#http://stackoverflow.com/questions/480214/how-do-you-remove-duplicates-from-a-list-in-python-whilst-preserving-order
seen = set()
seen_add = seen.add
self.blocks = [ x for x in newBlockArrangement if x not in seen and not seen_add(x)]
assert self.blocks
@property
def factor(self):
return self.parentData.tempoMap.factor
def setFactor(self, factor:float):
self.parentData.tempoMap.setFactor(factor)
def lilypond(self):
"""Based on the static export"""
def _ly(tempoItem, nextTempoItem):
if tempoItem["graphType"] in ("standalone", "linear"):
ramp = ""
if not tempoItem["graphType"] == "standalone":
#Accelerando and Deccelerando
if nextTempoItem["value"] < tempoItem["value"]: #value is y-reversed
ramp = '^"accel" '
elif nextTempoItem["value"] > tempoItem["value"]:
ramp = '^"rit" '
elif nextTempoItem["value"] == tempoItem["value"]:
ramp = ""
positionInD1024Skips = int((nextTempoItem["position"] - tempoItem["position"]) / D1024)
skipString = "s1024*{}{}".format(positionInD1024Skips, ramp)
if tempoItem["referenceTicks"] == _ly.lastItem["referenceTicks"] and tempoItem["unitsPerMinute"] == _ly.lastItem["unitsPerMinute"]:
_ly.lastItem = tempoItem
return skipString
else:
_ly.lastItem = tempoItem
return "\\tempo {} {} = {} {}".format(tempoItem["lilypondParameters"]["tempo"], duration.baseDurationToTraditionalNumber[tempoItem["referenceTicks"]], str(tempoItem["unitsPerMinute"]), skipString)
raise NotImplementedError
_ly.lastItem = {"referenceTicks":"", "unitsPerMinute":""}
onlyStandalone = list(sorted((tempoItem for tempoItem in self.staticRepresentation() if not tempoItem["type"] == "interpolated"), key=lambda i: i["position"]))
onlyStandalone = onlyStandalone + [onlyStandalone[-1]] #for pairs
pairs = pairwise(onlyStandalone)
result = " ".join(_ly(tempoItem, nextTempoItem) for tempoItem, nextTempoItem in pairs)
return result