nCine::N64AudioDevice class

Nintendo 64 implementation of IAudioDevice on top of libdragon's RSP mixer.

The console has no sound chip - its Audio Interface is a bare stereo DAC that DMAs 16-bit samples out of RDRAM - but it has the RSP, and libdragon's mixer runs there: up to 32 channels resampled, panned and summed by microcode, with the result written straight into the buffers the AI plays. This backend used to do all of that on the VR4300 itself, one output sample at a time; now the CPU only decides what plays where and the RSP does the arithmetic. It is also what makes music possible at all: libdragon's tracker player (xm64player_t) and its compressed waveforms (wav64_t) are both clients of the same mixer.

Every source is a logical voice that holds a mixer channel only while it is audible. Sound effects take channels from the bottom of the range, native streams (music) from the top, and when a new effect finds nothing free the effect that has been playing the longest gives its channel up - the one least likely to still be the sound the player is listening for.

The engine never decodes a sample here (see NCINE_HAS_NATIVE_AUDIO), so no sample data is held in RAM at all. A buffer is a .wav64 file (loadNativeBuffer()) the mixer streams from the cartridge a few hundred samples ahead of playback, so a sound effect costs only its channel's small ring while it plays. Music arrives as a native stream (openNativeStream()): a .xm64 module played by libdragon's libxm port, which reads its instruments from the cartridge as well, or a .wav64 recording (the tracks that could not be converted to XM are pre-rendered and compressed by the asset packer).

There is no mixer thread* - the mixer is polled from updatePlayers(), once per frame, on the main thread, with audio_init() given enough headroom to outlast a late frame. The mixer itself queues the RSP work asynchronously, so a poll costs the CPU only the bookkeeping.

Base classes

class AudioDeviceBase
Backend-independent part of an audio device.

Constructors, destructors, conversion operators

N64AudioDevice()
~N64AudioDevice() override

Public functions

auto isValid() const -> bool override
Returns true if the device was initialized successfully.
auto name() const -> const char* override
Returns the name of the underlying device.
void setGain(float gain) override
Sets the listener gain (master volume).
void updateListener(const Vector3f& position, const Vector3f& velocity) override
Updates the position and velocity of the listener.
auto nativeFrequency() -> std::int32_t override
Returns the native sample rate of the device.
auto registerPlayer(IAudioPlayer* player) -> std::uint32_t override
Registers a player so it receives state and buffer queue updates, returning its source id.
void updatePlayers() override
Updates the state of every registered player, including the buffer queue of stream players.
auto createBuffer(BufferUsage usage) -> std::uint32_t override
Creates an empty backend buffer, returning its id or 0 on failure.
void deleteBuffer(std::uint32_t bufferId) override
Destroys a buffer previously returned by createBuffer().
auto loadNativeBuffer(std::uint32_t bufferId, StringView path, NativeAudioInfo& info) -> bool override
Makes a buffer play a file the device reads by itself instead of holding its decoded samples.
auto openNativeStream(StringView path, NativeAudioInfo& info) -> std::uint32_t override
Opens a file the device streams by itself, returning its id or 0.
void closeNativeStream(std::uint32_t streamId) override
Closes a stream returned by openNativeStream(), stopping it if it is playing.
void setSourceNativeStream(std::uint32_t sourceId, std::uint32_t streamId) override
Binds a native stream to a source.
void setSourceBuffer(std::uint32_t sourceId, std::uint32_t bufferId) override
Attaches a buffer to a source for non-streamed playback, 0 detaches the current one.
void setSourceGain(std::uint32_t sourceId, float gain) override
Sets the gain of a source.
void setSourcePitch(std::uint32_t sourceId, float pitch) override
Sets the pitch of a source, as a multiplier of its natural playback rate.
void setSourceLooping(std::uint32_t sourceId, bool looping) override
Sets whether a source repeats its attached buffer.
void setSourceRelative(std::uint32_t sourceId, bool relative) override
Sets whether the position of a source is relative to the listener.
void setSourcePosition(std::uint32_t sourceId, const Vector3f& position) override
Sets the position of a source, in physical units.
void setSourceLowPass(std::uint32_t sourceId, float value) override
Sets the low-pass amount of a source, 1.0f disables the filter.
auto sourceSampleOffset(std::uint32_t sourceId) -> std::int32_t override
Returns the playback position of a source in samples.
void setSourceSampleOffset(std::uint32_t sourceId, std::int32_t offset) override
Sets the playback position of a source in samples.
void playSource(std::uint32_t sourceId) override
Starts or resumes a source.
void pauseSource(std::uint32_t sourceId) override
Pauses a source at its current position.
void stopSource(std::uint32_t sourceId) override
Stops a source.
auto isSourcePlaying(std::uint32_t sourceId) -> bool override
Returns true if a source is still producing sound.
void suspendDevice() override
Suspends the audio device.
void resumeDevice() override
Resumes the audio device.
auto ReserveExclusiveChannels(std::int32_t count) -> std::int32_t
Takes count consecutive mixer channels away from the engine, returning the first one or -1.
void ReleaseExclusiveChannels(std::int32_t firstChannel, std::int32_t count)
Gives channels taken by ReserveExclusiveChannels() back to the engine.
void PollMixer()
Runs the mixer if the AI queue has room, without the rest of updatePlayers().

Function documentation

bool nCine::N64AudioDevice::loadNativeBuffer(std::uint32_t bufferId, StringView path, NativeAudioInfo& info) override

Makes a buffer play a file the device reads by itself instead of holding its decoded samples.

The Nintendo 64 streams its sound effects straight out of the cartridge instead of holding them in its 8 MB of RAM: the device takes the file over here, and the buffer then plays like any other. Returns false for a file the device cannot open or play, which then stays silent.

std::uint32_t nCine::N64AudioDevice::openNativeStream(StringView path, NativeAudioInfo& info) override

Opens a file the device streams by itself, returning its id or 0.

The counterpart of loadNativeBuffer() for long content played through AudioStream - music in a format the hardware plays without the engine decoding it. 0 means the device cannot open or play the file.

void nCine::N64AudioDevice::setSourceNativeStream(std::uint32_t sourceId, std::uint32_t streamId) override

Binds a native stream to a source.

The source's own controls then drive the stream: playSource(), pauseSource(), stopSource() (which also rewinds it), setSourceGain(), setSourceLooping() and isSourcePlaying(). The binding lasts until the source is handed out again.

std::int32_t nCine::N64AudioDevice::ReserveExclusiveChannels(std::int32_t count)

Takes count consecutive mixer channels away from the engine, returning the first one or -1.

For code that drives the mixer by itself for a while - the full-motion video player plays its audio track on a channel of its own. Whatever the engine was playing there is stopped. The channels stay out of the engine's hands until ReleaseExclusiveChannels().

void nCine::N64AudioDevice::PollMixer()

Runs the mixer if the AI queue has room, without the rest of updatePlayers().

For a loop that owns the main thread for a while (the video player) and has to keep the audio fed.