# How Lighthouse Emulates the Nintendo 64 Audio System with libultralib

> Discover how Lighthouse emulates the Nintendo 64 audio system using libultralib. This port preserves original Banjo-Kazooie audio behavior on modern PCs.

- Repository: [Harbour Masters/Lighthouse](https://github.com/HarbourMasters/Lighthouse)
- Tags: deep-dive
- Published: 2026-08-04

---

**Lighthouse ports the original Nintendo 64 sound microcode to the PC platform by wrapping the `libultra` audio library—commonly called *libultralib*—in a thin abstraction layer that preserves the exact behavior of Banjo-Kazooie’s original audio code while outputting to modern audio APIs.**

The N64's audio pipeline was famously complex: the RSP (Reality Signal Processor) ran proprietary microcode to synthesize instruments, apply effects, and mix PCM samples, while the CPU submitted fresh command lists every frame. Lighthouse re-creates this entire flow by shipping a complete copy of the N64 SDK audio implementation in `lib/ultralib/audio/` and bridging it to host OS primitives. This approach lets the original game code call `n_alAudioFrame()`, `alSeqPlayer*()`, and other `libultralib` functions without modification.

## Core Components of N64 Audio Emulation in Lighthouse

### Audio Manager: The CPU-Side Controller

The **audio manager** in [`src/core1/audio_manager.c`](https://github.com/HarbourMasters/Lighthouse/blob/main/src/core1/audio_manager.c) initializes the heap, spawns the audio thread, and drives the per-frame synthesis loop. It allocates a fixed-size audio heap, configures the sequence player, and repeatedly invokes `n_alAudioFrame()` to generate sound buffers.

```c
/* src/core1/audio_manager.c */
void audioManager_init(void) {
    sALHeapBuffer = (u8 *)bk_malloc(AUDIO_HEAP_SIZE);
    bzero(sALHeapBuffer, AUDIO_HEAP_SIZE);
    alHeapInit(&sALHeapInfo, sALHeapBuffer, AUDIO_HEAP_SIZE);

    /* Set video clock for PAL/NTSC timing */
#if VERSION == VERSION_USA_1_0
    if (osTvType != OS_TV_NTSC) osViClock = VI_MPAL_CLOCK;
#elif VERSION == VERSION_PAL
    osViClock = VI_PAL_CLOCK;
#endif

    audioManager_create();        /* allocate queues, DMA, ACMD buffers */
    sfxInstruments_init();        /* load SFX instrument tables */
    musicInstruments_init();      /* load music instrument tables */
    audioManager_startThread();   /* spawn the audio thread */
}

```

Key responsibilities of [`audio_manager.c`](https://github.com/HarbourMasters/Lighthouse/blob/main/audio_manager.c):
- Calls `n_alInit(&sn_alGlobals, &sn_alConfig)` to initialize the synthesizer
- Registers custom oscillator callbacks via `audioManager_setupSeqp()`
- Spawns `audioManagerThread_entry()` which blocks on `audioFrameMsgQ`
- Handles frame messages and executes `n_alAudioFrame()` to produce mixed audio

### libultralib: The RSP Microcode in Software

The `lib/ultralib/audio/` directory contains a full software implementation of the N64's audio microcode. Files like [`load.c`](https://github.com/HarbourMasters/Lighthouse/blob/main/load.c), [`synthesizer.c`](https://github.com/HarbourMasters/Lighthouse/blob/main/synthesizer.c), and [`reverb.c`](https://github.com/HarbourMasters/Lighthouse/blob/main/reverb.c) implement the concrete `n_al*` functions that the original game expects.

| File | Purpose |
|------|---------|
| [`lib/ultralib/audio/load.c`](https://github.com/HarbourMasters/Lighthouse/blob/main/lib/ultralib/audio/load.c) | Instrument and sample loading |
| [`lib/ultralib/audio/synthesizer.c`](https://github.com/HarbourMasters/Lighthouse/blob/main/lib/ultralib/audio/synthesizer.c) | Voice mixing, envelope generation, pitch shifting |
| [`lib/ultralib/audio/reverb.c`](https://github.com/HarbourMasters/Lighthouse/blob/main/lib/ultralib/audio/reverb.c) | Reverb, chorus, and low-pass filter effects |
| [`lib/ultralib/audio/seqplayer.c`](https://github.com/HarbourMasters/Lighthouse/blob/main/lib/ultralib/audio/seqplayer.c) | MIDI sequence playback and control |

These files provide drop-in replacements for the N64 SDK's audio functions, preserving exact reproduction of instruments, envelopes, and spatial effects.

### Audio Interface: Bridging to PC Audio APIs

[`src/port/Audio/AudioInterface.c`](https://github.com/HarbourMasters/Lighthouse/blob/main/src/port/Audio/AudioInterface.c) mimics the N64's **Audio Interface (AI)** hardware—specifically its DMA registers and timing—while actually feeding data to SDL or PortAudio. The game code still calls `osAiSetFrequency(22000)` and `osAiGetLength()`, but these resolve to PC-compatible implementations that schedule real audio callbacks.

### OS Shim: Message Queues and Threading

The audio code relies on N64 OS primitives: `osCreateMesgQueue`, `osRecvMesg`, `osCreateThread`, `osSetTimer`. [`src/port/OS/OS.c`](https://github.com/HarbourMasters/Lighthouse/blob/main/src/port/OS/OS.c) re-implements these atop standard threading APIs, allowing the original synchronization patterns to function unchanged.

## How the Frame Loop Mirrors N64 Hardware

Lighthouse's audio flow precisely duplicates the N64's frame-by-frame command submission:

1. **Graphics thread builds the command list**—In [`src/core1/graphics_thread.c`](https://github.com/HarbourMasters/Lighthouse/blob/main/src/core1/graphics_thread.c) (lines 323-340), every other frame triggers audio task creation:
   ```c
   /* src/core1/graphics_thread.c (excerpt) */
   static s32 audiotimer_trigger = 0;
   audiotimer_trigger++;
   if (!(audiotimer_trigger & 1)) {
       struct ucode_task_data_s *active_audio_task;
       active_audio_task = sAudioTaskDataList[sActiveAudioTaskDataID];
       thread5_startAudioTask(active_audio_task);
       osSendMesg(sActiveAudioTaskDataPtr->audio_mesg_queue,
                  sActiveAudioTaskDataPtr->audio_mesg, 0);
   }
   ```

2. **Audio thread wakes and executes**—`audioManager_handleFrameMsg()` receives the message, locks audio state, and calls the synthesizer:
   ```c
   /* src/core1/audio_manager.c */
   bool audioManager_handleFrameMsg(AudioInfo *info, AudioInfo *prev_info) {
       s16 *outbuffer;
       Acmd *command_list_end;
       s32 command_list_len;

       port_lockAudio();
       outbuffer = (s16 *)osVirtualToPhysical(info->data);
       command_list_end = n_alAudioFrame(
           audioManager.ACMDList[sCmdBufferIndex],
           &command_list_len,
           outbuffer,
           info->frame_samples);
       core1_15B30_addAudioTaskData(audioManager.ACMDList[sCmdBufferIndex],
                                    command_list_end,
                                    &audioManager.audioReplyMsgQ,
                                    OS_MESG_PTR(&info->reply_mesg_data));
       return true;
   }
   ```

3. **libultralib processes the command list**—`n_alAudioFrame()` walks the `Acmd` list, mixes active voices, applies effects (reverb, chorus, filtering), and writes finished PCM to `info->data`.

4. **PC backend plays the buffer**—The audio interface consumes samples at the configured sample rate and outputs through the host device.

This mirrors the original: CPU builds `Acmd` → RSP processes microcode → AI DMAs to DAC. Lighthouse simply replaces RSP/AI with software implementations.

## Using the libultralib Sequence Player

Game code interacts with music playback through the standard `libultra` API, all resolved to `lib/ultralib/audio/`:

```c
/* Example from game code */
ALSeq *seq;
ALSeqPlayer *seqPlayer;

/* Load a sequence */
seq = alSeqNew(&sn_alGlobals, seqData, seqSize);

/* Create a sequence player and attach the sequence */
seqPlayer = alSeqPlayerNew(&sn_alGlobals, &sn_alConfig);
alSeqPlayerSetSeq(seqPlayer, seq);

/* Start playback */
alSeqPlayerPlay(seqPlayer);

```

The `sn_alGlobals` and `sn_alConfig` structures initialized in [`audio_manager.c`](https://github.com/HarbourMasters/Lighthouse/blob/main/audio_manager.c) configure polyphony limits, effect bus routing, and heap allocation—exactly as on N64 hardware.

## Key Source Files for N64 Audio Emulation

| Path | Role |
|------|------|
| [`src/core1/audio_manager.c`](https://github.com/HarbourMasters/Lighthouse/blob/main/src/core1/audio_manager.c) | Central audio initialization, thread management, frame handling |
| [`src/core1/graphics_thread.c`](https://github.com/HarbourMasters/Lighthouse/blob/main/src/core1/graphics_thread.c) | Audio task scheduling synchronized with graphics frames |
| [`src/port/Audio/AudioInterface.c`](https://github.com/HarbourMasters/Lighthouse/blob/main/src/port/Audio/AudioInterface.c) | N64 AI register emulation for PC audio output |
| [`src/port/OS/OS.c`](https://github.com/HarbourMasters/Lighthouse/blob/main/src/port/OS/OS.c) | Message queues, threads, timers for audio synchronization |
| `lib/ultralib/audio/*.c` | Complete software synthesizer implementing original microcode |
| [`lib/ultralib/audio/audio.h`](https://github.com/HarbourMasters/Lighthouse/blob/main/lib/ultralib/audio/audio.h) | Public API headers exposing `n_al*` functions |
| [`src/port/OS/libultra.c`](https://github.com/HarbourMasters/Lighthouse/blob/main/src/port/OS/libultra.c) | Glue mapping N64 SDK includes to local paths |

According to the HarbourMasters/Lighthouse source code, this architecture preserves byte-accurate audio behavior while eliminating the need for actual RSP emulation or binary blob injection.

## Summary

- **libultralib ports the N64 audio microcode**—The `lib/ultralib/audio/` directory contains a software reimplementation of the original RSP audio code, handling synthesis, effects, and sequencing.
- **Audio manager orchestrates the frame loop**—[`audio_manager.c`](https://github.com/HarbourMasters/Lighthouse/blob/main/audio_manager.c) initializes the system, spawns the thread, and drives `n_alAudioFrame()` calls that mirror RSP task execution.
- **Graphics thread triggers audio work**—Every frame, [`graphics_thread.c`](https://github.com/HarbourMasters/Lighthouse/blob/main/graphics_thread.c) enqueues an audio task and signals the audio thread via `audioFrameMsgQ`, duplicating N64 CPU-to-RSP communication.
- **PC shims replace hardware**—[`AudioInterface.c`](https://github.com/HarbourMasters/Lighthouse/blob/main/AudioInterface.c) and [`OS.c`](https://github.com/HarbourMasters/Lighthouse/blob/main/OS.c) translate N64 registers and OS primitives to portable APIs without changing game code.
- **Original sequence code runs unmodified**—Game code calling `alSeqPlayerNew()`, `alSeqPlayerPlay()`, and related functions executes exactly as on hardware.

## Frequently Asked Questions

### What sample rate does Lighthouse use for N64 audio emulation?

Lighthouse defaults to **22050 Hz** (the original N64 rate) via `osAiSetFrequency(22000)`, though the exact implementation in [`AudioInterface.c`](https://github.com/HarbourMasters/Lighthouse/blob/main/AudioInterface.c) may resample to match host device capabilities while preserving the original timing characteristics.

### Does Lighthouse use the original Banjo-Kazooie audio binaries?

No—Lighthouse uses **reconstructed source code** that calls the standard `libultra` audio API. The actual synthesizer implementation lives in `lib/ultralib/audio/`, which is a clean-room reimplementation of the N64 SDK audio library, not extracted microcode.

### How does the audio thread stay synchronized with the game?

The audio thread blocks on `osRecvMesg(&audioFrameMsgQ, ...)` in `audioManagerThread_entry()`. The graphics thread sends one message per frame (or every other frame, depending on `audiotimer_trigger`), ensuring audio generation stays locked to video refresh—just as the N64's RSP audio tasks were triggered by vertical blank.

### Can custom instrument definitions be loaded?

Yes. `audioManager_setupSeqp()` registers custom oscillator callbacks (`initOsc`, `updateOsc`, `stopOsc`) that the game's music system uses. The instrument tables loaded in `sfxInstruments_init()` and `musicInstruments_init()` define the actual sample maps and envelope parameters.