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 @@ + + + + Nozoids online + + + + + + + + +
+ 1 / Live sound : VLC network stream http://xrkia.org:8000/NZ + +
+
2 / Choose to play a MMO-3 or OCS-2 ? + +
+ + + + +
+ +
+ + + + + +
+ + +
+ + + + + diff --git a/web/indexaurora.html b/web/indexaurora.html new file mode 100644 index 0000000..4c59b42 --- /dev/null +++ b/web/indexaurora.html @@ -0,0 +1,288 @@ + + + + Aurora + + + + + + + + + + + + + +
+ + +
+
+

Aurora

+
+ +
not available + +
+ + +
+
+
+
+
+
2 curves mixer
+
+
+
+
+
Scale
+
+
+
+
+
Copy
+
+
+ + + + + + + + + + +
+ +
+ + + diff --git a/web/knobs/.DS_Store b/web/knobs/.DS_Store new file mode 100644 index 0000000..1618040 Binary files /dev/null and b/web/knobs/.DS_Store differ diff --git a/web/knobs/rebuild.png b/web/knobs/rebuild.png new file mode 100644 index 0000000..60a5bf1 Binary files /dev/null and b/web/knobs/rebuild.png differ diff --git a/web/knobs/simplegray.png b/web/knobs/simplegray.png new file mode 100755 index 0000000..9c57010 Binary files /dev/null and b/web/knobs/simplegray.png differ diff --git a/web/knobs/switch_toggle.png b/web/knobs/switch_toggle.png new file mode 100755 index 0000000..c8c5c65 Binary files /dev/null and b/web/knobs/switch_toggle.png differ diff --git a/web/mmo3.html b/web/mmo3.html new file mode 100644 index 0000000..bece34b --- /dev/null +++ b/web/mmo3.html @@ -0,0 +1,506 @@ + + + + MMO-3 + + + + + + + + + + + + + + + + +
not available + +
+ + + + +
+ + +
+
+ OSC 1 +
+
+
+
+
FQ -0
+
+
+
+
+
Mod 1 -1
+
+
+
+
+
Mod 2 -2
+
+
+
+
+
Mod 3 -3
+
+
+ + + +
+
+ OCS 2 +
+
+
+
+
FQ -5
+
+
+
+
+
Mod 1 -6
+
+
+
+
+
Mod 2 -7
+
+
+
+
+
Mod 3 -8
+
+
+ + + +
+
+ OSC 3 +
+
+
+
+
FQ -9
+
+
+
+
+
Mod 1 -10
+
+
+
+
+
Mod 2 -11
+
+
+
+
+
Mod 3 -12
+
+
+ +
+
+ LFO 1 +
+
+
+
+
FQ -13
+
+
+
+
+
WF -14
+
+
+
+
+
SYM -15
+
+
+
+
+
ATTACK -16
+
+
+ + +
+
+ LFO 2 +
+
+
+
+
FQ 1 -17
+
+
+
+
+
FQ 2 -18
+
+
+
+
+
MOD -19
+
+
+
+
+
DECAY -20
+
+
+ + +
+
+ LFO 3 +
+
+
+
+
FQ -21
+
+
+
+
+
Param 1 -22
+
+
+
+
+
Param 2 -23
+
+
+
+
+
SUSTAIN -24
+
+
+ + +
+
+ MIX +
+
+
+
+
OSC 1 -25
+
+
+
+
+
OSC 2 -26
+
+
+
+
+
OSC 3 -27
+
+
+
+
+
RELEASE -28
+
+
+ + +
+
+ AUDIO +
+
+
+
+
VOL -29
+
+
+ +
+
+ +
+
+ +
+
+ + + +
+
+ +
+ + + + + + + + + + + + + +
+ OCS-2 + +
+ + + diff --git a/web/ocs2.html b/web/ocs2.html new file mode 100644 index 0000000..e2b82b0 --- /dev/null +++ b/web/ocs2.html @@ -0,0 +1,515 @@ + + + + OCS-2 + + + + + + + + + + + + + + + + +
not available + +
+ + + + +
+ + +
+
+ VCO 1 +
+
+
+
+
FQ -0
+
+
+
+
+
WF -1
+
+
+
+
+
Mod 1 -2
+
+
+
+
+
Mod 2 -3
+
+
+ + + +
+
+ VCO 2 +
+
+
+
+
FQ -5
+
+
+
+
+
WF -6
+
+
+
+
+
Mod 1 -7
+
+
+
+
+
Mod 2 -8
+
+
+ + + +
+
+ VCF +
+
+
+
+
FQ -9
+
+
+
+
+
Q -10
+
+
+
+
+
Mod 1 -11
+
+
+
+
+
Mod 2 -12
+
+
+ +
+
+ LFO 1 +
+
+
+
+
FQ -13
+
+
+
+
+
WF -14
+
+
+
+
+
SYM -15
+
+
+
+
+
ATTACK -16
+
+
+ + +
+
+ LFO 2 +
+
+
+
+
FQ -17
+
+
+
+
+
WF -18
+
+
+
+
+
SYM -19
+
+
+
+
+
DECAY -20
+
+
+ + +
+
+ LFO 3 +
+
+
+
+
FQ -21
+
+
+
+
+
MOD -22
+
+
+
+
+
WET EFFECT -23
+
+
+
+
+
SUSTAIN -24
+
+
+ + +
+
+ CV GEN +
+
+
+
+
PARAM 1 -25
+
+
+
+
+
PARAM 2 -26
+
+
+
+
+
MOD EFFECT -27
+
+
+
+
+
RELEASE -28
+
+
+ + +
+
+ VCA +
+
+
+
+
MIX 1/2 -29
+
+
+
+
+
MOD -30
+
+
+
+
+
VOL -31
+
+
+ +
+
+ + + +
+ + + + + + + + + + + + + + + + + +
+ MMO-3 + +
+ + + diff --git a/web/selector.min.css b/web/selector.min.css new file mode 100755 index 0000000..061a532 --- /dev/null +++ b/web/selector.min.css @@ -0,0 +1 @@ +div.selector-element{font-size: 0.75em; display:inline-block;position:relative;width:100%;max-width: 85px;height: 24px;font-family:sans-serif;color: #fbfff5;background-color: #111111}div.selector-element div.selector-selected{display:inline-block;width:100%;height:100%;position:relative;cursor:pointer;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;line-height:30px;padding:0 12px;transition:.15s ease-in-out;border:1px solid gray}div.selector-element div.selector-selected:hover{background-color: #060606}div.selector-element div.selector-selected:after{content:'';position:absolute;top:50%;right:22px;transform:translateY(-50%);height:0;width:0;border-top:5px solid silver;border-left:5px solid transparent;border-right:5px solid transparent}div.selector-element div.selector-selected p.selected-text{display:inline-block;margin:0;width:100%;height:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding-right:30px;text-overflow:ellipsis;overflow:hidden;white-space:nowrap}div.selector-element div.selector-options{color: #dbdbdb; display:none;position:absolute;background-color: #111111;top:100%;width:100%;height:auto;max-height:156px;overflow:auto;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;border:1px solid grey;border-top:0;box-shadow:0 2px 3px rgba(0,0,0,0.2);z-index:1;transition:.2s ease-in-out}div.selector-element div.selector-options.options-search{max-height:220px}div.selector-element.open div.selector-options{display:block}div.selector-element div.selector-options div.selector-option{display:block;position:relative;width:100%;height:32px;border-bottom:1px solid silver;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:0 12px;line-height:31px;cursor:pointer;overflow:hidden;transition:.15s ease-in-out}div.selector-element div.selector-options div.selector-option.hide{height:0;border-bottom:0;transition:.1s ease-in-out}div.selector-element div.selector-options div.selector-option.option-selected{background-color: #111111}div.selector-element div.selector-options div.selector-option:hover{color: #fbfff5; background-color: #111111}div.selector-element div.selector-options div.selector-option:last-of-type{border-bottom:0}div.selector-element div.selector-options div.selector-option.option-disabled{opacity:.6}div.selector-element div.selector-options div.option-search{display:block;height:32px;line-height:33px;position:relative;width:100%;border-bottom:1px solid grey}div.selector-element div.selector-options div.option-search span{position:absolute;top:50%;transform:translateY(-48%) !important;height:14px;width:14px;cursor:pointer;z-index:1;background:url("../knobs/close.svg") center no-repeat;background-size:11px;right:12px}div.selector-element div.selector-options div.option-search input{display:inline-block;width:100%;height:100%;padding:0 36px 0 36px;font-family:sans-serif;font-size:1em;color: white;outline:0;border:0;background:url("../knobs/search.svg") left 13px center no-repeat;background-size:13px} \ No newline at end of file diff --git a/web/selector.min.js b/web/selector.min.js new file mode 100755 index 0000000..263e77d --- /dev/null +++ b/web/selector.min.js @@ -0,0 +1 @@ +function Selector(a){this.elements={};this.currentElement={};this.currentInstance={};this.config={selector:"select.selector-instance"};this.parameters=a;this.applyParameters();this.core()}Selector.prototype.applyParameters=function(){let allowedParameters=["selector"];for(let i in this.parameters){if(allowedParameters.indexOf(i)===-1){continue}this.config[i]=this.parameters[i]}};Selector.prototype.core=function(){this.getElements();for(let i=0;iMIDI Learn +
  • Learn
  • +
  • Clear
  • +
  • Close
  • +`; + let opt={ + useMidi:0, + midilearn:0, + mididump:0, + outline:0, + knobSrc:null, + knobSprites:0, + knobWidth:0, + knobHeight:0, + knobDiameter:64, + knobColors:"#e00;#000;#000", + sliderSrc:null, + sliderKnobsrc:null, + sliderWidth:0, + sliderHeight:0, + sliderKnobwidth:0, + sliderKnobheight:0, + sliderDitchlength:0, + sliderColors:"#e00;#000;#fcc", + switchWidth:0, + switchHeight:0, + switchDiameter:24, + switchColors:"#e00;#000;#fcc", + paramWidth:32, + paramHeight:16, + paramColors:"#fff;#000", + xypadColors:"#e00;#000;#fcc", + }; + if(window.WebAudioControlsOptions) + Object.assign(opt,window.WebAudioControlsOptions); + class WebAudioControlsWidget extends HTMLElement{ + constructor(){ + super(); + this.addEventListener("keydown",this.keydown); + this.addEventListener("mousedown",this.pointerdown,{passive:false}); + this.addEventListener("touchstart",this.pointerdown,{passive:false}); + this.addEventListener("wheel",this.wheel); + this.addEventListener("mouseover",this.pointerover); + this.addEventListener("mouseout",this.pointerout); + this.addEventListener("contextmenu",this.contextMenu); + this.hover=this.drag=0; + document.body.appendChild(midimenu); + this.basestyle=` +.webaudioctrl-tooltip{ + display:inline-block; + position:absolute; + margin:0 -1000px; + z-index: 999; + background:#eee; + color:#000; + border:1px solid #666; + border-radius:4px; + padding:5px 10px; + text-align:center; + left:0; top:0; + font-size:11px; + opacity:0; + visibility:hidden; +} +.webaudioctrl-tooltip:before{ + content: ""; + position: absolute; + top: 100%; + left: 50%; + margin-left: -8px; + border: 8px solid transparent; + border-top: 8px solid #666; +} +.webaudioctrl-tooltip:after{ + content: ""; + position: absolute; + top: 100%; + left: 50%; + margin-left: -6px; + border: 6px solid transparent; + border-top: 6px solid #eee; +} +`; + } + sendEvent(ev){ + let event; + event=document.createEvent("HTMLEvents"); + event.initEvent(ev,false,true); + this.dispatchEvent(event); + } + getAttr(n,def){ + let v=this.getAttribute(n); + if(v==""||v==null) return def; + switch(typeof(def)){ + case "number": + if(v=="true") return 1; + v=+v; + if(isNaN(v)) return 0; + return v; + } + return v; + } + showtip(d){ + function valstr(x,c,type){ + switch(type){ + case "x": return (x|0).toString(16); + case "X": return (x|0).toString(16).toUpperCase(); + case "d": return (x|0).toString(); + case "f": return x.toFixed(c); + case "s": return x.toString(); + } + return ""; + } + function numformat(s,x){ + if(typeof(x)=="undefined") + return; + let i=s.indexOf("%"); + let c=[0,0],type=0,m=0,r="",j=i+1; + for(;j=0){ + type=s[j]; + break; + } + if(s[j]==".") + m=1; + else + c[m]=c[m]*10+parseInt(s[j]); + } + if(typeof(x)=="number") + r=valstr(x,c[1],type); + else + r=valstr(x.x,c[1],type)+","+valstr(x.y,c[1],type); + if(c[0]>0) + r=(" "+r).slice(-c[0]); + r=s.replace(/%.*[xXdfs]/,r); + return r; + } + let s=this.tooltip; + if(this.drag||this.hover){ + if(this.valuetip){ + if(s==null) + s=`%.${this.digits}f`; + else if(s.indexOf("%")<0) + s+=` : %.${this.digits}f`; + } + if(s){ + this.ttframe.innerHTML=numformat(s,this.convValue); + this.ttframe.style.display="inline-block"; + this.ttframe.style.width="auto"; + this.ttframe.style.height="auto"; + this.ttframe.style.transition="opacity 0.5s "+d+"s,visibility 0.5s "+d+"s"; + this.ttframe.style.opacity=0.9; + this.ttframe.style.visibility="visible"; + let rc=this.getBoundingClientRect(),rc2=this.ttframe.getBoundingClientRect(),rc3=document.documentElement.getBoundingClientRect(); + this.ttframe.style.left=((rc.width-rc2.width)*0.5+1000)+"px"; + this.ttframe.style.top=(-rc2.height-8)+"px"; + return; + } + } + this.ttframe.style.transition="opacity 0.1s "+d+"s,visibility 0.1s "+d+"s"; + this.ttframe.style.opacity=0; + this.ttframe.style.visibility="hidden"; + } + pointerover(e) { + this.hover=1; + this.showtip(0.6); + } + pointerout(e) { + this.hover=0; + this.showtip(0); + } + contextMenu(e){ + if(window.webAudioControlsMidiManager && this.midilearn) + webAudioControlsMidiManager.contextMenuOpen(e,this); + e.preventDefault(); + e.stopPropagation(); + } + setMidiController(channel, cc) { + if (this.listeningToThisMidiController(channel, cc)) return; + this.midiController={ 'channel': channel, 'cc': cc}; + console.log("Added mapping for channel=" + channel + " cc=" + cc + " tooltip=" + this.tooltip); + } + listeningToThisMidiController(channel, cc) { + const c = this.midiController; + if((c.channel === channel || c.channel < 0) && c.cc === cc) + return true; + return false; + } + processMidiEvent(event){ + const channel = event.data[0] & 0xf; + const controlNumber = event.data[1]; + if(this.midiMode == 'learn') { + this.setMidiController(channel, controlNumber); + webAudioControlsMidiManager.contextMenuClose(); + this.midiMode = 'normal'; + } + if(this.listeningToThisMidiController(channel, controlNumber)) { + if(this.tagName=="WEBAUDIO-SWITCH"){ + switch(this.type){ + case "toggle": + if(event.data[2]>=64) + this.setValue(1-this.value,true); + break; + case "kick": + this.setValue(event.data[2]>=64?1:0); + break; + case "radio": + let els=document.querySelectorAll("webaudio-switch[type='radio'][group='"+this.group+"']"); + for(let i=0;i +${this.basestyle} +webaudio-knob{ + display:inline-block; + position:relative; + margin:0; + padding:0; + cursor:pointer; + font-family: sans-serif; + font-size: 11px; +} +.webaudio-knob-body{ + display:inline-block; + position:relative; + margin:0; + padding:0; + vertical-align:bottom; +} + +
    +`; + this.elem=root.childNodes[2]; + this.ttframe=root.childNodes[3]; + this.enable=this.getAttr("enable",1); + this._src=this.getAttr("src",opt.knobSrc); Object.defineProperty(this,"src",{get:()=>{return this._src},set:(v)=>{this._src=v;this.setupImage()}}); + this._value=this.getAttr("value",0); Object.defineProperty(this,"value",{get:()=>{return this._value},set:(v)=>{this._value=v;this.redraw()}}); + this.defvalue=this.getAttr("defvalue",0); + this._min=this.getAttr("min",0); Object.defineProperty(this,"min",{get:()=>{return this._min},set:(v)=>{this._min=+v;this.redraw()}}); + this._max=this.getAttr("max",100); Object.defineProperty(this,"max",{get:()=>{return this._max},set:(v)=>{this._max=+v;this.redraw()}}); + this._step=this.getAttr("step",1); Object.defineProperty(this,"step",{get:()=>{return this._step},set:(v)=>{this._step=+v;this.redraw()}}); + this._sprites=this.getAttr("sprites",opt.knobSprites); Object.defineProperty(this,"sprites",{get:()=>{return this._sprites},set:(v)=>{this._sprites=v;this.setupImage()}}); + this._width=this.getAttr("width",opt.knobWidth); Object.defineProperty(this,"width",{get:()=>{return this._width},set:(v)=>{this._width=v;this.setupImage()}}); + this._height=this.getAttr("height",opt.knobHeight); Object.defineProperty(this,"height",{get:()=>{return this._height},set:(v)=>{this._height=v;this.setupImage()}}); + this._diameter=this.getAttr("diameter",opt.knobDiameter); Object.defineProperty(this,"diameter",{get:()=>{return this._diameter},set:(v)=>{this._diameter=v;this.setupImage()}}); + this._colors=this.getAttr("colors",opt.knobColors); Object.defineProperty(this,"colors",{get:()=>{return this._colors},set:(v)=>{this._colors=v;this.setupImage()}}); + this.outline=this.getAttr("outline",opt.outline); + this.sensitivity=this.getAttr("sensitivity",1); + this.valuetip=this.getAttr("valuetip",1); + this.tooltip=this.getAttr("tooltip",null); + this.conv=this.getAttr("conv",null); + if(this.conv) + this.convValue=eval(this.conv)(this._value); + else + this.convValue=this._value; + this.midilearn=this.getAttr("midilearn",opt.midilearn); + this.midicc=this.getAttr("midicc",null); + + this.midiController={}; + this.midiMode="normal"; + if(this.midicc) { + let ch = parseInt(this.midicc.substring(0, this.midicc.lastIndexOf("."))) - 1; + let cc = parseInt(this.midicc.substring(this.midicc.lastIndexOf(".") + 1)); + this.setMidiController(ch, cc); + } + this.setupImage(); + this.digits=0; + this.coltab=["#e00","#000","#000"]; + if(window.webAudioControlsMidiManager) +// window.webAudioControlsMidiManager.updateWidgets(); + window.webAudioControlsMidiManager.addWidget(this); + } + disconnectedCallback(){} + setupImage(){ + this.kw=this.width||this.diameter; + this.kh=this.height||this.diameter; + if(!this.src){ + if(this.colors) + this.coltab = this.colors.split(";"); + if(!this.coltab) + this.coltab=["#e00","#000","#000"]; + let svg= +` + + +`; + for(let i=0;i<101;++i){ + svg += ``; + svg += ``; + } + svg += ""; + this.elem.style.backgroundImage = "url(data:image/svg+xml;base64,"+btoa(svg)+")"; +// this.elem.style.backgroundSize = "100% 10100%"; + this.elem.style.backgroundSize = `${this.kw}px ${this.kh*101}px`; + } + else{ + this.elem.style.backgroundImage = "url("+(this.src)+")"; + if(!this.sprites) + this.elem.style.backgroundSize = "100% 100%"; + else{ +// this.elem.style.backgroundSize = `100% ${(this.sprites+1)*100}%`; + this.elem.style.backgroundSize = `${this.kw}px ${this.kh*(this.sprites+1)}px`; + } + } + this.elem.style.outline=this.outline?"":"none"; + this.elem.style.width=this.kw+"px"; + this.elem.style.height=this.kh+"px"; + this.style.height=this.kh+"px"; + this.redraw(); + } + redraw() { + this.digits=0; + if(this.step && this.step < 1) { + for(let n = this.step ; n < 1; n *= 10) + ++this.digits; + } + if(this.valuethis.max){ + this.value=this.max; + return; + } + let range = this.max - this.min; + let style = this.elem.style; + let sp = this.src?this.sprites:100; + if(sp>=1){ + let offset = ((sp * (this.value - this.min) / range) | 0); + style.backgroundPosition = "0px " + (-offset*this.kh) + "px"; + style.transform = 'rotate(0deg)'; + } else { + let deg = 270 * ((this.value - this.min) / range - 0.5); + style.backgroundPosition="0px 0px"; + style.transform = 'rotate(' + deg + 'deg)'; + } + } + _setValue(v){ + if(this.step) + v=(Math.round((v-this.min)/this.step))*this.step+this.min; + this._value=Math.min(this.max,Math.max(this.min,v)); + if(this._value!=this.oldvalue){ + this.oldvalue=this._value; + if(this.conv) + this.convValue=eval(this.conv)(this._value); + else + this.convValue=this._value; + this.redraw(); + this.showtip(0); + return 1; + } + return 0; + } + setValue(v,f){ + if(this._setValue(v) && f) + this.sendEvent("input"),this.sendEvent("change"); + } + wheel(e) { + let delta=(this.max-this.min)*0.01; + delta=e.deltaY>0?-delta:delta; + if(!e.shiftKey) + delta*=5; + if(Math.abs(delta) < this.step) + delta = (delta > 0) ? +this.step : -this.step; + this.setValue(+this.value+delta,true); + e.preventDefault(); + e.stopPropagation(); + } + pointerdown(ev){ + if(!this.enable) + return; + let e=ev; + if(ev.touches){ + e = ev.changedTouches[0]; + this.identifier=e.identifier; + } + else { + if(e.buttons!=1 && e.button!=0) + return; + } + this.elem.focus(); + this.drag=1; + this.showtip(0); + let pointermove=(ev)=>{ + let e=ev; + if(ev.touches){ + for(let i=0;i{ + let e=ev; + if(ev.touches){ + for(let i=0;;){ + if(ev.changedTouches[i].identifier==this.identifier){ + break; + } + if(++i>=ev.changedTouches.length) + return; + } + } + this.drag=0; + this.showtip(0); + this.startPosX = this.startPosY = null; + window.removeEventListener('mousemove', pointermove); + window.removeEventListener('touchmove', pointermove, {passive:false}); + window.removeEventListener('mouseup', pointerup); + window.removeEventListener('touchend', pointerup); + window.removeEventListener('touchcancel', pointerup); + document.body.removeEventListener('touchstart', preventScroll,{passive:false}); + this.sendEvent("change"); + } + let preventScroll=(e)=>{ + e.preventDefault(); + } + if(e.ctrlKey || e.metaKey) + this.setValue(this.defvalue,true); + else { + this.startPosX = e.pageX; + this.startPosY = e.pageY; + this.startVal = this.value; + window.addEventListener('mousemove', pointermove); + window.addEventListener('touchmove', pointermove, {passive:false}); + } + window.addEventListener('mouseup', pointerup); + window.addEventListener('touchend', pointerup); + window.addEventListener('touchcancel', pointerup); + document.body.addEventListener('touchstart', preventScroll,{passive:false}); + ev.preventDefault(); + ev.stopPropagation(); + return false; + } + }); +} catch(error){ + console.log("webaudio-knob already defined"); +} + +try{ + customElements.define("webaudio-slider", class WebAudioSlider extends WebAudioControlsWidget { + constructor(){ + super(); + } + connectedCallback(){ + let root; +// if(this.attachShadow) +// root=this.attachShadow({mode: 'open'}); +// else + root=this; + root.innerHTML= +` +
    +`; + this.elem=root.childNodes[2]; + this.knob=this.elem.childNodes[0]; + this.ttframe=root.childNodes[3]; + this.enable=this.getAttr("enable",1); + this._src=this.getAttr("src",opt.sliderSrc); Object.defineProperty(this,"src",{get:()=>{return this._src},set:(v)=>{this._src=v;this.setupImage()}}); + this._knobsrc=this.getAttr("knobsrc",opt.sliderKnobsrc); Object.defineProperty(this,"knobsrc",{get:()=>{return this._knobsrc},set:(v)=>{this._knobsrc=v;this.setupImage()}}); + this._value=this.getAttr("value",0); Object.defineProperty(this,"value",{get:()=>{return this._value},set:(v)=>{this._value=v;this.redraw()}}); + this.defvalue=this.getAttr("defvalue",0); + this._min=this.getAttr("min",0); Object.defineProperty(this,"min",{get:()=>{return this._min},set:(v)=>{this._min=v;this.redraw()}}); + this._max=this.getAttr("max",100); Object.defineProperty(this,"max",{get:()=>{return this._max},set:(v)=>{this._max=v;this.redraw()}}); + this._step=this.getAttr("step",1); Object.defineProperty(this,"step",{get:()=>{return this._step},set:(v)=>{this._step=v;this.redraw()}}); + this._sprites=this.getAttr("sprites",0); Object.defineProperty(this,"sprites",{get:()=>{return this._sprites},set:(v)=>{this._sprites=v;this.setupImage()}}); + this._direction=this.getAttr("direction",null); Object.defineProperty(this,"direction",{get:()=>{return this._direction},set:(v)=>{this._direction=v;this.setupImage()}}); + this._width=this.getAttr("width",opt.sliderWidth); Object.defineProperty(this,"width",{get:()=>{return this._width},set:(v)=>{this._width=v;this.setupImage()}}); + this._height=this.getAttr("height",opt.sliderHeight); Object.defineProperty(this,"height",{get:()=>{return this._height},set:(v)=>{this._height=v;this.setupImage()}}); + if(this._direction=="horz"){ + if(this._width==0) this._width=128; + if(this._height==0) this._height=24; + } + else{ + if(this._width==0) this._width=24; + if(this._height==0) this._height=128; + } + this._knobwidth=this.getAttr("knobwidth",opt.sliderKnobwidth); Object.defineProperty(this,"knobwidth",{get:()=>{return this._knobwidth},set:(v)=>{this._knobwidth=v;this.setupImage()}}); + this._knobheight=this.getAttr("knbheight",opt.sliderKnobheight); Object.defineProperty(this,"knobheight",{get:()=>{return this._knobheight},set:(v)=>{this._knobheight=v;this.setupImage()}}); + this._ditchlength=this.getAttr("ditchlength",opt.sliderDitchlength); Object.defineProperty(this,"ditchlength",{get:()=>{return this._ditchlength},set:(v)=>{this._ditchlength=v;this.setupImage()}}); + this._colors=this.getAttr("colors",opt.sliderColors); Object.defineProperty(this,"colors",{get:()=>{return this._colors},set:(v)=>{this._colors=v;this.setupImage()}}); + this.outline=this.getAttr("outline",opt.outline); + this.sensitivity=this.getAttr("sensitivity",1); + this.valuetip=this.getAttr("valuetip",1); + this.tooltip=this.getAttr("tooltip",null); + this.conv=this.getAttr("conv",null); + if(this.conv) + this.convValue=eval(this.conv)(this._value); + else + this.convValue=this._value; + this.midilearn=this.getAttr("midilearn",opt.midilearn); + this.midicc=this.getAttr("midicc",null); + this.midiController={}; + this.midiMode="normal"; + if(this.midicc) { + let ch = parseInt(this.midicc.substring(0, this.midicc.lastIndexOf("."))) - 1; + let cc = parseInt(this.midicc.substring(this.midicc.lastIndexOf(".") + 1)); + this.setMidiController(ch, cc); + } + this.setupImage(); + this.digits=0; + if(window.webAudioControlsMidiManager) +// window.webAudioControlsMidiManager.updateWidgets(); + window.webAudioControlsMidiManager.addWidget(this); + this.elem.onclick=(e)=>{e.stopPropagation()}; + } + disconnectedCallback(){} + setupImage(){ + this.coltab = this.colors.split(";"); + this.dr=this.direction; + this.dlen=this.ditchlength; + if(!this.width){ + if(this.dr=="horz") + this.width=128; + else + this.width=24; + } + if(!this.height){ + if(this.dr=="horz") + this.height=24; + else + this.height=128; + } + if(!this.dr) + this.dr=(this.width<=this.height)?"vert":"horz"; + if(this.dr=="vert"){ + if(!this.dlen) + this.dlen=this.height-this.width; + } + else{ + if(!this.dlen) + this.dlen=this.width-this.height; + } + this.knob.style.backgroundSize = "100% 100%"; + this.elem.style.backgroundSize = "100% 100%"; + this.elem.style.width=this.width+"px"; + this.elem.style.height=this.height+"px"; + this.style.height=this.height+"px"; + this.kwidth=this.knobwidth||(this.dr=="horz"?this.height:this.width); + this.kheight=this.knobheight||(this.dr=="horz"?this.height:this.width); + this.knob.style.width = this.kwidth+"px"; + this.knob.style.height = this.kheight+"px"; + if(!this.src){ + let r=Math.min(this.width,this.height)*0.5; + let svgbody= +` +`; + this.elem.style.backgroundImage = "url(data:image/svg+xml;base64,"+btoa(svgbody)+")"; + } + else{ + this.elem.style.backgroundImage = "url("+(this.src)+")"; + } + if(!this.knobsrc){ + let svgthumb= +` + +`; + this.knob.style.backgroundImage = "url(data:image/svg+xml;base64,"+btoa(svgthumb)+")"; + } + else{ + this.knob.style.backgroundImage = "url("+(this.knobsrc)+")"; + } + this.elem.style.outline=this.outline?"":"none"; + this.redraw(); + } + redraw() { + this.digits=0; + if(this.step && this.step < 1) { + for(let n = this.step ; n < 1; n *= 10) + ++this.digits; + } + if(this.valuethis.max){ + this.value=this.max; + return; + } + let range = this.max - this.min; + let style = this.knob.style; + if(this.dr=="vert"){ + style.left=(this.width-this.kwidth)*0.5+"px"; + style.top=(1-(this.value-this.min)/range)*this.dlen+"px"; + this.sensex=0; this.sensey=1; + } + else{ + style.top=(this.height-this.kheight)*0.5+"px"; + style.left=(this.value-this.min)/range*this.dlen+"px"; + this.sensex=1; this.sensey=0; + } + } + _setValue(v){ + v=(Math.round((v-this.min)/this.step))*this.step+this.min; + this._value=Math.min(this.max,Math.max(this.min,v)); + if(this._value!=this.oldvalue){ + this.oldvalue=this._value; + if(this.conv) + this.convValue=eval(this.conv)(this._value); + else + this.convValue=this._value; + this.redraw(); + this.showtip(0); + return 1; + } + return 0; + } + setValue(v,f){ + if(this._setValue(v)&&f) + this.sendEvent("input"),this.sendEvent("change"); + } + wheel(e) { + let delta=(this.max-this.min)*0.01; + delta=e.deltaY>0?-delta:delta; + if(!e.shiftKey) + delta*=5; + if(Math.abs(delta) < this.step) + delta = (delta > 0) ? +this.step : -this.step; + this.setValue(+this.value+delta,true); + e.preventDefault(); + e.stopPropagation(); + this.redraw(); + } + pointerdown(ev){ + if(!this.enable) + return; + let e=ev; + if(ev.touches){ + e = ev.changedTouches[0]; + this.identifier=e.identifier; + } + else { + if(e.buttons!=1 && e.button!=0) + return; + } + this.elem.focus(); + this.drag=1; + this.showtip(0); + let pointermove=(ev)=>{ + let e=ev; + if(ev.touches){ + for(let i=0;i{ + let e=ev; + if(ev.touches){ + for(let i=0;;){ + if(ev.changedTouches[i].identifier==this.identifier){ + break; + } + if(++i>=ev.changedTouches.length) + return; + } + } + this.drag=0; + this.showtip(0); + this.startPosX = this.startPosY = null; + window.removeEventListener('mousemove', pointermove); + window.removeEventListener('touchmove', pointermove, {passive:false}); + window.removeEventListener('mouseup', pointerup); + window.removeEventListener('touchend', pointerup); + window.removeEventListener('touchcancel', pointerup); + document.body.removeEventListener('touchstart', preventScroll,{passive:false}); + this.sendEvent("change"); + } + let preventScroll=(e)=>{ + e.preventDefault(); + } + if(e.touches) + e = e.touches[0]; + if(e.ctrlKey || e.metaKey) + this.setValue(this.defvalue,true); + else { + this.startPosX = e.pageX; + this.startPosY = e.pageY; + this.startVal = this.value; + window.addEventListener('mousemove', pointermove); + window.addEventListener('touchmove', pointermove, {passive:false}); + } + window.addEventListener('mouseup', pointerup); + window.addEventListener('touchend', pointerup); + window.addEventListener('touchcancel', pointerup); + document.body.addEventListener('touchstart', preventScroll,{passive:false}); + e.preventDefault(); + e.stopPropagation(); + return false; + } + }); +} catch(error){ + console.log("webaudio-slider already defined"); +} + +try{ + customElements.define("webaudio-switch", class WebAudioSwitch extends WebAudioControlsWidget { + constructor(){ + super(); + } + connectedCallback(){ + let root; +// if(this.attachShadow) +// root=this.attachShadow({mode: 'open'}); +// else + root=this; + root.innerHTML= +` +
    +`; + this.elem=root.childNodes[2]; + this.ttframe=this.elem.childNodes[0]; + + this.enable=this.getAttr("enable",1); + this._src=this.getAttr("src",null); Object.defineProperty(this,"src",{get:()=>{return this._src},set:(v)=>{this._src=v;this.setupImage()}}); + this._value=this.getAttr("value",0); Object.defineProperty(this,"value",{get:()=>{return this._value},set:(v)=>{this._value=v;this.redraw()}}); + this.defvalue=this.getAttr("defvalue",0); + this.type=this.getAttr("type","toggle"); + this.group=this.getAttr("group",""); + this._width=this.getAttr("width",0); Object.defineProperty(this,"width",{get:()=>{return this._width},set:(v)=>{this._width=v;this.setupImage()}}); + this._height=this.getAttr("height",0); Object.defineProperty(this,"height",{get:()=>{return this._height},set:(v)=>{this._height=v;this.setupImage()}}); + this._diameter=this.getAttr("diameter",0); Object.defineProperty(this,"diameter",{get:()=>{return this._diameter},set:(v)=>{this._diameter=v;this.setupImage()}}); + this.invert=this.getAttr("invert",0); + this._colors=this.getAttr("colors",opt.switchColors); Object.defineProperty(this,"colors",{get:()=>{return this._colors},set:(v)=>{this._colors=v;this.setupImage()}}); + this.outline=this.getAttr("outline",opt.outline); + this.valuetip=0; + this.tooltip=this.getAttr("tooltip",null); + this.midilearn=this.getAttr("midilearn",opt.midilearn); + this.midicc=this.getAttr("midicc",null); + this.midiController={}; + this.midiMode="normal"; + if(this.midicc) { + let ch = parseInt(this.midicc.substring(0, this.midicc.lastIndexOf("."))) - 1; + let cc = parseInt(this.midicc.substring(this.midicc.lastIndexOf(".") + 1)); + this.setMidiController(ch, cc); + } + this.setupImage(); + this.digits=0; + if(window.webAudioControlsMidiManager) +// window.webAudioControlsMidiManager.updateWidgets(); + window.webAudioControlsMidiManager.addWidget(this); + this.elem.onclick=(e)=>{e.stopPropagation()}; + } + disconnectedCallback(){} + setupImage(){ + let w=this.width||this.diameter||opt.switchWidth||opt.switchDiameter; + let h=this.height||this.diameter||opt.switchHeight||opt.switchDiameter; + if(!this.src){ + this.coltab = this.colors.split(";"); + let mm=Math.min(w,h); + let svg= +` + + + + +`; + this.elem.style.backgroundImage = "url(data:image/svg+xml;base64,"+btoa(svg)+")"; + this.elem.style.backgroundSize = "100% 200%"; + } + else{ + this.elem.style.backgroundImage = "url("+(this.src)+")"; + if(!this.sprites) + this.elem.style.backgroundSize = "100% 200%"; + else + this.elem.style.backgroundSize = `100% ${(this.sprites+1)*100}%`; + } + this.elem.style.width=w+"px"; + this.elem.style.height=h+"px"; + this.style.height=h+"px"; + this.elem.style.outline=this.outline?"":"none"; + this.redraw(); + } + redraw() { + let style = this.elem.style; + if(this.value^this.invert) + style.backgroundPosition = "0px -100%"; + else + style.backgroundPosition = "0px 0px"; + } + setValue(v,f){ + this.value=v; + this.checked=(!!v); + if(this.value!=this.oldvalue){ + this.redraw(); + this.showtip(0); + if(f){ + this.sendEvent("input"); + this.sendEvent("change"); + } + this.oldvalue=this.value; + } + } + pointerdown(ev){ + if(!this.enable) + return; + let e=ev; + if(ev.touches){ + e = ev.changedTouches[0]; + this.identifier=e.identifier; + } + else { + if(e.buttons!=1 && e.button!=0) + return; + } + this.elem.focus(); + this.drag=1; + this.showtip(0); + let pointermove=(e)=>{ + e.preventDefault(); + e.stopPropagation(); + return false; + } + let pointerup=(e)=>{ + this.drag=0; + this.showtip(0); + window.removeEventListener('mousemove', pointermove); + window.removeEventListener('touchmove', pointermove, {passive:false}); + window.removeEventListener('mouseup', pointerup); + window.removeEventListener('touchend', pointerup); + window.removeEventListener('touchcancel', pointerup); + document.body.removeEventListener('touchstart', preventScroll,{passive:false}); + if(this.type=="kick"){ + this.value=0; + this.checked=false; + this.redraw(); + this.sendEvent("change"); + } + this.sendEvent("click"); + e.preventDefault(); + e.stopPropagation(); + } + let preventScroll=(e)=>{ + e.preventDefault(); + } + switch(this.type){ + case "kick": + this.setValue(1); + this.sendEvent("change"); + break; + case "toggle": + if(e.ctrlKey || e.metaKey) + this.value=defvalue; + else + this.value=1-this.value; + this.checked=!!this.value; + this.sendEvent("change"); + break; + case "radio": + let els=document.querySelectorAll("webaudio-switch[type='radio'][group='"+this.group+"']"); + for(let i=0;i +${this.basestyle} +webaudio-param{ + display:inline-block; + user-select:none; + margin:0; + padding:0; + font-family: sans-serif; + font-size: 8px; + cursor:pointer; + position:relative; + vertical-align:baseline; +} +.webaudio-param-body{ + display:inline-block; + position:relative; + text-align:center; + border:1px solid #888; + background:none; + border-radius:4px; + margin:0; + padding:0; + font-family:sans-serif; + font-size:11px; + vertical-align:bottom; +} + +
    +`; + this.elem=root.childNodes[2]; + this.ttframe=root.childNodes[3]; + this.enable=this.getAttr("enable",1); + this._value=this.getAttr("value",0); Object.defineProperty(this,"value",{get:()=>{return this._value},set:(v)=>{this._value=v;this.redraw()}}); + this.defvalue=this.getAttr("defvalue",0); + this._fontsize=this.getAttr("fontsize",9); Object.defineProperty(this,"fontsize",{get:()=>{return this._fontsize},set:(v)=>{this._fontsize=v;this.setupImage()}}); + this._src=this.getAttr("src",null); Object.defineProperty(this,"src",{get:()=>{return this._src},set:(v)=>{this._src=v;this.setupImage()}}); + this.link=this.getAttr("link",""); + this._width=this.getAttr("width",32); Object.defineProperty(this,"width",{get:()=>{return this._width},set:(v)=>{this._width=v;this.setupImage()}}); + this._height=this.getAttr("height",20); Object.defineProperty(this,"height",{get:()=>{return this._height},set:(v)=>{this._height=v;this.setupImage()}}); + this._colors=this.getAttr("colors","#fff;#000"); Object.defineProperty(this,"colors",{get:()=>{return this._colors},set:(v)=>{this._colors=v;this.setupImage()}}); + this.outline=this.getAttr("outline",opt.outline); + this.midiController={}; + this.midiMode="normal"; + if(this.midicc) { + let ch = parseInt(this.midicc.substring(0, this.midicc.lastIndexOf("."))) - 1; + let cc = parseInt(this.midicc.substring(this.midicc.lastIndexOf(".") + 1)); + this.setMidiController(ch, cc); + } + this.setupImage(); + if(window.webAudioControlsMidiManager) +// window.webAudioControlsMidiManager.updateWidgets(); + window.webAudioControlsMidiManager.addWidget(this); + this.fromLink=((e)=>{ + this.setValue(e.target.convValue.toFixed(e.target.digits)); + }).bind(this); + this.elem.onchange=()=>{ + this.value=this.elem.value; + let le=document.getElementById(this.link); + if(le) + le.setValue(+this.elem.value); + } + } + disconnectedCallback(){} + setupImage(){ + this.coltab = this.colors.split(";"); + this.elem.style.color=this.coltab[0]; + if(!this.src){ + this.elem.style.backgroundColor=this.coltab[1]; + } + else{ + this.elem.style.backgroundImage = "url("+(this.src)+")"; + this.elem.style.backgroundSize = "100% 100%"; + } + this.elem.style.width=this.width+"px"; + this.elem.style.height=this.height+"px"; + this.elem.style.fontSize=this.fontsize+"px"; + this.elem.style.outline=this.outline?"":"none"; + let l=document.getElementById(this.link); + if(l&&typeof(l.value)!="undefined"){ + this.setValue(l.value.toFixed(l.digits)); + l.addEventListener("input",(e)=>{this.setValue(l.value.toFixed(l.digits))}); + } + this.redraw(); + } + redraw() { + this.elem.value=this.value; + } + setValue(v,f){ + this.value=v; + if(this.value!=this.oldvalue){ + this.redraw(); + this.showtip(0); + if(f){ + let event=document.createEvent("HTMLEvents"); + event.initEvent("change",false,true); + this.dispatchEvent(event); + } + this.oldvalue=this.value; + } + } + pointerdown(ev){ + if(!this.enable) + return; + let e=ev; + if(ev.touches) + e = ev.touches[0]; + else { + if(e.buttons!=1 && e.button!=0) + return; + } + this.elem.focus(); + this.redraw(); + } + }); +} catch(error){ + console.log("webaudio-param already defined"); +} + +try{ + customElements.define("webaudio-keyboard", class WebAudioKeyboard extends WebAudioControlsWidget { + constructor(){ + super(); + } + connectedCallback(){ + let root; +// if(this.attachShadow) +// root=this.attachShadow({mode: 'open'}); +// else + root=this; + root.innerHTML= +` +
    +`; + this.cv=root.childNodes[2]; + this.ttframe=root.childNodes[3]; + this.ctx=this.cv.getContext("2d"); + this._values=[]; + this.enable=this.getAttr("enable",1); + this._width=this.getAttr("width",480); Object.defineProperty(this,"width",{get:()=>{return this._width},set:(v)=>{this._width=v;this.setupImage()}}); + this._height=this.getAttr("height",128); Object.defineProperty(this,"height",{get:()=>{return this._height},set:(v)=>{this._height=v;this.setupImage()}}); + this._min=this.getAttr("min",0); Object.defineProperty(this,"min",{get:()=>{return this._min},set:(v)=>{this._min=+v;this.redraw()}}); + this._keys=this.getAttr("keys",25); Object.defineProperty(this,"keys",{get:()=>{return this._keys},set:(v)=>{this._keys=+v;this.setupImage()}}); + this._colors=this.getAttr("colors","#222;#eee;#ccc;#333;#000;#e88;#c44;#c33;#800"); Object.defineProperty(this,"colors",{get:()=>{return this._colors},set:(v)=>{this._colors=v;this.setupImage()}}); + this.outline=this.getAttr("outline",opt.outline); + this.midilearn=this.getAttr("midilearn",0); + this.midicc=this.getAttr("midicc",null); + this.press=0; + this.keycodes1=[90,83,88,68,67,86,71,66,72,78,74,77,188,76,190,187,191,226]; + this.keycodes2=[81,50,87,51,69,82,53,84,54,89,55,85,73,57,79,48,80,192,222,219]; + this.addEventListener("keyup",this.keyup); + this.midiController={}; + this.midiMode="normal"; + if(this.midicc) { + let ch = parseInt(this.midicc.substring(0, this.midicc.lastIndexOf("."))) - 1; + let cc = parseInt(this.midicc.substring(this.midicc.lastIndexOf(".") + 1)); + this.setMidiController(ch, cc); + } + this.setupImage(); + this.digits=0; + if(window.webAudioControlsMidiManager) + window.webAudioControlsMidiManager.addWidget(this); + } + disconnectedCallback(){} + setupImage(){ + this.cv.style.width=this.width+"px"; + this.cv.style.height=this.height+"px"; + this.bheight = this.height * 0.55; + this.kp=[0,7/12,1,3*7/12,2,3,6*7/12,4,8*7/12,5,10*7/12,6]; + this.kf=[0,1,0,1,0,0,1,0,1,0,1,0]; + this.ko=[0,0,(7*2)/12-1,0,(7*4)/12-2,(7*5)/12-3,0,(7*7)/12-4,0,(7*9)/12-5,0,(7*11)/12-6]; + this.kn=[0,2,4,5,7,9,11]; + this.coltab=this.colors.split(";"); + this.cv.width = this.width; + this.cv.height = this.height; + this.cv.style.width = this.width+'px'; + this.cv.style.height = this.height+'px'; + this.style.height = this.height+'px'; + this.cv.style.outline=this.outline?"":"none"; + this.bheight = this.height * 0.55; + this.max=this.min+this.keys-1; + this.dispvalues=[]; + this.valuesold=[]; + if(this.kf[this.min%12]) + --this.min; + if(this.kf[this.max%12]) + ++this.max; + this.redraw(); + } + redraw(){ + function rrect(ctx, x, y, w, h, r, c1, c2) { + if(c2) { + let g=ctx.createLinearGradient(x,y,x+w,y); + g.addColorStop(0,c1); + g.addColorStop(1,c2); + ctx.fillStyle=g; + } + else + ctx.fillStyle=c1; + ctx.beginPath(); + ctx.moveTo(x, y); + ctx.lineTo(x+w, y); + ctx.lineTo(x+w, y+h-r); + ctx.quadraticCurveTo(x+w, y+h, x+w-r, y+h); + ctx.lineTo(x+r, y+h); + ctx.quadraticCurveTo(x, y+h, x, y+h-r); + ctx.lineTo(x, y); + ctx.fill(); + } + this.ctx.fillStyle = this.coltab[0]; + this.ctx.fillRect(0,0,this.width,this.height); + let x0=7*((this.min/12)|0)+this.kp[this.min%12]; + let x1=7*((this.max/12)|0)+this.kp[this.max%12]; + let n=x1-x0; + this.wwidth=(this.width-1)/(n+1); + this.bwidth=this.wwidth*7/12; + let h2=this.bheight; + let r=Math.min(8,this.wwidth*0.2); + for(let i=this.min,j=0;i<=this.max;++i) { + if(this.kf[i%12]==0) { + let x=this.wwidth*(j++)+1; + if(this.dispvalues.indexOf(i)>=0) + rrect(this.ctx,x,1,this.wwidth-1,this.height-2,r,this.coltab[5],this.coltab[6]); + else + rrect(this.ctx,x,1,this.wwidth-1,this.height-2,r,this.coltab[1],this.coltab[2]); + } + } + r=Math.min(8,this.bwidth*0.3); + for(let i=this.min;i=0) + rrect(this.ctx,x,1,this.bwidth,h2,r,this.coltab[7],this.coltab[8]); + else + rrect(this.ctx,x,1,this.bwidth,h2,r,this.coltab[3],this.coltab[4]); + this.ctx.strokeStyle=this.coltab[0]; + this.ctx.stroke(); + } + } + } + _setValue(v){ + if(this.step) + v=(Math.round((v-this.min)/this.step))*this.step+this.min; + this._value=Math.min(this.max,Math.max(this.min,v)); + if(this._value!=this.oldvalue){ + this.oldvalue=this._value; + this.redraw(); + this.showtip(0); + return 1; + } + return 0; + } + setValue(v,f){ + if(this._setValue(v) && f) + this.sendEvent("input"),this.sendEvent("change"); + } + wheel(e){} + keydown(e){ + let m=Math.floor((this.min+11)/12)*12; + let k=this.keycodes1.indexOf(e.keyCode); + if(k<0) { + k=this.keycodes2.indexOf(e.keyCode); + if(k>=0) k+=12; + } + if(k>=0){ + k+=m; + if(this.currentKey!=k){ + this.currentKey=k; + this.sendEventFromKey(1,k); + this.setNote(1,k); + } + } + } + keyup(e){ + let m=Math.floor((this.min+11)/12)*12; + let k=this.keycodes1.indexOf(e.keyCode); + if(k<0) { + k=this.keycodes2.indexOf(e.keyCode); + if(k>=0) k+=12; + } + if(k>=0){ + k+=m; + this.currentKey=-1; + this.sendEventFromKey(0,k); + this.setNote(0,k); + } + } + pointerdown(ev){ + this.cv.focus(); + if(this.enable) { + ++this.press; + } + let pointermove=(ev)=>{ + if(!this.enable) + return; + let r=this.getBoundingClientRect(); + let v=[],p; + if(ev.touches) + p=ev.targetTouches; + else if(this.press) + p=[ev]; + else + p=[]; + if(p.length>0) + this.drag=1; + for(let i=0;i=0&&py=this.min&&k<=this.max) + v.push(k); + } + } + v.sort(); + this.values=v; + this.sendevent(); + this.redraw(); + } + + let pointerup=(ev)=>{ + if(this.enable) { + if(ev.touches) + this.press=ev.touches.length; + else + this.press=0; + pointermove(ev); + this.sendevent(); + if(this.press==0){ + window.removeEventListener('mousemove', pointermove); + window.removeEventListener('touchmove', pointermove, {passive:false}); + window.removeEventListener('mouseup', pointerup); + window.removeEventListener('touchend', pointerup); + window.removeEventListener('touchcancel', pointerup); + document.body.removeEventListener('touchstart', preventScroll,{passive:false}); + } + this.redraw(); + } + this.drag=0; + ev.preventDefault(); + } + let preventScroll=(ev)=>{ + ev.preventDefault(); + } + window.addEventListener('mousemove', pointermove); + window.addEventListener('touchmove', pointermove, {passive:false}); + window.addEventListener('mouseup', pointerup); + window.addEventListener('touchend', pointerup); + window.addEventListener('touchcancel', pointerup); + document.body.addEventListener('touchstart', preventScroll,{passive:false}); + pointermove(ev); + ev.preventDefault(); + ev.stopPropagation(); + } + sendEventFromKey(s,k){ + let ev=document.createEvent('HTMLEvents'); + ev.initEvent('change',true,true); + ev.note=[s,k]; + this.dispatchEvent(ev); + } + sendevent(){ + let notes=[]; + for(let i=0,j=this.valuesold.length;i=0) this.dispvalues.splice(n,1); + } + } + setNote(state,note) { + this.setdispvalues(state,note); + this.redraw(); + } + }); +} catch(error){ + console.log("webaudio-keyboard already defined"); +} + +try{ + customElements.define("webaudio-xypad", class WebAudioXYPad extends WebAudioControlsWidget { + constructor(){ + super(); + } + connectedCallback(){ + let root; +// if(this.attachShadow) +// root=this.attachShadow({mode: 'open'}); +// else + root=this; + root.innerHTML= +` +
    +`; + this.elem=root.childNodes[2]; + this.knob=this.elem.childNodes[0]; + this.ttframe=root.childNodes[3]; + + this.enable=this.getAttr("enable",1); + this._src=this.getAttr("src",opt.sliderSrc); Object.defineProperty(this,"src",{get:()=>{return this._src},set:(v)=>{this._src=v;this.setupImage()}}); + this._knobsrc=this.getAttr("knobsrc",opt.sliderKnobsrc); Object.defineProperty(this,"knobsrc",{get:()=>{return this._knobsrc},set:(v)=>{this._knobsrc=v;this.setupImage()}}); + this._x=this.getAttr("x",50); Object.defineProperty(this,"x",{get:()=>{return this._x},set:(v)=>{this._x=v;this.redraw()}}); + this._y=this.getAttr("y",50); Object.defineProperty(this,"y",{get:()=>{return this._y},set:(v)=>{this._y=v;this.redraw()}}); + this.defx=this.getAttr("defx",50); + this.defy=this.getAttr("defy",50); + this._min=this.getAttr("min",0); Object.defineProperty(this,"min",{get:()=>{return this._min},set:(v)=>{this._min=v;this.redraw()}}); + this._max=this.getAttr("max",100); Object.defineProperty(this,"max",{get:()=>{return this._max},set:(v)=>{this._max=v;this.redraw()}}); + this._step=this.getAttr("step",1); Object.defineProperty(this,"step",{get:()=>{return this._step},set:(v)=>{this._step=v;this.redraw()}}); + this._sprites=this.getAttr("sprites",0); Object.defineProperty(this,"sprites",{get:()=>{return this._sprites},set:(v)=>{this._sprites=v;this.setupImage()}}); + this._width=this.getAttr("width",128); Object.defineProperty(this,"width",{get:()=>{return this._width},set:(v)=>{this._width=v;this.setupImage()}}); + this._height=this.getAttr("height",128); Object.defineProperty(this,"height",{get:()=>{return this._height},set:(v)=>{this._height=v;this.setupImage()}}); + this._knobwidth=this.getAttr("knobwidth",28); Object.defineProperty(this,"knobwidth",{get:()=>{return this._knobwidth},set:(v)=>{this._knobwidth=v;this.setupImage()}}); + this._knobheight=this.getAttr("knbheight",28); Object.defineProperty(this,"knobheight",{get:()=>{return this._knobheight},set:(v)=>{this._knobheight=v;this.setupImage()}}); + this._colors=this.getAttr("colors",opt.sliderColors); Object.defineProperty(this,"colors",{get:()=>{return this._colors},set:(v)=>{this._colors=v;this.setupImage()}}); + this.outline=this.getAttr("outline",opt.outline); + this.valuetip=this.getAttr("valuetip",1); + this.tooltip=this.getAttr("tooltip",null); + this.conv=this.getAttr("conv",null); + if(this.conv){ + this.convValue={x:eval(this.conv)(this._x),y:eval(this.conv)(this._y)}; + } + else + this.convValue={x:this._x,y:this._y}; + this.midilearn=this.getAttr("midilearn",opt.midilearn); + this.midicc=this.getAttr("midicc",null); + this.midiController={}; + this.midiMode="normal"; + if(this.midicc) { + let ch = parseInt(this.midicc.substring(0, this.midicc.lastIndexOf("."))) - 1; + let cc = parseInt(this.midicc.substring(this.midicc.lastIndexOf(".") + 1)); + this.setMidiController(ch, cc); + } + this.setupImage(); + this.digits=0; + if(window.webAudioControlsMidiManager) +// window.webAudioControlsMidiManager.updateWidgets(); + window.webAudioControlsMidiManager.addWidget(this); + this.elem.onclick=(e)=>{e.stopPropagation()}; + } + disconnectedCallback(){} + setupImage(){ + this.coltab = this.colors.split(";"); + this.dr=this.direction; + this.dlen=this.ditchlength; + if(!this.width) + this.width=256; + if(!this.height) + this.height=256; + this.knob.style.backgroundSize = "100% 100%"; + this.elem.style.backgroundSize = "100% 100%"; + this.elem.style.width=this.width+"px"; + this.elem.style.height=this.height+"px"; + this.kwidth=this.knobwidth||(this.width*0.15|0); + this.kheight=this.knobheight||(this.height*0.15|0); + this.knob.style.width = this.kwidth+"px"; + this.knob.style.height = this.kheight+"px"; + if(!this.src){ + let r=Math.min(this.width,this.height)*0.02; + let svgbody= +` +`; + this.elem.style.backgroundImage = "url(data:image/svg+xml;base64,"+btoa(svgbody)+")"; + } + else{ + this.elem.style.backgroundImage = "url("+(this.src)+")"; + } + if(!this.knobsrc){ + let svgthumb= +` + +`; + this.knob.style.backgroundImage = "url(data:image/svg+xml;base64,"+btoa(svgthumb)+")"; + } + else{ + this.knob.style.backgroundImage = "url("+(this.knobsrc)+")"; + } + this.elem.style.outline=this.outline?"":"none"; + this.redraw(); + } + redraw() { + this.digits=0; + if(this.step && this.step < 1) { + for(let n = this.step ; n < 1; n *= 10) + ++this.digits; + } + if(this.valuethis.max){ + this.value=this.max; + return; + } + let range = this.max - this.min; + let style = this.knob.style; + style.left=(this.width-this.kwidth)*(this._x-this.min)/(this.max-this.min)+"px"; style.top=(this.height-this.kheight)*(1-(this._y-this.min)/(this.max-this.min))+"px"; + this.sensex=0; this.sensey=1; + } + _setX(v){ + v=(Math.round((v-this.min)/this.step))*this.step+this.min; + this._x=Math.min(this.max,Math.max(this.min,v)); + if(this._x!=this.oldx){ + this.oldx=this._x; + if(this.conv){ + this.convValue={x:eval(this.conv)(this._x),y:eval(this.conv)(this._y)}; + } + else + this.convValue={x:this._x,y:this._y}; + this.redraw(); + this.showtip(0); + return 1; + } + return 0; + } + _setY(v){ + v=(Math.round((v-this.min)/this.step))*this.step+this.min; + this._y=Math.min(this.max,Math.max(this.min,v)); + if(this._y!=this.oldy){ + this.oldy=this._y; + if(this.conv){ + this.convValue={x:eval(this.conv)(this._x),y:eval(this.conv)(this._y)}; + } + else + this.convValue={x:this._x,y:this._y}; + this.redraw(); + this.showtip(0); + return 1; + } + return 0; + } + setX(v,f){ + if(this._setX(v)&&f) + this.sendEvent("input"),this.sendEvent("change"); + } + setY(v,f){ + if(this._setY(v)&&f) + this.sendEvent("input"),this.sendEvent("change"); + } + wheel(e) { + let delta=(this.max-this.min)*0.01; + delta=e.deltaY>0?-delta:delta; + if(!e.shiftKey) + delta*=5; + if(Math.abs(delta) < this.step) + delta = (delta > 0) ? +this.step : -this.step; + this.setValue(+this.value+delta,true); + e.preventDefault(); + e.stopPropagation(); + this.redraw(); + } + pointerdown(ev){ + if(!this.enable) + return; + let e=ev; + if(ev.touches){ + e = ev.changedTouches[0]; + this.identifier=e.identifier; + } + else { + if(e.buttons!=1 && e.button!=0) + return; + } + this.elem.focus(); + this.drag=1; + this.showtip(0); + let pointermove=(ev)=>{ + let e=ev; + if(ev.touches){ + for(let i=0;i{ + let e=ev; + if(ev.touches){ + for(let i=0;;){ + if(ev.changedTouches[i].identifier==this.identifier){ + break; + } + if(++i>=ev.changedTouches.length) + return; + } + } + this.drag=0; + this.showtip(0); + this.startPosX = this.startPosY = null; + window.removeEventListener('mousemove', pointermove); + window.removeEventListener('touchmove', pointermove, {passive:false}); + window.removeEventListener('mouseup', pointerup); + window.removeEventListener('touchend', pointerup); + window.removeEventListener('touchcancel', pointerup); + document.body.removeEventListener('touchstart', preventScroll,{passive:false}); + this.sendEvent("change"); + } + pointermove(ev); + let preventScroll=(e)=>{ + e.preventDefault(); + } + if(e.touches) + e = e.touches[0]; + if(e.ctrlKey || e.metaKey) + this.setValue(this.defvalue,true); + else { + this.startPosX = e.pageX; + this.startPosY = e.pageY; + this.startVal = this.value; + window.addEventListener('mousemove', pointermove); + window.addEventListener('touchmove', pointermove, {passive:false}); + } + window.addEventListener('mouseup', pointerup); + window.addEventListener('touchend', pointerup); + window.addEventListener('touchcancel', pointerup); + document.body.addEventListener('touchstart', preventScroll,{passive:false}); + e.preventDefault(); + e.stopPropagation(); + return false; + } + }); +} catch(error){ + console.log("webaudio-xypad already defined"); +} + + + + // FOR MIDI LEARN + class WebAudioControlsMidiManager { + constructor(){ + this.midiAccess = null; + this.listOfWidgets = []; + this.listOfExternalMidiListeners = []; + this.updateWidgets(); + this.initWebAudioControls(); + } + addWidget(w){ + this.listOfWidgets.push(w); + } + updateWidgets(){ +// this.listOfWidgets = document.querySelectorAll("webaudio-knob,webaudio-slider,webaudio-switch"); + } + initWebAudioControls() { + if(navigator.requestMIDIAccess) { + navigator.requestMIDIAccess().then( + (ma)=>{this.midiAccess = ma,this.enableInputs()}, + (err)=>{ console.log("MIDI not initialized - error encountered:" + err.code)} + ); + } + } + enableInputs() { + let inputs = this.midiAccess.inputs.values(); + console.log("Found " + this.midiAccess.inputs.size + " MIDI input(s)"); + for(let input = inputs.next(); input && !input.done; input = inputs.next()) { + console.log("Connected input: " + input.value.name); + input.value.onmidimessage = this.handleMIDIMessage.bind(this); + } + } + midiConnectionStateChange(e) { + console.log("connection: " + e.port.name + " " + e.port.connection + " " + e.port.state); + enableInputs(); + } + + onMIDIStarted(midi) { + this.midiAccess = midi; + midi.onstatechange = this.midiConnectionStateChange; + enableInputs(midi); + } + // Add hooks for external midi listeners support + addMidiListener(callback) { + this.listOfExternalMidiListeners.push(callback); + } + getCurrentConfigAsJSON() { + return currentConfig.stringify(); + } + handleMIDIMessage(event) { + this.listOfExternalMidiListeners.forEach(function (externalListener) { + externalListener(event); + }); + if(((event.data[0] & 0xf0) == 0xf0) || ((event.data[0] & 0xf0) == 0xb0 && event.data[1] >= 120)) + return; + for(let w of this.listOfWidgets) { + if(w.processMidiEvent) + w.processMidiEvent(event); + } + if(opt.mididump) + console.log(event.data); + } + contextMenuOpen(e,knob){ + if(!this.midiAccess) + return; + let menu=document.getElementById("webaudioctrl-context-menu"); + menu.style.left=e.pageX+"px"; + menu.style.top=e.pageY+"px"; + menu.knob=knob; + menu.classList.add("active"); + menu.knob.focus(); +// document.activeElement.onblur=this.contextMenuClose; + menu.knob.addEventListener("keydown",this.contextMenuCloseByKey.bind(this)); + } + contextMenuCloseByKey(e){ + if(e.keyCode==27) + this.contextMenuClose(); + } + contextMenuClose(){ + let menu=document.getElementById("webaudioctrl-context-menu"); + menu.knob.removeEventListener("keydown",this.contextMenuCloseByKey); + menu.classList.remove("active"); + let menuItemLearn=document.getElementById("webaudioctrl-context-menu-learn"); + menuItemLearn.innerHTML = 'Learn'; + menu.knob.midiMode = 'normal'; + } + contextMenuLearn(){ + let menu=document.getElementById("webaudioctrl-context-menu"); + let menuItemLearn=document.getElementById("webaudioctrl-context-menu-learn"); + menuItemLearn.innerHTML = 'Listening...'; + menu.knob.midiMode = 'learn'; + } + contextMenuClear(e){ + let menu=document.getElementById("webaudioctrl-context-menu"); + menu.knob.midiController={}; + this.contextMenuClose(); + } + } + if(window.UseWebAudioControlsMidi||opt.useMidi) + window.webAudioControlsMidiManager = new WebAudioControlsMidiManager(); +} diff --git a/web/webcomponents-lite.js b/web/webcomponents-lite.js new file mode 100755 index 0000000..d3e7beb --- /dev/null +++ b/web/webcomponents-lite.js @@ -0,0 +1,197 @@ +(function(){/* + + Copyright (c) 2016 The Polymer Project Authors. All rights reserved. + This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt + The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt + The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt + Code distributed by Google as part of the polymer project is also + subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt +*/ +'use strict';var p,q="undefined"!=typeof window&&window===this?this:"undefined"!=typeof global&&null!=global?global:this,ba="function"==typeof Object.defineProperties?Object.defineProperty:function(a,b,c){a!=Array.prototype&&a!=Object.prototype&&(a[b]=c.value)};function ca(){ca=function(){};q.Symbol||(q.Symbol=da)}var da=function(){var a=0;return function(b){return"jscomp_symbol_"+(b||"")+a++}}(); +function ea(){ca();var a=q.Symbol.iterator;a||(a=q.Symbol.iterator=q.Symbol("iterator"));"function"!=typeof Array.prototype[a]&&ba(Array.prototype,a,{configurable:!0,writable:!0,value:function(){return fa(this)}});ea=function(){}}function fa(a){var b=0;return ha(function(){return b"+this.innerHTML+""},set:function(a){if(this.parentNode){J.body.innerHTML=a;for(a=this.ownerDocument.createDocumentFragment();J.body.firstChild;)l.call(a, +J.body.firstChild);m.call(this.parentNode,a,this)}else throw Error("Failed to set the 'outerHTML' property on 'Element': This element has no parent node.");},configurable:!0})};na(a.prototype);aa(a.prototype);a.J=function(c){c=b(c,"template");for(var d=0,e=c.length,f;d]/g, +U=function(a){switch(a){case "&":return"&";case "<":return"<";case ">":return">";case "\u00a0":return" "}}}if(c||eb){a.ca=function(a,b){var c=f.call(a,!1);this.D&&this.D(c);b&&(l.call(c.content,f.call(a.content,!0)),fb(c.content,a.content));return c};var fb=function(c,d){if(d.querySelectorAll&&(d=b(d,"template"),0!==d.length)){c=b(c,"template");for(var e=0,f=c.length,h,g;e]*)(rel=['|"]?stylesheet['|"]?[^>]*>)/g,x={La:function(a,b){a.href&&a.setAttribute("href",x.Y(a.getAttribute("href"),b));a.src&&a.setAttribute("src",x.Y(a.getAttribute("src"),b));if("style"===a.localName){var c=x.ta(a.textContent,b,Ca);a.textContent=x.ta(c,b,Da)}},ta:function(a,b,c){return a.replace(c, +function(a,c,d,e){a=d.replace(/["']/g,"");b&&(a=x.Y(a,b));return c+"'"+a+"'"+e})},Y:function(a,b){if(void 0===x.ba){x.ba=!1;try{var c=new URL("b","http://a");c.pathname="c%20d";x.ba="http://a/c%20d"===c.href}catch(Lg){}}if(x.ba)return(new URL(a,b)).href;c=x.Ba;c||(c=document.implementation.createHTMLDocument("temp"),x.Ba=c,c.ma=c.createElement("base"),c.head.appendChild(c.ma),c.la=c.createElement("a"));c.ma.href=b;c.la.href=a;return c.la.href||a}},na={async:!0,load:function(a,b,c){if(a)if(a.match(/^data:/)){a= +a.split(",");var d=a[1];d=-1e.status?b(d,a):c(d)};e.send()}else c("error: href must be specified")}},aa=/Trident/.test(navigator.userAgent)||/Edge\/\d./i.test(navigator.userAgent); +k.prototype.loadImports=function(a){var b=this;a=m(a,"link[rel=import]");n(a,function(a){return b.s(a)})};k.prototype.s=function(a){var b=this,c=a.href;if(void 0!==this.a[c]){var d=this.a[c];d&&d.__loaded&&(a.__import=d,this.h(a))}else this.b++,this.a[c]="pending",na.load(c,function(a,d){a=b.Sa(a,d||c);b.a[c]=a;b.b--;b.loadImports(a);b.L()},function(){b.a[c]=null;b.b--;b.L()})};k.prototype.Sa=function(a,b){if(!a)return document.createDocumentFragment();aa&&(a=a.replace(Ea,function(a,b,c){return-1=== +a.indexOf("type=")?b+" type=import-disable "+c:a}));var c=document.createElement("template");c.innerHTML=a;if(c.content)a=c.content,l(a);else for(a=document.createDocumentFragment();c.firstChild;)a.appendChild(c.firstChild);if(c=a.querySelector("base"))b=x.Y(c.getAttribute("href"),b),c.removeAttribute("href");c=m(a,'link[rel=import],link[rel=stylesheet][href][type=import-disable],style:not([type]),link[rel=stylesheet][href]:not([type]),script:not([type]),script[type="application/javascript"],script[type="text/javascript"]'); +var d=0;n(c,function(a){h(a);x.La(a,b);a.setAttribute("import-dependency","");"script"===a.localName&&!a.src&&a.textContent&&(a.setAttribute("src","data:text/javascript;charset=utf-8,"+encodeURIComponent(a.textContent+("\n//# sourceURL="+b+(d?"-"+d:"")+".js\n"))),a.textContent="",d++)});return a};k.prototype.L=function(){var a=this;if(!this.b){this.c.disconnect();this.flatten(document);var b=!1,c=!1,d=function(){c&&b&&(a.loadImports(document),a.b||(a.c.observe(document.head,{childList:!0,subtree:!0}), +a.Pa()))};this.Ua(function(){c=!0;d()});this.Ta(function(){b=!0;d()})}};k.prototype.flatten=function(a){var b=this;a=m(a,"link[rel=import]");n(a,function(a){var c=b.a[a.href];(a.__import=c)&&c.nodeType===Node.DOCUMENT_FRAGMENT_NODE&&(b.a[a.href]=a,a.readyState="loading",a.__import=a,b.flatten(c),a.appendChild(c))})};k.prototype.Ta=function(a){function b(e){if(e]/g;function hc(a){switch(a){case "&":return"&";case "<":return"<";case ">":return">";case '"':return""";case "\u00a0":return" "}}function ic(a){for(var b={},c=0;c";break a;case Node.TEXT_NODE:g=g.data;g=k&&kc[k.localName]?g:g.replace(gc,hc);break a;case Node.COMMENT_NODE:g="\x3c!--"+g.data+"--\x3e";break a;default:throw window.console.error(g), +Error("not implemented");}}c+=g}return c};var B={},D=document.createTreeWalker(document,NodeFilter.SHOW_ALL,null,!1),E=document.createTreeWalker(document,NodeFilter.SHOW_ELEMENT,null,!1);function mc(a){var b=[];D.currentNode=a;for(a=D.firstChild();a;)b.push(a),a=D.nextSibling();return b}B.parentNode=function(a){D.currentNode=a;return D.parentNode()};B.firstChild=function(a){D.currentNode=a;return D.firstChild()};B.lastChild=function(a){D.currentNode=a;return D.lastChild()};B.previousSibling=function(a){D.currentNode=a;return D.previousSibling()}; +B.nextSibling=function(a){D.currentNode=a;return D.nextSibling()};B.childNodes=mc;B.parentElement=function(a){E.currentNode=a;return E.parentNode()};B.firstElementChild=function(a){E.currentNode=a;return E.firstChild()};B.lastElementChild=function(a){E.currentNode=a;return E.lastChild()};B.previousElementSibling=function(a){E.currentNode=a;return E.previousSibling()};B.nextElementSibling=function(a){E.currentNode=a;return E.nextSibling()}; +B.children=function(a){var b=[];E.currentNode=a;for(a=E.firstChild();a;)b.push(a),a=E.nextSibling();return b};B.innerHTML=function(a){return lc(a,function(a){return mc(a)})};B.textContent=function(a){switch(a.nodeType){case Node.ELEMENT_NODE:case Node.DOCUMENT_FRAGMENT_NODE:a=document.createTreeWalker(a,NodeFilter.SHOW_TEXT,null,!1);for(var b="",c;c=a.nextNode();)b+=c.nodeValue;return b;default:return a.nodeValue}};var nc=Object.getOwnPropertyDescriptor(Element.prototype,"innerHTML")||Object.getOwnPropertyDescriptor(HTMLElement.prototype,"innerHTML"),oc=document.implementation.createHTMLDocument("inert"),pc=Object.getOwnPropertyDescriptor(Document.prototype,"activeElement"),qc={parentElement:{get:function(){var a=this.__shady&&this.__shady.parentNode;a&&a.nodeType!==Node.ELEMENT_NODE&&(a=null);return void 0!==a?a:B.parentElement(this)},configurable:!0},parentNode:{get:function(){var a=this.__shady&&this.__shady.parentNode; +return void 0!==a?a:B.parentNode(this)},configurable:!0},nextSibling:{get:function(){var a=this.__shady&&this.__shady.nextSibling;return void 0!==a?a:B.nextSibling(this)},configurable:!0},previousSibling:{get:function(){var a=this.__shady&&this.__shady.previousSibling;return void 0!==a?a:B.previousSibling(this)},configurable:!0},className:{get:function(){return this.getAttribute("class")||""},set:function(a){this.setAttribute("class",a)},configurable:!0},nextElementSibling:{get:function(){if(this.__shady&& +void 0!==this.__shady.nextSibling){for(var a=this.nextSibling;a&&a.nodeType!==Node.ELEMENT_NODE;)a=a.nextSibling;return a}return B.nextElementSibling(this)},configurable:!0},previousElementSibling:{get:function(){if(this.__shady&&void 0!==this.__shady.previousSibling){for(var a=this.previousSibling;a&&a.nodeType!==Node.ELEMENT_NODE;)a=a.previousSibling;return a}return B.previousElementSibling(this)},configurable:!0}},rc={childNodes:{get:function(){if(vb(this)){if(!this.__shady.childNodes){this.__shady.childNodes= +[];for(var a=this.firstChild;a;a=a.nextSibling)this.__shady.childNodes.push(a)}var b=this.__shady.childNodes}else b=B.childNodes(this);b.item=function(a){return b[a]};return b},configurable:!0},childElementCount:{get:function(){return this.children.length},configurable:!0},firstChild:{get:function(){var a=this.__shady&&this.__shady.firstChild;return void 0!==a?a:B.firstChild(this)},configurable:!0},lastChild:{get:function(){var a=this.__shady&&this.__shady.lastChild;return void 0!==a?a:B.lastChild(this)}, +configurable:!0},textContent:{get:function(){if(vb(this)){for(var a=[],b=0,c=this.childNodes,d;d=c[b];b++)d.nodeType!==Node.COMMENT_NODE&&a.push(d.textContent);return a.join("")}return B.textContent(this)},set:function(a){if("undefined"===typeof a||null===a)a="";switch(this.nodeType){case Node.ELEMENT_NODE:case Node.DOCUMENT_FRAGMENT_NODE:for(;this.firstChild;)this.removeChild(this.firstChild);(0b.__shady.assignedNodes.length&&(b.__shady.ia=!0)}b.__shady.ia&&(b.__shady.ia=!1,od(this,b))}a=this.o;b=[];for(c=0;cb.indexOf(d))||b.push(d);for(a=0;a "+b}))}a=a.replace(Df,function(a,b,c){return'[dir="'+c+'"] '+b+", "+b+'[dir="'+c+'"]'});return{value:a,Ka:b,stop:f}}function Bf(a,b){a=a.split(Ef);a[0]+=b;return a.join(Ef)} +function Af(a,b){var c=a.match(Ff);return(c=c&&c[2].trim()||"")?c[0].match(Gf)?a.replace(Ff,function(a,c,f){return b+f}):c.split(Gf)[0]===b?c:Hf:a.replace(wf,b)}function If(a){a.selector===Jf&&(a.selector="html")}hf.prototype.c=function(a){return a.match(zf)?this.b(a,Kf):Bf(a.trim(),Kf)};q.Object.defineProperties(hf.prototype,{a:{configurable:!0,enumerable:!0,get:function(){return"style-scope"}}}); +var uf=/:(nth[-\w]+)\(([^)]+)\)/,Kf=":not(.style-scope)",sf=",",xf=/(^|[\s>+~]+)((?:\[.+?\]|[^\s>+~=[])+)/g,Gf=/[[.:#*]/,wf=":host",Jf=":root",zf="::slotted",vf=new RegExp("^("+zf+")"),Ff=/(:host)(?:\(((?:\([^)(]*\)|[^)(]*)+?)\))/,Cf=/(?:::slotted)(?:\(((?:\([^)(]*\)|[^)(]*)+?)\))/,Df=/(.*):dir\((?:(ltr|rtl))\)/,qf=".",Ef=":",mf="class",Hf="should_not_match",W=new hf;function Lf(a,b,c,d){this.w=a||null;this.b=b||null;this.ja=c||[];this.G=null;this.P=d||"";this.a=this.u=this.B=null}function X(a){return a?a.__styleInfo:null}function Mf(a,b){return a.__styleInfo=b}Lf.prototype.c=function(){return this.w};Lf.prototype._getStyleRules=Lf.prototype.c;var Nf,Of=window.Element.prototype;Nf=Of.matches||Of.matchesSelector||Of.mozMatchesSelector||Of.msMatchesSelector||Of.oMatchesSelector||Of.webkitMatchesSelector;var Pf=navigator.userAgent.match("Trident");function Qf(){}function Rf(a){var b={},c=[],d=0;af(a,function(a){Sf(a);a.index=d++;a=a.i.cssText;for(var c;c=Ve.exec(a);){var e=c[1];":"!==c[2]&&(b[e]=!0)}},function(a){c.push(a)});a.b=c;a=[];for(var e in b)a.push(e);return a} +function Sf(a){if(!a.i){var b={},c={};Tf(a,c)&&(b.v=c,a.rules=null);b.cssText=a.parsedCssText.replace(Ye,"").replace(Te,"");a.i=b}}function Tf(a,b){var c=a.i;if(c){if(c.v)return Object.assign(b,c.v),!0}else{c=a.parsedCssText;for(var d;a=Te.exec(c);){d=(a[2]||a[3]).trim();if("inherit"!==d||"unset"!==d)b[a[1].trim()]=d;d=!0}return d}} +function Uf(a,b,c){b&&(b=0<=b.indexOf(";")?Vf(a,b,c):ff(b,function(b,e,f,h){if(!e)return b+h;(e=Uf(a,c[e],c))&&"initial"!==e?"apply-shim-inherit"===e&&(e="inherit"):e=Uf(a,c[f]||f,c)||f;return b+(e||"")+h}));return b&&b.trim()||""} +function Vf(a,b,c){b=b.split(";");for(var d=0,e,f;d *"===f||"html"===f,g=0===f.indexOf(":host")&&!h;"shady"===c&&(h=f===e+" > *."+e||-1!==f.indexOf("html"),g=!h&&0===f.indexOf(e));"shadow"===c&&(h=":host > *"===f||"html"===f,g=g&&!h);if(h||g)c=e,g&&(R&&!b.m&&(b.m=rf(W,b,W.b,a?qf+a:"",e)),c=b.m||e),d({Xa:c,Qa:g,hb:h})}} +function Yf(a,b){var c={},d={},e=b&&b.__cssBuild;af(b,function(b){Xf(a,b,e,function(e){Nf.call(a.b||a,e.Xa)&&(e.Qa?Tf(b,c):Tf(b,d))})},null,!0);return{Wa:d,Oa:c}} +function Zf(a,b,c,d){var e=V(b),f=pf(e.is,e.P),h=new RegExp("(?:^|[^.#[:])"+(b.extends?"\\"+f.slice(0,-1)+"\\]":f)+"($|[.:[\\s>+~])");e=X(b).w;var g=$f(e,d);return nf(b,e,function(b){var e="";b.i||Sf(b);b.i.cssText&&(e=Vf(a,b.i.cssText,c));b.cssText=e;if(!R&&!cf(b)&&b.cssText){var k=e=b.cssText;null==b.ra&&(b.ra=We.test(e));if(b.ra)if(null==b.W){b.W=[];for(var n in g)k=g[n],k=k(e),e!==k&&(e=k,b.W.push(n))}else{for(n=0;n=l._useCount&&l.parentNode&&l.parentNode.removeChild(l));R?f.a?(f.a.textContent=e,d=f.a):e&&(d=df(e,g,a.shadowRoot,f.b)):d?d.parentNode|| +(Pf&&-1.\n var capturedCloneNode = Node.prototype.cloneNode;\n var capturedCreateElement = Document.prototype.createElement;\n var capturedImportNode = Document.prototype.importNode;\n var capturedRemoveChild = Node.prototype.removeChild;\n var capturedAppendChild = Node.prototype.appendChild;\n var capturedReplaceChild = Node.prototype.replaceChild;\n var capturedParseFromString = DOMParser.prototype.parseFromString;\n var capturedHTMLElementInnerHTML = Object.getOwnPropertyDescriptor(window.HTMLElement.prototype, 'innerHTML');\n var capturedChildNodes = Object.getOwnPropertyDescriptor(window.Node.prototype, 'childNodes');\n\n var elementQuerySelectorAll = Element.prototype.querySelectorAll;\n var docQuerySelectorAll = Document.prototype.querySelectorAll;\n var fragQuerySelectorAll = DocumentFragment.prototype.querySelectorAll;\n\n var scriptSelector = 'script:not([type]),script[type=\"application/javascript\"],script[type=\"text/javascript\"]';\n\n function QSA(node, selector) {\n // IE 11 throws a SyntaxError with `scriptSelector` if the node has no children due to the `:not([type])` syntax\n if (!node.childNodes.length) {\n return [];\n }\n switch (node.nodeType) {\n case Node.DOCUMENT_NODE:\n return docQuerySelectorAll.call(node, selector);\n case Node.DOCUMENT_FRAGMENT_NODE:\n return fragQuerySelectorAll.call(node, selector);\n default:\n return elementQuerySelectorAll.call(node, selector);\n }\n }\n\n // returns true if nested templates cannot be cloned (they cannot be on\n // some impl's like Safari 8 and Edge)\n // OR if cloning a document fragment does not result in a document fragment\n var needsCloning = (function() {\n if (!needsTemplate) {\n var t = document.createElement('template');\n var t2 = document.createElement('template');\n t2.content.appendChild(document.createElement('div'));\n t.content.appendChild(t2);\n var clone = t.cloneNode(true);\n return (clone.content.childNodes.length === 0 || clone.content.firstChild.content.childNodes.length === 0\n || brokenDocFragment);\n }\n })();\n\n var TEMPLATE_TAG = 'template';\n var PolyfilledHTMLTemplateElement = function() {};\n\n if (needsTemplate) {\n\n var contentDoc = document.implementation.createHTMLDocument('template');\n var canDecorate = true;\n\n var templateStyle = document.createElement('style');\n templateStyle.textContent = TEMPLATE_TAG + '{display:none;}';\n\n var head = document.head;\n head.insertBefore(templateStyle, head.firstElementChild);\n\n /**\n Provides a minimal shim for the