Skip to content

Instantly share code, notes, and snippets.

@NAEL2XD
Last active May 11, 2025 22:48
Show Gist options
  • Select an option

  • Save NAEL2XD/f62bc8c8415f51b62a01f1519b2c8ee9 to your computer and use it in GitHub Desktop.

Select an option

Save NAEL2XD/f62bc8c8415f51b62a01f1519b2c8ee9 to your computer and use it in GitHub Desktop.
3DS: Sounds Utils
#include "sound.h"
#include <tremor/ivorbisfile.h>
#include <string.h>
#include <stdio.h>
#define MAX_CHANNELS 24 // Maximum NDSP channels
#define MAX_SOUNDS 24 // Maximum number of pre-cached sounds
#define THREAD_STACK_SZ (32 * 1024)
#define THREAD_AFFINITY -1
typedef struct {
OggVorbis_File vorbisFile;
ndspWaveBuf waveBufs[3];
int16_t *audioBuffer;
LightEvent event;
volatile bool quit;
Thread threadId;
int channel;
bool playing;
bool loaded;
} AudioStream;
static AudioStream streams[MAX_SOUNDS];
static bool soundSystemInitialized = false;
// Internal function to fill a buffer with audio data.
static bool fillBuffer(AudioStream *stream, ndspWaveBuf *waveBuf) {
int totalBytes = 0;
while (totalBytes < waveBuf->nsamples * sizeof(int16_t)) {
int16_t *buffer = waveBuf->data_pcm16 + (totalBytes / sizeof(int16_t));
size_t bufferSize = waveBuf->nsamples * sizeof(int16_t) - totalBytes;
int bytesRead = ov_read(&stream->vorbisFile, (char *)buffer, bufferSize, NULL);
if (bytesRead <= 0) {
if (bytesRead == 0) break;
return false;
}
totalBytes += bytesRead;
}
if (totalBytes == 0) return false;
waveBuf->nsamples = totalBytes / sizeof(int16_t);
ndspChnWaveBufAdd(stream->channel, waveBuf);
DSP_FlushDataCache(waveBuf->data_pcm16, totalBytes);
return true;
}
// Internal function to pre-fill all buffers for a stream.
static void prefillBuffers(AudioStream *stream) {
for (size_t i = 0; i < 3; i++) {
if (!fillBuffer(stream, &stream->waveBufs[i])) {
stream->playing = false;
break;
}
}
}
// Internal thread function for audio playback.
static void audioThread(void *arg) {
AudioStream *stream = (AudioStream *)arg;
while (!stream->quit) {
if (!stream->playing) {
LightEvent_Wait(&stream->event);
continue;
}
for (size_t i = 0; i < 3; i++) {
if (stream->waveBufs[i].status == NDSP_WBUF_DONE) {
if (!fillBuffer(stream, &stream->waveBufs[i])) {
stream->playing = false;
break;
}
}
}
}
}
// Initializes the sound system.
bool SOUND_init() {
if (soundSystemInitialized) return true;
romfsInit();
ndspInit();
memset(streams, 0, sizeof(streams));
soundSystemInitialized = true;
return true;
}
// Preloads a sound file and assigns it an ID.
int SOUND_precacheToID(const char *path) {
for (int i = 0; i < MAX_SOUNDS; i++) {
if (streams[i].loaded) continue;
FILE *fh = fopen(path, "rb");
if (!fh) {
printf("Failed to open %s\n", path);
return -1;
}
if (ov_open(fh, &streams[i].vorbisFile, NULL, 0)) {
fclose(fh);
printf("Failed to load Ogg Vorbis file: %s\n", path);
return -1;
}
vorbis_info *vi = ov_info(&streams[i].vorbisFile, -1);
ndspChnReset(i);
ndspChnSetInterp(i, NDSP_INTERP_POLYPHASE);
ndspChnSetRate(i, vi->rate);
ndspChnSetFormat(i, vi->channels == 1 ? NDSP_FORMAT_MONO_PCM16 : NDSP_FORMAT_STEREO_PCM16);
size_t samplesPerBuf = vi->rate * 60 / 1000; // Smaller buffer for reduced delay
size_t waveBufSize = samplesPerBuf * vi->channels * sizeof(int16_t);
size_t bufferSize = waveBufSize * 3;
streams[i].audioBuffer = (int16_t *)linearAlloc(bufferSize);
if (!streams[i].audioBuffer) {
ov_clear(&streams[i].vorbisFile);
printf("Failed to allocate audio buffer\n");
return -1;
}
memset(streams[i].waveBufs, 0, sizeof(streams[i].waveBufs));
int16_t *buffer = streams[i].audioBuffer;
for (size_t j = 0; j < 3; j++) {
streams[i].waveBufs[j].data_vaddr = buffer;
streams[i].waveBufs[j].nsamples = samplesPerBuf;
streams[i].waveBufs[j].status = NDSP_WBUF_DONE;
buffer += samplesPerBuf * vi->channels;
}
streams[i].channel = i;
streams[i].playing = false;
streams[i].loaded = true;
LightEvent_Init(&streams[i].event, RESET_ONESHOT);
streams[i].quit = false;
streams[i].threadId = threadCreate(audioThread, &streams[i], THREAD_STACK_SZ, 0x30, THREAD_AFFINITY, false);
// Pre-fill buffers to minimize delay
prefillBuffers(&streams[i]);
return i;
}
printf("No available channels for sound\n");
return -1;
}
// Plays a sound by ID.
void SOUND_playByID(int id, bool restart) {
if (id < 0 || id >= MAX_SOUNDS || !streams[id].loaded) {
printf("Invalid sound ID: %d\n", id);
return;
}
if (restart || !streams[id].playing) {
ov_time_seek(&streams[id].vorbisFile, 0); // Restart the audio
streams[id].playing = true;
prefillBuffers(&streams[id]); // Ensure buffers are filled immediately
LightEvent_Signal(&streams[id].event);
}
}
// Stops a sound by ID.
void SOUND_stopByID(int id) {
if (id < 0 || id >= MAX_SOUNDS || !streams[id].loaded) {
printf("Invalid sound ID: %d\n", id);
return;
}
streams[id].playing = false;
}
// Cleans up and shuts down the sound system.
void SOUND_exit() {
if (!soundSystemInitialized) return;
for (int i = 0; i < MAX_SOUNDS; i++) {
if (streams[i].loaded) {
streams[i].quit = true;
LightEvent_Signal(&streams[i].event);
threadJoin(streams[i].threadId, U64_MAX);
threadFree(streams[i].threadId);
ndspChnReset(i);
linearFree(streams[i].audioBuffer);
ov_clear(&streams[i].vorbisFile);
}
}
ndspExit();
romfsExit();
soundSystemInitialized = false;
}
#ifndef SOUND_H
#define SOUND_H
#include <3ds.h>
/**
* @brief Initializes the sound system and prepares NDSP for audio playback.
*
* This function sets up the necessary resources for the sound system, including
* initializing NDSP and preparing audio channels for playback.
*
* @return true if the sound system was successfully initialized, false otherwise.
*/
bool SOUND_init();
/**
* @brief Preloads an Ogg Vorbis file into memory and assigns it a unique sound ID.
*
* This function loads the specified Ogg Vorbis file into memory and prepares it
* for playback. Each preloaded sound is assigned a unique ID that can be used to
* play or stop the sound.
*
* @param path The file path to the Ogg Vorbis file to preload.
* @return The unique ID of the preloaded sound, or -1 if the file could not be loaded.
*/
int SOUND_precacheToID(const char *path);
/**
* @brief Plays a preloaded sound by its unique ID.
*
* This function starts playback of the sound associated with the given ID. If the
* `restart` parameter is true, the sound will restart from the beginning.
*
* @param id The ID of the sound to play.
* @param restart If true, the sound will restart from the beginning.
*/
void SOUND_playByID(int id, bool restart);
/**
* @brief Stops playback of a sound by its unique ID.
*
* This function stops the playback of the sound associated with the given ID.
*
* @param id The ID of the sound to stop.
*/
void SOUND_stopByID(int id);
/**
* @brief Shuts down the sound system and cleans up all resources.
*
* This function stops all audio playback, frees all allocated memory, and shuts down NDSP.
* It should be called when the application is exiting or when sound is no longer needed.
*/
void SOUND_exit();
#endif // SOUND_H
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment