Poryaaaa is an audio synthesizer that emulates the GBA's m4a sound engine. It's compatible with the pokeemerald and pokefirered decompilation projects.
Composing custom music for Pokémon GBA games normally requires a slow iteration loop: write notes in a DAW, export midi file, build the ROM, and test in an emulator. Without a synth like poryaaaa, the DAW's audio will sound very different from how it sounds in-game on the GBA. This project eliminates that painful gap by emulating the m4a mixer and loading audio data (e.g. voicegroups, samples) directly from the decomp project's files.
These are the available poryaaaa tools:
poryaaaa.clap: a CLAP instrument plugin. Insert it on a MIDI track in your DAW and hear the GBA-accurate audio in real time as you compose.poryaaaa_standalone(.exe): a standalone GUI that wraps the CLAP plugin. It receives MIDI from any connected device or virtual cable and plays audio through your speakers.poryaaaa_render(.exe): a standalone command-line renderer. Feed it a MIDI file and a voicegroup name; it outputs a WAV file and/or plays audio through your speakers. Supports looping with configurable repeat count and fadeout.
All tools auto-discover the project structure and work with pokeemerald, pokefirered, and forked projects (including those with custom sound data directories).
Usage: poryaaaa_render <project_root> <voicegroup> --midi <file.mid> [options] Required: <project_root> Path to pokeemerald/pokefirered project root <voicegroup> Voicegroup name (e.g. petalburg) --midi <file.mid> MIDI input file Output (at least one required): --output <file.wav> Write rendered audio to WAV file --play Play audio through computer speakers Audio options: --song-volume <0-127> Song master volume (default: 127) --reverb <0-127> Reverb amount (default: 0) --analog-filter Enable GBA analog low-pass filter (default: off) --polyphony <1-12> Max simultaneous PCM channels (default: 5) --sample-rate <hz> Sample rate in Hz (default: 44100) --tail <seconds> Silence after last event, no loop markers (default: 3.0) Loop options (when MIDI contains '[' / ']' text events): --loop-count <n> Number of loop body repetitions (default: 2) --fadeout <seconds> Fadeout duration after final loop (default: 5.0) --total-duration-seconds <s> Override loop-count; set exact total duration (fadeout occupies the final --fadeout seconds) Examples:
# Render to WAV with reverb and analog filter ./build/poryaaaa_render /path/to/pokeemerald petalburg \ --midi song.mid --output out.wav --reverb 40 --analog-filter # Play through speakers ./build/poryaaaa_render /path/to/pokeemerald petalburg \ --midi song.mid --play --song-volume 100 # Render and play simultaneously ./build/poryaaaa_render /path/to/pokeemerald petalburg \ --midi song.mid --output out.wav --play # Loop twice with 5-second fadeout (default when '['/']' markers found) ./build/poryaaaa_render /path/to/pokeemerald petalburg \ --midi song.mid --output out.wav # Custom total duration of 90 seconds with 5-second fadeout ./build/poryaaaa_render /path/to/pokeemerald petalburg \ --midi song.mid --output out.wav --total-duration-seconds 90When the MIDI file contains text events (Meta type 0x01) or marker events (Meta type 0x06) with the content [ and ], poryaaaa_render treats those as loop boundaries:
- Everything before
[is the intro, played once. - The region from
[to]is the loop body, repeated--loop-counttimes. - After the final repetition, audio continues playing from
[while fading to silence over--fadeoutseconds.
--total-duration-seconds overrides --loop-count and sets the exact total render length. The fadeout still occupies the last --fadeout seconds of that duration.
poryaaaa_standalone(.exe) wraps the CLAP plugin as a self-contained application with its own audio output (via RtAudio) and MIDI input (via RtMidi). It presents the same ImGui settings GUI as the DAW plugin. No DAW or config file is needed to run it.
Simply launch the executable. The GUI window opens immediately:
# Linux ./poryaaaa_standalone # Windows (or double click on it) poryaaaa_standalone.exe- Edit Project Root and Voicegroup and press Reload to load a voicegroup.
- Adjust Song Volume and Reverb live.
- Close the window to exit.
The app reads poryaaaa.cfg (located next to the executable) on startup as initial defaults, using the same format as the plugin. See the Plugin config reference below.
RtMidi uses the WinMM MIDI backend. Any device or virtual MIDI port visible in Windows will be detected on startup.
To route MIDI from software (e.g. Sekaiju, a DAW, or other sequencer):
- Install loopMIDI and create a virtual port (e.g.
loopMIDI Port). - In your sequencer, set the MIDI output to the loopMIDI port.
- Launch
poryaaaa_standalone.exe— it will automatically open all available MIDI inputs (e.g. loopMIDI ports). - Play notes or send program changes from your sequencer.
RtMidi uses the ALSA sequencer backend. Create a virtual MIDI port with any tool and connect it to poryaaaa_standalone's input:
# Example using aconnect (part of alsa-utils) aconnect <source_port> <poryaaaa_port>Caveat: I haven't actually tested this on Linux. It theoretically works...
- Copy
poryaaaa.clapto your DAW's CLAP plugin directory.- e.g. On Windows,
%APPDATA%\CLAPorC:\Program Files\Common Files\CLAP
- e.g. On Windows,
- Copy
poryaaaa.cfg.exampleto the same directory as the.clapfile and rename itporyaaaa.cfg. - Edit the config to point at your project:
project_root=C:\Users\you\pokeemerald voicegroup=petalburg
- Insert the plugin as an instrument on a MIDI track and rescan plugins or relaunch your DAW so it sees the new plugin.
- Use Program Change (PC) midi messages to select instruments from the voicegroup.
- Play MIDI notes. You should hear GBA-accurate audio.
The plugin reads poryaaaa.cfg on startup for initial defaults. All settings can be changed live through the GUI and are saved in the DAW's project state.
| Key | Default | Description |
|---|---|---|
project_root | (required) | Path to project root |
voicegroup | (required) | Voicegroup name |
reverb | 0 | Reverb amount (0–127) |
master_volume | 15 | M4A master volume (0–15) |
song_master_volume | 127 | Song-level volume multiplier (0–127) |
sound_data_paths | (auto) | Extra .inc files for sample symbols (semicolon-separated, relative to project root) |
voicegroup_paths | (auto) | Extra voicegroup search directories or files |
sample_dirs | (auto) | Extra .wav sample search directories |
log | (off) | Diagnostic log file path |
The plugin opens a settings panel built with Dear ImGui and GLFW:
- Project Root / Voicegroup: edit and press Reload to apply
- Song Volume (0–127), Reverb (0–127): take effect immediately
On Windows the GUI is embedded inside the DAW's FX window. On Linux/macOS it opens as a floating window.
Requires CMake 3.16+, a C11/C++17 compiler, and the following development libraries on Linux:
sudo apt-get install build-essential cmake pkg-config sudo apt-get install libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libxext-dev libgl-dev libasound-devcmake -B build cmake --build buildThis produces the following targets:
| Target | Output | Description |
|---|---|---|
poryaaaa | poryaaaa.clap | CLAP instrument plugin |
poryaaaa-standalone | poryaaaa_standalone(.exe) | Standalone GUI |
poryaaaa_render | poryaaaa_render(.exe) | Standalone MIDI renderer |
poryaaaa_test | poryaaaa | Quick WAV export test (hardcoded sequence) |
poryaaaa_unit_tests | poryaaaa_unit_tests | Engine unit test suite |
To build a single target:
cmake --build build --target poryaaaa-standalone cmake --build build --target poryaaaa_rendersudo apt install gcc-mingw-w64-x86-64 g++-mingw-w64-x86-64 cmake -B build-windows -DCMAKE_TOOLCHAIN_FILE=mingw-toolchain.cmake cmake --build build-windowsCopy build-windows/poryaaaa.clap to your DAW's CLAP plugin directory (e.g. %APPDATA%\CLAP or C:\Program Files\Common Files\CLAP).
./build/poryaaaa_unit_testscmd/ poryaaaa_render.c Standalone MIDI renderer (CLI tool) plugin/ m4a_plugin.c/.h CLAP entry point, MIDI event handling, extension dispatch m4a_gui.cpp/.h Dear ImGui + GLFW settings GUI (C++ with C interface) m4a_engine.c/.h Core engine: tick processing, channel allocation, MIDI routing m4a_channel.c/.h PCM and CGB channel rendering, ADSR envelopes m4a_tables.c/.h Frequency/scale tables (from m4a_tables.c) m4a_reverb.c/.h Delay-based reverb effect voicegroup_loader.c/.h Project discovery, .inc/.s parser, sample loader standalone_main_win32.cpp Custom Win32 entry point for the standalone executable test/ test_engine.c Unit tests for engine algorithms test_wav_export.c Hardcoded-sequence WAV export test (poryaaaa_test) third_party/ miniaudio.h Single-header audio I/O library (used by poryaaaa_render) clap-sdk/ CLAP plugin SDK (submodule) clap-wrapper/ Wraps the CLAP plugin as a standalone app (submodule) glfw/ GLFW 3.4 (submodule) imgui/ Dear ImGui (submodule) | Extension | Purpose |
|---|---|
CLAP_EXT_AUDIO_PORTS | Declares one stereo output port |
CLAP_EXT_NOTE_PORTS | Declares one MIDI input port |
CLAP_EXT_STATE | Saves/restores voicegroup path and audio settings in the DAW project |
CLAP_EXT_GUI | Settings GUI (Win32 embedded on Windows, floating on Linux/macOS) |
CLAP_EXT_TIMER_SUPPORT | 16 ms timer that drives GUI rendering and applies live parameter changes |
The engine runs a tick at the GBA's VBlank rate (~59.7 Hz) to advance envelopes and LFO, while rendering audio sample-by-sample at the configured sample rate (typically 44100 or 48000 Hz).
PCM channels use the same 23-bit fractional sample position and linear interpolation as the GBA's SoundMainRAM mixer. Frequency is computed using MidiKeyToFreq with the exact scale/frequency table lookups, then scaled from the GBA's ~13379 Hz output rate to the target sample rate.
CGB channels are synthesized in software: square waves use an 8-step duty cycle pattern with a phase accumulator, programmable wave reads 4-bit nibbles from 16-byte waveforms, and noise uses a 15-bit LFSR.
poryaaaa_render pre-renders the entire song to float stereo buffers before writing or playing back. For looping songs it builds an extended event timeline by repeating the loop body events with increasing sample offsets, then applies a linear fadeout envelope in place over the final window.
The loader auto-discovers and parses project assembly source files at runtime:
- Project discovery: scans the
sound/directory tree to locate symbol definition files, voicegroup directories, monolithic voicegroup files, and.wavsample directories. Works with pokeemerald's individual-file layout (sound/voicegroups/<name>.inc) and pokefirered's monolithic layout (sound/voice_groups.incwith labeled sections). - Symbol maps: parses
direct_sound_data.incandprogrammable_wave_data.incto build symbol-to-file mappings for PCM and programmable wave samples. - Keysplit tables: parses
keysplit_tables.inc, supporting both pokeemerald's macro format (keysplit name, startNote) and pokefirered's raw format (.set/.bytedirectives). - Voice definitions: parses voice macros (
directsound,square,noise,keysplit, etc.) from the discovered voicegroup file. - Sample loading: loads
.wavsamples with a deduplication cache. When a sample symbol isn't found in the symbol map, the loader falls back to searching discovered.wavdirectories.
Keysplit and drumset sub-voicegroups are resolved recursively across all discovered voicegroup directories. Additional search paths can be configured for projects with non-standard layouts.
The engine reimplements algorithms from these pokeemerald source files:
| File | Content |
|---|---|
src/m4a.c | MidiKeyToFreq, TrkVolPitSet, CgbSound, CgbModVol |
src/m4a_1.s | SoundMainRAM (PCM mixer, reverb, envelope processing) |
src/m4a_tables.c | gScaleTable, gFreqTable, CGB frequency tables |
include/gba/m4a_internal.h | Struct definitions |
Much of this codebase was written with the aide of Claude Code, which allowed extremely rapid exploration, implementation, and iteration of the m4a engine code, along with consulting mgba's audio emulation.