matriarch-firmware-fixes, image 1 from its README
Firmware

matriarch-firmware-fixes

Loud87/100

Bug fixes for the Moog Matriarch firmware

matriarch-firmware-fixes is custom firmware for the Moog Music Matriarch, by @benjamindehli. Open source under GPL-3.0, latest release v1.4.0 (October 2026), 1 star on GitHub.

forMoog Music Matriarch·by@benjamindehli

Unclaimed. Is this yours? Sign in with GitHub as @benjamindehli to claim it.

Downloads
0
Star
1
Fork
1
Commits
33
Last commit
Get the source

No release files yet. Build from source.

GPL-3.0· benjamindehli/matriarch-firmware-fixes·benjamindehli.github.io/matriarch-firmware-fixes/·downloads come from the creator’s GitHub releases

24
Commits
5
Releases
24 commits and 5 releases in the last year
Activity · last 12 months

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

FixWeb patcherScriptStatus
Paraphonic Mixer Gate LengthYestools/patch_mixgate.pyTested on hardware, chords and staccato
HOLD sends sustain while the Arp/Seq is playingYestools/patch_holdcc.pyTested 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.

Waveforms in Logic Pro: the same MIDI part recorded with the original 125 ms hold, 50 ms and 5 ms. With 125 ms each short note keeps sounding well after it ends, with 5 ms it stops right at the end of the note.

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 thisv1.3.0 sendsPatched sends
HOLD on, Arp/Seq stoppedSustain onSustain on
HOLD on while the Arp/Seq playsSustain onNothing, and the pattern latches as before
Start the Arp/Seq while HOLD is onNothing, so notes keep sustainingSustain off, unless the sustain pedal is down
Stop the Arp/Seq while HOLD is onNothingSustain on
HOLD offSustain offSustain 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.

  1. Create the patched file:
    python3 tools/patch.py Matriarch_Firmware_v1_3_0.syx Matriarch_Firmware_v1_3_0_patched.syx --mixgate 5 --hold
    
    Leave out --mixgate or --hold to apply only one of the fixes. Add --simple for the simple mixer gate mode. The script refuses to patch anything that isn't unpatched v1.3.0.
  2. Flash it with a SysEx tool (SysEx Librarian on macOS, Bome SendSX on Windows), following Moog's normal procedure:
    1. Send Matriarch_FIRMWARE_INVALIDATE.syx.
    2. Re-select the Matriarch as the MIDI output (it reboots).
    3. Send Matriarch_FIRMWARE_ERASE.syx.
    4. Send the patched file.
  3. 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 (0x88ca9 calls 0xa2040): wraps the gate assignment 0x88a24 and 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 (0x88e65 calls 0xa2080, 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:

VersionMixer gate hold after key-upMulti Trig envelope retrigger window
v1.1.050 ms (deadline = ts + 0xC350 at 0x8a6e3)2 ms
v1.2.3125 ms (moved to engine+0xEF4, set at 0x8b901)4 ms
v1.3.0125 ms5 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 at 0xBFFFC.
  • The CPU is a TI TMS320F2837x (C28x + FPU), which is word-addressed. The application lives at 0x88000 to 0xBFFFF, and the bootloader sits below it and isn't part of updates.
  • Global settings sit at 0xC899 + n for SysEx global parameter n (v1.3.0). For example, paraphony mode is at 0xC8D0.
  • 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 at 0xF400, 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

FilePurpose
tools/matriarch_syx.pyParses .syx files, verifies checksums and patches words in place
tools/patch.pyApplies one or more fixes (--mixgate MS, --simple, --hold)
tools/patch_mixgate.pyThe mixer gate fix
tools/patch_holdcc.pyThe HOLD sustain fix, with its disassembly
tools/syx2bin.pyExtracts a flat image to load at word address 0x88000
tools/periph_map.pyShows 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.shDumps 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.