@lesjamusic @attiph - I’ve summarized the .ech6 container layout and the caveats below.
I’ve gone through it and checked it against some factory presets, so it should be free of mistakes
. If anything here contradicts what you’ve mapped yourselves, please let us know.
Hope this helps you both and the community further for the moment.
The extra SysEX implementation TBD later.
Cheers!
ECHON 6 preset file format
Layout of an .ech6 preset, including the voice-group sound-parameter block.
Applies to firmware v1.0.0, container version 1, voice-group block version 1.
Stability. The container is a versioned format designed to be extended. The
sound-parameter block inside it is a direct image of an internal engine structure: its
field order and size are tied to the firmware build, and a future release that adds a
modulation destination will move every offset after it. Validate the version bytes and
the block length, and fail loudly rather than parsing optimistically. See
Checklist.
File identity
.ech6 files and the on-device preset_<bank>_<sub>_<preset>.bin files are the same
bytes. The download path streams the stored file verbatim.
The naming conventions differ in one dangerous way:
| Form |
Example |
Indices |
On-device .bin |
preset_0_0_0.bin |
zero-based |
Exported .ech6 |
1_1_1.ech6, 111 - Name - Category.ech6 |
one-based |
Compression
Presets are raw DEFLATE — no zlib or gzip wrapper — using a 256-byte window.
Reading:
import zlib
container = zlib.decompress(open(path, "rb").read(), -15)
Writing must stay within that window. The default wbits=-15 produces a file the
instrument cannot decode; use -9:
import zlib
def compress_preset(container: bytes) -> bytes:
c = zlib.compressobj(9, zlib.DEFLATED, -9)
return c.compress(container) + c.flush()
A stored preset is 877-1554 bytes (average 1056) for an uncompressed payload of about
23520 bytes.
Uploads are stored verbatim. The upload path does not decompress, re-compress or
validate. A badly compressed preset uploads with every appearance of success and fails
at load time.
Layout
file = DEFLATE( container )
container = "E6PR" u8:version u8:chunk_count chunk*
chunk = u8:id u16:length bytes[length] (id 1 = preset payload)
preset payload = header metadata voice_groups note_keys?
voice group = "VG" u8:version header voice_list sound_block
sound_block = 32 modulation records | non-modulated block | switches | detach flags
All integers little-endian, all floats little-endian IEEE-754 binary32, no padding in the
sound block.
Container
| Offset |
Size |
Field |
| 0 |
4 |
Magic E6PR |
| 4 |
1 |
Container version (1) |
| 5 |
1 |
Chunk count |
| 6 |
… |
Chunks: u8 id, u16 length, length bytes |
Chunk id 1 is the preset payload. Skip unknown ids using their length — this is how
later firmware adds data without breaking existing readers.
Preset payload
| Offset |
Size |
Field |
Notes |
| 0 |
1 |
Bank |
0-5 |
| 1 |
1 |
Sub-bank |
0-5 |
| 2 |
1 |
Preset |
0-5 |
| 3 |
1 |
CC in |
0 / 1 |
| 4 |
1 |
CC out |
0 / 1 |
| 5 |
1 |
Unison |
0 / 1 |
| 6 |
1 |
Note priority |
0-4 |
| 7 |
1 |
Selected voice group |
0-5 |
| 8 |
32 |
Name |
UTF-8, \0-padded |
| 40 |
1 |
Category |
|
| 41 |
96 |
Description |
UTF-8, \0-padded |
| 137 |
32 |
Author |
UTF-8, \0-padded |
| 169 |
1 |
Voice-group count |
6 |
| 170 |
… |
Voice groups |
each u16 length then that many bytes |
| … |
0-28 |
Note-key state |
optional, see below |
MIDI channel is not stored in a preset — it lives in global settings, which is why it
survives preset changes. Per-unit VCO tuning and BBD calibration are not stored either.
Voice-group block
Six blocks, each independently framed.
| Offset |
Size |
Field |
Notes |
| 0 |
2 |
Magic VG |
|
| 2 |
1 |
Block version (1) |
|
| 3 |
1 |
Group number |
0-5 |
| 4 |
1 |
CC in |
0 / 1 |
| 5 |
1 |
CC out |
0 / 1 |
| 6 |
1 |
Unison |
0 / 1 |
| 7 |
1 |
Hold |
0 / 1 |
| 8 |
1 |
Ext in |
0 / 1 |
| 9 |
1 |
Note priority |
0-4 |
| 10 |
1 |
Selected modulation source |
0-8, panel state |
| 11 |
1 |
Selected quad LFO |
0-3, panel state |
| 12 |
1 |
Voice count |
0-6 |
| 13 |
n |
Voice numbers |
one byte each, 0-5 |
| 13+n |
2 |
Sound-block length |
3868 in version 1 |
| 15+n |
3868 |
Sound-parameter block |
below |
A voice belongs to at most one group — no voice number appears in two lists — but need not
belong to any. Of the 167 factory presets, 158 assign all six voices and 9 leave one to
three unassigned. The lists are disjoint, not exhaustive.
Sound-parameter block
3868 bytes, four sections, no padding:
| Section |
Size |
Contents |
| Modulation records |
3712 |
32 records × 116 bytes |
| Non-modulated block |
116 |
29 floats |
| Switches |
31 |
one byte each |
| Detach flags |
9 |
one per modulation source |
Modulation records
32 records, indices 0-31, matching the modulation destination table in the MIDI
documentation. Each is 29 floats (116 bytes):
| Float |
Count |
Field |
| 0 |
1 |
Unused — always 0.0 |
| 1 |
1 |
Parameter value |
| 2-10 |
9 |
Modulation depth, one per source |
| 11-19 |
9 |
Modulation polarity, one per source |
| 20-28 |
9 |
Modulation enable, one per source |
Depth 0.0-1.0. Polarity exactly -1.0 or +1.0. Enable exactly 0.0 (muted) or
1.0 (active). The nine slots are indexed by modulation source.
The matrix is sparse: across the factory bank the average patch uses 19 active routings
out of 1728 slots, the busiest 65.
Non-modulated block
The 116 bytes following the modulation records.
| Float |
Field |
| 0 |
Frequency coarse |
| 1 |
Frequency fine |
| 2 |
Sustain |
| 3 |
Release |
| 4 |
Note offset (0.5 = none) |
| 5 |
Envelope modulation attenuverter |
| 6 |
Quad LFO modulation attenuverter |
| 7 |
Modwheel modulation attenuverter |
| 8 |
Velocity modulation attenuverter |
| 9 |
Aftertouch modulation attenuverter |
| 10 |
Key-follow modulation attenuverter |
| 11-19 |
Frequency fine modulation depth, per source |
| 20-28 |
Frequency coarse modulation depth, per source |
The frequency depths sit here rather than in record 3 because coarse and fine tuning are
tracked separately, while the panel presents FREQUENCY as one destination.
Switches
31 bytes:
| Index |
Switch |
Values |
| 0 |
Exciter KB reset |
0 / 1 |
| 1 |
Range |
0-2 |
| 2 |
Legato |
0 / 1 |
| 3 |
Loop |
0 / 1 |
| 4 |
Invert |
0 / 1 |
| 5 |
Pitchbend depth |
0-5 → ±1, ±2, ±3, ±5, ±7, ±12 semitones |
| 6 |
Quad LFO select |
0-3, panel state |
Then six bytes per LFO, for LFO 1-4 in order (indices 7-12, 13-18, 19-24, 25-30):
| Sub-index |
Switch |
Values |
| +0 |
LFO type |
0 sine, 1 triangle, 2 ramp, 3 square |
| +1 |
Unipolar |
0 / 1 |
| +2 |
Single shot |
0 / 1 |
| +3 |
KB reset |
0 / 1 |
| +4 |
Random smooth |
0 / 1 |
| +5 |
Sync |
0 / 1 |
Sync is last, not third. The packed order differs from the order these switches are
declared internally.
Detach flags
9 bytes, one per modulation source. 0 = attached, 1 = detached from its attenuverter.
Note-key state
Up to four arrays, in order: MIDI note, velocity, aftertouch, modwheel. Each is
u8 count (= 6) then count bytes, one per note key. In the note array, 255 means
“no note held”.
The payload may end after any of them, including before the first — the section grew
as the firmware gained features and old presets are never rewritten. In the factory bank:
146 presets carry all four arrays (28 bytes), 9 carry two (14 bytes), 12 carry only the
note array (7 bytes).
Check for remaining data before each array rather than assuming a 28-byte tail. When an
array is absent the defaults are note 60, velocity 127, aftertouch 0, modwheel 0.
Current firmware always writes all four.
Enumerations
Modulation sources
Index into every 9-element array above.
| 0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
| Envelope |
Random/LFO 1 |
Random/LFO 2 |
Random/LFO 3 |
Random/LFO 4 |
Modwheel |
Velocity |
Aftertouch |
Key follow |
Note, velocity and aftertouch appear here as sources. They are not destinations, which
is why they are absent from the table below.
Modulation destinations
| Index |
Destination |
Index |
Destination |
| 0 |
Exciter wave / ext in |
16 |
LFO 1 rate |
| 1 |
Exciter noise S&H rate |
17 |
LFO 1 mix |
| 2 |
Exciter wave / ext in / noise mix |
18 |
LFO 1 amplitude |
| 3 |
Exciter frequency (derived) |
19 |
LFO 1 random rate |
| 4 |
Exciter level |
20 |
LFO 2 rate |
| 5 |
Envelope onset delay |
21 |
LFO 2 mix |
| 6 |
Envelope attack |
22 |
LFO 2 amplitude |
| 7 |
Envelope decay |
23 |
LFO 2 random rate |
| 8 |
Resonator input filter |
24 |
LFO 3 rate |
| 9 |
Resonator feedback filter |
25 |
LFO 3 mix |
| 10 |
Resonator feedback (quantised) |
26 |
LFO 3 amplitude |
| 11 |
Resonator glide |
27 |
LFO 3 random rate |
| 12 |
Resonator fine tune |
28 |
LFO 4 rate |
| 13 |
Resonator dry / wet |
29 |
LFO 4 mix |
| 14 |
Resonator panning |
30 |
LFO 4 amplitude |
| 15 |
Resonator level |
31 |
LFO 4 random rate |
Behaviours
Frequency is derived. The value of record 3 (FREQUENCY) is ignored on load and
recomputed from the coarse and fine values in the non-modulated block. Set frequency
through floats 0 and 1 there; writing record 3’s value does nothing. Its modulation
fields are read normally. This is also why record 3 is the only value in the factory bank
outside 0.0-1.0: it is a cached intermediate, not a control position.
Float 0 of every record is dead. Never written on save, never read on load, and 0.0
throughout the factory bank. The engine keeps two copies of each value — the live one and
the one the loaded preset asked for, used so a pot can be turned without the sound
jumping — and only the live one is stored. Write 0.0.
Feedback is quantised. Destination 10 is quantised down to the nearest 1/127 step on
save; 1.0 maps exactly to 1.0. Everything else keeps full float resolution. Feedback
will therefore snap to a coarser grid after a hardware round trip. Intentional, to keep
feedback reliable and stable; the quantisation always rounds down, never up.
Panel state is stored. Selected modulation source, selected quad LFO and selected
voice group affect what the panel shows on load, not how the patch sounds.
No checksum. Magic bytes, version bytes, chunk lengths and the 3868-byte block length
are validated, so a truncated or structurally damaged file is rejected. A bit-flip inside
a float payload loads silently and yields a subtly wrong patch. SysEx transfers are
separately protected by an XOR checksum; that covers the transport, not the file itself.
No value validation. Nothing range-checks the floats, so a NaN reaches the audio
path. Keep every value finite.
Checklist
- Decompress with raw deflate; check magic
E6PR and container version 1.
- Walk chunks by length; take id 1; skip unknown ids rather than failing.
- Per voice group: check magic
VG, version 1, and sound-block length exactly 3868.
Refuse to write if any differ.
- Keep float 0 of each modulation record at
0.0.
- Treat each note-key array as optional; stop when the payload runs out.
- Preserve chunks and trailing bytes you did not modify, so round-tripping is lossless.
- Match the filename convention: zero-based for
.bin, one-based for .ech6.
Step 3 matters most. If a future firmware changes the block layout, its length will almost
certainly change with it, so a reader that checks the length fails at parse time instead of
writing malformed data into a patch slot.