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

#! /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
}