Patch tools for the Echon 6: a working .ech6 library, a feature request, and some manual errata

Hi all! I’ve been deep in a rabbit hole with my Echon 6 and wanted to share where it’s led, plus a request for the Morphor and a few small manual corrections.

Over the past while I’ve built a Python library that reads and writes .ech6 preset files. It started as a personal patch librarian and grew into something more fun: I can now edit preset metadata (names, descriptions, categories, slot locations), generate new presets programmatically, and set a decent chunk of the sound parameters by name. I’ve been auditioning the results on hardware throughout, and it works: I’ve filled most of a bank with generated patches, first by recombining parameters from factory presets I love, and more recently by designing patches directly in code. The format has been partially mapped through diffing experiments (export a preset, change one knob, export again, compare), which is slow but effective. Happy to share the library or put it on GitHub if there’s interest, and I’d love to hear if anyone else has poked at this.

Which leads to the feature request: it would be wonderful to have the parameter layout of the voice group block inside a preset documented, or alternatively a SysEx path for reading and writing sound parameters by index. Either one would let community tools like librarians, editors and patch generators be built safely rather than by reverse engineering, and I suspect I’m not the only one who’d build things with it.

Finally, some small errata spotted in the v1.2 manual while working through all this: the MIDI CC table labels CCs 90 to 95 as LFO 3, but they appear to belong to LFO 4; the value columns for CC46 (fine tune) and CC47 (note offset) look like copy-paste errors from neighbouring rows; and page 97 still has a draft footer.

Thanks to the Morhpor team for an instrument that rewards this kind of obsession. The resonator architecture is genuinely unlike anything else I own, and the fact that the format is clean enough to work with from the outside says good things about what’s underneath.

Not the same as what you did, but I actually started mapping out timbre and settings based on what sounded like realistic modelled instruments vs synthetic ones. An X/Y table. Essentially permutationally modulated parameters and then used the output against a large model of known instruments to see if I could figure out what would get me a tone I wanted from just a visual map.

Might be some overlap here. I felt the biggest challenge beyond just blindly epxloring (which I do not want to devalue how much fun this instrument is for that) was figuring out the type of sounds that I wanted relative to what actually needed to be modulated or changed.

I’m a python/C++ person myself so would love to see where you got with your project, equally happy to push my stuff up into a community repo as well if that is something we could do for the morphor community?

Thanks for writing this up - this is exactly the kind of tinkering we hoped ECHON 6 would invite, and the fact that you got as far as you did by diffing exports says the format is at least reasonably sane, which I’m glad to hear.

On the feature request - both directions are worth doing, and they’re complementary:

Format documentation The on-device format is a versioned chunked container (payload chunk v1 today), and the voice-group layout has a clean structure we could publish later. That gets everyone off the diff-based reverse-engineering path, and the version byte gives you a clean signal to update whenever we change the layout.

Per-parameter SysEx is more work but it unlocks the live-editing case documentation alone can’t cover - real-time editors, DAW-side controllers, hardware bridges. The transport is already there (the configurator uses the same protocol and manufacturer ID), so most of the effort is enumerating parameters, deciding on the granularity for the modulation matrix, and picking sensible get/set command shapes. Realistic order: docs first, per-parameter SysEx next. If you have opinions on what you’d want that API to look like, I’d genuinely like to hear them.

On the errata, all three confirmed:

  • CCs 90–95 are LFO 4, not LFO 3 (label error).
  • CC46 / CC47 value columns are indeed miscopied from neighbouring rows.
  • Page 97 draft footer shouldn’t have shipped, sorry.

A corrected manual will go out with the next manual batch.

Thanks again! and for the kind words about the resonator. It means a lot that the format has held up under this kind of scrutiny.


Eli, thank you, this is a wonderful reply. Docs first, per-parameter SysEx next sounds exactly right to me, and I’ll happily take you up on the invitation to share thoughts on the API. Having spent some time building against the format from the outside, here’s my wishlist. All of it is offered as suggestions from one user’s vantage point, and I’m sure you’ll have better instincts on plenty of it:

  • Stable parameter IDs: get and set by ID rather than panel position, with IDs stable across firmware versions so tools don’t break on updates
  • Bulk read: a “get all parameters for a voice group” request, so editors can sync state on connect without hundreds of round trips
  • Native resolution: values in normalised form rather than quantised to 0-127, so external editors don’t lose resolution against the panel
  • Modulation matrix at slot granularity: source, destination, depth, polarity and mute in one atomic message per routing, rather than a stateful select-then-set sequence
  • A subscribe mode: panel moves emitting change messages, which is the difference between a librarian and a true bidirectional editor
  • Edit buffer vs stored preset: explicit control over which one a set touches, with a separate commit command
  • Smaller wishes: a format-version handshake before writing, and eventually a published parameter ID table in the manual appendix so the CC chart, SysEx API and file format all share one set of names

I’d also like to volunteer as a beta tester for both the format documentation and any SysEx draft. I have a working Python library for reading and writing .ech6 files, a growing corpus of generated presets running on hardware, and clearly far too much enthusiasm. And a small personal note: while I’m UK-based, I used to work for a Ghent-based company and have visited the city more than fifteen times. One of my favourite places in Europe, so it’s a pleasure to see what’s coming out of it.

Attiph, I love this, and I think our projects are two halves of the same tool. What I’ve built is the write path: named parameters in, valid preset files out. What you’ve been mapping is the part mine can’t do: which settings actually produce a wanted timbre. Between your X/Y map and my generator, “pick a point on the map, receive a loadable patch” becomes a real workflow, and your labelled listening data could steer generation toward targets instead of blind exploration, which is exactly the problem you described.

A community repo sounds great and I’d say let’s do it. My suggestion for shape: the format library and field map as the core, your timbre-mapping work as a sibling module since it’s a different concern and a C++ person shouldn’t need my Python stack to use the map, and a shared presets directory with generated patches and listening notes.

Eli, if a community repo like this is something you’d want to link from the forum once it exists, even better.

I’ll get my library cleaned up and pushed once it’s presentable, and Attiph, let’s compare notes on structure before merging your side in.

@lesjamusic , @Eli - these are both amazing responses. I love this community.

To mirror what @lesjamusic has just said, I’ll get my work cleaned up a bit over the coming days and happy to regroup when there’s some momentum at pooling all our findings into a shared community repo.

Really the key thing for me… at least coming from a software eng’ background, is ensuring the format it arrives in, does so in such a way that it doesn’t cause breaking changes on version release. Naming standards / conventions that are agreed upon early will save 100s of development hours / improve quality of life for both the development team and the consumer.

One thing to call out though, I 100% agree with the Atomic message per routing from my findings depth, polarity and mute sit in three parallel arrays which would cause a race condition for stateful select-then-set if my understanding of the architecture is correct?

  1. Voice groups surfaced in every message. Six groups, could thefile store them as six independent blocks. Any get/set needs a group index or the API would inherit the panel’s modal state.
  2. The full set of LFOs and a knob sweep will flood a DIN link pretty easily afaik. If we were to rate limit (or change coalescing) rather than emite on every move.
  3. There’s no checksumming as far as I can tell. Which means you could write / create a corrupted file and it would just silently go through. If this was surfaced in SysEx that might be a possible avenue to resolving that?
  4. Seems to be no error states, again I could be wrong. But, a set with an out-of-range value or unknown ID returns nothing versus a NAK response. I feel this is pretty important for a “librarian” tool of sorts?
  5. Finally one thing I’m less clear on atm, is a “get and set parameter by ID” API assumes everything in a preset is a knob with or per value, if each voice group holds around 33 records and 0-31 being panel destinations, that leaves 32 with a potential misbehaviour or something that’s not documented - again I am probably over complicating this more than it needs to be. But my understanding is if one were to write a patch, 32 would typically remain empty and result in an incomplete preset? The same would then be true for parameters that sit outside of the panel (note, velocity, aftertouch, etc) - So I guess the tl;dr is would the API cover the entire parameter list for the preset or JUST the sound parameters?

Peace out all! :heart_hands:

@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 :crossed_fingers:. 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

  1. Decompress with raw deflate; check magic E6PR and container version 1.
  2. Walk chunks by length; take id 1; skip unknown ids rather than failing.
  3. Per voice group: check magic VG, version 1, and sound-block length exactly 3868.
    Refuse to write if any differ.
  4. Keep float 0 of each modulation record at 0.0.
  5. Treat each note-key array as optional; stop when the payload runs out.
  6. Preserve chunks and trailing bytes you did not modify, so round-tripping is lossless.
  7. 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.

@eli, this is tremendous. Thank you, genuinely, not just for documenting it but for the quality of the documentation: the stability warnings, the checklist, the behavioural notes on quantisation and derived values are exactly the things that separate a spec you can build against from a spec you can only read. The fact that you included failure modes (“fail loudly rather than parsing optimistically”) tells me this was written by someone who’s been on the consuming end of underspecified formats.

A few delighted notes from checking it against what we’d mapped by diffing. The 32×116 record structure, the value-then-depths layout, and the parallel polarity and enable arrays all match what @attiph and I had independently converged on, which was reassuring to see confirmed. Several of our open mysteries dissolved instantly on reading: the “derived” frequency record explains an anomaly we’d flagged and couldn’t place, the feedback quantisation explains a suspiciously specific 0.84252 we’d captured in an experiment (107/127 exactly, as it turns out), and the optional note-key tail explains why one older factory preset inflates 21 bytes shorter than its siblings. And the variable-length voice group framing is a correction we needed: my library was slicing at a fixed stride that happened to survive the factory corpus, and it would have broken on exactly the multi-group presets it should handle best. The version-byte checklist is going in verbatim.

I’ll be updating my library to follow this spec properly (chunk walking, version validation, lossless round-tripping of unknown chunks) and will share it here once it’s presentable, which should also give the community repo @attiph and I discussed a solid foundation to start from.

Thanks again. Both for the document and for engaging with this the way you have. It’s rare and it’s noticed.

Damn @Eli you don’t mess about. This is gold. Nothing more to add over what @lesjamusic has said. I’ll fold this back into my local libs later this week. Been pretty slammed with the day job, so once I get a moments breather I’ll report back on here. @lesjamusic hit me up when you’re ready to move on your end.