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.
737 lines
36 KiB
737 lines
36 KiB
#! /usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
Copyright 2022, Nils Hilbricht, Germany ( https://www.hilbricht.net )
|
|
|
|
This file is part of the Laborejo Software Suite ( https://www.laborejo.org ),
|
|
|
|
Laborejo2 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; logger = logging.getLogger(__name__); logger.info("import")
|
|
|
|
#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
|
|
from .ccsubtrack import GraphTrackCC, GraphBlock
|
|
|
|
|
|
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, description=""): # 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": description, #for example 'Allegro' or 'Ein bischen schneller'. Only strings are allowed.
|
|
"hide" : False, #True will skip ly-export, for fermatas and other temporary changes.
|
|
}
|
|
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"]
|
|
if not "hide" in self.lilypondParameters: #Version 2.1.0
|
|
self.lilypondParameters["hide"] = False
|
|
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, defaultQuarterTempo:int= 120):
|
|
super().__init__(self)
|
|
#self.parentGraphTrack = parentGraphTrack
|
|
self.data = {0:TempoItem(defaultQuarterTempo, 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.
|
|
|
|
There is only one TempoTrack instance in the main data strucutre. """
|
|
|
|
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.
|
|
|
|
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):
|
|
"""
|
|
The tempo tracks needs to be at least as long as the score.
|
|
We force it on every staticExport.
|
|
|
|
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.
|
|
We therefore define it to "expected behaviour" and put a note in the users
|
|
manual to prevent that confusion"""
|
|
|
|
lastBlock = self.blocks[-1]
|
|
plainScoreDuration = self.parentData.duration()
|
|
duration = sum(block.duration for block in self.blocks)
|
|
durationWithoutLastBlock = self.durationWithoutLastBlock()
|
|
if plainScoreDuration > 0 and duration < plainScoreDuration:
|
|
lastBlock.duration = plainScoreDuration - durationWithoutLastBlock
|
|
|
|
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 = []
|
|
patternBlob = 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,
|
|
}
|
|
|
|
#There was a problem with this in nuitka 0.6.10 with py 3.9.
|
|
#The full dict was evaluating to False.
|
|
#Working around this was trivial, but let's hope that was not just one example of countless problems.
|
|
#if additionalData: #additional data is not for interpolated items
|
|
# print ("found additional for", id(thisGraphItem), additionalData)
|
|
# exportDictItem.update(additionalData)
|
|
#else:
|
|
# print ("found NO additional for", id(thisGraphItem), additionalData)
|
|
# if "referenceTicks" in additionalData:
|
|
# print ("refTicks in additional!", additionalData)
|
|
exportDictItem.update(additionalData) #updating with an empty dict is ok as well.
|
|
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 = {} #{positionInTicks:(bpmAsFloat, timesigUpper, timesigLower)}
|
|
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, 4, 4) #TODO: faked 4/4 meter because we do not combine tempo and timesig, but midi and jack wants to combine it.
|
|
|
|
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 staticTrackRepresentation(self):
|
|
result = {}
|
|
mini, maxi = self.getMinimumAndMaximumValues()
|
|
result["minimumAbsoluteTempoValue"] = mini
|
|
result["maximumAbsoluteTempoValue"] = maxi
|
|
return result
|
|
|
|
|
|
def lilypond(self):
|
|
"""Based on the static export"""
|
|
def _ly(tempoItem, nextTempoItem):
|
|
|
|
if tempoItem["lilypondParameters"]["hide"]:
|
|
return ""
|
|
|
|
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
|
|
t = tempoItem["lilypondParameters"]["tempo"]
|
|
d = duration.ticksToLilypond(tempoItem["referenceTicks"])
|
|
upm = str(tempoItem["unitsPerMinute"])
|
|
if t: #we have a custom string like "Allegro"
|
|
return f"\\tempo \"{t}\" {d} = {upm} {skipString}"
|
|
else:
|
|
return f"\\tempo {d} = {upm} {skipString}"
|
|
|
|
#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
|
|
|