# N64 Hardware Considerations for 2D Audio Streaming in Pyrite64

> Master N64 2D audio streaming with Pyrite64. Learn efficient 32-channel management, stereo playback, and WAV64 compression to overcome RAM constraints.

- Repository: [Max Bebök/pyrite64](https://github.com/hailtododongo/pyrite64)
- Tags: internals
- Published: 2026-02-19

---

**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`](https://github.com/HailToDodongo/pyrite64/blob/main/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.

```cpp
// 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`](https://github.com/HailToDodongo/pyrite64/blob/main/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.

```cpp
// 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`](https://github.com/HailToDodongo/pyrite64/blob/main/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.

```cpp
// 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`](https://github.com/HailToDodongo/pyrite64/blob/main/audio2d.h). In [`audio2d.cpp`](https://github.com/HailToDodongo/pyrite64/blob/main/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()`.

```cpp
// 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`](https://github.com/HailToDodongo/pyrite64/blob/main/audio2d.cpp) (lines 36-44):

```cpp
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`](https://github.com/HailToDodongo/pyrite64/blob/main/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`](https://github.com/HailToDodongo/pyrite64/blob/main/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:

```cpp
// 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

```

```cpp
// 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
}

```

```cpp
// 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`](https://github.com/HailToDodongo/pyrite64/blob/main/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.