LJ/libs3/midi3.py
2023-06-03 14:43:53 +02:00

759 lines
22 KiB
Python

#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
Midi3
v0.7.0
Midi Handler :
- Hook to the MIDI host
- Enumerate connected midi devices and spawn a process/device to handle incoming events
- Provide sending functions to
- found midi devices with IN port
- OSC targets /noteon /noteoff /cc (see midi2OSC).
- Launchpad mini led matrix from/to, see launchpad.py
- Bhoreal led matrix from/to, see bhoreal.py
todo :
Midi macros : plusieurs parametres evoluant les uns apres les autres ou en meme temps.
cadence
by Sam Neurohack
from /team/laser
for python 2 & 3
Laser selection
one universe / laser
Plugin selection
bank change/scene/
"""
import time
import rtmidi
from rtmidi.midiutil import open_midiinput
from threading import Thread
from rtmidi.midiconstants import (CHANNEL_PRESSURE, CONTROLLER_CHANGE, NOTE_ON, NOTE_OFF,
PITCH_BEND, POLY_PRESSURE, PROGRAM_CHANGE)
import mido
from mido import MidiFile
import traceback
import weakref
import sys
from sys import platform
from libs3 import gstt
from libs3 import bhoreal, launchpad, LPD8, beatstep
from queue import Queue
from OSC3 import OSCServer, OSCClient, OSCMessage
midiname = ["Name"] * 16
midiport = [rtmidi.MidiOut() for i in range(16) ]
OutDevice = []
InDevice = []
# max 16 midi port array
midinputsname = ["Name"] * 16
midinputsqueue = [Queue() for i in range(16) ]
midinputs = []
BhorealMidiName = "Bhoreal"
LaunchMidiName = "Launch"
BhorealPort, Midi1Port, Midi2Port, VirtualPort, MPort = -1,-1,-1, -1, -1
VirtualName = "LaunchPad Mini"
Mser = False
# Myxolidian 3 notes chords list
Myxo = [(59,51,54),(49,52,56),(49,52,56),(51,54,57),(52,56,59),(52,56,59),(54,57,48),(57,49,52)]
MidInsNumber = 0
clock = mido.Message(type="clock")
start = mido.Message(type ="start")
stop = mido.Message(type ="stop")
ccontinue = mido.Message(type ="continue")
reset = mido.Message(type ="reset")
songpos = mido.Message(type ="songpos")
mode = "maxwell"
'''
print("clock",clock)
print("start",start)
print("continue", ccontinue)
print("reset",reset)
print("sonpos",songpos)
'''
try:
input = raw_input
except NameError:
# Python 3
StandardError = Exception
STATUS_MAP = {
'noteon': NOTE_ON,
'noteoff': NOTE_OFF,
'programchange': PROGRAM_CHANGE,
'controllerchange': CONTROLLER_CHANGE,
'pitchbend': PITCH_BEND,
'polypressure': POLY_PRESSURE,
'channelpressure': CHANNEL_PRESSURE
}
# OSC targets list
midi2OSC = {
"lj": {"oscip": "127.0.0.1", "oscport": 8002, "notes": False, "msgs": False},
"nozoid": {"oscip": "127.0.0.1", "oscport": 8003, "notes": False, "msgs": False},
"dump": {"oscip": "127.0.0.1", "oscport": 8040, "notes": True, "msgs": True},
"maxwell": {"oscip": "127.0.0.1", "oscport": 8012, "notes": True, "msgs": True}
}
notes = ["C","C#","D","D#","E","F","F#","G","G#","A","A#","B"]
def midi2note(midinote):
print("midinote",midinote, "note", notes[midinote%12]+str(round(midinote/12)))
return notes[midinote%12]+str(round(midinote/12))
#mycontroller.midiport[LaunchHere].send_message([CONTROLLER_CHANGE, LaunchTop[number-1], color])
def send(msg,device):
'''
# if device is the midi name
if device in midiname:
deviceport = midiname.index(device)
midiport[deviceport].send_message(msg)
'''
if device == "Launchpad":
#print LaunchHere
midiport[launchpad.Here].send_message(msg)
if device == "Bhoreal":
midiport[bhoreal.Here].send_message(msg)
# mididest : all, launchpad, bhoreal, specificname
def NoteOn(note,color, mididest):
global MidInsNumber
gstt.note = note
gstt.velocity = color
for port in range(MidInsNumber):
# To Launchpad, if present.
if mididest == "launchpad" and midiname[port].find(LaunchMidiName) == 0:
launchpad.PadNoteOn(note%64,color)
# To Bhoreal, if present.
elif mididest == "bhoreal" and midiname[port].find(BhorealMidiName) == 0:
gstt.BhorLeds[note%64]=color
midiport[port].send_message([NOTE_ON, note%64, color])
#bhorosc.sendosc("/bhoreal", [note%64 , 0])
# To mididest
elif midiname[port].find(mididest) == 0:
midiport[port].send_message([NOTE_ON, note, color])
# To All
elif mididest == "all" and midiname[port].find(mididest) != 0 and midiname[port].find(BhorealMidiName) != 0 and midiname[port].find(LaunchMidiName) != 0:
midiport[port].send_message([NOTE_ON, note, color])
#virtual.send_message([NOTE_ON, note, color])
for OSCtarget in midi2OSC:
if (OSCtarget == mididest or mididest == 'all') and midi2OSC[OSCtarget]["notes"]:
OSCsend(OSCtarget, "/noteon", [note, color])
# mididest : all, launchpad, bhoreal, specificname
def NoteOff(note, mididest):
global MidInsNumber
gstt.note = note
gstt.velocity = 0
for port in range(MidInsNumber):
# To Launchpad, if present.
if mididest == "launchpad" and midiname[port].find(LaunchMidiName) == 0:
launchpad.PadNoteOff(note%64)
# To Bhoreal, if present.
elif mididest == "bhoreal" and midiname[port].find(BhorealMidiName) == 0:
midiport[port].send_message([NOTE_OFF, note%64, 0])
gstt.BhorLeds[note%64] = 0
#bhorosc.sendosc("/bhoreal", [note%64 , 0])
# To mididest
elif midiname[port].find(mididest) != -1:
midiport[port].send_message([NOTE_OFF, note, 0])
# To All
elif mididest == "all" and midiname[port].find(mididest) == -1 and midiname[port].find(BhorealMidiName) == -1 and midiname[port].find(LaunchMidiName) == -1:
midiport[port].send_message([NOTE_OFF, note, 0])
#virtual.send_message([NOTE_OFF, note, 0])
for OSCtarget in midi2OSC:
if (OSCtarget == mididest or mididest == 'all') and midi2OSC[OSCtarget]["notes"]:
OSCsend(OSCtarget, "/noteoff", note)
# mididest : all or specifiname, won't be sent to launchpad or Bhoreal.
def MidiMsg(midimsg, mididest):
#print("midi3 got MidiMsg", midimsg, "Dest", mididest)
desterror = -1
for port in range(MidInsNumber):
#print("port",port,"midiname", midiname[port])
# To mididest
if midiname[port].find(mididest) != -1:
#print("midi 3 sending to name", midiname[port], "port", port, ":", midimsg)
midiport[port].send_message(midimsg)
desterror = 0
# To All
elif mididest == "all" and midiname[port].find(mididest) == -1 and midiname[port].find(BhorealMidiName) == -1 and midiname[port].find(LaunchMidiName) == -1 and midiname[port].find(DJName) == -1:
#print("all sending to port",port,"name", midiname[port])
midiport[port].send_message(midimsg)
desterror = 0
for OSCtarget in midi2OSC:
if (OSCtarget == mididest or mididest == 'all') and midi2OSC[OSCtarget]["msgs"]:
OSCsend(OSCtarget, "/cc", [midimsg[1], midimsg[2]])
desterror = 0
if desterror == -1:
print ("** This midi or OSC destination doesn't exists **")
def OSCsend(name, oscaddress, oscargs =''):
ip = midi2OSC[name]["oscip"]
port = midi2OSC[name]["oscport"]
osclient = OSCClient()
osclient.connect((ip, port))
oscmsg = OSCMessage()
oscmsg.setAddress(oscaddress)
oscmsg.append(oscargs)
try:
if gstt.debug > 0:
print("Midi OSCSend : sending", oscmsg,"to", name, "at", ip , ":", port)
osclient.sendto(oscmsg, (ip, port))
oscmsg.clearData()
return True
except:
if gstt.debug > 0:
print('Midi OSCSend : Connection to IP', ip ,':', port,'refused : died ?')
#sendWSall("/status No plugin.")
#sendWSall("/status " + name + " is offline")
#sendWSall("/" + name + "/start 0")
#PluginStart(name)
return False
def Webstatus(message):
OSCsend("lj","/status", message)
#
# MIDI Startup and handling
#
mqueue = Queue()
inqueue = Queue()
#
# Events from Generic MIDI Handling
#
'''
def midinProcess(midiqueue):
midiqueue_get = midiqueue.get
while True:
msg = midiqueue_get()
print("midin ", msg)
time.sleep(0.001)
'''
# Event from Bhoreal or Launchpad
# Or it could be the midinprocess in launchpad.py or bhoreal.py
def MidinProcess(inqueue, portname):
inqueue_get = inqueue.get
mididest = "to Maxwell 1"
while True:
time.sleep(0.001)
msg = inqueue_get()
#print("Midinprocess", msg[0])
# Note On
if msg[0]==NOTE_ON:
print ("from", portname, "noteon", msg[1])
# NoteOn(msg[1],msg[2],mididest)
# Note Off
if msg[0]==NOTE_OFF:
print("from", portname,"noteoff")
# NoteOff(msg[1],msg[2], mididest)
# Midi CC message
if msg[0] == CONTROLLER_CHANGE:
print("from", portname,"CC :", msg[1], msg[2])
# other midi message
if msg[0] != NOTE_OFF and msg[0] != NOTE_ON and msg[0] != CONTROLLER_CHANGE:
print("from", portname,"other midi message")
MidiMsg(msg[0],msg[1],msg[2],mididest)
# Webstatus(''.join(("msg : ",msg[0]," ",msg[1]," ",msg[2])))
# Generic call back : new msg forwarded to queue
class AddQueue(object):
def __init__(self, portname, port):
self.portname = portname
self.port = port
#print("AddQueue", port)
self._wallclock = time.time()
def __call__(self, event, data=None):
message, deltatime = event
self._wallclock += deltatime
#print("inqueue : [%s] @%0.6f %r" % ( self.portname, self._wallclock, message))
midinputsqueue[self.port].put(message)
#
# MIDI OUT Handling
#
class OutObject():
_instances = set()
counter = 0
def __init__(self, name, kind, port):
self.name = name
self.kind = kind
self.port = port
self._instances.add(weakref.ref(self))
OutObject.counter += 1
print(self.name, "kind", self.kind, "port", self.port)
@classmethod
def getinstances(cls):
dead = set()
for ref in cls._instances:
obj = ref()
if obj is not None:
yield obj
else:
dead.add(ref)
cls._instances -= dead
def __del__(self):
OutObject.counter -= 1
def OutConfig():
global midiout, MidInsNumber
#
if len(OutDevice) == 0:
#print("")
print("MIDIout...")
print("List and attach to available devices on host with IN port :")
# Display list of available midi IN devices on the host, create and start an OUT instance to talk to each of these Midi IN devices
midiout = rtmidi.MidiOut()
available_ports = midiout.get_ports()
for port, name in enumerate(available_ports):
midiname[port]=name
midiport[port].open_port(port)
#print("Will send to [%i] %s" % (port, name))
#MidIns[port][1].open_port(port)
# Search for a Bhoreal
if name.find(BhorealMidiName) == 0:
OutDevice.append(OutObject(name, "bhoreal", port))
print("Bhoreal start animation")
bhoreal.Here = port
bhoreal.StartBhoreal(port)
time.sleep(0.2)
# Search for a LaunchPad
elif name.find(LaunchMidiName) == 0:
OutDevice.append(OutObject(name, "launchpad", port))
print("Launchpad mini start animation")
launchpad.Here = port
launchpad.Start(port)
time.sleep(0.2)
# Search for a LPD8
elif name.find('LPD8') == 0:
OutDevice.append(OutObject(name, "LPD8", port))
#print("LPD8 mini start animation")
LPD8.Here = port
#LPD8.StartLPD8(port)
time.sleep(0.2)
# Search for a Guitar Wing
elif name.find("Livid") == 0:
OutDevice.append(OutObject(name, "livid", port))
print("Livid Guitar Wing start animation")
gstt.WingHere = port
print(gstt.WingHere)
#guitarwing.StartWing(port)
time.sleep(0.2)
else:
OutDevice.append(OutObject(name, "generic", port))
#print("")
print(len(OutDevice), "Out devices")
#ListOutDevice()
MidInsNumber = len(OutDevice)+1
def ListOutDevice():
for item in OutObject.getinstances():
print(item.name)
def FindOutDevice(name):
port = -1
for item in OutObject.getinstances():
#print("searching", name, "in", item.name)
if name == item.name:
#print('found port',item.port)
port = item.port
return port
def DelOutDevice(name):
Outnumber = Findest(name)
print('deleting OutDevice', name)
if Outnumber != -1:
print('found OutDevice', Outnumber)
delattr(OutObject, str(name))
print("OutDevice", Outnumber,"was removed")
else:
print("OutDevice was not found")
#
# MIDI IN Handling
# Create processing thread and queue for each device
#
class InObject():
_instances = set()
counter = 0
def __init__(self, name, kind, port, rtmidi):
self.name = name
self.kind = kind
self.port = port
self.rtmidi = rtmidi
self.queue = Queue()
self._instances.add(weakref.ref(self))
InObject.counter += 1
#print("Adding InDevice name", self.name, "kind", self.kind, "port", self.port,"rtmidi", self.rtmidi, "Queue", self.queue)
@classmethod
def getinstances(cls):
dead = set()
for ref in cls._instances:
obj = ref()
if obj is not None:
yield obj
else:
dead.add(ref)
cls._instances -= dead
def __del__(self):
InObject.counter -= 1
def InConfig():
print("")
print("MIDIin...")
print("List and attach to available devices on host with OUT port :")
if platform == 'darwin':
mido.set_backend('mido.backends.rtmidi/MACOSX_CORE')
genericnumber = 0
for port, name in enumerate(mido.get_input_names()):
#print()
# Maxwell midi IN & OUT port names are different
if name.find("from ") == 0:
#print ("name",name)
name = "to "+name[5:]
#print ("corrected to",name)
outport = FindOutDevice(name)
midinputsname[port]=name
#print("name",name, "Port",port, "Outport", outport)
'''
# New Bhoreal found ?
if name.find(BhorealMidiName) == 0:
try:
bhorealin, port_name = open_midiinput(outport) # weird rtmidi call port number is not the same in mido enumeration and here
BhoreralDevice = InObject(port_name, "bhoreal", outport, bhorealin)
print("BhorealDevice.queue",BhoreralDevice.queue )
# thread launch to handle all queued MIDI messages from Bhoreal device
thread = Thread(target=bhoreal.MidinProcess, args=(bhoreal.bhorqueue,))
thread.setDaemon(True)
thread.start()
print("Attaching MIDI in callback handler to Bhoreal : ", name, "port", port, "portname", port_name)
BhoreralDevice.rtmidi.set_callback(bhoreal.AddQueue(port_name))
except Exception:
traceback.print_exc()
'''
# Old Bhoreal found ?
if name.find(BhorealMidiName) == 0:
try:
bhorealin, port_name = open_midiinput(outport) # weird rtmidi call port number is not the same in mido enumeration and here
except (EOFError, KeyboardInterrupt):
sys.exit
#midinputs.append(bhorealin)
InDevice.append(InObject(name, "bhoreal", outport, bhorealin))
# thread launch to handle all queued MIDI messages from Bhoreal device
thread = Thread(target=bhoreal.MidinProcess, args=(bhoreal.bhorqueue,))
#thread = Thread(target=bhoreal.MidinProcess, args=(InDevice[port].queue,))
thread.setDaemon(True)
thread.start()
#print("midinputs[port]", midinputs[port])
print(name)
InDevice[port].rtmidi.set_callback(bhoreal.AddQueue(name))
#midinputs[port].set_callback(bhoreal.AddQueue(name))
'''
# New LaunchPad Mini Found ?
if name.find(LaunchMidiName) == 0:
try:
launchin, port_name = open_midiinput(outport)
except (EOFError, KeyboardInterrupt):
sys.exit()
LaunchDevice = InObject(port_name, "launchpad", outport, launchin)
thread = Thread(target=launchpad.MidinProcess, args=(launchpad.launchqueue,))
thread.setDaemon(True)
thread.start()
print("Attaching MIDI in callback handler to Launchpad : ", name, "port", port, "portname", port_name)
LaunchDevice.rtmidi.set_callback(launchpad.LaunchAddQueue(name))
'''
# Old LaunchPad Mini Found ?
if name.find(LaunchMidiName) == 0:
try:
launchin, port_name = open_midiinput(outport)
except (EOFError, KeyboardInterrupt):
sys.exit()
#midinputs.append(launchin)
InDevice.append(InObject(name, "launchpad", outport, launchin))
thread = Thread(target=launchpad.MidinProcess, args=(launchpad.launchqueue,))
#thread = Thread(target=launchpad.MidinProcess, args=(InDevice[port].queue,))
thread.setDaemon(True)
thread.start()
print(name, "port", port, "portname", port_name)
InDevice[port].rtmidi.set_callback(launchpad.LaunchAddQueue(name))
#launchin.set_callback(launchpad.LaunchAddQueue(name))
# LPD8 Found ?
if name.find('LPD8') == 0:
print()
print('LPD8 Found..')
try:
LPD8in, port_name = open_midiinput(outport)
except (EOFError, KeyboardInterrupt):
sys.exit()
#midinputs.append(LPD8in)
InDevice.append(InObject(name, "LPD8", outport, LPD8in))
print ("Launching LPD8 thread..")
thread = Thread(target=LPD8.MidinProcess, args=(LPD8.LPD8queue,))
#thread = Thread(target=LPD8.MidinProcess, args=(InDevice[port].queue,))
thread.setDaemon(True)
thread.start()
print(name, "port", port, "portname", port_name)
InDevice[port].rtmidi.set_callback(LPD8.LPD8AddQueue(name))
# Everything that is not Bhoreal or Launchpad
if name.find(BhorealMidiName) != 0 and name.find(LaunchMidiName) != 0 and name.find('LPD8') != 0:
try:
#print (name, name.find("RtMidi output"))
if name.find("RtMidi output") > -1:
print("No thread started for device", name)
else:
portin = object
port_name = ""
portin, port_name = open_midiinput(outport)
#midinputs.append(portin)
InDevice.append(InObject(name, "generic", outport, portin))
thread = Thread(target=MidinProcess, args=(midinputsqueue[port],port_name))
thread.setDaemon(True)
thread.start()
print(name, "port", port, "portname", port_name)
#midinputs[port].set_callback(AddQueue(name),midinputsqueue[port])
#midinputs[port].set_callback(AddQueue(name))
#genericnumber += 1
InDevice[port].rtmidi.set_callback(AddQueue(name,port))
except Exception:
traceback.print_exc()
#print("")
print(InObject.counter, "In devices")
#ListInDevice()
def ListInDevice():
for item in InObject.getinstances():
print(item.name)
def FindInDevice(name):
port = -1
for item in InObject.getinstances():
#print("searching", name, "in", item.name)
if name in item.name:
#print('found port',item.port)
port = item.port
return port
def DelInDevice(name):
Innumber = Findest(name)
print('deleting InDevice', name)
if Innumber != -1:
print('found InDevice', Innumber)
delattr(InObject, str(name))
print("InDevice", Innumber,"was removed")
else:
print("InDevice was not found")
# all other devices
'''
port = mido.open_ioport(name,callback=AddQueue(name))
This doesn't work on OS X on French system "Réseau Session" has a bug with accent.
Todo : stop using different midi framework.
if name.find(BhorealMidiName) != 0 and name.find(LaunchMidiName) != 0:
thread = Thread(target=midinProcess, args=(midinputsqueue[port],))
thread.setDaemon(True)
thread.start()
try:
port = mido.open_ioport(name,callback=AddQueue(name))
#port_port, port_name = open_midiinput(port)
except (EOFError, KeyboardInterrupt):
sys.exit()
#midinputs.append(port_port)
print "Attaching MIDI in callback handler to : ", name
#midinputs[port].set_callback(AddQueue(name))
#MIDInport = mido.open_ioport("Laser",virtual=True,callback=MIDIn)
'''
def End():
global midiout
#midiin.close_port()
midiout.close_port()
#del virtual
if launchpad.Here != -1:
del launchpad.Here
if bhoreal.Here != -1:
del bhoreal.Here
if LPD8.Here != -1:
del LPD8.Here
def listdevice(number):
return midiname[number]
def check():
OutConfig()
InConfig()
#return listdevice(255)