Skip to content

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 # workspace

The 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.

Use SDL3 (SDL2 went to maintenance-only in 2024).3 One SDL codebase covers both ends:4

  • macOS (dev): the cocoa video driver is automatic — a normal resizable window mimics the panel.
  • Target (N100): SDL3 uses its x11 backend 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

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.

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)

macOS testing options:89

  • 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.

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 virt machine, add an explicit USB controller: -device qemu-xhci (x86 q35 has 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

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_event callback (a struct completion with 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/input node 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

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.

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.

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.

  1. SDL3/X11 touch input and native editor embedding on the actual N100 panel.
  2. 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
  3. QEMU struct layout (USBDevice/USBDeviceClass in include/hw/usb.h) — check against the exact QEMU version you build before writing the model.14
  4. RP2350 simulator scope — how faithfully to model node timing/clock-stretch vs. just the register map.
  5. Guest audio in Phase 2 — routing guest ALSA out through the Mac for end-to-end tests vs. keeping audio validation in Phase 1.
  1. 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 ↩

  2. JUCE juce_audio_devices — platform-agnostic AudioIODevice with per-OS driver subclasses. https://github.com/juce-framework/JUCE/blob/master/modules/juce_audio_devices/juce_audio_devices.cpp ↩ ↩2

  3. Phoronix, “SDL2 Now Transitioning To Maintenance Mode.” https://www.phoronix.com/news/SDL2-To-Maintenance-Mode ↩

  4. SDL3 macOS (Cocoa) support. SDL wiki, “README-macos.” https://wiki.libsdl.org/SDL3/README-macos ↩

  5. 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 ↩

  6. cpal — cross-platform Rust audio I/O (including CoreAudio and ALSA). https://github.com/RustAudio/cpal ↩

  7. midir — cross-platform Rust MIDI (CoreMIDI / ALSA-seq / JACK); virtual ports on macOS/Linux. https://github.com/Boddlnagg/midir ↩

  8. Apple, “Transfer MIDI information between apps” (IAC Driver setup). https://support.apple.com/guide/audio-midi-setup/transfer-midi-information-between-apps-ams1013/mac ↩

  9. VMPK — Virtual MIDI Piano Keyboard (cross-platform on-screen keyboard). https://vmpk.sourceforge.io/ ↩

  10. QEMU HVF acceleration on macOS + virt machine USB controller note. QEMU 6.2.0 release notes; UTM system docs. https://www.qemu.org/2021/12/14/qemu-6-2-0/ ↩ ↩2

  11. 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/ ↩

  12. Linux i2c-stub — in-memory fake I²C adapter (up to 10 addresses, no debugfs). https://docs.kernel.org/i2c/i2c-stub.html ↩ ↩2

  13. Linux hid-mcp2221 driver 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

  14. 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

  15. 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

  16. 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 ↩

  17. macOS blocks CLI apps from the USB exclusive-access entitlement → usb-host passthrough unreliable (multiple QEMU/UTM issues through 2025). https://github.com/utmapp/UTM/issues/7530 ↩

  18. Linux raw-gadget — userspace arbitrary-USB-device emulation (software-only path). https://github.com/xairy/raw-gadget ↩