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

@ -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
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)