ms-multi-output
Six channels of per-track USB audio for the Elektron Model:Samples and Model:Cycles. Ships patches, not firmware.
About
Highlights
- USB channels 1 to 6 carry tracks 1 to 6, at 48 kHz and 32 bit
- Builds for the Model:Samples, the Model:Cycles, and a cross-flash that turns a Model:Samples into a six-channel Cycles
- Ships patches, not firmware: you build it from your own OS 1.13 file
- Works on macOS, and on Windows with FlexASIO at 48 kHz
What you need
A Model:Samples or Model:Cycles on OS 1.13, your own official OS file, Python 3.9 or newer (mido and python-rtmidi for flashing) and a MIDI DIN interface: recovery from a bad flash only works over DIN.
Good to know
Verified on hardware on a Model:Samples, also running Cycles OS; the plain Cycles build has not been flashed to a real Model:Cycles yet. The stems are mono, without pan and without the delay and reverb sends, and the stereo mix is no longer sent over USB. While it is installed, CONFIG > UPGRADE over USB does not work; flash the stock OS to get it back.
README
ms-multi-output
Six channels of per-track USB audio for the Elektron Model:Samples and Model:Cycles.
Stock, both boxes send only the stereo mix over USB. This patch sends each of the six tracks on its own USB channel, so you can record real stems straight into a DAW instead of bouncing one track at a time.
| USB channel | carries |
|---|---|
| 1 – 6 | tracks 1 – 6 |
48 kHz, 32-bit, High Speed. The device still reports itself as OS 1.13.
Which build do I want?
| you own | you want | build |
|---|---|---|
| Model:Samples | six channels on it | --target samples |
| Model:Cycles | six channels on it | --target cycles |
| Model:Samples | to turn it into a six-channel Cycles | --target cycles-crossflash |
python3 build.py --target samples --syx model-samples_OS1.13.syx python3 build.py --target cycles --syx model-cycles_OS1.13.syx
How well tested is each?
| build | status |
|---|---|
samples |
✅ Verified on hardware. Channel map confirmed by per-track mute test. |
cycles-crossflash |
✅ Verified on hardware. Same, on a Model:Samples running Cycles OS. |
cycles |
⚠️ The code is verified; this packaging is not. |
⚠️ --target cycles has never been flashed to an actual Model:Cycles,
because no Model:Cycles was available during development. Be aware of what is
and is not proven:
- The patched code is hardware-verified. It is byte-identical to the
cycles-crossflashbuild, which was tested on a Model:Samples running Cycles OS — six independent channels, zero ring duplicates, channel map confirmed by mute test. The two builds differ only in which.syxcontainer the same patched MAIN OS is packed into. - What is untried is that container pairing on real Cycles hardware. It is
the natural one — a
0x11container to a0x11device, exactly how Elektron's own updates are packed — so there is good reason to expect it to work, but nobody has done it.
If you flash it to a real Model:Cycles, please open an issue either way.
That is the one result this project cannot produce for itself. Have a MIDI DIN
interface and the official model-cycles_OS1.13.syx to hand first — see
Recovery.
⚠️ Read this before you do anything
This flashes modified firmware to your instrument. It is not an Elektron product, it is not supported by Elektron, and it will almost certainly void your warranty. It has been tested on exactly one unit — a Model:Samples. The Model:Cycles code was developed by cross-flashing that same box, so no real Model:Cycles has ever run this. See Which build do I want?.
On Windows you need FlexASIO, at 48 kHz — a DAW's normal WASAPI or ASIO settings will not open the device. MME, DirectSound and WASAPI-shared all work; WASAPI-exclusive does not. See Platform support.
You must have a MIDI DIN interface before you start. Recovery from a bad flash goes over MIDI DIN only — the startup-menu updater ignores USB MIDI. If you do not own a DIN interface, stop here; a failed flash would leave you with no way back.
The recovery path is proven and has been used many times during development (see Recovery), because the bootloader lives in a flash sector that OS updates never write. That is the only reason flashing a modified image is a reasonable thing to do at all. It is still your instrument and your risk.
One functional trade-off: this image reuses the two bootloader USB
descriptors as code space, so CONFIG → UPGRADE over USB stops working
while it is installed. The startup-menu DIN rescue is in a different section
and is unaffected. Flash the stock OS back and USB upgrade returns.
What the stems actually are
Worth knowing before you build a mix around them:
- Mono. The audio is tapped before the summing stage, where pan is applied as a coefficient. Per-track pan is not in the stems and cannot be, without doubling to 12 channels.
- Track LEVEL is baked in, and it is linear. Turning LEVEL down turns the stem down.
- The delay and reverb sends are not included. They are derived from the summed main bus, not per track.
- The stereo mix is no longer sent over USB. Mix from the stems instead.
- High track LEVEL distorts, on the stems and on the analogue output alike. This is the voice path saturating, not the patch: measured THD on a sine goes from ~10% at LEVEL 100 to ~23% at LEVEL 127. If you want clean stems, keep LEVEL down. It is present on stock firmware too — you just could not see it per-track before.
Platform support
| status | |
|---|---|
| macOS | ✅ Working — six channels on both targets |
| Windows | ✅ Working — needs FlexASIO at 48 kHz, see below |
| Linux | Untested — the build is pure Python and should work, but nobody has confirmed it |
Windows: FlexASIO on MME, DirectSound or WASAPI — at 48 kHz
All six channels work on Windows, but not through a DAW's normal WASAPI or ASIO settings. Use FlexASIO:
- Install FlexASIO.
- Set its
backendto MME, Windows DirectSound or Windows WASAPI — via FlexASIO Control (the GUI) orFlexASIO.toml. All three are confirmed. - Set
channels = 6on the input, and make sure your DAW project is at 48 kHz. - In your DAW, select FlexASIO as the ASIO device.
Measured on Windows 11 24H2 (build 26100.9457, in-box usbaudio2.sys), with
the sequencer running and the capture checked for per-channel independence:
| FlexASIO backend | result |
|---|---|
| MME | ✅ six distinct channels |
| Windows DirectSound | ✅ six distinct channels |
| Windows WASAPI (shared) | ✅ six distinct channels |
| Windows WDM-KS | ⚠️ intermittent — streamed cleanly in one run, failed in the next |
| Windows WASAPI (exclusive) | ❌ never works |
An earlier version of this README told you to use DirectSound specifically. That was wrong — not about DirectSound, but about singling out any one backend. If a guide or forum post insists only one backend works, it is probably describing a wedged device rather than a backend limitation.
⚠️ If it stops working, power-cycle the instrument before changing any setting. The device occasionally wedges on Windows and needs an unplug/replug. While wedged it still enumerates but refuses to stream, so whichever backend you happen to be trying looks broken — which is exactly how people end up convinced that only their backend works. This was reproduced accidentally during testing: in a wedged state DirectSound failed 12 times out of 12 with a clean, convincing error, then worked perfectly after a power cycle. There is no known cause yet; please open an issue if you can reproduce it reliably.
48 kHz is not optional. The device offers exactly one capture format and no stereo fallback. PortAudio reports its default rate as 44100 under MME and DirectSound, so a host that accepts that default fails negotiation — on stock firmware just as readily as on this one.
This is not a descriptor fault. Windows enumerates the device correctly and
every host API reports it as 6-channel. The descriptors were decoded and
checked against every constraint Microsoft documents for usbaudio2.sys, and
they pass.
Requirements
- A Model:Samples or Model:Cycles running OS 1.13 (the version this is built against)
- The official OS
.syxfor the instrument you are flashing. Thecycles-crossflashbuild needs both — see Which build do I want? - A MIDI DIN interface wired to the Model:Samples' MIDI IN — required
- Python 3.9+ —
build.pyneeds only the standard library midoandpython-rtmidi, for flashing (installed into a venv below)- A C compiler, to build the packer in
setup.sh numpy, only if you want the optional stream check in Verify
No m68k toolchain is needed. The patch ships as a listed byte table.
Install
git clone https://github.com/scottmetoyer/ms-multi-output.git cd ms-multi-output ./setup.sh # builds the .syx packer python3 -m venv .venv # keeps deps off your system Python .venv/bin/pip install mido python-rtmidi # for flashing .venv/bin/pip install numpy # optional, for the stream check
Use the venv rather than bare pip. A stray pip earlier in PATH than
your real one will install into — or crash on — the wrong interpreter, and the
error it produces does not look like a Python problem at all. See
Troubleshooting.
Get the official OS yourself. No Elektron firmware is distributed here, so
download the OS .zip from elektron.se and unzip it.
# you own a Model:Samples, you want six channels on it python3 build.py --target samples --syx model-samples_OS1.13.syx # you own a Model:Cycles, you want six channels on it python3 build.py --target cycles --syx model-cycles_OS1.13.syx # you own a Model:Samples and want to turn it into a six-channel Cycles python3 build.py --target cycles-crossflash \ --syx model-cycles_OS1.13.syx \ --container model-samples_OS1.13.syx
Each target writes its own file: ms-multi-output.syx, mc-multi-output.syx
or mc-on-ms-multi-output.syx. Flash the one whose target matches the box in
front of you — the wrong one is silently ignored by the bootloader rather
than rejected with an error.
The build refuses unless your inputs are the exact OS 1.13 images, checks the expected stock bytes at every patch site (41 for Samples, 29 for Cycles), and verifies the finished file against a known hash:
applied 41 patch runs
patched section 3 matches the tested image
sha256 9ab326746903bd88a460914d08137e58e863d0acc814b1dc621949b009ec0169
✅ reproducible build: byte-identical to the tested image
If you do not see that last line, do not flash it.
Flash
Put the device into OS upgrade mode:
power off → hold [FUNC] → power on → [TRIG 4] (OS UPGRADE)
Dry run first — it will list your MIDI ports and check the image:
.venv/bin/python tools/flash.py ms-multi-output.syx # or mc-multi-output.syxThen send it, naming your DIN output. Pacing matters: at exactly wire speed
a packet eventually drops and the device sits on RECEIVING... forever (which
looks alarming but harms nothing — power-cycle and retry).
.venv/bin/python tools/flash.py ms-multi-output.syx --port "YOUR DIN PORT" --pace 1.4 --sendWatch the screen for the first few seconds. It must change from
READY TO RECEIVE to RECEIVING.... If it stays on READY TO RECEIVE the
image is being silently ignored and nothing is being written — the progress
counter will still run to 100% looking perfectly healthy. See
Model:Cycles.
Roughly 6–7 minutes. Do not power off, especially once the screen says
UPDATING FLASH. The unit reboots itself when done.
Verify
macOS:
system_profiler SPAudioDataType | grep -A4 "Model:" # Input Channels: 6
A Model:Samples carrying the Cycles build reports itself as
Elektron Model:Cycles — that is expected, and is how you know the
cross-flash took.
Linux: arecord -l, or check the device in your DAW. You should see six input
channels. Solo one track and confirm it appears on the matching channel.
Optionally check the stream is clean — play something and capture, then:
.venv/bin/python tools/analyse_dupes.py your-capture.wav
Every active channel should read CLEAN. Muted tracks read CONSTANT/DC,
which is normal — a silenced track sits at a tiny DC value, not exact zero.
Recovery
If the device will not boot, is stuck, or you simply want stock back:
- Power off, hold [FUNC], power on, press [TRIG 4] for OS UPGRADE.
- Send the official
model-samples_OS1.13.syxover MIDI DIN:
.venv/bin/python tools/flash.py model-samples_OS1.13.syx --port "YOUR DIN PORT" --pace 1.4 --sendThis works even when the main OS is unbootable, because the bootloader is in a sector OS updates never touch. It does not work over USB MIDI.
Model:Cycles
There are two ways to end up with a six-channel Cycles, and they are not interchangeable — the bootloader will silently ignore the wrong one.
You own a Model:Cycles → --target cycles
python3 build.py --target cycles --syx model-cycles_OS1.13.syx
One input, packed back into its own Cycles container. Flash
mc-multi-output.syx to the Model:Cycles exactly as you would an official
update.
⚠️ Untested on real Model:Cycles hardware — see Which build do I want? for exactly what that means. The code is verified; the packaging is not.
Recovery is the ordinary one: send the official model-cycles_OS1.13.syx over
MIDI DIN.
You own a Model:Samples → --target cycles-crossflash
This turns a Model:Samples into a six-channel Model:Cycles. It is how the Cycles support was developed and tested.
python3 build.py --target cycles-crossflash \
--syx model-cycles_OS1.13.syx \
--container model-samples_OS1.13.syxYou cannot just flash a Cycles .syx to a Model:Samples. The bootloader
checks the device id in the .syx container (0x0f Samples, 0x11
Cycles) and silently ignores a foreign image. There is no error: the screen
sits on READY TO RECEIVE while the entire 890 kB goes past at the correct
rate and your flashing tool reports a clean 100%. It is indistinguishable from
a dead MIDI cable.
The fix — originally found by a user on
r/Elektron
— is to keep the Samples container and replace only its section_3 (MAIN
OS) with the Cycles one. That is why this target needs two files:
| argument | supplies |
|---|---|
--syx model-cycles_OS1.13.syx |
the code (Cycles MAIN OS, patched) |
--container model-samples_OS1.13.syx |
the wrapper the bootloader accepts |
Recovery is unaffected. The post above warns that once cross-flashed the
box "thinks it's a Cycles" and needs a Cycles container to go back. That was
tested here and is not the case. Afterwards a 0x11 container is still
ignored and a 0x0f one still accepted, because only MAIN OS is replaced and
the Samples bootstrap — with its bootloader — is untouched. Plain
model-samples_OS1.13.syx remains your rescue image.
⚠️ The box then runs Cycles OS on top of Model:Samples project data. How it handles saving and loading is unexplored — back up your +Drive with Elektron Transfer first.
What you get, either way
The box boots as a Model:Cycles — Cycles machines, Cycles UI — with six
per-track USB channels, and identifies over USB as Elektron Model:Cycles.
Everything in What the stems actually are applies, with one difference: track LEVEL is confirmed pre-tap on the Model:Samples but untested on the Cycles, so whether the stems carry it is unknown.
How it works
The audio engine renders each track into its own mono block in SRAM — six blocks of 32 frames — and then sums them to stereo. The stock USB feeder is fed from the summed stereo buffer, which is why you normally only get the mix.
This patch:
- widens the USB audio descriptors from 2 to 6 channels, and the endpoint's
queue-head
MaxPacketLengthto match (missing that one constant silently breaks the stream — it is the single most important value here); - re-dimensions the transfer ring for 24 bytes per frame;
- replaces the feeder's copy loop with a stub that interleaves the six per-track blocks directly.
The stubs live in USB descriptor space that is dead on a High Speed host. Their
assembly sources are in patch/; the exact bytes applied are listed
in patch/rung10.json (Model:Samples, 41 runs) and
patch/cycles6.json (Model:Cycles, 29 runs), so nothing
about this is opaque — you can read every byte that changes and why.
The two firmwares share the same USB stack at byte-identical addresses, so the Cycles port is almost entirely an address translation: the same four stubs, relocated to where that image keeps its descriptors, its mode table and its per-track audio blocks.
Ring depth is load-bearing. The engine delivers 32 frames every 0.667 ms, so the ring must span more than that or the controller re-sends stale slots — audible as a bell-like ringing on transients. This build uses depth 8 (48 frames, 1.0 ms). An earlier build used depth 4 (24 frames) and repeated ~25% of all frames. If you fork this, do not shrink it.
Troubleshooting
pip crashes before it installs anything
If pip fails with a dynamic linker error rather than a package error — on
macOS something like dyld: Library not loaded: ..., on Linux
error while loading shared libraries: ... — then the interpreter is broken,
not the package. Python cannot start, so pip dies before doing any work,
and whichever package you named on the command line takes the blame for it.
Two common causes:
- A Python built against a library path that has since moved or been removed. A version-manager build (pyenv, asdf, conda) that predates an OS, package manager or CPU-architecture change is the usual culprit.
- A stale
pipscript sitting earlier inPATHthan your real one and pointing at that dead interpreter.python3can be perfectly healthy while everypipon yourPATHis not, which makes this confusing to spot.
Work out which you have:
which pip # which pip are you actually getting? head -1 "$(which pip)" # its shebang names the interpreter it will use python3 -m pip --version # bypasses stray pip scripts entirely
Version-specific names are not automatically safe — a pip3.N can carry a
shebang pointing at a different, dead Python.
Fixes, best first:
- Use the venv from Install.
python3 -m venv .venvbuilds on whateverpython3resolves to and generates its ownpipinside the venv, so nothing onPATHcan interfere. This is why the install steps use one. python3 -m pip install ...instead of barepip.- Remove or repair the stale script — check its shebang first, since the directory it lives in usually holds unrelated tools worth keeping.
The screen never leaves READY TO RECEIVE
The image is being silently rejected on its container device id, and
nothing is being written — even though the transfer runs to 100% normally.
You built for the wrong target. Flashing a Model:Cycles image to a
Model:Samples needs --target cycles-crossflash; flashing to an actual
Model:Cycles needs --target cycles. See Model:Cycles.
To confirm the MIDI path itself is fine, send the official
model-samples_OS1.13.syx: if that moves to RECEIVING... on the same cable
and port, your wiring is good and the container was the problem.
The device sits on RECEIVING... forever
A packet was dropped mid-transfer. Harmless — power-cycle, re-enter OS upgrade
mode, and send again. If it recurs, raise --pace above 1.4.
Credits
mischa85/elektron-firmware-toolby Marcel Bierling (MIT) — unpacks, repacks and re-signs the.syx. This project would not exist without it.mxldyn/octamax— the Octatrack work that established the OS format and the code-cave detour technique.
Licence and firmware
The code here is MIT (LICENSE).
No Elektron firmware — original or modified — is distributed in this repository. You supply your own lawfully obtained copy of the public OS image, and the build patches it locally. Elektron, Model:Samples and Model:Cycles are trademarks of Elektron Music Machines; this project is not affiliated with or endorsed by them.
Pictures in the README load from GitHub.
- Language
- Python
- License
- MIT License
- Stars
- 11
- Forks
- 1
- Created
- 09-18-2026
- Last commit
- 2 weeks ago
- Topics

