From b36da76c2203248e246bbb7d6c09125a31a8aaca Mon Sep 17 00:00:00 2001 From: leduc Date: Mon, 11 Oct 2021 09:40:24 +0200 Subject: [PATCH] New features --- README.md | 57 +++++- libs/__pycache__/midix.cpython-38.pyc | Bin 21514 -> 25192 bytes libs/midix.py | 264 ++++++++++++++++++++++++-- libs/tools.py | 194 +++++++++++++++++++ miredis.json | 114 ++++++++++- miredis.py | 20 +- 6 files changed, 602 insertions(+), 47 deletions(-) create mode 100755 libs/tools.py diff --git a/README.md b/README.md index 4be92c0..1c1bb38 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,11 @@ -# Forward Midi events to redis/OSC +# Forward Midi events to redis and OSC + +Miredis hooks to all midi devices connected and listen for events. All events are forwarded to your redis server and your OSC server. + +Miredis can optionnaly hook to opensourced Link protocol (200+ music & videos apps) -> "/beat" & "/bpm". + +Custom events can be triggered with reranged values from one or two CC. Szz custom action types -Miredis hooks to all midi devices connected and listen for events. -Miredis can optionnaly hook to opensourced Link protocol (200+ music & videos apps) -> "/beat" & "/bpm" -All events are forwarded to your redis server and your OSC server. ![Clitools](https://www.teamlaser.fr/images/miredispad.png) @@ -19,16 +22,28 @@ To enable Link : python3 miredis.py -link -(for cheap midi interface midisport/midiman from audio on Linux : apt-get install midisport-firmware) +(for cheap midi interface midisport/midiman support on Linux : apt-get install midisport-firmware) ## New Features -- Client example : midicontrol.py -- Added verbose mode -v -- Added redis subscribe events -- Added Clitools program selection mode for Launchpads -- Added custom redis 'key event', to be more semantic/hardware agnostic with Configuration file : miredis.json. i.e "/feedback/1/114/value" is generated each time a CC message on channel 1, CC 114 is recevied. +- Midi timecode thks to https://github.com/jeffmikels/timecode_tools.git +- CC values can be reranged, see 'custom action type' +- LJ2 custom actions for laser settings (midichannel -> laser number) +- Midi controlled python example : midicontrol.py +- Verbose mode -v +- Redis subscribe events +- Clitools program selection mode for Launchpads +- Cstom redis 'key event'. To be more semantic/hardware agnostic with configuration file miredis.json. i.e "/feedback/1/114/value" is generated each time a CC message on channel 1/ CC 114 is received. +## Custom action types (see miredis.json) + +Process given CC(s), create and send result with a "name" event. Builtin types with mandatory parameters : + +- 0 : note number (name, type, note) +- 7 : rerange 7 bits midi cc (0-127) to a lowend-highend number (name, type, CC, low end, high end, default value) +- 8 : rerange 7 bits midi cc (0-127) to 8 bits number (0-255) (name, type, CC, default value) +- 140 : rerange one midi cc (0-127) to 14 bits number (0-16383) (name, type, CC, default value) +- 14 : 2 cc (14 bits value) reranged to a lowend-highend number (name, type, highCC, lowCC, low end, high end, default value) ## OSC @@ -76,3 +91,25 @@ custom ones "/midi/cc/midichannel/ccnumber/ccvalue" customs one like "/feedback/1/114/value" + +## Custom events examples : LJ2 settings for midi controller + +Buttons : + +- grid/lasernumber note on 33 +- black/lasernumber note on 35 +- swap/X/lasernumber note on 24 +- swap/Y/lasernumber note on 26 + +Sliders : + +- kpps/lasernumber cc 0 cc 1 +- angle/lasernumber cc 2 (0-360) +- scale/X/lasernumber value cc 4 (0-200) +- scale/Y/lasernumber value cc 5 (0-200) +- red/lasernumber cc 8 (0-255) +- green/lasernumber cc 9 (0-255) +- blue/lasernumber cc 10 (0-255) +- intensity/lasernumber cc 11 (0-255) +- loffset/X/lasernumber cc 12 cc 13 (-27000, 27000) +- loffset/Y/lasernumber cc 14 cc 15 (-27000, 27000) diff --git a/libs/__pycache__/midix.cpython-38.pyc b/libs/__pycache__/midix.cpython-38.pyc index 10a23d3ebc05710da378074be7dc12c9e1eb5a6f..73ff17e3d203beb1bc25a71324ee5a67771ac647 100644 GIT binary patch literal 25192 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 delta 9614 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 62 kick.wav if MidiNote < 63 and MidiVel >0: @@ -360,6 +368,7 @@ def MidinProcess(inqueue, portname): else: MidiChannel = msg[0]-128 MidiNote = msg[1] + print() print("NOTE_off channel :", MidiChannel, "note :", MidiNote) NoteOff(MidiNote, "pads" , midichannel=MidiChannel) @@ -376,10 +385,10 @@ def MidinProcess(inqueue, portname): if CONTROLLER_CHANGE -1 < msg[0] < 192: MidiChannel = msg[0]-175 - + print() cc(MidiChannel, msg[1], msg[2], "pads" ) #print("channel", MidiChannel, "CC :", msg[1], msg[2]) - print("CC channel : "+str(msg[0]-175-1)+" CC :"+str(msg[1])+" value : "+str(msg[2])) + print("CC : channel : "+str(msg[0]-175-1)+" CC : "+str(msg[1])+" value : "+str(msg[2])) toKeyevent("/midi/cc/"+str(MidiChannel)+"/"+str(msg[1])+"/"+str(msg[2])) # redis key : "/midi/cc/midichannel/ccnumber" value : "ccvalue" @@ -393,11 +402,111 @@ def MidinProcess(inqueue, portname): for param in conf['params']: - if MidiChannel == param["chanIN"] and param["CC"] == msg[1]: - #print(param["name"]+"/"+ str(msg[1])+"/"+str(msg[2])) - SendOSC(param["name"],[msg[0]-175-1, msg[1], msg[2]]) - toKeyevent(param["name"]+"/"+ str(MidiChannel)+"/"+str(msg[1])+"/"+str(msg[2])) - toKey(param["name"],str(MidiChannel)+"/"+str(msg[1])+"/"+str(msg[2])) + #print() + #print('custom action',param["name"], param) + # print('type', param["type"] ) + islj2 = param["name"].find('lasernumber') + # print('lasernumber position', islj2) + + + # 0-127 parameter + if param["type"] == 7: + + # LJ2 parameter : replace lasernumber with channel number - 1 + if islj2 != -1: + + if param["CC"] == msg[1]: + + print('lj2 7 bits custom action') + tolj = param["name"].replace('lasernumber',str(MidiChannel-1)) + SendOSC(tolj, [midifactor(ccnumber = msg[1], channel = MidiChannel, low =param["low"], high=param["high"], default =1)]) + + print('tolj', tolj, midifactor(ccnumber = msg[1], channel = MidiChannel, low =param["low"], high=param["high"], default =1) ) + #toKeyevent(param["name"]+"/"+ str(MidiChannel)+"/"+str(msg[1])+"/"+str(msg[2]*2)) + #toKey(param["name"], str(MidiChannel)+"/"+str(msg[1])+"/"+str(msg[2]*2)) + #tolj = param["name"].replace('lasernumber',str(MidiChannel)) + #print("LJ2 cc :", tolj, ["1"]) + #SendOSC(tolj, ['1']) + + # Not LJ2 + elif MidiChannel == param["chanIN"] and param["CC"] == msg[1]: + #print(param["name"]+"/"+ str(msg[1])+"/"+str(msg[2])) + SendOSC(param["name"], [msg[0]-175-1, msg[1], msg[2]]) + toKeyevent(param["name"]+"/"+ str(MidiChannel)+"/"+str(msg[1])+"/"+str(msg[2])) + toKey(param["name"], str(MidiChannel)+"/"+str(msg[1])+"/"+str(msg[2])) + + + # convert 7 bits midi cc to 8 bits and send to "name" + if param["type"] == 8: + + # LJ2 parameter : replace lasernumber with channel number - 1 + if islj2 != -1: + + if param["CC"] == msg[1]: + print('lj2 8 bits custom action') + tolj = param["name"].replace('lasernumber',str(MidiChannel-1)) + SendOSC(tolj, [msg[2]*2]) + print('tolj', tolj, msg[2]*2 ) + #toKeyevent(param["name"]+"/"+ str(MidiChannel)+"/"+str(msg[1])+"/"+str(msg[2]*2)) + #toKey(param["name"], str(MidiChannel)+"/"+str(msg[1])+"/"+str(msg[2]*2)) + + # Not LJ2 + elif MidiChannel == param["chanIN"] and param["CC"] == msg[1]: + SendOSC(param["name"], [msg[0]-175-1, msg[1], msg[2]*2]) + toKeyevent(param["name"]+"/"+ str(MidiChannel)+"/"+str(msg[1])+"/"+str(msg[2]*2)) + toKey(param["name"], str(MidiChannel)+"/"+str(msg[1])+"/"+str(msg[2]*2)) + + + # reranged 2 cc -> 14 bits parameters + if param["type"] == 14: + + # LJ2 parameter : replace lasernumber with channel number - 1 + if islj2 != -1: + if param["lowCC"] == msg[1] or param["highCC"] == msg[1]: + + print('lj2 14 bits reranged custom action') + tolj = param["name"].replace('lasernumber',str(MidiChannel-1)) + print('highcc',str(param["highCC"]),r.get("/midi/cc/"+str(MidiChannel)+"/"+str(param["highCC"]))) + print('lowcc',str(param["lowCC"]),r.get("/midi/cc/"+str(MidiChannel)+"/"+str(param["lowCC"]))) + + SendOSC(tolj, [str(int(midifactor14(highcc=param["highCC"], lowcc=param["lowCC"], channel=MidiChannel, low=param["low"], high=param["high"], default=param["default"])))]) + print('tolj', tolj, int(midifactor14(highcc=param["highCC"], lowcc=param["lowCC"], channel=MidiChannel, low=param["low"], high=param["high"], default=param["default"])) ) + #toKeyevent(param["name"]+"/"+ str(MidiChannel)+"/"+str(msg[1])+"/"+str(msg[2]*2)) + #toKey(param["name"], str(MidiChannel)+"/"+str(msg[1])+"/"+str(msg[2]*2)) + + # Not LJ2 + elif MidiChannel == param["chanIN"] and (param["lowCC"] == msg[1] or param["highCC"] == msg[1]): + #print(param["name"]+"/"+ str(msg[1])+"/"+str(msg[2])) + + highcc = r.get("/midi/cc/"+str(MidiChannel)+"/"+str(msg[1])) + print('highcc',highcc) + midifactor14(highcc, lowcc, ccnumber, channel, param["low"], param["high"], default) + + SendOSC(param["name"], [msg[0]-175-1, msg[1], msg[2]]) + toKeyevent(param["name"]+"/"+ str(MidiChannel)+"/"+str(msg[1])+"/"+str(msg[2])) + toKey(param["name"],str(MidiChannel)+"/"+str(msg[1])+"/"+str(msg[2])) + + + # 14 bits parameters with one CC + if param["type"] == 140: + + # LJ2 parameter : replace lasernumber with channel number - 1 + if islj2 != -1: + if param["CC"] == msg[1]: + print('lj2 140 bits custom action') + tolj = param["name"].replace('lasernumber',str(MidiChannel-1)) + print('no program here') + + # Not LJ2 + elif MidiChannel == param["chanIN"] and param["CC"] == msg[1]: + #print(param["name"]+"/"+ str(msg[1])+"/"+str(msg[2])) + SendOSC(param["name"], [msg[0]-175-1, msg[1], msg[2]]) + toKeyevent(param["name"]+"/"+ str(MidiChannel)+"/"+str(msg[1])+"/"+str(msg[2])) + toKey(param["name"], str(MidiChannel)+"/"+str(msg[1])+"/"+str(msg[2])) + + midifactor140(ccnumber, channel=MidiChannel, low=param["low"], high=param["high"], default = param["default"]) + #midifactor14(highcc, lowcc, channel = cchannel, low =0, high=16383, default =1) + if msg[0] == TIMING_CLOCK: now = time.time() @@ -427,6 +536,7 @@ def MidinProcess(inqueue, portname): if msg[0] in (SONG_CONTINUE, SONG_START): running = True + #print("START/CONTINUE received.") #print("Midi in process send /aurora/start") @@ -443,7 +553,27 @@ def MidinProcess(inqueue, portname): # OSC : /midi/start SendOSC("/midi/stop",[]) print("osc : /midi/stop") - print() + + + if msg[0] == SYSTEM_EXCLUSIVE: + print('sysex', msg) + # check to see if this is a timecode frame + if len(msg) == 8 and msg[0:4] == [127,127,1,1]: + data = message.data[4:] + tc = tools.mtc_decode(data) + print('FF:',tc) + + + # https://en.wikipedia.org/wiki/MIDI_timecode + if msg[0] == MIDI_TIME_CODE: + frame_type = (msg[1] >> 4) & 0xF + quarter_frames[frame_type] = msg[1] & 15 + if frame_type == 7: + tc = tools.mtc_decode_quarter_frames(quarter_frames) + print('QF:',tc) + SendTCview("/MIDIQF",[str(tc)]) + + ''' # other midi message if msg[0] != NOTE_OFF and msg[0] != NOTE_ON and msg[0] != CONTROLLER_CHANGE: @@ -821,6 +951,59 @@ def NoteOn(note, velocity, mididest): if midiname[port].find(mididest) == 0: midiport[port].send_message([NOTE_ON, note, velocity]) ''' + + +# 1 CC -> 7 bits number : 0-127 +# rerange end value between low high +# midifactor(ccnumber, channel, low, high, default) +def midifactor(ccnumber, channel = 0, low =0, high=127, default =1): + + ccvalue = r.get('/midi/cc/'+str(channel)+'/'+str(ccnumber)) + + if ccvalue is not None: + return rerange(int(ccvalue), 0, 127, low, high) + + else: + print('Default value returned. No midi value in redis for channel', channel, 'CC', ccnumber) + return default + +# 2 CC -> 14 bits number : 0 - 16383 +# rerange end value between low high +# midifactor14(highcc, lowcc, ccnumber, channel, low, high, default) +def midifactor14(highcc, lowcc, channel = 0, low =0, high=16383, default =1): + + lowvalue = r.get('/midi/cc/'+str(channel)+'/'+str(lowcc)) + highvalue = r.get('/midi/cc/'+str(channel)+'/'+str(highcc)) + + if lowvalue is not None and highcc is not None: + return rerange((int(highvalue) << 7)+ int(lowvalue), 0, 16383, low, high) + + else: + print('Default value returned. No midi value in redis for channel', channel, 'CCs', highvalue, lowvalue) + return default + + +# 1 CC -> 14 bits number : 0 - 16383 +# rerange end value between low high +# set low cc (=Byte) to 0 +# midifactor140(ccnumber, channel, low, high, default) +def midifactor140(highcc, channel = 0, low =0, high=16383, default =1): + + highvalue = r.get('/midi/cc/'+str(channel)+'/'+str(highcc)) + + if highcc is not None: + return rerange((int(highvalue) << 7), 0, 16383, low, high) + + else: + print('Default value returned. No midi value in redis for channel', channel, 'CC', ccnumber) + return default + + + +def rerange(s,a1,a2,b1,b2): + return b1 + ((s - a1) * (b2 - b1) / (a2 - a1)) + + # # launchpad # @@ -1026,10 +1209,10 @@ def loadConf(): f = open(ConFile,"r") s = f.read() conf = json.loads(s) - print("params", len(conf['params'])) + print(len(conf['params']), "custom actions") nbparam = len(conf['params']) - print(conf) + #print(conf) return True except Exception as e: print("_loadPlaylist error when loading '{}':{}".format(ConFile,e)) @@ -1040,4 +1223,45 @@ def check(): InConfig() - +''' + +from rtmidi.midiconstants import (CONTROL_CHANGE, DATA_DECREMENT, + DATA_ENTRY_LSB, DATA_ENTRY_MSB, + DATA_INCREMENT, RPN_LSB, RPN_MSB) +from rtmidi.midiutil import open_midiinput + + +class RPNDecoder: + def __init__(self, channel=1): + self.channel = (channel - 1) & 0xF + self.rpn = 0 + self.values = defaultdict(int) + self.last_changed = None + + def __call__(self, event, data=None): + msg, deltatime = event + + # event type = upper four bits of first byte + if msg[0] == (CONTROL_CHANGE | self.channel): + cc, value = msg[1], msg[2] + + if cc == RPN_LSB: + self.rpn = (self.rpn >> 7) * 128 + value + elif cc == RPN_MSB: + self.rpn = value * 128 + (self.rpn & 0x7F) + elif cc == DATA_INCREMENT: + self.set_rpn(self.rpn, min(2 ** 14, self.values[self.rpn] + 1)) + elif cc == DATA_DECREMENT: + self.set_rpn(self.rpn, max(0, self.values[self.rpn] - 1)) + elif cc == DATA_ENTRY_LSB: + self.set_rpn(self.rpn, + (self.values[self.rpn] >> 7) * 128 + value) + elif cc == DATA_ENTRY_MSB: + self.set_rpn(self.rpn, + value * 128 + (self.values[self.rpn] & 0x7F)) + + def set_rpn(self, rpn, value): + self.values[rpn] = value + self.last_changed = rpn + +''' diff --git a/libs/tools.py b/libs/tools.py new file mode 100755 index 0000000..444dddc --- /dev/null +++ b/libs/tools.py @@ -0,0 +1,194 @@ +#!/usr/bin/env python3 +from timecode import Timecode + +def bitstring_to_bytes(s, bytecount=1, byteorder='big'): + return int(s, 2).to_bytes(bytecount, byteorder) + +# binary big-endian +def bbe(n, bits=8): + # terminal condition + retval = '' + if n == 0: + retval = '0' + else: + retval = bbe(n//2, None) + str(n%2) + if bits is None: + return retval + else: + return (('0'*bits) + retval)[-bits:] + + +# binary, little-endian +def ble(n, bits=8): + # terminal condition + retval = '' + if n == 0: + retval = '0' + else: + retval = str(n%2) + ble(n//2, None) + if bits is None: + return retval + else: + return (retval + ('0'*bits))[0:bits] + +def cint(n, bytecount=2): + return int(n).to_bytes(bytecount, byteorder='little') + +def units_tens(n): + return n % 10, int(n/10) + +## +## LTC functions +## +# GENERATE BINARY-CODED DATA FOR LTC +# ACCORDING TO https://en.wikipedia.org/wiki/Linear_timecode +# everything is encoded little endian +# so to encode the number 3 with four bits, we have 1100 +def ltc_encode(timecode, as_string=False): + LTC = '' + HLP = '' + hrs, mins, secs, frs = timecode.frames_to_tc(timecode.frames) + frame_units, frame_tens = units_tens(frs) + secs_units, secs_tens = units_tens(secs) + mins_units, mins_tens = units_tens(mins) + hrs_units, hrs_tens = units_tens(hrs) + + #frames units / user bits field 1 / frames tens + LTC += ble(frame_units,4) + '0000' + ble(frame_tens,2) + HLP += '---{u}____-{t}'.format(u=frame_units, t=frame_tens) + + #drop frame / color frame / user bits field 2 + LTC += '00'+'0000' + HLP += '__'+'____' + + #secs units / user bits field 3 / secs tens + LTC += ble(secs_units,4) + '0000' + ble(secs_tens,3) + HLP += '---{u}____--{t}'.format(u=secs_units, t=secs_tens) + + # bit 27 flag / user bits field 4 + LTC += '0' + '0000' + HLP += '_' + '____' + + #mins units / user bits field 5 / mins tens + LTC += ble(mins_units,4) + '0000' + ble(mins_tens,3) + HLP += '---{u}____--{t}'.format(u=mins_units, t=mins_tens) + + # bit 43 flag / user bits field 6 + LTC += '0' + '0000' + HLP += '_' + '____' + + #hrs units / user bits field 7 / hrs tens + LTC += ble(hrs_units,4) + '0000' + ble(hrs_tens,2) + HLP += '---{u}____--{t}'.format(u=hrs_units, t=hrs_tens) + + # bit 58 clock flag / bit 59 flag / user bits field 8 + LTC += '0' + '0' + '0000' + HLP += '_' + '_' + '____' + + # sync word + LTC += '0011111111111101' + HLP += '################' + if as_string: + return LTC + else: + return bitstring_to_bytes(LTC, bytecount=10) + + +## +## MTC functions +## +def mtc_encode(timecode, as_string=False): + # MIDI bytes are little-endian + # Byte 0 + # 0rrhhhhh: Rate (0–3) and hour (0–23). + # rr = 000: 24 frames/s + # rr = 001: 25 frames/s + # rr = 010: 29.97 frames/s (SMPTE drop-frame timecode) + # rr = 011: 30 frames/s + # Byte 1 + # 00mmmmmm: Minute (0–59) + # Byte 2 + # 00ssssss: Second (0–59) + # Byte 3 + # 000fffff: Frame (0–29, or less at lower frame rates) + hrs, mins, secs, frs = timecode.frames_to_tc(timecode.frames) + framerate = timecode.framerate + rateflags = { + '24': 0, + '25': 1, + '29.97': 2, + '30': 3 + } + rateflag = rateflags[framerate] * 32 # multiply by 32, because the rate flag starts at bit 6 + + # print('{:8} {:8} {:8} {:8}'.format(hrs, mins, secs, frs)) + if as_string: + b0 = bbe(rateflag + hrs, 8) + b1 = bbe(mins) + b2 = bbe(secs) + b3 = bbe(frs) + # print('{:8} {:8} {:8} {:8}'.format(b0, b1, b2, b3)) + return b0+b1+b2+b3 + else: + b = bytearray([rateflag + hrs, mins, secs, frs]) + # debug_string = ' 0x{:02} 0x{:02} 0x{:02} 0x{:02}' + # debug_array = [ord(b[0]), ord(b[1]), ord(b[2]), ord(b[3])] + # print(debug_string.format(debug_array)) + return b + +# convert a bytearray back to timecode +def mtc_decode(mtc_bytes): + rhh, mins, secs, frs = mtc_bytes + rateflag = rhh >> 5 + hrs = rhh & 31 + fps = ['24','25','29.97','30'][rateflag] + total_frames = int(frs + float(fps) * (secs + mins * 60 + hrs * 60 * 60)) + return Timecode(fps, frames=total_frames) + +def mtc_full_frame(timecode): + # if sending this to a MIDI device, remember that MIDI is generally little endian + # but the full frame timecode bytes are big endian + mtc_bytes = mtc_encode(timecode) + # mtc full frame has a special header and ignores the rate flag + return bytearray([0xf0, 0x7f, 0x7f, 0x01, 0x01]) + mtc_bytes + bytearray([0xf7]) + +def mtc_decode_full_frame(full_frame_bytes): + mtc_bytes = full_frame_bytes[5:-1] + return mtc_decode(mtc_bytes) + +def mtc_quarter_frame(timecode, piece=0): + # there are 8 different mtc_quarter frame pieces + # see https://en.wikipedia.org/wiki/MIDI_timecode + # and https://web.archive.org/web/20120212181214/http://home.roadrunner.com/~jgglatt/tech/mtc.htm + # these are little-endian bytes + # piece 0 : 0xF1 0000 ffff frame + mtc_bytes = mtc_encode(timecode) + this_byte = mtc_bytes[3 - piece//2] #the order of pieces is the reverse of the mtc_encode + if piece % 2 == 0: + # even pieces get the low nibble + nibble = this_byte & 15 + else: + # odd pieces get the high nibble + nibble = this_byte >> 4 + return bytearray([0xf1, piece * 16 + nibble]) + +def mtc_decode_quarter_frames(frame_pieces): + mtc_bytes = bytearray(4) + if len(frame_pieces) < 8: + return None + for piece in range(8): + mtc_index = 3 - piece//2 # quarter frame pieces are in reverse order of mtc_encode + this_frame = frame_pieces[piece] + if this_frame is bytearray or this_frame is list: + this_frame = this_frame[1] + data = this_frame & 15 # ignore the frame_piece marker bits + if piece % 2 == 0: + # 'even' pieces came from the low nibble + # and the first piece is 0, so it's even + mtc_bytes[mtc_index] += data + else: + # 'odd' pieces came from the high nibble + mtc_bytes[mtc_index] += data * 16 + return mtc_decode(mtc_bytes) + + diff --git a/miredis.json b/miredis.json index 82accd0..373008e 100644 --- a/miredis.json +++ b/miredis.json @@ -1,27 +1,123 @@ -{ -"params": [ +{"params": [ { "_comment": "normalized named CC for filters", "name": "/velocity", + "type": 7, "chanIN" : 1, - "CC" : 1 + "CC" : 30 }, { "name": "/strength", + "type": 7, "chanIN" : 1, - "CC" : 3 + "CC" : 31 }, { "name": "/decay", + "type": 7, "chanIN" : 1, - "CC" : 5 + "CC" : 32 }, { - "name": "/feedback ", + "name": "/feedback", + "type": 7, "chanIN" : 1, - "CC" : 114 + "CC" : 34 + }, + + { + "_comment": "LJ settings for Beatstep", + "name": "/grid/lasernumber", + "type": 0, + "note" : 33 + }, + { + "name": "/black/lasernumber", + "type": 0, + "note" : 35 + }, + { + "name": "/swap/X/lasernumber", + "type": 0, + "note" : 24 + }, + { + "name": "/swap/Y/lasernumber", + "type": 0, + "note" : 26 + }, + { + "name": "/kpps/lasernumber", + "type": 14, + "highCC" : 0, + "lowCC" : 1, + "low": 0, + "high": 70000, + "default": 20000 + }, + { + "name": "/angle/lasernumber", + "type": 14, + "highCC" : 2, + "lowCC" : 3, + "low": 0, + "high": 360, + "default": 0 + }, + { + "name": "/scale/X/lasernumber", + "type": 7, + "CC" : 4, + "low": 0, + "high": 200, + "default": 0 + }, + { + "name": "/scale/Y/lasernumber", + "type": 7, + "CC" : 5, + "low": 0, + "high": 200, + "default": 0 + }, + { + "name": "/red/lasernumber", + "type": 8, + "CC" : 8 + }, + { + "name": "/green/lasernumber", + "type": 8, + "CC" : 9 + }, + { + "name": "/blue/lasernumber", + "type": 8, + "CC" : 10 + }, + { + "name": "/intens/lasernumber", + "type": 8, + "CC" : 11 + }, + { + "name": "/loffset/X/lasernumber", + "type": 14, + "highCC" : 12, + "lowCC" : 13, + "low": -27000, + "high": 27000, + "default": 1 + }, + { + "name": "/loffset/Y/lasernumber", + "type": 14, + "highCC" : 14, + "lowCC" : 15, + "low": -27000, + "high": 27000, + "default": 1 } ] +} - -} \ No newline at end of file diff --git a/miredis.py b/miredis.py index 805d63f..b750ab0 100644 --- a/miredis.py +++ b/miredis.py @@ -3,7 +3,7 @@ """ miredis -v0.1 +v0.2 Forward midi events to OSC and redis. @@ -14,7 +14,7 @@ from ProtonPhoton from libs import log print() -log.infog('Miredis v0.1') +log.infog('Miredis v0.2') import argparse import redis @@ -38,11 +38,11 @@ argsparser = argparse.ArgumentParser(description="Miredis") # General Args argsparser.add_argument("-v","--verbose",action="store_true",help="Verbose") # Redis Args -argsparser.add_argument("-i","--ip",help="IP address of the Redis server ",default="127.0.0.1",type=str) -argsparser.add_argument("-p","--port",help="Port of the Redis server ",default="6379",type=str) +argsparser.add_argument("-i","--ip",help="Redis server IP address to forward midi events.",default="127.0.0.1",type=str) +argsparser.add_argument("-p","--port",help="Redis server port number",default="6379",type=str) # OSC Args -argsparser.add_argument("-o","--oscip",help="IP address of the OSC server to forward midi events.",default="127.0.0.1",type=str) -argsparser.add_argument("-q","--oscport",help="Port of the OSC server ",default="9000",type=str) +argsparser.add_argument("-o","--oscip",help="OSC server IP address to forward midi events.",default="127.0.0.1",type=str) +argsparser.add_argument("-q","--oscport",help="OSC server port number",default="9000",type=str) argsparser.add_argument('-link',help="Enable Ableton Link (disabled by default)", dest='link', action='store_true') argsparser.add_argument("-m","--mode",help="Mode choice : simplex, clitools",default="clitools",type=str) argsparser.set_defaults(link=False) @@ -55,6 +55,12 @@ midix.oscPORT = int(args.oscport) midix.debug = args.verbose midix.mode = args.mode +print("Destination OSC server : " +midix.oscIP+ " port "+str(midix.oscPORT)) + +r = redis.StrictRedis(host=redisIP , port=redisPORT, db=0) +print("Redis server : " +redisIP+ " port "+str(redisPORT)) +midix.r = r + # with Ableton Link if args.link == True: from libs import alink @@ -65,8 +71,6 @@ else: print("Link DISABLED") linked = False -r = redis.StrictRedis(host=redisIP , port=redisPORT, db=0) -midix.r = r def Osc():