-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathysfc_serializer.py
More file actions
5288 lines (4810 loc) · 259 KB
/
Copy pathysfc_serializer.py
File metadata and controls
5288 lines (4810 loc) · 259 KB
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
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
"""
YSFC Forge — Serializer
Parser, validator och encoder för Yamaha MODX M / ESP plugin / Montage M Y2L-format
(Performance container med EPFM/DPFM-chunks och engine-data-pool).
Referensdokument: YSFC_FORGE_FULL_CONTEXT.md
Stjärnbetyg:
★★★★★ Binärverifierat med testfil(er)
★★★★☆ Härledd från officiell källdata (Effect Type List, MIDI-tabell)
★★★☆☆ Predikterat från etablerat mönster
[STRUKT] Strukturellt identifierat, ej UI-mappat
[INTERN] MODX-internt fält, ignoreras vid editing
Förkortningar:
u8 unsigned 8-bit byte
u16le unsigned 16-bit little-endian (lo + hi*256)
c64 center=64 (raw = UI + 64)
c128 center=128 (raw = UI + 128)
c256 center=256 (u16le, raw = UI + 256)
c50 center=50 (raw = UI + 50)
c504 center=504 (u16le, AN-X pitch cents)
direct raw = UI-värde direkt
bool 0=Off, 1=On
Adress-bas:
abs absolut offset från performance-blobens början
PART+N offset relativt PART_BLOCK_START=6708
FMX_OP+N relativt FMX_OP1_BASE=12676 (stride=123)
ELEM+N relativt AWM2_ELEM1_BASE=12532 (stride=313)
ANX_OSC+N relativt ANX_OSC1_BASE=12638 (stride=125)
DRUM_KEY+N relativt DRUM_KEY1_BASE=12469 (stride=68)
Kritiska metadata-bytes:
blob[+6695] Fysisk/template Part-slot count (1..16); INTE logiskt source-Part-count
blob[+6700] Engine Type Part 1 (0=AWM2, 1=Drum, 2=FMX, 3=ANX)
blob[+12464], blob[+12465] Part 2 engine-prefix i multi-part-filer
"""
from math import log2
from pathlib import Path
from typing import Any, Union
# ─────────────────────────────────────────────────────────────────────────
# Optional: ysfc_enums-paketet
#
# Om paketet är installerat (eller ligger i samma directory) får serializern
# tillgång till stora referenstabeller för UI-namn:
# - WAVEFORMS (7635 AWM2-waveforms med namn + kategori)
# - PERFORMANCES (3427 factory performances)
# - ARPEGGIOS (10922 arpeggio-namn)
# - CONTROLLER_SOURCES (42), CONTROLLER_DESTINATIONS (414)
# - LFO_DESTINATIONS (70), KEY_CONTROLLER_DESTINATIONS (59)
# - Engine enums: AWM2_FILTER_TYPES, ANX_OSC_WAVEFORMS, FMX_ALGORITHMS, etc.
# - FX_TYPES (103 effekter med kategori, slot-info, msb/lsb)
#
# Paketet är optional — serializern fungerar fullt ut utan det.
# Om paketet finns: HAS_ENUMS=True och konstanter exponeras via
# `ENUMS`-namespace för enkel åtkomst i editor-kod.
# ─────────────────────────────────────────────────────────────────────────
try:
import ysfc_enums as ENUMS
HAS_ENUMS = True
ENUMS_VERSION = getattr(ENUMS, '__version__', 'unknown')
except ImportError:
ENUMS = None
HAS_ENUMS = False
ENUMS_VERSION = None
# ── FIELD_REGISTRY ────────────────────────────────────────────────────────
NOISE = {3, 35, 36, 63, 399, 710, 711, 6735, 6736, 6737, 7411, 7412}
# Timestamp/checksum bytes updated by MODX on every Store — ignored in diffs:
MODX_TIMESTAMP_BYTES = {23, 24, 6724, 6725} # confirmede
# CA+17 (abs = CA_PART_BASE + idx*22 + 17) is also MODX-internal —
# ignored during patch-editing (confirmednot visible in ESP)
# Drum-key collateral bytes — updated on any drum-key edit:
DRUM_COLLATERAL_BYTES = {6715, 6716, 6721} # ignore for round-trip on Drum
# ── SUB-BLOB STRUCTURE ─────────────────
# Y2L blob = sequence of sub-blobs. Each sub-blob has a 27-byte header
# (4 length prefix + 18 name + 1 null + 4 hash) followed by payload.
# Verified over 10 test files × 4 engines × 5 parts.
SUBBLOB_COMMON_SIZE = 6701 # Sub-blob 1 (Common): ALWAYS this size
SUBBLOB_DEFAULT_SIZE = 5765 # Empty placeholder sub-blob: ALWAYS this size
SUBBLOB_HEADER_SIZE = 27 # 4 + 18 + 1 + 4 bytes
PART1_SUBBLOB_START = 6701 # = SUBBLOB_COMMON_SIZE
PART1_PAYLOAD_START = 6728 # = 6701 + 27
# Per-engine engine-data sizes (in last sub-blob, in addition to 5765 default body):
ENGINE_DATA_SIZES = {
'ANX': 684, # AN-X engine (OSC1+OSC2+OSC3+Filter+EG+Modifier+Pitch EG/LFO)
'FMX': 1143, # FM-X engine (8 operators + matrices + envelopes)
'AWM2': 2503, # AWM2 engine (Element 1, sometimes 2-8)
'DRUM': 4963, # Drum engine (73 keys × 68 bytes + headers)
}
# When a part is "edited", engine block grows by +5 bytes (extra footer/checksum)
def subblob_start(part_idx: int) -> int:
"""Return abs offset of sub-blob START for given part index (0..15).
part_idx=0 means Part 1 in UI (always present at sub-blob 2)."""
if not (0 <= part_idx < 16):
raise ValueError(f"part_idx must be 0..15: {part_idx}")
return SUBBLOB_COMMON_SIZE + part_idx * SUBBLOB_DEFAULT_SIZE
def payload_start(part_idx: int) -> int:
"""Return abs offset of sub-blob PAYLOAD start (= header end) for given part."""
return subblob_start(part_idx) + SUBBLOB_HEADER_SIZE
def part_field_abs(part_idx: int, payload_offset: int) -> int:
"""Return abs offset for any per-part field given its payload offset.
payload_offset is engine-agnostic and identical across all 16 parts.
Examples: Volume payload_offset=103, Pan=105, AEG attack=141, etc."""
return payload_start(part_idx) + payload_offset
def rel_part_to_payload(rel_part: int) -> int:
"""Convert old-style 'rel_part' (offset from PART_BLOCK_START=6708) to
new payload-offset. Difference is 20 bytes (= 6728 - 6708)."""
return rel_part - 20
def payload_to_rel_part(payload_offset: int) -> int:
"""Inverse of rel_part_to_payload. Use for Part 1 abs calculations."""
return payload_offset + 20
# Common per-part field payload-offsets (verified universally):
PART_VOLUME_PAYLOAD_OFFSET = 103 # Volume: rel_part=123, abs=6831 (Part 1)
# More can be added as we verify them across multi-part files.
# ────────────────────────────────────────────────────────────────────────────
# ── ENGINE-BLOCK STRUKTUR ─────────────────────────────────────────
# Engine-area börjar @ last_subblob_start + 5765 och består av sekventiella
# per-part engine-blocks (i Part-nummer-ordning). Storleken per part-block beror
# på den engine-typ som parten använder (kan blandas i samma performance).
# Engine-block storlekar (bytes):
# First-position (Part 1): bas-storlek
# Other positions (Part N≥2 inom samma sub-blob): bas + 5 (för engine-header)
ENGINE_BLOCK_FIRST = {
'ANX': 684,
'FMX': 1143,
'AWM2': 2503,
'DRUM': 4963,
}
ENGINE_BLOCK_OTHER = {
'ANX': 689, # = 684 + 5 byte header
'FMX': 1148, # = 1143 + 5
'AWM2': 2508, # = 2503 + 5
'DRUM': 4968, # = 4963 + 5 (men 4327 i mellanliggande mixed-engine slot — edge case)
}
# Per-engine field rel-offsets within engine-block (Part 1 / "first" position):
# For Part N≥2, add 5 for engine-block header.
ENGINE_FIELDS = {
'ANX': {
'osc1_shaper': 188, # ★★★★★(range -64..+63, raw direct)
},
'FMX': {
'op1_level': 254, # ★★★★★(range 0..127, raw direct)
},
'AWM2': {
'element1_pan': 62, # ★★★★★(center=64, R30 = raw 84)
},
'DRUM': {
# Volume ligger i Part Common payload, inte engine-block
},
}
def detect_engine_from_name(subblob_name: str) -> str:
"""Identifiera engine-typ från sub-blob namn-sträng."""
n = subblob_name.strip()
if 'AN-X' in n: return 'ANX'
if 'FM-X' in n: return 'FMX'
if 'AWM2' in n: return 'AWM2'
if 'Drum' in n: return 'DRUM'
raise ValueError(f"Unknown engine in sub-blob name: {n!r}")
def parse_subblob_layout(blob: bytes) -> list:
"""Returnera lista av (offset, name, engine_type) för alla sub-blobs i blob.
Det första elementet är 'Common' (Sub 1), resten är Parts (1..N).
Använder POINTER-baserad detection (pointers vid SUBBLOB_POINTER_REL)
istället för namnsökning. Detta gör att funktionen fungerar för alla
filtyper inkl. Multi/GM (som inte har "Init "-strängar).
Engine-typer returneras som versaler ('AWM2', 'ANX', 'FMX', 'DRUM') för
att vara konsistent med ENGINE_BLOCK_FIRST/ENGINE_BLOCK_OTHER-nycklar.
"""
matches = []
max_part = get_max_active_part(blob)
if max_part == 0:
return matches
# Sub-blob 1 är alltid Common
matches.append((0, 'Common', 'Common'))
# Härled engine-typ per part via pointer-modellen:
# - Part N's egen engine = vad Part N-1:s pointer säger om "nästa"
# - Part 1's egen engine = vad sista parts marker säger
engines = ['???'] * max_part
# Part 1 från sista parts marker
is_last_n, part1_engine = read_subblob_pointer(blob, max_part)
if is_last_n:
engines[0] = part1_engine
# Part 2..N från föregående parts pointer
for n in range(1, max_part):
is_last, next_engine = read_subblob_pointer(blob, n)
if not is_last:
engines[n] = next_engine
# Normalisera engine-namn till versaler för konsistens med ENGINE_BLOCK-dict
# (read_subblob_pointer returnerar 'Drum' men ENGINE_BLOCK-dict har 'DRUM')
def normalize(e):
return {'Drum': 'DRUM', 'ANX': 'ANX', 'AWM2': 'AWM2', 'FMX': 'FMX',
'ANx': 'ANX', 'FMx': 'FMX'}.get(e, e.upper())
engines = [normalize(e) for e in engines]
# Bygg matches: (offset, name, engine_type)
for i in range(max_part):
offset = SUBBLOB_COMMON_SIZE + i * SUBBLOB_DEFAULT_SIZE
engine = engines[i]
# Namn-fältet kan finnas eller saknas (Multi/GM har inga "Init "-strängar)
# För kompatibilitet sätter vi ett syntetiskt namn baserat på engine
name = f'Part{i+1}({engine})'
matches.append((offset, name, engine))
return matches
def parse_subblob_layout_legacy(blob: bytes) -> list:
"""LEGACY: Returnera lista of (offset, name, engine_type) för alla sub-blobs.
Använder "Init "-strängsökning. Fungerar för 1-part och multi-part-filer
där varje sub-blob har "Init Normal (XXX)" eller "Init Drum" som namn.
Fungerar INTE för Multi/GM (som har "Multi/GM" som performance-namn istället).
Behållen för bakåtkompatibilitet. Använd parse_subblob_layout() istället."""
matches = []
pos = 0
while True:
idx = blob.find(b'Init ', pos)
if idx < 0: break
if idx >= 4 and blob[idx-4:idx] == b'\x00\x00\x00\x15':
name_end = blob.find(b'\x00', idx)
name = blob[idx:name_end].decode('ascii', errors='replace').rstrip()
try:
engine = detect_engine_from_name(name)
except ValueError:
engine = 'UNKNOWN'
matches.append((idx-4, name, engine))
pos = idx + 1
return matches
def part_engine_block_start(blob: bytes, part_idx: int) -> int:
"""Returnerar abs offset för början av Part N's engine-block.
part_idx: 0-indexerad (0 = Part 1 i UI).
Tar hänsyn till mixed engines.
BEGRÄNSNING: Funktionen ANTAR att alla parts är AKTIVT REDIGERADE
(vilket är vanligt i ESP-filer från en patch editor). I rå-export från
MODX där "tomma" parts har varierande engine-block-storlekar fungerar
inte denna formel direkt — i de fallen behöver man läsa engine-block
storlek från sub-blob-data dynamiskt. Behöver mer testdata för att
lösa edge-case helt."""
layout = parse_subblob_layout(blob)
# layout[0] = Common, layout[1..] = Parts 1..N
if part_idx + 1 >= len(layout):
raise ValueError(f"part_idx {part_idx} not present in blob")
last_subblob_start = layout[-1][0]
engine_area_start = last_subblob_start + 5765
# Beräkna offset genom att summera engine-blocks för parts 0..part_idx-1
offset = 0
for i in range(part_idx):
engine = layout[i+1][2] # Part i+1's engine-typ (layout[0] är Common)
if i == 0:
offset += ENGINE_BLOCK_FIRST.get(engine, 0)
else:
offset += ENGINE_BLOCK_OTHER.get(engine, 0)
return engine_area_start + offset
def part_engine_field(blob: bytes, part_idx: int,
engine_field_offset_p1: int) -> int:
"""Returnerar abs offset för engine-fält i given part.
engine_field_offset_p1: rel-offset i Part 1's engine-block (utan +5 header)."""
block = part_engine_block_start(blob, part_idx)
if part_idx == 0:
return block + engine_field_offset_p1
return block + 5 + engine_field_offset_p1
# ────────────────────────────────────────────────────────────────────────────
FMX_PART_STRIDE = 6913
FMX_OP_STRIDE = 123
FMX_OP1_BASE = 12676 #
AWM2_PART_STRIDE = 8273
AWM2_ELEM_STRIDE = 313
AWM2_FILTER_TYPES = [
"LPF24D","LPF18D","LPF12D","LPF6D","LPF+HPF",
"HPF24D","HPF18D","HPF12D","HPF6D",
"BPF12D×2","BPF6D×2","BPF12A",
"LPF24A","LPF18A","LPF12A","HPF24A","HPF12A",
"BPF12A×2","BPF6A×2","LPF24A+HPF","BPFmono","THRU",
] # 22 types; verified: 0=LPF24D,4=LPF+HPF,7=HPF12D,21=THRU
AWM2_ELEM1_BASE = 12532 #
ANX_PART_STRIDE = 6454
ANX_OSC_STRIDE = 125
ANX_OSC1_BASE = 12638
ANX_FILTER_BASE = 13019
ANX_AEG_BASE = 12565 # +0:Attack u8, +2:Decay u8, +4:Sustain u16LE, +6:Release u8, +8:TimeVel u16LE c=256
PART_COMMON = dict(
monoPoly=6751, volume=6843, pan=6845,
# OBS: Tidigare adresser för AEG/FEG-offsets här (6861-6875) var FELAKTIGA för AWM2.
# (Part Common rel +144..+158). Se PART_COMMON_REL nedan för korrekta AWM2-adresser:
# awm2PartAegOffsetAttack_rel = 144 → abs 6845
# awm2PartAegOffsetDecay_rel = 146 → abs 6847
# awm2PartAegOffsetSustain_rel = 148 → abs 6849
# awm2PartAegOffsetRelease_rel = 150 → abs 6851
# awm2PartFegOffsetAttack_rel = 152 → abs 6853
# awm2PartFegOffsetDecay_rel = 154 → abs 6855
# awm2PartFegOffsetSustain_rel = 156 → abs 6857
# awm2PartFegOffsetRelease_rel = 158 → abs 6859
# Adresserna 6861..6875 är troligen för annan engine eller annat fält (ej verifierat).
filterFEGDepth=6877, filterCutoff=6879, filterResonance=6881,
)
PART_DETUNE_BASE = 6929
PART_NOTESHIFT = 6931
# FM-X Part-level LFO fields (abs addresses, )
# 1st LFO: TempoSync/Loop i PART_COMMON (abs 6770-6771), övriga i LFO-subtabell
FMX_PART_LFO = dict(
fmxPartLfoTempoSync=6770, # abs, u8 bool 0=Off,1=On, default=0 ★★★★★
fmxPartLfoLoop=6771, # abs, u8 bool INVERTED 0=On,1=Off, default=0=On ★★★★★
fmxPartLfoWave=7201, # abs, u8 enum 0-12, default=0=Triangle ★★★★★
# 0=Triangle, 1=Triangle+, 2=SawUp, 3=SawDown, 4=Squ1/4, 5=Squ1/3,
# 6=Square, 7=Squ2/3, 8=Squ3/4, 9=Trapezoid, 10=S&H1, 11=S&H2, 12=User
fmxPartLfoSpeed=7203, # abs, u8 direct, default=32 ★★★★★
fmxPartLfoTempoNote=7205, # abs, u8 table-index raw=list_idx+5, default=11=1/4 ★★★★★
# See FMX_LFO_TEMPONOTE dict for complete table (raw 5-24 = 1/16 to 1/4×64)
fmxPartLfoDelay=7207, # abs, u8 direct, default=0 ★★★★★
fmxPartLfoFadeIn=7209, # abs, u8 direct, default=0 ★★★★★
fmxPartLfoHold=7211, # abs, u8 direct, default=127 ★★★★★
fmxPartLfoFadeOut=7213, # abs, u8 direct center=default=64 ★★★★★
fmxPartLfoKeyOnReset=7215, # abs, u8 enum 0=Off,1=Each,2=1st, default=2 ★★★★★
fmxPartLfoRandomSpeed=7265, # abs, u8 direct, default=0 ★★★★★
# 1st LFO Phase + Destinations — NEW★★★★★
fmxPartLfoPhase=7199, # abs, u8 enum 0-5, default=0 ★★★★★
# Phase enum (UI-confirmed):
# 0=0°(default), 1=90°, 2=120°, 3=180°(verified), 4=240°, 5=270°
# Note: NOT arithmetic 60° step — values are 0/90/120/180/240/270 (non-uniform).
fmxPartLfoDest1=7217, # abs, u8 enum (~0-78+, see destinations enum below), default=2 ★★★★★
fmxPartLfoDest1Depth=7219, # abs, u8 direct 0-127, default=0 ★★★★★
fmxPartLfoDest2=7221, # abs, u8 enum, default=4 ★★★★★
fmxPartLfoDest2Depth=7223, # abs, u8 direct, default=0 ★★★★★
fmxPartLfoDest3=7225, # abs, u8 enum, default=4 ★★★★★
fmxPartLfoDest3Depth=7227, # abs, u8 direct, default=0 ★★★★★
)
# FMX 1st LFO Destinations enum — UI-confirmed viaimage:
# InsA Param 1-24 (24 values, "Insertion A parameter targets")
# InsB Param 1-24 (24 values, "Insertion B parameter targets")
# ── Special destinations after InsB Param 24 ──
# 70 = Pan ★★★★★ verified
# 71 = 2nd LFO Speed (UI-list deduced, untested)
# 72 = Filter Cutoff ★★★★★ verified
# 73 = Resonance (UI-list deduced, untested)
# 74 = Feedback ★★★★★ verified
# 75 = Op Freq ★★★★★ verified
# 76 = Op Spectral (UI-list deduced, untested)
# 77 = Op Detune ★★★★★ verified
# 78 = Op Level ★★★★★ verified
# Default=2 likely refers to a specific InsA Param slot.
# Default=4 (Dest2/Dest3) likewise.
# UI-list start (raw 0-21 area) untested — possibly contains "Off"/"None" + other early params.
FMX_LFO_DESTINATIONS = {
# Special destinations only (verified subset)
70: 'Pan',
72: 'FilterCutoff',
74: 'Feedback',
75: 'OpFreq',
77: 'OpDetune',
78: 'OpLevel',
# UI-deduced (untested):
71: 'SecondLfoSpeed',
73: 'Resonance',
76: 'OpSpectral',
# InsA Param 1-24 and InsB Param 1-24 occupy raw values somewhere in 0-69 range.
# Exact mapping requires further testing if patch editor needs to set them.
}
# 2nd LFO (abs addresses, )
FMX_PART_2ND_LFO = dict(
# 2nd LFO COMPLETE 7/7 fields ★★★★★
fmxPart2ndLfoWave=12509, # abs, u8 enum 0-12 (samma som 1st LFO), default=0 ★★★★★
fmxPart2ndLfoSpeedNormal=12511, # abs, u8 direct, default=30 (aktiv när Extended=OFF) ★★★★★
fmxPart2ndLfoPhase=12513, # abs, u8 enum 0=0°,1=90°,2=180°,3=270°,4=360°, default=0 ★★★★★
fmxPart2ndLfoDelay=12515, # abs, u8 direct, default=0 ★★★★★
fmxPart2ndLfoKeyOnReset=12517, # abs, u8 bool 0=Off,1=On, default=0 ★★★★★
fmxPart2ndLfoExtended=12529, # abs, u8 bool 0=Off,1=On, default=1=ON ★★★★★
fmxPart2ndLfoSpeedExtended=12531, # abs, u8 direct, default=60 (aktiv när Extended=ON) ★★★★★
# Destination/Depth matrix (17 fields, abs=12547+): not mapped
# Pitch Mod ×8 OPs + Amp Mod ×8 OPs + Filter Mod ×1 = default=0 all
)
# FM-X Part PEG block (abs addresses, )
# Encoding PEG Levels: center=50 (raw = ui + 50, range -50 to +50)
# Encoding PEG Depth: enum [8,2,1,0.5]oct per raw 0-3 (8oct=default!)
# Encoding PitchKeyFollow: raw = round(pct*64/200) + 64 (AWM2-identisk)
# Encoding CenterKey: Yamaha note (C-2=0), C3=60=default
FMX_PART_PEG = dict(
fmxPegPitchVelSens=12477, # abs, u8 center=64, default=64 ★★★★★
fmxPegRandomPitch=12479, # abs, u8 direct, default=0 ★★★★★
fmxPegPitchKeyFollow=12481, # abs, u8 raw=round(pct*64/200)+64, default=96=100% ★★★★★
fmxPegCenterKey=12483, # abs, u8 Yamaha note (C-2=0), default=60=C3 ★★★★★
fmxPegInitialLevel=12485, # abs, u8 center=50, default=50 ★★★★★
fmxPegAttackLevel=12487, # abs, u8 center=50, default=50 ★★★★★
fmxPegDecay1Level=12489, # abs, u8 center=50, default=50 ★★★★★
fmxPegDecay2Level=12491, # abs, u8 center=50, default=50 ★★★★★
fmxPegReleaseLevel=12493, # abs, u8 center=50, default=50 ★★★★★
fmxPegAttackTime=12495, # abs, u8 direct, default=0 ★★★★★
fmxPegDecay1Time=12497, # abs, u8 direct, default=0 ★★★★★
fmxPegDecay2Time=12499, # abs, u8 direct, default=0 ★★★★★
fmxPegReleaseTime=12501, # abs, u8 direct, default=0 ★★★★★
fmxPegDepthVelSens=12503, # abs, u8 direct, default=0 ★★★★★
fmxPegDepth=12505, # abs, u8 enum raw 0-3: [8oct,2oct,1oct,0.5oct], default=0=8oct ★★★★★
fmxPegTimeKeySens=12507, # abs, u8 direct, default=0 ★★★★★
)
# Part-level LFO fields (PART_BLOCK_START=6708 + rel) — AWM2/generic
# Verified: PART_BLOCK offsets 74,75,505,507,509,519
PART_LFO = dict(
partLfoTempoSync=74, # u8 bool default=0 (AWM2, rel from PART_BASE)
partLfoLoop=75, # u8 bool default=0 (AWM2)
partLfoWave=505, # u8 enum default=0 (AWM2) — FM-X använder FMX_PART_LFO!
partLfoSpeed=507, # u8 direct default=32 (AWM2)
partLfoTempoNote=509, # u8 enum default=11 (AWM2)
partLfoKeyOnReset=519, # u8 direct default=2=On (AWM2)
# NOTE: FM-X Part LFO använder ANDRA absoluta adresser — se FMX_PART_LFO dict
# FM-X: Wave=7201, Speed=7203, Delay=7207, FadeIn=7209, Hold=7211,
# FadeOut=7213(center=64), KeyOnReset=7215, RandomSpeed=7265
)
# Part-level Filter fields (PART_BLOCK_START + rel)
PART_FILTER = dict(
partFilterType=4, # u8 enum (0=Thru, 2=LPF18D, ...) default=0
partFilterCutoff=5857, # u16 LE Hz default=2816
)
# AN-X Part-level LFO offsets (PART_BLOCK_START + rel)
# Verified against AN-X_00_Init_Part1_Base.Y2L (v2, 37KB, 1 perf) ✅
ANX_PART_LFO = dict(
anxPitchLfoSpeed=5795, # u16 LE direct, default=208 ✅
anxModLfoWave=6430, # u8 enum 0-4 default=2=Tri ✅
anxModLfoSpeed=6432, # u16 LE direct, default=208 ✅
anxModLfoDepth=6414, # u8 center=128 default=0 ✅
)
# AN-X Part Common: same as AWM2 PART_COMMON, shifted -12 bytes (4 fields confirmed ✅)
# Formula: AN-X_abs = AWM2_abs - 12 (verified: partPortaSW, portaTime, portaMode, volume)
ANX_PART_COMMON_SHIFT = -12
# AN-X Part Common (abs = AWM2_abs - 12, confirmed for 7+ fields ✅)
ANX_PART_COMMON = dict(
anxMonoPoly=31, # u8 bool default=1=Poly ✅ (AWM2 abs 6751-12=6739, rel+31)
anxPartPortaSW=32, # u8 bool default=1 ✅
anxVolume=123, # u8 direct default=100 ✅
anxPan=125, # u8 center=64 default=0 ✅
anxAegAttack=141, # u8 center=64 default=0 ✅ (abs 6849, ui=+20→84)
anxAegDecay=143, # u8 center=64 default=0 ✅
anxAegSustain=145, # u8 center=64 default=0 ✅
anxAegRelease=147, # u8 center=64 default=0 ✅
anxFEGDepthOffset=157, # u8 center=64 default=0 ✅ (abs 6865, ui=+50→114)
anxFilterCutoffOffset=159, # u8 center=64 default=0 ✅ (abs 6867, ui=+20→84)
anxResonanceOffset=161, # u8 center=64 default=0 ★★★★★ (abs 6869erified)
anxPortaTime=213, # u8 direct default=64 ✅
anxPortaMode=215, # u8 bool default=1=FullTime ✅
)
# FM-X Part Common (same -12 shift, confirmed for AEG ✅)
FMX_PART_COMMON = dict(
fmxMonoPoly=31, # u8 bool default=1=Poly ★★★★☆ (AWM2 abs 6751-12=6739, rel+31)
fmxVolume=123, # u8 direct default=100 ★★★★☆
fmxPan=125, # u8 center=64 default=0 ★★★★☆
fmxAegAttack=141, # u8 center=64 default=0 ✅ (abs 6849, verified)
fmxAegDecay=143, # u8 center=64 default=0 ✅
fmxAegSustain=145, # u8 center=64 default=0 ✅
fmxAegRelease=147, # u8 center=64 default=0 ✅
)
# ── DRUM ENGINE ──────────────────────────────────────
# 31 testfiler binärverifierade. Drum keys: 73 keys × 22 fields × 68 bytes stride.
# Drum keys area: [12469:17433] = 4964 bytes. Drum part-level uses ANX-style -12 shift.
DRUM_KEY1_BASE = 12469 # abs offset of drum key 1 (C0, MIDI 12)
DRUM_KEY_STRIDE = 68 # bytes per drum key (verified: SW=0x01 marker repeats every 68 b)
DRUM_KEY_COUNT = 73 # C0..C6 inclusive (MIDI 12..84)
# Per-drum-key fields (rel offset within key, abs = DRUM_KEY1_BASE + key_idx*68 + rel)
# All u8 unless marked u16le. All ★★★★★ binary-verified.
DRUM_KEY = dict(
drumKeySW = 0, # bool default=1=ON
drumKeyRcvNoteOff = 4, # bool default=0=Off
drumKeyAssignMode = 6, # enum default=1=Multi (0=Single)
drumKeyGroup = 8, # enum default=0=Off (1-26 = A-Z)
drumKeyWaveformNumber = 10, # u16le default=28
drumKeyPan = 12, # center=64 default=64=Center
drumKeyRandomPan = 14, # direct default=0 (0..127)
drumKeyAlternatePan = 16, # center=64 default=64=Center
drumKeyConnect = 22, # enum default=1=InsA (0=Thru?, 2=InsB?)
drumKeyLevel = 26, # direct default=127=max
drumKeyLevelVel = 28, # center=64 default=64
drumKeyTimeAttack = 30, # direct default=0
drumKeyTimeDecay1 = 32, # direct default=96
drumKeyTimeDecay2 = 34, # direct default=80
drumKeyLevelDecay1 = 36, # direct default=127=max
drumKeyCoarse = 38, # center=64 default=64
drumKeyFine = 40, # center=64 default=64
drumKeyPitchVel = 42, # center=64 default=64
drumKeyFilterCutoff = 44, # u16le default=1023=max
drumKeyFilterCutoffVel = 46, # center=64 default=64
drumKeyFilterResonance = 48, # direct default=0
drumKeyHpfCutoff = 50, # u16le default=0
# EQ — added(per-key 2-band EQ):
drumKeyEqType = 52, # enum: 0=2-band, 1=P.EQ, 2=Boost6, 5=Thru (3,4 untested)
drumKeyEqLowFreq = 56, # u8 logarithmic ~25 step/octave; default=54 (=62.5 Hz)
drumKeyEqLowGain = 58, # center=64, range ±24 dB; raw = 64 + UI_dB * (64/24)
drumKeyEqHiFreq = 60, # u8 logarithmic; default=231 (=7.4 kHz)
drumKeyEqHiGain = 62, # center=64, range ±24 dB
)
# Unused offsets within key: rel_key 1-3, 5, 7, 9, 11, 13, 15, 17, 19-21, 23-25,
# 27, 29, 31, 33, 35, 37, 39, 41, 43, 45, 47, 49, 51, 53-55, 57, 59, 61, 63-66.
# rel_key 18 (=90 default) and 67 (=64 default) have non-zero defaults but no UI mapping;
# likely internal padding or sub-state.
# Drum part-level fields (abs offsets, same -12 shift as ANX from AWM2 base)
DRUM_PART_COMMON = dict(
drumPartFilterCutoff = 6867, # center=64 default=64 ★★★★★ binärverifierat (Test-DRUM_Filter_Amp_Cutoff_+50.Y2L: 64→114)
drumPartResonance = 6869, # center=64 default=64 ★★★★★ binärverifierat (Test-DRUM_Filter_Amp_Resonance_+50.Y2L)
drumPitchBendUpper = 6913, # center=64 default=66 (=+2) ★★★★★ binärverifierat
drumPitchBendLower = 6915, # center=64 default=62 (=-2) ★★★★★ binärverifierat
drumDetuneHz = 6917, # u16le default=128 (=0 Hz) ★★★★★ binärverifierat (DRUM_00_Init_Detune_8Hz.Y2L: 128→208)
drumNoteShift = 6919, # center=64 default=64 ★★★★★ binärverifierat
# Part Common-fält ★★★★★ alla binärverifierade via DRUM-testkorpus (steg 114):
drumPartElemPanToggle = 6736, # bool default=1=ON
drumPartArpPlayOnly = 6802, # bool default=0=Off
drumPartMainCategory = 6815, # enum default=16 (DrumPerc=12) ★★★★★ NY (DRUM_PartSettings_MainCategory_DrumPerc.Y2L)
drumPartVelLimitLow = 6819, # direct default=1
drumPartVelLimitHigh = 6821, # direct default=127
drumPartNoteLimitLow = 6823, # direct (MIDI note) default=0=C-2
drumPartNoteLimitHigh = 6825, # direct (MIDI note) default=127=G8
drumPartVelDepth = 6827, # center=64 default=64
drumPartVelOffset = 6829, # center=64 default=64
drumPartVolume = 6831, # direct default=100
drumPartPan = 6833, # center=64 default=64
drumPartReverbSend = 6835, # direct default=0
drumPartVariationSend = 6837, # direct default=0
drumPartDryLevel = 6839, # direct default=127
drumPartOutput = 6847, # enum default=0=MainL+R, 9=USB1+2, 125=Off
# Filter AEG (Drum Part-level) ★★★★★ NYA (DRUM steg 114-tester):
drumPartFilterAegAttack = 6849, # c64 default=64 (Test-DRUM_Filter_Amp_AEG_Attack_+50.Y2L: 64→114)
drumPartFilterAegDecay = 6851, # c64 default=64 (Test-DRUM_Filter_Amp_AEG_Decay_+50.Y2L)
drumPartFilterAegSustain = 6853, # c64 default=64 (Test-DRUM_Filter_Amp_AEG_Sustain_+50.Y2L)
drumPartFilterAegRelease = 6855, # c64 default=64 (Test-DRUM_Filter_Amp_AEG_Release_+50.Y2L)
# Part Control Group ★★★★★ NYTT (DRUM_General_Pitch_PartControlGroup_A.Y2L):
drumPartControlGroup = 6903, # enum default=0=Off, 1=A, ...
drumPart2EqType = 6961, # enum part-level EQ Type (separate from per-key)
)
# Drum Part Common-fält i ESP Plugin v3.0 UI (Part Common base = abs 7392, rel-offsets)
# Drum har egen Part Common-layout som skiljer sig från AWM2/FM-X/AN-X.
# OBS: Drum Part Common rel +144/+146 är filter-fält, INTE AEG-offsets som för
# AWM2/FM-X/AN-X. Tolkningen styrs av engine_type.
DRUM_PART_COMMON_REL = dict(
drumAegAttack_rel = 126, # c64 default=64 (Filter/Amp > AEG > Attack)
drumAegDecay_rel = 128, # c64 default=64 (Filter/Amp > AEG > Decay)
drumAegSustain_rel = 130, # c64 default=64 (Filter/Amp > AEG > Sustain)
drumAegRelease_rel = 132, # c64 default=64 (Filter/Amp > AEG > Release)
drumFilterCutoff_rel = 144, # c64 default=64 (Filter/Amp > Cutoff)
drumFilterReso_rel = 146, # c64 default=64 (Filter/Amp > Resonance)
)
# Drum engine-pool/Drum Key 1-fält (utöver befintliga DRUM_KEY-mappningar)
# Verifierade via Pitch-tester på Drum Init Voice.
DRUM_KEY1_EXTRA = dict(
drumKeyConnectAbs = 13160, # = payload 12469 (= DRUM_KEY1_BASE) — Connect: Thru=0, InsA=1 default
drumKeyCoarseAbs = 13176, # = payload 12485 (Drum Key 1 rel +16) — c64 default=64
drumKeyFineAbs = 13178, # = payload 12487 — c64 default=64
drumKeyPitchVelAbs= 13180, # = payload 12489 — c64 default=64
drumAssigneMode = 13144, # bool default=1=Multi, 0=Single (Drum Key 1 assign mode)
)
def drum_key_abs(field_name: str, key_idx: int) -> int:
"""Return absolute offset for drum-key field. key_idx: 0..72 (C0..C6)."""
if field_name not in DRUM_KEY:
raise KeyError(f"Unknown drum key field: {field_name}")
if not (0 <= key_idx < DRUM_KEY_COUNT):
raise ValueError(f"key_idx out of range 0..{DRUM_KEY_COUNT-1}: {key_idx}")
return DRUM_KEY1_BASE + key_idx * DRUM_KEY_STRIDE + DRUM_KEY[field_name]
# ── PERFORMANCE COMMON — additions ────────────────────────────────
# All ★★★★★ binary-verified. Encoding 'direct' = raw = MIDI CC# (0..127).
# Common Control Numbers — abs [152:200], stride=2 per assign knob
# 15/17 ★★★★★ verified. Scene CC (92) and SuperKnob CC (95) HARD-CODED in
# MODX firmware — they do not appear in blob.
COMMON_CC = dict(
ribbonCC = 152, # direct default=16 ★★★★★
breathCC = 154, # direct default=2 ★★★★★
footCtrl1CC = 156, # direct default=11 ★★★★★
footCtrl2CC = 158, # direct default=96 ★★★★★
assignSw1CC = 160, # direct default=86 ★★★★★
assignSw2CC = 162, # direct default=87 ★★★★★
# abs 164: untested — possibly FS Assign destination (enum)
msTriggerCC = 166, # direct default=89 ★★★★★
assignKnob1CC = 168, # direct default=17 ★★★★★
assignKnob2CC = 170, # direct default=18 ★★★★★
assignKnob3CC = 172, # direct default=19 ★★★★★
assignKnob4CC = 174, # direct default=20 ★★★★★
assignKnob5CC = 176, # direct default=21 ★★★★★
assignKnob6CC = 178, # direct default=22 ★★★★★
assignKnob7CC = 180, # direct default=23 ★★★★★
assignKnob8CC = 182, # direct default=24 ★★★★★
# Scene CC (default 92) and SuperKnob CC (default 95) are HARDCODED
# in MODX firmware and not present in the blob. They show as fixed
# in ESP UI ("can not edit" indicator).
)
# Hardware Ribbon Assign mode flags ★★★★★
HW_RIBBON_ASSIGN = dict(
ribbonAssign1Mode = 30, # bool default=1=Latch, 0=Momentary
ribbonAssign2Mode = 31, # bool default=1=Latch, 0=Momentary
)
# Hardware Ribbon Control — perf-level
HW_RIBBON = dict(
ribbonModeHold = 33, # bool default=1=Reset, 0=Hold
sliderDirReverse = 57, # bool default=0=Normal, 1=Reverse
ribbonGridMode = 216, # enum default=0=Continuous, 1=5step (2=3step?)
)
# Master EQ — abs [560:593], 5 bands × 3 fields ★★★★★ KOMPLETT
# Layout per band: [Gain, Freq, Q | Type]
# Low band: Gain 560, Freq 562, Type 566 (no Q — shelving by default)
# Lo Mid: Gain 568, Freq 570, Q 572
# Mid: Gain 574, Freq 576, Q 578
# Hi Mid: Gain 580, Freq 582, Q 584
# High band: Gain 586, Freq 588, Type 592 (no Q — shelving by default)
# Encoding:
# Gain: center=64, ±24 dB, raw = 64 + UI_dB × (64/24)
# Freq: u8 logarithmic ~6 raw per octave
# Q: raw = UI_Q × 10 (default 7 = 0.7)
# Type: 0=Shelving (default Low/High), 1=Peaking
MASTER_EQ = dict(
# Low band
meqLowGain = 560, # center=64
meqLowFreq = 562, # logarithmic, default=12 (~50 Hz?)
meqLowType = 566, # 0=Shelving (default), 1=Peaking
# Lo Mid band
meqLowMidGain = 568,
meqLowMidFreq = 570, # default=20
meqLowMidQ = 572, # default=7 (=Q 0.7), raw=UI*10
# Mid band
meqMidGain = 574,
meqMidFreq = 576, # default=28
meqMidQ = 578,
# Hi Mid band
meqHiMidGain = 580,
meqHiMidFreq = 582, # default=44
meqHiMidQ = 584,
# High band
meqHighGain = 586,
meqHighFreq = 588, # default=52
meqHighType = 592, # 0=Shelving (default), 1=Peaking
# No Master EQ ON-toggle in blob — always considered active.
)
# ── COMMON FX BLOCKS — REVERB + VARIATION + MASTER FX ────────────────
# Three IDENTICAL 52-byte FX blocks, all in Common-area:
#
# REVERB [376:428] 52 bytes ★★★★★
# VARIATION [432:484] 52 bytes ★★★★★
# MASTER FX [598:650] 52 bytes ★★★★★
#
# Layout per block (52 bytes):
# rel 0-3: Header (Type + Preset/Category, 4 bytes)
# rel 4-51: 24 parameters with stride 2 (Type-specific encoding)
#
# All three blocks have separate ON/OFF toggles in the perf-status area:
# abs 34: Reverb ON/OFF (default 1=ON)
# abs 35: Variation ON/OFF (default 1=ON)
# abs 37: Master FX ON/OFF (default 0=OFF)
# Type=0 within a block also means OFF/Thru (passthrough).
# Reverb FX block — KOMPLETT ★★★★★
# Below is the layout for Type = "Shimmer Reverb"
REVERB_FX = dict(
reverbOnOff = 34, # u8 bool default=1=ON ★★★★★
reverbCategory = 376, # u8 enum (0=OFF/Thru, 96=Reverb-cat in test) ★★★★★
# abs 377: version-byte (always 1)
# abs 378-379: padding
reverbType = 380, # u16le, default 32, Shimmer Reverb=436 ★★★★★
reverbPreset = 382, # u16le, default 10, Basic=569 in test ★★★★★
# 22 parameters, stride 2, addresses [384:428]
# For Shimmer Reverb (UI-visible parameters in display order):
revParam01_shimmerGain = 384, # Shimmer Gain
revParam02_shimmerFdbk = 386, # Shimmer Fdbk
revParam03_shimmerHpf = 388, # Shimmer HPF
revParam04_shimmerLpf = 390, # Shimmer LPF
revParam05_p1p2Balance = 392, # P1/P2 Balance
revParam06_p1p2Panning = 394, # P1&P2 Panning
revParam07_pitch1 = 396, # Pitch 1
revParam08_fine1 = 398, # Fine 1
revParam09_pitch2 = 400, # Pitch 2
revParam10_fine2 = 402, # Fine 2
revParam11_crossFeedback = 404, # Cross-Feedback
revParam12_color = 406, # Color
revParam13_reverbTime = 408, # Reverb Time
revParam14_initialDelay = 410, # Initial Delay
revParam15_diffusion = 412, # Diffusion
revParam16_size = 414, # Size
revParam17_p1p2DlyOfs = 416, # P1&P2 Dly Ofs
revParam18_modDepth = 418, # Mod Depth
revParam19_modSpeed = 420, # Mod Speed
revParam20_amDepth = 422, # AM Depth
revParam21_amFreq = 424, # AM Freq
revParam22_amWaveform = 426, # AM Waveform (enum)
# NB: UI shows 23 params for Shimmer Reverb (incl AM LR Phase). The
# 23rd (AM LR Phase) may share rel 50 or be at rel 52 (outside block).
# Most likely AM Waveform + AM LR Phase are encoded together at one slot.
)
# Other Reverb Types use these same 22 slots with different parameter meanings.
# Variation FX block — KOMPLETT ★★★★★
# Identical structure to Master FX (same 24-param mold).
# Below is the layout for Type = "M/S EQ Compressor" (matches Master FX).
VARIATION_FX = dict(
variationOnOff = 35, # u8 bool default=1=ON ★★★★★
variationType = 432, # u8 enum (0=OFF/Thru, M/S EQ Compressor=80) ★★★★★
# abs 433: ? (preset/version-byte, value 8 for "Wide Side" preset)
# abs 434-435: padding
# 24 parameters, stride 2, addresses [436:484]
# For M/S EQ Compressor (matches Master FX layout):
varParam01_msBalance = 436,
varParam02_mThreshold = 438,
varParam03_mMakeupGain = 440,
varParam04_sThreshold = 442,
varParam05_sMakeupGain = 444,
varParam06_stereoExpand = 446,
varParam07_compType = 448,
varParam08_mCompCurve = 450,
varParam09_sCompCurve = 452,
varParam10_mGain = 454,
varParam11_sGain = 456,
varParam12_eqPosition = 458,
varParam13_mEqLowFreq = 460,
varParam14_mEqLowGain = 462,
varParam15_mEqLowQ = 464,
varParam16_mEqHighFreq = 466,
varParam17_mEqHighGain = 468,
varParam18_mEqHighQ = 470,
varParam19_sEqLowFreq = 472,
varParam20_sEqLowGain = 474,
varParam21_sEqLowQ = 476,
varParam22_sEqHighFreq = 478,
varParam23_sEqHighGain = 480,
varParam24_sEqHighQ = 482,
)
# Other Variation Types use these same 24 slots with different parameter meanings.
# Master FX block — KOMPLETT ★★★★★
# Layout: 1 toggle + 1 Type byte + 1 padding + 24 parameters (stride 2)
# The 24 parameters change meaning depending on Type.
# Below is the layout for Type = "M/S EQ Compressor" (Type-enum=80).
MASTER_FX = dict(
masterFxOnOff = 37, # u8 bool default=0=OFF ★★★★★
masterFxType = 598, # u8 enum default=32 (M/S EQ Compressor=80) ★★★★★
# abs 600: untested (possibly Preset selector or padding)
# 24 params, stride 2, addresses [602:650]
# These map to Type-specific UI parameters; for M/S EQ Compressor:
msfxParam01_msBalance = 602, # M/S Balance
msfxParam02_mThreshold = 604, # M Threshold
msfxParam03_mMakeupGain = 606, # M Makeup Gain
msfxParam04_sThreshold = 608, # S Threshold
msfxParam05_sMakeupGain = 610, # S Makeup Gain
msfxParam06_stereoExpand = 612, # Stereo Expand
msfxParam07_compType = 614, # Comp Type (Hard/Soft enum)
msfxParam08_mCompCurve = 616, # M Comp Curve
msfxParam09_sCompCurve = 618, # S Comp Curve
msfxParam10_mGain = 620, # M Gain
msfxParam11_sGain = 622, # S Gain
msfxParam12_eqPosition = 624, # EQ Position
msfxParam13_mEqLowFreq = 626, # M EQ Low Freq
msfxParam14_mEqLowGain = 628,
msfxParam15_mEqLowQ = 630,
msfxParam16_mEqHighFreq = 632,
msfxParam17_mEqHighGain = 634,
msfxParam18_mEqHighQ = 636,
msfxParam19_sEqLowFreq = 638,
msfxParam20_sEqLowGain = 640,
msfxParam21_sEqLowQ = 642,
msfxParam22_sEqHighFreq = 644,
msfxParam23_sEqHighGain = 646,
msfxParam24_sEqHighQ = 648,
)
# NB: Other Master FX Types (Reverb, Delay, etc) use the same 24 parameter
# slots but with different parameter meanings. UI shows correct labels
# automatically based on Type. Encoding per-parameter is also Type-specific.
# Audio In block — Common-area ★★★★★ (Performance-level)
AUDIO_IN = dict(
audioInInsASwitchCommon = 48, # u8 bool default=1=ON ★★★★★ (Common-area UI)
audioInInsBSwitchCommon = 49, # u8 bool default=1=ON ★★★★★
audioInVolume = 766, # u8 direct default=100 ★★★★★
# NB: same byte also exposed in UI as
# "EF AD Output Level" — UI-aliasing
audioInPan = 768, # u8 c64 default=64=Center ★★★★★
audioInRevSend = 770, # u8 direct default=0 ★★★★★ (TEST: Audio_Rev_50)
audioInVarSend = 772, # u8 direct default=0 ★★★★★ (TEST: Audio_Var_50)
audioInInsConnect = 774, # u8 enum 1=A→B (default), 2=B→A ★★★★★
# abs 776: untested
audioInDryLevel = 778, # u8 direct default=127 ★★★★★
# Audio In Mute + Solo: UI-state, EJ persisterat i blob (verifierat TEST5R3-AUDIO_MUTE_ON)
)
# Per-Part Audio In Insertion switches — ★★★★★ (sub-blob 2 +33, +34)
# Verifierat med TEST5R3-T1a/T1b (AWM2 Init Voice, Common/Audio/Insertion A/B toggles)
# Detta är SEPARAT från audioInInsASwitchCommon — UI har två paths för "samma" funktion
PART_COMMON_AUDIO_IN = dict(
audioInInsASwitchPart_rel = 33, # rel sub-blob 2 start = abs 6734 för Part 1
audioInInsBSwitchPart_rel = 34, # rel = abs 6735 för Part 1
)
def get_part_audio_in_switch_addr(part_idx, switch='A'):
"""Returnerar abs address för Per-Part Audio In Insertion switch."""
sub_blob_start = 6701 + (part_idx - 1) * 5765
rel = 33 if switch == 'A' else 34
return sub_blob_start + rel
# Common FX Returns + Routing — KOMPLETT ★★★★★
COMMON_FX_ROUTING = dict(
revReturn = 112, # u8 direct, default=64 ★★★★★
revPan = 114, # u8 c64, default=64=center ★★★★★
varReturn = 118, # u8 direct, default=96 ★★★★★
varPan = 120, # u8 c64, default=64=center ★★★★★
varToRevSend = 122, # u8 direct, default=0 ★★★★★
)
# Common Side Chain —★★★★★
COMMON_SIDE_CHAIN = dict(
sideChainMaster = 128, # u8 enum 127=OFF (default), 17=Master target ★★★★★
)
# SuperKnob Link per Scene —★★★★★
# 8 bytes at [40:48] in Common-area, plus mirror at [1717:1725] within Scene Struct 1
SUPERKNOB_LINK_BASE = 40
SUPERKNOB_LINK_SCENE_BASE = 1717 # mirror within Scene Struct 1
def get_sk_link_addr(scene, mirror=False):
"""Returns absolute address for SK Link toggle for scene 1-8.
mirror=True returns the address within Scene Struct 1 (same data, replicated)."""
base = SUPERKNOB_LINK_SCENE_BASE if mirror else SUPERKNOB_LINK_BASE
return base + (scene - 1)
# Envelope Follower —★★★★★
ENVELOPE_FOLLOWER = dict(
envFollowerMasterOutput = 68, # u8 direct default=127 ★★★★★
envFollowerGain = 780, # u8 c64 (=0 dB), default=64 ★★★★★
envFollowerAttack = 782, # u8 direct, default=16 ★★★★★
envFollowerRelease = 784, # u8 direct, default=7 ★★★★★
envFollowerPart1Output = 6831, # u8 direct, default=100 (Part Common-area) ★★★★★
# NB: envFollowerPart1Output lives in Part Common; per-part offset for Parts 2-16
# follows sub-blob model (rel_part = 6831 - 6708 = 123)
)
# ── PER-PART MIXER BLOCK ★★★★★ ────────────────────────────────────────────
# 5 mixer-fält per Part i Part Common, stride 2 inom blocket, stride 5765 mellan parts.
# Verifierat med TEST5R3-T2a-T2e (AWM2 Init Voice, Mixing-vy Part 1 Volume/Pan/Rev/Var/Dry).
# UI-aliasing: PartVolume = EF Part Output (samma byte i två UI-vyer).
PART_MIXER_BLOCK = dict(
partVolume_rel = 130, # u8 direct, default=100 (= EF Part Output via UI-aliasing)
partPan_rel = 132, # u8 c64, default=64=Center (TEST5R3-T2b: 64→44=L20)
partRevSend_rel = 134, # u8 direct, default=0 (TEST5R3-T2c: 0→50)
partVarSend_rel = 136, # u8 direct, default=0 (TEST5R3-T2d: 0→50)
partDryLevel_rel = 138, # u8 direct, default=127 (TEST5R3-T2e: 127→80)
)
def get_part_mixer_addr(part_idx: int, field: str) -> int:
"""Returnerar abs address för Part N's mixer-fält (Volume/Pan/RevSend/VarSend/DryLevel)."""
sub_blob_start = 6701 + (part_idx - 1) * 5765
return sub_blob_start + PART_MIXER_BLOCK[f'{field}_rel']
# ── COMMON SINGLE-FIELD TOGGLES ★★★★★ ──
COMMON_TOGGLES = dict(
portamentoMasterSwitch = 29, # u8 bool default=0=OFF ★★★★★
msMasterOn = 39, # u8 bool default=0=OFF ★★★★★
# (NB: löser 1 av 4 okartlagda toggles)
commonAudioSwitch = 50, # u8 bool default=1=ON ★★★★★
)
# ── PER-SCENE SUPERKNOB VALUES (Common-area) ★★★★★──
# 8 × u16le (= 16 bytes) at abs [184:200]
# Each scene has its own SuperKnob position
PER_SCENE_SUPERKNOB_BASE = 184
PER_SCENE_SUPERKNOB_STRIDE = 2 # u16le
def get_scene_superknob_addr(scene: int) -> int:
"""Returns abs address for SuperKnob u16le value for scene 1-8.
Default value = 512 (= mid position)."""
return PER_SCENE_SUPERKNOB_BASE + (scene - 1) * PER_SCENE_SUPERKNOB_STRIDE
# ── PART COMMON SINGLE-FIELD TOGGLES ★★★★★ ──
# These are at fixed abs offsets for Part 1.
# Per-part offset for Parts 2-16: subtract 6701 (sub-blob 2 start) + add Part N sub-blob start.
PART_COMMON_TOGGLES_PART1 = dict(
partKbdCtrlOn_part1 = 6732, # u8 bool default=1=ON ★★★★★
partPortamentoOn_part1 = 6740, # u8 bool default=1=ON ★★★★★
partArpMasterOn_part1 = 6801, # u8 bool default=1=ON ★★★★★
partTxRxChannel_part1 = 7273, # u8 enum default=0=Ch1, 1-15=Ch2-Ch16, 127=OFF ★★★★★
)
# Part Common rel offsets (relative to sub-blob start = 6701 for Part 1):
PART_COMMON_REL = dict(
partMode_rel = 30, # 6731 - 6701 ★★★★★
kbdCtrlOn_rel = 31, # 6732 - 6701
partMute_rel = 32, # 6733 - 6701 ★★★★★ (TEST5R3-T5i verified)
audioInInsASw_rel = 33, # 6734 - 6701
audioInInsBSw_rel = 34, # 6735 - 6701
elemPanToggle_rel = 35, # 6736 - 6701 ★★★★★ (Drum ElemPanToggle)
partSwitch_rel = 36, # 6737 - 6701 ★★★★★ (AN-X PartSwitch / MSMaster)
portamentoOn_rel = 39, # 6740 - 6701
# NOTE: Receive Switches (rel +43..+70, 26 switchar) — se RCV_SWITCH_*-konstanter
# i sektion 6. Inte duplicerade här. Rel +40,+41,+42 är PB/CAT/PAT-RcvSw,
# rel +44/54 är BankSelect/FC1 (delar av samma RcvSw-block).
fmxLfoLoopOff_rel = 70, # 6771 - 6701 ★★★★★ (FM-X LFO Loop verified)
# ANX Mod LFO extras — DELAR samma byte som fmxLfoLoopOff!
anxModLfoTempoSync_rel = 69, # 6770 - 6701
anxModLfoLoop_rel = 70, # 6771 - 6701 - delas med fmxLfoLoopOff
pgmChangeSw_rel = 74, # 6775 - 6701 ★★★★★
bankSelectSw_rel = 75, # 6776 - 6701 ★★★★★
panSw_rel = 89, # 6790 - 6701 ★★★★★
volExpSw_rel = 90, # 6791 - 6701 ★★★★★
arpMasterOn_rel = 100, # 6801 - 6701
arpPlayOnly_rel = 101, # 6802 - 6701 ★★★★★ (AN-X/Drum ArpPlayOnly)
arpLoop_rel = 103, # 6804 - 6701 ★★★★★ (Arp Common Loop)
arpStartQuantize_rel = 104, # 6805 - 6701 ★★★★★
arpRandomSfx_rel = 105, # 6806 - 6701 ★★★★★
arpKeyOnControl_rel = 106, # 6807 - 6701 ★★★★★
drumVelLimitLow_rel = 118, # 6819 - 6701 ★★★★★ (Drum)
drumVelLimitHigh_rel = 120, # 6821 - 6701 ★★★★★ (Drum)
velDepth_rel = 126, # 6827 - 6701 ★★★★★ (Drum VelDepth + ANX VelocityDepth - delas, )
velOffset_rel = 128, # 6829 - 6701 ★★★★★ (Drum VelOffset)
volume_rel = 130, # 6831 - 6701 (= EF Part Output UI-aliasing)
pan_rel = 132, # 6833
revSend_rel = 134, # 6835
varSend_rel = 136, # 6837
dryLevel_rel = 138, # 6839
partOutput_rel = 146, # 6847 - 6701 ★★★★★ (Drum USB1&2 routing)
# AEG Offset block ★★★★★
aegOffset_rel = 148, # 6849 (Attack offset)
anxAegOffsetDecay_rel = 150, # 6851
anxAegOffsetSustain_rel = 152, # 6853
anxAegOffsetRelease_rel = 154, # 6855
# Filter Offset block ★★★★★
anxFilterOffsetFegDepth_rel = 164, # 6865
feg_depth_offset_rel = 164, # alias
filter_cutoff_offset_rel = 166, # 6867
resonance_offset_rel = 168, # 6869
# AWM2 Part-level AEG/FEG Offsets
# OBS: AWM2 har egen layout som överlappar med AN-X-positioner men har annan semantik.
# AN-X +148/+150/+152/+154 = AEG Atk/Dec/Sus/Rel
# AWM2 +144 = AEG Offset Attack (separat från AN-X-blocket)
# AWM2 +152 = FEG Offset Attack (delar adress med AN-X AEG Sustain)
# AWM2 +154 = FEG Offset Decay (delar adress med AN-X AEG Release)
# Shared Part-level AEG/FEG Offsets
# AEG-blocket (rel +144..+150) är DELAT mellan ALLA TRE ENGINES (AWM2, FM-X, AN-X)
# — verifierat med +50-tester på alla tre engines.
# FEG-blocket (rel +152..+158) är AWM2-only — FM-X och AN-X har egna FEG-strukturer
# direkt i engine-poolen.
#
# Layout: 2-byte stride per fält, alla c64 (default 64).
# Tester använder +50 (raw 64 → 114) för enkel detektion.
#
# OBS — ESP-pluginen har TVÅ skilda UI-platser för "AEG Attack" i AN-X:
# 1) [PART] General/Pitch > Part Settings > AEG Offset > Atk/Dec/Sus/Rel
# → skriver till rel +144..+150 (Part Common, c64) — DELAT med AWM2/FM-X
# 2) [PART] Filter/Amp > AMP > AEG > Atk/Dec/Sus/Rel
# → skriver till AN-X engine-pool 12549..12555 (direct) — AN-X-specifikt
# Dessa är ALLA verifierade ★★★★★ men är två separata kontroller med olika
# encoding och olika semantik.
sharedPartAegOffsetAttack_rel = 144, # 6845
sharedPartAegOffsetDecay_rel = 146, # 6847
sharedPartAegOffsetSustain_rel = 148, # 6849
sharedPartAegOffsetRelease_rel = 150, # 6851
awm2PartFegOffsetAttack_rel = 152, # 6853
awm2PartFegOffsetDecay_rel = 154, # 6855
awm2PartFegOffsetSustain_rel = 156, # 6857
awm2PartFegOffsetRelease_rel = 158, # 6859
# Backward compat-aliaser för AWM2-specifika anrop:
awm2PartAegOffsetAttack_rel = 144,
awm2PartAegOffsetDecay_rel = 146,
awm2PartAegOffsetSustain_rel = 148,
awm2PartAegOffsetRelease_rel = 150,
# Ex Elem Sw — DELAR BYTE med arpRandomSfx_rel @ rel +105.
# ESP Plugin v3.0 UI har en "Ex Elem Sw" toggle på Part-Settings-sidan som
# ändrar EXAKT samma byte som arpRandomSfx.
# Möjligen två UI-paneler som kontrollerar samma underliggande switch,