-
Notifications
You must be signed in to change notification settings - Fork 1
/
miditoosc.py
385 lines (317 loc) · 11.6 KB
/
miditoosc.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
#Project URL for information/contact https://github.com/krixgris/MidiToOSC
#
import mido
import OSC
import json
import math
import time
import subprocess
from datetime import datetime
import easingalgos as easing
import configHandler
import httpHandler
#region TODO #2
#
# Feature to throttle *individual* events/actions for reicipients that can't handle them fast enough
# Example being Motu 828es which can't process OSC fast enough for a smooth experience, so throttling OSC being sent might help
#
# This is implemented on a global level now with the globalThrottleOverride 0..1 in the json-config
#
#
#
#endregion
#region GLOBALS
#can be set to anything you want
configFile = 'oscconfig.json'
# constant for doing calculations for scaling values
# keep note this is 7-bit midi only, no support for nrpn midi yet
midiMaxValue = 127.0
conf = configHandler.configHandler(configFile=configFile)
osc = OSC.OSCClient()
http = httpHandler.httpHandler()
#endregion
# MidiEventList = dict()
# for midiType in conf.definedMidi:
# MidiEventList[midiType] = dict()
# for midiNum in conf.definedMidi[midiType]:
# MidiEventList[midiType][midiNum] = MidiEvent(midiNum,midiType)
#generic MidiEvent-getter, can replace getAttribute/Type/Command etc
#returns a configHandler.MidiEvent
#deprecated function. still works, but very inefficient
def MidiEvent(midiNum, midiType):
#with no parameters, configHandler.MidiEvent() returns a dummy event
midiEvent = configHandler.MidiEvent()
if (midiType == 'control_change'):
midiEvent = configHandler.MidiEvent(conf.control_change.get(midiNum))
if (midiType == 'note_on'):
midiEvent = configHandler.MidiEvent(conf.note_on.get(midiNum))
if (midiType == 'note_off'):
midiEvent = configHandler.MidiEvent(conf.note_off.get(midiNum))
return midiEvent
#region Internal Commands that can be used for mtoCommand()
#add support for loading custom config via config-parameters configName='oscConfig', configFile='oscconfig.json' ?
def reloadConfig():
global conf
conf = configHandler.configHandler(configFile=configFile)
reconnectOSC()
reconnectHTTP()
print(str(datetime.now()) + " Configuration updated")
#ugly, make a nicer result at some point?
print conf
def quitViolently(message = 'Quitting violently!'):
print message
quit()
def runShellScript(shellScriptPath):
path = './scripts/' + shellScriptPath
print path
subprocess.call(['bash', path])
#endregion
#reconnects the OSC object to ip/port in config
def reconnectOSC():
osc.connect((conf.IP,conf.port))
#reconnects HTTP object. in reality just sets the IP to connect to when message is sent
#rename?
def reconnectHTTP():
http.setIP(conf.IP)
#simply returns midi channel in a -1 way to 'correct' 0 index values, which may confuse a user for debugging purposes
#inconsistent use throughout, may re-evaluate
def getMIDIInputChannel():
#todo: defaults?, 0?
return conf.midiChannelInput-1
#region Handlers for events
#generic handler?
def mtoAction(midiNum, midiValue, midiType):
midiEventType = conf.MidiEventList[midiType][midiNum].type
if(midiEventType == 'osc'):
mtoOSC(midiNum, midiValue, midiType)
if(midiEventType == 'http'):
mtoHTTP(midiNum, midiValue, midiType)
if(midiEventType == 'command'):
mtoCommand(midiNum, midiValue, midiType)
def mtoOSC(midiNum, midiValue, midiType):
osc.send(getOSCMessage(midiNum, midiValue, midiType))
#implement sending json of multiple values?
#maybe?
#maybe extension of handling multiple events per incoming midi uses json instead of multiple events for http for performance
def mtoHTTP(midiNum, midiValue, midiType):
#http can technically send batches of data with json, but only one parameter is currently supported
data = http.getValueList(getHTTPValueAttribute(midiNum,midiValue,midiType), getEventValue(midiNum,midiValue,midiType))
#print data
#print getHTTPAddress(midiNum,midiType)
http.patchData(getEventAddress(midiNum,midiType), data)
def mtoCommand(midiNum, midiValue, midiType):
midiEventCommand = conf.MidiEventList[midiType][midiNum].command
conf.MidiEventList[midiType][midiNum]
if(midiEventCommand == 'reloadConfig'):
reloadConfig()
if(midiEventCommand == 'quitLoop'):
quitViolently()
if(midiEventCommand == 'shellscript'):
runShellScript(getEventAddress(midiNum, midiType))
#endregion
#region Helper functions for getting attributes for mtoEventhandlers
def getHTTPValueAttribute(midiNum, midiValue, midiType):
# valAttr = MidiEvent(midiNum, midiType).attribute
valAttr = conf.MidiEventList[midiType][midiNum].attribute
if (valAttr is None):
valAttr = 'value'
return valAttr
def getValueScale(algorithm, value, base = 1):
scale = 1.0
#easeInCirc works great for motu 828 es volume
if algorithm == 'exp':
scale = (pow(base,value)-1)/(base-1)
elif algorithm == 'log':
scale = (math.log(1 + (base-1)*value)/math.log(base))
elif (algorithm == 'easeInSine'):
scale = easing.easeInSine(value)
elif (algorithm == 'easeInCubic'):
scale = easing.easeInCubic(value)
elif (algorithm == 'easeInQuint'):
scale = easing.easeInQuint(value)
elif (algorithm == 'easeInCirc'):
scale = easing.easeInCirc(value)
elif (algorithm == 'easeInQuad'):
scale = easing.easeInQuad(value)
elif (algorithm == 'easeInQuart'):
scale = easing.easeInQuart(value)
elif (algorithm == 'easeInExpo'):
scale = easing.easeInExpo(value)
elif (algorithm == 'easeOutSine'):
scale = easing.easeOutSine(value)
elif (algorithm == 'easeOutCubic'):
scale = easing.easeOutCubic(value)
elif (algorithm == 'easeOutQuint'):
scale = easing.easeOutQuint(value)
elif (algorithm == 'easeOutCirc'):
scale = easing.easeOutCirc(value)
elif (algorithm == 'easeOutQuad'):
scale = easing.easeOutQuad(value)
elif (algorithm == 'easeOutQuart'):
scale = easing.easeOutQuart(value)
elif (algorithm == 'easeOutExpo'):
scale = easing.easeOutExpo(value)
elif (algorithm == 'easeInOutSine'):
scale = easing.easeInOutSine(value)
elif (algorithm == 'easeInOutCubic'):
scale = easing.easeInOutCubic(value)
elif (algorithm == 'easeInOutQuint'):
scale = easing.easeInOutQuint(value)
elif (algorithm == 'easeInOutCirc'):
scale = easing.easeInOutCirc(value)
elif (algorithm == 'easeInOutQuad'):
scale = easing.easeInOutQuad(value)
elif (algorithm == 'easeInOutQuart'):
scale = easing.easeInOutQuart(value)
elif (algorithm == 'easeInOutExpo'):
scale = easing.easeInOutExpo(value)
elif (algorithm == 'easeInBounce'):
scale = easing.easeInBounce(value)
elif (algorithm == 'easeOutBounce'):
scale = easing.easeOutBounce(value)
elif (algorithm == 'easeInOutBounce'):
scale = easing.easeInOutBounce(value)
return scale
def getEventValue(midiNum, midiValue, midiType):
valMin = float(conf.MidiEventList[midiType][midiNum].min)
valMax = float(conf.MidiEventList[midiType][midiNum].max)
valScaling = conf.MidiEventList[midiType][midiNum].valueScaling
valScalingBase = conf.MidiEventList[midiType][midiNum].valueScalingBase
if valScaling is None:
valScaling = 'lin'
valScalingBase = 1
#value scaling base for log/exp of 20 seems okay for most things so far
#definitely try different bases for your particular application
# if(valScaling == 'exp'):
# eventValue = (valMax-valMin)*(pow(valScalingBase,(midiValue/midiMaxValue))-1)/(valScalingBase-1)+valMin
# elif(valScaling == 'log'):
# eventValue = (valMax-valMin)*(math.log(1 + (scaleBase-1)*midiValue/midiMaxValue)/math.log(scaleBase))+valMin
# else:
# #calculate linearly unless exp or log is defined
# if(midiValue == 0):
# eventValue = valMin
# else:
# eventValue = (valMax-valMin)/midiMaxValue*(midiValue+valMin)
value = midiValue/midiMaxValue
eventValue = (valMax-valMin)*getValueScale(valScaling, value, valScalingBase)+valMin
if (midiValue == 0):
eventValue = valMin
return eventValue
def getEventAddress(midiNum, midiType):
address = conf.MidiEventList[midiType][midiNum].address
return address
def getOSCMessage(midiNum, midiValue, midiType):
oscMsg = OSC.OSCMessage()
oscMsg.setAddress(getEventAddress(midiNum, midiType))
oscMsg.append(getEventValue(midiNum, midiValue, midiType))
return oscMsg
#endregion
#region Helper functions for MIDI messages, including checks if event is defined
#unused, probably getting removed
def isDefinedMidiLookup(midiNum, midiType, midiCh = -1):
if(midiCh == conf.midiChannelInput or midiCh == -1):
if(midiType in conf.definedMidi.keys()):
if(midiNum in conf.definedMidi[midiType]):
return True
return False
def getMidiValue(msg):
if(msg.type == 'control_change'):
return msg.value
if(msg.type == 'note_on' or msg.type == 'note_off'):
return msg.velocity
return -1
def getMidiNum(msg):
if(msg.type == 'control_change'):
return msg.control
if(msg.type == 'note_on' or msg.type == 'note_off'):
return msg.note
return -1
def isDefinedMidi(msg):
if(getattr(msg, 'channel', None)==getMIDIInputChannel()):
if(msg.type in conf.definedMidi.keys()):
if(getMidiNum(msg) in conf.definedMidi[msg.type]):
if (conf.globalThrottleOverride == 1):
isValidMessage = doThrottle(getMidiNum(msg), getMidiValue(msg), msg.type)
else:
isValidMessage = True
return isValidMessage
return False
def doThrottle(midiNum, midiValue, midiType):
global conf
# print conf.globalThrottleOverride
prevValue = conf.MidiEventList[midiType][midiNum].prevValue
prevTime = conf.MidiEventList[midiType][midiNum].prevTime
currTime = time.time()*1000
deltaTime = currTime-prevTime
deltaValue = abs(midiValue-prevValue)
if( (100>=deltaTime > 40 and deltaValue>20)
or (150>=deltaTime > 100 and deltaValue>4)
or (250>=deltaTime > 150 and deltaValue>2)
or (deltaTime > 250 and deltaValue>0)
or midiValue in [0,127]):
# print deltaTime
# print deltaValue
prevValue = conf.MidiEventList[midiType][midiNum].prevValue = midiValue
prevTime = conf.MidiEventList[midiType][midiNum].prevTime = currTime
return True
return False
def isValidMidiInput(inputDevice):
if conf.midiDeviceInput in mido.get_input_names():
return 1
else:
return 0
#endregion
#region debug functions
#put debug messages to run on launch here
#will only work if debug:1 is set in the json-config
def debugCommands():
print ''
print 'Debug messages from debugCommand():'
print ''
# mtoAction(80,0,'control_change')
mtoAction(80,0,'control_change')
mtoAction(80,1,'control_change')
mtoAction(80,2,'control_change')
mtoAction(80,3,'control_change')
mtoAction(80,4,'control_change')
mtoAction(80,5,'control_change')
mtoAction(80,6,'control_change')
mtoAction(80,127,'control_change')
#endregion
#region Inits
#reloads config, which also sets connections for http, osc
#entire config object is re-initialized
reloadConfig()
#use the information provided from the output here if you struggle with finding a working midi device
#
#for future development, mido.get_output_names() would be useful to add here
print ""
print "Available MIDI Inputs: "
print mido.get_input_names()
print ""
print "Listening on device: "
print conf.midiDeviceInput
print "Listening on channel (0-15), i.e. 0 = midi 1, 15 = midi 16 etc: "
print conf.midiChannelInput
#debug things here
if(conf.debug == 1):
debugCommands()
#endregion
#region main loop
if(isValidMidiInput(conf.midiDeviceInput)):
with mido.open_input(conf.midiDeviceInput) as inport:
for msg in inport:
if(isDefinedMidi(msg)):
mtoAction(getMidiNum(msg), getMidiValue(msg), msg.type)
if(conf.debug == 1):
print "Handled MIDI:"
print(msg)
else:
#debug handling to control printing of messages
#ALL messages gets printed here
if(conf.debug == 1):
print "Unhandled MIDI:"
print(msg)
else:
quitViolently("MIDI Device could not be found, make sure config matches one of the available MIDI Inputs.")
#endregion