diff --git a/README.md b/README.md index 2c751b3..f6b3764 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,142 @@ -# jamidi -Midi exchanges over websocket for webpages, midi devices/sofware,... \ No newline at end of file +Jamidi v0.1b +By llstr, Sam Neurohack + +LICENCE : CC NC + +Midi and more exchanges over LAN/Internet + +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. + +More you can use also a vcvrack complex patch to drive light fixtures, laser abstract generators, midi instruments, wherever they are... + + + + + +# Jamidi features + +- Jamidi vs rtpmidi ? rtpmidi is transport efficiency, but bound to midi specifications. Jamidi support midi cc and note but is more OSC style : you can add any type of "command". + +- Jamidi is websocket based, so accept anything (webpage, home made client,...). + +- Jamidi doesn't broadcast audio. Use whatever solution you like. We use icecast and VLC to listen. + +- Jamidi is experimental and nowhere safe. You need to understand safety risks with opening a network port, especially over Internet. Ask someone who knows if you don't. + + + + +# How it work : Websocket + + +Websocket transport a string like : + +/ocs2/cc/2 0 + +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. + + +Websocket default port is 8081 but one can change. + + + + +# How it work : Server + +Will receive all "commands" from all clients, forward them to local devices and broadcast them too. + + +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. + + + +# How it work : Clients + +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". + + + + +# Webpages examples + +1/ Run : + +python3 mainwip.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.. + + +These pages will work with local jamidi server, but they can't guess your online configuration. You need to modify IP and port in line : + +var LJ = 'ws://127.0.0.1:8081/' + + + +# Midi instrument usage + +1/ You need to edit jamidi.json and follow ocs2 example. + +2/ Create a client/webpage that will send /ocs2/xxx + +3/ Run : + +python3 mainwip.py + +4/ + + + + +# Install + +You need python3 and pip3 + +sudo apt-get install python3-pip + +pip3 install python-rtmidi (sudo apt install libasound2-dev, sudo apt install libjack-dev) + +pip3 install mido + +pip3 install numpy + + +for Websocket client : +Download : https://files.pythonhosted.org/packages/8b/0f/52de51b9b450ed52694208ab952d5af6ebbcbce7f166a48784095d930d8c/websocket_client-0.57.0.tar.gz + +Decompress + +python3 setup.py install diff --git a/client.py b/client.py new file mode 100644 index 0000000..f02421b --- /dev/null +++ b/client.py @@ -0,0 +1,213 @@ +#!/usr/bin/python2.7 +# -*- 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 + + + +''' + + +print "" +print "" +print "NozoidUI Client" +print "v0.0.1" + +from multiprocessing import Process, Queue, TimeoutError + + +import subprocess +import sys +import traceback +import os +import time + +from rtmidi.midiconstants import (CHANNEL_PRESSURE, CONTROLLER_CHANGE, NOTE_ON, NOTE_OFF, + PITCH_BEND, POLY_PRESSURE, PROGRAM_CHANGE) +import midi3 + +# from websocket_server import WebsocketServer +#import socket +import types, time +import websocket + + +try: + import thread +except ImportError: + import _thread as thread +import time + +debug = 1 + +# +# webUI server +# + +serverIP = "xrkia.org" +# serverIP = "127.0.0.1" +# serverIP = "10.8.0.46" +wsPORT = 8081 + + +# +# Midi part +# + +nozmidi = "BCR2000 Port 1" +# nozmidi = "Arturia BeatStep" +# nozmidi = "Virtual Midi A" +# nozmidi = "Virtual Sequencer" +# nowmidi = "IAC Driver Sequencer Bus 1" +# nozmidi = "UM-ONE:UM-ONE MIDI 1 20:0" + +midichanOCS2 = 2 +midichanMMO3 = 1 + +# resetMMO3 = [64,64,0,32,96] # un truc comme ca pour les valeurs de reset ? +resetMMO3 = [0] * 32 +resetOCS2 = [0] * 32 + +# /cc cc number value +def cc(midichannel, ccnumber, value, mididest): + + print "NozoidUI Sending Midi channel", midichannel, "cc", ccnumber, "value", value, "to", mididest + #if mididest == "BCR2000 Port 1": + + midi3.MidiMsg([CONTROLLER_CHANGE+midichannel-1, ccnumber, value], mididest) + + +# /reset nozoids with "default" values +def reset(nozoid): + + print "" + print "reseting", nozoid + + if nozoid == "mmo3": + for ccnumber in xrange(0,32): + midi3.MidiMsg([CONTROLLER_CHANGE+midichanMMO3-1, ccnumber, resetMMO3[ccnumber]], nozmidi) + sendWSall("/mmo3/cc/"+str(ccnumber)+" "+str(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])) + +# +# Websocket part +# + +def on_error(ws, error): + print(error) + +def on_close(ws): + print("### closed ###") + + +def on_open(ws): + + def run(*args): + + try: + + while True: + + time.sleep(1) + + except Exception: + traceback.print_exc() + + finally: + ws.close() + print("thread terminating...") + + + thread.start_new_thread(run, ()) + +def on_message(ws, message): + # + print "" + print(message) + if len(message) > 200: + message = message[:200]+'..' + + 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 + + wscommand = oscpath[0].split("/") + 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[2] == "reset": + if wscommand[1] == "ocs2": + reset("ocs2") + else: + reset("mmo3") + + + # if needed a loop back : WS Client -> server -> WS Client + # sendWSall(message) + + + +print "Running...." + +# Main loop do nothing. Maybe do the webui server ? +try: + + print "" + print "Connecting to NozoidUI server..." + print "at", serverIP, "port",wsPORT + + #websocket.enableTrace(True) + ws = websocket.WebSocketApp("ws://"+str(serverIP)+":"+str(wsPORT), + on_message = on_message, + on_error = on_error, + on_close = on_close) + + midi3.ws = ws + midi3.wsmode = True + + print "Midi Configuration..." + print "Midi Destination", nozmidi + + midi3.check() + + ws.on_open = on_open + ws.run_forever() + + +except KeyboardInterrupt: + pass + +# Gently stop on CTRL C + +print "Fin de NozoidUI." + + + + diff --git a/jamidi.json b/jamidi.json new file mode 100644 index 0000000..b733c2c --- /dev/null +++ b/jamidi.json @@ -0,0 +1,64 @@ +{ +"local" : +[ + { + "_comment": "Server is localhost", + "type": "serverconf", + "name": "local", + "IP": "127.0.0.1", + "port": 8081 + } +], + +"llstrvpn" : +[ + { + "_comment": "Server is llstrvpn", + "type": "serverconf", + "name": "llstrvpn", + "IP": "10.8.0.46", + "port": 8081 + } +], + +"xkkia" : +[ + { + "_comment": "Server is xrkia.org", + "type": "serverconf", + "name": "xrkia", + "IP": "xrkia.org", + "port": 8081 + } +], + +"ocs2": [ + { + "_comment": "OCS-2 parameters", + "type": "mididevice", + "mididevice": "UM-ONE:UM-ONE MIDI 1 20:0", + "midichan" : 2 + } +], + +"mmo3": [ + { + "_comment": "MMO-3 parameters", + "type": "mididevice", + "mididevice": "UM-ONE:UM-ONE MIDI 1 20:0", + "midichan" : 1 + } + ], + +"default": [ + { + "_comment": "Client : default midi device", + "type": "mididevice", + "mididevice": "BCR2000 Port 1", + "midichan" : 1 + } + +] + + +} \ No newline at end of file diff --git a/kick.wav b/kick.wav new file mode 100755 index 0000000..80647fa Binary files /dev/null and b/kick.wav differ diff --git a/main.py b/main.py new file mode 100644 index 0000000..25150ab --- /dev/null +++ b/main.py @@ -0,0 +1,308 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# -*- mode: Python -*- +''' +Jamidi Server v0.1b + +''' + +print("") +print("") +print("Jamidi Server") +print("v0.1b") + +#from multiprocessing import Process, Queue, TimeoutError +#import subprocess +import sys +import traceback +import os +import time + +from rtmidi.midiconstants import (CHANNEL_PRESSURE, CONTROLLER_CHANGE, NOTE_ON, NOTE_OFF, + PITCH_BEND, POLY_PRESSURE, PROGRAM_CHANGE) +import midi3 + +from websocket_server import WebsocketServer +#import socket +import types, time, json +import argparse + +debug = 1 + + +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.set_defaults(current=True) +argsparser.set_defaults(broadcast=True) + +args = argsparser.parse_args() + +# Mode +if args.servername: + servername = args.servername +else: + servername = "local" + +# Broadcast commands to all clients ? +if args.broadcast == False: + print("Broadcast disabled") + broadcast = False +else: + print("Broadcast enabled") + broadcast = True + +# Send current values to all new client ? +if args.current == False: + print("Do not send current values at startup disabled") + current = False +else: + print("Current values update at startup disabled") + current = True + + +# 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 + + +# +# Midi part +# + + +print("Midi Configuration...") +# print("Midi Destination", nozmidi) + +midi3.check() + +def GetTime(): + return time.strftime("%a, %d %b %Y %H:%M:%S", time.localtime()) + + + +# /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) + + midi3.MidiMsg([CONTROLLER_CHANGE+midichannel-1, ccnumber, value], mididest) + + +# /reset nozoids with "default" values +def reset(nozoid): + + print("") + print(GetTime(),"reseting", 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] + 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) + 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 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 + + + +# +# 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'])) + sendWSall("/players %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 will be like ['', 'ocs2', 'cc', '9'] + wscommand = oscpath[0].split("/") + if debug > 0: + print("wscommand :",wscommand) + + if len(oscpath) == 1: + args[0] = "noargs" + #print "noargs command" + + # like /ocs2/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 + + cc(Confs[wscommand[1]][0]["midichan"], ccvr, ccvl, Confs[wscommand[1]][0]["mididevice"]) + + # like /ocs2/reset 1 + elif wscommand[2] == "reset": + if wscommand[1] == "ocs2": + reset("ocs2") + else: + reset("mmo3") + + #print "" + + # if needed a loop back : WS Client -> server -> WS Client + sendWSall(message) + +''' +def handle_timeout(self): + self.timed_out = True +''' + +def sendWSall(message): + + if broadcast == True: + if debug >0: + print(GetTime(),"sending to all %s" % (message)) + + wserver.send_message_to_all(message) + + +LoadConfs() + +serverIP = Confs[servername][0]["IP"] +wsPORT = Confs[servername][0]["port"] + +print("Running....") + +# Main loop do nothing. Maybe do the webui server ? +try: + #while True: + + # Websocket startup + wserver = WebsocketServer(wsPORT,host=serverIP) + + #print wserver + 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) + + if reset == True: + reset("mmo3") + reset("ocs2") + + + #print "" + print(GetTime(),"WS server running forever...") + + wserver.run_forever() + + +except KeyboardInterrupt: + pass + +# Gently stop on CTRL C + + +print("Fin de Jamidi.") + + + + diff --git a/midi3.py b/midi3.py new file mode 100644 index 0000000..7fc77f0 --- /dev/null +++ b/midi3.py @@ -0,0 +1,488 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- + +""" +Midi3 light version for soundt/Jamidi/clapt +v0.7.0 + +Midi Handler : + +- Hook to the MIDI host +- Enumerate connected midi devices and spawn a process/device to handle incoming events + +by Sam Neurohack +from /team/laser + + +""" + + +import time +from threading import Thread + +import rtmidi +from rtmidi.midiutil import open_midiinput +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 +import os + + +is_py2 = sys.version[0] == '2' +if is_py2: + from queue import Queue + from OSC import OSCServer, OSCClient, OSCMessage +else: + from queue import Queue + from OSC3 import OSCServer, OSCClient, OSCMessage + + +print("") + +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 = [] + +debug = 0 + +# False = server / True = Client +wsmode = False + +#Mser = False + +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 + Exception = Exception + + +STATUS_MAP = { + 'noteon': NOTE_ON, + 'noteoff': NOTE_OFF, + 'programchange': PROGRAM_CHANGE, + 'controllerchange': CONTROLLER_CHANGE, + 'pitchbend': PITCH_BEND, + 'polypressure': POLY_PRESSURE, + 'channelpressure': CHANNEL_PRESSURE +} + + + +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)) + + +# +# MIDI Startup and handling +# + +mqueue = Queue() +inqueue = Queue() + +# +# Events from Generic MIDI Handling +# + +def MidinProcess(inqueue, portname): + + inqueue_get = inqueue.get + + while True: + time.sleep(0.001) + msg = inqueue_get() + print("") + print("Generic from", portname,"msg : ", msg) + + # Note On + if msg[0]==NOTE_ON: + + MidiChannel = msg[0]-144 + MidiNote = msg[1] + MidiVel = msg[2] + print("NOTE ON :", MidiNote, 'velocity :', MidiVel, "Channel", MidiChannel) + + + if MidiNote < 63 and MidiVel >0: + + if platform == 'darwin': + os.system("afplay snare.wav") + else: + os.system("aplay snare.wav") + + + if MidiNote > 62 and MidiVel >0: + + if platform == 'darwin': + os.system("afplay kick.wav") + else: + os.system("aplay kick.wav") + + + + # Note Off + if msg[0]==NOTE_OFF: + print("NOTE OFF :", MidiNote, 'velocity :', MidiVel, "Channel", MidiChannel) + + + # MMO-3 Midi CC message CHANNEL 1 + if msg[0] == CONTROLLER_CHANGE: + 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") + if wsmode == True: + ws.send("/mmo3/cc/"+str(msg[1])+" "+str(msg[2])) + + # OCS-2 Midi CC message CHANNEL 2 + if msg[0] == CONTROLLER_CHANGE+1: + 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") + if wsmode == True: + ws.send("/ocs2/cc/"+str(msg[1])+" "+str(msg[2])) + + + # other midi message + if msg[0] != NOTE_OFF and msg[0] != NOTE_ON and msg[0] != CONTROLLER_CHANGE: + pass + + + + +# 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)) + message.append(deltatime) + 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("Adding OutDevice name", 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 ) + #print "New OutDevice [%i] %s" % (port, name)) + + 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) + + @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...") + # client mode + if wsmode == True: + print("ws object", ws) + 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()): + + + outport = FindOutDevice(name) + midinputsname[port]=name + + #print "name",name, "Port",port, "Outport", outport) + # print "midinames", midiname) + + #ListInDevice() + + 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 "Thread launched for midi port", port, "portname", port_name, "Inname", midiname.index(port_name) + #print "counter", InObject.counter + #midinputs[port].set_callback(AddQueue(name),midinputsqueue[port]) + #midinputs[port].set_callback(AddQueue(name)) + #genericnumber += 1 + InDevice[InObject.counter-1].rtmidi.set_callback(AddQueue(name,port)) + + except Exception: + traceback.print_exc() + + #print "") + print(InObject.counter, "In devices") + #ListInDevice() + + +def ListInDevice(): + + #print "known IN devices :" + for item in InObject.getinstances(): + + print(item.name) + print("") + +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") + + + +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 + +# mididest : all or specifiname, won't be sent to launchpad or Bhoreal. +def MidiMsg(midimsg, mididest): + + + desterror = -1 + + print("midi3 got midimsg", midimsg, "for", mididest) + + for port in range(len(OutDevice)): + # To mididest + if midiname[port].find(mididest) != -1: + if debug>0: + print("midi 3 sending to name", midiname[port], "port", port, ":", midimsg) + midiport[port].send_message(midimsg) + desterror = 0 + + if desterror == -1: + print("mididest",mididest, ": ** This midi destination doesn't exists **") + + # send midi msg over ws. + #if wsmode == True: + # ws.send("/ocs2/cc/1 2") + + + +def NoteOn(note, velocity, mididest): + global MidInsNumber + + + for port in range(MidInsNumber): + + # To mididest + if midiname[port].find(mididest) == 0: + midiport[port].send_message([NOTE_ON, note, velocity]) + + + +def listdevice(number): + + return midiname[number] + +def check(): + + OutConfig() + InConfig() + + + diff --git a/nozWS.py b/nozWS.py new file mode 100755 index 0000000..132cac2 --- /dev/null +++ b/nozWS.py @@ -0,0 +1,451 @@ +#!/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 +''' + +print("") +print("") +print("NozoidUI Client") +print("v0.0.1") + +from multiprocessing import Process, Queue, TimeoutError + +import subprocess +import sys +import traceback +import os +import time + +from rtmidi.midiconstants import (CHANNEL_PRESSURE, CONTROLLER_CHANGE, NOTE_ON, NOTE_OFF, + PITCH_BEND, POLY_PRESSURE, PROGRAM_CHANGE) +import midi3 + +from websocket_server import WebsocketServer + +#import socket +import types, time +import websocket + +import socket +import struct +import argparse + +argsparser = argparse.ArgumentParser(description="Nozoid : Transform musical parameters in XXX visualisation") +argsparser.add_argument("-i","--iport",help="remserial IN port (9090 by default)",type=int) +argsparser.add_argument("-o","--oport",help="remserial OUT port (9091 by default)",type=int) +argsparser.add_argument("-t","--timeout",help="timeout connection (0.002 s by default)",type=float) +args = argsparser.parse_args() + +nozIP='127.0.0.1' +portrcv = 9090 +portwrt = 9091 +timeout=0.002 + +if args.iport: + portrcv=args.iport +if args.oport: + portwrt=args.oport +if args.timeout: + timeout=args.timeout + + +try: + import _thread +except ImportError: + import _thread as thread +import time + +debug = 0 + +# +# webUI server +# + +serverIP = "xrkia.org" +# serverIP = "127.0.0.1" +# serverIP = "10.8.0.46" +wsPORT = 8081 + + +# +# Midi part +# + +# nozmidi = "BCR2000 Port 1" +# nozmidi = "Arturia BeatStep" +# nozmidi = "Virtual Midi A" +# nozmidi = "Virtual Sequencer" +# nowmidi = "IAC Driver Sequencer Bus 1" +nozmidi = "UM-ONE:UM-ONE MIDI 1 20:0" + +midichanOCS2 = 2 +midichanMMO3 = 1 + +# resetMMO3 = [64,64,0,32,96] # un truc comme ca pour les valeurs de reset ? +resetMMO3 = [0] * 32 +resetOCS2 = [0] * 32 + +# /cc cc number value +def cc(midichannel, ccnumber, value, mididest): + + print("NozoidUI Sending Midi channel", midichannel, "cc", ccnumber, "value", value, "to", mididest) + #if mididest == "BCR2000 Port 1": + + midi3.MidiMsg([CONTROLLER_CHANGE+midichannel-1, ccnumber, value], mididest) + + +# /reset nozoids with "default" values +def reset(nozoid): + + print("") + print("reseting", nozoid) + + if nozoid == "mmo3": + for ccnumber in range(0,32): + midi3.MidiMsg([CONTROLLER_CHANGE+midichanMMO3-1, ccnumber, resetMMO3[ccnumber]], nozmidi) + sendWSall("/mmo3/cc/"+str(ccnumber)+" "+str(resetMMO3[ccnumber])) + else: + for ccnumber in range(0,32): + midi3.MidiMsg([CONTROLLER_CHANGE+midichanOCS2-1, ccnumber, resetOCS2[ccnumber]], nozmidi) + sendWSall("/ocs2/cc/"+str(ccnumber)+" "+str(resetMMO3[ccnumber])) + +# +# Websocket part +# + +def on_error(ws, error): + print(error) + +def on_close(ws): + print("### closed ###") + + +def on_open(ws): + + def run(*args): + + try: + + NozStream() + #while True: + + # time.sleep(1) + + except Exception: + traceback.print_exc() + + finally: + ws.close() + print("thread terminating...") + + + _thread.start_new_thread(run, ()) + +def on_message(ws, message): + # + print("") + print(message) + if len(message) > 200: + message = message[:200]+'..' + + 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) + + wscommand = oscpath[0].split("/") + 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[2] == "reset": + if wscommand[1] == "ocs2": + reset("ocs2") + else: + reset("mmo3") + + + # if needed a loop back : WS Client -> server -> WS Client + # sendWSall(message) + +################## +#BEGIN NOZOID PART +################## + +#backup each oscillator value +osc = [0] * 512 +oscnumber = 0 +Knob = [255] * 255 + +TimeoutCompt=0 +LastNozMsg=struct.pack('>BBBB',255,255,0,0) + +def knob2cc(s): + a1, a2 = 0,65535 + b1, b2 = 0, 127 + return b1 + ((s - a1) * (b2 - b1) / (a2 - a1)) + +def lhex(h): + return ':'.join(x.encode('hex') for x in h) + +def Write(sockwrt,stuff): + # Send data + print('sending',stuff) + my_bytes = bytearray() + for count in range(len(stuff)): + my_bytes.append(stuff[count]) + sockwrt.sendall(my_bytes) + + +def Read4(): + #print "**SockRcv %s"%SockRcv + return Read(SockRcv,4) + +# Read a given number of bytes +def Read(sockrcv,length): + + # Look for the response + amount_received_t = 0 + data = "" + + data = sockrcv.recv(length) + amount_received_t = len(data) + + while amount_received_t < length: + amount_expected = length-amount_received_t + #print "**data:",lhex(data), + #print "**still needing %d byte(s) to read"%amount_expected + data_received = sockrcv.recv(amount_expected) + amount_received = len(data_received) + + data += data_received + amount_received_t = len(data) + + if ord(data[0])==0xFF: + return data + else: + while ord(data[0])!=0xFF: + #print "*datax0",lhex(data) + data=data[1:] + data_received = sockrcv.recv(1) + data+=data_received + #data+=struct.pack('>B',255) + #print "*datax1",lhex(data) + #raw_input("Hit Enter To Continue...") + print("**Shiftin'!!!",lhex(data)) + #raw_input("Hit Enter To Continue...") + return data + #return struct.pack('>BBBB',255,255,0,0) + +# Nozoids Messages decoding +def Msg(NozMsg,nozport): + + global osc + global oscnumber + + if ord(NozMsg[1]) < 160: + (val,) = struct.unpack_from('>H', NozMsg, 2) + knob=ord(NozMsg[1])+(nozport*127) + #ccknobv= cc knob value + #if knob == 30 or knob == 31: + if knob >= 30: + #return (knob, val) + #return (knob, 0) + return (knob, 0-32767) + ccknobv=knob2cc(val) + if Knob[knob] != ccknobv: + if Knob[knob] != 255: + print("knob %d has changed from %d to %d value"%(knob,Knob[knob],ccknobv)) + #raw_input("Hit Enter To Continue...") + Knob[knob]=ccknobv + #return (knob, val) + return (knob, val-32767) + + if (ord(NozMsg[1]) >= 0xA0 and ord(NozMsg[1]) < 0xF0) or (ord(NozMsg[1]) >= 0xF6 and ord(NozMsg[1]) <= 0xF8): + OrdNozMsg=ord(NozMsg[1]) + + (val,) = struct.unpack_from('>h', NozMsg, 2) + oscnumber = ord(NozMsg[1])-0x9F+(nozport*127) + oscvalue = val + osc[oscnumber]=oscvalue + return (oscnumber, oscvalue) + + # type of nozoid + if ord(NozMsg[1]) == 0xF0: + #if NozMsg[1] == 0xF0: + print("Nozoid type sent:%s"%NozMsg[-2:]) + #raw_input("Hit Enter To Continue") + noztype[nozport] = NozMsg[-2:] + #print "noztype:%s"%noztype + return (0xF0, NozMsg[-2:]) + + if ord(NozMsg[1]) >= 0xF3 and ord(NozMsg[1]) <= 0xF5: + #if NozMsg[1] >= 0xF3 and NozMsg[1] <= 0xF5:#an osc value that belong to the [0,65535] interval (CV values ? see Arduino's code of MMO-3 and OSC-2 firmware) + (val,) = struct.unpack_from('>H', NozMsg, 2) + oscnumber=ord(NozMsg[1])-0x9F+(nozport*127) + oscvalue=val-32767 + osc[oscnumber]=oscvalue + return (oscnumber,oscvalue) + + if ord(NozMsg[1]) == 0xFA: #a note/keyboard has been sent + (val,) = struct.unpack_from('>H', NozMsg, 2) + #print("set notekb to", str(val)) + return (0xFA, val) + + if ord(NozMsg[1]) == 0xFF: #an error somewhere ? + (val,) = struct.unpack_from('>H', NozMsg, 2) + return (0xFF, val) + +def NozStream(): + global osc + global oscnumber + global TimeoutCompt + + if SockWrt: + Write(SockWrt,[0xFF]) + #Write(SockWrt,[0xF6])#VCF + #Write(SockWrt,[0xA0])#VCO1 + #Write(SockWrt,[0xA1])#VCO2 + Write(SockWrt,[0xA3])#LFO2 + #SockWrt.close() + + while True: + + try: + NozMsg = Read4()#here we read ! + #print "**NozMsg:%s"%NozMsg + LastNozMsg = NozMsg#backing up last good msg + #print "NozMsg:",lhex(NozMsg) + + MsgReturn = Msg(NozMsg,0)#Go decoding Msg and assigning global vars... + print("MsgReturn:",MsgReturn) + + #y=osc[oscnumber] + + ws.send("/ocs2/OSC"+str(oscnumber)+" "+str(osc[oscnumber])) + + + except socket.timeout: + TimeoutCompt+=1 + print('**caught a timeout(%d)'%TimeoutCompt) + time.sleep(timeout) + NozMsg = struct.pack('>BBBB',255,255,0,0)#0xFFFF0000 (ie error) + #print "NozMsg:",lhex(NozMsg) + + +# Nozoid reader via remserial +# ex : remserial -p 9090 -s "2000000 raw" /dev/ttyACM0 +# +print("Nozoid Reader connecting...") +try: +# Create a TCP/IP socket + SockRcv = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + +# Connect the socket to the port where the server is listening + serverrcv_address = (nozIP, portrcv) + + SockRcv.connect(serverrcv_address) + + SockRcv.settimeout(timeout)#timeout 0.002s + + print('connecting to %s portrcv %s' % serverrcv_address, file=sys.stderr) +except: + print("** huh! did you launch remserial on %s:%d ?"%(nozIP,portrcv)) + SockRcv.close() + SockRcv=None + input("Hit Enter To Continue...") + +# +# Nozoid write via remserial +# ex : remserial -p 9091 -w -s "2000000 raw" /dev/ttyACM0 +# +print("Nozoid Writer connecting...") +try: +# Create a TCP/IP socket + SockWrt = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + serverwrt_address = (nozIP, portwrt) + +# Connect the socket to the port where the server is listening + SockWrt.connect(serverwrt_address) + print('connecting to %s portwrt %s' % serverwrt_address, file=sys.stderr) +except: + print("** huh! did you launch remserial on %s:%d ?"%(nozIP,portwrt)) + SockWrt.close() + SockWrt=None + input("Hit Enter To Continue...") + + +################### +#END OF NOZOID PART +################### + +print("Running....") + +def GetTime(): + return time.strftime("%a, %d %b %Y %H:%M:%S", time.localtime()) + +def sendWSall(message): + if debug >0: + print(GetTime(),"sending to all %s" % (message)) + + ws.send_message_to_all(message) + +# Main loop do nothing. Maybe do the webui server ? +try: + + print("") + print("Connecting to NozoidUI server...") + print("at", serverIP, "port",wsPORT) + + #websocket.enableTrace(True) + ws = websocket.WebSocketApp("ws://"+str(serverIP)+":"+str(wsPORT), + on_message = on_message, + on_error = on_error, + on_close = on_close) + + midi3.ws = ws + midi3.wsmode = True + + print("Midi Configuration...") + print("Midi Destination", nozmidi) + + midi3.check() + + ws.on_open = on_open + ws.run_forever() + + #print "That Point Is Never Reach ?" + if SockWrt: + Write(SockWrt,[0xFF]) + print("closing nozoid connections!") + SockWrt.shutdown(socket.SHUT_RDWR) + SockWrt.close() + SockRcv.shutdown(socket.SHUT_RDWR) + SockRcv.close() + +except KeyboardInterrupt: + pass + +# Gently stop on CTRL C + +print("Fin de NozoidUI.") diff --git a/rules.json b/rules.json new file mode 100644 index 0000000..06bb259 --- /dev/null +++ b/rules.json @@ -0,0 +1,104 @@ +{ + + +"cc2cc": [ + { + "_comment": "route incoming ws CC to another local CC/midi device", + "name": "ocs2 lfo1 fq (= chan 2 cc 13) to bcr chan 2 cc 10", + "songname": "all", + "chanIN" : 2, + "ccs" : 13, + "chanOUT" : 2, + "ccOUT": 1, + "valuetype" : "linear", + "mididest" : "BCR2000 Port 1" + }, + { + "name": "bass2", + "songname": "song2", + "chanIN" : 1, + "ccs" : "all", + "chanOUT" : 1, + "ccOUT": 10, + "valuetype" : "curved", + "mididest" : "Virtual Midi A" + } +], + +"Specials": [ + { + "_comment": "Pas implementé encore dans nozoidUI", + "name": "autotempo", + "songname": "song1", + "chanIN" : 16, + "notes" : 127, + "notetype" : "on", + "code": "autotempo", + "valuetype" : 105, + "laser" : 0 + }, + { + "name": "resetCCON", + "songname": "all", + "chanIN" : 16, + "notes" : 126, + "notetype" : "on", + "code": "resetCCON", + "valuetype" : 105, + "laser" : 0 + }, + { + "name": "resetCCOFF", + "songname": "all", + "chanIN" : 16, + "notes" : 126, + "notetype" : "off", + "code": "resetCCOFF", + "valuetype" : 105, + "laser" : 0 + }, + { + "name": "BangON", + "songname": "all", + "chanIN" : 16, + "notes" : 125, + "notetype" : "on", + "code": "bangON", + "valuetype" : 105, + "laser" : 0 + }, + { + "name": "BangOFF", + "songname": "all", + "chanIN" : 16, + "notes" : 125, + "notetype" : "off", + "code": "bangOFF", + "valuetype" : 105, + "laser" : 0 + }, + { + "name": "StrobeON", + "songname": "all", + "chanIN" : 16, + "notes" : 124, + "notetype" : "on", + "code": "strobeON", + "valuetype" : 105, + "laser" : 0 + }, + { + "name": "StrobeOFF", + "songname": "all", + "chanIN" : 16, + "notes" : 124, + "notetype" : "off", + "code": "strobeOFF", + "valuetype" : 105, + "laser" : 0 + } +] + + + +} \ No newline at end of file diff --git a/snare.wav b/snare.wav new file mode 100755 index 0000000..c6a1142 Binary files /dev/null and b/snare.wav differ diff --git a/web/.DS_Store b/web/.DS_Store new file mode 100644 index 0000000..9b9704a Binary files /dev/null and b/web/.DS_Store differ diff --git a/web/css/style.css b/web/css/style.css new file mode 100755 index 0000000..613dcde --- /dev/null +++ b/web/css/style.css @@ -0,0 +1 @@ +* { box-sizing: border-box; } body {margin: 0;}.row{display:table;padding:10px;width:100%;}.cell{width:8%;display:table-cell;height:75px;}body, html{background-color:#1d1c25;margin:0;padding:0;}.range{-webkit-appearance:none;-moz-appearance:none;position:absolute;left:50%;top:50%;width:200px;margin-top:10px;transform:translate(-50%, -50%);}input[type=range]::-webkit-slider-runnable-track{-webkit-appearance:none;background:linear-gradient(45deg, rgba(59,173,227,1) 0%, rgba(87,111,230,1) 25%, rgba(152,68,183,1) 51%, rgba(255,53,127,1) 100%);filter:progid:DXImageTransform.Microsoft.gradient( startColorstr='#3bade3 ', endColorstr='#ff357f ', GradientType=1 );height:2px;}input[type=range]:focus{outline:none;}input[type=range]::-moz-range-track{-moz-appearance:none;background:linear-gradient(45deg, rgba(59,173,227,1) 0%, rgba(87,111,230,1) 25%, rgba(152,68,183,1) 51%, rgba(255,53,127,1) 100%);filter:progid:DXImageTransform.Microsoft.gradient( startColorstr='#3bade3 ', endColorstr='#ff357f ', GradientType=1 );height:2px;}input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;border:2px solid;border-radius:50%;height:25px;width:25px;max-width:80px;position:relative;bottom:11px;background-color:#1d1c25;cursor:-webkit-grab;-webkit-transition:border 1000ms ease;transition:border 1000ms ease;}input[type=range]::-moz-range-thumb{-moz-appearance:none;border:2px solid;border-radius:50%;height:25px;width:25px;max-width:80px;position:relative;bottom:11px;background-color:#1d1c25;cursor:-moz-grab;-moz-transition:border 1000ms ease;transition:border 1000ms ease;}.range.blue::-webkit-slider-thumb{border-color:rgb(59,173,227);}.range.ltpurple::-webkit-slider-thumb{border-color:rgb(87,111,230);}.range.purple::-webkit-slider-thumb{border-color:rgb(152,68,183);}.range.pink::-webkit-slider-thumb{border-color:rgb(255,53,127);}.range.blue::-moz-range-thumb{border-color:rgb(59,173,227);}.range.ltpurple::-moz-range-thumb{border-color:rgb(87,111,230);}.range.purple::-moz-range-thumb{border-color:rgb(152,68,183);}.range.pink::-moz-range-thumb{border-color:rgb(255,53,127);}input[type=range]::-webkit-slider-thumb:active{cursor:-webkit-grabbing;}input[type=range]::-moz-range-thumb:active{cursor:-moz-grabbing;}.range.blue{position:relative;background-color:rgba(31,27,27,0.09);width:75%;top:37.5px;}*{box-sizing:border-box;}body{margin:0;}#iy3nk{height:68px;}#il4ah{text-align:center;color:#ef4079;}#i7ox{font-family:Verdana, Geneva, sans-serif;color:#40acef;text-align:left;}#io5q{color:#40acef;}#iy2eh{text-align:center;}#i8a73{text-align:center;color:#4540ef;}#i3fzo{text-align:center;color:#7740ef;}#ikbjg{text-align:center;color:#b140ef;}@media (max-width: 768px){.cell{width:100%;display:block;}} \ No newline at end of file diff --git a/web/css/style2.css b/web/css/style2.css new file mode 100755 index 0000000..115c94b --- /dev/null +++ b/web/css/style2.css @@ -0,0 +1 @@ +input[type=range]::-webkit-slider-runnable-track{-webkit-appearance:none;background:linear-gradient(45deg, rgba(59,173,227,1) 0%, rgba(87,111,230,1) 25%, rgba(152,68,183,1) 51%, rgba(255,53,127,1) 100%);filter:progid:DXImageTransform.Microsoft.gradient( startColorstr='#3bade3 ', endColorstr='#ff357f ', GradientType=1 );height:2px;}input[type=range]:focus{outline:none;}input[type=range]::-moz-range-track{-moz-appearance:none;background:linear-gradient(45deg, rgba(59,173,227,1) 0%, rgba(87,111,230,1) 25%, rgba(152,68,183,1) 51%, rgba(255,53,127,1) 100%);filter:progid:DXImageTransform.Microsoft.gradient( startColorstr='#3bade3 ', endColorstr='#ff357f ', GradientType=1 );height:2px;}input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;border:2px solid;border-radius:50%;height:25px;width:25px;max-width:80px;position:relative;bottom:11px;background-color:#1d1c25;cursor:-webkit-grab;-webkit-transition:border 1000ms ease;transition:border 1000ms ease;}input[type=range]::-moz-range-thumb{-moz-appearance:none;border:2px solid;border-radius:50%;height:25px;width:25px;max-width:80px;position:relative;bottom:11px;background-color:#1d1c25;cursor:-moz-grab;-moz-transition:border 1000ms ease;transition:border 1000ms ease;}.range.blue::-webkit-slider-thumb{border-color:rgb(59,173,227);}.range.ltpurple::-webkit-slider-thumb{border-color:rgb(87,111,230);}.range.purple::-webkit-slider-thumb{border-color:rgb(152,68,183);}.range.pink::-webkit-slider-thumb{border-color:rgb(255,53,127);}.range.blue::-moz-range-thumb{border-color:rgb(59,173,227);}.range.ltpurple::-moz-range-thumb{border-color:rgb(87,111,230);}.range.purple::-moz-range-thumb{border-color:rgb(152,68,183);}.range.pink::-moz-range-thumb{border-color:rgb(255,53,127);}input[type=range]::-webkit-slider-thumb:active{cursor:-webkit-grabbing;}input[type=range]::-moz-range-thumb:active{cursor:-moz-grabbing;}.range.blue{position:relative;background-color:rgba(31,27,27,0.09);width:75%;top:37.5px;}body{margin:0;}#iy3nk{font-family: "Lucida Grande", Verdana, Arial, sans-serif; font-size: 10ex; height:68px;}#il4ah{text-align:center;color:#ef4079;}#i7ox{font-family:Verdana, Geneva, sans-serif;color:#40acef;text-align:left;}#io5q{color:#40acef;}#iy2eh{text-align:center;}#i8a73{text-align:center;color:#4540ef;}#i3fzo{text-align:center;color:#7740ef;}#ikbjg{text-align:center;color:#b140ef;}@media (max-width: 768px){} \ No newline at end of file diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..2a7f3d7 --- /dev/null +++ b/web/index.html @@ -0,0 +1,74 @@ + + +
+