New features

This commit is contained in:
leduc 2021-10-11 09:40:24 +02:00
parent dcaa716d29
commit b36da76c22
6 changed files with 602 additions and 47 deletions

View File

@ -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) ![Clitools](https://www.teamlaser.fr/images/miredispad.png)
@ -19,16 +22,28 @@ To enable Link :
python3 miredis.py -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 ## New Features
- Client example : midicontrol.py - Midi timecode thks to https://github.com/jeffmikels/timecode_tools.git
- Added verbose mode -v - CC values can be reranged, see 'custom action type'
- Added redis subscribe events - LJ2 custom actions for laser settings (midichannel -> laser number)
- Added Clitools program selection mode for Launchpads - Midi controlled python example : midicontrol.py
- 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. - 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 ## OSC
@ -76,3 +91,25 @@ custom ones
"/midi/cc/midichannel/ccnumber/ccvalue" "/midi/cc/midichannel/ccnumber/ccvalue"
customs one like "/feedback/1/114/value" 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)

View File

@ -3,7 +3,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
Midi3 light version for soundt/Jamidi/clapt Midi3 light version with LJ2 settings support
v0.7.0 v0.7.0
Midi Handler : Midi Handler :
@ -15,6 +15,7 @@ by Sam Neurohack
from /team/laser from /team/laser
Midi conversions from https://github.com/craffel/pretty-midi 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 import rtmidi
from rtmidi.midiutil import open_midiinput 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) PITCH_BEND, POLY_PRESSURE, PROGRAM_CHANGE, TIMING_CLOCK, SONG_CONTINUE, SONG_START, SONG_STOP)
import mido import mido
from mido import MidiFile from mido import MidiFile
@ -35,7 +36,7 @@ from sys import platform
import os import os
import re import re
from collections import deque from collections import deque
from libs import log from libs import log, tools
import json import json
oscIP = "127.0.0.1" oscIP = "127.0.0.1"
@ -168,19 +169,17 @@ def SendUI(oscaddress,oscargs=''):
# Ask redis for a given key # Ask redis for a given key
def fromKey(keyname): def fromKey(keyname):
return r.get(keyname) return r.get(keyname)
#
# Write to redis key # Write to redis key
def toKey(keyname,keyvalue): def toKey(keyname,keyvalue):
#print(keyname,keyvalue) #print(keyname,keyvalue)
# Store encoded data in Redis # Store encoded data in Redis
return r.set(keyname,keyvalue) return r.set(keyname,keyvalue)
# Publish to redis key
def toKeyevent(eventname): def toKeyevent(eventname):
print("redis midi event key :", eventname) print("redis midi event key :", eventname)
@ -281,8 +280,6 @@ def cc(midichannel, ccnumber, value, mididest):
# #
# MIDI Startup and handling # MIDI Startup and handling
# #
@ -313,7 +310,6 @@ def MidinProcess(inqueue, portname):
#print("") #print("")
#print("Generic from", portname,"msg : ", msg) #print("Generic from", portname,"msg : ", msg)
# NOTE ON message on all midi channels # NOTE ON message on all midi channels
if NOTE_ON -1 < msg[0] < 160 and msg[2] !=0 : 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) 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 # OSC : /midi/noteon midichannel note velocity
SendOSC("/midi/noteon",[MidiChannel, msg[1], msg[2]]) SendOSC("/midi/noteon",[MidiChannel, msg[1], msg[2]])
print("osc :","/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)) toKeyevent("/midi/noteon/"+str(MidiChannel)+"/"+str(MidiNote)+"/"+str(MidiVel))
''' '''
# Sampler mode : note <63 launch snare.wav / note > 62 kick.wav # Sampler mode : note <63 launch snare.wav / note > 62 kick.wav
if MidiNote < 63 and MidiVel >0: if MidiNote < 63 and MidiVel >0:
@ -360,6 +368,7 @@ def MidinProcess(inqueue, portname):
else: else:
MidiChannel = msg[0]-128 MidiChannel = msg[0]-128
MidiNote = msg[1] MidiNote = msg[1]
print()
print("NOTE_off channel :", MidiChannel, "note :", MidiNote) print("NOTE_off channel :", MidiChannel, "note :", MidiNote)
NoteOff(MidiNote, "pads" , midichannel=MidiChannel) NoteOff(MidiNote, "pads" , midichannel=MidiChannel)
@ -376,10 +385,10 @@ def MidinProcess(inqueue, portname):
if CONTROLLER_CHANGE -1 < msg[0] < 192: if CONTROLLER_CHANGE -1 < msg[0] < 192:
MidiChannel = msg[0]-175 MidiChannel = msg[0]-175
print()
cc(MidiChannel, msg[1], msg[2], "pads" ) cc(MidiChannel, msg[1], msg[2], "pads" )
#print("channel", MidiChannel, "CC :", msg[1], msg[2]) #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])) toKeyevent("/midi/cc/"+str(MidiChannel)+"/"+str(msg[1])+"/"+str(msg[2]))
# redis key : "/midi/cc/midichannel/ccnumber" value : "ccvalue" # redis key : "/midi/cc/midichannel/ccnumber" value : "ccvalue"
@ -393,12 +402,112 @@ def MidinProcess(inqueue, portname):
for param in conf['params']: for param in conf['params']:
if MidiChannel == param["chanIN"] and param["CC"] == msg[1]: #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])) #print(param["name"]+"/"+ str(msg[1])+"/"+str(msg[2]))
SendOSC(param["name"],[msg[0]-175-1, msg[1], 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])) toKeyevent(param["name"]+"/"+ str(MidiChannel)+"/"+str(msg[1])+"/"+str(msg[2]))
toKey(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: if msg[0] == TIMING_CLOCK:
now = time.time() now = time.time()
@ -427,6 +536,7 @@ def MidinProcess(inqueue, portname):
if msg[0] in (SONG_CONTINUE, SONG_START): if msg[0] in (SONG_CONTINUE, SONG_START):
running = True running = True
#print("START/CONTINUE received.") #print("START/CONTINUE received.")
#print("Midi in process send /aurora/start") #print("Midi in process send /aurora/start")
@ -443,7 +553,27 @@ def MidinProcess(inqueue, portname):
# OSC : /midi/start # OSC : /midi/start
SendOSC("/midi/stop",[]) SendOSC("/midi/stop",[])
print("osc : /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 # other midi message
if msg[0] != NOTE_OFF and msg[0] != NOTE_ON and msg[0] != CONTROLLER_CHANGE: 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: if midiname[port].find(mididest) == 0:
midiport[port].send_message([NOTE_ON, note, velocity]) 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 # launchpad
# #
@ -1026,10 +1209,10 @@ def loadConf():
f = open(ConFile,"r") f = open(ConFile,"r")
s = f.read() s = f.read()
conf = json.loads(s) conf = json.loads(s)
print("params", len(conf['params'])) print(len(conf['params']), "custom actions")
nbparam = len(conf['params']) nbparam = len(conf['params'])
print(conf) #print(conf)
return True return True
except Exception as e: except Exception as e:
print("_loadPlaylist error when loading '{}':{}".format(ConFile,e)) print("_loadPlaylist error when loading '{}':{}".format(ConFile,e))
@ -1040,4 +1223,45 @@ def check():
InConfig() 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
View 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 (03) and hour (023).
# 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 (059)
# Byte 2
# 00ssssss: Second (059)
# Byte 3
# 000fffff: Frame (029, 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)

View File

@ -1,27 +1,123 @@
{ {"params": [
"params": [
{ {
"_comment": "normalized named CC for filters", "_comment": "normalized named CC for filters",
"name": "/velocity", "name": "/velocity",
"type": 7,
"chanIN" : 1, "chanIN" : 1,
"CC" : 1 "CC" : 30
}, },
{ {
"name": "/strength", "name": "/strength",
"type": 7,
"chanIN" : 1, "chanIN" : 1,
"CC" : 3 "CC" : 31
}, },
{ {
"name": "/decay", "name": "/decay",
"type": 7,
"chanIN" : 1, "chanIN" : 1,
"CC" : 5 "CC" : 32
}, },
{ {
"name": "/feedback ", "name": "/feedback",
"type": 7,
"chanIN" : 1, "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
} }
] ]
} }

View File

@ -3,7 +3,7 @@
""" """
miredis miredis
v0.1 v0.2
Forward midi events to OSC and redis. Forward midi events to OSC and redis.
@ -14,7 +14,7 @@ from ProtonPhoton
from libs import log from libs import log
print() print()
log.infog('Miredis v0.1') log.infog('Miredis v0.2')
import argparse import argparse
import redis import redis
@ -38,11 +38,11 @@ argsparser = argparse.ArgumentParser(description="Miredis")
# General Args # General Args
argsparser.add_argument("-v","--verbose",action="store_true",help="Verbose") argsparser.add_argument("-v","--verbose",action="store_true",help="Verbose")
# Redis Args # Redis Args
argsparser.add_argument("-i","--ip",help="IP address of the Redis server ",default="127.0.0.1",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="Port of the Redis server ",default="6379",type=str) argsparser.add_argument("-p","--port",help="Redis server port number",default="6379",type=str)
# OSC Args # 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("-o","--oscip",help="OSC server IP address 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("-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('-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.add_argument("-m","--mode",help="Mode choice : simplex, clitools",default="clitools",type=str)
argsparser.set_defaults(link=False) argsparser.set_defaults(link=False)
@ -55,6 +55,12 @@ midix.oscPORT = int(args.oscport)
midix.debug = args.verbose midix.debug = args.verbose
midix.mode = args.mode 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 # with Ableton Link
if args.link == True: if args.link == True:
from libs import alink from libs import alink
@ -65,8 +71,6 @@ else:
print("Link DISABLED") print("Link DISABLED")
linked = False linked = False
r = redis.StrictRedis(host=redisIP , port=redisPORT, db=0)
midix.r = r
def Osc(): def Osc():