diff --git a/src/dusk/CMakeLists.txt b/src/dusk/CMakeLists.txt index 70894fd9..d3f9e31c 100644 --- a/src/dusk/CMakeLists.txt +++ b/src/dusk/CMakeLists.txt @@ -55,6 +55,7 @@ target_sources(${DUSK_BINARY_TARGET_NAME} add_subdirectory(animation) add_subdirectory(assert) add_subdirectory(asset) +add_subdirectory(audio) add_subdirectory(console) add_subdirectory(display) add_subdirectory(log) diff --git a/src/dusk/audio/CMakeLists.txt b/src/dusk/audio/CMakeLists.txt new file mode 100644 index 00000000..18ee2b45 --- /dev/null +++ b/src/dusk/audio/CMakeLists.txt @@ -0,0 +1,13 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +# Sources +target_sources(${DUSK_LIBRARY_TARGET_NAME} + PUBLIC + audio.c + audiostream.c + audiostreampcm.c + audiostreammp3.c +) \ No newline at end of file diff --git a/src/dusk/audio/audio.c b/src/dusk/audio/audio.c new file mode 100644 index 00000000..65ba62c2 --- /dev/null +++ b/src/dusk/audio/audio.c @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "audio.h" +#include "util/memory.h" + +audio_t AUDIO; + +errorret_t audioInit() { + memoryZero(&AUDIO, sizeof(audio_t)); + + errorChain(audioPlatformInit()); + + errorOk(); +} + +audiostream_t * audioAquireStream() { + for(uint8_t i = 0; i < AUDIO_STREAMS_MAX; i++) { + audiostream_t *stream = &AUDIO.streams[i]; + if(stream->type == AUDIO_STREAM_TYPE_NULL) { + return stream; + } + } + + return NULL; +} + +errorret_t audioUpdate() { + errorChain(audioPlatformUpdate()); + + for(uint8_t i = 0; i < AUDIO_STREAMS_MAX; i++) { + audiostream_t *stream = &AUDIO.streams[i]; + errorChain(audioStreamUpdate(stream)); + } + + errorOk(); +} + +errorret_t audioDispose() { + for(uint8_t i = 0; i < AUDIO_STREAMS_MAX; i++) { + audiostream_t *stream = &AUDIO.streams[i]; + errorChain(audioStreamDispose(stream)); + } + + errorChain(audioPlatformDispose()); + + errorOk(); +} \ No newline at end of file diff --git a/src/dusk/audio/audio.h b/src/dusk/audio/audio.h new file mode 100644 index 00000000..237f8c30 --- /dev/null +++ b/src/dusk/audio/audio.h @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "dusk.h" +#include "audio/audiostream.h" +#include "audio/audioplatform.h" + +#ifndef audioPlatformInit + #error "audioPlatformInit is not defined" +#endif + +#ifndef audioPlatformUpdate + #error "audioPlatformUpdate is not defined" +#endif + +#ifndef audioPlatformDispose + #error "audioPlatformDispose is not defined" +#endif + +typedef struct { + audiostream_t streams[AUDIO_STREAMS_MAX]; +} audio_t; + +extern audio_t AUDIO; + +/** + * Initializes the audio subsystem. + * + * @return Error indicating success or failure of the operation. + */ +errorret_t audioInit(); + +/** + * Aquires an available audio stream, can return NULL if there is no available + * stream. + * + * @return Pointer to an available audio stream. + */ +audiostream_t * audioAquireStream(); + +/** + * Updates the audio subsystem, updating every active stream. Should be + * called once per frame. + * + * @return Error indicating success or failure of the operation. + */ +errorret_t audioUpdate(); + +/** + * Disposes the audio subsystem, stopping and disposing every active stream. + * + * @return Error indicating success or failure of the operation. + */ +errorret_t audioDispose(); diff --git a/src/dusk/audio/audiostream.c b/src/dusk/audio/audiostream.c new file mode 100644 index 00000000..d3b281a0 --- /dev/null +++ b/src/dusk/audio/audiostream.c @@ -0,0 +1,112 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "audiostream.h" +#include "assert/assert.h" + +errorret_t audioStreamInit(audiostream_t *stream) { + assertNotNull(stream, "Stream cannot be NULL."); + + stream->state = 0; + stream->data = NULL; + stream->dataSize = 0; + stream->volume = 0xFF; + stream->directionality = AUDIO_STREAM_CENTER; + stream->loopStart = -1; + stream->loopTo = 0; + stream->duration = 0; + stream->user = NULL; + stream->onLoop = NULL; + stream->onEnd = NULL; + + // Type-specific setup (audioStreamPcmInit() / audioStreamMp3Init()) sets + // stream->type and asks the platform implementation to set up its state. + + errorOk(); +} + +void audioStreamPlay(audiostream_t *stream) { + assertNotNull(stream, "Stream cannot be NULL."); + stream->state |= AUDIO_STREAM_STATE_PLAYING; +} + +void audioStreamPause(audiostream_t *stream) { + assertNotNull(stream, "Stream cannot be NULL."); + stream->state &= ~AUDIO_STREAM_STATE_PLAYING; +} + +void audioStreamSetPosition(audiostream_t *stream, const float_t position) { + assertNotNull(stream, "Stream cannot be NULL."); + + // Clamp time. + float_t t = position; + while(t < 0) t += stream->duration; + while(t >= stream->duration) t -= stream->duration; + + // Here I will set the stream time, this will require new data to be fetched + // next update() call. + // To confirm: If this is called late in the frame, will the PCM data be sent + // to the output despite time being adjusted? is this going to cause static? +} + +void audioStreamSetVolume(audiostream_t *stream, const uint8_t volume) { + assertNotNull(stream, "Stream cannot be NULL."); + stream->volume = volume; + // TODO: Do I need to update the device output? PSP may require this +} + +void audioStreamSetDirectionality( + audiostream_t *stream, + const int8_t directionality +) { + assertNotNull(stream, "Stream cannot be NULL."); + stream->directionality = directionality; + // TODO: Need to update internal decoder? +} + +errorret_t audioStreamUpdate(audiostream_t *stream) { + assertNotNull(stream, "Stream cannot be NULL."); + + if(stream->type == AUDIO_STREAM_TYPE_NULL) { + errorOk(); + } + + // TODO: Once streamed (rather than fully in-memory) sources exist, this is + // where new data would be decoded/read in and re-buffered as playback + // consumes it, and looping (onLoop) would be handled. + if( + (stream->state & AUDIO_STREAM_STATE_PLAYING) && + !(stream->state & AUDIO_STREAM_STATE_BUFFERED) + ) { + errorChain(audioStreamPlatformBuffer(stream)); + stream->state |= AUDIO_STREAM_STATE_BUFFERED; + } else if( + (stream->state & AUDIO_STREAM_STATE_PLAYING) && + (stream->state & AUDIO_STREAM_STATE_BUFFERED) && + audioStreamPlatformIsFinished(stream) + ) { + stream->state &= ~(AUDIO_STREAM_STATE_PLAYING | AUDIO_STREAM_STATE_BUFFERED); + + if(stream->onEnd != NULL) { + stream->onEnd(stream); + } + } + + errorOk(); +} + +errorret_t audioStreamDispose(audiostream_t *stream) { + assertNotNull(stream, "Stream cannot be NULL."); + + if(stream->type != AUDIO_STREAM_TYPE_NULL) { + errorChain(audioStreamPlatformDispose(stream)); + } + + stream->type = AUDIO_STREAM_TYPE_NULL; + + errorOk(); +} \ No newline at end of file diff --git a/src/dusk/audio/audiostream.h b/src/dusk/audio/audiostream.h new file mode 100644 index 00000000..29cd865c --- /dev/null +++ b/src/dusk/audio/audiostream.h @@ -0,0 +1,160 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "audio/audiostreampcm.h" +#include "audio/audiostreammp3.h" +#include "audio/audiostreamplatform.h" + +#ifndef audioStreamPlatformInit + #error "audioStreamPlatformInit is not defined" +#endif + +#ifndef audioStreamPlatformDispose + #error "audioStreamPlatformDispose is not defined" +#endif + +#ifndef audioStreamPlatformBuffer + #error "audioStreamPlatformBuffer is not defined" +#endif + +#ifndef audioStreamPlatformIsFinished + #error "audioStreamPlatformIsFinished is not defined" +#endif + +#define AUDIO_STREAMS_MAX 8 +#define AUDIO_STREAM_STATE_PLAYING (1 << 0) +#define AUDIO_STREAM_STATE_LOOPING (1 << 1) +#define AUDIO_STREAM_STATE_BUFFERED (1 << 2) +#define AUDIO_STREAM_CENTER 0 +#define AUDIO_STREAM_LEFT -128 +#define AUDIO_STREAM_RIGHT 127 + +typedef enum { + AUDIO_STREAM_TYPE_NULL, + AUDIO_STREAM_TYPE_PCM, + AUDIO_STREAM_TYPE_MP3, + AUDIO_STREAM_TYPE_COUNT +} audistreamtype_t; + +typedef struct audiostream_s audiostream_t; + +typedef struct audiostream_s { + // Used for aquiring new data. + audistreamtype_t type; + + // What state the stream is in. + uint8_t state; + + // Raw PCM Data, already decoded. Not owned by the stream. + uint8_t *data; + + // Size of the buffer pointed to by data, in bytes. + size_t dataSize; + + // Loudness. Can only be 0 to 0xFF + uint8_t volume; + + // In stereo space, where do we send the audio. + // TODO: Can we use a 3D vector + Pro Logic II? + int8_t directionality; + + // At what point does the stream loop back, -1 means "at end of stream" + float_t loopStart; + + // When a loop occurs, where do we loop to? Defaults to 0 or start of stream. + float_t loopTo; + + // Cached duration of the stream in seconds. + float_t duration; + + // Callbacks + void *user; + void (*onLoop)(audiostream_t *stream); + void (*onEnd)(audiostream_t *stream); + + // Stream type specific data. + union { + audiostreampcm_t pcm; + audiostreammp3_t mp3; + }; + + // Platform-specific playback state (e.g. SDL2 device, PSP channel, ansnd + // voice). Defined by each platform's audiostreamplatform.h. + audiostreamplatform_t platform; +} audiostream_t; + +/** + * Initializes an audio stream in preparation for playback. Does not begin + * playback; call audioStreamPlay() once the stream is configured (see + * audioStreamPcmInit() / audioStreamMp3Init() for the type-specific setup + * this must be followed by). + * + * @param stream The audio stream to initialize. + * @return Error indicating success or failure. + */ +errorret_t audioStreamInit(audiostream_t *stream); + +/** + * Begins or resumes playback of the given audio stream. + * + * @param stream The audio stream to play. + */ +void audioStreamPlay(audiostream_t *stream); + +/** + * Pauses playback of the given audio stream. Playback position is retained, + * so audioStreamPlay() resumes from the same point. + * + * @param stream The audio stream to pause. + */ +void audioStreamPause(audiostream_t *stream); + +/** + * Sets the playback position of the given audio stream, in seconds. + * + * @param stream The audio stream to rewind. + */ +void audioStreamSetPosition(audiostream_t *stream, const float_t position); + +/** + * Sets the playback volume of the given audio stream. + * + * @param stream The audio stream to update. + * @param volume The new volume, from 0 (silent) to 0xFF (loudest). + */ +void audioStreamSetVolume(audiostream_t *stream, const uint8_t volume); + +/** + * Sets the stereo directionality (panning) of the given audio stream. + * + * @param stream The audio stream to update. + * @param directionality The new panning value, from AUDIO_STREAM_LEFT to + * AUDIO_STREAM_RIGHT. + */ +void audioStreamSetDirectionality( + audiostream_t *stream, + const int8_t directionality +); + +/** + * Updates the given audio stream, decoding new data and advancing playback + * as needed. Should be called every frame for every active stream. + * + * @param stream The audio stream to update. + * @return Error indicating success or failure. + */ +errorret_t audioStreamUpdate(audiostream_t *stream); + +/** + * Disposes the audio stream, stopping playback and releasing any resources + * associated with it. + * + * @param stream The audio stream to dispose. + * @return Error indicating success or failure. + */ +errorret_t audioStreamDispose(audiostream_t *stream); \ No newline at end of file diff --git a/src/dusk/audio/audiostreammp3.c b/src/dusk/audio/audiostreammp3.c new file mode 100644 index 00000000..08cc30a4 --- /dev/null +++ b/src/dusk/audio/audiostreammp3.c @@ -0,0 +1,8 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "audiostreammp3.h" \ No newline at end of file diff --git a/src/dusk/audio/audiostreammp3.h b/src/dusk/audio/audiostreammp3.h new file mode 100644 index 00000000..5db7c42f --- /dev/null +++ b/src/dusk/audio/audiostreammp3.h @@ -0,0 +1,13 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "error/error.h" + +typedef struct { + void *empty; +} audiostreammp3_t; \ No newline at end of file diff --git a/src/dusk/audio/audiostreampcm.c b/src/dusk/audio/audiostreampcm.c new file mode 100644 index 00000000..5c91463e --- /dev/null +++ b/src/dusk/audio/audiostreampcm.c @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "audiostreampcm.h" +#include "audiostream.h" +#include "assert/assert.h" + +errorret_t audioStreamPcmInit( + audiostream_t *stream, + uint8_t *data, + const size_t dataSize, + const uint32_t sampleRate, + const uint8_t channels +) { + assertNotNull(stream, "Stream cannot be NULL."); + assertNotNull(data, "Data cannot be NULL."); + + stream->type = AUDIO_STREAM_TYPE_PCM; + stream->data = data; + stream->dataSize = dataSize; + stream->pcm.sampleRate = sampleRate; + stream->pcm.channels = channels; + stream->duration = (float_t) dataSize / + (float_t) (channels * sizeof(int16_t)) / + (float_t) sampleRate; + + errorChain(audioStreamPlatformInit(stream)); + + errorOk(); +} diff --git a/src/dusk/audio/audiostreampcm.h b/src/dusk/audio/audiostreampcm.h new file mode 100644 index 00000000..457fa084 --- /dev/null +++ b/src/dusk/audio/audiostreampcm.h @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "error/error.h" + +typedef struct audiostream_s audiostream_t; + +typedef struct { + // Sample rate of the stream's data, in Hz. + uint32_t sampleRate; + + // Number of interleaved channels in the stream's data. + uint8_t channels; +} audiostreampcm_t; + +/** + * Configures the given audio stream to play raw, already-decoded 16-bit + * signed PCM data. Must be called after audioStreamInit() and before + * audioStreamPlay(). + * + * @param stream The audio stream to configure. + * @param data The raw PCM data to play. Not copied; must remain valid for as + * long as the stream is playing it. + * @param dataSize The size of data, in bytes. + * @param sampleRate The sample rate of data, in Hz. + * @param channels The number of interleaved channels in data. + * @return Error indicating success or failure. + */ +errorret_t audioStreamPcmInit( + audiostream_t *stream, + uint8_t *data, + const size_t dataSize, + const uint32_t sampleRate, + const uint8_t channels +); diff --git a/src/dusk/engine/engine.c b/src/dusk/engine/engine.c index 998be76c..8ec68a16 100644 --- a/src/dusk/engine/engine.c +++ b/src/dusk/engine/engine.c @@ -21,10 +21,25 @@ #endif #include "system/system.h" #include "console/console.h" -#include "save/save.h"\ +#include "save/save.h" +#include "audio/audio.h" +#include "util/math.h" +#include engine_t ENGINE; +#define AUDIO_TEST_TONE_SAMPLE_RATE 48000 +#define AUDIO_TEST_TONE_FREQUENCY 440 +#define AUDIO_TEST_TONE_SAMPLES AUDIO_TEST_TONE_SAMPLE_RATE +#define AUDIO_TEST_TONE_SIZE (AUDIO_TEST_TONE_SAMPLES * sizeof(int16_t)) + +// 1 second, 16-bit signed mono PCM sine wave, used to smoke-test playback. +static uint8_t AUDIO_TEST_TONE[AUDIO_TEST_TONE_SIZE]; + +void engineTestToneOnEnd(audiostream_t *stream) { + consolePrint("Test tone finished playing"); +} + errorret_t engineInit(const int32_t argc, const char_t **argv) { assertInit(); memoryZero(&ENGINE, sizeof(engine_t)); @@ -42,6 +57,7 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) { errorChain(saveInit()); errorChain(localeManagerInit()); errorChain(displayInit()); + errorChain(audioInit()); errorChain(uiInit()); errorChain(rpgInit()); #ifdef DUSK_NETWORK @@ -49,6 +65,23 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) { #endif errorChain(sceneInit()); + int16_t *toneSamples = (int16_t *) AUDIO_TEST_TONE; + for(uint32_t i = 0; i < AUDIO_TEST_TONE_SAMPLES; i++) { + float_t t = (float_t) i / AUDIO_TEST_TONE_SAMPLE_RATE; + toneSamples[i] = (int16_t) ( + sinf(2.0f * MATH_PI * AUDIO_TEST_TONE_FREQUENCY * t) * INT16_MAX + ); + } + + audiostream_t *stream = audioAquireStream(); + errorChain(audioStreamInit(stream)); + errorChain(audioStreamPcmInit( + stream, AUDIO_TEST_TONE, AUDIO_TEST_TONE_SIZE, + AUDIO_TEST_TONE_SAMPLE_RATE, 1 + )); + stream->onEnd = engineTestToneOnEnd; + audioStreamPlay(stream); + consolePrint("Engine initialized"); #ifdef DUSK_ASSERTIONS_FAKED @@ -71,6 +104,7 @@ errorret_t engineUpdate(void) { timeUpdate(); inputUpdate(); consoleUpdate(); + errorChain(audioUpdate()); errorChain(rpgUpdate()); errorChain(sceneUpdate()); errorChain(assetUpdate()); @@ -94,6 +128,7 @@ errorret_t engineDispose(void) { errorChain(rpgDispose()); localeManagerDispose(); errorChain(uiDispose()); + errorChain(audioDispose()); consoleDispose(); errorChain(displayDispose()); errorChain(saveDispose()); diff --git a/src/duskdolphin/audio/audiodolphin.c b/src/duskdolphin/audio/audiodolphin.c new file mode 100644 index 00000000..103a7078 --- /dev/null +++ b/src/duskdolphin/audio/audiodolphin.c @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "audiodolphin.h" + +errorret_t audioDolphinInit() { + errorOk(); +} + +errorret_t audioDolphinUpdate() { + errorOk(); +} + +errorret_t audioDolphinDispose() { + errorOk(); +} diff --git a/src/duskdolphin/audio/audiodolphin.h b/src/duskdolphin/audio/audiodolphin.h new file mode 100644 index 00000000..d8ab17d7 --- /dev/null +++ b/src/duskdolphin/audio/audiodolphin.h @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "error/error.h" + +/** + * Initializes the GameCube/Wii-specific audio subsystem state. + * + * @return Error state if any. + */ +errorret_t audioDolphinInit(); + +/** + * Updates the GameCube/Wii-specific audio subsystem state. Called once per + * frame. + * + * @return Error state if any. + */ +errorret_t audioDolphinUpdate(); + +/** + * Disposes the GameCube/Wii-specific audio subsystem state. + * + * @return Error state if any. + */ +errorret_t audioDolphinDispose(); diff --git a/src/duskdolphin/audio/audioplatform.h b/src/duskdolphin/audio/audioplatform.h new file mode 100644 index 00000000..3cd1a1da --- /dev/null +++ b/src/duskdolphin/audio/audioplatform.h @@ -0,0 +1,13 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "audiodolphin.h" + +#define audioPlatformInit audioDolphinInit +#define audioPlatformUpdate audioDolphinUpdate +#define audioPlatformDispose audioDolphinDispose diff --git a/src/duskdolphin/audio/audiostreamdolphin.c b/src/duskdolphin/audio/audiostreamdolphin.c new file mode 100644 index 00000000..fe7bded2 --- /dev/null +++ b/src/duskdolphin/audio/audiostreamdolphin.c @@ -0,0 +1,8 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "audiostreamdolphin.h" diff --git a/src/duskdolphin/audio/audiostreamdolphin.h b/src/duskdolphin/audio/audiostreamdolphin.h new file mode 100644 index 00000000..eb339395 --- /dev/null +++ b/src/duskdolphin/audio/audiostreamdolphin.h @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "error/error.h" + +typedef struct audiostream_s audiostream_t; + +typedef struct { + int32_t voiceId; +} audiostreamdolphin_t; + +/** + * Initializes the GameCube/Wii-specific playback state of an audio stream + * (e.g. allocating a voice via AESND_AllocateVoice/ansnd equivalent). + * + * @param stream The audio stream to initialize. + * @return Error state if any. + */ +errorret_t audioStreamDolphinInit(audiostream_t *stream); + +/** + * Disposes the GameCube/Wii-specific playback state of an audio stream, + * releasing its voice. + * + * @param stream The audio stream to dispose. + * @return Error state if any. + */ +errorret_t audioStreamDolphinDispose(audiostream_t *stream); + +/** + * Sends the stream's currently staged PCM data (stream->data) to its + * allocated voice. + * + * @param stream The audio stream to output. + * @return Error state if any. + */ +errorret_t audioStreamDolphinBuffer(audiostream_t *stream); + +/** + * Checks whether the stream's voice has finished playing its currently + * buffered data. + * + * @param stream The audio stream to check. + * @return true if playback has finished, false otherwise. + */ +bool_t audioStreamDolphinIsFinished(audiostream_t *stream); diff --git a/src/duskdolphin/audio/audiostreamplatform.h b/src/duskdolphin/audio/audiostreamplatform.h new file mode 100644 index 00000000..0f86b66a --- /dev/null +++ b/src/duskdolphin/audio/audiostreamplatform.h @@ -0,0 +1,16 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "audiostreamdolphin.h" + +typedef audiostreamdolphin_t audiostreamplatform_t; + +#define audioStreamPlatformInit audioStreamDolphinInit +#define audioStreamPlatformDispose audioStreamDolphinDispose +#define audioStreamPlatformBuffer audioStreamDolphinBuffer +#define audioStreamPlatformIsFinished audioStreamDolphinIsFinished diff --git a/src/dusklinux/CMakeLists.txt b/src/dusklinux/CMakeLists.txt index 2e468c9d..55a11237 100644 --- a/src/dusklinux/CMakeLists.txt +++ b/src/dusklinux/CMakeLists.txt @@ -11,6 +11,7 @@ target_include_directories(${DUSK_LIBRARY_TARGET_NAME} # Subdirs add_subdirectory(asset) +add_subdirectory(audio) add_subdirectory(log) add_subdirectory(input) if(DUSK_NETWORK) diff --git a/src/dusklinux/audio/CMakeLists.txt b/src/dusklinux/audio/CMakeLists.txt new file mode 100644 index 00000000..23f030ad --- /dev/null +++ b/src/dusklinux/audio/CMakeLists.txt @@ -0,0 +1,11 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +# Sources +target_sources(${DUSK_LIBRARY_TARGET_NAME} + PUBLIC + audiolinux.c + audiostreamlinux.c +) diff --git a/src/dusklinux/audio/audiolinux.c b/src/dusklinux/audio/audiolinux.c new file mode 100644 index 00000000..078a9524 --- /dev/null +++ b/src/dusklinux/audio/audiolinux.c @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "audiolinux.h" +#include + +errorret_t audioLinuxInit() { + if(SDL_InitSubSystem(SDL_INIT_AUDIO) != 0) { + errorThrow("Failed to initialize SDL2 audio subsystem: %s", SDL_GetError()); + } + + errorOk(); +} + +errorret_t audioLinuxUpdate() { + errorOk(); +} + +errorret_t audioLinuxDispose() { + SDL_QuitSubSystem(SDL_INIT_AUDIO); + + errorOk(); +} diff --git a/src/dusklinux/audio/audiolinux.h b/src/dusklinux/audio/audiolinux.h new file mode 100644 index 00000000..a9bb4ce2 --- /dev/null +++ b/src/dusklinux/audio/audiolinux.h @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "error/error.h" + +/** + * Initializes the Linux-specific audio subsystem state. + * + * @return Error state if any. + */ +errorret_t audioLinuxInit(); + +/** + * Updates the Linux-specific audio subsystem state. Called once per frame. + * + * @return Error state if any. + */ +errorret_t audioLinuxUpdate(); + +/** + * Disposes the Linux-specific audio subsystem state. + * + * @return Error state if any. + */ +errorret_t audioLinuxDispose(); diff --git a/src/dusklinux/audio/audioplatform.h b/src/dusklinux/audio/audioplatform.h new file mode 100644 index 00000000..51888682 --- /dev/null +++ b/src/dusklinux/audio/audioplatform.h @@ -0,0 +1,13 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "audiolinux.h" + +#define audioPlatformInit audioLinuxInit +#define audioPlatformUpdate audioLinuxUpdate +#define audioPlatformDispose audioLinuxDispose diff --git a/src/dusklinux/audio/audiostreamlinux.c b/src/dusklinux/audio/audiostreamlinux.c new file mode 100644 index 00000000..df0881aa --- /dev/null +++ b/src/dusklinux/audio/audiostreamlinux.c @@ -0,0 +1,67 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "audiostreamlinux.h" +#include "audio/audiostream.h" +#include "assert/assert.h" +#include "util/memory.h" + +errorret_t audioStreamLinuxInit(audiostream_t *stream) { + assertNotNull(stream, "Stream cannot be NULL."); + + SDL_AudioSpec desired; + memoryZero(&desired, sizeof(SDL_AudioSpec)); + desired.freq = (int) stream->pcm.sampleRate; + desired.format = AUDIO_S16SYS; + desired.channels = stream->pcm.channels; + desired.samples = 4096; + + stream->platform.device = SDL_OpenAudioDevice(NULL, 0, &desired, NULL, 0); + if(stream->platform.device == 0) { + errorThrow("Failed to open SDL2 audio device: %s", SDL_GetError()); + } + + errorOk(); +} + +errorret_t audioStreamLinuxDispose(audiostream_t *stream) { + assertNotNull(stream, "Stream cannot be NULL."); + + SDL_CloseAudioDevice(stream->platform.device); + + errorOk(); +} + +errorret_t audioStreamLinuxBuffer(audiostream_t *stream) { + assertNotNull(stream, "Stream cannot be NULL."); + + uint8_t *mixed = memoryAllocate(stream->dataSize); + memoryZero(mixed, stream->dataSize); + SDL_MixAudioFormat( + mixed, stream->data, AUDIO_S16SYS, (Uint32) stream->dataSize, + (stream->volume * SDL_MIX_MAXVOLUME) / 0xFF + ); + + int queued = SDL_QueueAudio( + stream->platform.device, mixed, (Uint32) stream->dataSize + ); + memoryFree(mixed); + + if(queued != 0) { + errorThrow("Failed to queue SDL2 audio data: %s", SDL_GetError()); + } + + SDL_PauseAudioDevice(stream->platform.device, 0); + + errorOk(); +} + +bool_t audioStreamLinuxIsFinished(audiostream_t *stream) { + assertNotNull(stream, "Stream cannot be NULL."); + + return SDL_GetQueuedAudioSize(stream->platform.device) == 0; +} diff --git a/src/dusklinux/audio/audiostreamlinux.h b/src/dusklinux/audio/audiostreamlinux.h new file mode 100644 index 00000000..d7bbf423 --- /dev/null +++ b/src/dusklinux/audio/audiostreamlinux.h @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "error/error.h" +#include + +typedef struct audiostream_s audiostream_t; + +typedef struct { + SDL_AudioDeviceID device; +} audiostreamlinux_t; + +/** + * Initializes the Linux-specific playback state of an audio stream by + * opening an SDL2 audio device matching the stream's PCM format. + * + * @param stream The audio stream to initialize. + * @return Error state if any. + */ +errorret_t audioStreamLinuxInit(audiostream_t *stream); + +/** + * Disposes the Linux-specific playback state of an audio stream, closing + * its SDL2 audio device. + * + * @param stream The audio stream to dispose. + * @return Error state if any. + */ +errorret_t audioStreamLinuxDispose(audiostream_t *stream); + +/** + * Sends the stream's currently staged PCM data (stream->data) to its SDL2 + * audio device and starts/resumes playback. + * + * @param stream The audio stream to output. + * @return Error state if any. + */ +errorret_t audioStreamLinuxBuffer(audiostream_t *stream); + +/** + * Checks whether the stream's SDL2 audio device has finished playing all + * queued data. + * + * @param stream The audio stream to check. + * @return true if playback has finished, false otherwise. + */ +bool_t audioStreamLinuxIsFinished(audiostream_t *stream); diff --git a/src/dusklinux/audio/audiostreamplatform.h b/src/dusklinux/audio/audiostreamplatform.h new file mode 100644 index 00000000..3251c189 --- /dev/null +++ b/src/dusklinux/audio/audiostreamplatform.h @@ -0,0 +1,16 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "audiostreamlinux.h" + +typedef audiostreamlinux_t audiostreamplatform_t; + +#define audioStreamPlatformInit audioStreamLinuxInit +#define audioStreamPlatformDispose audioStreamLinuxDispose +#define audioStreamPlatformBuffer audioStreamLinuxBuffer +#define audioStreamPlatformIsFinished audioStreamLinuxIsFinished diff --git a/src/duskpsp/audio/audioplatform.h b/src/duskpsp/audio/audioplatform.h new file mode 100644 index 00000000..46595536 --- /dev/null +++ b/src/duskpsp/audio/audioplatform.h @@ -0,0 +1,13 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "audiopsp.h" + +#define audioPlatformInit audioPSPInit +#define audioPlatformUpdate audioPSPUpdate +#define audioPlatformDispose audioPSPDispose diff --git a/src/duskpsp/audio/audiopsp.c b/src/duskpsp/audio/audiopsp.c new file mode 100644 index 00000000..f96be52d --- /dev/null +++ b/src/duskpsp/audio/audiopsp.c @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "audiopsp.h" + +errorret_t audioPSPInit() { + errorOk(); +} + +errorret_t audioPSPUpdate() { + errorOk(); +} + +errorret_t audioPSPDispose() { + errorOk(); +} diff --git a/src/duskpsp/audio/audiopsp.h b/src/duskpsp/audio/audiopsp.h new file mode 100644 index 00000000..b5657274 --- /dev/null +++ b/src/duskpsp/audio/audiopsp.h @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "error/error.h" + +/** + * Initializes the PSP-specific audio subsystem state. + * + * @return Error state if any. + */ +errorret_t audioPSPInit(); + +/** + * Updates the PSP-specific audio subsystem state. Called once per frame. + * + * @return Error state if any. + */ +errorret_t audioPSPUpdate(); + +/** + * Disposes the PSP-specific audio subsystem state. + * + * @return Error state if any. + */ +errorret_t audioPSPDispose(); diff --git a/src/duskpsp/audio/audiostreamplatform.h b/src/duskpsp/audio/audiostreamplatform.h new file mode 100644 index 00000000..0ea4c93c --- /dev/null +++ b/src/duskpsp/audio/audiostreamplatform.h @@ -0,0 +1,16 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "audiostreampsp.h" + +typedef audiostreampsp_t audiostreamplatform_t; + +#define audioStreamPlatformInit audioStreamPSPInit +#define audioStreamPlatformDispose audioStreamPSPDispose +#define audioStreamPlatformBuffer audioStreamPSPBuffer +#define audioStreamPlatformIsFinished audioStreamPSPIsFinished diff --git a/src/duskpsp/audio/audiostreampsp.c b/src/duskpsp/audio/audiostreampsp.c new file mode 100644 index 00000000..d3279c45 --- /dev/null +++ b/src/duskpsp/audio/audiostreampsp.c @@ -0,0 +1,8 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "audiostreampsp.h" diff --git a/src/duskpsp/audio/audiostreampsp.h b/src/duskpsp/audio/audiostreampsp.h new file mode 100644 index 00000000..cf22b5bf --- /dev/null +++ b/src/duskpsp/audio/audiostreampsp.h @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "error/error.h" + +typedef struct audiostream_s audiostream_t; + +typedef struct { + int channel; +} audiostreampsp_t; + +/** + * Initializes the PSP-specific playback state of an audio stream (e.g. + * reserving a hardware output channel via sceAudioChReserve). + * + * @param stream The audio stream to initialize. + * @return Error state if any. + */ +errorret_t audioStreamPSPInit(audiostream_t *stream); + +/** + * Disposes the PSP-specific playback state of an audio stream, releasing + * its hardware output channel. + * + * @param stream The audio stream to dispose. + * @return Error state if any. + */ +errorret_t audioStreamPSPDispose(audiostream_t *stream); + +/** + * Sends the stream's currently staged PCM data (stream->data) to its + * reserved hardware output channel. + * + * @param stream The audio stream to output. + * @return Error state if any. + */ +errorret_t audioStreamPSPBuffer(audiostream_t *stream); + +/** + * Checks whether the stream's reserved hardware channel has finished + * playing its currently buffered data. + * + * @param stream The audio stream to check. + * @return true if playback has finished, false otherwise. + */ +bool_t audioStreamPSPIsFinished(audiostream_t *stream);