Firmware

mi-sim

Hype26/100

Run unmodified Mutable Instruments Eurorack firmware (Plaits, Rings, Elements) on macOS: whole-firmware simulator with mock drivers, Python bindings, GUI and tests

mi-sim is custom firmware for the Mutable Instruments Plaits, Mutable Instruments Rings, and Mutable Instruments Elements, by @c4pt0r.

forMutable Instruments Plaits,Mutable Instruments Rings,Mutable Instruments Elements·by@c4pt0r

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

Downloads
0
Stars
0
Forks
0
Commits
3
Last commit
Get the source

No release files yet. Build from source.

Custom license· c4pt0r/mi-sim·downloads come from the creator’s GitHub releases

3
Commits
0
Releases
3 commits and 0 releases in the last year
Activity · last 12 months

mi-sim — Mutable Instruments firmware simulator for macOS

Runs the unmodified Mutable Instruments Eurorack firmware (pichenettes/eurorack, pulled in as a git submodule) on the Mac, with the STM32 drivers swapped for a virtual front panel. Not just the DSP: the real ui.cc (buttons, long presses, hidden parameters, LED animations, calibration, normalization probing) and settings.cc (flash storage with wear levelling) run block by block exactly as the DAC interrupt drives them on hardware.

Supported modules: Plaits (STM32F37x), Rings and Elements (STM32F4). The layout is per-module so others (Stages, Marbles, Clouds, ...) can follow the same recipe.

third_party/eurorack/          upstream firmware, untouched (submodule)
sim/hal/sim_hal.h              fake HAL (F37x + F4): only what firmware *headers* use inline
sim/shadow/stmlib/system/      storage.h replacement -> in-memory flash (page + sector layouts)
sim/shadow/rings/ui.h          one-line host-portability fix (checked by tools/check_shadows.py)
sim/common/                    VirtualFlash, CoreAudio output, global lock, zeroed allocation
sim/<module>/drivers/          replacement drivers (ADCs, LEDs, debug UART)
sim/<module>/<module>_hardware.*  virtual panel + analog front-end model
sim/<module>/<module>_sim.*    boots the firmware objects, replicates Init()/FillBuffer()/SysTick
sim/<module>/<module>_c_api.*  C API (thread-safe) for bindings
python/mi_sim/{plaits,rings,elements}.py  ctypes wrappers, ms-based deterministic time
python/mi_sim/gui.py + panels.py          Dear PyGui front panels with live audio + scope
tests/                         pytest suites exercising the firmware's behaviour

Build

git clone --recursive <this repo>      # or: git submodule update --init --recursive
brew install cmake ninja               # Xcode command line tools for clang
make build                             # -> build/libmi_sim.dylib
make venv                              # python deps: numpy, pytest, dearpygui
make test                              # 66 tests, < 1 s
make gui MODULE=rings                  # interactive panel with sound (plaits | rings | elements)

Only plaits/test/-style host compilation is needed: Apple clang, -DTEST, no ARM toolchain.

Scripted use

from mi_sim.plaits import PlaitsSim, Pot, CV, Switch

sim = PlaitsSim()                 # factory-fresh unit (blank flash)
sim.run(1000)                     # 1 s of power-on settling (pot filters)
sim.patch(CV.V_OCT, 1.0)          # insert a cable carrying 1 V
sim.tap(Switch.ROW_1)             # next engine
sim.hold_both(2100); sim.release_all()   # enter calibration mode
audio = sim.run(500)              # (frames, 2) int16, OUT / AUX
print(sim.led_pattern())          # e.g. "g......."  (blinking green LED 1)
print(sim.modulations["note"])    # 12.0
image = sim.flash                 # bytes; PlaitsSim(flash=image) reboots with it
from mi_sim.rings import RingsSim, CV, Switch

r = RingsSim(); r.run(1000)
r.patch_trigger(); r.strum()            # a cable in STRUM disables the internal exciter
r.tap(Switch.MODEL)                     # next resonator model
r.performance["internal_note"]          # False once V/OCT is patched
audio = r.run(500, audio_in=my_int16)   # feed the IN jack

from mi_sim.elements import ElementsSim, Pot, CV
e = ElementsSim(); e.run(1500)
e.press(); e.run(400)                   # PLAY button gates the voice
e.set_pot(Pot.RESONATOR_GEOMETRY, 0.7)

Time is deterministic: run(ms) executes an exact number of firmware blocks (Plaits: 4 per ms of 12 samples at 48 kHz; Rings: 2 per ms of 24 at 48 kHz; Elements: 2 per ms of 16 at 32 kHz). Rings and Elements run their 1 kHz SysTick UI poll and the main-loop event queue every second block, as on hardware. "Hold for 3 s" is always the same iterations, so tests never depend on wall-clock timing.

What you can observe

accessorsource in firmware
leds, leds_avg, led_pattern()Leds::Write (average over the last run gives PWM/blink brightness)
paramsPatch (knobs after smoothing, hidden params)
modulations (Plaits), performance (Rings, Elements)calibrated CV, cable-detection flags, note/strum/gate
state, calibration, settingswhat is persisted in flash
active_engine, polyphony, model, resonator_model, easter_eggVoice / Part
factory_test(cmd, arg)the UART factory-test protocol in Ui::HandleFactoryTestingRequest
flash, flash_erase_countvirtual flash image

Virtual hardware model

  • Pots: 0..1, 12-bit left-aligned like the ADC.
  • CV jacks: volts + a patched flag. Every input runs through the same inverting stage (adc = -0.2 * V + offset); V/OCT, TRIGGER and LEVEL carry the DC offsets implied by the factory calibration defaults. Override with set_front_end() to test calibration against a "bad" unit.
  • Normalization: unpatched jacks read the firmware's pseudo-random probe bit (Plaits PA9: FM/TIMBRE/MORPH/TRIG/LEVEL; Rings PC8: STRUM, V/OCT and, with one block of codec latency, the audio IN), so the cable detectors behave as on hardware.
  • Rings / Elements front-end: V/OCT adc = 0.7912 - 0.1424 V (the 1 V / 3 V readings quoted in CalibrateC3), other CVs adc = 0.505 - 0.0758 V so that a fully open attenuverter gives 1 V/oct on the FM inputs. Elements' STRENGTH jack is normalled to full.
  • Switches: active-low with debouncing done by the firmware.
  • Flash: 64 KB image, page erase = 0xFF fill, program = AND (bits can only be cleared), so wear-levelling and "write before erase" bugs show.
  • Debug UART: USART3 as a pair of byte queues.

GUI

make gui MODULE=elements (or python -m mi_sim --module rings --flash my_unit.bin). Left: knobs and attenuverters, the buttons (hold with the mouse), LEDs. Right: CV sliders with a cable checkbox each, pulse buttons for gate/trigger jacks, and for modules with audio inputs a cable checkbox, a noise level and an impulse button. Bottom: scope of the two outputs. Audio goes to the default output via CoreAudio; without audio the firmware still runs in real time.

Flashing the real module

See docs/flashing.md: cross-compiling upstream, encoding the audio-bootloader WAV with tools/qpsk_encoder.py (a Python 3 port of the upstream encoder, verified against the bootloader's demodulator), and SWD with OpenOCD.

Adding another module

  1. Look at <module>/test/makefile upstream for the host-buildable DSP set.
  2. Compile GPIO-only upstream drivers unchanged (switches, gate/trigger inputs, GPIO LEDs); write replacement drivers/*.cc only for ADCs, PWM LEDs and the debug UART. Extend sim/hal/sim_hal.h with whatever HAL symbols the headers use inline.
  3. Replicate the module's Init, FillBuffer and SysTick_Handler in a <module>_sim.cc; interleave SysTick and DoEvents() at 1 kHz.
  4. Derive the front-end model from the calibration defaults and comments.
  5. Expose a C API (struct : sim::ZeroedNew) and a thin Python class; add a panel in panels.py; write behaviour tests.
  6. If an upstream header does not compile on the host, shadow it under sim/shadow/ and register the substitution in tools/check_shadows.py.

Licence

Simulator code: MIT (see LICENSE). The firmware in third_party/eurorack keeps its own licences (MIT for STM32 modules, GPL for AVR modules, cc-by-sa for hardware).