New features
This commit is contained in:
parent
dcaa716d29
commit
b36da76c22
57
README.md
57
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)
|
||||
|
Binary file not shown.
262
libs/midix.py
262
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
|
||||
|
||||
'''
|
||||
|
194
libs/tools.py
Executable file
194
libs/tools.py
Executable file
@ -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)
|
||||
|
||||
|
114
miredis.json
114
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
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
20
miredis.py
20
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():
|
||||
|
Loading…
Reference in New Issue
Block a user