diff --git a/Resources/music/bright.ogg b/Resources/music/bright.ogg new file mode 100644 index 0000000..85c905b Binary files /dev/null and b/Resources/music/bright.ogg differ diff --git a/Resources/music/main_song.ogg b/Resources/music/main_song.ogg index 71dde7b..0f0104d 100644 Binary files a/Resources/music/main_song.ogg and b/Resources/music/main_song.ogg differ diff --git a/Resources/music/short_song.ogg b/Resources/music/short_song.ogg new file mode 100644 index 0000000..415b10a Binary files /dev/null and b/Resources/music/short_song.ogg differ diff --git a/YuppleMayham/CMakeLists.txt b/YuppleMayham/CMakeLists.txt index 7560675..f49a3b8 100644 --- a/YuppleMayham/CMakeLists.txt +++ b/YuppleMayham/CMakeLists.txt @@ -9,6 +9,7 @@ pkg_check_modules(LuaJIT REQUIRED IMPORTED_TARGET GLOBAL luajit) find_package(sol2 REQUIRED) find_package(tinyxml2 REQUIRED) find_package(Freetype REQUIRED) +find_package(OpenAL REQUIRED) find_package(glm CONFIG REQUIRED) @@ -32,6 +33,9 @@ find_package(glm CONFIG REQUIRED) add_executable (YuppleMayham "src/main.cpp" "src/thirdparty/glad.c" + "src/sound/engine.cpp" + "src/sound/audiostream.cpp" + "include/thirdparty/stb_vorbis.c" "src/utility/data/font_data.c" "src/utility/ftfont.cpp" "src/graphics/sprite.cpp" @@ -98,6 +102,6 @@ endif() target_include_directories(YuppleMayham PRIVATE "${PROJECT_SOURCE_DIR}/YuppleMayham/include" ${LuaJIT_INCLUDE_DIRS} ${FREETYPE_INCLUDE_DIR_ft2build}) -target_link_libraries(YuppleMayham SDL2::SDL2main SDL2::SDL2 SDL2_image::SDL2_image glm::glm-header-only sol2 tinyxml2 freetype ${LuaJIT_LINK_LIBRARIES}) +target_link_libraries(YuppleMayham SDL2::SDL2main SDL2::SDL2 SDL2_image::SDL2_image openal glm::glm-header-only sol2 tinyxml2 freetype ${LuaJIT_LINK_LIBRARIES}) # TODO: Add tests and install targets if needed. diff --git a/YuppleMayham/include/gameplay/game.h b/YuppleMayham/include/gameplay/game.h index 69b515c..6015be5 100644 --- a/YuppleMayham/include/gameplay/game.h +++ b/YuppleMayham/include/gameplay/game.h @@ -10,6 +10,7 @@ class Scene; class Text; class ResourceManager; class Renderer; +class AudioEngine; class GLWindow; enum { @@ -38,7 +39,7 @@ public: const unsigned getWindowWidth() const; const unsigned getWindowHeight() const; - void quit() { game_state = GAME_QUITTING; } + void quit(); ~Game(); private: @@ -51,6 +52,7 @@ private: std::shared_ptr resourceManager; std::shared_ptr renderer; std::shared_ptr textHandler; + std::shared_ptr audioEngine; std::shared_ptr globalEventManager; }; diff --git a/YuppleMayham/include/sound/audiostream.h b/YuppleMayham/include/sound/audiostream.h new file mode 100644 index 0000000..98d9d53 --- /dev/null +++ b/YuppleMayham/include/sound/audiostream.h @@ -0,0 +1,63 @@ +#ifndef _H_AUDIO_STREAM_H +#define _H_AUDIO_STREAM_H + +#define STB_VORBIS_HEADER_ONLY +#include "thirdparty/stb_vorbis.c" + +#include "utility/logger.h" + +#include +#include +#include +#include +#include +#include + +namespace AUDIO { + constexpr size_t CHUNK_SIZE = 4096; + constexpr int SAMPLE_RATE = 44100; +} + +class AudioStream +{ +public: + AudioStream(); + + bool PlayStream(); + void AddToQueue(std::string songName); + void PauseStream() { paused = !paused; } + + void kill() { stopThread = true; } + ~AudioStream(); +private: + std::queue songNames; + std::mutex mutex; + std::atomic stopThread; + std::atomic paused; + std::string CurStreamName(); + bool eof; + std::thread streamThread; + + int loadInitalChunk(); + int loadNextChunk(); + + int loadChunk(ALuint buffer); + + void popQueue(); + void cycleQueue(); + + bool openFile(std::string fileName); + + void stream(); + + short *data; + stb_vorbis *file; + stb_vorbis_info info; + ALuint buffers[4]; + ALuint source; + ALint state; + ALint format; + size_t last_buffer; +}; + +#endif // _H_AUDIO_STREAM_H diff --git a/YuppleMayham/include/sound/engine.h b/YuppleMayham/include/sound/engine.h new file mode 100644 index 0000000..3d5bab6 --- /dev/null +++ b/YuppleMayham/include/sound/engine.h @@ -0,0 +1,34 @@ +#ifndef _H_AUDIO_ENGINE_H +#define _H_AUDIO_ENGINE_H + +#include +#include +#include + +#include "utility/events.h" +#include "sound/audiostream.h" +#include "utility/resourcemanager.h" +#include "utility/logger.h" + +class AudioEngine +{ +public: + AudioEngine(std::weak_ptr _resource); + + void hookEventManager(std::weak_ptr _events); + void pushMusic(std::string _songName); + void playMusic(); + void pauseMusic(); + void killMusic(); + + ~AudioEngine(); +private: + std::unique_ptr musicPlayer; + std::weak_ptr globalEventManager; + std::weak_ptr resourceManager; + ALCdevice *device; + ALCcontext *context; +}; + + +#endif // _H_AUDIO_ENGINE_H diff --git a/YuppleMayham/src/gameplay/game.cpp b/YuppleMayham/src/gameplay/game.cpp index 1adf334..e1b0aea 100644 --- a/YuppleMayham/src/gameplay/game.cpp +++ b/YuppleMayham/src/gameplay/game.cpp @@ -13,6 +13,8 @@ #include "graphics/glwindow.h" +#include "sound/engine.h" + #include #include #include @@ -54,6 +56,14 @@ bool Game::init() globalEventManager = std::make_shared(); resourceManager = std::make_shared(); renderer = std::make_shared(resourceManager); + audioEngine = std::make_shared(resourceManager); + audioEngine->hookEventManager(globalEventManager); + /* Testing */ + audioEngine->pushMusic("music/short_song.ogg"); + audioEngine->pushMusic("music/bright.ogg"); + audioEngine->pushMusic("music/main_song.ogg"); + audioEngine->playMusic(); + /* */ renderer->hookEventManager(globalEventManager); textHandler = std::make_shared(); if (!textHandler->loadFonts("fonts")) @@ -108,6 +118,14 @@ void Game::render() window->swap(); } +void Game::quit() +{ + game_state = GAME_QUITTING; + if (audioEngine) { + audioEngine->killMusic(); + } +} + Game::~Game() { resourceManager->clearResources(); diff --git a/YuppleMayham/src/sound/audiostream.cpp b/YuppleMayham/src/sound/audiostream.cpp new file mode 100644 index 0000000..3c1b2dd --- /dev/null +++ b/YuppleMayham/src/sound/audiostream.cpp @@ -0,0 +1,193 @@ +#include "sound/audiostream.h" +#include +#include + +AudioStream::AudioStream() +{ + alGenBuffers(4, buffers); + alGenSources(1, &source); + data = (short *)std::malloc(AUDIO::CHUNK_SIZE * sizeof(short) * 2); // Make space for stereo even if we are playing mono + stopThread = false; + eof = true; + paused = true; + streamThread = std::thread(&AudioStream::stream, this); + streamThread.detach(); +} + +std::string AudioStream::CurStreamName() +{ + std::lock_guard lock(mutex); + return songNames.front(); +} + +bool AudioStream::PlayStream() +{ + if (CurStreamName().empty()) { + LOG(WARN, "Song queue empty!", NULL); + return false; + } + LOG(INFO, "Current song is: {}", CurStreamName()); + return (paused = true); +} + +void AudioStream::AddToQueue(std::string songName) +{ + std::lock_guard lock(mutex); + LOG(INFO, "Adding song {} to queue", songName); + songNames.push(songName); +} + +bool AudioStream::openFile(std::string fileName) +{ + int error = 0; + file = stb_vorbis_open_filename(CurStreamName().c_str(), &error, NULL); + if (file == nullptr || error != STBVorbisError::VORBIS__no_error) { + LOG(ERROR, "Failed to open file: '{}' error code: {}", CurStreamName(), error); + return false; + } + info = stb_vorbis_get_info(file); + if (info.sample_rate != AUDIO::SAMPLE_RATE) { + LOG(ERROR, "Failed to open file: '{}', make sure to convert sample rate to 44100!", CurStreamName()); + stb_vorbis_close(file); + return false; + } + stb_vorbis_seek_start(file); + std::free(data); + data = (short *)std::malloc(AUDIO::CHUNK_SIZE * sizeof(short) * info.channels); + return true; +} + +int AudioStream::loadChunk(ALuint buffer) +{ + int sample = 0; + if (file == NULL) { + return sample; + } + if (info.channels == 0) { + return sample; + } + sample = stb_vorbis_get_samples_short_interleaved(file, info.channels, data, AUDIO::CHUNK_SIZE); + if (sample == 0) + return sample; + format = info.channels == 1 ? AL_FORMAT_MONO16 : AL_FORMAT_STEREO16; + //LOG(INFO, "Buffer id {}, Format {}, data size {}, sample rate {},\nfilename {}", + // buffer, format == AL_FORMAT_MONO16 ? "MONO" : "STEREO", sample * info.channels * sizeof(short), info.sample_rate, songNames.front()); + alBufferData(buffer, format, data, sample * info.channels * sizeof(short), info.sample_rate); + + return sample; +} + +int AudioStream::loadInitalChunk() +{ + int samples = 0; + if (!openFile(CurStreamName())) { + return samples; + } + for (int i = 0; i < 4; i++) { + samples = loadChunk(buffers[i]); + if (samples == 0) { + LOG(ERROR, "Music file {} too small!", CurStreamName()); + break; + } + } + return samples; +} + +int AudioStream::loadNextChunk() +{ + int samples = loadChunk(buffers[last_buffer]); + return samples; +} + +void AudioStream::popQueue() +{ + std::lock_guard lock(mutex); + if (songNames.empty()) + return; + songNames.pop(); +} + +void AudioStream::cycleQueue() +{ + std::lock_guard lock(mutex); + LOG(INFO, "Cycling queue", NULL); + if (songNames.empty()) + return; + std::string buffer = songNames.front(); + songNames.pop(); + songNames.push(buffer); + LOG(INFO, "New front: {}", songNames.front()); +} + +void AudioStream::stream() +{ + int processed = 0; + int queued = 0; + bool inital = true; + + std::this_thread::sleep_for(std::chrono::seconds(1)); + + //LOG(INFO, "Call from in audio thread!", NULL); + //LOG(INFO, "Top of queue is: {}", CurStreamName()); + paused = false; + + while (!stopThread) { + if (songNames.empty() || paused) { + LOG(INFO, "Paused or queue empty", NULL); + continue; + } + alGetSourcei(source, AL_SOURCE_STATE, &state); + if (inital) { + int samples = loadInitalChunk(); + if (samples == 0) { + LOG(ERROR, "Music file '{}' either failed to load or not big enough!", CurStreamName()); + popQueue(); + continue; + } + alSourceQueueBuffers(source, 4, buffers); + alSourcePlay(source); + alGetSourcei(source, AL_SOURCE_STATE, &state); + inital = false; + eof = false; + } + // If we are at the end of file, the and stream queue has a song in it + // Load in the inital buffers + if (state != AL_PLAYING) { + alSourcePlay(source); + } + while (state == AL_PLAYING) { + if (paused) { + alSourcePause(source); + } + alGetSourcei(source, AL_SOURCE_STATE, &state); + alGetSourcei(source, AL_BUFFERS_PROCESSED, &processed); + alGetSourcei(source, AL_BUFFERS_QUEUED, &queued); + + while (processed != 0) { + alSourceUnqueueBuffers(source, 1, &buffers[last_buffer]); + int samples = loadNextChunk(); + if (samples == 0) { + LOG(INFO, "Hit the end of the song!", NULL); + cycleQueue(); + stb_vorbis_close(file); + while (!openFile(CurStreamName())) { + LOG(ERROR, "Failed to open next in stream! {}", CurStreamName()); + popQueue(); + } + } else { + } + //LOG(INFO, "Buffer index: {}", last_buffer); + alSourceQueueBuffers(source, 1, &buffers[last_buffer]); + last_buffer = (last_buffer + 1) % 4; + processed--; + } + } + } +} + +AudioStream::~AudioStream() +{ + std::free(data); + alDeleteBuffers(4, buffers); + alDeleteSources(1, &source); +} diff --git a/YuppleMayham/src/sound/engine.cpp b/YuppleMayham/src/sound/engine.cpp new file mode 100644 index 0000000..af56968 --- /dev/null +++ b/YuppleMayham/src/sound/engine.cpp @@ -0,0 +1,52 @@ +#include "sound/engine.h" +#include +#include + +AudioEngine::AudioEngine(std::weak_ptr _resource) +{ + // Open the default device for now + if ((device = alcOpenDevice(NULL)) == NULL) { + LOG(ERROR, "Failed to open default device {}", alGetError()); + return; + } + context = alcCreateContext(device, NULL); + alcMakeContextCurrent(context); + musicPlayer = std::make_unique(); + resourceManager = _resource; + alGetError(); +} + +void AudioEngine::hookEventManager(std::weak_ptr _events) +{ + globalEventManager = _events; +} + +void AudioEngine::pushMusic(std::string _songName) +{ + LOG(INFO, "Loading song {}", _songName); + musicPlayer->AddToQueue(_songName); +} + +void AudioEngine::playMusic() +{ + LOG(INFO, "Playing stream", NULL); + musicPlayer->PlayStream(); +} + +void AudioEngine::pauseMusic() +{ + musicPlayer->PauseStream(); +} + +void AudioEngine::killMusic() +{ + musicPlayer->kill(); +} + +AudioEngine::~AudioEngine() +{ + musicPlayer->kill(); + alcMakeContextCurrent(NULL); + alcDestroyContext(context); + alcCloseDevice(device); +}