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.
 
 
 
 
 
 

334 lines
15 KiB

#! /usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Copyright 2018, Nils Hilbricht, Germany ( https://www.hilbricht.net )
This file is part of the Laborejo Software Suite ( https://www.laborejo.org ),
more specifically its template base application.
The Template Base 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; logging.info("import {}".format(__file__))
#Python Standard Library
from dataclasses import dataclass
from collections import defaultdict
#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
#Client Modules
class Data(template.engine.sequencer.Score):
"""There must always be a Data class in a file main.py.
Simply inheriting from engine.data.Data is easiest.
You need to match the init parameters of your parent class. They vary from class to class
of course. Simply copy and paste them from your Data parent class
About Vico:
We have a track that has 10 layers. Each layer holds events.
Events are of type Event (a dataclass) and are ONLY create in layer.newEvent
"""
def __init__(self, parentSession):
super().__init__(parentSession)
#Add the Singleton Track instance
self.allEventsById = {} #This needs to be in init and instanceFromSerialized to be ready ro the loading stage. all events in all layers, keyed by id. value is the actual event instance. Does contain deleted items for undo, but who cares. Just don't use this to iterate over all actual events. This is a quick reference to find items without searching through all layers.
self.track = self.addTrack("out")
self._processAfterInit()
def _processAfterInit(self):
self.parentSession.recordingEnabled = True
self.tempoMap.isTransportMaster = False # no own tempo map
#Recording Buffer. Every noteOn inserts its pitch. noteOff removes.
#Keeping track per layer is only future proofing. The standard mode of operation does not allow recording to multiple layers at once.
#However, explicit is better than implicit.
self.noteOnsInProgress = {i:set() for i in range(10)}
self.track.sequencerInterface.insertEmptyClip() #needed for recording. Creates a maximum length empty clip
self.eventCollectorForUndo = None #The api switches this between a list and None
def liveNoteOn(self, tickindex:int, note:int, velocity:int):
"""Connected via the api, which sends further callbacks to the GUI.
A note on with velocity 0 is impossible. The api already redirects this as liveNoteOff.
"""
if self.parentSession.recordingEnabled and not tickindex is None:
assert not self.eventCollectorForUndo is None
assert velocity > 0, velocity
self.noteOnsInProgress[self.track.activeLayer].add(note)
resultEvent = self.track.layers[self.track.activeLayer].newEvent(tickindex, 0x90, note, velocity)
self.eventCollectorForUndo.append(resultEvent)
return resultEvent
def liveNoteOff(self, tickindex:int, note:int, velocity:int):
"""Connected via the api, which sends further callbacks to the GUI"""
if not note in self.noteOnsInProgress[self.track.activeLayer]:
logging.info("Note Off without Note On")
return None #It is possible that recording started with a physical midi key already down, thus we received a note-off without a preceding note-on. Ignore.
if self.parentSession.recordingEnabled and not tickindex is None:
assert not self.eventCollectorForUndo is None
self.noteOnsInProgress[self.track.activeLayer].remove(note)
resultEvent = self.track.layers[self.track.activeLayer].newEvent(tickindex, 0x80, note, velocity)
self.eventCollectorForUndo.append(resultEvent)
return resultEvent
#def playbackStatusChanged(self, state:bool):
# """auto-triggered via api callback"""
# if not state and self.parentSession.recordingEnabled:
# self._finalizeRecording()
def finalizeRecording(self, state:bool): #state comes from the api callback
"""When recording stops each dangling noteOne can be matched with an artificial noteOff.
We send out the note off to jack midi via the non-rt message
This function basically has two return values. One is immediate, a list of all forced note
offs. A GUI uses this to stop drawing rectangles with the playback cursor.
Then is fills data.eventCollectorForUndo , but with all events since the last recording
started, including note on and already sent note-off.
"""
#assert not self.eventCollectorForUndo is None #Can't do that here because finalizeRecording is called right at the start of the program. Nothing happens because all data structures are empty but eventCollectorForUndo is still None.
if not state and self.parentSession.recordingEnabled:
forcedNoteOffs = [] #flat list to return after the layers noteOnBuffer have been cleared
tickindex = cbox.Transport.status().pos_ppqn
for layerIndex, noteOnBuffer in self.noteOnsInProgress.items():
for noteOnPitch in noteOnBuffer:
cbox.send_midi_event(0x80, noteOnPitch, 0, output=self.track.sequencerInterface.cboxMidiOutUuid)
resultEvent = self.track.layers[self.track.activeLayer].newEvent(tickindex, 0x80, noteOnPitch, 0)
forcedNoteOffs.append(resultEvent)
self.eventCollectorForUndo.append(resultEvent)
noteOnBuffer.clear()
return forcedNoteOffs
else:
return [] #no force note offs
#def serialize(self)->dict:
#dictionary = super().serialize()
#dictionary.update( { #update in place
# "track" : self.track.serialize(),
#})
#return dictionary
@classmethod
def instanceFromSerializedData(cls, parentSession, serializedData):
self = cls.__new__(cls)
self.allEventsById = {} #This needs to be in init and instanceFromSerialized to be ready ro the loading stage. all events in all layers, keyed by id. value is the actual event instance. Does contain deleted items for undo, but who cares. Just don't use this to iterate over all actual events. This is a quick reference to find items without searching through all layers.
#Tracks depend on the rest of the data already in place because they create a cache on creation.
#self.track = Track.instanceFromSerializedData(parentData=self, serializedData=serializedData["track"])
super().copyFromSerializedData(parentSession, serializedData, self) #Tracks, parentSession and tempoMap
self.track = self.tracks[0]
self._processAfterInit()
return self
class Track(object):
"""A Vico instance has one hardcoded track with ten subtracks.
You can use these for notes or CC data.
"""
def __init__(self, parentData, name=None):
self.parentData = parentData
self.sequencerInterface = template.engine.sequencer.SequencerInterface(parentTrack=self, name=name)
self.layers = {i:Layer(self, i) for i in range(10)}
self.activeLayer = 1 #We order 0 before 1 but the keyboard layout trumps programming logic. First key is 1, therefore default layer is 1
self._processAfterInit()
def _processAfterInit(self):
"""Call this after either init or instanceFromSerializedData"""
def insertEvent(self, event):
"""Insert an existing Event object. This is different from layer.newEvent which creates
the instance"""
self.layers[event.layer].insertEvent(event)
def deleteEvent(self, event):
"""Search for an event and delete it. Once found, abort.
No big search is involved because events know their layer.
"""
self.layers[event.layer].deleteEvent(event)
#Save / Load / Export
def serialize(self)->dict:
return {
"sequencerInterface" : self.sequencerInterface.serialize(),
"activeLayer" : self.activeLayer,
"layers" : [l.serialize() for l in self.layers.values()],
}
def generateCalfboxMidi(self):
for layer in self.layers.values():
layer.generateCalfboxMidi() #only builds if dirty data
@classmethod
def instanceFromSerializedData(cls, parentData, serializedData):
self = cls.__new__(cls)
self.parentData = parentData
self.sequencerInterface = template.engine.sequencer.SequencerInterface.instanceFromSerializedData(self, serializedData["sequencerInterface"])
self.activeLayer = serializedData["activeLayer"]
self.layers = {}
for seriLayer in serializedData["layers"]:
self.layers[seriLayer["index"]] = Layer.instanceFromSerializedData(self, seriLayer)
self._processAfterInit()
return self
def export(self)->dict:
return {
"sequencerInterface" : self.sequencerInterface.export(),
"activeLayer" : self.activeLayer,
}
#Dependency Injections.
template.engine.sequencer.Score.TrackClass = Track #Score will look for Track in its module.
class Layer(object):
"""
Main data structure is self.events which holds tickspositions and tuples.
"""
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._processAfterInit()
def _processAfterInit(self):
"""Call this after either init or instanceFromSerializedData"""
def newEvent(self, tickindex:int, statusType:int, byte1:int, byte2:int):
"""The only place in the program where events get created."""
ev = Event(tickindex, statusType, byte1, byte2, self.index)
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. This is different from layer.newEvent which creates
the instance"""
assert not event in self.events[event.status]
self.events[event.status].append(event)
self._dirty = True
def deleteEvent(self, event):
self.events[event.status].remove(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,
}
@classmethod
def instanceFromSerializedData(cls, parentTrack, serializedData):
self = cls.__new__(cls)
self.parentTrack = parentTrack
self.color = serializedData["color"]
self.index = serializedData["index"]
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):
if self._dirty:
blob = bytes()
maxPos = 0
for status, eventlist in self.events.items():
for event in eventlist:
if event.position > maxPos:
maxPos = event.position
blob += event.toCboxBytes()
self.parentTrack.sequencerInterface.setSubtrack(self.index, [(blob, 0, maxPos+1)]) #(bytes-blob, position, length)
self._dirty = False
def _exportEvents(self) -> list:
"""Includes generating midi"""
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["id"]) #sort by position
def export(self)->dict:
return {
"index" : self.index,
"color" : self.color,
"events" : self._exportEvents(), #side effect: generate midi
}
@dataclass
class Event:
"""To make remembering the names easier we use pitch and velocity, eventhough that is wrong
for CCs
for example:
[0x90, 60, 100] for a loud middle c on channel 0
[0xE0, 4, 85] pitchbend to 85*128 + 4 = 10884 steps
There is no duration value. Connections, e.g. note rectangles in the GUI, have to be calculated.
"""
position: int
status: int # e.g. 0x90 for note-on or 0xE0 for pitchbend
pitch: int # e.g. 60 for middle c
velocity: int #eg. 100 for a rather loud note
layer: int #0-9 incl. . Events need to know their layer for undo.
def serialize(self)->tuple:
return (self.position, self.status, self.pitch, self.velocity)
def export(self)->dict:
#return (id(self), self.position, self.status, self.pitch, self.velocity)
return {
"id": id(self),
"position": self.position,
"status" : self.status,
"byte1" : self.pitch,
"byte2" : self.velocity,
"layer" : self.layer
}
def toCboxBytes(self)->bytes:
return cbox.Pattern.serialize_event(self.position, self.status, self.pitch, self.velocity)