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.
262 lines
12 KiB
262 lines
12 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 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
|
|
|
|
#Third Party Modules
|
|
from template.calfbox import cbox
|
|
|
|
#Template Modules
|
|
from . import pitch
|
|
|
|
|
|
class MidiInput(object):
|
|
"""MidiIn provides a port from calfboxes realtime midi in parsing to python data and functions.
|
|
Please note that it is not directly related to a track and is not responsible for actual midi
|
|
recording. This is the base class if you want to have midi control your program, like controlling
|
|
a GUI, step entry like in Laborejo or visualizers with multiple tracks.
|
|
|
|
You can therefore choose how you interpret the incoming data. It is possible to only have one
|
|
midi in port for the whole program and decide where to route the incoming events based on e.g.
|
|
GUI status (which track is selected).
|
|
|
|
Or you could mirror your internal tracks one by one with a midi input.
|
|
|
|
MidiIn is also the base class for instruments. However, these are subclasses.
|
|
|
|
It is recommended that you add a MidiIn object to your Python structures.
|
|
Don't subclass it because you might want to have MidiOut and audios as well, this
|
|
will produce a messy multiple inheritance situation.
|
|
|
|
One usecase is Laborejo: A single global step midi input that calls api functions.
|
|
In this case MidiInput is an alternative to the GUI. It does not belong in the engine but at
|
|
lowest in the api.
|
|
"""
|
|
|
|
def __init__(self, session, portName):
|
|
"""The processor adds itself to the event loop.
|
|
You need to call prepareDelete before removing a midi output again"""
|
|
|
|
self.session = session
|
|
self.portName = portName
|
|
self.scene = cbox.Document.get_engine().new_scene()
|
|
self.scene.clear()
|
|
self.scene.set_enable_default_song_input(False)
|
|
self.cboxMidiPortUid = cbox.JackIO.create_midi_input(portName)
|
|
self.realtimeMidiThroughLayer = self.scene.add_new_midi_layer(self.cboxMidiPortUid) #Create a midi layer for our input port. That layer support manipulation like transpose or channel routing.
|
|
cbox.JackIO.set_appsink_for_midi_input(self.cboxMidiPortUid, True) #This enables forwarding to Python for our midiProcessors get_new_events(self.parentInput.cboxMidiPortUid)
|
|
cbox.JackIO.route_midi_input(self.cboxMidiPortUid, self.scene.uuid) #Route midi input to the scene. Without this we have no sound, but the python processor will still work.
|
|
|
|
self._currentMidiThruOutputMidiPortUid = None #RT midi thru
|
|
self.setMidiThruChannel(1)
|
|
|
|
self.midiProcessor = MidiProcessor(parentInput = self)
|
|
self.session.eventLoop.fastConnect(self.midiProcessor.processEvents)
|
|
self.readyToDelete = False
|
|
|
|
def prepareDelete(self):
|
|
self.session.eventLoop.fastDisconnect(self.midiProcessor.processEvents)
|
|
self.readyToDelete = True
|
|
|
|
def setMidiThru(self, cboxMidiOutUuid):
|
|
"""
|
|
This is the output portion of the program. Everything in init is only for input.
|
|
|
|
Instruct the RT part to echo midi in directly to the connect output ports
|
|
so we hear the current track with the tracks instrument.
|
|
|
|
e.g. if you have a single global midi input but multiple track based outputs you can use
|
|
this to route the input RT to a track output while still getting the python data to process.
|
|
|
|
If you use such a configuration for data entry the user will hear his inputs echoed by
|
|
the actual target instrument and not e.g. a generic piano or sine wave sound.
|
|
"""
|
|
if not self._currentMidiThruOutputMidiPortUid == cboxMidiOutUuid: #most of the time this stays the same e.g. cursor left/right. we only care about up and down
|
|
self._currentMidiThruOutputMidiPortUid = cboxMidiOutUuid
|
|
self.realtimeMidiThroughLayer.set_external_output(cboxMidiOutUuid)
|
|
|
|
def setMidiThruChannel(self, channel):
|
|
if channel < 1 or channel > 16:
|
|
raise ValueError("Channels are from 1 to 16 (inclusive). You sent " + str(channel))
|
|
self.realtimeMidiThroughLayer.set_out_channel(channel)
|
|
|
|
def connectToHardware(self, portPattern:str):
|
|
if not portPattern:
|
|
portPattern = ".*"
|
|
|
|
hardwareMidiPorts = set(cbox.JackIO.get_ports(portPattern, cbox.JackIO.MIDI_TYPE, cbox.JackIO.PORT_IS_SOURCE | cbox.JackIO.PORT_IS_PHYSICAL))
|
|
for hp in hardwareMidiPorts:
|
|
cbox.JackIO.port_connect(hp, cbox.JackIO.status().client_name + ":" + self.portName)
|
|
|
|
|
|
class MidiProcessor(object):
|
|
"""
|
|
|
|
The parameter parentInput MUST be a an object that has the attribute "cboxMidiPortUid" of
|
|
type cbox.JackIO.create_midi_input(portName)
|
|
|
|
There are two principal modes: Step Entry and Live Recording.
|
|
|
|
Add your function to callbacks3 for notes and CC or callbacks2 for program change and Channel Pressure.
|
|
|
|
self.callbacks3[(MidiProcessor.SIMPLE_EVENT, MidiProcessor.NOTE_ON)] = lambda: print(channel, note, velocity)"""
|
|
|
|
|
|
SIMPLE_EVENT = "/io/midi/simple_event"
|
|
TRANSPORT_PLAY = "/io/midi/event_time_ppqn"
|
|
TRANSPORT_STOPPED = "/io/midi/event_time_samples"
|
|
M_NOTE_ON = 0x90
|
|
M_NOTE_OFF = 0x80
|
|
M_ACTIVE_SENSE = 0xFE #254
|
|
|
|
M_AFTERTOUCH = 0xA0 #160
|
|
M_CONTROL_CHANGE = 0xB0 #176
|
|
M_PROGRAMCHANGE = 0xC0 #192
|
|
M_CHANNELPRESSURE = 0xD0 #208
|
|
M_PITCHBEND = 0xE0 #224
|
|
|
|
CC_BANKCHANGE_COARSE = 32
|
|
CC_BANKCHANGE_FINE = 0
|
|
|
|
def __init__(self, parentInput):
|
|
self.parentInput = parentInput
|
|
|
|
if not hasattr(parentInput, "cboxMidiPortUid"):
|
|
raise ValueError("argument 'parentInput' must be an object with the attribute cboxMidiPortUid, returned from cbox.JackIO.create_midi_input(portName)")
|
|
|
|
self.callbacks3 = {} #keys are tuples
|
|
self.callbacks2 = {} #keys are tuples
|
|
self.active = True
|
|
self.ccState = {}
|
|
self.lastTimestamp = None #None for Transport not rolling, Value while rolling
|
|
#for cc in range(128): #0-127 inclusive
|
|
# self.ccState[cc] = None #start with undefined.
|
|
|
|
|
|
def processEvents(self):
|
|
"""events come in packages.
|
|
If you press just a key you'll get a list with length 2: timestamp, midi-message
|
|
For each event received in the same timeslot you'll get two events more.
|
|
A chord of four keys, played staccato simultaniously will yield 16 events.
|
|
2 for each key (=8), times two for note on and note off each.(=16)
|
|
|
|
We don't want the whole function too many indentation levels deep so we take a few shortcuts.
|
|
|
|
This function gets called very often. So every optimisation is good.
|
|
"""
|
|
events = cbox.JackIO.get_new_events(self.parentInput.cboxMidiPortUid)
|
|
if not self.active:
|
|
return
|
|
if not events:
|
|
return
|
|
for message, stuff, dataList in events:
|
|
l = len(dataList)
|
|
|
|
#Check recording mode. These message only get sent in front of another event, like a note.
|
|
#That means this is not a playback state detector because it only works after receiving a midi event
|
|
if message == MidiProcessor.TRANSPORT_PLAY:
|
|
#if self.lastTimestamp is None:
|
|
# print ("switching to live recording")
|
|
self.lastTimestamp = dataList[0]
|
|
#print (self.lastTimestamp)
|
|
|
|
elif message == MidiProcessor.TRANSPORT_STOPPED :
|
|
#if not self.lastTimestamp is None:
|
|
# print ("switching to step recording")
|
|
self.lastTimestamp = None
|
|
|
|
#Process Data
|
|
elif l == 3: #notes and CC
|
|
if message == MidiProcessor.SIMPLE_EVENT:
|
|
m_type, m_note, m_velocity = dataList #of course these can be different than a note, but this is easier to read then "byte 1", "byte 2"
|
|
m_channel = m_type & 0x0F
|
|
m_mode = m_type & 0xF0 #0x90 note on, 0x80 note off and so on.
|
|
key = (message, m_mode)
|
|
if m_mode == 0xB0:
|
|
self.ccState[m_note] = m_velocity #note is CCn , like 7=Volume, velocity is value.
|
|
|
|
if key in self.callbacks3:
|
|
self.callbacks3[key](self.lastTimestamp, m_channel, m_note, m_velocity)
|
|
|
|
elif l == 2: #program change, aftertouch,
|
|
if message == MidiProcessor.SIMPLE_EVENT:
|
|
m_type, m_value = dataList
|
|
m_channel = m_type & 0x0F
|
|
m_mode = m_type & 0xF0
|
|
key = (message, m_mode)
|
|
if key in self.callbacks2:
|
|
self.callbacks2[key](self.lastTimestamp, m_channel, m_value)
|
|
|
|
#Active Sense (Keep Alive Signal)
|
|
#TOOD: this can be commented out for performance reasons if we don't need to print out messages for debug reasons.
|
|
#TODO: Sadly there is no compile time. Use the compiledPrefix instead?
|
|
#elif l == 1 and message == MidiProcessor.SIMPLE_EVENT and dataList == [MidiProcessor.M_ACTIVE_SENSE]:
|
|
# pass
|
|
|
|
#else:
|
|
# print (message, stuff, dataList)
|
|
|
|
|
|
|
|
#Convenience Functions
|
|
def register_NoteOn(self, functionWithFourParametersTimestampChannelNoteVelocity):
|
|
"""A printer would be:
|
|
lambda timestamp, channel, note, velocity: print(timestamp, channel, note, velocity)
|
|
"""
|
|
self.callbacks3[(MidiProcessor.SIMPLE_EVENT, MidiProcessor.M_NOTE_ON)] = functionWithFourParametersTimestampChannelNoteVelocity
|
|
|
|
|
|
def register_NoteOff(self, functionWithFourParametersTimestampChannelNoteVelocity):
|
|
"""A printer would be:
|
|
lambda timestamp, channel, note, velocity: print(timestamp, channel, note, velocity)
|
|
"""
|
|
self.callbacks3[(MidiProcessor.SIMPLE_EVENT, MidiProcessor.M_NOTE_OFF)] = functionWithFourParametersTimestampChannelNoteVelocity
|
|
|
|
def register_PolyphonicAftertouch(self, functionWithFourParametersTimestampChannelTypeValue):
|
|
self.callbacks3[(MidiProcessor.SIMPLE_EVENT, MidiProcessor.M_AFTERTOUCH)] = functionWithFourParametersTimestampChannelTypeValue
|
|
|
|
def register_CC(self, functionWithFourParametersTimestampChannelTypeValue):
|
|
self.callbacks3[(MidiProcessor.SIMPLE_EVENT, MidiProcessor.M_CONTROL_CHANGE)] = functionWithFourParametersTimestampChannelTypeValue
|
|
|
|
def register_ProgramChange(self, functionWithThreeParametersTimestampChannelValue):
|
|
self.callbacks2[(MidiProcessor.SIMPLE_EVENT, MidiProcessor.M_PROGRAMCHANGE)] = functionWithThreeParametersTimestampChannelValue
|
|
|
|
def register_ChannelPressure(self, functionWithThreeParametersTimestampChannelValue):
|
|
self.callbacks2[(MidiProcessor.SIMPLE_EVENT, MidiProcessor.M_CHANNELPRESSURE)] = functionWithThreeParametersTimestampChannelValue
|
|
|
|
def register_PitchBend(self, functionWithFourParametersTimestampChannelTypeValue):
|
|
self.callbacks3[(MidiProcessor.SIMPLE_EVENT, MidiProcessor.M_PITCHBEND)] = functionWithFourParametersTimestampChannelTypeValue
|
|
|
|
#Hier weiter machen. After Touch, aber wie darstellen? Das ist ja per Taste. Wie CC Type? Oder unten im Velocity View?
|
|
|
|
def notePrinter(self, state:bool):
|
|
if state:
|
|
def _printer(timestamp, channel, note, velocity):
|
|
print(f"[{timestamp}] Chan: {channel} Note: {note} -> {pitch.midi_notenames_english[note]}: Vel: {velocity}")
|
|
self.callbacks3[(MidiProcessor.SIMPLE_EVENT, MidiProcessor.M_NOTE_ON)] = _printer
|
|
else:
|
|
try:
|
|
del self.callbacks2[(MidiProcessor.SIMPLE_EVENT, MidiProcessor.M_NOTE_ON)]
|
|
except KeyError:
|
|
pass
|
|
|