diff --git a/README.md b/README.md index c2f773d..de9a98b 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ By llstr, Sam Neurohack LICENCE : CC NC -Midi and more exchanges over LAN/Internet +Midi and more exchanges over LAN/Internet. Now with ORCA live coding support !! Imagine Bob own a TR 808 at home and Alice a nozoid OCS-2. They can control each other devices in a webpage or a midi controller. If John has a mutliple encoders midi controller at home and want to control Alice and Bob devices, all changes made by John will be displayed to everyone and played by devices. @@ -25,6 +25,38 @@ More you can use also a vcvrack complex patch to drive light fixtures, laser abs - Jamidi is experimental and nowhere safe. Network managment and security is not in jamidi scope. Some ideas : All computers can be on the same encrypted vpn (tinc, zerotier,..). The server can run on a VPS and all client connect to it. The server is at home, closer to midi devices,... +- Look at jamidi.json for set your configuration. 2 element types : midi device and IP servers + + + + +# ORCA livecoding support. + +Livecode some instrument somewhere else. https://github.com/hundredrabbits/Orca + +On computer linked with desired midi instrument : + +python3 main.py + + +Livecoder should configure ORCA and use it : + +CTRL K ip:ipaddress +CTRL K udp:udport (default is 8083) + +MIDI CCs base 36 use ;cmnd + + m : midi channel 0-F (0-15) + n : number 0-Z (0-35) + d : data 0-Z will output (d/36)*127 + + +MIDI NOTES use ;nmonv + + m : midi channel 0-F (0-15) + o : octave 0-8 + n : Note A-G. For note with # : a-g + v : velocity 0-Z will output (v/36)*127 @@ -39,9 +71,9 @@ or /tr808/note/1 -cc : is a "command". Currently cc, reset (highly specific to nozoid synthetiser). You can add any tyoe of "command". +cc : is a "command". Currently cc, reset (highly specific to nozoid synthetiser). You can add any tyoe of "command". -ocs2 : is a "device", that must be described, follow examples in jamidi.json. Here the Alice OCS2 will reveive broadcasted midi informations. +ocs2 : is a "device", that must be described, follow examples in jamidi.json. Here the Alice OCS2 will reveive broadcasted midi informations. /ocs2 and /tr808 changes made by John will be displayed to everyone and played by devices. @@ -55,46 +87,55 @@ Websocket default port is 8081 but one can change. Will receive all "commands" from all clients, forward them to local devices and broadcast them too. +servername : 'local', 'llstrvpn'. Servers (IP, port,...) must be described in jamidi.json + +How to Run python server : + +python3 main.py Options : -servername : 'local', 'llstrvpn'. Servers (IP, port,...) must be described in jamidi.json + -h Show this help message and exit ---broadcast : Broadcast all incomings commands to all client. Default option. + -s SERVERNAME Servername: 'local', 'llstrvpn' (local by default) ---no-broadcast : Do not broadcast all incomings commands to all client. + -d DEVICENAME Mididevice for incoming ORCA via OSC (mmo3 by default) + -nocurrent Do not send all current CC values to all new client (enabled by default) ---reset : Send reset values to local device at startup. Default option. + -nobroadcast Do not broadcast all incomings commands to all client (enabled by default) ---no-reset : Do not send reset values to local device at startup. + -noreset Do not broadcast all incomings commands to all client (enabled by default) + -nothrough Disable the builtin midithrough from any midi IN to --device enabled by default ---current : Send all current CC values to all new client. Default option. - ---no-current : Do not send all current CC values to all new client. + -verbose Enable debug mode (disabled by default) # How it work : Clients -Can be webpages or midi instrument/software. Multiple clients types is supported. +Send midi over network to Jamidi. Can be webpages or midi instrument/software. Multiple clients types is supported. + +How to Run python client : + +python3 client.py Options : servername : Remote server 'local', 'xrkia' ('local' by default). Servers must be described in jamidi.json -default : Network <-> default midi device (True or False) +-s servername servername: 'local', 'xrkia' ('local' by default) + +-nodefault Do not send reset values to local device a startup. Some "rules" are available. Say you want all network incoming midi CC 1 channel 0 goes to a specific device on channel 3 CC 48. Rules must be described in rules.json Each rule may happen at all time or only during a "song". -How to Run python client : -python3 client.py diff --git a/jamidi.json b/jamidi.json index e16d282..3f036ba 100644 --- a/jamidi.json +++ b/jamidi.json @@ -51,6 +51,19 @@ } ], +"sam" : +[ + { + "_comment": "Server is laser.teamlaser.fr", + "type": "serverconf", + "name": "tmlsr", + "IP": "192.168.2.43", + "port": 8081, + "oscport": 8082, + "udport": 8083 + } +], + "sq-1": [ { "_comment": "SQ-1 device parameters", @@ -89,59 +102,6 @@ "midichan" : 1, "xname" : "mmo3" } - ], - -"ocs2bcr": [ - { - "_comment": "OCS-2 control with BCR2000", - "type": "mididevice", - "mididevice": "BCR2000 Port 1", - "midichan" : 2, - "xname" : "ocs2" - } -], - -"mmo3bcr": [ - { - "_comment": "MMO-3 control with BCR2000", - "type": "mididevice", - "mididevice": "BCR2000 Port 1", - "midichan" : 1, - "xname" : "mmo3" - } - ], - -"launchpad": [ - { - "_comment": "Launchpad mini device parameters", - "type": "mididevice", - "mididevice": "Launchpad Mini", - "midichan" : 0, - "xname" : "launchpad" - } - ], - -"maxwell": [ - { - "_comment": "Mawell device parameters", - "type": "mididevice", - "mididevice": "to Maxwell 1", - "midichan" : 0, - "xname" : "ocs2" - } - ], - - -"default": [ - { - "_comment": "Client : default midi device", - "type": "mididevice", - "mididevice": "BCR2000 Port 1", - "midichan" : 3, - "xname" : "default" - } - -] - + ] } diff --git a/libs/OSCom.py b/libs/OSCom.py new file mode 100644 index 0000000..93041ab --- /dev/null +++ b/libs/OSCom.py @@ -0,0 +1,207 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# -*- mode: Python -*- + +''' +OSCcom for jamidi v0.1b + +OSCom.Start(serverIP, OSCPORT) +default handler : handler(path, tags, args, source) +register particular OSC command in Start(): i.e oscserver.addMsgHandler( "/n", Note) + +''' + +import midi3 + +#import socket +import types, json +from OSC3 import OSCServer, OSCClient, OSCMessage +import _thread, time +import gstt +import WScom, UDPcom +import midi3 + +#base36 = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"] + + +def GetTime(): + return time.strftime("%a, %d %b %Y %H:%M:%S", time.localtime()) + +# this method of reporting timeouts only works by convention +# that before calling handle_request() field .timed_out is +# set to False +def handle_timeout(self): + self.timed_out = True + + +def Start(serverIP, OSCPORT): + global oscserver + + #print(GetTime(),gstt.oscname, gstt.Confs[gstt.oscname][0]["midichan"]) + #print(gstt.Confs) + #print(gstt.Confs[gstt.oscname]) + for i in range(len(gstt.Confs[gstt.oscname])): + print(GetTime(),gstt.oscname, gstt.Confs[gstt.oscname][i]["midichan"]) + + oscserver = OSCServer( (serverIP, OSCPORT) ) + oscserver.timeout = 0 + # funny python's way to add a method to an instance of a class + import types + oscserver.handle_timeout = types.MethodType(handle_timeout, oscserver) + + oscserver.addMsgHandler( "default", handler ) + oscserver.addMsgHandler( "/n", Note) + oscserver.addMsgHandler( "/c", CC) + oscserver.addMsgHandler( "/p", PB) + _thread.start_new_thread(osc_thread, ()) + + +# RAW OSC Frame available ? +def OSCframe(): + # clear timed_out flag + oscserver.timed_out = False + # handle all pending requests then return + while not oscserver.timed_out: + oscserver.handle_request() + + + + +# OSC server Thread : handler, dacs reports and simulator points sender to UI. +def osc_thread(): + + + #print("osc Thread launched") + try: + while True: + + time.sleep(0.005) + OSCframe() + + except Exception as e: + import sys, traceback + print('\n---------------------') + print('Exception: %s' % e) + print('- - - - - - - - - - -') + traceback.print_tb(sys.exc_info()[2]) + print("\n") + + + + +# Properly close the system. Todo +def Stop(): + oscserver.close() + + +# default handler +def handler(path, tags, args, source): + + oscaddress = ''.join(path.split("/")) + print() + print("Jamidi Default OSC Handler got from " + str(source[0]),"OSC msg", path, "args", args) + #print("OSC address", path) + #print("find.. /bhoreal ?", path.find('/bhoreal')) + if len(args) > 0: + #print("with args", args) + pass + + ''' + # for example + if path == '/truc': + arg1 = args[0] + arg2 = args[1]) + ''' + +''' +MIDI NOTES +=n in ORCA +/n in OSC + ORCA OSC + =nmonv /n m o n v + m : midi channel (0-15 / ORCA 0-F) + o : octave (0-8 / ORCA 0-7) + n : Note A to G + v : velocity 0-Z will output (v/36)*127 + +''' +def Note(path, tags, args, source): + + #print('Note from ORCA received',args) + + midichannel = int(args[0],36) + octave = int(args[1],36) + note = args[2] + velocity = int((int(args[3],36)/36)*127) + + if note.istitle() == True: + notename = str(note)+ str(octave) + else: + notename = str(note)+ "#"+ str(octave) + + if gstt.debug > 0: + print("incoming note", note, octave, notename, midi3.note2midi(notename) ) + + for mididevice in midi3.findJamDevices(gstt.oscname): + midi3.NoteOn(midi3.note2midi(notename), velocity, mididevice) + #midi3.NoteOn(int(wspath[1]), int(wspath[2]), gstt.Confs[wscommand[1]][0]["mididevice"]) + + +''' +CC +=c in ORCA +/c in OSC + + ORCA OSC + =cmcd /c m n d + m : midi channel + n : number (0-35 / ORCA 0-Z) + d : data 0-Z will output (d/36)*127 +''' +def CC(path, tags, args, source): + + midichannel = int(args[0],36) + ccvr = int(args[1],36) + ccvl = int((int(args[2],36)/36)*127) + + if gstt.debug > 0: + print("ccvr=%d/ccvl=%d"%(ccvr,ccvl)) + if gstt.oscname == "ocs2": + gstt.crtvalueOCS2[ccvr]=ccvl + else: + gstt.crtvalueMMO3[ccvr]=ccvl + + for mididevice in midi3.findJamDevices(gstt.oscname): + midi3.cc(gstt.Confs[gstt.oscname][0]["midichan"], ccvr, ccvl, mididevice) + + + +def PB(path, tags, args, source): + + #print("Pitch number",ccnumber, value) + midichannel = int(args[0]) + ccnumber = int(args[1]) + ccdata = int(args[3]) + +''' +# If needed to send some OSC +def SendOSC(ip,port,oscaddress,oscargs=''): + + oscmsg = OSCMessage() + oscmsg.setAddress(oscaddress) + oscmsg.append(oscargs) + + osclient = OSCClient() + osclient.connect((ip, port)) + + if gstt.debug == True : + print("sending OSC message : ", oscmsg, "to", ip, ":", port) + + try: + osclient.sendto(oscmsg, (ip, port)) + oscmsg.clearData() + return True + except: + print ('Connection to', ip, 'refused : died ?') + return False +''' diff --git a/libs/UDPcom.py b/libs/UDPcom.py new file mode 100644 index 0000000..37d6a12 --- /dev/null +++ b/libs/UDPcom.py @@ -0,0 +1,175 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# -*- mode: Python -*- + +''' +UDPcom for jamidi v0.1b + +UDPcom.Start(serverIP, UDPORT) +Handler : udp_thread() + +Read below for : + +- MIDI NOTES use ;nmonv +- MIDI CCs use ;cmnd + +''' + +import midi3 + +#import socket +import types, json +import socket +import _thread, time +import midi3 +import WScom, OSCom +import gstt +import time + +#base36 = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"] + +def GetTime(): + return time.strftime("%a, %d %b %Y %H:%M:%S", time.localtime()) + +def udp_thread(): + + while True: + payload, client_address = sock.recvfrom(1024) + udpath = payload.decode('utf_8') + if gstt.debug > 1: + print(GetTime(),"UDP got", udpath, "from", str(client_address)) + #print(udpath[0:1], " ",udpath[1:2], " ",udpath[2:3], " ",udpath[3:4], " " ) + + if udpath[0:1] == "n": + Note(udpath) + + if udpath[0:1] == "c": + CC(udpath) + + if udpath[0:1] == "f": + FullCC(udpath) + + time.sleep(0.005) + + + +def Start(serverIP, UDPORT): + global sock + + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + server = ( serverIP,UDPORT) + sock.bind(server) + _thread.start_new_thread(udp_thread, ()) + + +''' + +MIDI NOTES use ;nmonv + + m : midi channel 0-F (0-15) + o : octave 0-8 + n : Note A-G. For note with # : a-g + v : velocity 0-Z will output (v/36)*127 + +''' +def Note(udpnote): + + if gstt.debug>0: + print() + print(GetTime(),'UDPNote from ORCA received', udpnote, udpnote[1:1]) + + midichannel = int(udpnote[1:2],36) + octave = int(udpnote[2:3],36) + note = udpnote[3:4] + velocity = (int(udpnote[4:5],36)/36)*127 + + #if gstt.debug>0: + print(GetTime(),'UDPNote from ORCA received:','midichannel:',midichannel,'octave:',octave,'note:',note,'velocity:',velocity) + + + #if octave < 9 or midichannel < 16 or int(note,36) < 10 or int(note,36) > 16: + if octave < 9 and midichannel < 16 and int(note,36) >= 10 and int(note,36) <= 16: + + if note.istitle() == True: + notename = str(note.upper())+ str(octave) + else: + notename = str(note.upper())+ "#"+ str(octave) + + if gstt.debug > 0: + print(GetTime(),"Incoming note", notename, "=", midi3.note2midi(notename), "velocity", velocity, "for channel", midichannel) + + + for mididevice in midi3.findJamDevices(gstt.oscname): + midi3.NoteOn(midi3.note2midi(notename), int(velocity), mididevice, midichannel-1) + + # if sending note back to WS users : + #WScom.sendWSall("/"+midi3.findJamName(gstt.oscname, midichannel)+"/noteon "+str(midi3.note2midi(notename))) + + else: + print(GetTime(),"Note", midichannel, octave, note, velocity,"had offchart parameters.") + + +''' +MIDI CCs base 36 use ;cmnd + + m : midi channel 0-F (0-15) + n : number 0-Z (0-35) + d : data 0-Z will output (d/36)*127 +''' +def CC(udpcc): + + print() + midichannel = int(udpcc[1:2],36) + #midichannel = base36.index(udpcc[1:2].upper()) + ccvr = int(udpcc[2:3],36) + ccvl = int((int(udpcc[3:4],36)/36)*127) + + if midichannel < 16: + + if gstt.debug > 0: + print(GetTime(),"ccvr=%d/ccvl=%d"%(ccvr,ccvl)) + + if gstt.oscname == "ocs2": + gstt.crtvalueOCS2[ccvr]=ccvl + else: + gstt.crtvalueMMO3[ccvr]=ccvl + + for mididevice in midi3.findJamDevices(gstt.oscname): + midi3.cc(midichannel, ccvr, ccvl, mididevice) + #midi3.cc(gstt.Confs[gstt.oscname][0]["midichan"], ccvr, ccvl, mididevice) + WScom.sendWSall("/"+midi3.findJamName(mididevice, midichannel)+"/cc/"+str(ccvr)+" "+str(ccvl)) + else: + print(GetTime(),"Bad midichannel") + +''' +MIDI Full CCs all 128 channels and data use ;fmnndd + + m : midi channel 0-F (0-15) + nn : number 0-3J (0-127) + dd : data 0-3J (0-127) +''' +def FullCC(udpcc): + + print() + midichannel = int(udpcc[1:2],36) + #midichannel = base36.index(udpcc[1:2].upper()) + ccvr = int(udpcc[2:4],36) + ccvl = int(udpcc[4:6],36) + + if midichannel < 16: + + if gstt.debug > 0: + print(GetTime(),"ccvr=%d/ccvl=%d"%(ccvr,ccvl)) + + if gstt.oscname == "ocs2": + gstt.crtvalueOCS2[ccvr]=ccvl + else: + gstt.crtvalueMMO3[ccvr]=ccvl + + for mididevice in midi3.findJamDevices(gstt.oscname): + midi3.cc(midichannel, ccvr, ccvl, mididevice) + WScom.sendWSall("/"+midi3.findJamName(mididevice, midichannel)+"/cc/"+str(ccvr)+" "+str(ccvl)) + + else: + print(GetTime(),"Bad midichannel") + diff --git a/libs/WScom.py b/libs/WScom.py new file mode 100644 index 0000000..55ac681 --- /dev/null +++ b/libs/WScom.py @@ -0,0 +1,169 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# -*- mode: Python -*- + +''' +WScom for jamidi v0.1b + +WScom.Start(serverIP, wsPORT) +WScom.runforever() +handler : message_received(client, wserver, message) + +''' + +import midi3 + +#import socket +import types, json +import _thread, time + +from websocket_server import WebsocketServer +import gstt +import UDPcom, OSCom + +def Start(serverIP, wsPORT): + global wserver + + wserver = WebsocketServer(wsPORT,host=serverIP) + midi3.ws = wserver + + wserver.set_fn_new_client(new_client) + wserver.set_fn_client_left(client_left) + wserver.set_fn_message_received(message_received) + + +def runforever(): + wserver.run_forever() + + +# Called for every WS client connecting (after handshake) +def new_client(client, wserver): + + + print(midi3.GetTime(),"New WS client connected and was given id %d" % client['id']) + #sendWSall("/status Hello %d" % client['id']) + if gstt.current == True: + sendallcurrentccvalues("mmo3") + sendallcurrentccvalues("ocs2") + + gstt.Players+=1 + sendWSall("/status Hello %d" %(client['id'])) + if gstt.Players > 1: + #sendWSall("/gstt.Players %d" %(Players)) + sendWSall("/players (players:%d)" %(gstt.Players)) + else: + sendWSall("/players (player:%d)" %(gstt.Players)) + + + +# Called for every WS client disconnecting +def client_left(client, wserver): + + + try: + print(midi3.GetTime(),"WS Client(%d) disconnected" % client['id']) + gstt.Players-=1 + sendWSall("/players %d" %(gstt.Players)) + except: + print("Something weird if coming from",client,"on the wire...") + pass + + +# Called for each WS received message. +def message_received(client, wserver, message): + + print("") + if len(message) > 200: + message = message[:200]+'..' + + wspath = message.split(" ") + if gstt.debug > 0: + print(midi3.GetTime(),"Main got from WS", client['id'], "said :", message, "splitted in an wspath :", wspath) + else: + print(midi3.GetTime(),"Main got WS Client", client['id'], "said :", message) + + wscommand = wspath[0].split("/") + + # gstt.debug + if gstt.debug > 0: + print("wscommand :",wscommand) + + # noarg + if len(wspath) == 1: + args[0] = "noargs" + #print "noargs command" + + + # CC : /device/cc/2 127 + elif wscommand[2] == "cc": + ccvr=int(wscommand[3]) #cc variable + ccvl=int(wspath[1]) #cc value + if gstt.debug > 0: + print("ccvr=%d/ccvl=%d"%(ccvr,ccvl)) + if wscommand[1] == "ocs2": + gstt.crtvalueOCS2[ccvr]=ccvl + else: + gstt.crtvalueMMO3[ccvr]=ccvl + + for mididevice in midi3.findJamDevices(wscommand[1]): + midi3.cc(gstt.Confs[wscommand[1]][0]["midichan"], ccvr, ccvl, mididevice) + + + # RESET : /device/reset 1 + elif wscommand[2] == "reset": + if wscommand[1] == "ocs2": + reset("ocs2") + else: + reset("mmo3") + + + # NOTEON : /device/noteon note velocity + elif wscommand[2] == "noteon": + for mididevice in midi3.findJamDevices(wscommand[1]): + midi3.NoteOn(int(wspath[1]), int(wspath[2]), mididevice) + #midi3.NoteOn(int(wspath[1]), int(wspath[2]), gstt.Confs[wscommand[1]][0]["mididevice"]) + + + # NOTEOFF /device/noteoff note + elif wscommand[2] == "noteoff": + for mididevice in midi3.findJamDevices(wscommand[1]): + midi3.NoteOff(int(wspath[1]), mididevice) + #midi3.NoteOff(int(wspath[1]), gstt.Confs[wscommand[1]][0]["mididevice"]) + + + # Loop back : WS Client -> server -> WS Client + sendWSall(message) + +# Send through websocket. +# Different websocket library for client (websocket) or server (websocket_server. +# ws object is added here by main.py or client.py startup : midi3.ws = +def send(message): + + if gstt.clientmode == True: + send(message) + else: + wserver.send_message_to_all(msg = message) + + + +def sendWSall(message): + + if gstt.broadcast == True: + if gstt.debug >0: + print(midi3.GetTime(),"WS sending to all %s" % (message)) + + wserver.send_message_to_all(message) + +# /send all current cc values +def sendallcurrentccvalues(nozoid): + + if gstt.broadcast == True: + #print "" + print(midi3.GetTime(),"sending all current cc values of", nozoid) + + if nozoid == "mmo3": + for ccnumber in range(0,32): + sendWSall("/mmo3/cc/"+str(ccnumber)+" "+str(gstt.crtvalueMMO3[ccnumber])) + else: + for ccnumber in range(0,32): + sendWSall("/ocs2/cc/"+str(ccnumber)+" "+str(gstt.crtvalueOCS2[ccnumber])) diff --git a/libs/gstt.py b/libs/gstt.py new file mode 100644 index 0000000..ba6a431 --- /dev/null +++ b/libs/gstt.py @@ -0,0 +1,39 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# -*- mode: Python -*- + +""" +Jamidi states +v0.2.4b + + +LICENCE : CC +by Sam Neurohack +from /team/laser + + +""" + +# reset = [64,64,0,32,96] # un truc comme ca pour les valeurs de reset ? +resetMMO3 = [0] * 32 +resetOCS2 = [0] * 32 + +# record current values +crtvalueMMO3 = [0] * 32 +crtvalueOCS2 = [0] * 32 + +# record number of loaded pages (aka client id or players) +Players=0 + +oscname = "" +Confs = [] + +debug = 0 + +BS="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" +def to_base(s, b): + res = "" + while s: + res+=BS[s%b] + s//= b + return res[::-1] or "0" \ No newline at end of file diff --git a/libs/midi3.py b/libs/midi3.py index ebfdb24..e91ce9a 100644 --- a/libs/midi3.py +++ b/libs/midi3.py @@ -36,6 +36,8 @@ import sys from sys import platform import os import re +import gstt +import WScom is_py2 = sys.version[0] == '2' @@ -61,10 +63,8 @@ midinputsname = ["Name"] * 16 midinputsqueue = [Queue() for i in range(16) ] midinputs = [] -debug = 0 - # False = server / True = Client -clientmode = False +gstt.clientmode = False #Mser = False @@ -152,6 +152,7 @@ def note2midi(note_name): pitch = match.group('n').upper() offset = acc_map[match.group('off')] octave = int(match.group('oct')) + except: raise ValueError('Improper note format: {}'.format(note_name)) # Convert from the extrated ints to a full note number @@ -189,17 +190,17 @@ def midi2hz(note_number): # in a 440 Hz tuning return 440.0*(2.0**((note_number - 69)/12.0)) +# /cc cc number value +def cc(midichannel, ccnumber, value, mididest): + + if gstt.debug>0: + print(GetTime(),"Jamidi Sending Midi channel", midichannel, "cc", ccnumber, "value", value, "to", mididest) + + MidiMsg([CONTROLLER_CHANGE+midichannel-1, ccnumber, value], mididest) + -# Send through websocket. -# Different websocket library for client (websocket) or server (websocket_server. -# ws object is added here by main.py or client.py startup : midi3.ws = -def wssend(message): - if clientmode == True: - ws.send(message) - else: - ws.send_message_to_all(msg = message) # # MIDI Startup and handling @@ -231,8 +232,18 @@ def MidinProcess(inqueue, portname): MidiVel = msg[2] print(GetTime(),"NOTE ON :", MidiNote, 'velocity :', MidiVel, "Channel", MidiChannel) #NoteOn(msg[1],msg[2],mididest) + #NoteOn(msg[1],msg[2],mididest,MidiChannel-1) + + #beware !! + if gstt.nothrough == False: + for mididevice in findJamDevices(gstt.oscname): + #NoteOn(msg[1],msg[2],"XXXX",MidiChannel-1) + if gstt.debug>0: + print(GetTime(),"mididevice/oscname:",mididevice,"/",gstt.oscname) + NoteOn(msg[1],msg[2],mididevice,MidiChannel-1) + print(GetTime(),"Midi in process send /"+findJamName(portname, MidiChannel)+"/noteon "+str(msg[1])+" "+str(msg[2])) - wssend("/"+findJamName(portname, MidiChannel)+"/noteon "+str(msg[1])+" "+str(msg[2])) + WScom.send("/"+findJamName(portname, MidiChannel)+"/noteon "+str(msg[1])+" "+str(msg[2])) ''' # Sampler mode : note <63 launch snare.wav / note > 62 kick.wav @@ -264,7 +275,7 @@ def MidinProcess(inqueue, portname): print(GetTime(),"NOTE OFF :", MidiNote, 'velocity :', MidiVel, "Channel", MidiChannel) #NoteOff(msg[1],msg[2], mididest) print(GetTime(),"Midi in process send /"+findJamName(portname, MidiChannel)+"/noteoff "+str(msg[1])) - wssend("/"+findJamName(portname, MidiChannel)+"/noteoff "+str(msg[1])) + WScom.send("/"+findJamName(portname, MidiChannel)+"/noteoff "+str(msg[1])) # # CC on all Midi Channels @@ -274,7 +285,7 @@ def MidinProcess(inqueue, portname): #findJamName(portname, MidiChannel) print(GetTime(),"channel", MidiChannel, " ",findJamName(portname, MidiChannel), " CC :", msg[1], msg[2]) print(GetTime(),"Midi in process send /"+findJamName(portname, MidiChannel)+"/cc/"+str(msg[1])+" "+str(msg[2])+" to WS") - wssend("/"+findJamName(portname, MidiChannel)+"/cc/"+str(msg[1])+" "+str(msg[2])) + WScom.send("/"+findJamName(portname, MidiChannel)+"/cc/"+str(msg[1])+" "+str(msg[2])) ''' @@ -282,7 +293,7 @@ def MidinProcess(inqueue, portname): if CONTROLLER_CHANGE -1 < msg[0] < 192: print("channel 1 (MMO-3) CC :", msg[1], msg[2]) print("Midi in process send /mmo3/cc/"+str(msg[1])+" "+str(msg[2])+" to WS") - wssend("/mmo3/cc/"+str(msg[1])+" "+str(msg[2])) + WScom.send("/mmo3/cc/"+str(msg[1])+" "+str(msg[2])) # OCS-2 Midi CC message CHANNEL 2 @@ -290,7 +301,7 @@ def MidinProcess(inqueue, portname): print("channel 2 (OCS-2) CC :", msg[1], msg[2]) print("Midi in process send /ocs2/cc/"+str(msg[1])+" "+str(msg[2])+" to WS") - wssend("/ocs2/cc/"+str(msg[1])+" "+str(msg[2])) + WScom.send("/ocs2/cc/"+str(msg[1])+" "+str(msg[2])) ''' @@ -303,19 +314,24 @@ def MidinProcess(inqueue, portname): ''' -def NoteOn(note,color, mididest): +#def NoteOn(note, color, mididest): +#https://pypi.org/project/python-rtmidi/0.3a/ +#à NOTE_ON=#90 et NOTE_OFF=#80 on ajoute le channel (0 le premier) pour envoyer effectivement sur le channel +def NoteOn(note, color, mididest, midichannel=0): global MidInsNumber - + if gstt.debug >0: + print(GetTime(),"Sending", note, color, "to", mididest, "on channel", midichannel) + for port in range(MidInsNumber): # To mididest if midiname[port].find(mididest) == 0: - midiport[port].send_message([NOTE_ON, note, color]) + midiport[port].send_message([NOTE_ON+midichannel, note, color]) # To All elif mididest == "all" and midiname[port].find(mididest) != 0: - midiport[port].send_message([NOTE_ON, note, color]) + midiport[port].send_message([NOTE_ON+midichannel, note, color]) @@ -491,8 +507,8 @@ def InConfig(): print(GetTime(),"MIDIin...") # client mode - if debug > 0: - if clientmode == True: + if gstt.debug > 0: + if gstt.clientmode == True: print(GetTime(),"midi3 in client mode") else: print(GetTime(),"midi3 in server mode") @@ -603,8 +619,8 @@ def MidiMsg(midimsg, mididest): for port in range(len(OutDevice)): # To mididest if midiname[port].find(mididest) != -1: - if debug>0: - print(GetTime(),"jamidi 3 sending to name", midiname[port], "port", port, ":", midimsg) + if gstt.debug>0: + print(GetTime(),"jamidi3 sending to name", midiname[port], "port", port, ":", midimsg) midiport[port].send_message(midimsg) desterror = 0 @@ -612,11 +628,11 @@ def MidiMsg(midimsg, mididest): print(GetTime(),"mididest",mididest, ": ** This midi destination doesn't exists **") # send midi msg over ws. - #if clientmode == True: + #if gstt.clientmode == True: # ws.send("/ocs2/cc/1 2") - +''' def NoteOn(note, velocity, mididest): global MidInsNumber @@ -626,7 +642,7 @@ def NoteOn(note, velocity, mididest): # To mididest if midiname[port].find(mididest) == 0: midiport[port].send_message([NOTE_ON, note, velocity]) - +''' def listdevice(number): @@ -638,17 +654,20 @@ def listdevice(number): # return device name for given mididevice and midichannel def findJamName(mididevice, midichan): - #print("searching", mididevice, "channel", midichan,'...') - for (k, v) in Confs.items(): + if gstt.debug >0: + print(GetTime(),"Findjamname searching", mididevice, "channel", midichan,'...') + + for (k, v) in gstt.Confs.items(): #print("Key: " + k) #print("Value: " + str(v)) if v[0]["type"] == "mididevice": - #print(v[0]["mididevice"],v[0]["midichan"], type(v[0]["midichan"])) + #print(k, v[0]["mididevice"],v[0]["midichan"],type(v[0]["midichan"]),v[0]["xname"], "?") if (v[0]["mididevice"] == mididevice) and (v[0]["midichan"] == midichan): print(GetTime(),"Incoming event from", k, "xname", v[0]["xname"]) return v[0]["xname"] + return "None" @@ -656,15 +675,17 @@ def findJamName(mididevice, midichan): def findJamDevices(name): devices = [] - print (GetTime(),"searching", name) - for (k, v) in Confs.items(): + if gstt.debug >0: + print (GetTime(),"Findjamdevice searching", name) + for (k, v) in gstt.Confs.items(): if v[0]["type"] == "mididevice": #print(k, name,v[0]["xname"]) if v[0]["xname"] == name: #print(v[0]["mididevice"]) devices.append(v[0]["mididevice"]) - # print(devices) + if gstt.debug>0: + print(GetTime(),devices) return devices diff --git a/main.py b/main.py index 16dd5eb..90db2f9 100755 --- a/main.py +++ b/main.py @@ -14,6 +14,21 @@ wserver.run_forever() wserver.send_message_to_all(message) +ORCA: + +CTRL K pour rentrer une commande + +CTRL K ip:127.0.0.1 +CTRL K osc:8082 +CTRL K cc:0 +CTRL K udp:udport + + + +/p +Pitch bend + + ''' @@ -33,28 +48,36 @@ from rtmidi.midiconstants import (CHANNEL_PRESSURE, CONTROLLER_CHANGE, NOTE_ON, sys.path.append('libs/') import midi3 -from websocket_server import WebsocketServer #import socket import types, json import argparse +import _thread, time -debug = 1 +from midi3 import note2midi +from midi3 import GetTime + +import OSCom +import WScom +import UDPcom +import gstt print ("") print ("Arguments parsing if needed...") argsparser = argparse.ArgumentParser(description="Jamidi Server v0.1b commands help mode") argsparser.add_argument("-s","--servername",help="Servername: 'local', 'llstrvpn' (local by default)", type=str) -argsparser.add_argument('--current',help="Send all current CC values to all new client. Default option" , dest='current', action='store_true') -argsparser.add_argument('--no-current',help="Do not send all current CC values to all new client. ", dest='current', action='store_false') -argsparser.add_argument('--broadcast',help="Broadcast all incomings commands to all client. Default option" , dest='broadcast', action='store_true') -argsparser.add_argument('--no-broadcast',help="Do not broadcast all incomings commands to all client", dest='broadcast', action='store_false') -argsparser.add_argument('--reset',help="Send reset values to local device a startup. Default option" , dest='reset', action='store_true') -argsparser.add_argument('--no-reset',help="Do not send reset values to local device a startup.", dest='reset', action='store_false') -argsparser.set_defaults(reset=True) +argsparser.add_argument("-d","--device",help="midi device for incoming ORCA via UDP (mmo3 by default)", type=str) +argsparser.add_argument('-nothrough',help="Disable the builtin midithrough from any midi IN to --device enabled by default", dest='nothrough', action='store_true') +argsparser.set_defaults(nothrough=False) +argsparser.add_argument('-nocurrent',help="Do not send all current CC values to all new client (enabled by default)", dest='current', action='store_false') argsparser.set_defaults(current=True) +argsparser.add_argument('-nobroadcast',help="Do not broadcast all incomings commands to all client (enabled by default)", dest='broadcast', action='store_false') argsparser.set_defaults(broadcast=True) +argsparser.add_argument('-noreset',help="Do not broadcast all incomings commands to all client (enabled by default)", dest='reset', action='store_false') +argsparser.set_defaults(reset=True) +argsparser.add_argument('-verbose',help="Enable debug mode (disabled by default)", dest='verbose', action='store_true') +argsparser.set_defaults(verbose=False) args = argsparser.parse_args() @@ -64,41 +87,95 @@ if args.servername: else: servername = "local" +# ORCA destination device +if args.device: + gstt.oscname = args.device +else: + gstt.oscname = "mmo3" + + # Broadcast commands to all clients ? if args.broadcast == False: print("Broadcast disabled") - broadcast = False + gstt.broadcast = False else: print("Broadcast enabled") - broadcast = True + gstt.broadcast = True # Send current values to all new client ? if args.current == False: print("Do not send current values at startup disabled") - current = False + gstt.current = False else: print("Current values update at startup disabled") - current = True + gstt.current = True # Reset at startup ? if args.reset == False: print("Reset at startup disabled") - startreset = False + gstt.startreset = False else: print("Reset at startup enabled") - startreset = True + gstt.startreset = True + +# Debug/verbose mode ? +if args.verbose == False: + print("Debug mode disabled") + gstt.debug = 0 +else: + print("Debug mode enabled") + gstt.debug = 1 + +# nomidithrough mode ? +if args.nothrough == False: + print("Midi through mode") + gstt.nothrough = False +else: + print("No midi through mode") + gstt.nothrough = True -# reset = [64,64,0,32,96] # un truc comme ca pour les valeurs de reset ? -resetMMO3 = [0] * 32 -resetOCS2 = [0] * 32 +#base36 = {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"} -# record current values -crtvalueMMO3 = [0] * 32 -crtvalueOCS2 = [0] * 32 +''' +base36 = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z} + +transpose = +"0": None, "1": None, "2": None, "3":, None, "4":, None, "5":, "6":, "7":, "8":, "9":, "A": "A0", "B": B0, +C ,"C0", D : "D0", "E": "E0" F G H I J K L M N +"C0" "D0" "E0" "F0" "G0" "A0" "B0" "C1" "D1" "E1" "F1" "G1" +O P Q R S T U V W X Y Z +"A1" "B1" "C2" "D2" "E2" "F2" "G2" "A2" "B2" "C3" "D3" "E3" +''' + +# +# Settings from jamidi.json +# + +# Load midi definitions in jamidi.json +def LoadConfs(): + + if os.path.exists('jamidi.json'): + f=open("jamidi.json","r") + + s = f.read() + gstt.Confs = json.loads(s) + #print(GetTime(),gstt.Confs) + + +# return midi confname number for given type +def findConfs(confname,conftype): + + #print("searching", midiconfname,'...') + position = -1 + for counter in range(len(gstt.Confs[conftype])): + if confname == gstt.Confs[conftype][counter]['name']: + #print(confname, "is ", counter) + position = counter + return position + +LoadConfs() -# record number of loaded pages (aka client id or players) -Players=0 # @@ -117,8 +194,8 @@ def GetTime(): # /cc cc number value def cc(midichannel, ccnumber, value, mididest): - if debug>0: - print("Jamidi Sending Midi channel", midichannel, "cc", ccnumber, "value", value, "to", mididest) + if gstt.debug>0: + print(GetTime(),"Jamidi Sending Midi channel", midichannel, "cc", ccnumber, "value", value, "to", mididest) midi3.MidiMsg([CONTROLLER_CHANGE+midichannel-1, ccnumber, value], mididest) @@ -131,173 +208,17 @@ def reset(nozoid): if nozoid == "mmo3": for ccnumber in range(0,32): - midi3.MidiMsg([CONTROLLER_CHANGE+Confs["mmo3"][0]["midichan"]-1, ccnumber, resetMMO3[ccnumber]], Confs["mmo3"][0]["mididevice"]) - sendWSall("/mmo3/cc/"+str(ccnumber)+" "+str(resetMMO3[ccnumber])) - crtvalueMMO3[ccnumber]=resetMMO3[ccnumber] + midi3.MidiMsg([CONTROLLER_CHANGE+gstt.Confs["mmo3"][0]["midichan"]-1, ccnumber, gstt.resetMMO3[ccnumber]], gstt.Confs["mmo3"][0]["mididevice"]) + WScom.sendWSall("/mmo3/cc/"+str(ccnumber)+" "+str(gstt.resetMMO3[ccnumber])) + gstt.crtvalueMMO3[ccnumber]=gstt.resetMMO3[ccnumber] else: for ccnumber in range(0,32): - midi3.MidiMsg([CONTROLLER_CHANGE+Confs["ocs2"][0]["midichan"]-1, ccnumber, resetOCS2[ccnumber]], Confs["ocs2"][0]["mididevice"]) - sendWSall("/ocs2/cc/"+str(ccnumber)+" "+str(resetOCS2[ccnumber])) - crtvalueOCS2[ccnumber]=resetOCS2[ccnumber] - print("End of reset for", nozoid) + midi3.MidiMsg([CONTROLLER_CHANGE+gstt.Confs["ocs2"][0]["midichan"]-1, ccnumber, gstt.resetOCS2[ccnumber]], gstt.Confs["ocs2"][0]["mididevice"]) + WScom.sendWSall("/ocs2/cc/"+str(ccnumber)+" "+str(gstt.resetOCS2[ccnumber])) + gstt.crtvalueOCS2[ccnumber]=gstt.resetOCS2[ccnumber] + print(GetTime(),"End of reset for", nozoid) print("") -# /send all current cc values -def sendallcurrentccvalues(nozoid): - - if broadcast == True: - #print "" - print(GetTime(),"sending all current cc values of", nozoid) - - if nozoid == "mmo3": - for ccnumber in range(0,32): - sendWSall("/mmo3/cc/"+str(ccnumber)+" "+str(crtvalueMMO3[ccnumber])) - else: - for ccnumber in range(0,32): - sendWSall("/ocs2/cc/"+str(ccnumber)+" "+str(crtvalueOCS2[ccnumber])) - -# -# Settings from jamidi.json -# - -# Load midi definitions in jamidi.json -def LoadConfs(): - global Confs, nbmidiconf - - if os.path.exists('jamidi.json'): - f=open("jamidi.json","r") - - s = f.read() - Confs = json.loads(s) - - -# return midi confname number for given type -def findConfs(confname,conftype): - - #print("searching", midiconfname,'...') - position = -1 - for counter in range(len(Confs[conftype])): - if confname == Confs[conftype][counter]['name']: - #print(confname, "is ", counter) - position = counter - return position - - - -# -# Websocket part -# - -# Called for every WS client connecting (after handshake) -def new_client(client, wserver): - - global Players - - print(GetTime(),"New WS client connected and was given id %d" % client['id']) - #sendWSall("/status Hello %d" % client['id']) - if current == True: - sendallcurrentccvalues("mmo3") - sendallcurrentccvalues("ocs2") - - Players+=1 - sendWSall("/status Hello %d" %(client['id'])) - if Players > 1: - #sendWSall("/players %d" %(Players)) - sendWSall("/players (players:%d)" %(Players)) - else: - sendWSall("/players (player:%d)" %(Players)) - - - -# Called for every WS client disconnecting -def client_left(client, wserver): - - global Players - - try: - print(GetTime(),"WS Client(%d) disconnected" % client['id']) - Players-=1 - sendWSall("/players %d" %(Players)) - except: - print("Something weird if coming from",client,"on the wire...") - pass - - -# Called for each WS received message. -def message_received(client, wserver, message): - - print("") - if len(message) > 200: - message = message[:200]+'..' - - oscpath = message.split(" ") - if debug > 0: - print(GetTime(),"Main got from WS", client['id'], "said :", message, "splitted in an oscpath :", oscpath) - else: - print(GetTime(),"Main got WS Client", client['id'], "said :", message) - - wscommand = oscpath[0].split("/") - - # debug - if debug > 0: - print("wscommand :",wscommand) - - # noarg - if len(oscpath) == 1: - args[0] = "noargs" - #print "noargs command" - - - # CC : /device/cc/2 127 - elif wscommand[2] == "cc": - ccvr=int(wscommand[3]) #cc variable - ccvl=int(oscpath[1]) #cc value - if debug > 0: - print("ccvr=%d/ccvl=%d"%(ccvr,ccvl)) - if wscommand[1] == "ocs2": - #cc(Confs[wscommand[1]][0]["midichan"], ccvr, ccvl, Confs[wscommand[1]][0]["mididevice"]) - crtvalueOCS2[ccvr]=ccvl - else: - #cc(Confs[wscommand[1]][0]["midichan"], ccvr, ccvl, Confs[wscommand[1]][0]["mididevice"]) - crtvalueMMO3[ccvr]=ccvl - - for mididevice in midi3.findJamDevices(wscommand[1]): - cc(Confs[wscommand[1]][0]["midichan"], ccvr, ccvl, mididevice) - - - # RESET : /device/reset 1 - elif wscommand[2] == "reset": - if wscommand[1] == "ocs2": - reset("ocs2") - else: - reset("mmo3") - - - # NOTEON : /device/noteon note velocity - elif wscommand[2] == "noteon": - for mididevice in midi3.findJamDevices(wscommand[1]): - midi3.NoteOn(int(oscpath[1]), int(oscpath[2]), mididevice) - #midi3.NoteOn(int(oscpath[1]), int(oscpath[2]), Confs[wscommand[1]][0]["mididevice"]) - - - # NOTEOFF /device/noteoff note - elif wscommand[2] == "noteoff": - for mididevice in midi3.findJamDevices(wscommand[1]): - midi3.NoteOff(int(oscpath[1]), mididevice) - #midi3.NoteOff(int(oscpath[1]), Confs[wscommand[1]][0]["mididevice"]) - - - # Loop back : WS Client -> server -> WS Client - sendWSall(message) - - -def sendWSall(message): - - if broadcast == True: - if debug >0: - print(GetTime(),"sending to all %s" % (message)) - - wserver.send_message_to_all(message) @@ -305,45 +226,43 @@ def sendWSall(message): # Running... # -LoadConfs() -serverIP = Confs[servername][0]["IP"] -wsPORT = Confs[servername][0]["port"] -print(Confs["ocs2"][0]["mididevice"]) +serverIP = gstt.Confs[servername][0]["IP"] +wsPORT = gstt.Confs[servername][0]["port"] +OSCPORT = gstt.Confs[servername][0]["oscport"] +UDPORT = gstt.Confs[servername][0]["udport"] -print("Running....") +print() +print(GetTime(),"Launching servers...") +print(GetTime(),"Launching OSC Server", serverIP,':', OSCPORT) +OSCom.Start(serverIP, OSCPORT) + +print(GetTime(),"Launching WS Server", serverIP,':', wsPORT) +UDPcom.Start(serverIP, UDPORT) + +print(GetTime(),"Launching UDP Server", serverIP,':', UDPORT) +WScom.Start(serverIP, wsPORT) + + +if gstt.startreset == True: + print(GetTime(),"resetting nozoids...") + reset("mmo3") + reset("ocs2") + +# Main -# Main loop do nothing. Maybe do the webui server ? try: - #while True: - - # Websocket startup - wserver = WebsocketServer(wsPORT,host=serverIP) - midi3.ws = wserver - midi3.Confs = Confs - #midi3.findJamDevices("ocs2") - print("") - print(GetTime(),"Launching Jamidi Websocket server...") - print(GetTime(),"at", serverIP, "port",wsPORT) - wserver.set_fn_new_client(new_client) - wserver.set_fn_client_left(client_left) - wserver.set_fn_message_received(message_received) + print(GetTime(),"Jamidi running forever...") - if startreset == True: - print("resetting nozoids...") - reset("mmo3") - reset("ocs2") - - - #print "" - print(GetTime(),"WS server running forever...") - - wserver.run_forever() + WScom.runforever() except Exception: traceback.print_exc() +finally: + OSCom.Stop() + # Gently stop on CTRL C