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

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