An audio synthesizer that emulates the GBA's m4a sound engine
  • C 84.5%
  • C++ 13.5%
  • CMake 2%
Find a file
Marcus Huderle 394b12a481 Render CLI: hardware-faithful handling of notes crossing the loop end
A note still keyed on at a loop wrap crosses the loop end. Previously it
was always released at the wrap; now it matches mid2agb + the GBA player:

- <= 96 mid2agb clocks: emitted as a direct note command whose channel
  gate keeps counting through the GOTO, so the note-off is rescheduled at
  the wrapped position and the note releases at its written duration
  (gate-carry).
- > 96 clocks: becomes TIE + EOT with the EOT beyond the loop end,
  unreachable, so no note-off is emitted at all - the note is held
  forever, stacking a fresh instance every pass, exactly as on hardware.

RenderEvent now carries the SMF tick, parse_midi hands out the tempo map
and loop ticks for exact wrapped-position math, and a new
--extended-clocks flag mirrors mid2agb -X (48 clocks/beat), which raises
the tie threshold.
2026-07-15 08:32:44 -05:00
clap-sdk@29ffcc273b submodule clap-sdk 2026-02-18 17:55:24 -06:00
clap-wrapper@62bf4f193d Ship a standalone synth target 2026-02-25 19:23:09 -06:00
cmd Render CLI: hardware-faithful handling of notes crossing the loop end 2026-07-15 08:32:44 -05:00
imgui@6ded5230d0 Integrate Imgui 2026-02-21 16:45:05 -06:00
plugin Emulate square 1 frequency sweep (NR10) 2026-07-07 08:23:28 -05:00
test Emulate square 1 frequency sweep (NR10) 2026-07-07 08:23:28 -05:00
third_party Macos bitwig fix (#1) 2026-06-16 17:36:37 -05:00
.gitignore Macos bitwig fix (#1) 2026-06-16 17:36:37 -05:00
.gitmodules Use pugl for GUI 2026-02-28 13:51:25 -06:00
CMakeLists.txt Perf improvements via caching various unchanging values per-sample 2026-06-20 09:06:42 -05:00
LICENSE LICENSE 2026-07-03 20:10:00 -05:00
mingw-toolchain.cmake Ship a standalone synth target 2026-02-25 19:23:09 -06:00
poryaaaa.cfg.example Implement accurate pcm-mixing sample rate 2026-07-01 09:04:07 -05:00
README.md Raise MAX_PCM_CHANNELS from 12 to 15 2026-07-06 09:02:56 -05:00
TODOS.md Perf improvements via caching various unchanging values per-sample 2026-06-20 09:06:42 -05:00

Poryaaaa

Poryaaaa is an audio synthesizer that emulates the GBA's m4a sound engine. It's compatible with the pokeemerald and pokefirered decompilation projects.

Overview

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 -- Standalone command-line MIDI renderer

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-15>          Max simultaneous PCM channels (default: 5)
  --sample-rate <hz>          Sample rate in Hz (default: 44100)
  --pcm-mix-rate <hz>         DirectSound (PCM) mix rate; 0 means same as sample-rate (default: 13379)
  --tail <seconds>            Silence after last event, no loop markers (default: 3.0)

Opt-in effect features (off by default; extend the stock m4a engine):
  --respect-base-midi-key     Treat a PCM voice's key as the sample's base MIDI note
  --portamento                Enable the portamento glide effect (CC 5)
  --pwm                       Enable pulse-width modulation on CGB square channels (CC 0x17/0x19)

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)

The opt-in effect features require the matching m4a engine extensions in your project. See huderlem/pokeemerald @ m4a_extensions.

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 90

Loop markers

When 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-count times.
  • After the final repetition, audio continues playing from [ while fading to silence over --fadeout seconds.

--total-duration-seconds overrides --loop-count and sets the exact total render length. The fadeout still occupies the last --fadeout seconds of that duration.

Standalone executable

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.

Running

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.

Sending MIDI on Windows

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):

  1. Install loopMIDI and create a virtual port (e.g. loopMIDI Port).
  2. In your sequencer, set the MIDI output to the loopMIDI port.
  3. Launch poryaaaa_standalone.exe — it will automatically open all available MIDI inputs (e.g. loopMIDI ports).
  4. Play notes or send program changes from your sequencer.

Sending MIDI on Linux

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

CLAP plugin in a DAW

  1. Copy poryaaaa.clap to your DAW's CLAP plugin directory.
    • e.g. On Windows, %APPDATA%\CLAP or C:\Program Files\Common Files\CLAP
  2. Copy poryaaaa.cfg.example to the same directory as the .clap file and rename it poryaaaa.cfg.
  3. Edit the config to point at your project:
    project_root=C:\Users\you\pokeemerald
    voicegroup=petalburg
    
  4. Insert the plugin as an instrument on a MIDI track and rescan plugins or relaunch your DAW so it sees the new plugin.
  5. Use Program Change (PC) midi messages to select instruments from the voicegroup.
  6. 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.

Plugin config reference

Key Default Description
project_root (required) Path to project root
voicegroup (required) Voicegroup name
reverb 0 Reverb amount (0127)
master_volume 15 M4A master volume (015)
song_master_volume 127 Song-level volume multiplier (0127)
max_channels 5 Max simultaneous PCM (DirectSound) channels (115)
pcm_mix_rate 13379 DirectSound (PCM) mix rate in Hz; 13379 = GBA-accurate aliasing, 0 = follow host rate (clean)
respect_base_midi_key 0 Opt-in: treat a PCM voice's key as the sample's base MIDI note so pressed notes play at the intended pitch
portamento 0 Opt-in: enable the portamento glide effect (CC 5 = glide time in ticks)
pwm 0 Opt-in: enable pulse-width modulation on CGB square channels (CC 0x17 / 0x19)
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 opt-in effect features (respect_base_midi_key, portamento, pwm) require the matching m4a engine extensions in your project. See huderlem/pokeemerald @ m4a_extensions.

GUI

The plugin opens a settings panel built with Dear ImGui and Pugl (a lightweight embeddable windowing library):

  • General tab — Project Root / Voicegroup: edit and press Reload to apply; Song Volume (0127), Reverb (0127), Polyphony, GBA Analog Filter, and PCM Mix Rate take effect immediately
  • Voices tab — inspect and edit individual voices in the loaded voicegroup
  • Polyphony tab — realtime monitor for debugging polyphony overflow
  • Options menu — toggle the opt-in effect features (Respect Base MIDI Key, Portamento, Pulse-Width Modulation); hover an item for help text. Toggles take effect immediately and are saved per project.

On Windows the GUI is embedded inside the DAW's FX window. On Linux/macOS it opens as a floating window.

Building

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-dev
cmake -B build
cmake --build build

This 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_render

Cross-compiling for Windows (from WSL2/Linux)

sudo 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-windows

Copy build-windows/poryaaaa.clap to your DAW's CLAP plugin directory (e.g. %APPDATA%\CLAP or C:\Program Files\Common Files\CLAP).

Running tests

./build/poryaaaa_unit_tests

Architecture

cmd/
  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 + Pugl settings GUI (C++ with C interface)
  imgui_impl_pugl.cpp/.h      Custom ImGui Pugl windowing backend
  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)
  pugl/                  Vendored Pugl windowing library (GUI backend)

clap-sdk/                CLAP plugin SDK (submodule)
clap-wrapper/            Wraps the CLAP plugin as a standalone app (submodule)
imgui/                   Dear ImGui (submodule)

CLAP extensions

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

How the engine works

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. The channels (and the DirectSound reverb) are mixed at the configurable PCM mix rate — 13379 Hz by default, matching the GBA's hardware DirectSound rate — then linearly upsampled to the output sample rate. Because the hardware mixes at that low rate, pitched-up high notes alias below its ~6.7 kHz Nyquist exactly as they do in-game; raising the mix rate (up to "host rate") progressively removes that aliasing at the cost of accuracy. CGB channels are synthesized directly at the output rate and mixed in after the upsample.

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.

Voicegroup loading

The loader auto-discovers and parses project assembly source files at runtime:

  1. Project discovery: scans the sound/ directory tree to locate symbol definition files, voicegroup directories, monolithic voicegroup files, and .wav sample directories. Works with pokeemerald's individual-file layout (sound/voicegroups/<name>.inc) and pokefirered's monolithic layout (sound/voice_groups.inc with labeled sections).
  2. Symbol maps: parses direct_sound_data.inc and programmable_wave_data.inc to build symbol-to-file mappings for PCM and programmable wave samples.
  3. Keysplit tables: parses keysplit_tables.inc, supporting both pokeemerald's macro format (keysplit name, startNote) and pokefirered's raw format (.set/.byte directives).
  4. Voice definitions: parses voice macros (directsound, square, noise, keysplit, etc.) from the discovered voicegroup file.
  5. Sample loading: loads .wav samples with a deduplication cache. When a sample symbol isn't found in the symbol map, the loader falls back to searching discovered .wav directories.

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.

Golden Sun synth instruments

Poryaaaa supports the synthesized instruments from Golden Sun's custom mixer, as featured in ipatix's improved ("HQ") mixer: pulse wave (with duty-cycle modulation), pseudo-sawtooth, and triangle. A DirectSound voice whose sample has a length of zero is played as one of these synth tones instead of PCM data. Support is always enabled — projects without synth samples are unaffected.

Both ways of defining synth voices are recognized:

  • Binary samples: a .bin sample with size 0 whose data bytes select the waveform and pulse parameters, referenced from direct_sound_data.inc like any other sample.
  • Assembly macros: inline set_synth_pulse p1, p2, p3, p4, set_synth_saw, and set_synth_triangle definitions, auto-discovered from sound/direct_sound_synth_data.inc. The alias names set_synth_custom, set_synth_25, and set_synth_50 are also accepted.

GBA source reference

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

AI Disclaimer

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.