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.
183 lines
7.3 KiB
183 lines
7.3 KiB
#! /usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
Copyright 2021, Nils Hilbricht, Germany ( https://www.hilbricht.net )
|
|
|
|
This file is part of the Laborejo Software Suite ( https://www.laborejo.org ),
|
|
|
|
This 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")
|
|
|
|
#Python Standard Library
|
|
from collections import defaultdict
|
|
from statistics import median
|
|
|
|
#Third Party Modules
|
|
from calfbox import cbox
|
|
|
|
#Template Modules
|
|
import template.engine.sequencer
|
|
import template.engine.pitch as pitch
|
|
from template.engine.duration import DB, DL, D1, D2, D4, D8, D16, D32, D64, D128, D256, D512, D1024
|
|
|
|
|
|
class Track(object):
|
|
"""
|
|
Main data structure is self.events which holds tickspositions and tuples.
|
|
"""
|
|
|
|
def __init__(self, parentData,
|
|
name:str="",
|
|
color:str=None,
|
|
simpleNoteNames:List[str]=None):
|
|
|
|
logger.info("Creating empty Vico Track instance")
|
|
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.
|
|
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.
|
|
self.group = "" # "" is a standalone track. 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.events = defaultdict(list) # statusType: [Event]
|
|
self.dirty = False # indicates wether this needs re-export. Obviously not saved.
|
|
|
|
self._processAfterInit()
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self, parentTrack, index:int):
|
|
self.parentTrack = parentTrack
|
|
self.index = index
|
|
self.color = "cyan"
|
|
self.events = defaultdict(list) # statusType: [Event]
|
|
self.dirty = False # indicates wether this needs re-export. Obviously not saved.
|
|
self.midiChannel = 1 #1-16 inclusive.
|
|
self._processAfterInit()
|
|
|
|
def _processAfterInit(self):
|
|
"""Call this after either init or instanceFromSerializedData"""
|
|
self.cachedMedianVelocity = 64
|
|
self.cachedLastEventPosition = 0 #set in generateCalfboxMidi. Used by the track
|
|
self.cachedFirstEventPosition = 0 #set in generateCalfboxMidi. Used by the track
|
|
|
|
def newEvent(self, tickindex:int, statusType:int, byte1:int, byte2:int, freeText:str):
|
|
"""The only place in the program where events get created, except Score.getCopyBufferCopy
|
|
and where self.parentTrack.parentData.allEventsById gets populated. """
|
|
ev = Event(tickindex, statusType, byte1, byte2, self.index, freeText)
|
|
ev.parentLayer = self
|
|
self.parentTrack.parentData.allEventsById[id(ev)] = ev
|
|
self.events[statusType].append(ev)
|
|
self.dirty = True
|
|
return ev
|
|
|
|
def insertEvent(self, event):
|
|
"""Insert an existing Event object, that was at one time created through newEvent"""
|
|
assert not event in self.events[event.status]
|
|
self.events[event.status].append(event)
|
|
event.parentLayer = self
|
|
self.dirty = True
|
|
|
|
def deleteEvent(self, event):
|
|
"""Don't be surprised. This gets called by api.createNote to temporarily remove
|
|
notes and later add them with insertEvent again"""
|
|
try:
|
|
self.events[event.status].remove(event)
|
|
except ValueError:
|
|
logger.error(f"Event was not in our list. Layer{self.index}. Event: {event}")
|
|
self.dirty = True
|
|
|
|
#Save / Load / Export
|
|
def serialize(self)->dict:
|
|
events = []
|
|
|
|
for statusType, eventlist in self.events.items():
|
|
for event in eventlist:
|
|
events.append(event.serialize())
|
|
|
|
return {
|
|
"index" : self.index,
|
|
"color" : self.color,
|
|
"events": events,
|
|
"midiChannel": self.midiChannel,
|
|
}
|
|
|
|
@classmethod
|
|
def instanceFromSerializedData(cls, parentTrack, serializedData):
|
|
self = cls.__new__(cls)
|
|
self.parentTrack = parentTrack
|
|
self.color = serializedData["color"]
|
|
self.index = serializedData["index"]
|
|
self.midiChannel = serializedData["midiChannel"]
|
|
self.events = defaultdict(list) # statusType: [Event]
|
|
for seriEvent in serializedData["events"]: #tuples or list
|
|
self.newEvent(*seriEvent)
|
|
self.dirty = True
|
|
self._processAfterInit()
|
|
return self
|
|
|
|
def generateCalfboxMidi(self):
|
|
"""We use subtracks, therefore they do not change the cachedDuration of the song.
|
|
Instead we save our own version of cached ticks of the actual song length.
|
|
Useful for the GUI so it doesn not have to draw MAX_DURATION"""
|
|
if self.dirty:
|
|
velocities = []
|
|
blob = bytes()
|
|
maxPos = 0
|
|
minPos = template.engine.sequencer.MAXIMUM_TICK_DURATION
|
|
for status, eventlist in self.events.items():
|
|
for event in eventlist:
|
|
if event.position > maxPos:
|
|
maxPos = event.position
|
|
if event.position < minPos:
|
|
minPos = event.position
|
|
if event.status == 0x90:
|
|
velocities.append(event.byte2)
|
|
blob += event.toCboxBytes()
|
|
|
|
if velocities:
|
|
self.cachedMedianVelocity = int(median(velocities))
|
|
|
|
self.parentTrack.sequencerInterface.setSubtrack(self.index, [(blob, 0, maxPos+1)]) #(bytes-blob, position, length)
|
|
self.dirty = False
|
|
self.cachedLastEventPosition = maxPos
|
|
self.cachedFirstEventPosition = minPos
|
|
|
|
|
|
def _exportEvents(self) -> list:
|
|
"""Includes generating midi.
|
|
This is not called after every change!
|
|
In fact it is only triggered by api._layerChanged which happens once on program start
|
|
and on very extensive operations on the whole layer (like transpose)
|
|
"""
|
|
result = []
|
|
for status, eventlist in self.events.items():
|
|
for event in eventlist:
|
|
result.append(event.export())
|
|
self.generateCalfboxMidi()
|
|
return sorted(result, key=lambda e: e["position"])
|
|
|
|
|
|
def export(self)->dict:
|
|
return {
|
|
"index" : self.index,
|
|
"color" : self.color,
|
|
"events" : self._exportEvents(), #side effect: generate midi
|
|
}
|
|
|
|
|
|
|