#pragma once /* olcPGEX_MiniAudio.h +-------------------------------------------------------------+ | OneLoneCoder Pixel Game Engine Extension | | MiniAudio v1.5 | +-------------------------------------------------------------+ NOTE: UNDER ACTIVE DEVELOPMENT - THERE MAY BE BUGS/GLITCHES 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© 2024 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://www.moros1138.com */ #include "olcPixelGameEngine.h" #include #ifdef OLC_PGEX_MINIAUDIO #define MINIAUDIO_IMPLEMENTATION #endif #include "miniaudio.h" namespace olc { class MiniAudio : public olc::PGEX { public: std::string name = "olcPGEX_MiniAudio v1.5"; public: MiniAudio(); ~MiniAudio(); virtual bool OnBeforeUserUpdate(float& fElapsedTime) override; static void data_callback(ma_device* pDevice, void* pOutput, const void* pInput, ma_uint32 frameCount); static bool backgroundPlay; public: // CONFIGURATION // set whether audio will continue playing when the app has lost focus void SetBackgroundPlay(bool state); public: // LOADING ROUTINES enum SoundEffectFlag{ SFX, BGM, }; const size_t LoadSound(const std::string& path,const SoundEffectFlag soundType=BGM); //Setting sound effect to true avoids loading it from a resource pack. const size_t LoadResource(const std::string& path); void UnloadSound(const int id); public: // PLAYBACK CONTROLS // plays a sample, can be set to loop void Play(const int id, const bool loop = false); // plays a sound file, as a one off, and automatically unloads it void Play(const std::string& path,const float&vol=1.0f,const float&pan=0.0f,const float&pitch=1.0f); // stops a sample, rewinds to beginning void Stop(const int id); // pauses a sample, does not change position void Pause(const int id); // toggle between play and pause void Toggle(const int id, bool rewind = false); public: // SEEKING CONTROLS // seek to the provided position in the sound, by milliseconds void Seek(const int id, const unsigned long long milliseconds); // seek to the provided position in the sound, by float 0.f is beginning, 1.0f is end void Seek(const int id, const float& location); // seek forward from current position by the provided time void Forward(const int id, const unsigned long long milliseconds); // seek forward from current position by the provided time void Rewind(const int id, const unsigned long long milliseconds); public: // MISC CONTROLS // set volume of a sound, 0.0f is mute, 1.0f is full void SetVolume(const int id, const float& volume); // set pan of a sound, -1.0f is left, 1.0f is right, 0.0f is center void SetPan(const int id, const float& pan); // set pitch of a sound, 1.0f is normal void SetPitch(const int id, const float& pitch); public: // MISC INFORMATION // determine if a sound is playing bool IsPlaying(const int id); // gets the current position in the sound, in milliseconds unsigned long long GetCursorMilliseconds(const int id); // gets the current position in the sound, as a float between 0.0f and 1.0f float GetCursorFloat(const int id); public: // ADVANCED FEATURES for those who want to use more of miniaudio // gets the currently loaded persistent sounds const std::vector& GetSounds() const; // gets the currently loaded one-off sounds const std::vector& GetOneOffSounds() const; // gets a pointer to the ma_device ma_device* GetDevice(); // gets a pointer to the ma_engine ma_engine* GetEngine(); private: /* Soooo, i'm not going to spend a whole lot of time documenting miniaudio features, if you want to know more I invite you to visit their very very nicely documented webiste at: https://miniaud.io/docs/manual/index.html */ ma_device device; ma_engine engine; ma_resource_manager resourceManager; // sample rate for the device and engine int sampleRate; // this is where the sounds are kept std::vector vecSounds; std::vector vecOneOffSounds; std::vector>vecResourcePackBuffers; }; /** * EXCEPTIONS, long story short. I needed to be able * to construct the PGEX in a state where it could * fail at runtime. If you have a better way of * accomplishing the PGEX pattern without using * exceptions, I'm open to suggestions! */ struct MiniAudioDeviceException : public std::exception { const char* what() const throw() { return "Failed to initialize a device."; } }; struct MiniAudioResourceManagerException : public std::exception { const char* what() const throw() { return "Failed to initialize the resource manager."; } }; struct MiniAudioEngineException : public std::exception { const char* what() const throw() { return "Failed to initialize the audio engine."; } }; struct MiniAudioSoundException : public std::exception { const char* what() const throw() { return "Failed to initialize a sound."; } }; } #ifdef OLC_PGEX_MINIAUDIO #undef OLC_PGEX_MINIAUDIO #include "AdventuresInLestoria.h" INCLUDE_game namespace olc { bool MiniAudio::backgroundPlay = false; MiniAudio::MiniAudio() : olc::PGEX(true) { sampleRate = 48000; ma_device_config deviceConfig = ma_device_config_init(ma_device_type_playback); deviceConfig.playback.format = ma_format_f32; deviceConfig.playback.channels = 2; deviceConfig.sampleRate = sampleRate; deviceConfig.dataCallback = MiniAudio::data_callback; deviceConfig.pUserData = &engine; if(ma_device_init(NULL, &deviceConfig, &device) != MA_SUCCESS) throw MiniAudioDeviceException(); ma_resource_manager_config resourceManagerConfig = ma_resource_manager_config_init(); resourceManagerConfig.decodedFormat = ma_format_f32; resourceManagerConfig.decodedChannels = 0; resourceManagerConfig.decodedSampleRate = sampleRate; #ifdef __EMSCRIPTEN__ resourceManagerConfig.jobThreadCount = 0; resourceManagerConfig.flags |= MA_RESOURCE_MANAGER_FLAG_NON_BLOCKING; resourceManagerConfig.flags |= MA_RESOURCE_MANAGER_FLAG_NO_THREADING; #endif if(ma_resource_manager_init(&resourceManagerConfig, &resourceManager) != MA_SUCCESS) throw MiniAudioResourceManagerException(); ma_engine_config engineConfig = ma_engine_config_init(); engineConfig.pDevice = &device; engineConfig.pResourceManager = &resourceManager; if(ma_engine_init(&engineConfig, &engine) != MA_SUCCESS) throw MiniAudioEngineException(); MiniAudio::backgroundPlay = false; } MiniAudio::~MiniAudio() { for(auto sound : vecSounds) { if(sound != nullptr) { ma_sound_uninit(sound); delete sound; } } ma_resource_manager_uninit(&resourceManager); ma_engine_uninit(&engine); } bool MiniAudio::OnBeforeUserUpdate(float& fElapsedTime) { #ifdef __EMSCRIPTEN__ ma_resource_manager_process_next_job(&resourceManager); #endif for(int i = 0; i < vecOneOffSounds.size(); i++) { if(!ma_sound_is_playing(vecOneOffSounds.at(i))) { ma_sound_uninit(vecOneOffSounds.at(i)); vecOneOffSounds.erase(vecOneOffSounds.begin() + i); break; } } return false; } void MiniAudio::data_callback(ma_device* pDevice, void* pOutput, const void* pInput, ma_uint32 frameCount) { if(!MiniAudio::backgroundPlay && !pge->IsFocused()) return; ma_engine_read_pcm_frames((ma_engine*)(pDevice->pUserData), pOutput, frameCount, NULL); } void MiniAudio::SetBackgroundPlay(bool state) { MiniAudio::backgroundPlay = state; } const size_t MiniAudio::LoadSound(const std::string& path,const SoundEffectFlag soundType) { // create the sound ma_sound* sound = new ma_sound(); // assume no empty slots... size_t id = vecSounds.size(); bool foundSound{false}; // attempt to re-use an empty slot for(int i = 0; i < vecSounds.size(); i++) { if(vecSounds.at(i) == nullptr) { vecSounds.at(i) = sound; id=i; foundSound=true; break; } } if(!foundSound)vecSounds.emplace_back(sound); if(soundType==BGM){ ma_audio_buffer newBuffer{}; ResourceBuffer rb{game->gamepack.GetFileBuffer(path)}; short*decodedOggFile; int numSamples{stb_vorbis_decode_memory((const unsigned char*)(rb.vMemory.data()),rb.vMemory.size(),(int*)(&device.playback.channels),(int*)(&device.sampleRate),&decodedOggFile)}; if(numSamples==-1)ERR(std::format("Failed to decode Ogg Vorbis file! {}",path)); LOG(std::format("Samples: {}, Channels: {}, Sample Rate: {}",numSamples,device.playback.channels,device.sampleRate)); ma_audio_buffer_config config{ma_audio_buffer_config_init(device.playback.format,device.playback.channels,numSamples,decodedOggFile,nullptr)}; if(ma_audio_buffer_init(&config,&newBuffer)!=MA_SUCCESS)ERR(std::format("WARNING! Failed to load audio buffer~! {}",path)); if(ma_sound_init_from_data_source(&engine,&newBuffer,MA_SOUND_FLAG_DECODE|MA_SOUND_FLAG_ASYNC,nullptr,sound)!=MA_SUCCESS)ERR(std::format("Could not initialize sound! {}",path)); vecResourcePackBuffers.emplace_back(std::pair{id,newBuffer}); }else{ //Sound effects // load it from the file and decode it if(ma_sound_init_from_file(&engine, path.c_str(), MA_SOUND_FLAG_DECODE | MA_SOUND_FLAG_ASYNC, NULL, NULL, sound) != MA_SUCCESS) throw MiniAudioSoundException(); } return id; } void MiniAudio::UnloadSound(const int id) { ma_sound_uninit(vecSounds.at(id)); delete vecSounds.at(id); vecSounds.at(id) = nullptr; std::erase_if(vecResourcePackBuffers,[&id](const std::pair&bufferData){return id==bufferData.first;}); } void MiniAudio::Play(const int id, const bool loop) { if(ma_sound_is_playing(vecSounds.at(id))) { ma_sound_seek_to_pcm_frame(vecSounds.at(id), 0); return; } ma_sound_set_looping(vecSounds.at(id), loop); ma_sound_start(vecSounds.at(id)); } void MiniAudio::Play(const std::string& path,const float&vol,const float&pan,const float&pitch) { // create the sound ma_sound* sound = new ma_sound(); // load it from the file and decode it if(ma_sound_init_from_file(&engine, path.c_str(), MA_SOUND_FLAG_DECODE | MA_SOUND_FLAG_ASYNC, NULL, NULL, sound) != MA_SUCCESS) throw MiniAudioSoundException(); ma_sound_set_volume(sound,vol); ma_sound_set_pan(sound,pan); ma_sound_set_pitch(sound,pitch); ma_sound_start(sound); vecOneOffSounds.push_back(sound); } void MiniAudio::Stop(const int id) { ma_sound_seek_to_pcm_frame(vecSounds.at(id), 0); ma_sound_stop(vecSounds.at(id)); } void MiniAudio::Pause(const int id) { auto it = vecSounds.begin() + id; ma_sound_stop(vecSounds.at(id)); } void MiniAudio::Toggle(const int id, bool rewind) { if(ma_sound_is_playing(vecSounds.at(id))) { ma_sound_stop(vecSounds.at(id)); if(rewind) ma_sound_seek_to_pcm_frame(vecSounds.at(id), 0); return; } ma_sound_start(vecSounds.at(id)); } void MiniAudio::Seek(const int id, const unsigned long long milliseconds) { unsigned long long frame = (milliseconds * engine.sampleRate) / 1000; ma_sound_seek_to_pcm_frame(vecSounds.at(id), frame); } void MiniAudio::Seek(const int id, const float& location) { unsigned long long length; ma_sound_get_length_in_pcm_frames(vecSounds.at(id), &length); unsigned long long frame = length * location; ma_sound_seek_to_pcm_frame(vecSounds.at(id), frame); } void MiniAudio::Forward(const int id, const unsigned long long milliseconds) { unsigned long long cursor; ma_sound_get_cursor_in_pcm_frames(vecSounds.at(id), &cursor); unsigned long long frame = (milliseconds * engine.sampleRate) / 1000; ma_sound_seek_to_pcm_frame(vecSounds.at(id), cursor + frame); } void MiniAudio::Rewind(const int id, const unsigned long long milliseconds) { unsigned long long cursor; ma_sound_get_cursor_in_pcm_frames(vecSounds.at(id), &cursor); unsigned long long frame = (milliseconds * engine.sampleRate) / 1000; ma_sound_seek_to_pcm_frame(vecSounds.at(id), cursor - frame); } void MiniAudio::SetVolume(const int id, const float& volume) { ma_sound_set_volume(vecSounds.at(id), volume); } void MiniAudio::SetPan(const int id, const float& pan) { ma_sound_set_pan(vecSounds.at(id), pan); } void MiniAudio::SetPitch(const int id, const float& pitch) { ma_sound_set_pitch(vecSounds.at(id), pitch); } unsigned long long MiniAudio::GetCursorMilliseconds(const int id) { unsigned long long cursor; ma_sound_get_cursor_in_pcm_frames(vecSounds.at(id), &cursor); cursor *= 1000; cursor /= sampleRate; return cursor; } bool MiniAudio::IsPlaying(const int id) { return ma_sound_is_playing(vecSounds.at(id)); } float MiniAudio::GetCursorFloat(const int id) { unsigned long long cursor; ma_sound_get_cursor_in_pcm_frames(vecSounds.at(id), &cursor); unsigned long long length; ma_sound_get_length_in_pcm_frames(vecSounds.at(id), &length); return (float)cursor / length; } const std::vector& MiniAudio::GetSounds() const { return vecSounds; } const std::vector& MiniAudio::GetOneOffSounds() const { return vecOneOffSounds; } ma_device* MiniAudio::GetDevice() { return &device; } ma_engine* MiniAudio::GetEngine() { return &engine; } } // olc #endif