This repository has been archived by the owner on Mar 14, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 10
/
Copy pathe2sysex.py
458 lines (309 loc) · 13.5 KB
/
e2sysex.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
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
import mido
from mido import Message
import logging
from subprocess import run
from e2pat2syx import pat_to_syx
from e2syx2pat import syx_to_pat
from e2_syx_codec import syx_enc
from e2_syx_codec import syx_dec
def main():
logging.basicConfig(level=logging.DEBUG)
e = E2Sysex()
class E2Sysex:
def __init__(self):
self.inport = mido.open_input('electribe2 sampler electribe2 s')
self.outport = mido.open_output('electribe2 sampler electribe2 s')
self.global_channel, self.id, self.version = self.search_device()
self.sysex_head = [0x42, 0x30 + self.global_channel, 0x00, 0x01, self.id]
logging.info('Found electribe')
logging.info('Global channel ' + str(self.global_channel))
logging.info('Firmware version ' + self.version)
def search_device(self):
msg = Message('sysex', data=[0x42, 0x50, 0x00, 0x00])
self.outport.send(msg)
response = self.sysex_response()
if response[:4] != [0xF0, 0x42, 0x50, 0x01]:
logging.warning('Invalid response: Not search device reply message.')
return -1
global_channel = response[4]
electribe_id = response[6]
version = str(response[10]) + '.' + str(response[11])
return global_channel, electribe_id, version
# get pattern from device
# source is pattern number
# returns pattern as sysex bytes
def get_pattern(self, source):
msg = Message('sysex', data=self.sysex_head +
[0x1C] +
self.int_to_midi(source))
self.outport.send(msg)
response = self.sysex_response()
if response[6] == 0x24:
logging.warning('DATA LOAD ERROR: Pattern dump request unsuccessful')
return -1
elif response[6] == 0x4C:
logging.info('PATTERN DATA DUMP: Pattern dump request successful')
data = response [9:-1]
return bytes(data)
# send pattern to device
# pattern is pattern file as list of sysex bytes
# dest is pattern number (0-249)
# returns SysEx response code
def set_pattern(self, dest, pattern):
msg = Message('sysex', data=self.sysex_head +
[0x4C] +
self.int_to_midi(dest) +
pattern)
#self.port.send(msg)
#response = self.sysex_response()
# long sysex messages fail
response = self.workaround_long_sysex(msg)
if response[6] == 0x24:
logging.warning('DATA LOAD ERROR: Pattern dump unsuccessful')
return 0x24
elif response[6] == 0x23:
logging.info('PATTERN DATA DUMP: Pattern dump successful')
return 0x23
# get current pattern edit buffer from device
# returns current pattern as sysex bytes SysEx error code
def get_current_pattern(self):
msg = Message('sysex', data=self.sysex_head+[0x10])
self.outport.send(msg)
response = self.sysex_response()
if response[6] == 0x24:
logging.warning('DATA LOAD ERROR: Current pattern dump request unsuccessful')
return 0x24
elif response[6] == 0x40:
logging.info('CURRENT PATTERN DATA DUMP: Current pattern dump request successful')
data = response[7:-1]
return bytes(data)
# send pattern to device edit buffer
# pattern is pattern file as sysex bytes
# returns SysEx response code
def set_current_pattern(self, pattern):
msg = Message('sysex', data=self.sysex_head +
[0x40] +
pattern)
# long sysex messages fail
response = self.workaround_long_sysex(msg)
# self.port.send(msg)
# response = self.sysex_response()
if response[6] == 0x24:
logging.warning('DATA LOAD ERROR: Current Pattern dump unsuccessful')
return 0x24
elif response[6] == 0x23:
logging.info('PATTERN DATA DUMP: Current pattern dump successful')
return 0x23
# writes current edit buffer on device
# returns SysEx Response code
def write_pattern(self):
logging.info('WRITE PATTERN: Not implemented yet')
# helper function, uses get_pattern
# get all patterns from device
# returns list of pattern files as sysex bytes
def get_all_patterns(self):
return [self.get_pattern(i) for i in range(250)]
# helper function, uses set_pattern
# sends all patterns to device
# patterns is list of patterns as sysex bytes
def set_all_patterns(self, patterns):
logging.info('SET ALL PATTERNS: Not implemented yet')
# get global settings
# returns settings as sysex bytes
def get_global(self):
msg = Message('sysex', data=self.sysex_head+[0x1e])
self.outport.send(msg)
response = self.sysex_response()
if response[6] == 0x24:
logging.warning('DATA LOAD ERROR: Global data dump request unsuccessful')
return -1
elif response[6] == 0x40:
logging.info('CURRENT PATTERN DATA DUMP: Global data dump request successful')
data = response[7:-1]
return bytes(data)
# sends global settings to device
# settings is global settings as sysex bytes
# checks response and returns 0 if successful
def set_global(self, settings):
logging.info('SET GLOBAL DATA: Not implemented yet')
def sysex_response(self):
for msg in self.inport:
if msg.type == 'sysex':
response = msg.bytes()
break
return response
# convert integer x <= 255 to midi bytes
# returns little endian list of 7-bit bytes
def int_to_midi(self, x):
return [ x%128, x//128 ]
# FIX - find a proper solution
# ? can't send long sysex messages via ?mido/rtmidi?
# alsa midi output buffer overrun?
# works using amidi, but is very slow
def workaround_long_sysex(self, msg):
logging.info('Working around long sysex bug')
self.outport.close()
run(["amidi", "-p", "hw:1", "-S", msg.hex()])
response = self.sysex_response()
self.outport = mido.open_output('electribe2 sampler electribe2 s')
return response
# val is list of sysex bytes
def test_sysex_message(self, val):
msg = Message('sysex', data=self.sysex_head+val)
self.outport.send(msg)
response = self.sysex_response()
return response
# val is list of sysex bytes
def test_long_sysex_message(self, val):
msg = Message('sysex', data=self.sysex_head+val)
self.workaround_long_sysex(msg)
response = self.sysex_response()
return response
# Read CPU RAM at address for length bytes
# Returns data as bytearray
def read_cpu_ram(self, address, length):
# Encode values as sysex
addr = address.to_bytes(4, byteorder='little')
leng = length.to_bytes(4, byteorder='little')
syx_al = syx_enc(addr+leng)
# Send message
msg = Message('sysex', data=self.sysex_head+[0x52]+syx_al)
self.outport.send(msg)
response = self.sysex_response()
# Decode sysex response
byt_data = syx_dec(response[9:-1])
return bytearray(byt_data)
# Write data to CPU RAM at address
# data is byte list
def write_cpu_ram(self, address, data):
# First, set write address and length
# Encode values as sysex
addr = address.to_bytes(4, byteorder='little')
leng = len(data).to_bytes(4, byteorder='little')
syx_al = syx_enc(addr+leng)
# Send first message
msg = Message('sysex', data=self.sysex_head+[0x53]+syx_al)
self.outport.send(msg)
response = self.sysex_response()
# Ignore response for now
# UPDATE - test for success
# Now send data to write
# Encode data as sysex
syx_dat = syx_enc(data)
# Send final message
msg = Message('sysex', data=self.sysex_head+[0x54]+syx_dat)
if len(msg) > 0x200:
response = self.workaround_long_sysex(msg)
else:
self.outport.send(msg)
response = self.sysex_response()
return response
# Get IFX preset from CPU RAM at index ifx_idx
# Returns preset as bytearray
# Uses get_cpu_ram for now
# UPDATE - Add firmware hack for specific sysex function
def get_ifx(self, ifx_idx):
if ifx_idx > 99 or ifx_idx < 0:
logging.warning('IFX index out of range - must be >= 0 & < 100.')
return
# Calculate IFX preset address
ifx_base = 0xc00a80f0
ifx_leng = 0x20c
ifx_addr = ifx_base + ifx_leng * ifx_idx
# Read IFX preset from CPU RAM
ifx = self.read_cpu_ram(ifx_addr, ifx_leng)
return ifx
# Set IFX preset in CPU RAM at index ifx_idx
# ifx is byte list
# Uses write_cpu_ram for now
# UPDATE - Add firmware hack for specific sysex function
def set_ifx(self, ifx_idx, ifx):
if ifx_idx > 99 or ifx_idx < 0:
logging.warning('IFX index out of range - must be >= 0 & < 100.')
return
# Calculate IFX preset address
ifx_base = 0xc00a80f0
ifx_leng = 0x20c
ifx_addr = ifx_base + ifx_leng * ifx_idx
# Write IFX preset from CPU RAM
# Writing in two halves fails less often
ifx_a = ifx[:0x100]
self.write_cpu_ram(ifx_addr, ifx_a)
ifx_b = ifx[0x100:]
self.write_cpu_ram(ifx_addr+0x100, ifx_b)
return
# Add new IFX preset, increasing total count
# ifx is byte list
def add_ifx(self, ifx):
# Get current max IFX index
ifx_idx = self.read_cpu_ram(0xc003efdc, 1)[0]
if ifx_idx > 99 or ifx_idx < 0:
logging.warning('IFX index out of range - must be >= 0 & < 100.')
return
# Set IFX preset data
self.set_ifx(ifx_idx, ifx)
# Increase limits for menu and saved parameters
# UPDATE - Add firmware hack to set these values in one location
self.write_cpu_ram(0xc003efdc, [ifx_idx+1])
self.write_cpu_ram(0xc0048f80, [ifx_idx])
self.write_cpu_ram(0xc0049ef0, [ifx_idx])
self.write_cpu_ram(0xc004a1f8, [ifx_idx])
self.write_cpu_ram(0xc009814c, [ifx_idx])
self.write_cpu_ram(0xc0098150, [ifx_idx+1])
self.write_cpu_ram(0xc0098188, [ifx_idx])
self.write_cpu_ram(0xc0098194, [ifx_idx+1])
self.write_cpu_ram(0xc00980e8, [ifx_idx])
self.write_cpu_ram(0xc00980ec, [ifx_idx+1])
self.write_cpu_ram(0xc009809c, [ifx_idx+1])
self.write_cpu_ram(0xc009811c, [ifx_idx+1])
self.write_cpu_ram(0xc0098138, [ifx_idx+1])
return
# Get groove template from CPU RAM at index gv_idx
# Returns preset as list of bytearray
# Uses get_cpu_ram for now
# UPDATE - Add firmware hack for specific sysex function
def get_groove(self, gv_idx):
if gv_idx > 127 or gv_idx < 0:
logging.warning('Groove index out of range - must be >= 0 & < 128.')
return
# Calculate groove template address
gv_base = 0xc0143b00
gv_leng = 0x140
gv_addr = gv_base + gv_leng * gv_idx
# Read groove template from CPU RAM
gv = self.read_cpu_ram(gv_addr, gv_leng)
return gv
# Set Groove template in CPU RAM at index gv_idx
# gv is byte list
# Uses write_cpu_ram for now
# UPDATE - Add firmware hack for specific sysex function
def set_groove(self, gv_idx, gv):
if gv_idx > 127 or gv_idx < 0:
logging.warning('Groove index out of range - must be >= 0 & < 128.')
return
# Calculate groove template address
gv_base = 0xc0143b00
gv_leng = 0x140
gv_addr = gv_base + gv_leng * gv_idx
# Write Groove template from CPU RAM
self.write_cpu_ram(gv_addr, gv)
return
# Add new groove template, increasing total count
# gv is byte list
def add_groove(self, gv):
# Get current max groove index
gv_idx = self.read_cpu_ram(0xc007bb88, 1)[0]
if gv_idx > 127 or gv_idx < 0:
logging.warning('Groove index out of range - must be >= 0 & < 128.')
return
# Set groove template data
self.set_groove(gv_idx, gv)
# Increase limits for menu and saved parameters
# UPDATE - Add firmware hack to set these values in one location
self.write_cpu_ram(0xc0049da4, [gv_idx])
self.write_cpu_ram(0xc007bb90, [gv_idx])
self.write_cpu_ram(0xc007bb88, [gv_idx+1])
self.write_cpu_ram(0xc007bb94, [gv_idx+1])
return
if __name__ == '__main__':
main()