How Lighthouse Emulates the Nintendo 64 Audio System with libultralib

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

/* 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:

  • 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, synthesizer.c, and reverb.c implement the concrete n_al* functions that the original game expects.

File Purpose
lib/ultralib/audio/load.c Instrument and sample loading
lib/ultralib/audio/synthesizer.c Voice mixing, envelope generation, pitch shifting
lib/ultralib/audio/reverb.c Reverb, chorus, and low-pass filter effects
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 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 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 (lines 323-340), every other frame triggers audio task creation:

    /* 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:

    /* 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/:

/* 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 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 Central audio initialization, thread management, frame handling
src/core1/graphics_thread.c Audio task scheduling synchronized with graphics frames
src/port/Audio/AudioInterface.c N64 AI register emulation for PC audio output
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 Public API headers exposing n_al* functions
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 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 enqueues an audio task and signals the audio thread via audioFrameMsgQ, duplicating N64 CPU-to-RSP communication.
  • PC shims replace hardware—AudioInterface.c and 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 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.

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 →