Nils
4 years ago
12 changed files with 482 additions and 41 deletions
@ -0,0 +1,24 @@ |
|||
#! /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 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") |
|||
|
|||
#This file only exists as a reminder to _not_ create it again wrongly in the future. |
@ -0,0 +1,28 @@ |
|||
#! /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 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") |
|||
|
|||
from template.engine.input_midi import MidiInput |
|||
|
|||
def addTrack(): |
|||
print ("vico track") |
@ -0,0 +1,74 @@ |
|||
#! /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") |
|||
|
|||
class Event: |
|||
""" |
|||
[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. |
|||
|
|||
Byte2 can be None, for Program Change or Channel Pressure. |
|||
|
|||
Vico events have no channel. The recorded channel is converted to channel 0. |
|||
""" |
|||
def __init__(self, position:int, status:int, byte1:int, byte2:int, layer:int, freeText:str=""): |
|||
self.position = position |
|||
self.status = status # e.g. 0x90 for note-on or 0xE0 for pitchbend |
|||
self.byte1 = byte1 # e.g. 60 for middle c |
|||
self.byte2 = byte2 #eg. 100 for a rather loud note. Can be None for Program Changes. |
|||
self.layer = layer #0-9 incl. . Events need to know their layer for undo. |
|||
self.freeText = freeText |
|||
#self.parentLayer = Injected and changed by the Layer itself. |
|||
|
|||
def serialize(self)->tuple: |
|||
return (int(self.position), self.status, self.byte1, self.byte2, self.freeText) |
|||
|
|||
def export(self)->dict: |
|||
return { |
|||
"id": id(self), |
|||
"position": self.position, |
|||
"status" : self.status, |
|||
"byte1" : self.byte1, |
|||
"byte2" : self.byte2, |
|||
"layer" : self.layer, |
|||
"freeText" : self.freeText, |
|||
} |
|||
|
|||
|
|||
def toCboxBytes(self)->bytes: |
|||
byte1 = pitch.midiPitchLimiter(self.byte1, 0) |
|||
byte2 = pitch.midiPitchLimiter(self.byte2, 0) |
|||
status = self.status + self.parentLayer.midiChannel - 1 #we index channels from 1 to 16, so -1 here because status is itself already chan 1 |
|||
if self.position >= 0: |
|||
return cbox.Pattern.serialize_event(self.position, status, byte1, byte2) |
|||
else: |
|||
logger.warning(f"Event {self.byte1},{self.byte2} has position less than 0. Limiting to 0 in midi output. Please fix manually") |
|||
return cbox.Pattern.serialize_event(0, status, byte1, byte2) |
|||
|
|||
def __repr__(self): |
|||
return str(self.export()) |
|||
|
|||
def copy(self): |
|||
"""Returns a standalone copy of self. Needs to be inserted into a layer.""" |
|||
#return Event(*self.serialize(), layer=None) |
|||
return Event(position=self.position, status=self.status, byte1=self.byte1, byte2=self.byte2, layer=None, freeText=self.freeText) |
@ -0,0 +1,183 @@ |
|||
#! /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 |
|||
} |
|||
|
|||
|
Loading…
Reference in new issue