N64 Hardware Considerations for 2D Audio Streaming in Pyrite64

Pyrite64 manages the Nintendo 64’s strict 32-channel audio limit by pre-allocating mono slots in AudioManager and consuming consecutive pairs for stereo playback, while converting assets to compressed wav64_t format to respect the console’s 4 MiB RAM constraint.

Pyrite64 is a modern game engine for the Nintendo 64 that wraps libdragon’s audio API into a component-based 2D audio system. When streaming or playing 2D sound effects and music, developers must account for the N64’s hardwired channel limitations and memory boundaries. The engine abstracts these hardware constraints through the AudioManager class and Audio2D components, handling channel allocation, format conversion, and mixing automatically.

The 32-Channel Hard Limit and Slot Management

The Nintendo 64 audio hardware can mix a maximum of 32 mono channels (equivalent to 16 stereo pairs). In n64/engine/src/audio/audioManager.cpp, Pyrite64 defines CHANNEL_COUNT = 32 and pre-allocates a fixed array of slots to track usage.

The AudioManager tracks free slots through getFreeSlot() for mono sources and getFreeSlotStereo() for stereo sources. These methods scan the pre-allocated array to find available indices, ensuring the engine never attempts to allocate beyond the hardware limit.

// From audioManager.cpp - Channel allocation logic
#define CHANNEL_COUNT 32

int AudioManager::getFreeSlot() {
    // Scans slots 0-31 for unused channel
    for(int i = 0; i < CHANNEL_COUNT; ++i) {
        if(!slots[i].active) return i;
    }
    return -1; // Hardware limit reached
}

Exceeding this limit results in silent playback or failed allocation, so the engine enforces strict accounting at the software level.

Memory Footprint and wav64 Format Constraints

With only 4 MiB of RDRAM available on a standard N64, audio assets must remain memory-resident or be streamed from cartridge. Pyrite64 uses libdragon’s wav64_t format—a compressed ADPCM container—to minimize RAM usage.

The build pipeline in src/build/audioBuilder.cpp (lines 16-40) invokes the external audioconv64 tool to convert raw WAV files into wav64 format, optionally forcing mono conversion, resampling, and compression. At runtime, Audio2D::initDelete loads these assets via AssetManager::getByIndex() and stores a persistent pointer, keeping the data resident for the component’s lifetime.

// From audio2d.cpp - Asset loading during initialization
void Audio2D::initDelete(Audio2DData* data, Audio2DInitData* initData) {
    data->wave = (wav64_t*)AssetManager::getByIndex(initData->assetIndex);
    // Data remains in RAM until component destruction
}

This approach trades cartridge storage for RAM capacity, ensuring samples are immediately available for the mixer without mid-frame disk access.

Stereo vs. Mono Channel Allocation Strategy

Stereo samples consume two consecutive hardware channels, while mono samples use one. In audioManager.cpp (lines 87-100), the play2D() method checks wave.channels == 2 to determine allocation strategy.

When playing a stereo wav64, the engine calls getFreeSlotStereo() to reserve a pair of adjacent indices, then duplicates slot data for the second channel. This design prevents channel fragmentation and ensures the N64’s stereo mixing hardware receives properly paired inputs.

// Conceptual allocation from play2D()
if(wave->channels == 2) {
    int slotLeft = getFreeSlotStereo(); // Finds pair (n, n+1)
    int slotRight = slotLeft + 1;
    // Configure both slots for left/right panning
}

To maximize simultaneous playback, reserve stereo allocation for music or high-priority atmospheric effects, and use mono assets for short SFX.

Runtime Playback Configuration

Looping and Auto-Play Flags

The Audio2D component supports hardware-accelerated looping and immediate playback through bit flags defined in audio2d.h. In audio2d.cpp (lines 38-44), initDelete evaluates FLAG_LOOP (bit 0) to invoke wav64_set_loop(), and FLAG_AUTO_PLAY to immediately trigger AudioManager::play2D().

// From audio2d.cpp - Flag handling during init
if(initData->flags & FLAG_LOOP) {
    wav64_set_loop(data->wave, true);
}
if(initData->flags & FLAG_AUTO_PLAY) {
    data->handle = AudioManager::play2D(data->wave);
}

Volume Scaling and Master Control

Per-sample volume is stored as a 16-bit integer (0-65535) in the initialization data and converted to a normalized float (0.0-1.0) before application. The conversion occurs in audio2d.cpp (lines 36-44):

data->volume = (float)initData->volume * (1.0f / 0xFFFF);
handle.setVolume(data->volume);

Global scaling is applied in the mixer update loop. AudioManager::setMasterVolume() updates an internal masterVol variable, which the update loop multiplies against per-channel volumes for final stereo output (lines 47-54 and 70-76 in audioManager.cpp).

The Frame Update Cycle

The engine must call AudioManager::update() every frame to synchronize the mixer state. This method, implemented in audioManager.cpp (lines 57-78), processes channel completion callbacks, applies per-slot volume scaling, and updates timing statistics. Missing this call results in stalled audio or desynchronized volume changes.

Implementation Examples

The following patterns demonstrate safe 2D audio usage within hardware constraints:

// Component-based background music with looping
Audio2D music;
music.flags = Audio2D::FLAG_LOOP | Audio2D::FLAG_AUTO_PLAY;
music.volume = 0.8f * 0xFFFF; // 80% in fixed-point range
music.audio = (wav64_t*)AssetManager::getByIndex(MUSIC_ASSET_ID);
sceneObject.addComponent(&music);
// initDelete automatically starts playback on scene load
// Manual handle for dynamic SFX with early termination
Audio::Handle sfx = AudioManager::play2D(
    (wav64_t*)AssetManager::getByIndex(SFX_ASSET_ID)
);
sfx.setVolume(0.5f);

// Later in the frame loop
if(gameState.shouldStopSfx && !sfx.isDone()) {
    sfx.stop(); // Frees channel slot immediately
}
// Global volume adjustment (e.g., options menu)
AudioManager::setMasterVolume(0.6f); // 60% of all channel volumes

Summary

  • Channel Limit: The N64 hardware supports 32 mono channels; Pyrite64 enforces this via CHANNEL_COUNT = 32 and slot tracking in AudioManager.
  • Memory Efficiency: Assets convert to wav64_t via audioconv64 during build and load into the 4 MiB RDRAM pool through AssetManager.
  • Stereo Cost: Stereo samples consume two consecutive slots via getFreeSlotStereo(), reducing maximum simultaneous voices to 16 pairs.
  • Runtime Requirements: Developers must call AudioManager::update() every frame and use Audio::Handle for manual control of non-component audio.

Frequently Asked Questions

How many simultaneous 2D sounds can Pyrite64 play at once?

Pyrite64 supports a maximum of 32 mono channels or 16 stereo pairs simultaneously, matching the Nintendo 64 hardware limit. The AudioManager tracks allocation through getFreeSlot() and getFreeSlotStereo(); attempting to play beyond this limit returns an invalid handle or fails silently.

Why must I convert audio to wav64 format instead of using raw WAV files?

Raw PCM WAV files consume excessive RAM within the N64’s 4 MiB limit. The audioBuilder.cpp pipeline converts assets to wav64_t (ADPCM compressed) using audioconv64, reducing memory footprint by approximately 50% while maintaining playback quality. This conversion happens during the build process, not at runtime.

What is the difference between FLAG_AUTO_PLAY and manual play2D() calls?

FLAG_AUTO_PLAY triggers playback immediately inside Audio2D::initDelete, suitable for ambient sounds attached to scene objects. Manual AudioManager::play2D() calls return an Audio::Handle, allowing runtime control over volume, stopping, and loop status for dynamic sound effects triggered by game logic.

How does the master volume affect individual channel limits?

AudioManager::setMasterVolume() applies a global multiplier (0.0-1.0) to all active channel volumes during the update() cycle. It does not affect channel allocation or the 32-slot limit; it only scales the final mixed output level before submission to the N64’s audio DAC.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →