Skip to content

Commit 3820ac2

Browse files
committed
Adding Korg Triton adaptation by @AJRubenstein
1 parent 658dc47 commit 3820ac2

1 file changed

Lines changed: 308 additions & 0 deletions

File tree

adaptations/Korg_Triton.py

Lines changed: 308 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,308 @@
1+
# Korg Triton Classic – Program mode adaption
2+
from typing import List
3+
4+
# ---------------- Constants ----------------
5+
6+
KORG = 0x42
7+
TRITON_SERIES = 0x50
8+
TRITON_CLASSIC = 0x05 # Only used for device identity, NOT in operational messages!
9+
10+
# Request functions
11+
MODE_REQUEST = 0x12
12+
CURRENT_PROGRAM_DUMP_REQUEST = 0x10
13+
PROGRAM_BANK_DUMP_REQUEST = 0x1C
14+
15+
# Response functions
16+
MODE_CHANGE = 0x4E
17+
CURRENT_PROGRAM_DUMP = 0x40
18+
PROGRAM_BANK_DUMP = 0x4C
19+
PARAMETER_CHANGE = 0x41
20+
DATA_DUMP_ERROR = 0x24
21+
22+
# Per TABLE 1 in MIDI implementation: PCM programs are 540 bytes (0-539)
23+
PROGRAM_DATA_SIZE = 540
24+
25+
26+
# ---------------- Metadata ----------------
27+
28+
def name():
29+
return "Korg Triton Classic"
30+
31+
32+
# ---------------- Device detection ----------------
33+
34+
def createDeviceDetectMessage(channel):
35+
return [0xF0, 0x7E, channel & 0x0F, 0x06, 0x01, 0xF7]
36+
37+
38+
def deviceDetectWaitMilliseconds():
39+
return 300
40+
41+
42+
def generalMessageDelay():
43+
# Triton is slow - give it time
44+
return 1000
45+
46+
47+
def needsChannelSpecificDetection():
48+
return True
49+
50+
51+
def channelIfValidDeviceResponse(message):
52+
if (
53+
len(message) >= 15
54+
and message[0] == 0xF0
55+
and message[1] == 0x7E
56+
and message[3] == 0x06
57+
and message[4] == 0x02
58+
and message[5] == 0x42 # KORG
59+
and message[6] == 0x50 # Triton series
60+
and message[7] == 0x00
61+
and message[8] == 0x05 # Triton Classic (model ID only here!)
62+
and message[9] == 0x00
63+
and message[14] == 0xF7
64+
):
65+
return message[2] # IMPORTANT: unmasked
66+
return -1
67+
68+
69+
# ---------------- Edit buffer ----------------
70+
71+
def createEditBufferRequest(channel):
72+
"""
73+
Request current program parameter dump (edit buffer)
74+
75+
Format: F0, 42, 3g, 50, 10, 00, F7
76+
"""
77+
return [
78+
0xF0,
79+
KORG, # 0x42
80+
0x30 | (channel & 0x0F), # 3g where g = channel
81+
TRITON_SERIES, # 0x50
82+
CURRENT_PROGRAM_DUMP_REQUEST, # 0x10 (5th byte)
83+
0x00, # Reserved
84+
0xF7
85+
]
86+
87+
88+
def isEditBufferDump(message):
89+
"""
90+
Check if message is a current program dump
91+
Expected format: F0 42 3g 50 40 0t [data] F7
92+
"""
93+
if not message or message[0] != 0xF0 or len(message) < 7:
94+
return False
95+
96+
return (
97+
message[1] == KORG
98+
and (message[2] & 0xF0) == 0x30
99+
and message[3] == TRITON_SERIES
100+
and message[4] == CURRENT_PROGRAM_DUMP # 0x40 in 5th byte position
101+
)
102+
103+
104+
def nameFromDump(message):
105+
"""
106+
Extract program name from dump
107+
Format: F0 42 3g 50 40 0t [escaped_data] F7
108+
"""
109+
if not message or len(message) < 8:
110+
return "Invalid"
111+
112+
if message[4] == CURRENT_PROGRAM_DUMP: # 0x40
113+
try:
114+
# Skip: F0(1) 42(1) 3g(1) 50(1) 40(1) 0t(1) = 6 bytes
115+
data = unescapeSysex(message[6:-1])
116+
# Program names are 16 chars at the start
117+
if len(data) >= 16:
118+
return ''.join(chr(x) if 32 <= x < 127 else ' ' for x in data[0:16])
119+
except:
120+
pass
121+
122+
return "Unknown"
123+
124+
125+
def convertToEditBuffer(channel, message):
126+
if isEditBufferDump(message):
127+
return (
128+
message[0:2]
129+
+ [0x30 | (channel & 0x0F)]
130+
+ message[3:]
131+
)
132+
raise Exception("Not an edit buffer dump")
133+
134+
135+
# ---------------- Bank handling ----------------
136+
137+
def bankDescriptors():
138+
return [
139+
{"bank": 0x00, "name": "INT-A", "size": 128, "type": "Patch"},
140+
{"bank": 0x01, "name": "INT-B", "size": 128, "type": "Patch"},
141+
{"bank": 0x02, "name": "INT-C", "size": 128, "type": "Patch"},
142+
{"bank": 0x03, "name": "INT-D", "size": 128, "type": "Patch"},
143+
]
144+
145+
146+
def createBankDumpRequest(channel, bank):
147+
"""
148+
Request program parameter dump for one bank (Program mode)
149+
150+
Format: F0 42 3g 50 1C 00kk0bbb 0ppppppp 00 F7
151+
152+
k = 0 : All Programs
153+
1 : 1 Bank Programs (Use b)
154+
2 : 1 Program (Use b & pp)
155+
b = 0-5 : Bank A-F
156+
"""
157+
kind = 0x01 # 1 Bank Program
158+
kind_bits = (kind & 0x03) << 4 # bits 5–4
159+
bank_bits = bank & 0x07 # bits 2–0
160+
161+
return [
162+
0xF0,
163+
KORG,
164+
0x30 | (channel & 0x0F),
165+
TRITON_SERIES,
166+
PROGRAM_BANK_DUMP_REQUEST, # 0x1C
167+
kind_bits | bank_bits, # 00kk0bbb
168+
0x00, # program number (ignored for bank)
169+
0x00, # reserved
170+
0xF7
171+
]
172+
173+
174+
def isPartOfBankDump(message):
175+
"""
176+
Check if message is a bank dump
177+
The Triton sends ONE large 0x4C message containing all programs
178+
"""
179+
if not message or message[0] != 0xF0 or len(message) < 8:
180+
return False
181+
182+
return (
183+
message[1] == KORG and
184+
(message[2] & 0xF0) == 0x30 and
185+
message[3] == TRITON_SERIES and
186+
message[4] == PROGRAM_BANK_DUMP # 0x4C
187+
)
188+
189+
190+
def isBankDumpFinished(messages):
191+
"""
192+
Check if we received a complete bank dump
193+
194+
The Triton sends ONE large 0x4C message with all 128 programs,
195+
terminated by F7.
196+
"""
197+
for msg in messages:
198+
if (len(msg) > 8 and
199+
msg[0] == 0xF0 and
200+
msg[1] == KORG and
201+
(msg[2] & 0xF0) == 0x30 and
202+
msg[3] == TRITON_SERIES and
203+
msg[4] == PROGRAM_BANK_DUMP and
204+
msg[-1] == 0xF7):
205+
# Found a complete bank dump message
206+
return True
207+
208+
return False
209+
210+
211+
def extractPatchesFromBank(message):
212+
"""
213+
Extract individual patches from bank dump message
214+
215+
The Triton sends one 0x4C message for a bank:
216+
F0 42 3g 50 4C [000v] [00kk0bbb] [0ppp] [escaped_data_for_all_programs] F7
217+
218+
According to TABLE 1 in the MIDI implementation, each PCM program is
219+
exactly 540 bytes (bytes 0-539). The programs are concatenated directly
220+
in the unescaped data with NO headers between them.
221+
222+
Strategy:
223+
1. Unescape the entire data block
224+
2. Split into 540-byte chunks
225+
3. Re-escape each chunk and create individual 0x40 messages
226+
"""
227+
if not isPartOfBankDump(message):
228+
raise Exception("Not a Triton program bank dump")
229+
230+
channel = message[2] & 0x0F
231+
232+
# The message structure is:
233+
# F0 42 3g 50 4C [byte5] [byte6] [byte7] [escaped_data...] F7
234+
# We need to unescape starting from byte 8
235+
escaped_data = message[8:-1] # Skip header and F7
236+
unescaped_data = unescapeSysex(escaped_data)
237+
238+
# Calculate how many programs we have
239+
num_programs = len(unescaped_data) // PROGRAM_DATA_SIZE
240+
241+
programs = []
242+
243+
for prog_num in range(num_programs):
244+
# Extract this program's data (540 bytes)
245+
start = prog_num * PROGRAM_DATA_SIZE
246+
end = start + PROGRAM_DATA_SIZE
247+
program_data = unescaped_data[start:end]
248+
249+
# Create an edit buffer dump message for this program
250+
# F0 42 3g 50 40 0t [escaped_data] F7
251+
prog_type = 0x00 # PCM type (0x00), MOSS would be 0x01
252+
253+
escaped_prog_data = escapeSysex(program_data)
254+
255+
prog_dump = (
256+
[0xF0, KORG, 0x30 | channel, TRITON_SERIES, CURRENT_PROGRAM_DUMP, prog_type]
257+
+ escaped_prog_data
258+
+ [0xF7]
259+
)
260+
261+
programs.append(prog_dump)
262+
263+
# Return all programs concatenated
264+
result = []
265+
for prog in programs:
266+
result.extend(prog)
267+
268+
return result
269+
270+
271+
# ---------------- Sysex helpers ----------------
272+
273+
def unescapeSysex(sysex: List[int]) -> List[int]:
274+
"""Unescape Korg's 7-bit MIDI encoding"""
275+
result = []
276+
i = 0
277+
while i < len(sysex):
278+
if i >= len(sysex):
279+
break
280+
msb = sysex[i]
281+
i += 1
282+
for bit in range(7):
283+
if i < len(sysex):
284+
result.append(sysex[i] | (((msb >> bit) & 1) << 7))
285+
i += 1
286+
else:
287+
break
288+
return result
289+
290+
291+
def escapeSysex(data: List[int]) -> List[int]:
292+
"""Escape data to Korg's 7-bit MIDI encoding"""
293+
result = []
294+
i = 0
295+
while i < len(data):
296+
msb = 0
297+
for bit in range(7):
298+
if i + bit < len(data):
299+
msb |= ((data[i + bit] >> 7) & 1) << bit
300+
result.append(msb)
301+
302+
for bit in range(7):
303+
if i < len(data):
304+
result.append(data[i] & 0x7F)
305+
i += 1
306+
else:
307+
break
308+
return result

0 commit comments

Comments
 (0)