diff --git a/README.md b/README.md index 70c1584..f0702b7 100644 --- a/README.md +++ b/README.md @@ -125,7 +125,8 @@ Header file should now be located at `build/olcPixelGameEngine3.h`. ### Available CMake Configuration Flags -* ``-DBUILD_EXAMPLES=ON`` - builds examples, will be located at `build/examples` +* ``-DBUILD_EXAMPLES=ON`` - builds examples, will be located at `build/dev/examples` +* ``-DBUILD_EXTENSIONS=ON`` - builds examples, will be located at `build/dev/extensions` * ``-DBUILD_WAYLAND=ON`` - if examples are built, build them using wayland. Linux ONLY! * ``-DUSE_STB=ON`` - build using stb_image for the image loader (See section on STB Image) diff --git a/extensions/CMakeLists.txt b/extensions/CMakeLists.txt index 9409cd7..e30af16 100644 --- a/extensions/CMakeLists.txt +++ b/extensions/CMakeLists.txt @@ -1,2 +1,2 @@ -# add_subdirectory(miniaudio) +add_subdirectory(miniaudio) # add_subdirectory(splashscreen) diff --git a/extensions/miniaudio/CMakeLists.txt b/extensions/miniaudio/CMakeLists.txt new file mode 100644 index 0000000..f678d58 --- /dev/null +++ b/extensions/miniaudio/CMakeLists.txt @@ -0,0 +1,14 @@ +# Fetch 3rd Party miniaudio library +FetchContent_Declare( + miniaudio + GIT_REPOSITORY https://github.com/mackron/miniaudio.git + GIT_TAG 0.11.25 +) + +FetchContent_MakeAvailable(miniaudio) + +add_example(ext_demo_Miniaudio demo_Miniaudio.cpp) + +target_include_directories(ext_demo_Miniaudio PRIVATE ${miniaudio_SOURCE_DIR}) + +handle_assets(ext_demo_Miniaudio ${CMAKE_CURRENT_SOURCE_DIR}/assets) diff --git a/extensions/miniaudio/README.md b/extensions/miniaudio/README.md index e69de29..c1e1526 100644 --- a/extensions/miniaudio/README.md +++ b/extensions/miniaudio/README.md @@ -0,0 +1,424 @@ +# olcPGEX3_Miniaudio - Usage Documentation + +## Overview + +**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. + +### 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 +--- + +## Installation & Setup + +### 1. Acquire miniaudio + +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. + +### 2. Include the Header after PGE3 +```cpp +#define OLC_PGEX3_MINIAUDIO +#include "olcPGEX3_Miniaudio.h" +``` + +### 3. Install the Extension in Your PGE Application +```cpp +class MyGame : public olc::PixelGameEngine +{ +public: + MyGame() + { + sAppName = "My Audio Game"; + if(!InstallSystemExtension(&audio)) + 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 + +The main interface to the audio system. Manages all sounds, waveforms, and the underlying audio device. + +#### Configuration + +Configure the audio engine before creation in your constructor: + +```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); +``` + +#### Background Playback + +Control whether audio plays when the window loses focus: + +```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: + +```cpp +ma_engine& engine = audio.GetEngine(); +ma_device& device = audio.GetDevice(); +ma_resource_manager& rm = audio.GetResourceManager(); + +int channels = audio.GetDeviceChannels(); +int sampleRate = audio.GetDeviceSampleRate(); +ma_format format = audio.GetDeviceFormat(); +``` + +> **Note:** In the example above `audio` is the instance of `olc::ext::Miniaudio::AudioEngine` + +--- + +## olc::ext::Miniaudio::Sound + +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 +``` + +#### From Memory +```cpp +// Load from memory buffer +std::vector audioData = /* ... load file data ... */; +audio.CreateSoundFromMemory(mySound, audioData.data(), audioData.size()); + +// Or pass vector directly +audio.CreateSoundFromMemory(mySound, audioData); +``` +> **Note:** In the example above `audio` is the instance of `olc::ext::Miniaudio::AudioEngine` + +### Playback Control + +#### Play +```cpp +// Play once +mySound.Play(); + +// Play with looping +mySound.Play(true); // Loops continuously +``` + +#### Stop +```cpp +// Stop and rewind to beginning +mySound.Stop(); +``` + +#### Pause +```cpp +// Pause without changing position +mySound.Pause(); +``` + +#### Toggle +```cpp +// Play if paused, pause if playing +mySound.Toggle(); +``` + +### Seeking (Cursor Control) + +#### 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 +``` + +#### Forward/Rewind +```cpp +// Move forward by 2 seconds +mySound.Forward(2000); + +// Move backward by 1 second +mySound.Rewind(1000); +``` + +### Audio Effects + +#### 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 +``` + +#### 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 +``` + +#### 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 +``` + +### Information Queries + +#### Playback Status +```cpp +// Check if currently playing +if(mySound.IsPlaying()) +{ + // Sound is playing +} + +// Check if loaded successfully +if(mySound.IsLoaded()) +{ + // Sound was loaded successfully +} +``` + +#### Cursor Position +```cpp +// Get cursor position in milliseconds +ma_uint64 timeMs = mySound.GetCursor(); +std::cout << "Position: " << timeMs << " ms\n"; + +// 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(); + +// Example: Set 3D position +ma_sound_set_position(maSound, 10.0f, 0.0f, -5.0f); +``` + +--- + +## olc::ext::Miniaudio::Waveform + +Generate and play procedural waveforms for synthesis and sound effects. + +### Supported Waveform Types + +```cpp +enum class Waveform::Type +{ + Sine, // Smooth, pure tone + Square, // Digital, buzzy tone + Triangle, // Bright, harmonic tone + Sawtooth // Bright, harsh tone +}; +``` + +### 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) +); +``` +> **Note:** In the example above `audio` is the instance of `olc::ext::Miniaudio::AudioEngine` +### Playback Control + +#### Play +```cpp +sineWave.Play(); +``` + +#### Stop +```cpp +sineWave.Stop(); +``` + +### Configuration + +#### Change Amplitude +```cpp +// Amplitude controls volume (0.0 = silent, 1.0 = loud) +sineWave.SetAmplitude(0.1); // 10% volume +sineWave.SetAmplitude(0.5); // 50% volume +``` + +#### 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 +``` + +#### 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); +``` + +### Information Queries + +```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 + +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 +{ +private: + olc::ext::Miniaudio::AudioEngine audio; + +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(); + } + + return true; + } +}; +``` + +### Raw Audio Data Callback + +For maximum control, provide a custom data callback: + +```cpp +audio.SetDataCallback([this](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 */; + } +}); + +// Clear callback later +audio.ClearDataCallback(); +``` +> **Note:** This example doesn't do anything useful, that's up to you! + +--- + +## Troubleshooting + +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? + +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 + +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) + +--- + +## Acknowledgements (From Moros1138) + +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! + +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! + +## License + +Licensed under the OLC-3 License (OneLoneCoder Public License v3) + +Copyright 2023-2026 Moros Smith + +For full license details, see the header file or original source. diff --git a/extensions/miniaudio/assets/SampleA.wav b/extensions/miniaudio/assets/SampleA.wav new file mode 100644 index 0000000..e7cf99f Binary files /dev/null and b/extensions/miniaudio/assets/SampleA.wav differ diff --git a/extensions/miniaudio/assets/song1-license.txt b/extensions/miniaudio/assets/song1-license.txt new file mode 100644 index 0000000..a0b183d --- /dev/null +++ b/extensions/miniaudio/assets/song1-license.txt @@ -0,0 +1,3 @@ +Music: Joy Ride [Full version] by MusicLFiles +Free download: https://filmmusic.io/song/11627-joy-ride-full-version +Licensed under CC BY 4.0: https://filmmusic.io/standard-license diff --git a/extensions/miniaudio/assets/song1.mp3 b/extensions/miniaudio/assets/song1.mp3 new file mode 100644 index 0000000..0e2c777 Binary files /dev/null and b/extensions/miniaudio/assets/song1.mp3 differ diff --git a/extensions/miniaudio/demo_Miniaudio.cpp b/extensions/miniaudio/demo_Miniaudio.cpp new file mode 100644 index 0000000..fdce653 --- /dev/null +++ b/extensions/miniaudio/demo_Miniaudio.cpp @@ -0,0 +1,299 @@ +/* + 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_Miniaudio : public olc::PixelGameEngine +{ +public: + Example_Miniaudio() + { + 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 + { + // load `assets/song1.mp3` into `song1` + audio.CreateSoundFromFile(song1, "assets/song1.mp3"); + + /** + * this is here to demonstrate how the adventurous can + * exploit other features of miniaudio that hasn't been + * abstracted by the PGEX + * + * 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); + + // load `assets/SampleA.wav` into `sample` + audio.CreateSoundFromFile(sample, "assets/SampleA.wav"); + + // create all of the waveforms at 0.1 amplitude at 440Mhz (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); + audio.CreateWaveform(sawtooth, olc::ext::Miniaudio::Waveform::Type::Sawtooth, 0.1, 440.0); + + return true; + } + + // Called every frame, so update things here + bool OnUserUpdate(float fElapsedTime) override + { + // toggle background playback + if(keyboard.GetKey(olc::Key::K1).bPressed) + { + backgroundPlay = !backgroundPlay; + if(backgroundPlay) + audio.EnableBackgroundPlayback(); + else + audio.DisableBackgroundPlayback(); + } + + // ensure all waveforms are stopped + // before we check for held keys + sine.Stop(); square.Stop(); + triangle.Stop(); sawtooth.Stop(); + + // play `sine` when held + if(keyboard.GetKey(olc::Key::K7).bHeld) + sine.Play(); + + // play `sqaure` when held + if(keyboard.GetKey(olc::Key::K8).bHeld) + square.Play(); + + // play `triangle` when held + if(keyboard.GetKey(olc::Key::K9).bHeld) + triangle.Play(); + + // play `sawtooth` when held + if(keyboard.GetKey(olc::Key::K0).bHeld) + sawtooth.Play(); + + // toggle `song1` playback/pause + if(keyboard.GetKey(olc::Key::SPACE).bPressed) + song1.Toggle(); + + // play `sample` + if(keyboard.GetKey(olc::Key::S).bPressed) + sample.Play(); + + // panning + if(keyboard.GetKey(olc::Key::MINUS).bHeld) + pan -= 1.0f * fElapsedTime; + + if(keyboard.GetKey(olc::Key::EQUALS).bHeld) + pan += 1.0f * fElapsedTime; + + // pitch + if(keyboard.GetKey(olc::Key::OEM_4).bHeld) + pitch -= 1.0f * fElapsedTime; + + if(keyboard.GetKey(olc::Key::OEM_6).bHeld) + pitch += 1.0f * fElapsedTime; + + // volume + if(keyboard.GetKey(olc::Key::DOWN).bHeld) + volume -= 1.0f * fElapsedTime; + + if(keyboard.GetKey(olc::Key::UP).bHeld) + volume += 1.0f * fElapsedTime; + + // distance + if(keyboard.GetKey(olc::Key::LEFT).bHeld) + distance -= 10.0f * fElapsedTime; + + if(keyboard.GetKey(olc::Key::RIGHT).bHeld) + distance += 10.0f * fElapsedTime; + + // Reset pan, pitch, and volume + if(keyboard.GetKey(olc::Key::R).bPressed) + { + pan = 0.0f; + pitch = 1.0f; + volume = 1.0f; + distance = 0.0f; + } + + // panning + pan = std::clamp(pan, -1.0f, 1.0f); + song1.SetPan(pan); + + // pitch + pitch = std::clamp(pitch, 0.0f, 2.0f); + song1.SetPitch(pitch); + + // volume + volume = std::clamp(volume, 0.0f, 1.0f); + song1.SetVolume(volume); + + // 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); + + // // get float cursor. 0.0f to 1.0f + cursorFloat = song1.GetCursorFloat(); + // // get cursor in milliseconds + cursorMillis = song1.GetCursor(); + + // Clear whole screen + draw.Clear(olc::Colour::BLACK); + + if(song1.IsPlaying()) + draw.Clear(olc::Colour::VERY_DARK_BLUE); + + draw.String( + {10, 10}, + "--CONTROLS---------INFORMATION--------------\n" + "\n" + " - = | Pan <" + std::to_string(pan) + ">\n" + "\n" + " [ ] | Pitch <" + std::to_string(pitch) + ">\n" + "\n" + " Up Down | Volume <" + std::to_string(volume) + ">\n" + "\n" + " Left Right | Distance <" + std::to_string(distance) + ">\n" + "\n" + " S | One-Off Sounds\n" + "\n" + " K1 | BackgroundPlay <" + ((backgroundPlay) ? "On": "Off") + ">\n", + olc::Colour::WHITE + ); + + draw.String( + {480, 10}, + "Controls - Waveform""\n" + "-------------------" + "\n\n" + " K7 | Sine" + "\n\n" + " K8 | Square" + "\n\n" + " K9 | Triangle" + "\n\n" + " K0 | Sawtooth", + olc::Colour::WHITE + ); + + olc::vi2d center = ScreenSize() / 2; + olc::vf2d scale{2.8f, 2.8f}; + std::string demoMessage = "olcPGEX3_miniaudio 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 To Toggle Playback"; + draw.String( + center - (draw.GetTextSize(demoMessage, false, scale) / 2), + demoMessage, + olc::Colour::WHITE, + scale + ); + + demoMessage = "Hit TO Reset Pan/Pitch/Volume"; + draw.String( + center + olc::vi2d{0, 24} - (draw.GetTextSize(demoMessage, false, scale) / 2), + demoMessage, + olc::Colour::WHITE, + scale + ); + + draw.String( + {10, 288}, + "Cursor (ms): " + std::to_string(cursorMillis) + "\n" + "Cursor (float): " + std::to_string(cursorFloat), + olc::Colour::WHITE + ); + + draw.String( + {10, 320}, + "Music: Joy Ride [Full version] by MusicLFiles\n" + "Free download: https://filmmusic.io/song/11627-joy-ride-full-version\n" + "Licensed under CC BY 4.0: https://filmmusic.io/standard-license\n", + olc::Colour::WHITE + ); + + // Draw The Playback Cursor (aka the position in the sound file) + draw.FilledRect({0, 350}, {ScreenSize().x * cursorFloat, 20}, olc::Colour::YELLOW); + +#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: + // sounds + olc::ext::Miniaudio::Sound song1; + olc::ext::Miniaudio::Sound sample; + + olc::ext::Miniaudio::Waveform sine; + olc::ext::Miniaudio::Waveform square; + olc::ext::Miniaudio::Waveform triangle; + olc::ext::Miniaudio::Waveform sawtooth; + + // For demonstration controls, with sensible default values + float pan = 0.0f; + float pitch = 1.0f; + float volume = 1.0f; + float distance = 0.0f; + bool backgroundPlay = false; + ma_uint64 cursorMillis = 0ull; + float cursorFloat = 0.0f; + +}; + + +// Main entry point for the application +int main() +{ + // Construct demo application + Example_Miniaudio 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; +} diff --git a/extensions/miniaudio/olcPGEX3_Miniaudio.h b/extensions/miniaudio/olcPGEX3_Miniaudio.h new file mode 100644 index 0000000..7d1145c --- /dev/null +++ b/extensions/miniaudio/olcPGEX3_Miniaudio.h @@ -0,0 +1,1037 @@ +#pragma once +/* + olcPGEX3_MiniAudio.h + + +-------------------------------------------------------------+ + | OneLoneCoder Pixel Game Engine Extension | + | Miniaudio v3.0 | + +-------------------------------------------------------------+ + + What is this? + ~~~~~~~~~~~~~ + This extension abstracts the very robust and powerful miniaudio + library. It provides simple loading and playback of WAV and MP3 + files. Because it's built on top of miniaudio, it requires next + to no addictional build configurations in order to be built + for cross-platform. + + License (OLC-3) + ~~~~~~~~~~~~~~~ + + Copyright 2023-2026 Moros Smith + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions or derivations of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + 2. Redistributions or derivative works in binary form must reproduce the above + copyright notice. This list of conditions and the following disclaimer must be + reproduced in the documentation and/or other materials provided with the distribution. + + 3. Neither the name of the copyright holder nor the names of its contributors may + be used to endorse or promote products derived from this software without specific + prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT + SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR + BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + SUCH DAMAGE. + + Links + ~~~~~ + YouTube: https://www.youtube.com/@Moros1138 + GitHub: https://www.github.com/Moros1138 + Homepage: https://moros1138.com +*/ +#if defined(OLC_MULTIHEADER) +#include "olcpge3.h" +#else +#include "olcPixelGameEngine3.h" +#endif + +#ifdef OLC_PGEX3_MINIAUDIO +#define MINIAUDIO_IMPLEMENTATION +#include "miniaudio.h" +#endif + +#include +#include +#include +#include +#include +#include + +namespace olc::ext::Miniaudio +{ + class AudioEngine; + +#pragma region Sound + + class Sound + { + friend class AudioEngine; + public: + Sound() = default; + ~Sound(); + + 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 be set to loop + void Play(const bool looping = false); + // 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; + }; +#pragma endregion + +#pragma region Waveform + + + class Waveform + { + friend class AudioEngine; + + public: + enum class Type + { + Sine, + Square, + 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}; + }; + +#pragma endregion + +#pragma region Miniaudio + + class AudioEngine : public olc::PGESystemExtension + { + friend class Sound; + friend class Waveform; + + public: + struct Config + { + // device: number of channels. default(2) + int DeviceChannels{2}; + // device: format of the audio data. default(ma_format_f32) + ma_format DeviceFormat{ma_format_f32}; + // device: sample rate. default(48000) + int DeviceSampleRate{ma_standard_sample_rate_48000}; + // device: type of device. default(ma_device_type_playback) + ma_device_type DeviceType{ma_device_type_playback}; + // Miniaudio: is background play enabled? default(false) + bool BackgroundPlay{false}; + // 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. + void EnableBackgroundPlayback(); + // disable playback when the application window does not have focus. + void DisableBackgroundPlayback(); + + public: // Callback + static void data_callback(ma_device* pDevice, void* pOutput, const void* pInput, ma_uint32 frameCount); + + 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); + // Create a sound resource based on a sound file asset in memory + 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); + + public: // Waveforms + bool CreateWaveform(Waveform& waveform, const Waveform::Type type, const double amplitude, const double frequency); + void DestroyWaveform(Waveform& waveform); + public: // Synth + void SetSynthCallback(std::function callback); + void ClearSynthCallback(); + public: // Absolute POWER!! + void SetDataCallback(std::function callback); + void ClearDataCallback(); + + public: // getters + // + ma_device& GetDevice(); + ma_engine& GetEngine(); + ma_resource_manager& GetResourceManager(); + + int GetDeviceChannels() const; + ma_format GetDeviceFormat() const; + int GetDeviceSampleRate() const; + ma_device_type GetDeviceType() const; + + public: + 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; + ma_device_config m_device_config; + + ma_resource_manager m_resource_manager; + ma_resource_manager_config m_resource_manager_config; + + ma_engine m_engine; + ma_engine_config m_engine_config; + std::vector m_waveform_buffer; + + // synth callback function + std::function m_synth_callback; + // data callback function + std::function m_data_callback; + + // track sounds and waveforms + std::vector m_sounds; + std::vector m_waveforms; + + bool m_is_initialized{false}; + olc::PixelGameEngine* m_pge{nullptr}; + }; +} +#pragma endregion + + +#if defined(OLC_PGEX3_MINIAUDIO) +#undef OLC_PGEX3_MINIAUDIO + +namespace olc::ext::Miniaudio +{ + +#pragma region Sound + + uint32_t Sound::m_id_tracker = 0; + + Sound::~Sound() + { + DestroySound(); + } + + bool Sound::CreateSoundFromFile(const std::string& sFileName, AudioEngine* pgex, uint32_t nNumVoices) + { +#if OLC_HOST == OLC_HOST_ANDROID + AAsset* pAsset = AAssetManager_open( + olc::host::Host_Android::androidApp->activity->assetManager, + sFileName.c_str(), + AASSET_MODE_BUFFER + ); + + if (pAsset == nullptr) + return false; + + off_t size = AAsset_getLength(pAsset); + m_buffer.resize(size); + AAsset_read(pAsset, m_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()); + f.seekg(0); + f.read(reinterpret_cast(m_buffer.data()), m_buffer.size()); + f.close(); +#endif + m_pgex = pgex; + m_num_voices = nNumVoices; + return _internalSoundLoader(); + } + + bool Sound::CreateSoundFromMemory(const uint8_t* data, const size_t bytes, AudioEngine* pgex, uint32_t nNumVoices) + { + if(!data) return false; + if(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()) + return false; + + m_pgex = pgex; + m_num_voices = nNumVoices; + return _internalSoundLoader(); + } + + bool Sound::CreateSoundFromMemory(const std::vector& data, AudioEngine* pgex, uint32_t nNumVoices) + { + 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; + } + + bool Sound::_internalSoundLoader() + { + ma_result result; + m_id = ++m_id_tracker; + m_virtual_path = "sound/" + std::to_string(m_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() + ); + + if(result != MA_SUCCESS) + return false; + + ma_fence fence; + result = ma_fence_init(&fence); + + if(result != MA_SUCCESS) + { + ma_resource_manager_unregister_data(ma_engine_get_resource_manager(&m_pgex->GetEngine()), m_virtual_path.c_str()); + return false; + } + + result = ma_sound_init_from_file( + &m_pgex->GetEngine(), + m_virtual_path.c_str(), + MA_SOUND_FLAG_DECODE, + nullptr, + &fence, + &m_base_sound + ); + + if(result != MA_SUCCESS) + { + ma_resource_manager_unregister_data(ma_engine_get_resource_manager(&m_pgex->GetEngine()), m_virtual_path.c_str()); + return false; + } + + m_voices.resize(m_num_voices); + for(int i = 0; i < m_num_voices; ++i) + { + result = ma_sound_init_copy(&m_pgex->GetEngine(), &m_base_sound, 0, nullptr, &m_voices[i]); + if(result != MA_SUCCESS) + break; + } + + // if the last result out of that loop isn't success, we failed + if(result != MA_SUCCESS) + { + for(auto& v : m_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()); + return false; + } + + // wait here until the sound is fully loaded and dedoded + 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); + + m_is_loaded = true; + return true; + } + + // plays a sound, can be set to loop + void Sound::Play(const bool looping) + { + if(!m_is_paused) + m_current_voice = (m_current_voice + 1) % m_num_voices; + + 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; + } + + // stops a sound, rewinds to beginning + void Sound::Stop() + { + if(!ma_sound_is_playing(&m_voices[m_current_voice])) + return; + ma_sound_stop(&m_voices[m_current_voice]); + ma_sound_seek_to_pcm_frame(&m_voices[m_current_voice], 0); + } + + // pauses a sound, does not change position + void Sound::Pause() + { + if(!ma_sound_is_playing(&m_voices[m_current_voice])) + return; + + ma_sound_stop(&m_voices[m_current_voice]); + m_is_paused = true; + } + + // toggle between play and pause + void Sound::Toggle() + { + if(ma_sound_is_playing(&m_voices[m_current_voice])) + { + ma_sound_stop(&m_voices[m_current_voice]); + m_is_paused = true; + return; + } + + ma_sound_start(&m_voices[m_current_voice]); + m_is_paused = false; + } + + // seek to the provided position in the sound, by milliseconds + void Sound::Seek(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); + } + + // seek to the provided position in the sound, by float 0.f is beginning, 1.0f is end + void Sound::Seek(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); + } + + // seek forward from current position by the provided time + void Sound::Forward(const ma_uint64 milliseconds) + { + 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); + + // calculate the step and add it to the current position + frame_to_seek_to += ((milliseconds * m_pgex->GetDeviceSampleRate()) / 1000); + + // seek to the new position + ma_sound_seek_to_pcm_frame(&m_voices[m_current_voice], frame_to_seek_to); + } + + // seek forward from current position by the provided time + void Sound::Rewind(const ma_uint64 milliseconds) + { + 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); + + // calculate the step and add it to the current position + frame_to_seek_to -= ((milliseconds * m_pgex->GetDeviceSampleRate()) / 1000); + + // seek to the new position + ma_sound_seek_to_pcm_frame(&m_voices[m_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) + { + for(auto& v : m_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) + { + for(auto& v : m_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) + { + for(auto& v : m_voices) + { + ma_sound_set_pitch(&v, std::max({0.0f, pitch})); + } + } + + // determine if a sound is playing + bool Sound::IsPlaying() + { + for(auto& v : m_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 cursor; + ma_sound_get_cursor_in_pcm_frames(&m_voices[m_current_voice], &cursor); + return (cursor * 1000) / m_pgex->GetDeviceSampleRate(); + } + + // gets the current position in the sound, as a float between 0.0f and 1.0f + float Sound::GetCursorFloat() + { + float cursor; + ma_sound_get_cursor_in_seconds(&m_voices[m_current_voice], &cursor); + return cursor / m_length_in_seconds; + } + + // determine if this sound has been loaded successfully + bool Sound::IsLoaded() const + { + return m_is_loaded; + } + + ma_sound* Sound::GetMASound() + { + // realistically, one wouldn't call this unless it was loaded + if(!IsLoaded()) + 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 we're playing, get the pointer of the current voice + return &m_voices[m_current_voice]; + } + +#pragma endregion + + +#pragma region Waveform + + bool Waveform::CreateWaveform(Waveform& waveform, const Type type, const double amplitude, const double frequency, AudioEngine* pgex) + { + m_pgex = pgex; + m_waveform_config = ma_waveform_config_init( + m_pgex->GetDeviceFormat(), + m_pgex->GetDeviceChannels(), + m_pgex->GetDeviceSampleRate(), + static_cast(type), + amplitude, + frequency + ); + + m_rampStep = 1.0f / (m_pgex->GetDeviceSampleRate() * 0.02f); + + if(ma_waveform_init(&m_waveform_config, &m_waveform) != MA_SUCCESS) + { + return false; + } + + m_is_loaded = true; + 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() + { + if(!IsLoaded()) + return; + + m_target = 0.0f; + } + + + void Waveform::SetAmplitude(const double amplitude) + { + if(!IsLoaded()) + return; + ma_waveform_set_amplitude(&m_waveform, amplitude); + } + + void Waveform::SetFrequency(const double frequency) + { + if(!IsLoaded()) + 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)); + } + + bool Waveform::IsPlaying() const + { + return m_target > 0.0f || m_gain > 0.0f; + } + + bool Waveform::IsLoaded() const + { + return m_is_loaded; + } + + ma_waveform& Waveform::Get() + { + 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"; + return; + } + + m_cfg = cfg; + } + + void AudioEngine::EnableBackgroundPlayback() + { + 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"}; + + if(!ma->m_cfg.BackgroundPlay && !ma->m_pge->IsFocused()) + return; + + // with great power comes... + if(ma->m_data_callback) + { + ma->m_data_callback((float*)pOutput, frameCount); + return; + } + + 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); + + 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()); + + 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++) + { + 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); + } + + void AudioEngine::DestroySound(Sound& sound) + { + sound.DestroySound(); + + m_sounds.erase( + std::remove_if( + m_sounds.begin(), + m_sounds.end(), + [&](Sound* s) { return (s == &sound); } + ), + m_sounds.end() + ); + } + + bool AudioEngine::CreateWaveform(Waveform& waveform, const Waveform::Type type, const double amplitude, const double frequency) + { + m_waveforms.push_back(&waveform); + return waveform.CreateWaveform(waveform, type, amplitude, frequency, this); + } + + void AudioEngine::DestroyWaveform(Waveform& waveform) + { + waveform.DestroyWaveform(); + m_waveforms.erase( + std::remove_if( + m_waveforms.begin(), + m_waveforms.end(), + [&](Waveform* w) { return (w == &waveform); } + ), + m_waveforms.end() + ); + } + + void AudioEngine::SetSynthCallback(std::function callback) + { + m_synth_callback = callback; + } + + void AudioEngine::ClearSynthCallback() + { + m_synth_callback = {}; + } + + void AudioEngine::SetDataCallback(std::function callback) + { + m_data_callback = callback; + } + + void AudioEngine::ClearDataCallback() + { + m_data_callback = {}; + } + + ma_device& AudioEngine::GetDevice() + { + return m_device; + } + + ma_engine& AudioEngine::GetEngine() + { + return m_engine; + } + + ma_resource_manager& AudioEngine::GetResourceManager() + { + return m_resource_manager; + } + + int AudioEngine::GetDeviceChannels() const + { + return m_cfg.DeviceChannels; + } + + ma_format AudioEngine::GetDeviceFormat() const + { + return m_cfg.DeviceFormat; + } + + int AudioEngine::GetDeviceSampleRate() const + { + return m_cfg.DeviceSampleRate; + } + + ma_device_type AudioEngine::GetDeviceType() const + { + return m_cfg.DeviceType; + } + + bool AudioEngine::OnInstall([[maybe_unused]] olc::PixelGameEngine* pge) + { + m_pge = pge; + + m_device_config = ma_device_config_init(GetDeviceType()); + m_device_config.playback.format = GetDeviceFormat(); + m_device_config.playback.channels = GetDeviceChannels(); + m_device_config.sampleRate = GetDeviceSampleRate(); + m_device_config.dataCallback = AudioEngine::data_callback; + m_device_config.pUserData = this; + + if(ma_device_init(NULL, &m_device_config, &m_device) != MA_SUCCESS) + { + std::cerr << "PGEX3_Miniaudio: failed to initialize device\n"; + 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(); + + #ifdef __EMSCRIPTEN__ + m_resource_manager_config.jobThreadCount = 0; + m_resource_manager_config.flags |= MA_RESOURCE_MANAGER_FLAG_NON_BLOCKING; + m_resource_manager_config.flags |= MA_RESOURCE_MANAGER_FLAG_NO_THREADING; + #endif + + if(ma_resource_manager_init(&m_resource_manager_config, &m_resource_manager) != MA_SUCCESS) + { + std::cerr <<"PGEX3_Miniaudio: failed to initialize resource manager\n"; + return false; + } + + m_engine_config = ma_engine_config_init(); + m_engine_config.pDevice = &m_device; + m_engine_config.pResourceManager = &m_resource_manager; + + if(ma_engine_init(&m_engine_config, &m_engine) != MA_SUCCESS) + { + std::cerr << "PGEX3_Miniaudio: failed to initialize engine\n"; + return false; + } + m_is_initialized = true; + return true; + } + + bool AudioEngine::OnBeforeUserCreate([[maybe_unused]] olc::PixelGameEngine* pge) + { + return true; + } + + bool AudioEngine::OnAfterUserCreate([[maybe_unused]] olc::PixelGameEngine* pge) + { + return true; + } + + bool AudioEngine::OnBeforeSystemUpdate([[maybe_unused]] olc::PixelGameEngine* pge, [[maybe_unused]] float fElapsedTime) + { + #if OLC_HOST == OLC_HOST_EMSCRIPTEN + ma_resource_manager_process_next_job(&m_resource_manager); + #endif + + return true; + } + + bool AudioEngine::OnAfterSystemUpdate([[maybe_unused]] olc::PixelGameEngine* pge, [[maybe_unused]] float fElapsedTime) + { + return true; + } +#pragma endregion + +} +#endif