diff --git a/README.md b/README.md index f6b3764..13a3b10 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ or /tr808/note/1 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 and /tr808 changes made by John will be displayed to everyone and played by devices. @@ -60,12 +61,17 @@ Options : servername : 'local', 'llstrvpn'. Servers (IP, port,...) must be described in jamidi.json --broadcast : Broadcast all incomings commands to all client. Default option. + --no-broadcast : Do not broadcast all incomings commands to all client. + --reset : Send reset values to local device a startup. Default option. + --no-reset : Do not send reset values to local device a startup. + --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. @@ -78,11 +84,17 @@ Can be webpages or midi instrument/software. Multiple clients types is supported Options : servername : Remote server 'local', 'xrkia' ('local' by default). Servers must be described in jamidi.json + default : Network <-> default midi device (True or False) + 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 @@ -90,13 +102,18 @@ Each rule may happen at all time or only during a "song". 1/ Run : -python3 mainwip.py + +python3 main.py + 2/ In a browser open 2 instances of each of these : + indexaurora.html is a three rotating encoders example. + mmo3.html and ocs2.html demo to control 2 real life nozoids. + 3/ Each aurora pages talk to each other. Each mmo3 to each other and so on.. @@ -114,7 +131,7 @@ var LJ = 'ws://127.0.0.1:8081/' 3/ Run : -python3 mainwip.py +python3 main.py 4/ diff --git a/client.py b/client.py index f02421b..7e485e8 100644 --- a/client.py +++ b/client.py @@ -1,37 +1,27 @@ -#!/usr/bin/python2.7 +#!/usr/bin/python3 # -*- coding: utf-8 -*- # -*- mode: Python -*- + ''' -NozoidUI Client v0.0.1 - -Input : local midi instruments -Output : nozoidtUI server - - - -Websocket INSTALLER - - -https://files.pythonhosted.org/packages/8b/0f/52de51b9b450ed52694208ab952d5af6ebbcbce7f166a48784095d930d8c/websocket_client-0.57.0.tar.gz - +Jamidi Client v0.1b +Input : local midi (hardware/software) instruments +Output : Jamidi server ''' +print("") +print("") +print("Jamidi Client") +print("v0.1b") -print "" -print "" -print "NozoidUI Client" -print "v0.0.1" - -from multiprocessing import Process, Queue, TimeoutError - - -import subprocess +#from multiprocessing import Process, Queue, TimeoutError +#import subprocess import sys import traceback import os import time +import json from rtmidi.midiconstants import (CHANNEL_PRESSURE, CONTROLLER_CHANGE, NOTE_ON, NOTE_OFF, PITCH_BEND, POLY_PRESSURE, PROGRAM_CHANGE) @@ -44,27 +34,46 @@ import websocket try: - import thread + import _thread except ImportError: import _thread as thread -import time + +import argparse debug = 1 -# -# webUI server -# -serverIP = "xrkia.org" -# serverIP = "127.0.0.1" -# serverIP = "10.8.0.46" -wsPORT = 8081 +print ("") +print ("Arguments parsing if needed...") +argsparser = argparse.ArgumentParser(description="Jamidi Client v0.1b commands help mode") +argsparser.add_argument("-s","--servername",help="servername: 'local', 'xrkia' ('local' by default)", type=str) +argsparser.add_argument('--default',help="All incoming midi <-> default midi device. Default option." , dest='default', action='store_true') +argsparser.add_argument('--no-default',help="Do not send reset values to local device a startup.", dest='default', action='store_false') +argsparser.set_defaults(default=True) + +args = argsparser.parse_args() + +# Server +if args.servername: + servername = args.servername +else: + servername = "local" + +# Default midi device like controller +if args.default == False: + print("Default device disabled") + defaultdevice = False +else: + print("Default device enabled") + defaultdevice = True # # Midi part # +# default local MIDI device + nozmidi = "BCR2000 Port 1" # nozmidi = "Arturia BeatStep" # nozmidi = "Virtual Midi A" @@ -79,30 +88,151 @@ midichanMMO3 = 1 resetMMO3 = [0] * 32 resetOCS2 = [0] * 32 -# /cc cc number value -def cc(midichannel, ccnumber, value, mididest): +songs = ["song1", "song2"] +song = 0 - print "NozoidUI Sending Midi channel", midichannel, "cc", ccnumber, "value", value, "to", mididest + +def GetTime(): + return time.strftime("%a, %d %b %Y %H:%M:%S", time.localtime()) + + +# +# Settings from clientruler.json +# + +# Load midi routing definitions in clientruler.json +def LoadMidiRules(): + global MidiRules, nbmidirule + + if os.path.exists('rules.json'): + f=open("rules.json","r") + + s = f.read() + MidiRules = json.loads(s) + + + + +# return midi rulename number for given type 'Specials', 'cc2cc' +def findMidiRules(rulename,ruletype): + + #print("searching", midirulename,'...') + position = -1 + for counter in range(len(MidiRules[ruletype])): + if rulename == MidiRules[ruletype][counter]['name']: + #print(rulename, "is ", counter) + position = counter + return position + +# +# Settings from jamidi.json +# + +# Load midi routing definitions in clientconfr.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 'Specials', 'cc2cc' +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 + + + +def curved(value): + return round(np.sqrt(value)*11.27) + + +# /cc cc number value +def cc(midichannel, ccnumber, value, mididest): + + #print "cc()" + print("Jamidi Sending locally Midi channel", midichannel, "cc", ccnumber, "value", value, "to", mididest) + + # Apply CC routing ? + if len(MidiRules["cc2cc"]) > 0 : + #print "cc2cc test for channel", midichannel, "CC", ccnumber, "val", value, "song", songs[song] + for counter in range(len(MidiRules["cc2cc"])): + if (MidiRules["cc2cc"][counter]["songname"] == songs[song] or MidiRules["cc2cc"][counter]["songname"] == "all") and (MidiRules["cc2cc"][counter]["chanIN"] == midichannel or MidiRules["cc2cc"][counter]["chanIN"] == "all") and (MidiRules["cc2cc"][counter]["ccs"] == ccnumber or MidiRules["cc2cc"][counter]["ccs"] == "all"): + print("cc2cc routing for song",MidiRules["cc2cc"][counter]["songname"], ":", MidiRules["cc2cc"][counter]["name"]) + #print("cc2cc got song :", MidiRules["cc2cc"][counter]["songname"]," IN Channel :", MidiRules["cc2cc"][counter]["chanIN"]," Code :", MidiRules["cc2cc"][counter]["code"], " value :",MidiRules["ZnotesLcc"][counter]["valuetype"], ) + if MidiRules["cc2cc"][counter]["valuetype"] == "linear": + print("Linear", MidiVal) + midi3.MidiMsg((176 + MidiRules["cc2cc"][counter]["chanOUT"], MidiRules["cc2cc"][counter]["ccOUT"], MidiVal), MidiRules["cc2cc"][counter]["mididest"]) + + if MidiRules["cc2cc"][counter]["valuetype"] == "curved": + print(MidiVal,"got curved", curved(MidiVal)) + midi3.MidiMsg((176 + MidiRules["cc2cc"][counter]["chanOUT"], MidiRules["cc2cc"][counter]["ccOUT"], curved(MidiVal)), MidiRules["cc2cc"][counter]["mididest"]) + + + #else: + print("Jamidi Sending locally Midi channel", midichannel, "cc", ccnumber, "value", value, "to", mididest) #if mididest == "BCR2000 Port 1": midi3.MidiMsg([CONTROLLER_CHANGE+midichannel-1, ccnumber, value], mididest) + ''' + # Specials features ? + if len(MidiRules['Specials']) > 0: + + for counter in range(len(MidiRules["Specials"])): + + # print() + # print("Name", MidiRules["Specials"][counter]["name"]) + # print("Song", MidiRules["Specials"][counter]["songname"], songs[song]) # name, "all" + # print("Channel", MidiRules["Specials"][ counter]["chanIN"], MidiChannel) # number, "all" + # print("Note", MidiRules["Specials"][counter]["notes"], MidiNote) # number, "all" + # print("Notetype", MidiRules["Specials"][counter]["notetype"], "on") # "on", "off", "all" + + if (MidiRules["Specials"][counter]["songname"] == songs[song] or MidiRules["Specials"][counter]["songname"] == "all") and (MidiRules["Specials"][counter]["chanIN"] == MidiChannel or MidiRules["Specials"][counter]["chanIN"] == "all") and (MidiRules["Specials"][counter]["notes"] == MidiNote or MidiRules["Specials"][counter]["notes"] == "all") and (MidiRules["Specials"][counter]["notetype"] == "off" or MidiRules["Specials"][counter]["notetype"] == "all") : + midirulecode = MidiRules["Specials"][counter]["code"] + print("Specials function :",MidiRules["Specials"][counter]["songname"], ":", MidiRules["Specials"][counter]["name"], midirulecode) + + # python function + if midirulecode.count('.') > 0: + #print(midirulecode+"("+str(MidiNote)+')') + eval(midirulecode+"("+str(MidiNote)+')') + + # Maxwell function + elif midirulecode.count('/') > 0: + #print("Specials NoteON got :", MidiRules["Specials"][counter]["songname"]," IN Channel :", MidiRules["Specials"][counter]["chanIN"]," Code :", midirulecode, " CC", maxwellccs.FindCC(MidiRules["Specials"][counter]["code"]), " value :",MidiRules["Specials"][counter]["valuetype"], " laser :", MidiRules["ZccLcc"][counter]["laser"] ) + midi3.MidiMsg((CONTROLLER_CHANGE, maxwellccs.FindCC(MidiRules["Specials"][counter]["code"]), MidiRules["Specials"][counter]["valuetype"]), mididest, laser = MidiRules["Specials"][counter]["laser"]) + + ''' + # /reset nozoids with "default" values def reset(nozoid): - print "" - print "reseting", nozoid + print("") + print(GetTime(),"reseting", nozoid) if nozoid == "mmo3": - for ccnumber in xrange(0,32): - midi3.MidiMsg([CONTROLLER_CHANGE+midichanMMO3-1, ccnumber, resetMMO3[ccnumber]], nozmidi) + 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] else: - for ccnumber in xrange(0,32): - midi3.MidiMsg([CONTROLLER_CHANGE+midichanOCS2-1, ccnumber, resetOCS2[ccnumber]], nozmidi) - sendWSall("/ocs2/cc/"+str(ccnumber)+" "+str(resetMMO3[ccnumber])) - + 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) + print("") # # Websocket part # @@ -132,11 +262,11 @@ def on_open(ws): print("thread terminating...") - thread.start_new_thread(run, ()) + _thread.start_new_thread(run, ()) def on_message(ws, message): # - print "" + print("") print(message) if len(message) > 200: message = message[:200]+'..' @@ -144,22 +274,31 @@ def on_message(ws, message): oscpath = message.split(" ") if debug > 0: #print "Client got from WS", client['id'], "said :", message, "splitted in an oscpath :", oscpath - print "Client got from WS said :", message, "splitted in an oscpath :", oscpath + print("clientruler got from WS :", message, "splitted in an oscpath :", oscpath) wscommand = oscpath[0].split("/") - print "WS command was :",wscommand + print("WS command was :",wscommand) if len(oscpath) == 1: args[0] = "noargs" #print "noargs command" - elif wscommand[2] == "cc": - if wscommand[1] == "ocs2": - print "Incoming OCS-2 WS" - cc(midichanOCS2, int(wscommand[3]), int(oscpath[1]), nozmidi) - else: - print "Incoming MMO-3 WS" - cc(midichanMMO3, int(wscommand[3]), int(oscpath[1]), nozmidi) + elif wscommand[1] == "ocs2": + if wscommand[2] == "cc": + print("Incoming OCS-2 WS CC", wscommand[3], ":", int(oscpath[1])) + cc(Confs["ocs2"][0]["midichan"], int(wscommand[3]), int(oscpath[1]), Confs["default"][0]["mididevice"]) + + if wscommand[2] == "OSC1": + print("Incoming OCS-2 WS OSC1", wscommand[3], ":", int(oscpath[1])) + + + elif wscommand[1] == "mmo3": + if wscommand[2] == "cc": + print("Incoming MMO-3 WS CC", wscommand[3], ":", int(oscpath[1])) + cc(Confs["mmo3"][0]["midichan"], int(wscommand[3]), int(oscpath[1]), Confs["default"][0]["mididevice"]) + + if wscommand[2] == "OSC1": + print("Incoming MMO-3 WS OSC1", wscommand[3], ":", int(oscpath[1])) elif wscommand[2] == "reset": @@ -173,15 +312,21 @@ def on_message(ws, message): # sendWSall(message) +LoadMidiRules() +LoadConfs() -print "Running...." +serverIP = Confs[servername][0]["IP"] +#serverIP = "10.8.0.46" +wsPORT = Confs[servername][0]["port"] + +print("Running....") # Main loop do nothing. Maybe do the webui server ? try: - print "" - print "Connecting to NozoidUI server..." - print "at", serverIP, "port",wsPORT + print("") + print("Connecting to Jamidi server...") + print("at", serverIP, "port",wsPORT) #websocket.enableTrace(True) ws = websocket.WebSocketApp("ws://"+str(serverIP)+":"+str(wsPORT), @@ -192,21 +337,22 @@ try: midi3.ws = ws midi3.wsmode = True - print "Midi Configuration..." - print "Midi Destination", nozmidi + print("Midi Configuration...") midi3.check() ws.on_open = on_open ws.run_forever() +except Exception: + traceback.print_exc() except KeyboardInterrupt: pass # Gently stop on CTRL C -print "Fin de NozoidUI." +print("Fin de Jamidi.")