A playback engine for the GBA "Sappy" (M4A/MP2000) sound format, meant to be embedded in a Metroid Zero Mission / Metroid Fusion editor. It is a new implementation inspired by the SapPy project (a Python port of Sappy 2006), not a port of it.
Right now all of this vibecoded, as is apparent by the Claude-authored commits and general look and layout of the project. The playback engine works pretty well already and is only missing some smaller features. Songs can be exported to .wav files. The project features a small CLI wrapper around the engine for use outside of an editor implementation.
Everything following is the default AI-generated summary of the project
| Project | Purpose |
|---|---|
src/Sappen.Core |
The engine library (net10.0, no audio or UI dependencies). |
src/Sappen.Audio |
OpenAL output (Silk.NET, native OpenAL Soft bundled): streams any IAudioSource to the sound card. |
src/Sappen.Cli |
Command-line tool: info, dump, voices, export, play; --interactive keeps a ROM loaded. |
tests/Sappen.Tests |
xUnit tests. Real-ROM tests run only if SAPPEN_TEST_ROM and SAPPEN_TEST_SONG_TABLE (hex) are set; no ROM is ever committed. |
Inside Sappen.Core:
Rom/bounds-checked ROM access and GBA addressing.Driver/the game's sound driver settings (SoundDriverMode).Song/song table and header, track command decoding (TrackDecoder), and static control-flow listings (TrackListing).Voices/voice groups (DirectSound, PSG, key-split, drum) and sample headers.Engine/the runtime:SoundEngine.StepFrame()runs the sequencer, PSG channels and DirectSound envelopes for one video frame (59.7275 Hz) and reportsEngineEvents.Playback/SongPlayer: pause/resume, position, seek, per-track mute/solo, volume; anIAudioSourcean output pulls from.Mixing/turns the engine's channels into audio:Mixer(DirectSound playback, PSG synthesis, per-track mute/solo),OfflineRenderer(fast render with loop count and fade-out) andWavWriter.
- Done: ROM/song/voice parsing, command decoding, control-flow listing, sequencer, channel allocation and envelopes,
mixer with reverb, WAV export, live playback with seek and mute/solo, Zero Mission's
B6(return to previous song). - Not done: length counters and sweep on the PSG channels, compressed samples, Fusion support, tempo/speed control in the player.
dotnet run --project src/Sappen.Cli -- -i path\to\zm.gba 0x8F2C0
> preset zm
> export 1 song1.wav
> play 1 (through the sound card; then: status, pause, resume, seek 60, mute 3, solo 1, volume 80, stop)
The runtime in Engine/ was written against the public pokeemerald decompilation of the standard M4A library
(its tables in Engine/PitchTables.cs are extracted from it). Zero Mission does not use that code: the game ships
its own driver (asm/soundcode.s, asm/audio_internal.s and src/audio.c in the mzm decompilation) that reads the
same song data but differs in behavior, for example in tempo, LFO depth, note frequency and channel handling.
Parsing (Rom/, Song/, Voices/) is shared by both, but Engine/ needs to be checked against the game's own driver
before it can be called accurate for Zero Mission or Fusion. Until then treat it as a faithful M4A engine, not a
Metroid-exact one.