11 — Development Environment
Status · reconciled 2026-08-30. Native macOS development and the QEMU MCP2221/I²C smoke path work. M6.1 also proves the versioned ctld/app socket through the guest; interactive simulation, control mapping and N100 deployment remain.
This document defines how Auvra is developed before (and alongside) the real hardware exists, in two phases:
- Phase 1 — Native Mac app. The synth UI + DSP run natively on macOS in a window that mimics the touch display; audio goes to system audio (CoreAudio), MIDI comes from system MIDI (CoreMIDI). Fast iteration on the app itself.
- Phase 2 — Virtual control surface in QEMU. The real target software stack runs inside a QEMU Linux guest, and the knobs/faders/buttons/LEDs are presented by a custom QEMU USB device so the production driver path is exercised without any real hardware.
The unifying idea: write the app once as a portable core with thin, swappable platform backends, so the same code runs on the Mac window, in the QEMU guest, and on the N100.
flowchart LR
subgraph P1["Phase 1 — native on macOS"]
direction TB
A1["Synth app (core)"]
SDLW["SDL3 Cocoa window<br/>(mimics touch panel)"]
CA["CoreAudio (cpal)"]
CM["CoreMIDI (midir)"]
A1 --- SDLW
A1 --- CA
A1 --- CM
end
subgraph P2["Phase 2 — QEMU guest"]
direction TB
G["aarch64/x86-64 Linux guest<br/>(real RT stack + synth app + control daemon)"]
UDEV["Custom QEMU USB device<br/>(emulated MCP2221A)"]
SIM["RP2350 simulator<br/>(host process)"]
G --- UDEV
UDEV -->|"chardev socket"| SIM
end
P1 -->|"port app, add backends"| P2
1. Shared architecture: portable core + thin backends
Section titled “1. Shared architecture: portable core + thin backends”Keep everything platform-specific out of the core. The core is pure Rust and never names an sdl3, cpal, or midir type.12
flowchart TB
subgraph CORE["auvra-core (zero platform deps)"]
DSP["engine / mixer / events"]
MODEL["patch + control state"]
end
subgraph PLAT["platform/ (thin adapters)"]
SDLB["SDL3 — Cocoa / X11 window + input"]
RTA["cpal — CoreAudio / ALSA"]
RTM["midir — CoreMIDI / ALSA-seq"]
end
APP["app/ — wires backends into core (per target)"]
CORE --- APP
PLAT --- APP
Suggested tree:
crates/ auvra-core/ # dsp + patch/param model + ui draw-logic — no platform types auvra-platform/ # sdl3 (window/input) · cpal (audio) · midir (MIDI) auvra-app/ # binary "auvra": wires backends into core (Mac now, N100 later)Cargo.toml # workspaceThe core exposes Rust traits (AudioSource, MidiSink, Painter); the platform crate implements them over sdl3, cpal, and midir, and the app wires them together (Cargo cfg/features select the backend per target). Keeping the core language- and OS-agnostic mirrors the core/platform split JUCE uses for its audio devices.2
Build & run with Cargo: cargo run -p auvra-app (or make run) opens the Mac
window; the same app runs in the N100’s minimal X11 kiosk. Repo layout
(crates/, qemu/, site/) is in the top-level README.
2. Phase 1 — native macOS app
Section titled “2. Phase 1 — native macOS app”Graphics & windowing — SDL3
Section titled “Graphics & windowing — SDL3”Use SDL3 (SDL2 went to maintenance-only in 2024).3 One SDL codebase covers both ends:4
- macOS (dev): the
cocoavideo driver is automatic — a normal resizable window mimics the panel. - Target (N100): SDL3 uses its
x11backend inside a minimal kiosk. This keeps Auvra fullscreen while allowing native CLAP editors to embed into the same display server (16).
Touch: handle SDL_EVENT_FINGER_DOWN/MOTION/UP. On the Mac there’s no touchscreen, so set SDL_HINT_MOUSE_TOUCH_EVENTS=1 to synthesize finger events from mouse clicks (off by default on desktop) — lets you exercise the touch-UI code paths during dev.5
Audio — cpal
Section titled “Audio — cpal”cpal gives one audio API over CoreAudio (macOS) and ALSA (Linux); the
same callback drives the core Engine.6 On the product, cpal opens the ALSA
PCM device exclusively with a small buffer (see 03). JACK
or PipeWire may be added later as optional desktop-routing adapters; neither is
part of the MVP contract.
MIDI — midir
Section titled “MIDI — midir”midir unifies CoreMIDI (macOS) and ALSA-seq / JACK-MIDI (Linux).7 For development it opens a virtual input port (“Auvra Keys”) that a software keyboard or DAW feeds directly:
let input = MidiInput::new("Auvra")?;let _conn = input.create_virtual("Auvra Keys", on_midi, ())?; // macOS/Linux (not Windows)- IAC Driver (Audio MIDI Setup → MIDI Studio → IAC → “Device is online”, add a bus) — a persistent virtual bus any two apps can share. Restart apps after adding a port.
- On-screen keyboards: VMPK (cross-platform), MidiKeys (macOS), or Logic/GarageBand “Musical Typing”.
- Class-compliant USB controllers just work via CoreMIDI — no driver.
This mirrors the real instrument, where the keybed scanner is a USB-MIDI device (05) — in Phase 1 it is simply replaced by any system-MIDI source.
What Phase 1 validates (and doesn’t)
Section titled “What Phase 1 validates (and doesn’t)”| Validates | Does not validate |
|---|---|
| UI layout/rendering, touch interaction, DSP/voice engine, patch model, MIDI-note handling | Real RT latency (03), silent boot (07), the I²C control-surface path (06) |
3. Phase 2 — virtual control surface in QEMU
Section titled “3. Phase 2 — virtual control surface in QEMU”Phase 2 runs the actual target stack (RT Linux + synth app + control daemon)
inside QEMU and feeds it a control surface through an emulated USB device.
The automated smoke currently proves hid-mcp2221 → /dev/i2c-N → control
daemon → versioned app IPC with no hardware; app-to-engine mapping is M6.2.
flowchart LR
subgraph HOSTM["macOS host"]
SIM["RP2350 simulator<br/>(control-node firmware behaviour)"]
subgraph QEMU["QEMU (HVF)"]
direction TB
DEVM["Custom USB device model<br/>(emulated MCP2221A, in QEMU tree)"]
subgraph GUEST["Linux guest = target stack"]
DRV["hid-mcp2221 driver"]
I2C["/dev/i2c-N"]
DAEMON["control daemon"]
SYN["synth app"]
DRV --> I2C --> DAEMON --> SYN
end
DEVM --> DRV
end
end
DEVM <-->|"chardev unix socket<br/>(I²C transactions)"| SIM
3.1 Host/guest architecture — pick by your Mac
Section titled “3.1 Host/guest architecture — pick by your Mac”QEMU accelerates guests with HVF on macOS. USB device models are CPU-arch-independent, so the choice is only about guest speed:1011
| Dev Mac | Recommended guest | Why |
|---|---|---|
| Apple Silicon | aarch64 Linux (HVF) | Near-native speed; also rehearses the future ARM target (02). x86-64 here falls back to TCG (~8–15× slower). |
| Intel Mac | x86-64 Linux (HVF) | Matches the N100 target directly. |
On the ARM
virtmachine, add an explicit USB controller:-device qemu-xhci(x86q35has one by default).10
3.2 Staged plan (quick win → full fidelity)
Section titled “3.2 Staged plan (quick win → full fidelity)”Because a full custom device is real work, stage it:
| Stage | Approach | Fidelity | Effort |
|---|---|---|---|
| 2a | i2c-stub kernel module → fake /dev/i2c-N |
Exercises the control daemon + register protocol; no USB, no RP2350 behaviour | Minutes12 |
| 2b | Custom QEMU MCP2221A device + RP2350 simulator over a chardev socket | Full bench control path incl. real hid-mcp2221 driver |
Days–weeks13 |
i2c-stub gives up to 10 fake chip addresses immediately — perfect for developing the control daemon’s register map (06) while 2b is built. (Note: i2c-stub is plain register storage with no debugfs control interface and no autonomous behaviour.)12
3.3 The custom QEMU MCP2221A device
Section titled “3.3 The custom QEMU MCP2221A device”Where it lives: QEMU has no stable external-plugin ABI, so a custom USB device is compiled into the QEMU source tree. Study hw/usb/dev-serial.c (the FTDI model) — it is the closest template: vendor-specific control requests bridged to a chardev backend, with a receive ring buffer. hw/usb/dev-hid.c is the simplest starting point; u2f.c/canokey.c are upstreamed custom-protocol precedents.1415 A device is a QOM object with parent = TYPE_USB_DEVICE implementing realize, handle_reset, handle_control, handle_data; descriptors are declarative structs; long operations complete asynchronously (USB_RET_ASYNC → usb_packet_complete).14 Emulating a USB-I²C bridge so the guest needs no custom driver is a strategy QEMU maintainers themselves have suggested.16
What to implement (from the driver source): the mainline hid-mcp2221 driver is the spec.13 It binds a USB-HID device (VID 0x04D8 / PID 0x00DD) and:
- drives I/O purely via
hid_hw_output_report()+ the async.raw_eventcallback (astruct completionwith a 4-second timeout — generous slack; model timing need not be exact); - needs only a syntactically valid HID report descriptor (semantics don’t matter; no
/dev/inputnode is created); - treats GPIO/ADC/DAC discovery as non-fatal — a minimal model only has to implement the I²C command set to get a working
/dev/i2c-N.
Core I²C commands to handle: 0x90/0x94 write (+no-stop), 0x91/0x93 read (+repeated-start), 0x40 get-data, 0x10 param/status (bus-status poll), 0x20 set-speed. The driver only issues the [write-no-stop][repeated-start read] two-message pattern, and uses variable-length output reports (not fixed 64-byte frames).13
The one genuinely new piece: stock QEMU HID devices STALL USB_TOKEN_OUT and have no interrupt-OUT endpoint, so a bidirectional raw-HID device is new code, not a config tweak.14 Realistic effort: a few hundred to ~1000 lines, days to ~2 weeks for someone comfortable with QEMU’s USB APIs; use each device’s built-in pcap=<file> to capture bus traffic for Wireshark while debugging.14
Backend: pair -chardev socket,id=i2csim,path=/tmp/rp2350.sock with -device auvra-mcp2221,chardev=i2csim; the device forwards each I²C transaction to a host-side RP2350 simulator that mimics the control-node firmware (register map, pot/fader values, LED writes). dev-serial.c shows the exact chardev wiring.15
3.4 Alternatives considered
Section titled “3.4 Alternatives considered”| Option | Verdict |
|---|---|
usb-host passthrough of a real MCP2221A dongle |
Avoid on a Mac. macOS blocks CLI apps from the USB exclusive-access entitlement — documented broken/unreliable through late 2025. Viable only from a real Linux host.17 |
dummy_hcd + raw-gadget + USB/IP into QEMU |
Software-only USB emulation, architecturally plausible but unproven for this exact chain, and needs a Linux host/VM.18 |
| Full custom QEMU device (3.3) | Highest fidelity; the recommended endpoint for CI/repeatable integration tests. |
i2c-stub (2a) |
Fastest; logic-only, no USB realism. Start here. |
4. Keybed / MIDI in the dev environment
Section titled “4. Keybed / MIDI in the dev environment”The keybed scanner is a USB-MIDI device on real hardware (05). In both phases it is simplest to feed system/virtual MIDI (Phase 1: CoreMIDI; Phase 2: an ALSA-seq port in the guest, or QEMU MIDI passthrough). If you later want to rehearse the scanner’s own USB enumeration too, a USB-MIDI gadget can be emulated the same way as the control device — but that is optional and out of scope for Phase 2’s control-surface focus.
5. Recommended sequence
Section titled “5. Recommended sequence”flowchart LR
S1["① Portable core + SDL3/cpal/midir<br/>native macOS app"]
S2["② i2c-stub in aarch64 HVF guest<br/>develop control daemon"]
S3["③ Custom QEMU MCP2221A + RP2350 sim<br/>full virtual control surface"]
S4["④ Bench on real N100<br/>(X11 kiosk, ALSA, real MCP2221A)"]
S1 --> S2 --> S3 --> S4
Phase 1 (stage 1 in the diagram) delivers immediate value and is where most UI/DSP work happens. Stage 2 is a fast parallel track for the control-daemon logic. Stage 3 is the full bench virtual controller surface. Stage 4 validates N100 timing and the UI. The selected Tang FPGA path has separate F0–F10 gates in 17; it is not covered by the MCP2221A QEMU model.
6. Open questions ([verify])
Section titled “6. Open questions ([verify])”- SDL3/X11 touch input and native editor embedding on the actual N100 panel.
- HID report descriptor bytes for the emulated MCP2221A — author a valid vendor descriptor; the real chip’s exact descriptor isn’t published but only needs to parse.13
- QEMU struct layout (
USBDevice/USBDeviceClassininclude/hw/usb.h) — check against the exact QEMU version you build before writing the model.14 - RP2350 simulator scope — how faithfully to model node timing/clock-stretch vs. just the register map.
- Guest audio in Phase 2 — routing guest ALSA out through the Mac for end-to-end tests vs. keeping audio validation in Phase 1.
Footnotes
Section titled “Footnotes”-
Platform-abstraction/HAL pattern for portable C++ (core vs. per-OS backends). Medium, “C++ project structure for CMake.” https://medium.com/swlh/c-project-structure-for-cmake-67d60135f6f5 ↩
-
JUCE
juce_audio_devices— platform-agnosticAudioIODevicewith per-OS driver subclasses. https://github.com/juce-framework/JUCE/blob/master/modules/juce_audio_devices/juce_audio_devices.cpp ↩ ↩2 -
Phoronix, “SDL2 Now Transitioning To Maintenance Mode.” https://www.phoronix.com/news/SDL2-To-Maintenance-Mode ↩
-
SDL3 macOS (Cocoa) support. SDL wiki, “README-macos.” https://wiki.libsdl.org/SDL3/README-macos ↩
-
SDL3
SDL_HINT_MOUSE_TOUCH_EVENTS(synthesize touch from mouse; off by default on desktop). https://wiki.libsdl.org/SDL3/SDL_HINT_MOUSE_TOUCH_EVENTS ↩ -
cpal — cross-platform Rust audio I/O (including CoreAudio and ALSA). https://github.com/RustAudio/cpal ↩
-
midir — cross-platform Rust MIDI (CoreMIDI / ALSA-seq / JACK); virtual ports on macOS/Linux. https://github.com/Boddlnagg/midir ↩
-
Apple, “Transfer MIDI information between apps” (IAC Driver setup). https://support.apple.com/guide/audio-midi-setup/transfer-midi-information-between-apps-ams1013/mac ↩
-
VMPK — Virtual MIDI Piano Keyboard (cross-platform on-screen keyboard). https://vmpk.sourceforge.io/ ↩
-
QEMU HVF acceleration on macOS +
virtmachine USB controller note. QEMU 6.2.0 release notes; UTM system docs. https://www.qemu.org/2021/12/14/qemu-6-2-0/ ↩ ↩2 -
x86-on-Apple-Silicon runs under TCG (~8–15× slower). Benchmark discussion. https://www.nequalsonelifestyle.com/2022/07/06/linux-x86-builds-apple-silicon-impractical/ ↩
-
Linux
i2c-stub— in-memory fake I²C adapter (up to 10 addresses, no debugfs). https://docs.kernel.org/i2c/i2c-stub.html ↩ ↩2 -
Linux
hid-mcp2221driver source — the de-facto spec for the emulated device (commands, async.raw_event, 4 s timeout, VID/PID). https://raw.githubusercontent.com/torvalds/linux/master/drivers/hid/hid-mcp2221.c ↩ ↩2 ↩3 ↩4 -
QEMU USB device model API (QOM
USBDevice,handle_control/handle_data, in-tree build). QEMU docs +hw/usb/dev-hid.c. https://qemu.readthedocs.io/en/master/system/devices/usb.html ↩ ↩2 ↩3 ↩4 ↩5 -
QEMU
hw/usb/dev-serial.c— FTDI model bridging vendor commands to a chardev backend (template). https://github.com/qemu/qemu/blob/master/hw/usb/dev-serial.c ↩ ↩2 -
P. Bonzini suggests emulating a USB-I²C bridge so the guest needs no custom driver. qemu-devel, 2014. https://lists.nongnu.org/archive/html/qemu-devel/2014-02/msg02440.html ↩
-
macOS blocks CLI apps from the USB exclusive-access entitlement →
usb-hostpassthrough unreliable (multiple QEMU/UTM issues through 2025). https://github.com/utmapp/UTM/issues/7530 ↩ -
Linux
raw-gadget— userspace arbitrary-USB-device emulation (software-only path). https://github.com/xairy/raw-gadget ↩