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 10a23d3..73ff17e 100644 Binary files a/libs/__pycache__/midix.cpython-38.pyc and b/libs/__pycache__/midix.cpython-38.pyc differ diff --git a/libs/midix.py b/libs/midix.py index bcbbfd1..fddc67b 100644 --- a/libs/midix.py +++ b/libs/midix.py @@ -3,7 +3,7 @@ # -*- coding: utf-8 -*- """ -Midi3 light version for soundt/Jamidi/clapt +Midi3 light version with LJ2 settings support v0.7.0 Midi Handler : @@ -15,6 +15,7 @@ by Sam Neurohack from /team/laser Midi conversions from https://github.com/craffel/pretty-midi +Guide to the MIDI Software Specification : http://www.somascape.org/midi/tech/spec.html#rpns """ @@ -23,7 +24,7 @@ from threading import Thread import rtmidi from rtmidi.midiutil import open_midiinput -from rtmidi.midiconstants import (CHANNEL_PRESSURE, CONTROLLER_CHANGE, NOTE_ON, NOTE_OFF, +from rtmidi.midiconstants import (CHANNEL_PRESSURE, CONTROLLER_CHANGE, NOTE_ON, NOTE_OFF, SYSTEM_EXCLUSIVE, MIDI_TIME_CODE, PITCH_BEND, POLY_PRESSURE, PROGRAM_CHANGE, TIMING_CLOCK, SONG_CONTINUE, SONG_START, SONG_STOP) import mido from mido import MidiFile @@ -35,7 +36,7 @@ from sys import platform import os import re from collections import deque -from libs import log +from libs import log, tools import json oscIP = "127.0.0.1" @@ -168,19 +169,17 @@ def SendUI(oscaddress,oscargs=''): # Ask redis for a given key - def fromKey(keyname): return r.get(keyname) -# # Write to redis key def toKey(keyname,keyvalue): #print(keyname,keyvalue) # Store encoded data in Redis return r.set(keyname,keyvalue) - +# Publish to redis key def toKeyevent(eventname): print("redis midi event key :", eventname) @@ -281,8 +280,6 @@ def cc(midichannel, ccnumber, value, mididest): - - # # MIDI Startup and handling # @@ -313,7 +310,6 @@ def MidinProcess(inqueue, portname): #print("") #print("Generic from", portname,"msg : ", msg) - # NOTE ON message on all midi channels if NOTE_ON -1 < msg[0] < 160 and msg[2] !=0 : @@ -329,10 +325,22 @@ def MidinProcess(inqueue, portname): NoteOn(MidiNote, MidiVel, "pads" , midichannel=MidiChannel) + for param in conf['params']: + + islj2 = param["name"].find('lasernumber') + if param["type"] == 0 and param["note"] == msg[1] and islj2 != -1: + + # LJ2 parameter : replace lasernumber with channel number + tolj = param["name"].replace('lasernumber',str(MidiChannel)) + print("LJ2 button :", tolj, ["1"]) + SendOSC(tolj, ['1']) + + # OSC : /midi/noteon midichannel note velocity SendOSC("/midi/noteon",[MidiChannel, msg[1], msg[2]]) print("osc :","/midi/noteon/",[MidiChannel, msg[1], msg[2]]) toKeyevent("/midi/noteon/"+str(MidiChannel)+"/"+str(MidiNote)+"/"+str(MidiVel)) + ''' # Sampler mode : note <63 launch snare.wav / note > 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():