matriarch-firmware-fixes
Unofficial bug fixes for the Moog Matriarch firmware v1.3.0, the last release before the Matriarch was discontinued.
Unofficial. Use at your own risk. This project isn't affiliated with or endorsed by Moog Music. Flashing modified firmware may void your warranty, and the authors take no responsibility for any damage. This repository contains no Moog firmware. You patch your own copy of Moog's official v1.3.0 file.
Fixes
| Fix | Web patcher | Script | Status |
|---|---|---|---|
| Paraphonic Mixer Gate Length | Yes | tools/patch_mixgate.py | Tested on hardware, chords and staccato |
| HOLD sends sustain while the Arp/Seq is playing | Yes | tools/patch_holdcc.py | Tested on hardware, with a Yamaha DX7 on MIDI out |
Paraphonic Mixer Gate Length
Moog's v1.3.0 known-issues list says: "Paraphonic Mixer Gate Length - Mixer gate can stay high longer than keys are pressed."
In 2- and 4-note paraphonic mode, releasing a key while another key is still held keeps that oscillator sounding for another 125 ms. Staccato notes over a held note, or fast playing where keys overlap, smear together (see the Moog forum thread "Matriarch - staccato playing in paraphonic modes"). The patch shortens the hold:
--ms 5: released notes stop almost immediately (recommended)--ms 50: the v1.1.0 behaviour
The hold also decides whether keys were lifted together: when you lift a chord, its notes ring out with the release only if each key comes up within the hold time of the previous one. So the fix keeps Moog's 125 ms for a note you pressed together with a key that's still held, and uses the short hold only for the others, such as staccato notes over a held chord or the notes of a fast run. When every key of a chord is up but other keys are still held, such as a staccato chord over a held bass note, the whole chord stops after the short hold. Lifted chords ring out exactly as with Moog's firmware.
With --simple, every released note gets the short hold, chords included, as in v1.0.0 to v1.2.0 of this project. It's predictable and suits short releases, but with a short hold and a long release, a lifted chord keeps only its last note or two. The chord-aware mode is the default and recommended.
When the last key is released, the VCA envelope's release shapes the tail exactly as before.
The same MIDI part recorded on a Matriarch with the original firmware (125 ms), then with each patch (50 ms and 5 ms): a held note with short notes on top. The highlighted note ends at the same point in all three. With 125 ms it keeps sounding well past the end, with 50 ms the tail is shorter, and with 5 ms it stops where the note ends.
HOLD sends sustain while the Arp/Seq is playing
Moog's v1.3.0 known-issues list says: "ARP sends Sustain CC while Hold is active - HOLD should send sustain CC when Arp/Seq is inactive."
Since the v1.2 updates, HOLD latches the Arp/Seq when it's running and works as a sustain pedal when it isn't, sending MIDI CC 64. Firmware v1.3.0 sends CC 64 in both cases, so latching an arpeggio also sustains every note on connected gear. With the fix, connected gear sustains exactly when HOLD is on and the Arp/Seq isn't running:
| You do this | v1.3.0 sends | Patched sends |
|---|---|---|
| HOLD on, Arp/Seq stopped | Sustain on | Sustain on |
| HOLD on while the Arp/Seq plays | Sustain on | Nothing, and the pattern latches as before |
| Start the Arp/Seq while HOLD is on | Nothing, so notes keep sustaining | Sustain off, unless the sustain pedal is down |
| Stop the Arp/Seq while HOLD is on | Nothing | Sustain on |
| HOLD off | Sustain off | Sustain off |
The Matriarch's own sound, the sustain pedal and incoming MIDI sustain work exactly as before. Without the patch there's a workaround: after latching with HOLD, press and release the sustain pedal. That sends sustain off while the pattern stays latched.
How to patch and flash
You need Moog's official v1.3.0 firmware (Matriarch_Firmware_v1_3_0.syx), which comes with the Moog Firmware Updater or Moog's v1.3.0 download.
In your browser (easiest)
Open the Matriarch Firmware Patcher, choose your firmware file, pick the fixes, and download the patched file. Everything runs locally in your browser. Your file is never uploaded. The page then shows how to flash the patched file.
On the command line
You need Python 3.
- Create the patched file:
Leave outpython3 tools/patch.py Matriarch_Firmware_v1_3_0.syx Matriarch_Firmware_v1_3_0_patched.syx --mixgate 5 --hold--mixgateor--holdto apply only one of the fixes. Add--simplefor the simple mixer gate mode. The script refuses to patch anything that isn't unpatched v1.3.0. - Flash it with a SysEx tool (SysEx Librarian on macOS, Bome SendSX on Windows), following Moog's normal procedure:
- Send
Matriarch_FIRMWARE_INVALIDATE.syx. - Re-select the Matriarch as the MIDI output (it reboots).
- Send
Matriarch_FIRMWARE_ERASE.syx. - Send the patched file.
- Send
- To go back to stock, repeat step 2 with Moog's original
Matriarch_Firmware_v1_3_0.syx.
Use a direct USB cable (no hub) and don't touch the synth during the transfer. If a transfer fails, power-cycle, reconnect, and start again from INVALIDATE. The updates never touch the bootloader.
How the fixes work
Paraphonic mixer gate length
Note-off (0x88de4) doesn't close a released voice's mixer gate immediately when other keys are held. It marks every oscillator whose gate flag is 0 as release-pending with a timestamp, and the main loop (0x8a56b, at 0x8a81d) closes a pending gate once now - timestamp > engine.mixGateDelay, but only if other keys are still down. The engine init (0x8b3f0) sets that delay to 125 ms:
0008b98f 8d00 0ef4 MOVL XAR0,#0xef4
0008b991 8f01 e848 MOVL XAR4,#0x1e848 ; 125000 us
0008b994 a893 MOVL *+XAR3[AR0],XAR4 ; engine(0x10640).mixGateDelay
Because every note-off restarts the timer for all pending gates, the hold does two jobs: it delays the cut-off, and it groups releases, so a lifted chord rings out with the envelope release as long as each key comes up within the hold time of the previous one. Shortening the hold (v1.0.0 to v1.2.0 of this project) fixed staccato but cut off chord releases with a long release time.
The fix keeps Moog's 125 ms and adds two routines in unused flash:
- Note-on (
0x88ca9calls0xa2040): wraps the gate assignment0x88a24and stores the press time in the timestamp field (+6) of every oscillator that became gated. That field is only read while a release is pending. - Note-off (
0x88e65calls0xa2080, replacing Moog's "mark released gates pending" block): for a just-released oscillator, if any held oscillator was pressed within 40 ms of it, it gets Moog's window (pending 1). Otherwise it gets the short hold (pending 2), with its timestamp back-dated so the 125 ms timer runs out after the short hold. In that case, any oscillator still in Moog's window gets the short hold too, because the chord it was released from is no longer held. Otherwise oscillators already in Moog's window are refreshed as Moog does. Short ones are left alone.
The main loop, the gate closing and "keep gates open when the last key comes up" are Moog's, unchanged. The full disassembly is in tools/patch_mixgate.py.
The simple mode (--simple) changes only the 125 ms value at 0x8b991, the same patch as v1.0.0 to v1.2.0 of this project. Its builds are byte-identical to those releases.
History, from comparing v1.1.0, v1.2.3 and v1.3.0:
| Version | Mixer gate hold after key-up | Multi Trig envelope retrigger window |
|---|---|---|
| v1.1.0 | 50 ms (deadline = ts + 0xC350 at 0x8a6e3) | 2 ms |
| v1.2.3 | 125 ms (moved to engine+0xEF4, set at 0x8b901) | 4 ms |
| v1.3.0 | 125 ms | 5 ms |
The 125 ms change isn't in any release notes. Moog support has said the hold is intentional, "to avoid completely missing quickly played notes". The Multi Trig retrigger window (the stuck-envelope fixes in v1.2.x) is separate code with its own constant and isn't affected by this patch.
HOLD and sustain
The HOLD button handler (0x8c5a5) sets the HOLD parameter, then calls the shared sustain routine 0x8c56b(on, send). That routine sets the keyboard sustain state and, when send is set, sends CC 64 through the MIDI output (0x97037). The handler sets send from the global "MIDI Out Filter - Keys" without looking at the Arp/Seq:
0008c5b8 9224 MOV AL,@0x24 ; 0xc8a4 = "MIDI Out Filter - Keys"
0008c5ba 56b0 01a8 MOVB @AH,#0x1,NEQ ; AH = send CC 64
0008c5bc 92a6 MOV AL,@AR6 ; AL = new HOLD state
0008c5bd 7648 c56b LCR 0x0008c56b ; <- redirected to 0xa2000
The patch adds two short routines in empty flash at 0xa2000, after the last data in sector H (0xa1c3f). The first clears send when HOLD is switched on while the Arp/Seq is playing (0xe4c0 + 0x3f), then jumps into Moog's routine. The second wraps the Arp/Seq start/stop routine 0x90b88, which only the PLAY parameter's handler calls, and resyncs CC 64 when the Arp/Seq starts or stops while HOLD is on. The full disassembly is in tools/patch_holdcc.py.
Two details matter. The Arp/Seq latch (0xe4c0 + 0xa5) can't be used, because the HOLD parameter's handler that sets it runs later from an event queue. And the new code is covered by the firmware checksum without any extra work, because the Matriarch computes that checksum itself after an update.
Firmware format and hardware notes
- The firmware is a single SysEx message
F0 04 17 ... F7. Each 16-bit word is packed into 3 bytes (0x40|b15..12,b11..6,b5..0). - Data records are
0013 0088 0080 0000 addrLo addrHi cksum <128 words> 0000, where cksum is the 16-bit sum of the other words. The header record (0012 ...) holds the version and its own checksum at word 14. The file has no whole-image checksum. After an update, the Matriarch computes a 32-bit sum of the application area itself (0x99f30) and stores it at0xBFFFC. - The CPU is a TI TMS320F2837x (C28x + FPU), which is word-addressed. The application lives at
0x88000to0xBFFFF, and the bootloader sits below it and isn't part of updates. - Global settings sit at
0xC899 + nfor SysEx global parametern(v1.3.0). For example, paraphony mode is at0xC8D0. - Front-panel LEDs and other digital outputs go through a shift-register shadow at
0x1374A, flushed over SPI. - Panel parameters are described in a table at
0x9F380(10 words each: ID, type, RAM address, handler). Setting one stores the value at once and posts its handler to an event queue at0xF400, so the handler runs after the code that set it. - The flash erase and program routines only run during a firmware update, which erases sectors E to N. Nothing at runtime erases the application's sectors.
Tools
| File | Purpose |
|---|---|
tools/matriarch_syx.py | Parses .syx files, verifies checksums and patches words in place |
tools/patch.py | Applies one or more fixes (--mixgate MS, --simple, --hold) |
tools/patch_mixgate.py | The mixer gate fix |
tools/patch_holdcc.py | The HOLD sustain fix, with its disassembly |
tools/syx2bin.py | Extracts a flat image to load at word address 0x88000 |
tools/periph_map.py | Shows which functions touch which peripherals (from refs.tsv) |
tools/ghidra_scripts/ | ExportDecomp.java (decompiled C + references), DumpListing.java (listings) and AssemblePatch.java (assembles C28x instructions with the processor module's assembler) |
tools/listing.sh | Dumps a listing from a local Ghidra project in work/ghidra |
web/ | The website with the web patcher (index.html, style.css, and matriarch-syx.js and fixes.js, JavaScript ports of the Python tools), plus 404.html, robots.txt, sitemap.xml, llms.txt, the Markdown mirror index.html.md and images |
tests/ | Tests for both patchers against Moog's official v1.3.0 file, with expected hashes in expected.json |
The analysis setup is Ghidra 12.1.2 with the ghidra-tms320c28x processor module. Import the image from syx2bin.py as raw binary with language TMS320C28x:LE:32:default and base address 0x88000, then run the module's SetupF28377D.java CPU1 and SeedFunctions.java before auto-analysis.
Development
Python code is linted and formatted with Ruff (config in pyproject.toml). Markdown and JSON are formatted with Prettier (config in .prettierrc.json). Editors that support EditorConfig pick up the basic settings for all files from .editorconfig.
pip install ruff
ruff check . # lint
ruff format . # format Python
npm install
npm run format # format Markdown, JSON, HTML and JavaScript
The tests need Moog's official v1.3.0 file. Point MATRIARCH_FIRMWARE at it (the tests are skipped otherwise):
export MATRIARCH_FIRMWARE=/path/to/Matriarch_Firmware_v1_3_0.syx
python3 -m unittest discover -s tests -v
npm test
They check that both patchers produce files byte-identical to the expected builds. To try the web patcher locally, serve the web/ folder (python3 -m http.server -d web) and open http://localhost:8000. It doesn't work when opened directly as a file.
The CI workflow (.github/workflows/ci.yml) runs the linters and tests on every push and pull request. For the tests, it downloads Moog's official firmware from the URL their own updater uses. The file is only used for testing and is never published. When everything passes on main, the workflow deploys web/ to GitHub Pages.
Releases
Release notes live in release-notes/, one file per version (release-notes/v1.0.0.md). The first # heading becomes the release title and the rest becomes the release description. To publish a release, add the notes file, update version and date-released in CITATION.cff and version in pyproject.toml, commit, then push a matching tag:
git tag v1.0.0
git push origin v1.0.0
The Release workflow (.github/workflows/release.yml) then creates the GitHub release from the notes. Running it again for an existing release updates the notes.
Citation
To cite this project, use the "Cite this repository" button on GitHub, which reads CITATION.cff.
License
Copyright (C) 2026 Benjamin Dehli
The tools and documentation in this repository are free software, licensed under the GNU General Public License v3.0 or later. Moog Music's firmware isn't included and isn't covered by this license.
