Feature/ios auv3 spike#34
Conversation
Minimal iOS port proof on a new branch: compiles the existing emulator
core (deps/sameboy + SystemBase/SameBoySystem/LinkGroup, unmodified)
into an iOS framework and wraps it as an AUv3 instrument ('aumu'/'mgbs'/
'RPtm') with a SwiftUI container app hosting it in-process.
- ios/generate.sh: host-side generation mirroring the desktop CMake
(per-channel patch, RGBDS boot ROMs, embedded mGB ROM, GB_VERSION,
reflect-cpp headers) so the Xcode build needs no cross-compiled tools
- ios/project.yml: XcodeGen — RetroPlugKit framework + RetroPlugAU
app-extension + RetroPlug app (automatic signing, Toilville team)
- RetroPlugAudioUnit.mm: internalRenderBlock -> onMidi + onProcess,
the same seam PluginDSP::run drives on desktop
- Deliberately excludes Engine/QuickJS, Mesen, LSDj, real UI (phases
listed in ios/README.md)
Verified: xcodebuild -destination generic/platform=iOS succeeds.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Builds the AUv3 spike out into a usable iPad player and a real instrument: - RetroPlugCoreBridge (RetroPlugKit): main-thread control surface over the render thread — an SPSC command ring for realtime ops (buttons, reset, gain, fast boot) and a bypass gate for heavy ones (ROM swap, model change, save/load state & SRAM); lock-free video frame copy and a non-blocking SRAM snapshot for autosave - RetroPlugAudioUnit: multi-out (bus 0 mix + busses 1-4 GB channel stems, the desktop ChannelSplit routing); all seven active desktop lsdj-sync modes translated in the render block (mGB passthrough, MIDI sync, Arduinoboy, MIDI map, keyboard MIDI, MI.OUT, master sync) with AU parameters + MIDI output; fullState host session persistence - App: start menu + ROM library under Documents/ (Files-app sharing, .sav/.rplg sidecar import), CADisplayLink LCD blit shared with the appex target, touch controls + physical game controllers, mGB note pads, settings sheet (model / fast boot / gain / MIDI sync), SRAM autosave on background, savestates - .rplg sidecars carry a project's model/fast-boot/sync settings across, decoded forward-tolerantly like the desktop role-config path Also ignores xcuserdata/ and commits the workspace pointing at the generated ios/RetroPlugIOS.xcodeproj. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Technically this means that midi out works for any rom? Nanoloop too?
• Trackers and synth ROMs give clean output. LSDj, Nanoloop, etc. write one note per trigger with stable pitches, so you get a tidy note stream — this is the intended use case, and it's why that mode exists as the ROM-agnostic alternative to MI.OUT. So: the "any ROM" claim in the settings footer is accurate, and for LSDj specifically it means no more hunting for MI.OUT builds. Just set expectations that pointing it at a game soundtrack yields a raw hardware transcription — great for sampling/analysis or novelty MIDI capture, noisier than tracker output for driving other synths. |
Summary
This branch brings RetroPlug to iOS as an AUv3 instrument (
aumu/mgbs/RPtm) plus a SwiftUI container/player app, by wrapping the desktop emulator core in a new Xcode project underios/. The AU loads the embedded mGB synth by default (silent until MIDI arrives), lets the user swap in a ROM from Files directly in the plugin UI, exposes the four GB channels as separate output busses, ports the desktop's full LSDj MIDI-sync role natively, and persists its complete session (cartridge, battery RAM, savestate, settings) into the host DAW's project file.On top of that base it reaches full Arduinoboy settings parity — the per-mode MIDI channel assignments and MI.OUT CC matrix the hardware's maxpat Editor configures over SysEx are AU parameters here — adds a GB Note Out mode (MIDI out from any LSDj build via an APU register tap, no MI.OUT-patched ROM needed), an LSDj song manager in the player, and per-instance mGB channel blocks for multi-instance DAW setups.
The work is all additive, in three waves:
791a9879— the AUv3 spike: SameBoy core + mGB as an iPad audio unit4dbf896b— the standalone player app + LSDj sync parity in the AU (also includes DAW session persistence, the in-plugin ROM picker, and the iPad orientation fix)What changed in the parent (shared) tree
Small and deliberate. Two kinds of changes outside
ios/:Inert, repo-level additions:
.gitignoreios/generated/,ios/.deps/,ios/.build/, the xcodegen-emitted.xcodeprojandInfo.plists,xcuserdata/).claude/settings.jsonxcodegen/xcodebuild(dev tooling only)retroplug.xcworkspace/contents.xcworkspacedataios/RetroPlugIOS.xcodeprojso the iOS project opens from the repo rootThe APU register-write tap (backs the GB Note Out mode; inert on desktop):
cmake/patches/sameboy-per-channel-audio.patch— the already-tracked SameBoy patch grows a register-write hook inGB_apu_writealongside the existing per-channel sample tap. Same patch file, applied by the same idempotent mechanism on both platforms.packages/native/src/system/sameboy/SameBoySystem.{cpp,hpp}— an opt-in per-block APU write log (setApuWriteCapture/apuWriteLog_), following the serial-out capture's exact gate pattern: nothing runs unless a host arms it, and nothing on desktop arms it (yet — the same log could back a desktop Note Out later).docs/lsdj.md/AGENTS.md— documentation for the above.Still not touched:
packages/retroplug/(the TS/UI side),cmake/build scripts,build.sh/build.bat, and every submodule pointer (deps/sameboy,deps/dpf.js, etc.). The desktop macOS / Windows / Linux builds compile from the same sources plus the extended-but-unarmed tap, and their behavior is unchanged.Shared code consumed read-only by the iOS targets
The iOS project deliberately reuses the parent tree rather than forking it:
RetroPlugKit(viaproject.ymlsource references):packages/native/src/system/SystemBase.cpp,system/sameboy/SameBoySystem.cpp,system/sameboy/LinkGroup.cpp, anddeps/sameboy/Core(built withGB_INTERNAL, warnings silenced — mirroringcmake/sameboy.cmake's non-Windows path).cmake/patches/sameboy-per-channel-audio.patch— the same tracked patch the desktop configure applies is applied idempotently byios/generate.sh(samegit apply --reverse --checkdance). This is whydeps/sameboyshows as dirty; that is expected on both platforms.resources/roms/mGB.gband the SameBoy boot-ROM sources — embedded/assembled byios/generate.shintoios/generated/(gitignored), replicatingcmake/bin2h.cmake/bin2c.cmake/sameboy_bootroms.cmakewith host tools only (RGBDS + cc + python3), so no cross-compiled build helpers are needed.packages/retroplug/src/dspRoles.ts,lsdjKeyboardMap.ts,lsdjArduinoboy.ts), and the render loop mirrorspackages/native/plugin/PluginDSP.cpp's prepare → step → finish triad. The TS originals are unchanged; parity drift between the two is a maintenance consideration called out inios/README.md.What is confined to iOS (everything else)
All under
ios/, three targets defined byproject.yml(xcodegen):RetroPlugKit(dynamic framework — shared by app and extension)RetroPlugAudioUnit.mm— the AUv3 ↔ SameBoySystem bridge:syncMode/syncTempoDivisor/syncAutoStart, so DAWs can automate them): mGB passthrough, LSDj MIDI sync (drift-free 24/divisor-PPQ tick walk off the host transport), Arduinoboy slave (note 24/25 arm, 26–29 divisor, 0xFA/0xFC bookends), MIDI Map (row jumps), Keyboard (PS/2 scancodes), and two output modes — MI.OUT (flag-framed serial → host MIDI notes/CC/PC) and Master Sync (LSDj clocks the DAW) — via a published AU MIDI output port.fullState: per-mode MIDI channel assignments (slave sync / master sync / keyboard / MIDI map, mGB's five voice channels with unassigned-channel drop, MI.OUT's per-voice note+PC and CC channels — shared by Note Out); an mGB base channel (0 = per-voice assignments; 1–12 = the voices sit at base..base+4, so each DAW instance takes its own channel block with one knob); and the full MI.OUT CC matrix — per voice a CC mode (single-CC vs. LSDjXhigh-digit selecting one of seven CC numbers), a scaling flag, and the seven assignable CC numbers, ported verbatim from the firmware'splayCC(defaults 1/2/3/7/10/11/12, which line up with mGB's own CC map so a MI.OUT instance can drive an mGB instance directly).fullState): saves/restores the cartridge (raw bytes for user ROMs; just the id string"mgb"for the built-in synth, so no ROM ships anywhere it isn't already), SRAM, a full savestate, and model/highpass/fast-boot/gain into the host project plist. Versioned, type-checked, tolerant of pre-feature state; funnels host XPC-thread access onto the main thread to preserve the control plane's single-writer contract. On session restore the saved cartridge replaces the default silently. User ROMs live only in user data (app Documents + the user's DAW project) — never in the app bundle; the only ROM embedded in the binary is mGB, which was already there.RetroPlugCoreBridge.h— the main-thread-only control surface (load ROM/SRAM/state, model, buttons, video frame reads via a lock-free triple buffer, autosave SRAM snapshots). IncludesloadSram:for manual.savloads: swaps the cartridge's battery RAM under the bypass gate and reboots (LSDj only reads its save at boot).LsdjSav.swift(inShared/) — a minimal Swift port of the desktop LSDj codec (packages/retroplug/src/lsdj/codec/{sav,rle}.ts): sav-header parse (song names/versions/active slot) and the RLE block decompressor. Songs stay raw 0x8000-byte blobs — the emulator is the editor; this only lists and loads.RetroPlugAU(app-extension)AudioUnitViewController— the AUAudioUnitFactory plus a minimal plugin UI: a status label, Load ROM… and Load .sav… (Files document pickers, copied into the extension sandbox so no security-scope bookkeeping), and Reset to mGB synth. Whatever is loaded persists with the host project viafullState. Sibling.savpairing stays app-only (a picker scope never covers directory siblings); DAW sessions carry SRAM in project state instead.aumu/mgbs/RPtm, sandbox-safe); the parent app embeds the framework, the appex reaches it via rpath.RetroPlug(SwiftUI app)GameBoyScreenView, shared with the extension), touch controls, physical game-controller input, ROM library backed by Documents/ with Files-app sharing (UIFileSharingEnabled+LSSupportsOpeningDocumentsInPlace— the only sandbox-legal way ROM/.sav sibling pairing works), player settings, and UTI declarations for.gb/.gbc/.sav/.rplg.LsdjSongsSheet, in the player's toolbar menu): lists the stored projects in the running cart's battery RAM (name, version, active-slot marker) with tap-to-load (decompresses the song into working memory, marks it active, reboots — the archive is untouched so LSDj's own LOAD/SAVE screen still sees everything), plus a manual Load .sav File… importer that replaces battery RAM wholesale. Loading confirms only when it would lose something: the working song is compared byte-for-byte against its slot's stored copy, and a clean match loads silently. The previous battery save is written out before any swap.UserDefaultsand is pushed into the AU on launch.PrivacyInfo.xcprivacyprivacy manifest.UIRequiresFullScreenand keeps Split View support).Tooling
ios/generate.sh— idempotent one-time codegen (boot ROMs, embedded mGB,GB_VERSION, reflect-cpp headers); run beforexcodegen.ios/README.md— build steps, architecture, phase plan.macOS / desktop impact
None. There is no macOS target in this branch; the macOS plugin remains the DPF build from
packages/native/plugin, untouched. The only thing a desktop developer will notice is the newretroplug.xcworkspaceat the repo root and the extra.gitignoreentries.Testing
reaper:lsdj-*timing matrix still applies to the desktop build, whose behavior this branch doesn't alter (the APU tap ships unarmed).playCC(including its unscaled-multi-mode pass-through quirk) with the EEPROM factory defaults, and the default CC/channel layout was cross-checked against mGB's MIDI implementation (its note-off handling ignores note numbers, so the decode's NoteOff-on-note-0 shorthand stops mGB notes correctly).test:pluginyet),auvalvalidation on device, and host-matrix testing (AUM / Logic for iPad / Cubasis) of multi-out, the in-plugin ROM picker, and fullState round-trips.