#! /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 )
def fullName ( self ) - > str :
return 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 ) #We get the events even if not active. Otherwise they pile up.
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