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.
253 lines
14 KiB
253 lines
14 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 ),
|
|
|
|
This application 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 typing import List, Set, Dict, Tuple
|
|
|
|
#Third Party Modules
|
|
|
|
#Template Modules
|
|
import template.engine.sequencer
|
|
|
|
#Our modules
|
|
from .pattern import Pattern
|
|
|
|
class Track(object): #injection at the bottom of this file!
|
|
"""The pattern is same as the track, even if the GUI does not represent it that way
|
|
|
|
Init parameters are for cloned and copied tracks, but not for loading from json.
|
|
"""
|
|
|
|
def __init__(self, parentData,
|
|
name:str="",
|
|
structure:Set[int]=None,
|
|
scale:Tuple[int]=None,
|
|
color:str=None,
|
|
whichPatternsAreScaleTransposed:Dict[int,int]=None,
|
|
whichPatternsAreHalftoneTransposed:Dict[int,int]=None,
|
|
simpleNoteNames:List[str]=None,
|
|
whichPatternsAreStepDelayed:Dict[int,int]=None,
|
|
whichPatternsHaveAugmentationFactor:Dict[int,float]=None,
|
|
stepDelayWrapAround:bool=False,
|
|
repeatDiminishedPatternInItself:bool=False,
|
|
):
|
|
|
|
self.parentData = parentData
|
|
self.sequencerInterface = template.engine.sequencer.SequencerInterface(parentTrack=self, name=name) #needs parentData
|
|
self.color = color if color else "#00FFFF" # "#rrggbb" in hex. no alpha. a convenience slot for the GUI to save a color.
|
|
|
|
#2.0
|
|
#The distinction between Track and Pattern in our code is artificial, it is the same thing,
|
|
#we take inspiration from the GUI that presents the Track on its own.
|
|
#The following setting is most likely to be found in the track sub-window:
|
|
self.patternLengthMultiplicator = 1 #int. >= 1 the multiplicator is added after all other calculations, like subdivions. We can't integrate this into howManyUnits because that is the global score value
|
|
self.midiChannel = 0 # 0-15 midi channel is always set.
|
|
|
|
#2.1
|
|
self.group = "" # "" is a standalone track, the normal one which existed since version 1.0. Using a name here will group these tracks together. A GUI can use this information. Also all tracks in a group share a single jack out port.
|
|
self.visible = True #only used together with groups. the api and our Datas setGroup function take care that standalone tracks are never hidden.
|
|
self.stepDelayWrapAround = stepDelayWrapAround #every note that falls down on the "right side" of the pattern will wrap around to the beginning.
|
|
self.repeatDiminishedPatternInItself = repeatDiminishedPatternInItself # if augmentationFactor < 1: repeat the pattern multiple times, as many as fit into the measure.
|
|
|
|
self.pattern = Pattern(parentTrack=self, scale=scale, simpleNoteNames=simpleNoteNames)
|
|
self.structure = structure if structure else set() #see buildTrack(). This is the main track data structure besides the pattern. Just integers (starts at 0) as switches which are positions where to play the patterns. In between are automatic rests.
|
|
self.whichPatternsAreScaleTransposed = whichPatternsAreScaleTransposed if whichPatternsAreScaleTransposed else {} #position:integers between -7 and 7. Reversed pitch, row based: -7 is higher than 7!!
|
|
self.whichPatternsAreHalftoneTransposed = whichPatternsAreHalftoneTransposed if whichPatternsAreHalftoneTransposed else {} #position:integers between -7 and 7. Reversed pitch, row based: -7 is higher than 7!!
|
|
self.whichPatternsAreStepDelayed = whichPatternsAreStepDelayed if whichPatternsAreStepDelayed else {} #position:signed integer
|
|
self.whichPatternsHaveAugmentationFactor = whichPatternsHaveAugmentationFactor if whichPatternsHaveAugmentationFactor else {} #position:float > 0
|
|
self._processAfterInit()
|
|
|
|
@property
|
|
def cboxMidiOutAbstraction(self):
|
|
"""Returns the real cbox midi out for standalone tracks and the bus one for groups"""
|
|
if self.group:
|
|
return self.parentData.groups[self.group].cboxMidiOutUuid
|
|
else:
|
|
return self.sequencerInterface.cboxMidiOutUuid
|
|
|
|
def _processAfterInit(self):
|
|
pass
|
|
|
|
def buildTrack(self):
|
|
"""The goal is to create a cbox-track, consisting of cbox-clips which hold cbox-pattern,
|
|
generated with our own note data. The latter happens in structures_pattern.
|
|
|
|
This is called after every small change.
|
|
"""
|
|
|
|
if self.group:
|
|
#We still have an inactive sequencerinterface but instead we use the group ones as subtrack.
|
|
ourCalfboxTrack = self.parentData.groups[self.group]._subtracks[id(self)].calfboxSubTrack
|
|
else:
|
|
ourCalfboxTrack = self.sequencerInterface.calfboxTrack
|
|
|
|
if ourCalfboxTrack == "":
|
|
#we are running in --mute mode. Why is that needed?! We should not test for this.
|
|
#Because the cbox null module works by forwarding the requests to status() and then we get back a real object.
|
|
#However, we fail in the assert below, that tests directly for ourCalfboxTrack and not just calls a function.
|
|
#In a positive way we can see that as optimisation :)
|
|
#print ("mute mode", type(ourCalfboxTrack.status()))
|
|
return
|
|
|
|
assert ourCalfboxTrack, (ourCalfboxTrack, self, self.group, self.parentData, self.sequencerInterface)
|
|
|
|
#First clean all modifications from the default value.
|
|
#We test for k in self.structure to not have modifications for measures that will be switched on later. This way when you deactivate a measure it will delete modifications. However, that was inconvenient. See below:
|
|
#Can this possibly lead to a race condition where we load modifications first and then load the structure, which results in the mods getting perma-deleted here. e.g. we could not set default value in init, even for testing purposes.
|
|
#Yes, this happened with undo. every {comprehension} had a "and if k in self.structure" at the end. We took that out without remembering what that was for.
|
|
self.whichPatternsAreScaleTransposed = {k:v for k,v in self.whichPatternsAreScaleTransposed.items() if v!=0}
|
|
self.whichPatternsAreHalftoneTransposed = {k:v for k,v in self.whichPatternsAreHalftoneTransposed.items() if v!=0}
|
|
self.whichPatternsAreStepDelayed = {k:v for k,v in self.whichPatternsAreStepDelayed.items() if v!=0}
|
|
self.whichPatternsHaveAugmentationFactor = {k:v for k,v in self.whichPatternsHaveAugmentationFactor.items() if v!=1.0} #default is 1.0
|
|
|
|
oneMeasureInTicks = (self.parentData.howManyUnits * self.parentData.whatTypeOfUnit) / self.parentData.subdivisions #subdivisions is 1 by default. bigger values mean shorter values, which is compensated by the user setting bigger howManyUnits manually.
|
|
oneMeasureInTicks = int(oneMeasureInTicks) * self.patternLengthMultiplicator
|
|
|
|
filteredStructure = [index for index in sorted(self.structure) if index < self.parentData.numberOfMeasures] #not <= because we compare count with range
|
|
cboxclips = [o.clip for o in ourCalfboxTrack.status().clips]
|
|
|
|
globalOffset = self.parentData.cachedOffsetInTicks
|
|
|
|
for cboxclip in cboxclips:
|
|
cboxclip.delete() #removes itself from the track
|
|
for index in filteredStructure:
|
|
scaleTransposition = self.whichPatternsAreScaleTransposed[index] if index in self.whichPatternsAreScaleTransposed else 0
|
|
halftoneTransposition = self.whichPatternsAreHalftoneTransposed[index] if index in self.whichPatternsAreHalftoneTransposed else 0
|
|
stepDelay = self.whichPatternsAreStepDelayed[index] if index in self.whichPatternsAreStepDelayed else 0
|
|
augmentationFactor = self.whichPatternsHaveAugmentationFactor[index] if index in self.whichPatternsHaveAugmentationFactor else 1
|
|
cboxPattern = self.pattern.buildPattern(scaleTransposition, halftoneTransposition, self.parentData.howManyUnits, self.parentData.whatTypeOfUnit, self.parentData.subdivisions, self.patternLengthMultiplicator, stepDelay, augmentationFactor)
|
|
r = ourCalfboxTrack.add_clip(globalOffset + index*oneMeasureInTicks, 0, oneMeasureInTicks, cboxPattern) #pos, pattern-internal offset, length, pattern.
|
|
|
|
######Old optimisations. Keep for later####
|
|
##########################################
|
|
#if changeClipsInPlace: #no need for track.buildTrack. Very cheap pattern exchange.
|
|
# cboxclips = [o.clip for o in self.parentTrack.sequencerInterface.calfboxTrack.status().clips]
|
|
# for cboxclip in cboxclips:
|
|
# cboxclip.set_pattern(self.cboxPattern[cboxclip.patroneoScaleTransposed])
|
|
|
|
|
|
#Save / Load / Export
|
|
|
|
def serialize(self)->dict:
|
|
return {
|
|
"sequencerInterface" : self.sequencerInterface.serialize(),
|
|
"color" : self.color,
|
|
"structure" : list(self.structure),
|
|
"pattern" : self.pattern.serialize(),
|
|
"whichPatternsAreScaleTransposed" : self.whichPatternsAreScaleTransposed,
|
|
"whichPatternsAreHalftoneTransposed" : self.whichPatternsAreHalftoneTransposed,
|
|
"patternLengthMultiplicator" : self.patternLengthMultiplicator,
|
|
"whichPatternsAreStepDelayed" : self.whichPatternsAreStepDelayed,
|
|
"whichPatternsHaveAugmentationFactor" : self.whichPatternsHaveAugmentationFactor,
|
|
"midiChannel" : self.midiChannel,
|
|
"group" : self.group,
|
|
"visible" : self.visible,
|
|
"stepDelayWrapAround" : self.stepDelayWrapAround,
|
|
"repeatDiminishedPatternInItself" : self.repeatDiminishedPatternInItself,
|
|
}
|
|
|
|
@classmethod
|
|
def instanceFromSerializedData(cls, parentData, serializedData):
|
|
self = cls.__new__(cls)
|
|
self.parentData = parentData
|
|
self.sequencerInterface = template.engine.sequencer.SequencerInterface.instanceFromSerializedData(self, serializedData["sequencerInterface"])
|
|
|
|
#Version 2.0+ changes
|
|
if "patternLengthMultiplicator" in serializedData:
|
|
self.patternLengthMultiplicator = serializedData["patternLengthMultiplicator"]
|
|
else:
|
|
self.patternLengthMultiplicator = 1
|
|
|
|
if "midiChannel" in serializedData:
|
|
self.midiChannel = serializedData["midiChannel"]
|
|
else:
|
|
self.midiChannel = 0
|
|
|
|
if "group" in serializedData:
|
|
self.group = serializedData["group"]
|
|
else:
|
|
self.group = "" #standalone track
|
|
|
|
if "visible" in serializedData:
|
|
self.visible = serializedData["visible"]
|
|
else:
|
|
self.visible = True
|
|
|
|
if "stepDelayWrapAround" in serializedData:
|
|
self.stepDelayWrapAround = bool(serializedData["stepDelayWrapAround"])
|
|
else:
|
|
self.stepDelayWrapAround = False
|
|
|
|
if "repeatDiminishedPatternInItself" in serializedData:
|
|
self.repeatDiminishedPatternInItself = bool(serializedData["repeatDiminishedPatternInItself"])
|
|
else:
|
|
self.repeatDiminishedPatternInItself = False
|
|
|
|
if "whichPatternsAreStepDelayed" in serializedData:
|
|
self.whichPatternsAreStepDelayed = {int(k):int(v) for k,v in serializedData["whichPatternsAreStepDelayed"].items()} #json saves dict keys as strings
|
|
else:
|
|
self.whichPatternsAreStepDelayed = {}
|
|
|
|
if "whichPatternsHaveAugmentationFactor" in serializedData:
|
|
self.whichPatternsHaveAugmentationFactor = {int(k):float(v) for k,v in serializedData["whichPatternsHaveAugmentationFactor"].items()} #json saves dict keys as strings
|
|
else:
|
|
self.whichPatternsHaveAugmentationFactor = {}
|
|
|
|
|
|
|
|
self.color = serializedData["color"]
|
|
self.structure = set(serializedData["structure"])
|
|
self.whichPatternsAreHalftoneTransposed = {int(k):int(v) for k,v in serializedData["whichPatternsAreHalftoneTransposed"].items()} #json saves dict keys as strings
|
|
self.whichPatternsAreScaleTransposed = {int(k):int(v) for k,v in serializedData["whichPatternsAreScaleTransposed"].items()} #json saves dict keys as strings
|
|
self.pattern = Pattern.instanceFromSerializedData(parentTrack=self, serializedData=serializedData["pattern"] )
|
|
self._processAfterInit()
|
|
return self
|
|
|
|
def export(self)->dict:
|
|
return {
|
|
"id" : id(self),
|
|
"sequencerInterface" : self.sequencerInterface.export(),
|
|
"realCboxMidiOutUuid" : self.cboxMidiOutAbstraction,
|
|
"color" : self.color,
|
|
"structure" : sorted(self.structure),
|
|
"patternBaseLength" : self.parentData.howManyUnits, #for convenient access. How many steps are on the X axis?
|
|
"patternLengthMultiplicator" : self.patternLengthMultiplicator, #int
|
|
"pattern": self.pattern.exportCache,
|
|
"scale": self.pattern.scale,
|
|
"averageVelocity": self.pattern.averageVelocity,
|
|
"numberOfSteps": self.pattern.numberOfSteps, #pitches. Convenience for len(scale). How many steps are on the Y axis?
|
|
"simpleNoteNames": self.pattern.simpleNoteNames,
|
|
"numberOfMeasures": self.parentData.numberOfMeasures,
|
|
"whichPatternsAreScaleTransposed": self.whichPatternsAreScaleTransposed,
|
|
"whichPatternsAreHalftoneTransposed": self.whichPatternsAreHalftoneTransposed,
|
|
"whichPatternsAreStepDelayed": self.whichPatternsAreStepDelayed,
|
|
"whichPatternsHaveAugmentationFactor": self.whichPatternsHaveAugmentationFactor,
|
|
"midiChannel" : self.midiChannel+1, #1-16
|
|
"group" : self.group, #string
|
|
"visible" : self.visible, #bool. Always True for standalone tracks
|
|
"stepDelayWrapAround" : self.stepDelayWrapAround, #bool.
|
|
"repeatDiminishedPatternInItself" : self.repeatDiminishedPatternInItself, #bool
|
|
}
|
|
|
|
#Dependency Injections.
|
|
template.engine.sequencer.Score.TrackClass = Track #Score will look for Track in its module.
|
|
|