From a9f67b2dd6a3ca5ea26e8842cf53839f3db31840 Mon Sep 17 00:00:00 2001 From: Moros Smith Date: Sat, 26 Sep 2026 20:48:10 -0400 Subject: [PATCH 01/21] [ext][miniaudio] fix implementation macro --- extensions/miniaudio/olcPGEX3_Miniaudio.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/extensions/miniaudio/olcPGEX3_Miniaudio.h b/extensions/miniaudio/olcPGEX3_Miniaudio.h index 7d1145c..d4edf1f 100644 --- a/extensions/miniaudio/olcPGEX3_Miniaudio.h +++ b/extensions/miniaudio/olcPGEX3_Miniaudio.h @@ -59,9 +59,10 @@ #ifdef OLC_PGEX3_MINIAUDIO #define MINIAUDIO_IMPLEMENTATION -#include "miniaudio.h" #endif +#include "miniaudio.h" + #include #include #include From b37c81854de789e41e1f63d7c9c2099f36626616 Mon Sep 17 00:00:00 2001 From: Moros Smith Date: Sat, 26 Sep 2026 20:48:59 -0400 Subject: [PATCH 02/21] [ext][miniaudio] fix copy/move semantics, delete copy, allow move. --- extensions/miniaudio/olcPGEX3_Miniaudio.h | 49 +++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/extensions/miniaudio/olcPGEX3_Miniaudio.h b/extensions/miniaudio/olcPGEX3_Miniaudio.h index d4edf1f..9807967 100644 --- a/extensions/miniaudio/olcPGEX3_Miniaudio.h +++ b/extensions/miniaudio/olcPGEX3_Miniaudio.h @@ -82,6 +82,10 @@ namespace olc::ext::Miniaudio public: Sound() = default; ~Sound(); + Sound(const Sound&) = delete; + Sound& operator=(const Sound&) = delete; + Sound(Sound&& other) noexcept; + Sound& operator=(Sound&& other) noexcept; public: // Loaders // Create an image resource based on an image file asset on disk @@ -334,7 +338,52 @@ namespace olc::ext::Miniaudio #pragma region Sound uint32_t Sound::m_id_tracker = 0; + Sound::Sound(Sound&& other) noexcept + : m_pgex(other.m_pgex), + m_buffer(std::move(other.m_buffer)), + m_base_sound(other.m_base_sound), + m_voices(std::move(other.m_voices)), + m_id(other.m_id), + m_virtual_path(std::move(other.m_virtual_path)), + m_is_loaded(other.m_is_loaded), + m_is_paused(other.m_is_paused), + m_num_voices(other.m_num_voices), + m_current_voice(other.m_current_voice), + m_length_in_pcm_frames(other.m_length_in_pcm_frames), + m_length_in_seconds(other.m_length_in_seconds) + { + other.m_pgex = nullptr; + other.m_base_sound = {0}; + other.m_is_loaded = false; + other.m_id = 0; + }; + Sound& Sound::operator=(Sound&& other) noexcept + { + if(this != &other) + { + DestroySound(); + m_pgex = other.m_pgex; + m_buffer = std::move(other.m_buffer); + m_base_sound = other.m_base_sound; + m_voices = std::move(other.m_voices); + m_id = other.m_id; + m_virtual_path = std::move(other.m_virtual_path); + m_is_loaded = other.m_is_loaded; + m_is_paused = other.m_is_paused; + m_num_voices = other.m_num_voices; + m_current_voice = other.m_current_voice; + m_length_in_pcm_frames = other.m_length_in_pcm_frames; + m_length_in_seconds = other.m_length_in_seconds; + + other.m_pgex = nullptr; + other.m_base_sound = {0}; + other.m_is_loaded = false; + other.m_id = 0; + } + return *this; + } + Sound::~Sound() { DestroySound(); From 3020896a669b962dcb386842332102e24fb759fd Mon Sep 17 00:00:00 2001 From: Moros Smith Date: Sat, 26 Sep 2026 20:50:16 -0400 Subject: [PATCH 03/21] [ext][miniaudio] allow per-voice tweaking of volume, pan, and pitch at Play() --- extensions/miniaudio/olcPGEX3_Miniaudio.h | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/extensions/miniaudio/olcPGEX3_Miniaudio.h b/extensions/miniaudio/olcPGEX3_Miniaudio.h index 9807967..b9ad5ba 100644 --- a/extensions/miniaudio/olcPGEX3_Miniaudio.h +++ b/extensions/miniaudio/olcPGEX3_Miniaudio.h @@ -104,7 +104,7 @@ namespace olc::ext::Miniaudio public: // playback routines // plays a sound, can be set to loop - void Play(const bool looping = false); + void Play(const bool looping = false, const float volume = 1.0f, const float pan = 0.0f, const float pitch = 1.0f); // stops a sound, rewinds to beginning void Stop(); // pauses a sound, does not change position @@ -529,11 +529,14 @@ namespace olc::ext::Miniaudio } // plays a sound, can be set to loop - void Sound::Play(const bool looping) + void Sound::Play(const bool looping, const float volume, const float pan, const float pitch) { if(!m_is_paused) m_current_voice = (m_current_voice + 1) % m_num_voices; + ma_sound_set_volume(&m_voices[m_current_voice], std::clamp(volume, 0.0f, 1.0f)); + ma_sound_set_pan(&m_voices[m_current_voice], std::clamp(pan, -1.0f, 1.0f)); + ma_sound_set_pitch(&m_voices[m_current_voice], std::max({0.0f, pitch})); ma_sound_set_looping(&m_voices[m_current_voice], looping); ma_sound_seek_to_pcm_frame(&m_voices[m_current_voice], 0); ma_sound_start(&m_voices[m_current_voice]); From 4d398e9667ad60bad2c7e2da777244ca5c5485af Mon Sep 17 00:00:00 2001 From: Moros Smith Date: Sat, 26 Sep 2026 20:54:58 -0400 Subject: [PATCH 04/21] [ext][miniaudio] allow engine to continue processing when not focused, regardless of background playback. --- extensions/miniaudio/olcPGEX3_Miniaudio.h | 26 ++++++++++++++++------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/extensions/miniaudio/olcPGEX3_Miniaudio.h b/extensions/miniaudio/olcPGEX3_Miniaudio.h index b9ad5ba..8949765 100644 --- a/extensions/miniaudio/olcPGEX3_Miniaudio.h +++ b/extensions/miniaudio/olcPGEX3_Miniaudio.h @@ -835,9 +835,6 @@ namespace olc::ext::Miniaudio if(ma == nullptr) throw std::runtime_error{"unable to access miniaudio pgex instance from data_callback"}; - if(!ma->m_cfg.BackgroundPlay && !ma->m_pge->IsFocused()) - return; - // with great power comes... if(ma->m_data_callback) { @@ -845,8 +842,10 @@ namespace olc::ext::Miniaudio return; } + const bool audioShouldPlay = !(!ma->m_cfg.BackgroundPlay && !ma->m_pge->IsFocused()); + std::span engineBuffer((float*)pOutput, frameCount * ma->GetDeviceChannels()); - ma_engine_read_pcm_frames(&ma->m_engine, engineBuffer.data(), frameCount, NULL); + ma_engine_read_pcm_frames(&ma->m_engine, engineBuffer.data(), frameCount, NULL); // resize, if required. frameCount is not guaranteed not to change. if(ma->m_waveform_buffer.size() != (frameCount * ma->GetDeviceChannels())) @@ -860,9 +859,12 @@ namespace olc::ext::Miniaudio if (!waveform->IsPlaying()) continue; - ma_waveform_read_pcm_frames(&waveform->m_waveform, ma->m_waveform_buffer.data(), frameCount, NULL); + // if audio shouldn't play, move on here. + if(!audioShouldPlay) + continue; + for(int frame = 0; frame < frameCount; ++frame) { // Ramp gain toward target one step per frame @@ -886,9 +888,11 @@ namespace olc::ext::Miniaudio { float left, right; ma->m_synth_callback(left, right, 1.0f / ma->GetDeviceSampleRate()); - - engineBuffer[(i * ma->GetDeviceChannels())] += left; - engineBuffer[(i * ma->GetDeviceChannels()) + 1] += right; + if(audioShouldPlay) + { + engineBuffer[(i * ma->GetDeviceChannels())] += left; + engineBuffer[(i * ma->GetDeviceChannels()) + 1] += right; + } } } @@ -897,6 +901,12 @@ namespace olc::ext::Miniaudio for(int i = 0; i < engineBuffer.size(); i++) { + if(!audioShouldPlay) + { + engineBuffer[i] = 0.0f; + continue; + } + float peak = fabsf(engineBuffer[i]); if (peak > 1.0f) From 3a2a4cb53d9a07393407ec917ae2bf5338fb6f28 Mon Sep 17 00:00:00 2001 From: Moros Smith Date: Sun, 27 Sep 2026 12:09:02 -0400 Subject: [PATCH 05/21] [ext][miniaudio] fetch only the header for miniaudio --- extensions/miniaudio/CMakeLists.txt | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/extensions/miniaudio/CMakeLists.txt b/extensions/miniaudio/CMakeLists.txt index f678d58..8846aeb 100644 --- a/extensions/miniaudio/CMakeLists.txt +++ b/extensions/miniaudio/CMakeLists.txt @@ -1,14 +1,15 @@ # Fetch 3rd Party miniaudio library +# Fetch miniaudio.h FetchContent_Declare( miniaudio - GIT_REPOSITORY https://github.com/mackron/miniaudio.git - GIT_TAG 0.11.25 + URL "https://raw.githubusercontent.com/mackron/miniaudio/0.11.25/miniaudio.h" + DOWNLOAD_NO_EXTRACT ON + SOURCE_DIR "${CMAKE_BINARY_DIR}/_deps/miniaudio-src" ) FetchContent_MakeAvailable(miniaudio) add_example(ext_demo_Miniaudio demo_Miniaudio.cpp) -target_include_directories(ext_demo_Miniaudio PRIVATE ${miniaudio_SOURCE_DIR}) - +target_include_directories(ext_demo_Miniaudio PRIVATE "${CMAKE_BINARY_DIR}/_deps/miniaudio-src") handle_assets(ext_demo_Miniaudio ${CMAKE_CURRENT_SOURCE_DIR}/assets) From e4816efa3b4a26f994674637a9e4bfd80fd5566e Mon Sep 17 00:00:00 2001 From: Moros Smith Date: Sun, 27 Sep 2026 12:09:29 -0400 Subject: [PATCH 06/21] [ext][miniaudio] add note about additional arguments to Sound::Play --- extensions/miniaudio/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/extensions/miniaudio/README.md b/extensions/miniaudio/README.md index c1e1526..13baacc 100644 --- a/extensions/miniaudio/README.md +++ b/extensions/miniaudio/README.md @@ -133,6 +133,8 @@ mySound.Play(); mySound.Play(true); // Loops continuously ``` +> **Note For Advanced Users:** the `olc::ext::Miniaudio::Sound` has a pool of voices, the `Play` function has several optional arguments to allow you to set the **volume**, **pan**, and **pitch** of an individual voice at the moment playback begins. + #### Stop ```cpp // Stop and rewind to beginning From 541d422df22c15e564938ec323173f627cfc0ae5 Mon Sep 17 00:00:00 2001 From: Moros Smith Date: Sun, 27 Sep 2026 12:10:09 -0400 Subject: [PATCH 07/21] [ext][miniaudio] set Sound::Play default values out of range to detect if they've been explicitly set --- extensions/miniaudio/olcPGEX3_Miniaudio.h | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/extensions/miniaudio/olcPGEX3_Miniaudio.h b/extensions/miniaudio/olcPGEX3_Miniaudio.h index 8949765..2a488eb 100644 --- a/extensions/miniaudio/olcPGEX3_Miniaudio.h +++ b/extensions/miniaudio/olcPGEX3_Miniaudio.h @@ -103,8 +103,8 @@ namespace olc::ext::Miniaudio public: // playback routines - // plays a sound, can be set to loop - void Play(const bool looping = false, const float volume = 1.0f, const float pan = 0.0f, const float pitch = 1.0f); + // plays a sound, can set looping, volume, pan, or pitch (note: default values are out of range deliberately) + void Play(const bool looping = false, const float volume = 2.0f, const float pan = 2.0f, const float pitch = 2.0f); // stops a sound, rewinds to beginning void Stop(); // pauses a sound, does not change position @@ -534,9 +534,15 @@ namespace olc::ext::Miniaudio if(!m_is_paused) m_current_voice = (m_current_voice + 1) % m_num_voices; - ma_sound_set_volume(&m_voices[m_current_voice], std::clamp(volume, 0.0f, 1.0f)); - ma_sound_set_pan(&m_voices[m_current_voice], std::clamp(pan, -1.0f, 1.0f)); - ma_sound_set_pitch(&m_voices[m_current_voice], std::max({0.0f, pitch})); + if(volume != 2.0f) + ma_sound_set_volume(&m_voices[m_current_voice], std::clamp(volume, 0.0f, 1.0f)); + + if(pan != 2.0f) + ma_sound_set_pan(&m_voices[m_current_voice], std::clamp(pan, -1.0f, 1.0f)); + + if(pitch != 2.0f) + ma_sound_set_pitch(&m_voices[m_current_voice], std::max({0.0f, pitch})); + ma_sound_set_looping(&m_voices[m_current_voice], looping); ma_sound_seek_to_pcm_frame(&m_voices[m_current_voice], 0); ma_sound_start(&m_voices[m_current_voice]); From 6b11f224ef131f636f05b99c1c8493ff660ec336 Mon Sep 17 00:00:00 2001 From: Moros Smith Date: Thu, 1 Oct 2026 06:17:36 -0400 Subject: [PATCH 08/21] [ext][miniaudio] paradigm shift --- extensions/miniaudio/demo_Miniaudio.cpp | 30 +- extensions/miniaudio/olcPGEX3_Miniaudio.h | 1028 +++++++++------------ 2 files changed, 465 insertions(+), 593 deletions(-) diff --git a/extensions/miniaudio/demo_Miniaudio.cpp b/extensions/miniaudio/demo_Miniaudio.cpp index fdce653..bf28506 100644 --- a/extensions/miniaudio/demo_Miniaudio.cpp +++ b/extensions/miniaudio/demo_Miniaudio.cpp @@ -44,7 +44,7 @@ class Example_Miniaudio : public olc::PixelGameEngine * Here you get a pointer to a next active voice, or the * currently playing voice. */ - ma_sound_set_position(song1.GetMASound(), 0.0f, 0.0f, 0.0f); + ma_sound_set_position(audio.GetMASound(song1), 0.0f, 0.0f, 0.0f); // load `assets/SampleA.wav` into `sample` audio.CreateSoundFromFile(sample, "assets/SampleA.wav"); @@ -73,32 +73,32 @@ class Example_Miniaudio : public olc::PixelGameEngine // ensure all waveforms are stopped // before we check for held keys - sine.Stop(); square.Stop(); - triangle.Stop(); sawtooth.Stop(); + audio.Stop(sine); audio.Stop(square); + audio.Stop(triangle); audio.Stop(sawtooth); // play `sine` when held if(keyboard.GetKey(olc::Key::K7).bHeld) - sine.Play(); + audio.Play(sine); // play `sqaure` when held if(keyboard.GetKey(olc::Key::K8).bHeld) - square.Play(); + audio.Play(square); // play `triangle` when held if(keyboard.GetKey(olc::Key::K9).bHeld) - triangle.Play(); + audio.Play(triangle); // play `sawtooth` when held if(keyboard.GetKey(olc::Key::K0).bHeld) - sawtooth.Play(); + audio.Play(sawtooth); // toggle `song1` playback/pause if(keyboard.GetKey(olc::Key::SPACE).bPressed) - song1.Toggle(); + audio.Toggle(song1); // play `sample` if(keyboard.GetKey(olc::Key::S).bPressed) - sample.Play(); + audio.Play(sample); // panning if(keyboard.GetKey(olc::Key::MINUS).bHeld) @@ -139,15 +139,15 @@ class Example_Miniaudio : public olc::PixelGameEngine // panning pan = std::clamp(pan, -1.0f, 1.0f); - song1.SetPan(pan); + audio.SetPan(song1, pan); // pitch pitch = std::clamp(pitch, 0.0f, 2.0f); - song1.SetPitch(pitch); + audio.SetPitch(song1, pitch); // volume volume = std::clamp(volume, 0.0f, 1.0f); - song1.SetVolume(volume); + audio.SetVolume(song1, volume); // this is here to demosntrate how the adventurous can exploit other // features of miniaudio that haven't been abstracted by the PGEX. @@ -155,14 +155,14 @@ class Example_Miniaudio : public olc::PixelGameEngine ma_engine_listener_set_position(&audio.GetEngine(), 0, 0.0f, distance, 0.0f); // // get float cursor. 0.0f to 1.0f - cursorFloat = song1.GetCursorFloat(); + cursorFloat = audio.GetCursorFloat(song1); // // get cursor in milliseconds - cursorMillis = song1.GetCursor(); + cursorMillis = audio.GetCursor(song1); // Clear whole screen draw.Clear(olc::Colour::BLACK); - if(song1.IsPlaying()) + if(audio.IsPlaying(song1)) draw.Clear(olc::Colour::VERY_DARK_BLUE); draw.String( diff --git a/extensions/miniaudio/olcPGEX3_Miniaudio.h b/extensions/miniaudio/olcPGEX3_Miniaudio.h index 2a488eb..acc1cd6 100644 --- a/extensions/miniaudio/olcPGEX3_Miniaudio.h +++ b/extensions/miniaudio/olcPGEX3_Miniaudio.h @@ -57,7 +57,7 @@ #include "olcPixelGameEngine3.h" #endif -#ifdef OLC_PGEX3_MINIAUDIO +#if defined(OLC_PGEX3_MINIAUDIO) #define MINIAUDIO_IMPLEMENTATION #endif @@ -72,118 +72,48 @@ namespace olc::ext::Miniaudio { - class AudioEngine; + class AudioEngine; + + namespace internal + { + struct SoundInstance + { + std::vector buffer; + ma_sound base_sound{0}; + std::vector voices; + uint32_t id{0}; + std::string virtual_path{""}; + bool is_loaded{false}; + bool is_paused{false}; + uint32_t num_voices{8}; + uint32_t current_voice; + ma_uint64 length_in_pcm_frames{0}; + float length_in_seconds{0.0f}; + static uint32_t id_tracker; + }; + + struct WaveformInstance + { + bool is_loaded{false}; + float gain{0.0f}; + float target{0.0f}; + float rampStep{0.0f}; + ma_waveform waveform; + ma_waveform_config waveform_config; + }; -#pragma region Sound + } class Sound { friend class AudioEngine; - public: - Sound() = default; - ~Sound(); - Sound(const Sound&) = delete; - Sound& operator=(const Sound&) = delete; - Sound(Sound&& other) noexcept; - Sound& operator=(Sound&& other) noexcept; - - public: // Loaders - // Create an image resource based on an image file asset on disk - bool CreateSoundFromFile(const std::string& sFileName, AudioEngine* pgex, uint32_t nNumVoices); - // Create an image resource based on an image file asset in memory - bool CreateSoundFromMemory(const uint8_t* data, const size_t bytes, AudioEngine* pgex, uint32_t nNumVoices); - // Create an image resource based on an image file asset in memory - bool CreateSoundFromMemory(const std::vector& data, AudioEngine* pgex, uint32_t nNumVoices); - private: - // performs the necessary unloading of the parts of this sound - void DestroySound(); - - private: // loader function common to all loaders - bool _internalSoundLoader(); - - - public: // playback routines - // plays a sound, can set looping, volume, pan, or pitch (note: default values are out of range deliberately) - void Play(const bool looping = false, const float volume = 2.0f, const float pan = 2.0f, const float pitch = 2.0f); - // stops a sound, rewinds to beginning - void Stop(); - // pauses a sound, does not change position - void Pause(); - // toggle between play and pause - void Toggle(); - - public: // seeking controls - // seek to the provided position in the sound, by milliseconds - void Seek(const ma_uint64 milliseconds); - // seek to the provided position in the sound, by float 0.f is beginning, 1.0f is end - void Seek(const float& location); - // seek forward from current position by the provided time - void Forward(const ma_uint64 milliseconds); - // seek forward from current position by the provided time - void Rewind(const ma_uint64 milliseconds); - - public: // expression controls - // set volume of a sound, 0.0f is mute, 1.0f is full - void SetVolume(const float& volume); - // set pan of a sound, -1.0f is left, 1.0f is right, 0.0f is center - void SetPan(const float& pan); - // set pitch of a sound, 1.0f is normal - void SetPitch(const float& pitch); - - public: // misc information - // determine if a sound is playing - bool IsPlaying(); - // gets the current position in the sound, in milliseconds - ma_uint64 GetCursor(); - // gets the current position in the sound, as a float between 0.0f and 1.0f - float GetCursorFloat(); - // determine if this sound has been loaded successfully - bool IsLoaded() const; - public: // advanced usage - ma_sound* GetMASound(); - private: - // pointer to the calling pgex - AudioEngine* m_pgex; - // contains the sound file in memory - std::vector m_buffer; - // the base sound from which the voices are copied - ma_sound m_base_sound{0}; - // the voices of this sound - std::vector m_voices; - // the id of this sound, used to derive virtual path - uint32_t m_id{0}; - // the virtual path for the resource manager, usually sound/ - std::string m_virtual_path{""}; - private: // sound status - // has the sound been loaded successfully - bool m_is_loaded{false}; - // is the sound in a paused state - bool m_is_paused{false}; - // number of voices - uint32_t m_num_voices{8}; - // track the current voice - uint32_t m_current_voice{0}; - - private: // info - // the length of this sound in pcm frames - ma_uint64 m_length_in_pcm_frames{0}; - // the length of this sound in seconds - float m_length_in_seconds{0.0f}; - - private: // globals, has an effect on all sounds - // id tracker allows us to ensure every sound has a unique id - static uint32_t m_id_tracker; + internal::SoundInstance* sound{nullptr}; }; -#pragma endregion -#pragma region Waveform - - class Waveform { friend class AudioEngine; - public: enum class Type { @@ -192,49 +122,12 @@ namespace olc::ext::Miniaudio Triangle, Sawtooth }; - - public: // lifecycle - Waveform() = default; - - bool CreateWaveform(Waveform& waveform, const Type type, const double amplitude, const double frequency, AudioEngine* pgex); - private: - void DestroyWaveform(); - - public: // playback - void Play(); - void Stop(); - - public: // configuration - void SetAmplitude(const double amplitude); - void SetFrequency(const double frequency); - void SetType(const Type type); - - bool IsPlaying() const; - bool IsLoaded() const; - - ma_waveform& Get(); - private: - bool m_is_loaded{false}; - float m_gain = 0.0f; // current gain - float m_target = 0.0f; // 0.0 = stopped, 1.0 = playing - float m_rampStep = 0.0f; // set once at init: 1.0f / (sampleRate * 0.010f) - - ma_waveform m_waveform; - ma_waveform_config m_waveform_config; - - AudioEngine* m_pgex{nullptr}; + internal::WaveformInstance* waveform{nullptr}; }; -#pragma endregion - -#pragma region Miniaudio - class AudioEngine : public olc::PGESystemExtension - { - friend class Sound; - friend class Waveform; - + { public: struct Config { @@ -251,7 +144,6 @@ namespace olc::ext::Miniaudio // Logging: is logging verbose? default(false) bool Verbose{false}; }; - // configure the audio engine, see struct Config void Configure(const Config& cfg); // enable playback when the application window does not have focus. @@ -269,10 +161,63 @@ namespace olc::ext::Miniaudio bool CreateSoundFromMemory(Sound& sound, const uint8_t* data, const size_t bytes, uint32_t nNumVoices = 8); bool CreateSoundFromMemory(Sound& sound, const std::vector& data, uint32_t nNumVoices = 8); void DestroySound(Sound& sound); + bool _internalSoundLoader(Sound& sound); + + public: // Sound Playback and Controls + // plays a sound, can set looping, volume, pan, or pitch (note: default values are out of range deliberately) + void Play(Sound& sound, const bool looping = false, const float volume = 2.0f, const float pan = 2.0f, const float pitch = 2.0f); + // stops a sound, rewinds to beginning + void Stop(Sound& sound); + // pauses a sound, does not change position + void Pause(Sound& sound); + // toggle between play and pause + void Toggle(Sound& sound); + // seek to the provided position in the sound, by milliseconds + void Seek(Sound& sound, const ma_uint64 milliseconds); + // seek to the provided position in the sound, by float 0.f is beginning, 1.0f is end + void Seek(Sound& sound, const float& location); + // seek forward from current position by the provided time + void Forward(Sound& sound, const ma_uint64 milliseconds); + // seek forward from current position by the provided time + void Rewind(Sound& sound, const ma_uint64 milliseconds); + // set volume of a sound, 0.0f is mute, 1.0f is full + void SetVolume(Sound& sound, const float& volume); + // set pan of a sound, -1.0f is left, 1.0f is right, 0.0f is center + void SetPan(Sound& sound, const float& pan); + // set pitch of a sound, 1.0f is normal + void SetPitch(Sound& sound, const float& pitch); + // determine if a sound is playing + bool IsPlaying(Sound& sound); + // determine if this sound has been loaded successfully + bool IsLoaded(Sound& sound) const; + // gets the current position in the sound, in milliseconds + ma_uint64 GetCursor(Sound& sound); + // gets the current position in the sound, as a float between 0.0f and 1.0f + float GetCursorFloat(Sound& sound); + public: // advanced usage + ma_sound* GetMASound(Sound& sound); public: // Waveforms + // creates a waveform with the specified type, amplitude, and frequency bool CreateWaveform(Waveform& waveform, const Waveform::Type type, const double amplitude, const double frequency); + // destroys a waveform void DestroyWaveform(Waveform& waveform); + // plays a waveform + void Play(Waveform& waveform); + // stops a waveform + void Stop(Waveform& waveform); + // set the amplitude of a waveform + void SetWaveformAmplitude(Waveform& waveform, const double amplitude); + // set the frequency of a waveform + void SetWaveformFrequency(Waveform& waveform, const double frequency); + // set the type of a waveform + void SetWaveformType(Waveform& waveform, const Waveform::Type type); + // determine if this waveform is playing + bool IsPlaying(Waveform& waveform) const; + // determine if this waveform is loaded + bool IsLoaded(Waveform& waveform) const; + ma_waveform* GetMAWaveform(Waveform& waveform); + public: // Synth void SetSynthCallback(std::function callback); void ClearSynthCallback(); @@ -290,17 +235,18 @@ namespace olc::ext::Miniaudio ma_format GetDeviceFormat() const; int GetDeviceSampleRate() const; ma_device_type GetDeviceType() const; - + public: - AudioEngine(); + AudioEngine(); ~AudioEngine(); + virtual bool OnInstall([[maybe_unused]] olc::PixelGameEngine* pge); virtual bool OnBeforeUserCreate([[maybe_unused]] olc::PixelGameEngine* pge); virtual bool OnAfterUserCreate([[maybe_unused]] olc::PixelGameEngine* pge); virtual bool OnBeforeSystemUpdate([[maybe_unused]] olc::PixelGameEngine* pge, [[maybe_unused]] float fElapsedTime); virtual bool OnAfterSystemUpdate([[maybe_unused]] olc::PixelGameEngine* pge, [[maybe_unused]] float fElapsedTime); - + private: Config m_cfg; ma_device m_device; @@ -319,78 +265,180 @@ namespace olc::ext::Miniaudio std::function m_data_callback; // track sounds and waveforms - std::vector m_sounds; - std::vector m_waveforms; + std::vector m_sounds; + std::vector m_waveforms; bool m_is_initialized{false}; - olc::PixelGameEngine* m_pge{nullptr}; - }; + olc::PixelGameEngine* m_pge{nullptr}; + }; } -#pragma endregion - -#if defined(OLC_PGEX3_MINIAUDIO) +#ifdef OLC_PGEX3_MINIAUDIO #undef OLC_PGEX3_MINIAUDIO namespace olc::ext::Miniaudio { + uint32_t internal::SoundInstance::id_tracker = 0; -#pragma region Sound - - uint32_t Sound::m_id_tracker = 0; - Sound::Sound(Sound&& other) noexcept - : m_pgex(other.m_pgex), - m_buffer(std::move(other.m_buffer)), - m_base_sound(other.m_base_sound), - m_voices(std::move(other.m_voices)), - m_id(other.m_id), - m_virtual_path(std::move(other.m_virtual_path)), - m_is_loaded(other.m_is_loaded), - m_is_paused(other.m_is_paused), - m_num_voices(other.m_num_voices), - m_current_voice(other.m_current_voice), - m_length_in_pcm_frames(other.m_length_in_pcm_frames), - m_length_in_seconds(other.m_length_in_seconds) - { - other.m_pgex = nullptr; - other.m_base_sound = {0}; - other.m_is_loaded = false; - other.m_id = 0; - }; - - Sound& Sound::operator=(Sound&& other) noexcept - { - if(this != &other) + AudioEngine::AudioEngine() + { + } + + AudioEngine::~AudioEngine() + { + if(m_is_initialized) { - DestroySound(); - m_pgex = other.m_pgex; - m_buffer = std::move(other.m_buffer); - m_base_sound = other.m_base_sound; - m_voices = std::move(other.m_voices); - m_id = other.m_id; - m_virtual_path = std::move(other.m_virtual_path); - m_is_loaded = other.m_is_loaded; - m_is_paused = other.m_is_paused; - m_num_voices = other.m_num_voices; - m_current_voice = other.m_current_voice; - m_length_in_pcm_frames = other.m_length_in_pcm_frames; - m_length_in_seconds = other.m_length_in_seconds; + for(auto& sound : m_sounds) + { + if(sound == nullptr) continue; + for(auto& v : sound->voices) + { + if(ma_sound_is_playing(&v)) + ma_sound_stop(&v); + + ma_sound_uninit(&v); + } + ma_sound_uninit(&sound->base_sound); + ma_resource_manager_unregister_data(&m_resource_manager, sound->virtual_path.c_str()); + sound->is_loaded = false; + + delete sound; + } + m_sounds.clear(); + + for(auto& w : m_waveforms) + { + if(w == nullptr) continue; + if(!w->is_loaded) continue; + ma_waveform_uninit(&w->waveform); + } + m_waveforms.clear(); + + ma_resource_manager_uninit(&m_resource_manager); + + ma_engine_stop(&m_engine); + ma_engine_uninit(&m_engine); - other.m_pgex = nullptr; - other.m_base_sound = {0}; - other.m_is_loaded = false; - other.m_id = 0; + ma_device_stop(&m_device); + ma_device_uninit(&m_device); } - return *this; + } + + void AudioEngine::Configure(const Config& cfg) + { + if(m_is_initialized) + { + std::cerr << "olcPGEX3_miniaudio: Configure called after initialized.\n"; + return; + } + + m_cfg = cfg; } - Sound::~Sound() + void AudioEngine::EnableBackgroundPlayback() { - DestroySound(); + m_cfg.BackgroundPlay = true; } + + void AudioEngine::DisableBackgroundPlayback() + { + m_cfg.BackgroundPlay = false; + } + + void AudioEngine::data_callback(ma_device* pDevice, void* pOutput, const void* pInput, ma_uint32 frameCount) + { + AudioEngine* ma = (AudioEngine*)pDevice->pUserData; + if(ma == nullptr) + throw std::runtime_error{"unable to access miniaudio pgex instance from data_callback"}; + + // with great power comes... + if(ma->m_data_callback) + { + ma->m_data_callback((float*)pOutput, frameCount); + return; + } - bool Sound::CreateSoundFromFile(const std::string& sFileName, AudioEngine* pgex, uint32_t nNumVoices) + const bool audioShouldPlay = !(!ma->m_cfg.BackgroundPlay && !ma->m_pge->IsFocused()); + + std::span engineBuffer((float*)pOutput, frameCount * ma->GetDeviceChannels()); + ma_engine_read_pcm_frames(&ma->m_engine, engineBuffer.data(), frameCount, NULL); + + // resize, if required. frameCount is not guaranteed not to change. + if(ma->m_waveform_buffer.size() != (frameCount * ma->GetDeviceChannels())) + { + ma->m_waveform_buffer.resize(frameCount * ma->GetDeviceChannels(), 0); + } + + // waveforms + for(auto& w : ma->m_waveforms) + { + // if waveform is not playing, continue + if (!(w->target > 0.0f || w->gain > 0.0f)) + continue; + + ma_waveform_read_pcm_frames(&w->waveform, ma->m_waveform_buffer.data(), frameCount, NULL); + + // if audio shouldn't play, move on here. + if(!audioShouldPlay) + continue; + + for(int frame = 0; frame < frameCount; ++frame) + { + // Ramp gain toward target one step per frame + if (w->gain < w->target) + w->gain = std::min(w->gain + w->rampStep, w->target); + else if (w->gain > w->target) + w->gain = std::max(w->gain - w->rampStep, w->target); + + for(int channel = 0; channel < ma->GetDeviceChannels(); ++channel) + { + int i = frame * ma->GetDeviceChannels() + channel; + engineBuffer[i] += ma->m_waveform_buffer[i] * w->gain; + } + } + } + + // synth function + if(ma->m_synth_callback) + { + for(ma_uint32 i = 0; i < frameCount; i++) + { + float left, right; + ma->m_synth_callback(left, right, 1.0f / ma->GetDeviceSampleRate()); + if(audioShouldPlay) + { + engineBuffer[(i * ma->GetDeviceChannels())] += left; + engineBuffer[(i * ma->GetDeviceChannels()) + 1] += right; + } + } + } + + // limiter + static float envelope = 1.0f; + + for(int i = 0; i < engineBuffer.size(); i++) + { + if(!audioShouldPlay) + { + engineBuffer[i] = 0.0f; + continue; + } + + float peak = fabsf(engineBuffer[i]); + + if (peak > 1.0f) + envelope = fminf(envelope, 1.0f / peak); // duck the gain + else + envelope = fminf(1.0f, envelope * 1.001f); // slowly recover + // limit the output + engineBuffer[i] *= envelope; + } + } + + bool AudioEngine::CreateSoundFromFile(Sound& sound, const std::string& sFileName, uint32_t nNumVoices) { + sound.sound = new internal::SoundInstance(); + #if OLC_HOST == OLC_HOST_ANDROID AAsset* pAsset = AAssetManager_open( olc::host::Host_Android::androidApp->activity->assetManager, @@ -402,74 +450,61 @@ namespace olc::ext::Miniaudio return false; off_t size = AAsset_getLength(pAsset); - m_buffer.resize(size); - AAsset_read(pAsset, m_buffer.data(), size); + sound.sound->buffer.resize(size); + AAsset_read(pAsset, sound.sound->buffer.data(), size); AAsset_close(pAsset); #else std::ifstream f(sFileName, std::ios::binary | std::ios::ate); if(f.fail()) return false; - m_buffer.resize(f.tellg()); + sound.sound->buffer.resize(f.tellg()); f.seekg(0); - f.read(reinterpret_cast(m_buffer.data()), m_buffer.size()); + f.read(reinterpret_cast(sound.sound->buffer.data()), sound.sound->buffer.size()); f.close(); #endif - m_pgex = pgex; - m_num_voices = nNumVoices; - return _internalSoundLoader(); + sound.sound->num_voices = nNumVoices; + return _internalSoundLoader(sound); } - - bool Sound::CreateSoundFromMemory(const uint8_t* data, const size_t bytes, AudioEngine* pgex, uint32_t nNumVoices) + + bool AudioEngine::CreateSoundFromMemory(Sound& sound, const uint8_t* data, const size_t bytes, uint32_t nNumVoices) { - if(!data) return false; - if(bytes <= 0) return false; + sound.sound = new internal::SoundInstance(); + if(!data || bytes <= 0) return false; - m_buffer.resize(bytes); - uint8_t* result = reinterpret_cast(std::memcpy(m_buffer.data(), data, m_buffer.size())); - if(result == m_buffer.data()) + // Thanks Linh + try + { + sound.sound->buffer.assign(data, data + bytes); + } + catch (const std::exception&) + { + sound.sound->buffer.clear(); return false; + } - m_pgex = pgex; - m_num_voices = nNumVoices; - return _internalSoundLoader(); + sound.sound->num_voices = nNumVoices; + return _internalSoundLoader(sound); } - - bool Sound::CreateSoundFromMemory(const std::vector& data, AudioEngine* pgex, uint32_t nNumVoices) + + bool AudioEngine::CreateSoundFromMemory(Sound& sound, const std::vector& data, uint32_t nNumVoices) { + sound.sound = new internal::SoundInstance(); if(data.size() <= 0) return false; - m_buffer = data; - m_pgex = pgex; - m_num_voices = nNumVoices; - return _internalSoundLoader(); - } - - void Sound::DestroySound() - { - if(!m_is_loaded) return; - - for(auto& v : m_voices) - { - if(ma_sound_is_playing(&v)) - ma_sound_stop(&v); - - ma_sound_uninit(&v); - } - m_voices.clear(); - ma_sound_uninit(&m_base_sound); - ma_resource_manager_unregister_data(ma_engine_get_resource_manager(&m_pgex->GetEngine()), m_virtual_path.c_str()); - m_is_loaded = false; + sound.sound->buffer = data; + sound.sound->num_voices = nNumVoices; + return _internalSoundLoader(sound); } - - bool Sound::_internalSoundLoader() + + bool AudioEngine::_internalSoundLoader(Sound& sound) { ma_result result; - m_id = ++m_id_tracker; - m_virtual_path = "sound/" + std::to_string(m_id); + sound.sound->id = ++sound.sound->id_tracker; + sound.sound->virtual_path = "sound/" + std::to_string(sound.sound->id); result = ma_resource_manager_register_encoded_data( - ma_engine_get_resource_manager(&m_pgex->GetEngine()), - m_virtual_path.c_str(), - m_buffer.data(), m_buffer.size() + &m_resource_manager, + sound.sound->virtual_path.c_str(), + sound.sound->buffer.data(), sound.sound->buffer.size() ); if(result != MA_SUCCESS) @@ -480,29 +515,29 @@ namespace olc::ext::Miniaudio if(result != MA_SUCCESS) { - ma_resource_manager_unregister_data(ma_engine_get_resource_manager(&m_pgex->GetEngine()), m_virtual_path.c_str()); + ma_resource_manager_unregister_data(&m_resource_manager, sound.sound->virtual_path.c_str()); return false; } result = ma_sound_init_from_file( - &m_pgex->GetEngine(), - m_virtual_path.c_str(), + &GetEngine(), + sound.sound->virtual_path.c_str(), MA_SOUND_FLAG_DECODE, nullptr, &fence, - &m_base_sound + &sound.sound->base_sound ); if(result != MA_SUCCESS) { - ma_resource_manager_unregister_data(ma_engine_get_resource_manager(&m_pgex->GetEngine()), m_virtual_path.c_str()); + ma_resource_manager_unregister_data(&m_resource_manager, sound.sound->virtual_path.c_str()); return false; } - m_voices.resize(m_num_voices); - for(int i = 0; i < m_num_voices; ++i) + sound.sound->voices.resize(sound.sound->num_voices); + for(int i = 0; i < sound.sound->num_voices; ++i) { - result = ma_sound_init_copy(&m_pgex->GetEngine(), &m_base_sound, 0, nullptr, &m_voices[i]); + result = ma_sound_init_copy(&GetEngine(), &sound.sound->base_sound, 0, nullptr, &sound.sound->voices[i]); if(result != MA_SUCCESS) break; } @@ -510,10 +545,10 @@ namespace olc::ext::Miniaudio // if the last result out of that loop isn't success, we failed if(result != MA_SUCCESS) { - for(auto& v : m_voices) + for(auto& v : sound.sound->voices) ma_sound_uninit(&v); - ma_sound_uninit(&m_base_sound); - ma_resource_manager_unregister_data(ma_engine_get_resource_manager(&m_pgex->GetEngine()), m_virtual_path.c_str()); + ma_sound_uninit(&sound.sound->base_sound); + ma_resource_manager_unregister_data(&m_resource_manager, sound.sound->virtual_path.c_str()); return false; } @@ -521,461 +556,299 @@ namespace olc::ext::Miniaudio ma_fence_wait(&fence); ma_fence_uninit(&fence); - ma_sound_get_length_in_pcm_frames(&m_base_sound, &m_length_in_pcm_frames); - ma_sound_get_length_in_seconds(&m_base_sound, &m_length_in_seconds); + ma_sound_get_length_in_pcm_frames(&sound.sound->base_sound, &sound.sound->length_in_pcm_frames); + ma_sound_get_length_in_seconds(&sound.sound->base_sound, &sound.sound->length_in_seconds); - m_is_loaded = true; + sound.sound->is_loaded = true; + m_sounds.push_back(sound.sound); return true; } - // plays a sound, can be set to loop - void Sound::Play(const bool looping, const float volume, const float pan, const float pitch) + void AudioEngine::DestroySound(Sound& sound) + { + if(sound.sound == nullptr) return; + if(!sound.sound->is_loaded) return; + for(auto& v : sound.sound->voices) + { + if(ma_sound_is_playing(&v)) + ma_sound_stop(&v); + + ma_sound_uninit(&v); + } + sound.sound->voices.clear(); + ma_sound_uninit(&sound.sound->base_sound); + ma_resource_manager_unregister_data(&m_resource_manager, sound.sound->virtual_path.c_str()); + sound.sound->is_loaded = false; + + delete sound.sound; + + sound.sound = nullptr; + } + + void AudioEngine::Play(Sound& sound, const bool looping, const float volume, const float pan, const float pitch) { - if(!m_is_paused) - m_current_voice = (m_current_voice + 1) % m_num_voices; + if(sound.sound == nullptr) return; + + if(!sound.sound->is_paused) + sound.sound->current_voice = (sound.sound->current_voice + 1) % sound.sound->num_voices; if(volume != 2.0f) - ma_sound_set_volume(&m_voices[m_current_voice], std::clamp(volume, 0.0f, 1.0f)); + ma_sound_set_volume(&sound.sound->voices[sound.sound->current_voice], std::clamp(volume, 0.0f, 1.0f)); if(pan != 2.0f) - ma_sound_set_pan(&m_voices[m_current_voice], std::clamp(pan, -1.0f, 1.0f)); + ma_sound_set_pan(&sound.sound->voices[sound.sound->current_voice], std::clamp(pan, -1.0f, 1.0f)); if(pitch != 2.0f) - ma_sound_set_pitch(&m_voices[m_current_voice], std::max({0.0f, pitch})); + ma_sound_set_pitch(&sound.sound->voices[sound.sound->current_voice], std::max({0.0f, pitch})); - ma_sound_set_looping(&m_voices[m_current_voice], looping); - ma_sound_seek_to_pcm_frame(&m_voices[m_current_voice], 0); - ma_sound_start(&m_voices[m_current_voice]); - m_is_paused = false; + ma_sound_set_looping(&sound.sound->voices[sound.sound->current_voice], looping); + ma_sound_seek_to_pcm_frame(&sound.sound->voices[sound.sound->current_voice], 0); + ma_sound_start(&sound.sound->voices[sound.sound->current_voice]); + sound.sound->is_paused = false; } - - // stops a sound, rewinds to beginning - void Sound::Stop() + + void AudioEngine::Stop(Sound& sound) { - if(!ma_sound_is_playing(&m_voices[m_current_voice])) + if(sound.sound == nullptr) return; + if(!ma_sound_is_playing(&sound.sound->voices[sound.sound->current_voice])) return; - ma_sound_stop(&m_voices[m_current_voice]); - ma_sound_seek_to_pcm_frame(&m_voices[m_current_voice], 0); + ma_sound_stop(&sound.sound->voices[sound.sound->current_voice]); + ma_sound_seek_to_pcm_frame(&sound.sound->voices[sound.sound->current_voice], 0); } - // pauses a sound, does not change position - void Sound::Pause() + void AudioEngine::Pause(Sound& sound) { - if(!ma_sound_is_playing(&m_voices[m_current_voice])) + if(sound.sound == nullptr) return; + if(!ma_sound_is_playing(&sound.sound->voices[sound.sound->current_voice])) return; - ma_sound_stop(&m_voices[m_current_voice]); - m_is_paused = true; + ma_sound_stop(&sound.sound->voices[sound.sound->current_voice]); + sound.sound->is_paused = true; } - - // toggle between play and pause - void Sound::Toggle() + + void AudioEngine::Toggle(Sound& sound) { - if(ma_sound_is_playing(&m_voices[m_current_voice])) + if(sound.sound == nullptr) return; + if(ma_sound_is_playing(&sound.sound->voices[sound.sound->current_voice])) { - ma_sound_stop(&m_voices[m_current_voice]); - m_is_paused = true; + ma_sound_stop(&sound.sound->voices[sound.sound->current_voice]); + sound.sound->is_paused = true; return; } - ma_sound_start(&m_voices[m_current_voice]); - m_is_paused = false; + ma_sound_start(&sound.sound->voices[sound.sound->current_voice]); + sound.sound->is_paused = false; } - // seek to the provided position in the sound, by milliseconds - void Sound::Seek(const ma_uint64 milliseconds) + void AudioEngine::Seek(Sound& sound, const ma_uint64 milliseconds) { - ma_uint64 frame_to_seek_to = (milliseconds * m_pgex->GetDeviceSampleRate()) / 1000; - ma_sound_seek_to_pcm_frame(&m_voices[m_current_voice], frame_to_seek_to); + if(sound.sound == nullptr) return; + ma_uint64 frame_to_seek_to = (milliseconds * GetDeviceSampleRate()) / 1000; + ma_sound_seek_to_pcm_frame(&sound.sound->voices[sound.sound->current_voice], frame_to_seek_to); + } - - // seek to the provided position in the sound, by float 0.f is beginning, 1.0f is end - void Sound::Seek(const float& location) + + void AudioEngine::Seek(Sound& sound, const float& location) { - ma_uint64 frame_to_seek_to = static_cast(m_length_in_pcm_frames * location); - ma_sound_seek_to_pcm_frame(&m_voices[m_current_voice], frame_to_seek_to); + if(sound.sound == nullptr) return; + ma_uint64 frame_to_seek_to = static_cast(sound.sound->length_in_pcm_frames * location); + ma_sound_seek_to_pcm_frame(&sound.sound->voices[sound.sound->current_voice], frame_to_seek_to); } - - // seek forward from current position by the provided time - void Sound::Forward(const ma_uint64 milliseconds) + + void AudioEngine::Forward(Sound& sound, const ma_uint64 milliseconds) { + if(sound.sound == nullptr) return; ma_uint64 frame_to_seek_to; // get the current position - ma_sound_get_cursor_in_pcm_frames(&m_voices[m_current_voice], &frame_to_seek_to); + ma_sound_get_cursor_in_pcm_frames(&sound.sound->voices[sound.sound->current_voice], &frame_to_seek_to); // calculate the step and add it to the current position - frame_to_seek_to += ((milliseconds * m_pgex->GetDeviceSampleRate()) / 1000); + frame_to_seek_to += ((milliseconds * GetDeviceSampleRate()) / 1000); // seek to the new position - ma_sound_seek_to_pcm_frame(&m_voices[m_current_voice], frame_to_seek_to); + ma_sound_seek_to_pcm_frame(&sound.sound->voices[sound.sound->current_voice], frame_to_seek_to); } - - // seek forward from current position by the provided time - void Sound::Rewind(const ma_uint64 milliseconds) + + void AudioEngine::Rewind(Sound& sound, const ma_uint64 milliseconds) { + if(sound.sound == nullptr) return; ma_uint64 frame_to_seek_to; // get the current position - ma_sound_get_cursor_in_pcm_frames(&m_voices[m_current_voice], &frame_to_seek_to); + ma_sound_get_cursor_in_pcm_frames(&sound.sound->voices[sound.sound->current_voice], &frame_to_seek_to); // calculate the step and add it to the current position - frame_to_seek_to -= ((milliseconds * m_pgex->GetDeviceSampleRate()) / 1000); + frame_to_seek_to -= ((milliseconds * GetDeviceSampleRate()) / 1000); // seek to the new position - ma_sound_seek_to_pcm_frame(&m_voices[m_current_voice], frame_to_seek_to); + ma_sound_seek_to_pcm_frame(&sound.sound->voices[sound.sound->current_voice], frame_to_seek_to); } - // set volume of a sound, 0.0f is mute, 1.0f is full - void Sound::SetVolume(const float& volume) + void AudioEngine::SetVolume(Sound& sound, const float& volume) { - for(auto& v : m_voices) + if(sound.sound == nullptr) return; + for(auto& v : sound.sound->voices) { ma_sound_set_volume(&v, std::clamp(volume, 0.0f, 1.0f)); } } - // set pan of a sound, -1.0f is left, 1.0f is right, 0.0f is center - void Sound::SetPan(const float& pan) + void AudioEngine::SetPan(Sound& sound, const float& pan) { - for(auto& v : m_voices) + if(sound.sound == nullptr) return; + for(auto& v : sound.sound->voices) { ma_sound_set_pan(&v, std::clamp(pan, -1.0f, 1.0f)); } } - - // set pitch of a sound, 1.0f is normal - void Sound::SetPitch(const float& pitch) + + void AudioEngine::SetPitch(Sound& sound, const float& pitch) { - for(auto& v : m_voices) + if(sound.sound == nullptr) return; + for(auto& v : sound.sound->voices) { ma_sound_set_pitch(&v, std::max({0.0f, pitch})); } } - // determine if a sound is playing - bool Sound::IsPlaying() + bool AudioEngine::IsPlaying(Sound& sound) { - for(auto& v : m_voices) + if(sound.sound == nullptr) return false; + for(auto& v : sound.sound->voices) { if(ma_sound_is_playing(&v)) return true; } return false; } - - // gets the current position in the sound, in milliseconds - ma_uint64 Sound::GetCursor() + + ma_uint64 AudioEngine::GetCursor(Sound& sound) { + if(sound.sound == nullptr) return 0; ma_uint64 cursor; - ma_sound_get_cursor_in_pcm_frames(&m_voices[m_current_voice], &cursor); - return (cursor * 1000) / m_pgex->GetDeviceSampleRate(); + ma_sound_get_cursor_in_pcm_frames(&sound.sound->voices[sound.sound->current_voice], &cursor); + return (cursor * 1000) / GetDeviceSampleRate(); } - - // gets the current position in the sound, as a float between 0.0f and 1.0f - float Sound::GetCursorFloat() + + float AudioEngine::GetCursorFloat(Sound& sound) { + if(sound.sound == nullptr) return 0.0f; float cursor; - ma_sound_get_cursor_in_seconds(&m_voices[m_current_voice], &cursor); - return cursor / m_length_in_seconds; + ma_sound_get_cursor_in_seconds(&sound.sound->voices[sound.sound->current_voice], &cursor); + return cursor / sound.sound->length_in_seconds; } - // determine if this sound has been loaded successfully - bool Sound::IsLoaded() const + bool AudioEngine::IsLoaded(Sound& sound) const { - return m_is_loaded; + if(sound.sound == nullptr) return false; + return sound.sound->is_loaded; } - ma_sound* Sound::GetMASound() + ma_sound* AudioEngine::GetMASound(Sound& sound) { + if(sound.sound == nullptr) return nullptr; // realistically, one wouldn't call this unless it was loaded - if(!IsLoaded()) + if(!sound.sound->is_loaded) return nullptr; // if we're not currently playing, get the pointer of the next voice - if(!IsPlaying()) - return &m_voices[(m_current_voice + 1) % m_num_voices]; + if(!IsPlaying(sound)) + return &sound.sound->voices[(sound.sound->current_voice + 1) % sound.sound->num_voices]; // if we're playing, get the pointer of the current voice - return &m_voices[m_current_voice]; + return &sound.sound->voices[sound.sound->current_voice]; } -#pragma endregion - - -#pragma region Waveform - - bool Waveform::CreateWaveform(Waveform& waveform, const Type type, const double amplitude, const double frequency, AudioEngine* pgex) + bool AudioEngine::CreateWaveform(Waveform& waveform, const Waveform::Type type, const double amplitude, const double frequency) { - m_pgex = pgex; - m_waveform_config = ma_waveform_config_init( - m_pgex->GetDeviceFormat(), - m_pgex->GetDeviceChannels(), - m_pgex->GetDeviceSampleRate(), + waveform.waveform = new internal::WaveformInstance(); + waveform.waveform->waveform_config = ma_waveform_config_init( + GetDeviceFormat(), + GetDeviceChannels(), + GetDeviceSampleRate(), static_cast(type), amplitude, frequency ); - m_rampStep = 1.0f / (m_pgex->GetDeviceSampleRate() * 0.02f); + waveform.waveform->rampStep = 1.0f / (GetDeviceSampleRate() * 0.02f); - if(ma_waveform_init(&m_waveform_config, &m_waveform) != MA_SUCCESS) + if(ma_waveform_init(&waveform.waveform->waveform_config, &waveform.waveform->waveform) != MA_SUCCESS) { + delete waveform.waveform; + waveform.waveform = nullptr; return false; } - m_is_loaded = true; + waveform.waveform->is_loaded = true; + m_waveforms.push_back(waveform.waveform); return true; } - - void Waveform::DestroyWaveform() - { - ma_waveform_uninit(&m_waveform); - m_is_loaded = false; - } - - void Waveform::Play() - { - if(!IsLoaded()) - return; - m_target = 1.0f; - } - - void Waveform::Stop() + void AudioEngine::DestroyWaveform(Waveform& waveform) { - if(!IsLoaded()) - return; - - m_target = 0.0f; + if(waveform.waveform == nullptr) return; + ma_waveform_uninit(&waveform.waveform->waveform); + delete waveform.waveform; + waveform.waveform = nullptr; } - - void Waveform::SetAmplitude(const double amplitude) + void AudioEngine::Play(Waveform& waveform) { - if(!IsLoaded()) + if(!IsLoaded(waveform)) return; - ma_waveform_set_amplitude(&m_waveform, amplitude); + waveform.waveform->target = 1.0f; } - void Waveform::SetFrequency(const double frequency) + void AudioEngine::Stop(Waveform& waveform) { - if(!IsLoaded()) + if(!IsLoaded(waveform)) return; - ma_waveform_set_frequency(&m_waveform, frequency); - } - - void Waveform::SetType(const Type type) - { - if(!IsLoaded()) - return; - ma_waveform_set_type(&m_waveform, static_cast(type)); + waveform.waveform->target = 0.0f; } - bool Waveform::IsPlaying() const + void AudioEngine::SetWaveformAmplitude(Waveform& waveform, const double amplitude) { - return m_target > 0.0f || m_gain > 0.0f; - } - - bool Waveform::IsLoaded() const - { - return m_is_loaded; + if(!IsLoaded(waveform)) + return; + ma_waveform_set_amplitude(&waveform.waveform->waveform, amplitude); } - ma_waveform& Waveform::Get() + void AudioEngine::SetWaveformFrequency(Waveform& waveform, const double frequency) { - return m_waveform; - } - -#pragma endregion - -#pragma region Miniaudio - - AudioEngine::AudioEngine() - { - } - - AudioEngine::~AudioEngine() - { - if(m_is_initialized) - { - for(auto sound : m_sounds) - sound->DestroySound(); - - m_sounds.clear(); - ma_resource_manager_uninit(&m_resource_manager); - - ma_engine_stop(&m_engine); - ma_engine_uninit(&m_engine); - - - ma_device_stop(&m_device); - ma_device_uninit(&m_device); - } - } - - void AudioEngine::Configure(const Config& cfg) - { - if(m_is_initialized) - { - std::cerr << "olcPGEX3_miniaudio: Configure called after initialized.\n"; + if(!IsLoaded(waveform)) return; - } - - m_cfg = cfg; + ma_waveform_set_frequency(&waveform.waveform->waveform, frequency); } - void AudioEngine::EnableBackgroundPlayback() - { - m_cfg.BackgroundPlay = true; - } - - void AudioEngine::DisableBackgroundPlayback() + void AudioEngine::SetWaveformType(Waveform& waveform, const Waveform::Type type) { - m_cfg.BackgroundPlay = false; - } - - void AudioEngine::data_callback(ma_device* pDevice, void* pOutput, const void* pInput, ma_uint32 frameCount) - { - AudioEngine* ma = (AudioEngine*)pDevice->pUserData; - if(ma == nullptr) - throw std::runtime_error{"unable to access miniaudio pgex instance from data_callback"}; - - // with great power comes... - if(ma->m_data_callback) - { - ma->m_data_callback((float*)pOutput, frameCount); + if(!IsLoaded(waveform)) return; - } - - const bool audioShouldPlay = !(!ma->m_cfg.BackgroundPlay && !ma->m_pge->IsFocused()); - - std::span engineBuffer((float*)pOutput, frameCount * ma->GetDeviceChannels()); - ma_engine_read_pcm_frames(&ma->m_engine, engineBuffer.data(), frameCount, NULL); - - // resize, if required. frameCount is not guaranteed not to change. - if(ma->m_waveform_buffer.size() != (frameCount * ma->GetDeviceChannels())) - { - ma->m_waveform_buffer.resize(frameCount * ma->GetDeviceChannels(), 0); - } - - // waveforms - for(auto& waveform : ma->m_waveforms) - { - if (!waveform->IsPlaying()) - continue; - - ma_waveform_read_pcm_frames(&waveform->m_waveform, ma->m_waveform_buffer.data(), frameCount, NULL); - - // if audio shouldn't play, move on here. - if(!audioShouldPlay) - continue; - - for(int frame = 0; frame < frameCount; ++frame) - { - // Ramp gain toward target one step per frame - if (waveform->m_gain < waveform->m_target) - waveform->m_gain = std::min(waveform->m_gain + waveform->m_rampStep, waveform->m_target); - else if (waveform->m_gain > waveform->m_target) - waveform->m_gain = std::max(waveform->m_gain - waveform->m_rampStep, waveform->m_target); - - for(int channel = 0; channel < ma->GetDeviceChannels(); ++channel) - { - int i = frame * ma->GetDeviceChannels() + channel; - engineBuffer[i] += ma->m_waveform_buffer[i] * waveform->m_gain; - } - } - } - - // synth function - if(ma->m_synth_callback) - { - for(ma_uint32 i = 0; i < frameCount; i++) - { - float left, right; - ma->m_synth_callback(left, right, 1.0f / ma->GetDeviceSampleRate()); - if(audioShouldPlay) - { - engineBuffer[(i * ma->GetDeviceChannels())] += left; - engineBuffer[(i * ma->GetDeviceChannels()) + 1] += right; - } - } - } - - // limiter - static float envelope = 1.0f; - - for(int i = 0; i < engineBuffer.size(); i++) - { - if(!audioShouldPlay) - { - engineBuffer[i] = 0.0f; - continue; - } - - float peak = fabsf(engineBuffer[i]); - - if (peak > 1.0f) - envelope = fminf(envelope, 1.0f / peak); // duck the gain - else - envelope = fminf(1.0f, envelope * 1.001f); // slowly recover - // limit the output - engineBuffer[i] *= envelope; - } - } - - bool AudioEngine::CreateSoundFromFile(Sound& sound, const std::string& sFileName, uint32_t nNumVoices) - { - m_sounds.push_back(&sound); - return sound.CreateSoundFromFile(sFileName, this, nNumVoices); - } - - bool AudioEngine::CreateSoundFromMemory(Sound& sound, const uint8_t* data, const size_t bytes, uint32_t nNumVoices) - { - m_sounds.push_back(&sound); - return sound.CreateSoundFromMemory(data, bytes, this, nNumVoices); - } - - bool AudioEngine::CreateSoundFromMemory(Sound& sound, const std::vector& data, uint32_t nNumVoices) - { - m_sounds.push_back(&sound); - return sound.CreateSoundFromMemory(data, this, nNumVoices); + ma_waveform_set_type(&waveform.waveform->waveform, static_cast(type)); } - - void AudioEngine::DestroySound(Sound& sound) + + bool AudioEngine::IsPlaying(Waveform& waveform) const { - sound.DestroySound(); - - m_sounds.erase( - std::remove_if( - m_sounds.begin(), - m_sounds.end(), - [&](Sound* s) { return (s == &sound); } - ), - m_sounds.end() - ); + if(waveform.waveform == nullptr) return false; + return waveform.waveform->target > 0.0f || waveform.waveform->gain > 0.0f; } - bool AudioEngine::CreateWaveform(Waveform& waveform, const Waveform::Type type, const double amplitude, const double frequency) + bool AudioEngine::IsLoaded(Waveform& waveform) const { - m_waveforms.push_back(&waveform); - return waveform.CreateWaveform(waveform, type, amplitude, frequency, this); + if(waveform.waveform == nullptr) return false; + return waveform.waveform->is_loaded; } - void AudioEngine::DestroyWaveform(Waveform& waveform) + ma_waveform* AudioEngine::GetMAWaveform(Waveform& waveform) { - waveform.DestroyWaveform(); - m_waveforms.erase( - std::remove_if( - m_waveforms.begin(), - m_waveforms.end(), - [&](Waveform* w) { return (w == &waveform); } - ), - m_waveforms.end() - ); + if(waveform.waveform == nullptr) return nullptr; + if(!waveform.waveform->is_loaded) return nullptr; + return &waveform.waveform->waveform; } - void AudioEngine::SetSynthCallback(std::function callback) + void AudioEngine::SetSynthCallback(std::function callback) { m_synth_callback = callback; } @@ -1099,8 +972,7 @@ namespace olc::ext::Miniaudio bool AudioEngine::OnAfterSystemUpdate([[maybe_unused]] olc::PixelGameEngine* pge, [[maybe_unused]] float fElapsedTime) { return true; - } -#pragma endregion - + } } -#endif + +#endif \ No newline at end of file From 13cbfcf57aebec0c17a974604f7618aac2ca0e6f Mon Sep 17 00:00:00 2001 From: Moros Smith Date: Sun, 4 Oct 2026 11:30:20 -0400 Subject: [PATCH 09/21] [ext][miniaudio] update docs to reflect ChAnGe --- extensions/miniaudio/README.md | 507 +++++++++--------------- extensions/miniaudio/demo_Miniaudio.cpp | 2 +- 2 files changed, 194 insertions(+), 315 deletions(-) diff --git a/extensions/miniaudio/README.md b/extensions/miniaudio/README.md index 13baacc..47f40b7 100644 --- a/extensions/miniaudio/README.md +++ b/extensions/miniaudio/README.md @@ -1,426 +1,305 @@ -# olcPGEX3_Miniaudio - Usage Documentation +# olcPGEX3_Miniaudio -## Overview +An audio extension for the [OneLoneCoder Pixel Game Engine 3](https://github.com/OneLoneCoder) built on top of the [miniaudio](https://miniaud.io) library. It gives you simple, high-level loading and playback of sound files, real-time waveform generation, and hooks for custom synthesis, while still letting you reach down into miniaudio for anything the extension doesn't wrap. -**olcPGEX3_Miniaudio** is an olcPixelGameEngine3 extension that provides a simple, abstracted interface to the powerful **miniaudio** library. It enables easy loading and playback of WAV and MP3 audio files with support for various audio effects and controls. +Because it sits on miniaudio, it needs next to no extra build configuration to work cross-platform. -### Key Features -- Load and play WAV and MP3 files -- Generate procedural waveforms (Sine, Square, Triangle, Sawtooth) -- Full playback control (play, pause, stop, toggle) -- Audio effects: volume, pan, pitch adjustment -- Audio seeking and cursor position tracking -- Multiple voice support for simultaneous playback -- Synthesizer callbacks for procedural audio generation -- Cross-platform support (including Emscripten) -- Custom audio data callbacks for advanced users -- Exposure to underlying miniaudio library, for advanced users ---- +## Features -## Installation & Setup +- **Sound loading** from disk or from memory (WAV and MP3, plus anything your miniaudio build can decode) +- **Polyphonic playback**: each sound has a pool of voices (default 8), so rapid-fire sound effects overlap instead of cutting each other off +- **Per-sound controls**: volume, pan, pitch, looping, seek, forward, rewind, pause, toggle +- **Playback position** queries in milliseconds or as a normalised float (handy for progress bars) +- **Waveform generators**: sine, square, triangle and sawtooth, with adjustable amplitude, frequency and type, and a short fade in/out to avoid clicks +- **Synth callback**: generate or process audio sample by sample +- **Raw data callback**: take over the audio device output completely +- **Built-in limiter** to prevent clipping when many sources play at once +- **Background playback control**: audio mutes when the window loses focus, unless you enable background playback +- **Escape hatches** to the underlying `ma_engine`, `ma_device`, `ma_resource_manager`, `ma_sound` and `ma_waveform` objects +- **Platform support** for desktop, Emscripten (web) and Android asset loading -### 1. Acquire miniaudio +## Requirements -Visit the [miniaudio website](https://miniaud.io/) to download the header file. Simply add it to your project with the rest of your source/header files. +- Pixel Game Engine 3 (`olcPixelGameEngine3.h`, or `olcpge3.h` if you define `OLC_MULTIHEADER`) +- `miniaudio.h` on your include path +- A **C++20** compiler (the extension uses `std::span`) + +## Installation + +1. Copy `olcPGEX3_Miniaudio.h` next to your project (or anywhere on your include path). +2. Make sure `miniaudio.h` is available. +3. In **exactly one** translation unit, define `OLC_PGEX3_MINIAUDIO` before including the extension. This pulls in both the extension's implementation and miniaudio's implementation. -### 2. Include the Header after PGE3 ```cpp +#define OLC_PGE3_APPLICATION +#include "olcPixelGameEngine3.h" + #define OLC_PGEX3_MINIAUDIO #include "olcPGEX3_Miniaudio.h" ``` -### 3. Install the Extension in Your PGE Application +In any other file that needs the audio types, include the header **without** the define. + +## Quick start + +Make the `AudioEngine` a member of your application class, and install it as a system extension in the constructor: + ```cpp class MyGame : public olc::PixelGameEngine { public: MyGame() { - sAppName = "My Audio Game"; + sAppName = "My Game"; + if(!InstallSystemExtension(&audio)) - throw std::runtime_error("Failed to install olcPGEX3_miniaudio"); + throw std::runtime_error("Failed to install olcPGEX3_Miniaudio"); } -private: - olc::ext::Miniaudio::AudioEngine audio; -}; -``` - -See [olcPGE3_Miniaudio.cpp](olcPGE3_Miniaudio.cpp) for practical example usage! - ---- -## Core Components - -### olc::ext::Miniaudio::AudioEngine + bool OnUserCreate() override + { + audio.CreateSoundFromFile(music, "assets/song.mp3"); + audio.CreateSoundFromFile(jump, "assets/jump.wav"); + audio.Play(music, true); // loop the music + return true; + } -The main interface to the audio system. Manages all sounds, waveforms, and the underlying audio device. + bool OnUserUpdate(float fElapsedTime) override + { + if(keyboard.GetKey(olc::Key::SPACE).bPressed) + audio.Play(jump); -#### Configuration + return true; + } -Configure the audio engine before creation in your constructor: + olc::ext::Miniaudio::AudioEngine audio; -```cpp -olc::ext::Miniaudio::AudioEngine::Config config; -config.DeviceChannels = 2; // Mono (1) or Stereo (2) -config.DeviceSampleRate = 48000; // Sample rate in Hz -config.DeviceFormat = ma_format_f32; // Audio format (32-bit float) -config.BackgroundPlay = true; // Play when window unfocused -config.Verbose = false; // Enable verbose logging - -audio.Configure(config); +private: + olc::ext::Miniaudio::Sound music; + olc::ext::Miniaudio::Sound jump; +}; ``` -#### Background Playback +All types live in the `olc::ext::Miniaudio` namespace. -Control whether audio plays when the window loses focus: +## Configuration -```cpp -audio.EnableBackgroundPlayback(); // Continue playing in background -audio.DisableBackgroundPlayback(); // Stop when window loses focus -``` - -#### Accessing Low-Level Objects - -For advanced users who need direct access to miniaudio objects: +To change the defaults, call `Configure()` **before** the extension is installed (that is, before `InstallSystemExtension`). Calling it afterwards prints an error and has no effect. ```cpp -ma_engine& engine = audio.GetEngine(); -ma_device& device = audio.GetDevice(); -ma_resource_manager& rm = audio.GetResourceManager(); +olc::ext::Miniaudio::AudioEngine::Config cfg; +cfg.DeviceChannels = 2; +cfg.DeviceSampleRate = 44100; +cfg.BackgroundPlay = true; -int channels = audio.GetDeviceChannels(); -int sampleRate = audio.GetDeviceSampleRate(); -ma_format format = audio.GetDeviceFormat(); +audio.Configure(cfg); +InstallSystemExtension(&audio); ``` -> **Note:** In the example above `audio` is the instance of `olc::ext::Miniaudio::AudioEngine` - ---- +| Field | Default | Description | +|---|---|---| +| `DeviceChannels` | `2` | Number of output channels | +| `DeviceFormat` | `ma_format_f32` | Sample format of the device | +| `DeviceSampleRate` | `48000` | Sample rate in Hz | +| `DeviceType` | `ma_device_type_playback` | Miniaudio device type | +| `BackgroundPlay` | `false` | Keep playing when the window is unfocused | +| `Verbose` | `false` | Verbose logging | -## olc::ext::Miniaudio::Sound +Background playback can also be toggled at runtime: -Represents a playable audio resource that can be loaded from file or memory. - -### Loading Sounds - -#### From File ```cpp -olc::ext::Miniaudio::Sound mySound; - -// Load from disk -audio.CreateSoundFromFile(mySound, "path/to/sound.mp3"); // could also be .wav +audio.EnableBackgroundPlayback(); +audio.DisableBackgroundPlayback(); ``` -#### From Memory -```cpp -// Load from memory buffer -std::vector audioData = /* ... load file data ... */; -audio.CreateSoundFromMemory(mySound, audioData.data(), audioData.size()); +When background playback is off and the window loses focus, the output is silenced (sounds keep advancing, they just aren't audible). -// Or pass vector directly -audio.CreateSoundFromMemory(mySound, audioData); -``` -> **Note:** In the example above `audio` is the instance of `olc::ext::Miniaudio::AudioEngine` +## Working with sounds -### Playback Control +### Loading -#### Play ```cpp -// Play once -mySound.Play(); - -// Play with looping -mySound.Play(true); // Loops continuously -``` +olc::ext::Miniaudio::Sound sound; -> **Note For Advanced Users:** the `olc::ext::Miniaudio::Sound` has a pool of voices, the `Play` function has several optional arguments to allow you to set the **volume**, **pan**, and **pitch** of an individual voice at the moment playback begins. +// from a file on disk (on Android, loaded from the APK assets) +audio.CreateSoundFromFile(sound, "assets/explosion.wav"); -#### Stop -```cpp -// Stop and rewind to beginning -mySound.Stop(); -``` +// from memory +audio.CreateSoundFromMemory(sound, pData, nBytes); +audio.CreateSoundFromMemory(sound, vecData); -#### Pause -```cpp -// Pause without changing position -mySound.Pause(); +// optionally choose how many simultaneous voices the sound gets (default 8) +audio.CreateSoundFromFile(sound, "assets/shot.wav", 16); ``` -#### Toggle -```cpp -// Play if paused, pause if playing -mySound.Toggle(); -``` +Each loader returns `true` on success and `false` on failure. The sound is fully decoded before the call returns, so it is ready to play immediately. Check at any time with `audio.IsLoaded(sound)`. -### Seeking (Cursor Control) +Release a sound when you're done with it: -#### Seek to Position ```cpp -// Seek to specific time (milliseconds) -mySound.Seek(5000); // Go to 5 seconds - -// Seek to position (0.0 = start, 1.0 = end) -mySound.Seek(0.5f); // Go to middle of sound +audio.DestroySound(sound); ``` -#### Forward/Rewind -```cpp -// Move forward by 2 seconds -mySound.Forward(2000); - -// Move backward by 1 second -mySound.Rewind(1000); -``` +Any sounds still alive when the `AudioEngine` is destroyed are cleaned up automatically. -### Audio Effects +### Playback -#### Volume Control ```cpp -// Set volume (0.0 = mute, 1.0 = full volume) -mySound.SetVolume(0.5f); // 50% volume -mySound.SetVolume(0.0f); // Muted -mySound.SetVolume(1.0f); // Full volume +audio.Play(sound); // play once +audio.Play(sound, true); // loop +audio.Play(sound, false, 0.5f); // half volume +audio.Play(sound, false, 1.0f, -1.0f); // hard left +audio.Play(sound, false, 1.0f, 0.0f, 1.5f); // higher pitch ``` -#### Panning -```cpp -// Set pan (-1.0 = left, 0.0 = center, 1.0 = right) -mySound.SetPan(-1.0f); // Full left -mySound.SetPan(0.0f); // Center -mySound.SetPan(1.0f); // Full right -mySound.SetPan(-0.5f); // 50% left -``` +The signature is `Play(sound, looping, volume, pan, pitch)`. The default values for volume, pan and pitch are deliberately out of range (`2.0f`), which means "leave this setting as it is". Values you do pass are clamped to their valid range. -#### Pitch -```cpp -// Set pitch (1.0 = normal, 0.5 = half speed, 2.0 = double speed) -mySound.SetPitch(1.0f); // Normal speed -mySound.SetPitch(0.5f); // Half speed (lower pitch) -mySound.SetPitch(2.0f); // Double speed (higher pitch) -mySound.SetPitch(1.5f); // 1.5x speed -``` +| Function | Description | +|---|---| +| `Play(sound, looping, volume, pan, pitch)` | Start the sound from the beginning on the next voice | +| `Stop(sound)` | Stop and rewind to the beginning | +| `Pause(sound)` | Stop without changing position | +| `Toggle(sound)` | Switch between playing and paused (resumes from the paused position) | +| `Seek(sound, ms)` | Jump to a position in milliseconds | +| `Seek(sound, float)` | Jump to a position from `0.0f` (start) to `1.0f` (end) | +| `Forward(sound, ms)` / `Rewind(sound, ms)` | Move relative to the current position | +| `SetVolume(sound, v)` | `0.0f` is mute, `1.0f` is full | +| `SetPan(sound, p)` | `-1.0f` left, `0.0f` centre, `1.0f` right | +| `SetPitch(sound, p)` | `1.0f` is normal speed and pitch | +| `IsPlaying(sound)` | True if any voice of the sound is playing | +| `GetCursor(sound)` | Current position in milliseconds | +| `GetCursorFloat(sound)` | Current position as `0.0f` to `1.0f` | -### Information Queries +### How voices work -#### Playback Status -```cpp -// Check if currently playing -if(mySound.IsPlaying()) -{ - // Sound is playing -} +Every call to `Play()` moves to the next voice in a round-robin pool and starts it from the beginning. This is what lets the same sound effect overlap with itself. A few things follow from this: -// Check if loaded successfully -if(mySound.IsLoaded()) -{ - // Sound was loaded successfully -} -``` +- `Stop`, `Pause`, `Toggle`, `Seek`, `Forward`, `Rewind`, `GetCursor` and `GetCursorFloat` act on the **current** voice (the most recently started one). +- `SetVolume`, `SetPan` and `SetPitch` apply to **all** voices of the sound. +- `IsPlaying` returns true if **any** voice is playing. +- If a sound was paused, calling `Play()` reuses the paused voice and restarts it from the beginning. Use `Toggle()` to resume instead. +- When all voices are busy, the oldest is reused, so give frequently triggered effects more voices. -#### Cursor Position -```cpp -// Get cursor position in milliseconds -ma_uint64 timeMs = mySound.GetCursor(); -std::cout << "Position: " << timeMs << " ms\n"; +A music track with a progress bar, as in the demo: -// Get cursor position as float (0.0 to 1.0) -float progress = mySound.GetCursorFloat(); -std::cout << "Progress: " << (progress * 100) << "%\n"; -``` - -### Advanced Usage - -#### Direct Access to Miniaudio Sound ```cpp -// Get pointer to underlying ma_sound for advanced features -ma_sound* maSound = mySound.GetMASound(); +if(keyboard.GetKey(olc::Key::SPACE).bPressed) + audio.Toggle(song1); -// Example: Set 3D position -ma_sound_set_position(maSound, 10.0f, 0.0f, -5.0f); +float progress = audio.GetCursorFloat(song1); +draw.FilledRect({0, 350}, {ScreenSize().x * progress, 20}, olc::Colour::YELLOW); ``` ---- - -## olc::ext::Miniaudio::Waveform +> **Note:** a sound that has never had `Play()` called on it can be started with `Toggle()`, as the demo does with its music. -Generate and play procedural waveforms for synthesis and sound effects. +## Waveforms -### Supported Waveform Types +The extension includes a simple oscillator for tones, beeps and chiptune-style effects. ```cpp -enum class Waveform::Type -{ - Sine, // Smooth, pure tone - Square, // Digital, buzzy tone - Triangle, // Bright, harmonic tone - Sawtooth // Bright, harsh tone -}; -``` +olc::ext::Miniaudio::Waveform sine; -### Creating Waveforms - -```cpp -olc::ext::Miniaudio::Waveform sineWave; - -audio.CreateWaveform( - sineWave, - olc::ext::Miniaudio::Waveform::Type::Sine, // Type - 0.1, // Amplitude (0.0-1.0) - 440.0 // Frequency in Hz (A4 note) -); +// type, amplitude (0.0 - 1.0), frequency in Hz +audio.CreateWaveform(sine, olc::ext::Miniaudio::Waveform::Type::Sine, 0.1, 440.0); ``` -> **Note:** In the example above `audio` is the instance of `olc::ext::Miniaudio::AudioEngine` -### Playback Control -#### Play -```cpp -sineWave.Play(); -``` +Available types: `Sine`, `Square`, `Triangle`, `Sawtooth`. -#### Stop -```cpp -sineWave.Stop(); -``` +| Function | Description | +|---|---| +| `CreateWaveform(wave, type, amplitude, frequency)` | Create a generator. Returns `false` on failure | +| `DestroyWaveform(wave)` | Free a generator | +| `Play(wave)` / `Stop(wave)` | Fade the waveform in or out (about 20 ms ramp, so there are no clicks) | +| `SetWaveformAmplitude(wave, amp)` | Change amplitude | +| `SetWaveformFrequency(wave, hz)` | Change frequency | +| `SetWaveformType(wave, type)` | Change shape | +| `IsPlaying(wave)` / `IsLoaded(wave)` | State queries | -### Configuration +Waveforms are a natural fit for "play while key is held" behaviour. Because `Play` and `Stop` only set a target level, you can call them every frame: -#### Change Amplitude ```cpp -// Amplitude controls volume (0.0 = silent, 1.0 = loud) -sineWave.SetAmplitude(0.1); // 10% volume -sineWave.SetAmplitude(0.5); // 50% volume +audio.Stop(sine); +if(keyboard.GetKey(olc::Key::K7).bHeld) + audio.Play(sine); ``` -#### Change Frequency -```cpp -// Frequency controls pitch in Hertz -sineWave.SetFrequency(440.0); // A4 note -sineWave.SetFrequency(880.0); // One octave higher -sineWave.SetFrequency(220.0); // One octave lower -``` +## Custom audio -#### Change Type -```cpp -// Set waveform types -sineWave.SetType(olc::ext::Miniaudio::Waveform::Type::Square); -sineWave.SetType(olc::ext::Miniaudio::Waveform::Type::Triangle); -sineWave.SetType(olc::ext::Miniaudio::Waveform::Type::Sawtooth); -``` +### Synth callback -### Information Queries +For procedural audio, register a callback that is invoked once per sample frame. Whatever you write to the output channels is **added** to the mix (sounds and waveforms keep playing alongside it). ```cpp -// Check if waveform is currently playing -if(sineWave.IsPlaying()) -{ - // Waveform is playing -} - -// Check if waveform was created successfully -if(sineWave.IsLoaded()) -{ - // Waveform is ready to use -} -``` - -## Advanced Features - -### Synthesizer Callback +float phase = 0.0f; -Provide custom synthesis function for procedural audio generation: -> **HELP:** I need a sound guy to help flesh out this example so that it's actually useful. I'm not versed enough to do something actually useful here. Thanks to whoever steps up! -> -> -Moros1138 - -```cpp -class MyGame : public olc::PixelGameEngine +audio.SetSynthCallback([&](float& left, float& right, float fElapsedTime) { -private: - olc::ext::Miniaudio::AudioEngine audio; + phase += 440.0f * fElapsedTime; + float s = 0.1f * sinf(phase * 2.0f * 3.14159f); -public: - bool OnUserCreate() override - { - // Set custom synthesizer callback - audio.SetSynthCallback([this](float& fLeftChannel, float& fRightChannel, float fElapsedTime) - { - // Generate custom audio - // fElapsedTime is in seconds since last sample - float frequency = 440.0f; - float phase = fmodf(/* your phase */, 6.28f); - float sample = sinf(phase); - - fLeftChannel = sample * 0.1f; - fRightChannel = sample * 0.1f; - }); - - return true; - } - - bool OnUserUpdate(float fElapsedTime) override - { - // Clear callback if no longer needed - if(keyboard.GetKey(olc::Key::C).bPressed) - { - audio.ClearSynthCallback(); - } + left = s; + right = s; +}); - return true; - } -}; +// later +audio.ClearSynthCallback(); ``` -### Raw Audio Data Callback +`fElapsedTime` is the duration of one sample (`1 / sample rate`). Always assign both `left` and `right`. They are not pre-zeroed. + +### Raw data callback -For maximum control, provide a custom data callback: +For complete control, replace the extension's mixing entirely: ```cpp -audio.SetDataCallback([this](float* pFramesOut, ma_uint64 frameCount) +audio.SetDataCallback([](float* pFramesOut, ma_uint64 frameCount) { - // pFramesOut: pointer to output buffer - // frameCount: number of frames to fill - - // Fill the buffer with your custom audio data - for(ma_uint64 i = 0; i < frameCount * 2; ++i) // *2 for stereo - { - pFramesOut[i] = /* your audio sample */; - } + // fill pFramesOut with frameCount * channels interleaved float samples }); -// Clear callback later audio.ClearDataCallback(); ``` -> **Note:** This example doesn't do anything useful, that's up to you! ---- +While a data callback is set, the extension does **no** mixing at all. Sounds, waveforms, the synth callback, the limiter and the focus muting are all bypassed. This callback runs on the audio thread, so keep it fast and avoid allocating memory or locking. + +### Output limiter + +The final mix passes through a simple limiter that ducks the gain when the signal would exceed full scale and recovers it slowly afterwards. This keeps stacked effects from clipping harshly. Keep individual sound volumes and waveform amplitudes sensible (the demo uses `0.1` for waveforms) to leave headroom. -## Troubleshooting +## Advanced: direct miniaudio access -1. **Sound Won't Play?** - - Is the file path correct? - - Is the audio engine installed via `InstallSystemExtension(&audio)`? - - Use `IsLoaded()` to verify the sound loaded successfully - - Is the volume set to 0? +When you need something the extension doesn't wrap, you can get at the underlying miniaudio objects: -2. **Audio Crackles or Pops** - - This often indicates a limiter is catching clipping. Reduce the amplitude of waveforms or volume of sounds. Ensure the total output doesn't exceed 1.0 in amplitude +| Function | Returns | +|---|---| +| `GetMASound(sound)` | `ma_sound*` for a voice of the sound | +| `GetMAWaveform(wave)` | `ma_waveform*` | +| `GetEngine()` | `ma_engine&` | +| `GetDevice()` | `ma_device&` | +| `GetResourceManager()` | `ma_resource_manager&` | +| `GetDeviceChannels()`, `GetDeviceFormat()`, `GetDeviceSampleRate()`, `GetDeviceType()` | Current device settings | -3. **Sounds Not Working on Emscripten/Web** - - Browser audio requires user interaction before playing. Ensure playback is triggered by a user event (click, key press) +`GetMASound` returns the currently playing voice, or the *next* voice to be used if the sound isn't playing. It returns `nullptr` if the sound isn't loaded. ---- +For example, the demo uses miniaudio's 3D audio features to position a sound and move the listener, producing a distance effect: + +```cpp +ma_sound_set_position(audio.GetMASound(song1), 0.0f, 0.0f, 0.0f); +ma_engine_listener_set_position(&audio.GetEngine(), 0, 0.0f, distance, 0.0f); +``` -## Acknowledgements (From Moros1138) +Per-voice settings made through miniaudio directly only affect that one voice. For anything that should apply to every voice of a sound, prefer the extension's own functions. -I'd like to give a special thanks to JavidX9 (aka OneLoneCoder), AniCator, JustinRichardsMusic, and everybody else who was a part of that audiophile conversation when I asked for help! Your patience and feedback made this project possible. Thank you! +## Platform notes -Also, for v2 of this extension, I'd also like to single out sigonasr2 (Dense Dance 2π) for the waveform functionality and for crafting the demos for them! While the waveform and synthesis has seen some change, overall the v3 version of this extension wouldn't have this functionality were it not for sigonasr2's contributions. Thank you Sig! +- **Emscripten:** the extension configures miniaudio's resource manager for single-threaded, non-blocking operation and processes pending jobs each frame automatically. Browsers require a user gesture before audio starts, so expect silence until the user interacts with the page. +- **Android:** `CreateSoundFromFile` reads from the app's bundled assets. +- **Desktop:** files are read from disk using the path you provide, relative to the working directory. -## License +## Credits and licence -Licensed under the OLC-3 License (OneLoneCoder Public License v3) +Copyright 2023-2026 Moros Smith. Licensed under the **OLC-3** licence (see the header of `olcPGEX3_Miniaudio.h` for the full text). -Copyright 2023-2026 Moros Smith +Demo music: *Joy Ride [Full version]* by MusicLFiles, from [filmmusic.io](https://filmmusic.io/song/11627-joy-ride-full-version), licensed under [CC BY 4.0](https://filmmusic.io/standard-license). -For full license details, see the header file or original source. +Built on [miniaudio](https://miniaud.io) by David Reid. \ No newline at end of file diff --git a/extensions/miniaudio/demo_Miniaudio.cpp b/extensions/miniaudio/demo_Miniaudio.cpp index bf28506..c011ff8 100644 --- a/extensions/miniaudio/demo_Miniaudio.cpp +++ b/extensions/miniaudio/demo_Miniaudio.cpp @@ -49,7 +49,7 @@ class Example_Miniaudio : public olc::PixelGameEngine // load `assets/SampleA.wav` into `sample` audio.CreateSoundFromFile(sample, "assets/SampleA.wav"); - // create all of the waveforms at 0.1 amplitude at 440Mhz (A4) + // create all of the waveforms at 0.1 amplitude at 440hz (A4) audio.CreateWaveform(sine, olc::ext::Miniaudio::Waveform::Type::Sine, 0.1, 440.0); audio.CreateWaveform(square, olc::ext::Miniaudio::Waveform::Type::Square, 0.1, 440.0); audio.CreateWaveform(triangle, olc::ext::Miniaudio::Waveform::Type::Triangle, 0.1, 440.0); From 13aafafc0b02610bf072517c765cfa052b3b8b83 Mon Sep 17 00:00:00 2001 From: Moros Smith Date: Sun, 4 Oct 2026 14:52:08 -0400 Subject: [PATCH 10/21] [ext][miniaudio] change engine/device/resource manager getters to pointers --- extensions/miniaudio/olcPGEX3_Miniaudio.h | 26 +++++++++++++---------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/extensions/miniaudio/olcPGEX3_Miniaudio.h b/extensions/miniaudio/olcPGEX3_Miniaudio.h index acc1cd6..9066afb 100644 --- a/extensions/miniaudio/olcPGEX3_Miniaudio.h +++ b/extensions/miniaudio/olcPGEX3_Miniaudio.h @@ -109,6 +109,7 @@ namespace olc::ext::Miniaudio friend class AudioEngine; private: internal::SoundInstance* sound{nullptr}; + AudioEngine* pgex{nullptr}; }; class Waveform @@ -124,6 +125,7 @@ namespace olc::ext::Miniaudio }; private: internal::WaveformInstance* waveform{nullptr}; + AudioEngine* pgex{nullptr}; }; class AudioEngine : public olc::PGESystemExtension @@ -226,10 +228,9 @@ namespace olc::ext::Miniaudio void ClearDataCallback(); public: // getters - // - ma_device& GetDevice(); - ma_engine& GetEngine(); - ma_resource_manager& GetResourceManager(); + ma_device* GetDevice(); + ma_engine* GetEngine(); + ma_resource_manager* GetResourceManager(); int GetDeviceChannels() const; ma_format GetDeviceFormat() const; @@ -240,7 +241,6 @@ namespace olc::ext::Miniaudio AudioEngine(); ~AudioEngine(); - virtual bool OnInstall([[maybe_unused]] olc::PixelGameEngine* pge); virtual bool OnBeforeUserCreate([[maybe_unused]] olc::PixelGameEngine* pge); virtual bool OnAfterUserCreate([[maybe_unused]] olc::PixelGameEngine* pge); @@ -463,6 +463,7 @@ namespace olc::ext::Miniaudio f.close(); #endif sound.sound->num_voices = nNumVoices; + sound.pgex = this; return _internalSoundLoader(sound); } @@ -483,6 +484,7 @@ namespace olc::ext::Miniaudio } sound.sound->num_voices = nNumVoices; + sound.pgex = this; return _internalSoundLoader(sound); } @@ -492,6 +494,7 @@ namespace olc::ext::Miniaudio if(data.size() <= 0) return false; sound.sound->buffer = data; sound.sound->num_voices = nNumVoices; + sound.pgex = this; return _internalSoundLoader(sound); } @@ -782,6 +785,7 @@ namespace olc::ext::Miniaudio } waveform.waveform->is_loaded = true; + waveform.pgex = this; m_waveforms.push_back(waveform.waveform); return true; } @@ -868,19 +872,19 @@ namespace olc::ext::Miniaudio m_data_callback = {}; } - ma_device& AudioEngine::GetDevice() + ma_device* AudioEngine::GetDevice() { - return m_device; + return &m_device; } - ma_engine& AudioEngine::GetEngine() + ma_engine* AudioEngine::GetEngine() { - return m_engine; + return &m_engine; } - ma_resource_manager& AudioEngine::GetResourceManager() + ma_resource_manager* AudioEngine::GetResourceManager() { - return m_resource_manager; + return &m_resource_manager; } int AudioEngine::GetDeviceChannels() const From 3d163017b5d52862db5b0a7eed8b7f01ed782dc5 Mon Sep 17 00:00:00 2001 From: Moros Smith Date: Sun, 4 Oct 2026 15:01:08 -0400 Subject: [PATCH 11/21] [ext][miniaudio] fix broken getter usage. --- extensions/miniaudio/demo_Miniaudio.cpp | 2 +- extensions/miniaudio/olcPGEX3_Miniaudio.h | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/extensions/miniaudio/demo_Miniaudio.cpp b/extensions/miniaudio/demo_Miniaudio.cpp index c011ff8..9e1dcbe 100644 --- a/extensions/miniaudio/demo_Miniaudio.cpp +++ b/extensions/miniaudio/demo_Miniaudio.cpp @@ -152,7 +152,7 @@ class Example_Miniaudio : public olc::PixelGameEngine // this is here to demosntrate how the adventurous can exploit other // features of miniaudio that haven't been abstracted by the PGEX. distance = std::clamp(distance, 0.0f, 100.0f); - ma_engine_listener_set_position(&audio.GetEngine(), 0, 0.0f, distance, 0.0f); + ma_engine_listener_set_position(audio.GetEngine(), 0, 0.0f, distance, 0.0f); // // get float cursor. 0.0f to 1.0f cursorFloat = audio.GetCursorFloat(song1); diff --git a/extensions/miniaudio/olcPGEX3_Miniaudio.h b/extensions/miniaudio/olcPGEX3_Miniaudio.h index 9066afb..4de38f6 100644 --- a/extensions/miniaudio/olcPGEX3_Miniaudio.h +++ b/extensions/miniaudio/olcPGEX3_Miniaudio.h @@ -523,7 +523,7 @@ namespace olc::ext::Miniaudio } result = ma_sound_init_from_file( - &GetEngine(), + GetEngine(), sound.sound->virtual_path.c_str(), MA_SOUND_FLAG_DECODE, nullptr, @@ -540,7 +540,7 @@ namespace olc::ext::Miniaudio sound.sound->voices.resize(sound.sound->num_voices); for(int i = 0; i < sound.sound->num_voices; ++i) { - result = ma_sound_init_copy(&GetEngine(), &sound.sound->base_sound, 0, nullptr, &sound.sound->voices[i]); + result = ma_sound_init_copy(GetEngine(), &sound.sound->base_sound, 0, nullptr, &sound.sound->voices[i]); if(result != MA_SUCCESS) break; } From 43b1ff32e0730a15a5a9a07280af32b25387e19f Mon Sep 17 00:00:00 2001 From: Moros Smith Date: Sun, 4 Oct 2026 22:35:29 -0400 Subject: [PATCH 12/21] [ext][miniaudio] add sound groups --- extensions/miniaudio/olcPGEX3_Miniaudio.h | 196 +++++++++++++++++++++- 1 file changed, 190 insertions(+), 6 deletions(-) diff --git a/extensions/miniaudio/olcPGEX3_Miniaudio.h b/extensions/miniaudio/olcPGEX3_Miniaudio.h index 4de38f6..59465b8 100644 --- a/extensions/miniaudio/olcPGEX3_Miniaudio.h +++ b/extensions/miniaudio/olcPGEX3_Miniaudio.h @@ -76,6 +76,13 @@ namespace olc::ext::Miniaudio namespace internal { + struct SoundGroupInstance + { + bool is_loaded{false}; + ma_sound_group group; + ma_sound_group_config group_config; + }; + struct SoundInstance { std::vector buffer; @@ -101,8 +108,15 @@ namespace olc::ext::Miniaudio ma_waveform waveform; ma_waveform_config waveform_config; }; - } + + class SoundGroup + { + friend class AudioEngine; + private: + internal::SoundGroupInstance* group{nullptr}; + AudioEngine* pgex{nullptr}; + }; class Sound { @@ -155,7 +169,30 @@ namespace olc::ext::Miniaudio public: // Callback static void data_callback(ma_device* pDevice, void* pOutput, const void* pInput, ma_uint32 frameCount); - + + public: // SoundGroup + // Create a sound group + bool CreateSoundGroup(SoundGroup& group); + void DestroySoundGroup(SoundGroup& group); + // set the SoundGroup that a Sound belongs to + bool SetGroup(Sound& sound, SoundGroup& group); + + public: // SoundGroup Playback and Controls + // plays a sound group, can set volume, pan, or pitch (note: default values are out of range deliberately) + void Play(SoundGroup& group, const float volume = 2.0f, const float pan = 2.0f, const float pitch = 2.0f); + // stops playback of a soundgroup + void Stop(SoundGroup& group); + // set volume of a sound, 0.0f is mute, 1.0f is full + void SetVolume(SoundGroup& group, const float& volume); + // set pan of a sound, -1.0f is left, 1.0f is right, 0.0f is center + void SetPan(SoundGroup& group, const float& pan); + // set pitch of a sound, 1.0f is normal + void SetPitch(SoundGroup& group, const float& pitch); + // determine if this SoundGroup is playing + bool IsPlaying(SoundGroup& group) const; + // determine if this SoundGroup is loaded + bool IsLoaded(SoundGroup& group) const; + public: // Sounds // Create a sound resource based on a sound file asset on disk bool CreateSoundFromFile(Sound& sound, const std::string& sFileName, uint32_t nNumVoices = 8); @@ -237,6 +274,8 @@ namespace olc::ext::Miniaudio int GetDeviceSampleRate() const; ma_device_type GetDeviceType() const; + SoundGroup& GetMainGroup(); + public: AudioEngine(); ~AudioEngine(); @@ -264,7 +303,11 @@ namespace olc::ext::Miniaudio // data callback function std::function m_data_callback; - // track sounds and waveforms + // the main sound group + SoundGroup m_main_sound_group; + + // track SoundGroup, Sound and Waveforms instances + std::vector m_sound_groups; std::vector m_sounds; std::vector m_waveforms; @@ -314,6 +357,14 @@ namespace olc::ext::Miniaudio } m_waveforms.clear(); + for(auto& g : m_sound_groups) + { + if(g == nullptr) continue; + if(!g->is_loaded) continue; + ma_sound_group_uninit(&g->group); + } + m_sound_groups.clear(); + ma_resource_manager_uninit(&m_resource_manager); ma_engine_stop(&m_engine); @@ -435,6 +486,126 @@ namespace olc::ext::Miniaudio } } + bool AudioEngine::CreateSoundGroup(SoundGroup& group) + { + // This group has already been created + if(group.group != nullptr) + return false; + + group.group = new internal::SoundGroupInstance(); + group.pgex = this; + + group.group->group_config = ma_sound_config_init(); + + // set parent to the main group, if it's been created, otherwise, we're creating the main group + ma_sound_group* parent = nullptr; + if(!m_sound_groups.empty()) + parent = &m_sound_groups[0]->group; + + ma_result result = ma_sound_group_init(GetEngine(), 0, parent, &group.group->group); + + if(result != MA_SUCCESS) + { + delete group.group; + } + + m_sound_groups.push_back(group.group); + group.group->is_loaded = true; + + return group.group->is_loaded; + } + + void AudioEngine::DestroySoundGroup(SoundGroup& group) + { + if(group.group == nullptr) return; + if(!group.group->is_loaded) return; + + ma_sound_group_uninit(&group.group->group); + group.group->is_loaded = false; + delete group.group; + group.group = nullptr; + } + + bool AudioEngine::SetGroup(Sound& sound, SoundGroup& group) + { + if(sound.sound == nullptr) return false; + if(!sound.sound->is_loaded) return false; + if(group.group == nullptr) return false; + if(!group.group->is_loaded) return false; + + ma_result result = ma_node_attach_output_bus(&sound.sound->base_sound, 0, &group.group->group, 0); + if(result != MA_SUCCESS) + throw std::runtime_error("Failed to attach a sound to a sound group"); + + for(auto& v : sound.sound->voices) + { + result = ma_node_attach_output_bus(&v, 0, &group.group->group, 0); + if(result != MA_SUCCESS) + throw std::runtime_error("Failed to attach a sound to a sound group"); + } + return true; + } + + void AudioEngine::Play(SoundGroup& group, const float volume, const float pan, const float pitch) + { + if(group.group == nullptr) return; + if(!group.group->is_loaded) return; + if(ma_sound_group_is_playing(&group.group->group)) return; + + if(volume != 2.0f) + ma_sound_group_set_volume(&group.group->group, std::clamp(volume, 0.0f, 1.0f)); + + if(pan != 2.0f) + ma_sound_group_set_pan(&group.group->group, std::clamp(pan, -1.0f, 1.0f)); + + if(pitch != 2.0f) + ma_sound_group_set_pitch(&group.group->group, std::max({0.0f, pitch})); + + ma_sound_group_start(&group.group->group); + } + + void AudioEngine::Stop(SoundGroup& group) + { + if(group.group == nullptr) return; + if(!group.group->is_loaded) return; + if(!ma_sound_group_is_playing(&group.group->group)) return; + ma_sound_group_stop(&group.group->group); + } + + void AudioEngine::SetVolume(SoundGroup& group, const float& volume) + { + if(group.group == nullptr) return; + if(!group.group->is_loaded) return; + ma_sound_group_set_volume(&group.group->group, std::clamp(volume, 0.0f, 1.0f)); + } + + void AudioEngine::SetPan(SoundGroup& group, const float& pan) + { + if(group.group == nullptr) return; + if(!group.group->is_loaded) return; + ma_sound_group_set_pan(&group.group->group, std::clamp(pan, -1.0f, 1.0f)); + } + + void AudioEngine::SetPitch(SoundGroup& group, const float& pitch) + { + if(group.group == nullptr) return; + if(!group.group->is_loaded) return; + ma_sound_group_set_pitch(&group.group->group, std::max({0.0f, pitch})); + } + + bool AudioEngine::IsPlaying(SoundGroup& group) const + { + if(group.group == nullptr) return false; + if(!group.group->is_loaded) return false; + return ma_sound_group_is_playing(&group.group->group); + } + + bool AudioEngine::IsLoaded(SoundGroup& group) const + { + if(group.group == nullptr) return false; + return group.group->is_loaded; + } + bool AudioEngine::CreateSoundFromFile(Sound& sound, const std::string& sFileName, uint32_t nNumVoices) { sound.sound = new internal::SoundInstance(); @@ -521,12 +692,12 @@ namespace olc::ext::Miniaudio ma_resource_manager_unregister_data(&m_resource_manager, sound.sound->virtual_path.c_str()); return false; } - + result = ma_sound_init_from_file( GetEngine(), sound.sound->virtual_path.c_str(), MA_SOUND_FLAG_DECODE, - nullptr, + &GetMainGroup().group->group, &fence, &sound.sound->base_sound ); @@ -540,7 +711,7 @@ namespace olc::ext::Miniaudio sound.sound->voices.resize(sound.sound->num_voices); for(int i = 0; i < sound.sound->num_voices; ++i) { - result = ma_sound_init_copy(GetEngine(), &sound.sound->base_sound, 0, nullptr, &sound.sound->voices[i]); + result = ma_sound_init_copy(GetEngine(), &sound.sound->base_sound, 0, &GetMainGroup().group->group, &sound.sound->voices[i]); if(result != MA_SUCCESS) break; } @@ -907,6 +1078,11 @@ namespace olc::ext::Miniaudio return m_cfg.DeviceType; } + SoundGroup& AudioEngine::GetMainGroup() + { + return m_main_sound_group; + } + bool AudioEngine::OnInstall([[maybe_unused]] olc::PixelGameEngine* pge) { m_pge = pge; @@ -950,6 +1126,14 @@ namespace olc::ext::Miniaudio std::cerr << "PGEX3_Miniaudio: failed to initialize engine\n"; return false; } + + // group 0 + if(!CreateSoundGroup(m_main_sound_group)) + { + std::cerr << "PGEX3_Miniaudio: failed to create main sound group\n"; + return false; + } + m_is_initialized = true; return true; } From eb1aa48c625c450875b7ce581f184fe97010f7c6 Mon Sep 17 00:00:00 2001 From: Moros Smith Date: Sun, 4 Oct 2026 22:52:57 -0400 Subject: [PATCH 13/21] [ext][miniaudio] update docs to reflect ChAnGe --- extensions/miniaudio/README.md | 71 ++++++++++++++++++++++++++++++++-- 1 file changed, 67 insertions(+), 4 deletions(-) diff --git a/extensions/miniaudio/README.md b/extensions/miniaudio/README.md index 47f40b7..7c5e078 100644 --- a/extensions/miniaudio/README.md +++ b/extensions/miniaudio/README.md @@ -9,6 +9,7 @@ Because it sits on miniaudio, it needs next to no extra build configuration to w - **Sound loading** from disk or from memory (WAV and MP3, plus anything your miniaudio build can decode) - **Polyphonic playback**: each sound has a pool of voices (default 8), so rapid-fire sound effects overlap instead of cutting each other off - **Per-sound controls**: volume, pan, pitch, looping, seek, forward, rewind, pause, toggle +- **Sound groups**: bundle sounds into groups (music, effects, UI...) and control volume, pan and pitch for the whole group at once, plus a built-in main group that doubles as a master volume - **Playback position** queries in milliseconds or as a normalised float (handy for progress bars) - **Waveform generators**: sine, square, triangle and sawtooth, with adjustable amplitude, frequency and type, and a short fade in/out to avoid clicks - **Synth callback**: generate or process audio sample by sample @@ -192,6 +193,65 @@ draw.FilledRect({0, 350}, {ScreenSize().x * progress, 20}, olc::Colour::YELLOW); > **Note:** a sound that has never had `Play()` called on it can be started with `Toggle()`, as the demo does with its music. +## Sound groups + +A sound group lets you control several sounds together. Typical uses are separate volume sliders for music and sound effects, or ducking all effects while a menu is open. + +```cpp +olc::ext::Miniaudio::SoundGroup musicGroup; +olc::ext::Miniaudio::SoundGroup sfxGroup; + +// create the groups (do this after the extension is installed, e.g. in OnUserCreate) +audio.CreateSoundGroup(musicGroup); +audio.CreateSoundGroup(sfxGroup); + +// assign already-loaded sounds to a group +audio.SetGroup(song1, musicGroup); +audio.SetGroup(jump, sfxGroup); +audio.SetGroup(coin, sfxGroup); + +// now control them together +audio.SetVolume(musicGroup, 0.4f); +audio.SetVolume(sfxGroup, 0.8f); +audio.SetPan(sfxGroup, -0.25f); +``` + +| Function | Description | +|---|---| +| `CreateSoundGroup(group)` | Create a group. Returns `false` on failure, or if that `SoundGroup` object has already been created | +| `DestroySoundGroup(group)` | Free a group | +| `SetGroup(sound, group)` | Route all voices of a sound through the group. Returns `false` if the sound or group isn't loaded | +| `SetVolume(group, v)` | `0.0f` is mute, `1.0f` is full | +| `SetPan(group, p)` | `-1.0f` left, `0.0f` centre, `1.0f` right | +| `SetPitch(group, p)` | `1.0f` is normal | +| `Play(group, volume, pan, pitch)` | Start the group, optionally applying volume, pan and pitch first (same out-of-range "leave unchanged" defaults as sounds) | +| `Stop(group)` | Stop the group | +| `IsPlaying(group)` / `IsLoaded(group)` | State queries | +| `GetMainGroup()` | The built-in main group (see below) | + +### The main group + +When the engine is installed it creates a **main group**, and every sound you load is attached to it by default. Every group you create is a child of the main group, so audio flows like this: + +``` +sound -> its group -> main group -> output device +``` + +The volume, pan and pitch at each stage are combined, which makes the main group a convenient master control: + +```cpp +// master volume +audio.SetVolume(audio.GetMainGroup(), 0.5f); +``` + +### Things to know + +- `Play(group)` does nothing if the group is already playing, and in that case the volume, pan and pitch arguments are **not** applied either. To change settings on a running group, use `SetVolume`, `SetPan` or `SetPitch`. +- `Stop(group)` stops the group's output, so everything routed through it goes quiet. Call `Play(group)` to bring it back. +- `SetGroup` applies to every voice of the sound. It throws `std::runtime_error` if miniaudio fails to make the connection. +- Before destroying a group, move its sounds elsewhere (for example `audio.SetGroup(sound, audio.GetMainGroup())`) so nothing is left routed through a freed group. Don't destroy the main group. +- Groups only affect **sounds**. Waveforms, the synth callback and the raw data callback are mixed in separately, so they are not affected by group volume, pan or pitch (or by the main group). + ## Waveforms The extension includes a simple oscillator for tones, beeps and chiptune-style effects. @@ -274,10 +334,13 @@ When you need something the extension doesn't wrap, you can get at the underlyin |---|---| | `GetMASound(sound)` | `ma_sound*` for a voice of the sound | | `GetMAWaveform(wave)` | `ma_waveform*` | -| `GetEngine()` | `ma_engine&` | -| `GetDevice()` | `ma_device&` | -| `GetResourceManager()` | `ma_resource_manager&` | +| `GetEngine()` | `ma_engine*` | +| `GetDevice()` | `ma_device*` | +| `GetResourceManager()` | `ma_resource_manager*` | | `GetDeviceChannels()`, `GetDeviceFormat()`, `GetDeviceSampleRate()`, `GetDeviceType()` | Current device settings | +| `GetMainGroup()` | `SoundGroup&` for the main group | + +> **Note:** `GetEngine()`, `GetDevice()` and `GetResourceManager()` return **pointers**. Pass them straight to miniaudio functions without the `&` operator. `GetMASound` returns the currently playing voice, or the *next* voice to be used if the sound isn't playing. It returns `nullptr` if the sound isn't loaded. @@ -285,7 +348,7 @@ For example, the demo uses miniaudio's 3D audio features to position a sound and ```cpp ma_sound_set_position(audio.GetMASound(song1), 0.0f, 0.0f, 0.0f); -ma_engine_listener_set_position(&audio.GetEngine(), 0, 0.0f, distance, 0.0f); +ma_engine_listener_set_position(audio.GetEngine(), 0, 0.0f, distance, 0.0f); ``` Per-voice settings made through miniaudio directly only affect that one voice. For anything that should apply to every voice of a sound, prefer the extension's own functions. From b921601183bc733597c69b13855071ebd3e38bf4 Mon Sep 17 00:00:00 2001 From: Moros Smith Date: Tue, 6 Oct 2026 07:08:52 -0400 Subject: [PATCH 14/21] [ext][miniaudio][cmake] build all demos --- extensions/miniaudio/CMakeLists.txt | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/extensions/miniaudio/CMakeLists.txt b/extensions/miniaudio/CMakeLists.txt index 8846aeb..1bc4cb0 100644 --- a/extensions/miniaudio/CMakeLists.txt +++ b/extensions/miniaudio/CMakeLists.txt @@ -9,7 +9,12 @@ FetchContent_Declare( FetchContent_MakeAvailable(miniaudio) -add_example(ext_demo_Miniaudio demo_Miniaudio.cpp) +file(GLOB SOURCES "*.cpp") -target_include_directories(ext_demo_Miniaudio PRIVATE "${CMAKE_BINARY_DIR}/_deps/miniaudio-src") -handle_assets(ext_demo_Miniaudio ${CMAKE_CURRENT_SOURCE_DIR}/assets) +# loop through each source file +foreach(source ${SOURCES}) + get_filename_component(EXE_NAME ${source} NAME_WE) + add_example(${EXE_NAME} ${source}) + target_include_directories(${EXE_NAME} PRIVATE "${CMAKE_BINARY_DIR}/_deps/miniaudio-src") + handle_assets(${EXE_NAME} ${CMAKE_CURRENT_SOURCE_DIR}/assets) +endforeach() From b45f931cfeb98e28ecd0f235bb791aef549886da Mon Sep 17 00:00:00 2001 From: Moros Smith Date: Tue, 6 Oct 2026 07:09:31 -0400 Subject: [PATCH 15/21] [ext][miniaudio] adjust waveform rampStep to 0.01f . formatting --- extensions/miniaudio/olcPGEX3_Miniaudio.h | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/extensions/miniaudio/olcPGEX3_Miniaudio.h b/extensions/miniaudio/olcPGEX3_Miniaudio.h index 59465b8..641b43f 100644 --- a/extensions/miniaudio/olcPGEX3_Miniaudio.h +++ b/extensions/miniaudio/olcPGEX3_Miniaudio.h @@ -946,7 +946,7 @@ namespace olc::ext::Miniaudio frequency ); - waveform.waveform->rampStep = 1.0f / (GetDeviceSampleRate() * 0.02f); + waveform.waveform->rampStep = 1.0f / (GetDeviceSampleRate() * 0.01f); if(ma_waveform_init(&waveform.waveform->waveform_config, &waveform.waveform->waveform) != MA_SUCCESS) { @@ -1088,7 +1088,7 @@ namespace olc::ext::Miniaudio m_pge = pge; m_device_config = ma_device_config_init(GetDeviceType()); - m_device_config.playback.format = GetDeviceFormat(); + m_device_config.playback.format = GetDeviceFormat(); m_device_config.playback.channels = GetDeviceChannels(); m_device_config.sampleRate = GetDeviceSampleRate(); m_device_config.dataCallback = AudioEngine::data_callback; @@ -1100,10 +1100,10 @@ namespace olc::ext::Miniaudio return false; } - m_resource_manager_config = ma_resource_manager_config_init(); - m_resource_manager_config.decodedFormat = GetDeviceFormat(); - m_resource_manager_config.decodedChannels = GetDeviceChannels(); - m_resource_manager_config.decodedSampleRate = GetDeviceSampleRate(); + m_resource_manager_config = ma_resource_manager_config_init(); + m_resource_manager_config.decodedFormat = GetDeviceFormat(); + m_resource_manager_config.decodedChannels = GetDeviceChannels(); + m_resource_manager_config.decodedSampleRate = GetDeviceSampleRate(); #ifdef __EMSCRIPTEN__ m_resource_manager_config.jobThreadCount = 0; @@ -1118,7 +1118,7 @@ namespace olc::ext::Miniaudio } m_engine_config = ma_engine_config_init(); - m_engine_config.pDevice = &m_device; + m_engine_config.pDevice = &m_device; m_engine_config.pResourceManager = &m_resource_manager; if(ma_engine_init(&m_engine_config, &m_engine) != MA_SUCCESS) From 9c0972cd27845fa9e9da1b80f69add4d5d6b418f Mon Sep 17 00:00:00 2001 From: Moros Smith Date: Thu, 8 Oct 2026 19:44:51 -0400 Subject: [PATCH 16/21] [ext][miniaudio] add explicit values and Count to WaveForm::Type enum --- extensions/miniaudio/olcPGEX3_Miniaudio.h | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/extensions/miniaudio/olcPGEX3_Miniaudio.h b/extensions/miniaudio/olcPGEX3_Miniaudio.h index 641b43f..8ba6738 100644 --- a/extensions/miniaudio/olcPGEX3_Miniaudio.h +++ b/extensions/miniaudio/olcPGEX3_Miniaudio.h @@ -132,10 +132,11 @@ namespace olc::ext::Miniaudio public: enum class Type { - Sine, - Square, - Triangle, - Sawtooth + Sine = 0, + Square = 1, + Triangle = 2, + Sawtooth = 3, + Count }; private: internal::WaveformInstance* waveform{nullptr}; From 65a4957d01ce675f337156b0e0d7082a8f1b33ba Mon Sep 17 00:00:00 2001 From: Moros Smith Date: Thu, 8 Oct 2026 19:45:03 -0400 Subject: [PATCH 17/21] [ext][miniaudio][demo] add waveform demo --- extensions/miniaudio/demo_Waveforms.cpp | 185 ++++++++++++++++++++++++ 1 file changed, 185 insertions(+) create mode 100644 extensions/miniaudio/demo_Waveforms.cpp diff --git a/extensions/miniaudio/demo_Waveforms.cpp b/extensions/miniaudio/demo_Waveforms.cpp new file mode 100644 index 0000000..0ebf51d --- /dev/null +++ b/extensions/miniaudio/demo_Waveforms.cpp @@ -0,0 +1,185 @@ +/* + olc::PixelGameEngine3 Example - olcPGEX3_Miniaudio + + Demonstrates using olcPGEX3_Miniaudio + + Licenced under the OLC-3 License +*/ + + +// Define OLC_PGE3_APPLICATION to include the implementation of +// the Pixel Game Engine as part of this translation unit +#define OLC_PGE3_APPLICATION +#include "olcPixelGameEngine3.h" + +#define OLC_PGEX3_MINIAUDIO +#include "olcPGEX3_Miniaudio.h" + +// Example application demonstrating the miniaudio extension. +// This class overrides the olc::PixelGameEngine base class +// by implementing the OnUserCreate() and OnUserUpdate() +// functions +class Example_Waveforms : public olc::PixelGameEngine +{ +public: + Example_Waveforms() + { + sAppName = "Example - olcPGEX3_Miniaudio"; + if(!InstallSystemExtension(&audio)) + throw std::runtime_error("Failed to install olcPGEX3_Miniaudio"); + } + +public: + // Called once at the start, so create things here + bool OnUserCreate() override + { + { + using namespace olc::ext::Miniaudio; + auto CreateWaveform = [&](olc::Key key, Waveform::Type type, double frequency) + { + Waveform w; + audio.CreateWaveform(w, type, 0.01, frequency); + mapKeys[key] = w; + }; + + CreateWaveform(olc::Key::A, Waveform::Type::Sine, 207.65f); + CreateWaveform(olc::Key::Z, Waveform::Type::Sine, 220.00f); + CreateWaveform(olc::Key::S, Waveform::Type::Sine, 233.08f); + CreateWaveform(olc::Key::X, Waveform::Type::Sine, 246.94f); + CreateWaveform(olc::Key::C, Waveform::Type::Sine, 261.63f); + CreateWaveform(olc::Key::F, Waveform::Type::Sine, 277.18f); + CreateWaveform(olc::Key::V, Waveform::Type::Sine, 293.66f); + CreateWaveform(olc::Key::G, Waveform::Type::Sine, 311.13f); + CreateWaveform(olc::Key::B, Waveform::Type::Sine, 329.63f); + CreateWaveform(olc::Key::N, Waveform::Type::Sine, 349.23f); + CreateWaveform(olc::Key::J, Waveform::Type::Sine, 369.99f); + CreateWaveform(olc::Key::M, Waveform::Type::Sine, 392.00f); + CreateWaveform(olc::Key::K, Waveform::Type::Sine, 415.30f); + CreateWaveform(olc::Key::COMMA, Waveform::Type::Sine, 440.00f); + CreateWaveform(olc::Key::L, Waveform::Type::Sine, 466.16f); + CreateWaveform(olc::Key::PERIOD, Waveform::Type::Sine, 493.88f); + CreateWaveform(olc::Key::OEM_2, Waveform::Type::Sine, 523.25f); + } + + return true; + } + + // Called every frame, so update things here + bool OnUserUpdate(float fElapsedTime) override + { + if(keyboard.GetKey(olc::Key::LEFT).bPressed) + { + typeTracker = ((typeTracker + (int)olc::ext::Miniaudio::Waveform::Type::Count) - 1) % (int)olc::ext::Miniaudio::Waveform::Type::Count; + for(auto& i : mapKeys) + audio.SetWaveformType(i.second, (olc::ext::Miniaudio::Waveform::Type)typeTracker); + } + + if(keyboard.GetKey(olc::Key::RIGHT).bPressed) + { + typeTracker = (typeTracker + 1) % (int)olc::ext::Miniaudio::Waveform::Type::Count; + for(auto& i : mapKeys) + audio.SetWaveformType(i.second, (olc::ext::Miniaudio::Waveform::Type)typeTracker); + } + + for(auto& i : mapKeys) + { + audio.Stop(i.second); + if(keyboard.GetKey(i.first).bHeld) + audio.Play(i.second); + } + + draw.Clear(olc::Colour::VERY_DARK_BLUE); + + olc::vi2d center = ScreenSize() / 2; + olc::vf2d scale{2.3f, 2.3f}; + std::string demoMessage = "olcPGEX3_miniaudio Waveform Demo"; + + draw.String( + center - olc::vi2d{0, 32} - (draw.GetTextSize(demoMessage, false, scale) / 2), + demoMessage, + olc::Colour::WHITE, + scale + ); + + scale = {1.5f, 1.5f}; + demoMessage = "Hit or To Change Waveform Type"; + draw.String( + center - (draw.GetTextSize(demoMessage, false, scale) / 2), + demoMessage, + olc::Colour::WHITE, + scale + ); + + demoMessage = "Current Wave (" + waveformTypeToName.at((olc::ext::Miniaudio::Waveform::Type)typeTracker) + ")"; + draw.String( + center + olc::vi2d{0, 24} - (draw.GetTextSize(demoMessage, false, scale) / 2), + demoMessage, + olc::Colour::WHITE, + scale + ); + + olc::vf2d pianoStrSize{draw.GetTextSize(piano)}; + draw.String( + olc::vf2d{ + (ScreenSize().x / 2) - (pianoStrSize.x / 2), + ScreenSize().y - pianoStrSize.y + }, + piano, + olc::Colour::WHITE + ); + +#if OLC_HOST == OLC_HOST_EMSCRIPTEN + return true; +#else + return !keyboard.GetKey(olc::Key::ESCAPE).bPressed; +#endif + } + + // put this here to have access to audio! + olc::ext::Miniaudio::AudioEngine audio; + +private: + int typeTracker = 0; + + const std::unordered_map waveformTypeToName + { + {olc::ext::Miniaudio::Waveform::Type::Sine, "SINE"}, + {olc::ext::Miniaudio::Waveform::Type::Square, "SQUARE"}, + {olc::ext::Miniaudio::Waveform::Type::Triangle, "TRIANGLE"}, + {olc::ext::Miniaudio::Waveform::Type::Sawtooth, "SAWTOOTH"}, + }; + + std::unordered_map mapKeys; + + const std::string piano{ + " | | | | | | | | | | | | | | | | |\n" + "A | | S | | | F | | G | | | J | | K | | L | | |\n" + "__| |___| | |___| |___| | |___| |___| |___| | |__\n" + "| | | | | | | | | | |\n" + "| Z | X | C | V | B | N | M | , | . | / |\n" + "|_____|_____|_____|_____|_____|_____|_____|_____|_____|_____|" + }; +}; + + +// Main entry point for the application +int main() +{ + // Construct demo application + Example_Waveforms demo; + + // Create "screen" of 640x360 "pixels" + // with a pixel size of 2x2 actual screen pixels + PGEConfig config; + config.bVSync = false; + config.vPixelSize = { 2,2 }; + config.vScreenSize = { 640,360 }; + + if (demo.Construct(config)) + { + // Start the application + demo.Start(); + } + + return 0; +} From 9c22fb885f4c4c9751ea1f5087bbe97e7eb033cb Mon Sep 17 00:00:00 2001 From: Moros Smith Date: Fri, 9 Oct 2026 04:52:09 -0400 Subject: [PATCH 18/21] [ext][miniaudio][demo] a proper waveform demo, with instructions --- extensions/miniaudio/demo_Waveforms.cpp | 77 ++++++++++++------------- 1 file changed, 38 insertions(+), 39 deletions(-) diff --git a/extensions/miniaudio/demo_Waveforms.cpp b/extensions/miniaudio/demo_Waveforms.cpp index 0ebf51d..1036ce4 100644 --- a/extensions/miniaudio/demo_Waveforms.cpp +++ b/extensions/miniaudio/demo_Waveforms.cpp @@ -15,6 +15,9 @@ #define OLC_PGEX3_MINIAUDIO #include "olcPGEX3_Miniaudio.h" +// Save us a whole lot of typing in this demo +using namespace olc::ext::Miniaudio; + // Example application demonstrating the miniaudio extension. // This class overrides the olc::PixelGameEngine base class // by implementing the OnUserCreate() and OnUserUpdate() @@ -33,33 +36,30 @@ class Example_Waveforms : public olc::PixelGameEngine // Called once at the start, so create things here bool OnUserCreate() override { + auto CreateWaveform = [&](olc::Key key, Waveform::Type type, double frequency) { - using namespace olc::ext::Miniaudio; - auto CreateWaveform = [&](olc::Key key, Waveform::Type type, double frequency) - { - Waveform w; - audio.CreateWaveform(w, type, 0.01, frequency); - mapKeys[key] = w; - }; - - CreateWaveform(olc::Key::A, Waveform::Type::Sine, 207.65f); - CreateWaveform(olc::Key::Z, Waveform::Type::Sine, 220.00f); - CreateWaveform(olc::Key::S, Waveform::Type::Sine, 233.08f); - CreateWaveform(olc::Key::X, Waveform::Type::Sine, 246.94f); - CreateWaveform(olc::Key::C, Waveform::Type::Sine, 261.63f); - CreateWaveform(olc::Key::F, Waveform::Type::Sine, 277.18f); - CreateWaveform(olc::Key::V, Waveform::Type::Sine, 293.66f); - CreateWaveform(olc::Key::G, Waveform::Type::Sine, 311.13f); - CreateWaveform(olc::Key::B, Waveform::Type::Sine, 329.63f); - CreateWaveform(olc::Key::N, Waveform::Type::Sine, 349.23f); - CreateWaveform(olc::Key::J, Waveform::Type::Sine, 369.99f); - CreateWaveform(olc::Key::M, Waveform::Type::Sine, 392.00f); - CreateWaveform(olc::Key::K, Waveform::Type::Sine, 415.30f); - CreateWaveform(olc::Key::COMMA, Waveform::Type::Sine, 440.00f); - CreateWaveform(olc::Key::L, Waveform::Type::Sine, 466.16f); - CreateWaveform(olc::Key::PERIOD, Waveform::Type::Sine, 493.88f); - CreateWaveform(olc::Key::OEM_2, Waveform::Type::Sine, 523.25f); - } + Waveform w; + audio.CreateWaveform(w, type, 0.01, frequency); + mapKeys[key] = w; + }; + + CreateWaveform(olc::Key::A, Waveform::Type::Sine, 207.65f); + CreateWaveform(olc::Key::Z, Waveform::Type::Sine, 220.00f); + CreateWaveform(olc::Key::S, Waveform::Type::Sine, 233.08f); + CreateWaveform(olc::Key::X, Waveform::Type::Sine, 246.94f); + CreateWaveform(olc::Key::C, Waveform::Type::Sine, 261.63f); + CreateWaveform(olc::Key::F, Waveform::Type::Sine, 277.18f); + CreateWaveform(olc::Key::V, Waveform::Type::Sine, 293.66f); + CreateWaveform(olc::Key::G, Waveform::Type::Sine, 311.13f); + CreateWaveform(olc::Key::B, Waveform::Type::Sine, 329.63f); + CreateWaveform(olc::Key::N, Waveform::Type::Sine, 349.23f); + CreateWaveform(olc::Key::J, Waveform::Type::Sine, 369.99f); + CreateWaveform(olc::Key::M, Waveform::Type::Sine, 392.00f); + CreateWaveform(olc::Key::K, Waveform::Type::Sine, 415.30f); + CreateWaveform(olc::Key::COMMA, Waveform::Type::Sine, 440.00f); + CreateWaveform(olc::Key::L, Waveform::Type::Sine, 466.16f); + CreateWaveform(olc::Key::PERIOD, Waveform::Type::Sine, 493.88f); + CreateWaveform(olc::Key::OEM_2, Waveform::Type::Sine, 523.25f); return true; } @@ -69,16 +69,16 @@ class Example_Waveforms : public olc::PixelGameEngine { if(keyboard.GetKey(olc::Key::LEFT).bPressed) { - typeTracker = ((typeTracker + (int)olc::ext::Miniaudio::Waveform::Type::Count) - 1) % (int)olc::ext::Miniaudio::Waveform::Type::Count; + waveformTypeTracker = ((waveformTypeTracker + (int)Waveform::Type::Count) - 1) % (int)Waveform::Type::Count; for(auto& i : mapKeys) - audio.SetWaveformType(i.second, (olc::ext::Miniaudio::Waveform::Type)typeTracker); + audio.SetWaveformType(i.second, (Waveform::Type)waveformTypeTracker); } if(keyboard.GetKey(olc::Key::RIGHT).bPressed) { - typeTracker = (typeTracker + 1) % (int)olc::ext::Miniaudio::Waveform::Type::Count; + waveformTypeTracker = (waveformTypeTracker + 1) % (int)Waveform::Type::Count; for(auto& i : mapKeys) - audio.SetWaveformType(i.second, (olc::ext::Miniaudio::Waveform::Type)typeTracker); + audio.SetWaveformType(i.second, (Waveform::Type)waveformTypeTracker); } for(auto& i : mapKeys) @@ -110,7 +110,7 @@ class Example_Waveforms : public olc::PixelGameEngine scale ); - demoMessage = "Current Wave (" + waveformTypeToName.at((olc::ext::Miniaudio::Waveform::Type)typeTracker) + ")"; + demoMessage = "Current Wave (" + waveformTypeToName.at((Waveform::Type)waveformTypeTracker) + ")"; draw.String( center + olc::vi2d{0, 24} - (draw.GetTextSize(demoMessage, false, scale) / 2), demoMessage, @@ -139,17 +139,16 @@ class Example_Waveforms : public olc::PixelGameEngine olc::ext::Miniaudio::AudioEngine audio; private: - int typeTracker = 0; - - const std::unordered_map waveformTypeToName + int waveformTypeTracker = 0; + const std::unordered_map waveformTypeToName { - {olc::ext::Miniaudio::Waveform::Type::Sine, "SINE"}, - {olc::ext::Miniaudio::Waveform::Type::Square, "SQUARE"}, - {olc::ext::Miniaudio::Waveform::Type::Triangle, "TRIANGLE"}, - {olc::ext::Miniaudio::Waveform::Type::Sawtooth, "SAWTOOTH"}, + {Waveform::Type::Sine, "SINE"}, + {Waveform::Type::Square, "SQUARE"}, + {Waveform::Type::Triangle, "TRIANGLE"}, + {Waveform::Type::Sawtooth, "SAWTOOTH"}, }; - std::unordered_map mapKeys; + std::unordered_map mapKeys; const std::string piano{ " | | | | | | | | | | | | | | | | |\n" From 89019bdb9c999ea7be939f005263f215dfa147da Mon Sep 17 00:00:00 2001 From: Moros Smith Date: Fri, 9 Oct 2026 05:49:59 -0400 Subject: [PATCH 19/21] [ext][miniaudio] clear callbacks in AudioEngine destructor --- extensions/miniaudio/olcPGEX3_Miniaudio.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/extensions/miniaudio/olcPGEX3_Miniaudio.h b/extensions/miniaudio/olcPGEX3_Miniaudio.h index 8ba6738..0f288e1 100644 --- a/extensions/miniaudio/olcPGEX3_Miniaudio.h +++ b/extensions/miniaudio/olcPGEX3_Miniaudio.h @@ -366,6 +366,9 @@ namespace olc::ext::Miniaudio } m_sound_groups.clear(); + ClearSynthCallback(); + ClearDataCallback(); + ma_resource_manager_uninit(&m_resource_manager); ma_engine_stop(&m_engine); From 8008183833a452f01c6b8c58ff5fbcbb52f93991 Mon Sep 17 00:00:00 2001 From: Moros Smith Date: Fri, 9 Oct 2026 05:50:35 -0400 Subject: [PATCH 20/21] [ext][miniaudio][demo] fix comment formatting and content --- extensions/miniaudio/demo_Miniaudio.cpp | 7 ------- extensions/miniaudio/demo_Waveforms.cpp | 13 +++---------- 2 files changed, 3 insertions(+), 17 deletions(-) diff --git a/extensions/miniaudio/demo_Miniaudio.cpp b/extensions/miniaudio/demo_Miniaudio.cpp index 9e1dcbe..46cae99 100644 --- a/extensions/miniaudio/demo_Miniaudio.cpp +++ b/extensions/miniaudio/demo_Miniaudio.cpp @@ -6,19 +6,12 @@ Licenced under the OLC-3 License */ - -// Define OLC_PGE3_APPLICATION to include the implementation of -// the Pixel Game Engine as part of this translation unit #define OLC_PGE3_APPLICATION #include "olcPixelGameEngine3.h" #define OLC_PGEX3_MINIAUDIO #include "olcPGEX3_Miniaudio.h" -// Example application demonstrating the miniaudio extension. -// This class overrides the olc::PixelGameEngine base class -// by implementing the OnUserCreate() and OnUserUpdate() -// functions class Example_Miniaudio : public olc::PixelGameEngine { public: diff --git a/extensions/miniaudio/demo_Waveforms.cpp b/extensions/miniaudio/demo_Waveforms.cpp index 1036ce4..10b0368 100644 --- a/extensions/miniaudio/demo_Waveforms.cpp +++ b/extensions/miniaudio/demo_Waveforms.cpp @@ -1,14 +1,11 @@ /* - olc::PixelGameEngine3 Example - olcPGEX3_Miniaudio + olc::PixelGameEngine3 Example - demo_Waveforms - Demonstrates using olcPGEX3_Miniaudio + Demonstrates using waveforms from olcPGEX3_Miniaudio Licenced under the OLC-3 License */ - -// Define OLC_PGE3_APPLICATION to include the implementation of -// the Pixel Game Engine as part of this translation unit #define OLC_PGE3_APPLICATION #include "olcPixelGameEngine3.h" @@ -18,16 +15,12 @@ // Save us a whole lot of typing in this demo using namespace olc::ext::Miniaudio; -// Example application demonstrating the miniaudio extension. -// This class overrides the olc::PixelGameEngine base class -// by implementing the OnUserCreate() and OnUserUpdate() -// functions class Example_Waveforms : public olc::PixelGameEngine { public: Example_Waveforms() { - sAppName = "Example - olcPGEX3_Miniaudio"; + sAppName = "Example - demo_Waveforms"; if(!InstallSystemExtension(&audio)) throw std::runtime_error("Failed to install olcPGEX3_Miniaudio"); } From aca214fef838ebd0467379903d94e0489aeccfa3 Mon Sep 17 00:00:00 2001 From: Moros Smith Date: Fri, 9 Oct 2026 05:50:51 -0400 Subject: [PATCH 21/21] [ext][miniaudio][demo] add synth demonstration --- extensions/miniaudio/demo_Synth.cpp | 682 ++++++++++++++++++++++++++++ 1 file changed, 682 insertions(+) create mode 100644 extensions/miniaudio/demo_Synth.cpp diff --git a/extensions/miniaudio/demo_Synth.cpp b/extensions/miniaudio/demo_Synth.cpp new file mode 100644 index 0000000..85a6f0b --- /dev/null +++ b/extensions/miniaudio/demo_Synth.cpp @@ -0,0 +1,682 @@ +/* + olc::PixelGameEngine3 Example - demo_Synth + + Demonstrates using synth callbacks from olcPGEX3_Miniaudio + + Licenced under the OLC-3 License +*/ + +#define OLC_PGE3_APPLICATION +#include "olcPixelGameEngine3.h" + +#define OLC_PGEX3_MINIAUDIO +#include "olcPGEX3_Miniaudio.h" + +// Save us a whole lot of typing in this demo +using namespace olc::ext::Miniaudio; + +constexpr int NOTE_COUNT = 17; +constexpr float thirtyFramesPerSecond = 1.0f / 30.f; + +// --------------------------------------------------------------------------- +// Synth building blocks. Everything in here is called from the AUDIO THREAD, +// so: no allocation, no locks, no rand(), no I/O. +// --------------------------------------------------------------------------- +namespace synth +{ + constexpr double kPi = 3.14159265358979323846; + constexpr double kTwoPi = 2.0 * kPi; + + // Don't generate harmonics above this; anything past Nyquist (24 kHz at + // 48 kHz sample rate) folds back down as harsh aliasing. + constexpr double kMaxHarmonicHz = 20000.0; + + enum class Oscillator + { + SINE, + SQUARE, + TRIANGLE, + SAW_ANALOG, + SAW_DIGITAL, + NOISE, + }; + + // Converts frequency (Hz) to angular velocity + inline double w(const double hertz) + { + return hertz * kTwoPi; + } + + // Pre-computed equal-tempered scale: 256 Hz * 2^(noteID / 12). + // Avoids calling pow() several times per sample per note. + constexpr int kScaleOffset = 24; + constexpr int kScaleSize = 128; + + inline const std::array kScaleTable = [] + { + std::array table{}; + for(int i = 0; i < kScaleSize; i++) + table[i] = 256.0 * std::pow(2.0, double(i - kScaleOffset) / 12.0); + return table; + }(); + + inline double scale(const int noteID) + { + return kScaleTable[std::clamp(noteID + kScaleOffset, 0, kScaleSize - 1)]; + } + + // Tiny lock-free RNG (xorshift32). Only ever touched by the audio thread. + inline uint32_t g_noiseState = 2463534242u; + + inline float noise() + { + g_noiseState ^= g_noiseState << 13; + g_noiseState ^= g_noiseState >> 17; + g_noiseState ^= g_noiseState << 5; + return 2.0f * (float(g_noiseState) / 4294967295.0f) - 1.0f; + } + + // PolyBLEP: smooths the discontinuity of a naive square/saw so it doesn't alias. + // t = normalised phase [0,1), dt = phase increment per sample. + inline double polyBlep(double t, const double dt) + { + if(t < dt) + { + t /= dt; + return t + t - t * t - 1.0; + } + if(t > 1.0 - dt) + { + t = (t - 1.0) / dt; + return t * t + t + t + 1.0; + } + return 0.0; + } + + // Oscillator. + // time absolute time in seconds (double!) + // dt seconds per sample (used for band-limiting) + // hertz frequency + // LFOHertz/LFOAmplitude optional vibrato + // custom max harmonic count for SAW_ANALOG + inline double osc(const double time, const double dt, const double hertz, + const Oscillator type = Oscillator::SINE, + const double LFOHertz = 0.0, const double LFOAmplitude = 0.0, + const double custom = 50.0) + { + double phase = w(hertz) * time; + if(LFOAmplitude != 0.0) + phase += LFOAmplitude * hertz * std::sin(w(LFOHertz) * time); + + // Phase increment per sample, as a fraction of a cycle (for BLEP) + const double inc = std::min(hertz * dt, 0.49); + + switch(type) + { + case Oscillator::SINE: // Sine wave between -1 and +1 + return std::sin(phase); + + case Oscillator::SQUARE: // Band-limited square wave between -1 and +1 + { + double p = phase / kTwoPi; + p -= std::floor(p); + double v = (p < 0.5) ? 1.0 : -1.0; + v += polyBlep(p, inc); + v -= polyBlep(std::fmod(p + 0.5, 1.0), inc); + return v; + } + + case Oscillator::TRIANGLE: // Triangle wave between -1 and +1 + return std::asin(std::sin(phase)) * (2.0 / kPi); + + case Oscillator::SAW_ANALOG: // Saw wave (analogue / warm) + { + // Sum of sin(n*x)/n, using the Chebyshev recurrence + // sin((n+1)x) = 2cos(x) sin(nx) - sin((n-1)x) + // so we only need ONE sin and ONE cos instead of one sin per harmonic. + int maxN = std::min(int(custom) - 1, int(kMaxHarmonicHz / hertz)); + if(maxN < 1) maxN = 1; + + const double c2 = 2.0 * std::cos(phase); + double s0 = 0.0; + double s1 = std::sin(phase); + double out = s1; + + for(int n = 2; n <= maxN; n++) + { + const double s2 = c2 * s1 - s0; + out += s2 / n; + s0 = s1; + s1 = s2; + } + return out * (2.0 / kPi); + } + + case Oscillator::SAW_DIGITAL: // Band-limited digital saw + { + double p = phase / kTwoPi; + p -= std::floor(p); + return 2.0 * p - 1.0 - polyBlep(p, inc); + } + + case Oscillator::NOISE: + return noise(); + + default: + return 0.0; + } + } + + // ADSR parameters (just data; shared by every voice using an instrument) + struct EnvelopeADSR + { + double attackTime = 0.1; + double decayTime = 0.1; + double sustainAmplitude = 1.0; + double releaseTime = 0.2; + double startAmplitude = 1.0; + }; + + // Per-voice envelope STATE. Generates one amplitude value per sample and + // always continues from its current level, so retriggering a note that is + // still fading out never causes a sudden jump (= click). + struct EnvelopeState + { + enum class Stage { Idle, Attack, Decay, Sustain, Release }; + + Stage stage{Stage::Idle}; + double level{0.0}; + double releaseStep{0.0}; + + bool active() const { return stage != Stage::Idle; } + + void noteOn() + { + stage = Stage::Attack; // level is deliberately NOT reset + } + + void noteOff(const EnvelopeADSR& e, const double dt) + { + if(stage == Stage::Idle || stage == Stage::Release) + return; + stage = Stage::Release; + // Fall linearly from wherever we are to zero over releaseTime + releaseStep = level * dt / std::max(e.releaseTime, 1e-4); + } + + double next(const EnvelopeADSR& e, const double dt) + { + constexpr double kMinStageTime = 1e-4; + + switch(stage) + { + case Stage::Idle: + return 0.0; + + case Stage::Attack: + level += dt * e.startAmplitude / std::max(e.attackTime, kMinStageTime); + if(level >= e.startAmplitude) + { + level = e.startAmplitude; + stage = Stage::Decay; + } + break; + + case Stage::Decay: + { + const double step = dt * std::abs(e.startAmplitude - e.sustainAmplitude) + / std::max(e.decayTime, kMinStageTime); + + if(level > e.sustainAmplitude) + { + level -= step; + if(level <= e.sustainAmplitude) + { + level = e.sustainAmplitude; + stage = Stage::Sustain; + } + } + else + { + level += step; + if(level >= e.sustainAmplitude) + { + level = e.sustainAmplitude; + stage = Stage::Sustain; + } + } + break; + } + + case Stage::Sustain: + level = e.sustainAmplitude; + // A zero sustain (e.g. bell) is silent: stop doing work. + if(level <= 0.0) + stage = Stage::Idle; + break; + + case Stage::Release: + level -= releaseStep; + if(level <= 0.0) + { + level = 0.0; + stage = Stage::Idle; + } + break; + } + + return level; + } + }; + + // Abstract base for instruments. An instrument only produces the raw + // waveform; the envelope and volume are applied by the voice. + struct Instrument + { + virtual ~Instrument() = default; + + std::string name; + EnvelopeADSR env; + float gain{1.0f}; + + virtual float sound(const double time, const double dt, const int noteId) const = 0; + }; + + struct Bell : public Instrument + { + Bell() + { + env.attackTime = 0.01; + env.decayTime = 1.0; + env.sustainAmplitude = 0.0; + env.releaseTime = 1.0; + + gain = 1.0f; + name = "Bell"; + } + + float sound(const double time, const double dt, const int id) const override + { + return float( + + 1.00 * osc(time, dt, scale(id + 12), Oscillator::SINE, 5.0, 0.001) + + 0.50 * osc(time, dt, scale(id + 24)) + + 0.25 * osc(time, dt, scale(id + 36))); + } + }; + + struct Bell8 : public Instrument + { + Bell8() + { + env.attackTime = 0.01; + env.decayTime = 0.5; + env.sustainAmplitude = 0.8; + env.releaseTime = 1.0; + + gain = 1.0f; + name = "Bell 8-bit"; + } + + float sound(const double time, const double dt, const int id) const override + { + return float( + + 1.00 * osc(time, dt, scale(id), Oscillator::SQUARE, 5.0, 0.001) + + 0.50 * osc(time, dt, scale(id + 12)) + + 0.25 * osc(time, dt, scale(id + 24))); + } + }; + + struct Harmonica : public Instrument + { + Harmonica() + { + env.attackTime = 0.05; + env.decayTime = 1.0; + env.sustainAmplitude = 0.95; + env.releaseTime = 0.1; + + gain = 1.0f; + name = "Harmonica"; + } + + float sound(const double time, const double dt, const int id) const override + { + return float( + + 1.00 * osc(time, dt, scale(id), Oscillator::SQUARE, 5.0, 0.001) + + 0.50 * osc(time, dt, scale(id + 12), Oscillator::SQUARE) + + 0.05 * osc(time, dt, scale(id + 24), Oscillator::NOISE)); + } + }; + + struct SawAnalog : public Instrument + { + SawAnalog() + { + env.attackTime = 0.05; + env.decayTime = 1.0; + env.sustainAmplitude = 0.95; + env.releaseTime = 0.1; + + gain = 1.0f; + name = "Analog Saw"; + } + + float sound(const double time, const double dt, const int id) const override + { + return float(osc(time, dt, scale(id), Oscillator::SAW_ANALOG, 5.0, 0.001)); + } + }; + + // Everything the audio thread keeps for one playing note. + // Touched ONLY by the audio thread. + struct Voice + { + EnvelopeState env; + const Instrument* inst{nullptr}; // instrument this note started with + bool gate{false}; // last key state the audio thread saw + }; +} + +class Example_Synth : public olc::PixelGameEngine +{ + // NOTICE + // + // This example is highly inspired by the Sound Synthesizer Part 3 series created by javidx9! + // Checkout the original at https://github.com/OneLoneCoder/synth/blob/master/main3a.cpp + // and the accompanying video at https://www.youtube.com/watch?v=kDuvruJTjOs + // + +public: + Example_Synth() + { + sAppName = "Example - demo_Synth"; + if(!InstallSystemExtension(&audio)) + throw std::runtime_error("Failed to install olcPGEX3_Miniaudio"); + } + +public: + + bool OnUserCreate() override + { + // Create instruments BEFORE the audio callback is installed. After that + // this list is read-only, so the audio thread can safely read it. + instruments.emplace_back(std::make_unique()); + instruments.emplace_back(std::make_unique()); + instruments.emplace_back(std::make_unique()); + instruments.emplace_back(std::make_unique()); + + sampleRate = double(audio.GetDeviceSampleRate()); + sampleDt = 1.0 / sampleRate; + + // One-pole smoothing coefficient with a ~10 ms time constant + smoothCoef = 1.0 - std::exp(-1.0 / (0.010 * sampleRate)); + smoothVolume = volume.load(); + smoothPan = pan.load(); + + // This is it... This is how we interface into the miniaudio engine any noise we want heard! + // Called once per sample frame, on the AUDIO thread. + noiseCallbackFunc = [this](float& noiseLeftChannel, float& noiseRightChannel, const float)->void + { + // Time comes from an integer sample counter: exact, never drifts, + // and keeps full double precision no matter how long we run. + const double time = double(sampleCounter++) * sampleDt; + + // Smooth the controls toward their targets (prevents zipper noise) + smoothVolume += (double(volume.load(std::memory_order_relaxed)) - smoothVolume) * smoothCoef; + smoothPan += (double(pan.load(std::memory_order_relaxed)) - smoothPan) * smoothCoef; + + const synth::Instrument* selected = + instruments[selectedInstrumentInd.load(std::memory_order_relaxed)].get(); + + double mix = 0.0; + + for(int i = 0; i < NOTE_COUNT; i++) + { + synth::Voice& v = voices[i]; + + // Detect key press / release using the audio clock only + const bool held = keyHeld[i].load(std::memory_order_relaxed); + if(held != v.gate) + { + v.gate = held; + + if(held) + { + // Only adopt the newly selected instrument if this voice is + // silent. If it's still fading out we keep its old instrument + // so the waveform doesn't change abruptly mid-sound. + if(!v.env.active()) + v.inst = selected; + v.env.noteOn(); + } + else if(v.inst) + { + v.env.noteOff(v.inst->env, sampleDt); + } + } + + // Silent voice: skip ALL oscillator math + if(!v.env.active()) + continue; + + const double amp = v.env.next(v.inst->env, sampleDt); + mix += amp * double(v.inst->sound(time, sampleDt, keys[i].id)) * double(v.inst->gain); + } + + mix *= smoothVolume; + + // This lerp formula shrinks the -1 to 1 range to 0 to 1, which determines + // the percentage of the total noise to play in each side. + const double t = smoothPan * 0.5 + 0.5; + + // Soft clip. Keeps the summed notes below 1.0 so the header's limiter + // (instant gain ducking = audible pumping/crackle) never has to act. + noiseLeftChannel = float(std::tanh(mix * (1.0 - t))); + noiseRightChannel = float(std::tanh(mix * t)); + }; + + // And finally... Tell the system this is where to send audio requests to... Now we can modify audio! + audio.SetSynthCallback(noiseCallbackFunc); + + return true; + } + + bool OnUserUpdate(float fElapsedTime) override + { + fElapsedTime = (fElapsedTime > thirtyFramesPerSecond) ? thirtyFramesPerSecond : fElapsedTime; + + olc::Pixel backgroundCol{olc::Colour::VERY_DARK_GREY}; + + bool keyPressed{false}; + + // Just publish which keys are held. The audio thread works out the + // press/release edges and timing itself. + for(int i = 0; i < NOTE_COUNT; i++) + { + const bool held = keyboard.GetKey(keys[i].key).bHeld; + keyHeld[i].store(held, std::memory_order_relaxed); + keyPressed |= held; + } + + const int instrumentCount = int(instruments.size()); + int instrumentInd = selectedInstrumentInd.load(); + float vol = volume.load(); + float pn = pan.load(); + + if(keyboard.GetKey(olc::Key::UP).bPressed) + instrumentInd = (instrumentInd + 1) % instrumentCount; + + if(keyboard.GetKey(olc::Key::DOWN).bPressed) + instrumentInd = (instrumentInd + instrumentCount - 1) % instrumentCount; + + if(keyboard.GetKey(olc::Key::RIGHT).bHeld) + vol = std::min(1.f, vol + 1.f * fElapsedTime); + if(keyboard.GetKey(olc::Key::LEFT).bHeld) + vol = std::max(0.f, vol - 1.f * fElapsedTime); + + if(keyboard.GetKey(olc::Key::O).bHeld) + pn = std::max(-1.f, pn - 1.f * fElapsedTime); + if(keyboard.GetKey(olc::Key::P).bHeld) + pn = std::min(1.f, pn + 1.f * fElapsedTime); + + if(keyboard.GetKey(olc::Key::R).bPressed) + { + instrumentInd = 0; + vol = 0.1f; + pn = 0.f; + } + + selectedInstrumentInd.store(instrumentInd); + volume.store(vol); + pan.store(pn); + + if(keyPressed) + backgroundCol = olc::Colour::VERY_DARK_BLUE; + + draw.Clear(backgroundCol); + + + olc::vi2d center = ScreenSize() / 2; + olc::vf2d scale{2.3f, 2.3f}; + std::string demoMessage = "olcPGEX3_miniaudio Waveform Demo"; + + draw.String( + center - olc::vi2d{0, 32} - (draw.GetTextSize(demoMessage, false, scale) / 2), + demoMessage, + olc::Colour::WHITE, + scale + ); + + scale = {1.5f, 1.5f}; + demoMessage = "Instrument: <"+ instruments[instrumentInd]->name +"> UP, DOWN"; + draw.String( + center - (draw.GetTextSize(demoMessage, false, scale) / 2), + demoMessage, + olc::Colour::WHITE, + scale + ); + + demoMessage = "Volume: <"+ std::to_string(vol) +"> LEFT, RIGHT"; + draw.String( + center + olc::vi2d{0, 24} - (draw.GetTextSize(demoMessage, false, scale) / 2), + demoMessage, + olc::Colour::WHITE, + scale + ); + + demoMessage = "Pan: <"+ std::to_string(pn) +"> O, P"; + draw.String( + center + olc::vi2d{0, 48} - (draw.GetTextSize(demoMessage, false, scale) / 2), + demoMessage, + olc::Colour::WHITE, + scale + ); + + demoMessage = "Reset Settings "; + draw.String( + center + olc::vi2d{0, 72} - (draw.GetTextSize(demoMessage, false, scale) / 2), + demoMessage, + olc::Colour::WHITE, + scale + ); + + olc::vf2d pianoStrSize{draw.GetTextSize(piano)}; + draw.String( + olc::vf2d{ + (ScreenSize().x / 2) - (pianoStrSize.x / 2), + ScreenSize().y - pianoStrSize.y + }, + piano, + olc::Colour::WHITE + ); + + #if defined(__EMSCRIPTEN__) + return true; + #else + return !keyboard.GetKey(olc::Key::ESCAPE).bPressed; + #endif + } + +private: + + // The instance of the audio engine, no fancy config required. + AudioEngine audio; + + std::function noiseCallbackFunc; + + // ---- Shared between threads: only simple atomics ---------------------- + std::atomic volume{0.1f}; + std::atomic pan{0.0f}; // -1.f for only left channel, 1.f for only right channel + std::atomic selectedInstrumentInd{0}; + std::array, NOTE_COUNT> keyHeld{}; + + // ---- Owned exclusively by the audio thread ---------------------------- + uint64_t sampleCounter{0}; // running count of samples generated + double sampleRate{48000.0}; + double sampleDt{1.0 / 48000.0}; + double smoothCoef{0.002}; + double smoothVolume{0.1}; + double smoothPan{0.0}; + std::array voices{}; + + // Read-only once the audio callback is installed + std::vector> instruments; + + struct KeyDef + { + const char* label; // display name of the note + olc::Key key; + int id; // Position in scale + }; + + static inline const std::array keys{{ + {"G#", olc::Key::A, -1}, //This is not a typo. javid's original scale formula starts at "A", so this will retrieve the note ID prior to that one. + {"A" , olc::Key::Z, 0}, + {"A#", olc::Key::S, 1}, + {"B" , olc::Key::X, 2}, + {"C" , olc::Key::C, 3}, + {"C#", olc::Key::F, 4}, + {"D" , olc::Key::V, 5}, + {"D#", olc::Key::G, 6}, + {"E" , olc::Key::B, 7}, + {"F" , olc::Key::N, 8}, + {"F#", olc::Key::J, 9}, + {"G" , olc::Key::M, 10}, + {"G#", olc::Key::K, 11}, + {"A" , olc::Key::COMMA, 12}, + {"A#", olc::Key::L, 13}, + {"B" , olc::Key::PERIOD, 14}, + {"C" , olc::Key::OEM_2, 15}, + }}; + + const std::string piano{ + " | | | | | | | | | | | | | | | | |\n" + "A | | S | | | F | | G | | | J | | K | | L | | |\n" + "__| |___| | |___| |___| | |___| |___| |___| | |__\n" + "| | | | | | | | | | |\n" + "| Z | X | C | V | B | N | M | , | . | / |\n" + "|_____|_____|_____|_____|_____|_____|_____|_____|_____|_____|" + }; +}; + +// Main entry point for the application +int main() +{ + // Construct demo application + Example_Synth demo; + + // Create "screen" of 640x360 "pixels" + // with a pixel size of 2x2 actual screen pixels + PGEConfig config; + config.bVSync = false; + config.vPixelSize = { 2,2 }; + config.vScreenSize = { 640,360 }; + + if (demo.Construct(config)) + { + // Start the application + demo.Start(); + } + + return 0; +} \ No newline at end of file