diff --git a/.github/workflows/appimage.yml b/.github/workflows/appimage.yml index f799077e..398c3b5c 100644 --- a/.github/workflows/appimage.yml +++ b/.github/workflows/appimage.yml @@ -23,7 +23,7 @@ jobs: - name: install dependencies run: | sudo apt-get update - sudo apt-get install -y build-essential libglfw3-dev libglfw3 libglew-dev libglm-dev libpng-dev libopenal-dev libluajit-5.1-dev cmake squashfs-tools + sudo apt-get install -y build-essential libglfw3-dev libglfw3 libglew-dev libglm-dev libpng-dev libopenal-dev libluajit-5.1-dev libvorbis-dev cmake squashfs-tools sudo ln -s /usr/lib/x86_64-linux-gnu/libluajit-5.1.a /usr/lib/x86_64-linux-gnu/liblua5.1.a sudo ln -s /usr/include/luajit-2.1 /usr/include/lua - name: configure diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index bd01c399..075c4b74 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -35,7 +35,7 @@ jobs: # make && make install INSTALL_INC=/usr/include/lua run: | sudo apt-get update - sudo apt-get install libglfw3-dev libglfw3 libglew-dev libglm-dev libpng-dev libopenal-dev libluajit-5.1-dev + sudo apt-get install libglfw3-dev libglfw3 libglew-dev libglm-dev libpng-dev libopenal-dev libluajit-5.1-dev libvorbis-dev sudo ln -s /usr/lib/x86_64-linux-gnu/libluajit-5.1.a /usr/lib/x86_64-linux-gnu/liblua-5.1.a sudo ln -s /usr/include/luajit-2.1 /usr/include/lua diff --git a/CMakeLists.txt b/CMakeLists.txt index b772fb72..82a81fec 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -89,10 +89,12 @@ if (WIN32) find_package(glfw3 REQUIRED) find_package(spng REQUIRED) find_package(glm REQUIRED) + find_package(vorbis REQUIRED) set(PNGLIB spng::spng) else() find_package(Lua REQUIRED) set(PNGLIB spng) + set(VORBISLIB vorbis vorbisfile) # not tested add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/libs/glfw) endif() else() @@ -100,6 +102,7 @@ else() find_package(Lua REQUIRED) find_package(PNG REQUIRED) set(PNGLIB PNG::PNG) + set(VORBISLIB vorbis vorbisfile) endif() if (APPLE) @@ -118,7 +121,7 @@ if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") endif() include_directories(${LUA_INCLUDE_DIR}) -target_link_libraries(${PROJECT_NAME} ${LIBS} glfw OpenGL::GL ${OPENAL_LIBRARY} GLEW::GLEW ZLIB::ZLIB ${PNGLIB} ${LUA_LIBRARIES} ${CMAKE_DL_LIBS}) +target_link_libraries(${PROJECT_NAME} ${LIBS} glfw OpenGL::GL ${OPENAL_LIBRARY} GLEW::GLEW ZLIB::ZLIB ${VORBISLIB} ${PNGLIB} ${LUA_LIBRARIES} ${CMAKE_DL_LIBS}) file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/res DESTINATION ${CMAKE_CURRENT_BINARY_DIR}) diff --git a/Dockerfile b/Dockerfile index 3785e02c..ad2fce9f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,6 +21,7 @@ RUN apt-get update && apt-get install --no-install-recommends -y \ libpng-dev \ libopenal-dev \ libluajit-5.1-dev \ + libvorbis-dev \ ca-certificates \ && rm -rf /var/lib/apt/lists/* diff --git a/README.md b/README.md index f8c94187..bf2a6e18 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ cmake --build . #### Debian-based distro: ```sh -sudo apt install libglfw3-dev libglfw3 libglew-dev libglm-dev libpng-dev libopenal-dev libluajit-5.1-dev +sudo apt install libglfw3-dev libglfw3 libglew-dev libglm-dev libpng-dev libopenal-dev libluajit-5.1-dev libvorbis-dev ``` CMake missing LUA_INCLUDE_DIR and LUA_LIBRARIES fix: @@ -44,7 +44,7 @@ sudo ln -s /usr/include/luajit-2.1 /usr/include/lua #### RHEL-based distro: ```sh -sudo dnf install glfw-devel glfw glew-devel glm-devel libpng-devel openal-devel +sudo dnf install glfw-devel glfw glew-devel glm-devel libpng-devel libvorbis-devel openal-devel ``` \+ install LuaJIT @@ -52,12 +52,12 @@ sudo dnf install glfw-devel glfw glew-devel glm-devel libpng-devel openal-devel #### Arch-based distro: If you use X11 ```sh -sudo pacman -S glfw-x11 glew glm libpng openal +sudo pacman -S glfw-x11 glew glm libpng libvorbis openal ``` If you use Wayland ```sh -sudo pacman -S glfw-wayland glew glm libpng openal +sudo pacman -S glfw-wayland glew glm libpng libvorbis openal ``` \+ install LuaJIT @@ -72,7 +72,7 @@ make && sudo make install INSTALL_INC=/usr/include/lua #### macOS: ``` -brew install glfw3 glew glm libpng lua luajit openal-soft +brew install glfw3 glew glm libpng libvorbis lua luajit openal-soft ``` If homebrew for some reason could not install the necessary packages: ```lua luajit openal-soft```, then download, install and compile them manually (Lua, LuaJIT and OpenAL). diff --git a/dev/AppImageBuilder.yml b/dev/AppImageBuilder.yml index 567dd872..4025fa72 100644 --- a/dev/AppImageBuilder.yml +++ b/dev/AppImageBuilder.yml @@ -29,6 +29,9 @@ AppDir: - libopengl0 - libasound2 - libglx0 + - libogg0 + - libvorbis0a + - libvorbisfile3 exclude: - hicolor-icon-theme - sound-theme-freedesktop diff --git a/src/assets/Assets.cpp b/src/assets/Assets.cpp index 5183cca2..f05cda5f 100644 --- a/src/assets/Assets.cpp +++ b/src/assets/Assets.cpp @@ -1,5 +1,6 @@ #include "Assets.h" +#include "../audio/audio.h" #include "../graphics/Texture.h" #include "../graphics/Shader.h" #include "../graphics/Atlas.h" @@ -18,7 +19,7 @@ Texture* Assets::getTexture(std::string name) const { } void Assets::store(Texture* texture, std::string name){ - textures[name].reset(texture); + textures.emplace(name, texture); } @@ -30,7 +31,7 @@ Shader* Assets::getShader(std::string name) const{ } void Assets::store(Shader* shader, std::string name){ - shaders[name].reset(shader); + shaders.emplace(name, shader); } @@ -42,7 +43,7 @@ Font* Assets::getFont(std::string name) const { } void Assets::store(Font* font, std::string name){ - fonts[name].reset(font); + fonts.emplace(name, font); } Atlas* Assets::getAtlas(std::string name) const { @@ -53,7 +54,18 @@ Atlas* Assets::getAtlas(std::string name) const { } void Assets::store(Atlas* atlas, std::string name){ - atlases[name].reset(atlas); + atlases.emplace(name, atlas); +} + +audio::Sound* Assets::getSound(std::string name) const { + auto found = sounds.find(name); + if (found == sounds.end()) + return nullptr; + return found->second.get(); +} + +void Assets::store(audio::Sound* sound, std::string name) { + sounds.emplace(name, sound); } const std::vector& Assets::getAnimations() { @@ -72,7 +84,7 @@ UiDocument* Assets::getLayout(std::string name) const { } void Assets::store(UiDocument* layout, std::string name) { - layouts[name].reset(layout); + layouts.emplace(name, layout); } void Assets::extend(const Assets& assets) { @@ -91,6 +103,9 @@ void Assets::extend(const Assets& assets) { for (auto entry : assets.layouts) { layouts[entry.first] = entry.second; } + for (auto entry : assets.sounds) { + sounds[entry.first] = entry.second; + } animations.clear(); for (auto entry : assets.animations) { animations.emplace_back(entry); diff --git a/src/assets/Assets.h b/src/assets/Assets.h index 87e94380..f085ea7e 100644 --- a/src/assets/Assets.h +++ b/src/assets/Assets.h @@ -14,11 +14,9 @@ class Font; class Atlas; class UiDocument; -struct LayoutCfg { - int env; - - LayoutCfg(int env) : env(env) {} -}; +namespace audio { + class Sound; +} class Assets { std::unordered_map> textures; @@ -26,6 +24,7 @@ class Assets { std::unordered_map> fonts; std::unordered_map> atlases; std::unordered_map> layouts; + std::unordered_map> sounds; std::vector animations; public: ~Assets(); @@ -41,6 +40,9 @@ public: Atlas* getAtlas(std::string name) const; void store(Atlas* atlas, std::string name); + audio::Sound* getSound(std::string name) const; + void store(audio::Sound* sound, std::string name); + const std::vector& getAnimations(); void store(const TextureAnimation& animation); diff --git a/src/assets/AssetsLoader.cpp b/src/assets/AssetsLoader.cpp index 23604f5f..cd8f6d3e 100644 --- a/src/assets/AssetsLoader.cpp +++ b/src/assets/AssetsLoader.cpp @@ -24,7 +24,7 @@ void AssetsLoader::addLoader(int tag, aloader_func func) { loaders[tag] = func; } -void AssetsLoader::add(int tag, const std::string filename, const std::string alias, std::shared_ptr settings) { +void AssetsLoader::add(int tag, const std::string filename, const std::string alias, std::shared_ptr settings) { entries.push(aloader_entry{ tag, filename, alias, settings}); } diff --git a/src/assets/AssetsLoader.h b/src/assets/AssetsLoader.h index de401846..17826fa8 100644 --- a/src/assets/AssetsLoader.h +++ b/src/assets/AssetsLoader.h @@ -7,24 +7,41 @@ #include #include -const short ASSET_TEXTURE = 1; -const short ASSET_SHADER = 2; -const short ASSET_FONT = 3; -const short ASSET_ATLAS = 4; -const short ASSET_LAYOUT = 5; +inline constexpr short ASSET_TEXTURE = 1; +inline constexpr short ASSET_SHADER = 2; +inline constexpr short ASSET_FONT = 3; +inline constexpr short ASSET_ATLAS = 4; +inline constexpr short ASSET_LAYOUT = 5; +inline constexpr short ASSET_SOUND = 6; class ResPaths; class Assets; class AssetsLoader; class Content; -using aloader_func = std::function)>; +struct AssetCfg { + virtual ~AssetCfg() {} +}; + +struct LayoutCfg : AssetCfg { + int env; + + LayoutCfg(int env) : env(env) {} +}; + +struct SoundCfg : AssetCfg { + bool keepPCM; + + SoundCfg(bool keepPCM) : keepPCM(keepPCM) {} +}; + +using aloader_func = std::function)>; struct aloader_entry { int tag; const std::string filename; const std::string alias; - std::shared_ptr config; + std::shared_ptr config; }; class AssetsLoader { @@ -39,7 +56,7 @@ public: int tag, const std::string filename, const std::string alias, - std::shared_ptr settings=nullptr + std::shared_ptr settings=nullptr ); diff --git a/src/assets/assetload_funcs.cpp b/src/assets/assetload_funcs.cpp index 5a7c6809..7c351cf3 100644 --- a/src/assets/assetload_funcs.cpp +++ b/src/assets/assetload_funcs.cpp @@ -4,6 +4,7 @@ #include #include "Assets.h" #include "AssetsLoader.h" +#include "../audio/audio.h" #include "../files/files.h" #include "../files/engine_paths.h" #include "../coders/png.h" @@ -25,7 +26,7 @@ bool assetload::texture( const ResPaths* paths, const std::string filename, const std::string name, - std::shared_ptr + std::shared_ptr ) { std::unique_ptr texture( png::load_texture(paths->find(filename).u8string()) @@ -44,7 +45,7 @@ bool assetload::shader( const ResPaths* paths, const std::string filename, const std::string name, - std::shared_ptr + std::shared_ptr ) { fs::path vertexFile = paths->find(filename+".glslv"); fs::path fragmentFile = paths->find(filename+".glslf"); @@ -92,7 +93,7 @@ bool assetload::atlas( const ResPaths* paths, const std::string directory, const std::string name, - std::shared_ptr + std::shared_ptr ) { AtlasBuilder builder; for (const auto& file : paths->listdir(directory)) { @@ -112,7 +113,7 @@ bool assetload::font( const ResPaths* paths, const std::string filename, const std::string name, - std::shared_ptr + std::shared_ptr ) { std::vector> pages; for (size_t i = 0; i <= 4; i++) { @@ -138,10 +139,10 @@ bool assetload::layout( const ResPaths* paths, const std::string file, const std::string name, - std::shared_ptr config + std::shared_ptr config ) { try { - LayoutCfg* cfg = reinterpret_cast(config.get()); + auto cfg = dynamic_cast(config.get()); auto document = UiDocument::read(loader, cfg->env, name, file); assets->store(document.release(), name); return true; @@ -152,6 +153,25 @@ bool assetload::layout( } } +bool assetload::sound( + AssetsLoader& loader, + Assets* assets, + const ResPaths* paths, + const std::string file, + const std::string name, + std::shared_ptr config +) { + auto cfg = dynamic_cast(config.get()); + auto sound = audio::loadSound(paths->find(file), cfg->keepPCM); + if (sound == nullptr) { + std::cerr << "failed to load sound '" << name << "' from '"; + std::cerr << file << "'" << std::endl; + return false; + } + assets->store(sound, name); + return true; +} + bool assetload::animation(Assets* assets, const ResPaths* paths, const std::string directory, diff --git a/src/assets/assetload_funcs.h b/src/assets/assetload_funcs.h index a9ef5d26..893ba2a5 100644 --- a/src/assets/assetload_funcs.h +++ b/src/assets/assetload_funcs.h @@ -8,6 +8,7 @@ class ResPaths; class Assets; class AssetsLoader; class Atlas; +struct AssetCfg; namespace assetload { bool texture( @@ -16,7 +17,7 @@ namespace assetload { const ResPaths* paths, const std::string filename, const std::string name, - std::shared_ptr settings + std::shared_ptr settings ); bool shader( AssetsLoader&, @@ -24,7 +25,7 @@ namespace assetload { const ResPaths* paths, const std::string filename, const std::string name, - std::shared_ptr settings + std::shared_ptr settings ); bool atlas( AssetsLoader&, @@ -32,7 +33,7 @@ namespace assetload { const ResPaths* paths, const std::string directory, const std::string name, - std::shared_ptr settings + std::shared_ptr settings ); bool font( AssetsLoader&, @@ -40,7 +41,7 @@ namespace assetload { const ResPaths* paths, const std::string filename, const std::string name, - std::shared_ptr settings + std::shared_ptr settings ); bool layout( AssetsLoader&, @@ -48,7 +49,16 @@ namespace assetload { const ResPaths* paths, const std::string file, const std::string name, - std::shared_ptr settings + std::shared_ptr settings + ); + + bool sound( + AssetsLoader&, + Assets*, + const ResPaths* paths, + const std::string file, + const std::string name, + std::shared_ptr settings ); bool animation( diff --git a/src/audio/AL/ALAudio.cpp b/src/audio/AL/ALAudio.cpp new file mode 100644 index 00000000..8cbf01e4 --- /dev/null +++ b/src/audio/AL/ALAudio.cpp @@ -0,0 +1,365 @@ +#include "ALAudio.h" +#include "alutil.h" +#include +#include + +using namespace audio; + +ALSound::ALSound(ALAudio* al, uint buffer, std::shared_ptr pcm, bool keepPCM) +: al(al), buffer(buffer) +{ + duration = pcm->getDuration(); + if (keepPCM) { + this->pcm = pcm; + } +} + +ALSound::~ALSound() { + al->freeBuffer(buffer); + buffer = 0; +} + +Speaker* ALSound::newInstance(int priority) const { + uint source = al->getFreeSource(); + if (source == 0) { + return nullptr; + } + AL_CHECK(alSourcei(source, AL_BUFFER, buffer)); + return new ALSpeaker(al, source, priority); +} + +ALStream::ALStream(ALAudio* al, std::shared_ptr source, bool keepSource) +: al(al), source(source), keepSource(keepSource) { +} + +ALStream::~ALStream() { + bindSpeaker(0); + source = nullptr; +} + +std::shared_ptr ALStream::getSource() const { + if (keepSource) { + return source; + } else { + return nullptr; + } +} + +bool ALStream::preloadBuffer(uint buffer, bool loop) { + size_t read = source->readFully(this->buffer, BUFFER_SIZE, loop); + if (!read) + return false; + ALenum format = AL::to_al_format(source->getChannels(), source->getBitsPerSample()); + AL_CHECK(alBufferData(buffer, format, this->buffer, read, source->getSampleRate())); + return true; +} + +Speaker* ALStream::createSpeaker(bool loop) { + this->loop = loop; + uint source = al->getFreeSource(); + if (source == 0) { + return nullptr; + } + for (uint i = 0; i < ALStream::STREAM_BUFFERS; i++) { + uint buffer = al->getFreeBuffer(); + if (!preloadBuffer(buffer, loop)) { + break; + } + AL_CHECK(alSourceQueueBuffers(source, 1, &buffer)); + } + return new ALSpeaker(al, source, PRIORITY_HIGH); +} + + +void ALStream::bindSpeaker(speakerid_t speaker) { + auto sp = audio::get(this->speaker); + if (sp) { + sp->stop(); + } + this->speaker = speaker; +} + +speakerid_t ALStream::getSpeaker() const { + return speaker; +} + +void ALStream::update(double delta) { + if (this->speaker == 0) { + return; + } + Speaker* speaker = audio::get(this->speaker); + if (speaker == nullptr) { + speaker = 0; + return; + } + ALSpeaker* alspeaker = dynamic_cast(speaker); + uint source = alspeaker->source; + uint processed = AL::getSourcei(source, AL_BUFFERS_PROCESSED); + + while (processed--) { + uint buffer; + AL_CHECK(alSourceUnqueueBuffers(source, 1, &buffer)); + unusedBuffers.push(buffer); + } + + uint preloaded = 0; + if (!unusedBuffers.empty()) { + uint buffer = unusedBuffers.front(); + if (preloadBuffer(buffer, loop)) { + preloaded++; + unusedBuffers.pop(); + AL_CHECK(alSourceQueueBuffers(source, 1, &buffer)); + } + } + if (speaker->isStopped() && !speaker->isStoppedManually()) { + if (preloaded) { + speaker->play(); + } else { + speaker->stop(); + } + } +} + +void ALStream::setTime(duration_t time) { + // TODO: implement +} + +ALSpeaker::ALSpeaker(ALAudio* al, uint source, int priority) : al(al), priority(priority), source(source) { +} + +ALSpeaker::~ALSpeaker() { + if (source) { + stop(); + } +} + +State ALSpeaker::getState() const { + int state = AL::getSourcei(source, AL_SOURCE_STATE, AL_STOPPED); + switch (state) { + case AL_PLAYING: return State::playing; + case AL_PAUSED: return State::paused; + default: return State::stopped; + } +} + +float ALSpeaker::getVolume() const { + return AL::getSourcef(source, AL_GAIN); +} + +void ALSpeaker::setVolume(float volume) { + AL_CHECK(alSourcef(source, AL_GAIN, volume)); +} + +float ALSpeaker::getPitch() const { + return AL::getSourcef(source, AL_PITCH); +} + +void ALSpeaker::setPitch(float pitch) { + AL_CHECK(alSourcef(source, AL_PITCH, pitch)); +} + +bool ALSpeaker::isLoop() const { + return AL::getSourcei(source, AL_LOOPING) == AL_TRUE; +} + +void ALSpeaker::setLoop(bool loop) { + AL_CHECK(alSourcei(source, AL_LOOPING, loop ? AL_TRUE : AL_FALSE)); +} + +void ALSpeaker::play() { + stoppedManually = false; + AL_CHECK(alSourcePlay(source)); +} + +void ALSpeaker::pause() { + AL_CHECK(alSourcePause(source)); +} + +void ALSpeaker::stop() { + stoppedManually = true; + if (source) { + AL_CHECK(alSourceStop(source)); + al->freeSource(source); + source = 0; + } +} + +bool ALSpeaker::isStoppedManually() const { + return stoppedManually; +} + +duration_t ALSpeaker::getTime() const { + return static_cast(AL::getSourcef(source, AL_SEC_OFFSET)); +} + +void ALSpeaker::setTime(duration_t time) { + AL_CHECK(alSourcef(source, AL_SEC_OFFSET, static_cast(time))); +} + +void ALSpeaker::setPosition(glm::vec3 pos) { + AL_CHECK(alSource3f(source, AL_POSITION, pos.x, pos.y, pos.z)); +} + +glm::vec3 ALSpeaker::getPosition() const { + return AL::getSource3f(source, AL_POSITION); +} + +void ALSpeaker::setVelocity(glm::vec3 vel) { + AL_CHECK(alSource3f(source, AL_VELOCITY, vel.x, vel.y, vel.z)); +} + +glm::vec3 ALSpeaker::getVelocity() const { + return AL::getSource3f(source, AL_VELOCITY); +} + +void ALSpeaker::setRelative(bool relative) { + AL_CHECK(alSourcei(source, AL_SOURCE_RELATIVE, relative ? AL_TRUE : AL_FALSE)); +} + +bool ALSpeaker::isRelative() const { + return AL::getSourcei(source, AL_SOURCE_RELATIVE) == AL_TRUE; +} + +int ALSpeaker::getPriority() const { + return priority; +} + + +ALAudio::ALAudio(ALCdevice* device, ALCcontext* context) +: device(device), context(context) +{ + ALCint size; + alcGetIntegerv(device, ALC_ATTRIBUTES_SIZE, 1, &size); + std::vector attrs(size); + alcGetIntegerv(device, ALC_ALL_ATTRIBUTES, size, &attrs[0]); + for (size_t i = 0; i < attrs.size(); ++i){ + if (attrs[i] == ALC_MONO_SOURCES) { + std::cout << "AL: max mono sources: " << attrs[i+1] << std::endl; + maxSources = attrs[i+1]; + } + } + auto devices = getAvailableDevices(); + std::cout << "AL devices:" << std::endl; + for (auto& name : devices) { + std::cout << " " << name << std::endl; + } +} + +ALAudio::~ALAudio() { + for (uint source : allsources) { + int state = AL::getSourcei(source, AL_SOURCE_STATE); + if (state == AL_PLAYING || state == AL_PAUSED) { + AL_CHECK(alSourceStop(source)); + } + AL_CHECK(alDeleteSources(1, &source)); + } + + for (uint buffer : allbuffers){ + AL_CHECK(alDeleteBuffers(1, &buffer)); + } + + AL_CHECK(alcMakeContextCurrent(context)); + alcDestroyContext(context); + if (!alcCloseDevice(device)) { + std::cerr << "AL: device not closed!" << std::endl; + } + device = nullptr; + context = nullptr; +} + +Sound* ALAudio::createSound(std::shared_ptr pcm, bool keepPCM) { + auto format = AL::to_al_format(pcm->channels, pcm->bitsPerSample); + uint buffer = getFreeBuffer(); + AL_CHECK(alBufferData(buffer, format, pcm->data.data(), pcm->data.size(), pcm->sampleRate)); + return new ALSound(this, buffer, pcm, keepPCM); +} + +Stream* ALAudio::openStream(std::shared_ptr stream, bool keepSource) { + return new ALStream(this, stream, keepSource); +} + +ALAudio* ALAudio::create() { + ALCdevice* device = alcOpenDevice(nullptr); + if (device == nullptr) + return nullptr; + ALCcontext* context = alcCreateContext(device, nullptr); + if (!alcMakeContextCurrent(context)){ + alcCloseDevice(device); + return nullptr; + } + AL_CHECK(); + std::cout << "AL: initialized" << std::endl; + return new ALAudio(device, context); +} + +uint ALAudio::getFreeSource(){ + if (!freesources.empty()){ + uint source = freesources.back(); + freesources.pop_back(); + return source; + } + if (allsources.size() == maxSources){ + std::cerr << "attempted to create new source, but limit is " << maxSources << std::endl; + return 0; + } + ALuint id; + alGenSources(1, &id); + if (!AL_GET_ERROR()) + return 0; + allsources.push_back(id); + return id; +} + +uint ALAudio::getFreeBuffer(){ + if (!freebuffers.empty()){ + uint buffer = freebuffers.back(); + freebuffers.pop_back(); + return buffer; + } + ALuint id; + alGenBuffers(1, &id); + if (!AL_GET_ERROR()) { + return 0; + } + + allbuffers.push_back(id); + return id; +} + +void ALAudio::freeSource(uint source){ + freesources.push_back(source); +} + +void ALAudio::freeBuffer(uint buffer){ + freebuffers.push_back(buffer); +} + +std::vector ALAudio::getAvailableDevices() const { + std::vector devicesVec; + + const ALCchar* devices; + devices = alcGetString(device, ALC_DEVICE_SPECIFIER); + if (!AL_GET_ERROR()) { + return devicesVec; + } + + const char* ptr = devices; + do { + devicesVec.push_back(std::string(ptr)); + ptr += devicesVec.back().size() + 1; + } + while (ptr[0]); + + return devicesVec; +} + +void ALAudio::setListener(glm::vec3 position, glm::vec3 velocity, glm::vec3 at, glm::vec3 up){ + ALfloat listenerOri[] = { at.x, at.y, at.z, up.x, up.y, up.z }; + + AL_CHECK(alListener3f(AL_POSITION, position.x, position.y, position.z)); + AL_CHECK(alListener3f(AL_VELOCITY, velocity.x, velocity.y, velocity.z)); + AL_CHECK(alListenerfv(AL_ORIENTATION, listenerOri)); +} + +void ALAudio::update(double delta) { +} diff --git a/src/audio/AL/ALAudio.h b/src/audio/AL/ALAudio.h new file mode 100644 index 00000000..e2afd03b --- /dev/null +++ b/src/audio/AL/ALAudio.h @@ -0,0 +1,157 @@ +#ifndef SRC_AUDIO_AUDIO_H_ +#define SRC_AUDIO_AUDIO_H_ + +#include +#include +#include +#include +#include + +#ifdef __APPLE__ +#include +#include +#else +#include +#include +#endif + +#include "../audio.h" +#include "../../typedefs.h" + +namespace audio { + struct ALBuffer; + class ALAudio; + class PCMStream; + + class ALSound : public Sound { + ALAudio* al; + uint buffer; + std::shared_ptr pcm; + duration_t duration; + public: + ALSound(ALAudio* al, uint buffer, std::shared_ptr pcm, bool keepPCM); + ~ALSound(); + + duration_t getDuration() const override { + return duration; + } + + std::shared_ptr getPCM() const override { + return pcm; + } + + Speaker* newInstance(int priority) const override; + }; + + class ALStream : public Stream { + static inline constexpr size_t BUFFER_SIZE = 44100; + + ALAudio* al; + std::shared_ptr source; + std::queue unusedBuffers; + speakerid_t speaker = 0; + bool keepSource; + char buffer[BUFFER_SIZE]; + bool loop = false; + + bool preloadBuffer(uint buffer, bool loop); + public: + ALStream(ALAudio* al, std::shared_ptr source, bool keepSource); + ~ALStream(); + + std::shared_ptr getSource() const override; + void bindSpeaker(speakerid_t speaker) override; + Speaker* createSpeaker(bool loop) override; + speakerid_t getSpeaker() const override; + void update(double delta) override; + void setTime(duration_t time) override; + + static inline constexpr uint STREAM_BUFFERS = 3; + }; + + /// @brief AL source adapter + class ALSpeaker : public Speaker { + ALAudio* al; + int priority; + bool stoppedManually = false; + public: + uint source; + + ALSpeaker(ALAudio* al, uint source, int priority); + ~ALSpeaker(); + + State getState() const override; + + float getVolume() const override; + void setVolume(float volume) override; + + float getPitch() const override; + void setPitch(float pitch) override; + + bool isLoop() const override; + void setLoop(bool loop) override; + + void play() override; + void pause() override; + void stop() override; + bool isStoppedManually() const override; + + duration_t getTime() const override; + void setTime(duration_t time) override; + + void setPosition(glm::vec3 pos) override; + glm::vec3 getPosition() const override; + + void setVelocity(glm::vec3 vel) override; + glm::vec3 getVelocity() const override; + + void setRelative(bool relative) override; + bool isRelative() const override; + + int getPriority() const override; + }; + + class ALAudio : public Backend { + ALCdevice* device; + ALCcontext* context; + + std::vector allsources; + std::vector freesources; + + std::vector allbuffers; + std::vector freebuffers; + + uint maxSources; + + ALAudio(ALCdevice* device, ALCcontext* context); + public: + ~ALAudio(); + + uint getFreeSource(); + uint getFreeBuffer(); + void freeSource(uint source); + void freeBuffer(uint buffer); + + std::vector getAvailableDevices() const; + + Sound* createSound(std::shared_ptr pcm, bool keepPCM) override; + Stream* openStream(std::shared_ptr stream, bool keepSource) override; + + void setListener( + glm::vec3 position, + glm::vec3 velocity, + glm::vec3 lookAt, + glm::vec3 up + ) override; + + void update(double delta) override; + + bool isDummy() const override { + return false; + } + + static ALAudio* create(); + }; +} + +#endif /* SRC_AUDIO_AUDIO_H_ */ diff --git a/src/audio/AL/alutil.cpp b/src/audio/AL/alutil.cpp new file mode 100644 index 00000000..0dcfce3d --- /dev/null +++ b/src/audio/AL/alutil.cpp @@ -0,0 +1,44 @@ +#include "alutil.h" + +#include +#include +#include +#include +#include + +#ifdef __APPLE__ +#include +#include +#else +#include +#include +#endif + +bool AL::check_errors(const std::string& filename, const std::uint_fast32_t line){ + ALenum error = alGetError(); + if(error != AL_NO_ERROR){ + std::cerr << "OpenAL ERROR (" << filename << ": " << line << ")\n" ; + switch(error){ + case AL_INVALID_NAME: + std::cerr << "AL_INVALID_NAME: a bad name (ID) was passed to an OpenAL function"; + break; + case AL_INVALID_ENUM: + std::cerr << "AL_INVALID_ENUM: an invalid enum value was passed to an OpenAL function"; + break; + case AL_INVALID_VALUE: + std::cerr << "AL_INVALID_VALUE: an invalid value was passed to an OpenAL function"; + break; + case AL_INVALID_OPERATION: + std::cerr << "AL_INVALID_OPERATION: the requested operation is not valid"; + break; + case AL_OUT_OF_MEMORY: + std::cerr << "AL_OUT_OF_MEMORY: the requested operation resulted in OpenAL running out of memory"; + break; + default: + std::cerr << "UNKNOWN AL ERROR: " << error; + } + std::cerr << std::endl; + return false; + } + return true; +} diff --git a/src/audio/AL/alutil.h b/src/audio/AL/alutil.h new file mode 100644 index 00000000..f07dbc46 --- /dev/null +++ b/src/audio/AL/alutil.h @@ -0,0 +1,76 @@ +#ifndef SRC_AUDIO_AUDIOUTIL_H_ +#define SRC_AUDIO_AUDIOUTIL_H_ + +#include +#include +#include + +#ifdef __APPLE__ +#include +#else +#include +#endif + +#include +#include "../../typedefs.h" + +#define AL_CHECK(STATEMENT) STATEMENT; AL::check_errors(__FILE__, __LINE__) +#define AL_GET_ERROR() AL::check_errors(__FILE__, __LINE__) + +namespace AL { + bool check_errors(const std::string& filename, const std::uint_fast32_t line); + + /// @brief alGetSourcef wrapper + /// @param source target source + /// @param field enum value + /// @param def default value will be returned in case of error + /// @return field value or default + inline float getSourcef(uint source, ALenum field, float def=0.0f) { + float value = def; + AL_CHECK(alGetSourcef(source, field, &value)); + return value; + } + + /// @brief alGetSource3f wrapper + /// @param source target source + /// @param field enum value + /// @param def default value will be returned in case of error + /// @return field value or default + inline glm::vec3 getSource3f(uint source, ALenum field, glm::vec3 def={}) { + glm::vec3 value = def; + AL_CHECK(alGetSource3f(source, field, &value.x, &value.y, &value.z)); + return value; + } + + /// @brief alGetSourcei wrapper + /// @param source target source + /// @param field enum value + /// @param def default value will be returned in case of error + /// @return field value or default + inline float getSourcei(uint source, ALenum field, int def=0) { + int value = def; + AL_CHECK(alGetSourcei(source, field, &value)); + return value; + } + + static inline ALenum to_al_format(short channels, short bitsPerSample){ + bool stereo = (channels > 1); + + switch (bitsPerSample) { + case 16: + if (stereo) + return AL_FORMAT_STEREO16; + else + return AL_FORMAT_MONO16; + case 8: + if (stereo) + return AL_FORMAT_STEREO8; + else + return AL_FORMAT_MONO8; + default: + return -1; + } + } +} + +#endif /* SRC_AUDIO_AUDIOUTIL_H_ */ diff --git a/src/audio/Audio.cpp b/src/audio/Audio.cpp deleted file mode 100644 index 1074d23c..00000000 --- a/src/audio/Audio.cpp +++ /dev/null @@ -1,193 +0,0 @@ -#include "Audio.h" -#include "audioutil.h" -#include -#include - -#ifdef __APPLE__ -#include -#include -#else -#include -#include -#endif - -ALCdevice* Audio::device; -ALCcontext* Audio::context; -unsigned Audio::maxSources; -unsigned Audio::maxBuffers = 1024; -std::vector Audio::allsources; -std::vector Audio::freesources; -std::vector Audio::allbuffers; -std::vector Audio::freebuffers; - -bool ALSource::setBuffer(ALBuffer* buffer) { - alSourcei(id, AL_BUFFER, buffer->id); - return alCheckErrorsMacro(); -} - -bool ALSource::play(){ - alSourcePlay(id); - return alCheckErrorsMacro(); -} - -bool ALSource::isPlaying() { - int state; - alGetSourcei(id, AL_SOURCE_STATE, &state); - return state == AL_PLAYING; -} - -bool ALSource::setPosition(glm::vec3 position) { - alSource3f(id, AL_POSITION, position.x, position.y, position.z); - return alCheckErrorsMacro(); -} - -bool ALSource::setVelocity(glm::vec3 velocity) { - alSource3f(id, AL_VELOCITY, velocity.x, velocity.y, velocity.z); - return alCheckErrorsMacro(); -} - -bool ALSource::setLoop(bool loop) { - alSourcei(id, AL_LOOPING, AL_TRUE ? loop : AL_FALSE); - return alCheckErrorsMacro(); -} - -bool ALSource::setGain(float gain) { - alSourcef(id, AL_GAIN, gain); - return alCheckErrorsMacro(); -} - - -bool ALSource::setPitch(float pitch) { - alSourcef(id, AL_PITCH, pitch); - return alCheckErrorsMacro(); -} - -bool ALBuffer::load(int format, const char* data, int size, int freq) { - alBufferData(id, format, data, size, freq); - return alCheckErrorsMacro(); -} - - -bool Audio::initialize() { - device = alcOpenDevice(nullptr); - if (device == nullptr) - return false; - context = alcCreateContext(device, nullptr); - if (!alcMakeContextCurrent(context)){ - alcCloseDevice(device); - return false; - } - if (!alCheckErrorsMacro()) - return false; - - ALCint size; - alcGetIntegerv(device, ALC_ATTRIBUTES_SIZE, 1, &size); - std::vector attrs(size); - alcGetIntegerv(device, ALC_ALL_ATTRIBUTES, size, &attrs[0]); - for(size_t i=0; iisPlaying()){ - alSourceStop(source->id); alCheckErrorsMacro(); - } - alDeleteSources(1, &source->id); alCheckErrorsMacro(); - } - - for (ALBuffer* buffer : allbuffers){ - alDeleteBuffers(1, &buffer->id); alCheckErrorsMacro(); - } - - alcMakeContextCurrent(context); - alcDestroyContext(context); - if (!alcCloseDevice(device)){ - std::cerr << "device not closed!" << std::endl; - } - device = nullptr; - context = nullptr; -} - -ALSource* Audio::getFreeSource(){ - if (!freesources.empty()){ - ALSource* source = freesources.back(); - freesources.pop_back(); - return source; - } - if (allsources.size() == maxSources){ - std::cerr << "attempted to create new source, but limit is " << maxSources << std::endl; - return nullptr; - } - ALuint id; - alGenSources(1, &id); - if (!alCheckErrorsMacro()) - return nullptr; - - ALSource* source = new ALSource(id); - allsources.push_back(source); - return source; -} - -ALBuffer* Audio::getFreeBuffer(){ - if (!freebuffers.empty()){ - ALBuffer* buffer = freebuffers.back(); - freebuffers.pop_back(); - return buffer; - } - if (allbuffers.size() == maxBuffers){ - std::cerr << "attempted to create new ALbuffer, but limit is " << maxBuffers << std::endl; - return nullptr; - } - ALuint id; - alGenBuffers(1, &id); - if (!alCheckErrorsMacro()) - return nullptr; - - ALBuffer* buffer = new ALBuffer(id); - allbuffers.push_back(buffer); - return buffer; -} - -void Audio::freeSource(ALSource* source){ - freesources.push_back(source); -} - -void Audio::freeBuffer(ALBuffer* buffer){ - freebuffers.push_back(buffer); -} - -bool Audio::get_available_devices(std::vector& devicesVec){ - const ALCchar* devices; - devices = alcGetString(device, ALC_DEVICE_SPECIFIER); - if (!alCheckErrorsMacro()) - return false; - - const char* ptr = devices; - - devicesVec.clear(); - - do { - devicesVec.push_back(std::string(ptr)); - ptr += devicesVec.back().size() + 1; - } - while(*(ptr + 1) != '\0'); - - return true; -} - -void Audio::setListener(glm::vec3 position, glm::vec3 velocity, glm::vec3 at, glm::vec3 up){ - ALfloat listenerOri[] = { at.x, at.y, at.z, up.x, up.y, up.z }; - - alListener3f(AL_POSITION, position.x, position.y, position.z); - alCheckErrorsMacro(); - alListener3f(AL_VELOCITY, velocity.x, velocity.y, velocity.z); - alCheckErrorsMacro(); - alListenerfv(AL_ORIENTATION, listenerOri); - alCheckErrorsMacro(); -} diff --git a/src/audio/Audio.h b/src/audio/Audio.h deleted file mode 100644 index 087122a6..00000000 --- a/src/audio/Audio.h +++ /dev/null @@ -1,67 +0,0 @@ -#ifndef SRC_AUDIO_AUDIO_H_ -#define SRC_AUDIO_AUDIO_H_ - -#include -#include - -#ifdef __APPLE__ -#include -#include -#else -#include -#include -#endif - -#include - - -struct ALBuffer; - -struct ALSource { - ALuint id; - ALSource(ALuint id) : id(id) {} - - bool isPlaying(); - bool setPosition(glm::vec3 position); - bool setVelocity(glm::vec3 velocity); - bool setBuffer(ALBuffer* buffer); - bool setLoop(bool loop); - bool setGain(float gain); - bool setPitch(float pitch); - bool play(); -}; - -struct ALBuffer { - ALuint id; - ALBuffer(ALuint id) : id(id) {} - bool load(int format, const char* data, int size, int freq); -}; - -class Audio { - static ALCdevice* device; - static ALCcontext* context; - - static std::vector allsources; - static std::vector freesources; - - static std::vector allbuffers; - static std::vector freebuffers; - - static unsigned maxSources; - static unsigned maxBuffers; - -public: - static ALSource* getFreeSource(); - static ALBuffer* getFreeBuffer(); - static void freeSource(ALSource* source); - static void freeBuffer(ALBuffer* buffer); - - static bool initialize(); - static void finalize(); - static bool get_available_devices(std::vector& devicesVec); - - static void setListener(glm::vec3 position, glm::vec3 velocity, glm::vec3 at, glm::vec3 up); - -}; - -#endif /* SRC_AUDIO_AUDIO_H_ */ diff --git a/src/audio/NoAudio.cpp b/src/audio/NoAudio.cpp new file mode 100644 index 00000000..b6193fd0 --- /dev/null +++ b/src/audio/NoAudio.cpp @@ -0,0 +1,22 @@ +#include "NoAudio.h" + +using namespace audio; + +NoSound::NoSound(std::shared_ptr pcm, bool keepPCM) { + duration = pcm->getDuration(); + if (keepPCM) { + this->pcm = pcm; + } +} + +Sound* NoAudio::createSound(std::shared_ptr pcm, bool keepPCM) { + return new NoSound(pcm, keepPCM); +} + +Stream* NoAudio::openStream(std::shared_ptr stream, bool keepSource) { + return new NoStream(stream, keepSource); +} + +NoAudio* NoAudio::create() { + return new NoAudio(); +} diff --git a/src/audio/NoAudio.h b/src/audio/NoAudio.h new file mode 100644 index 00000000..8d91ebd3 --- /dev/null +++ b/src/audio/NoAudio.h @@ -0,0 +1,84 @@ +#ifndef AUDIO_NOAUDIO_H_ +#define AUDIO_NOAUDIO_H_ + +#include "audio.h" + +namespace audio { + class NoSound : public Sound { + std::shared_ptr pcm; + duration_t duration; + public: + NoSound(std::shared_ptr pcm, bool keepPCM); + ~NoSound() {} + + duration_t getDuration() const override { + return duration; + } + + std::shared_ptr getPCM() const override { + return pcm; + } + + Speaker* newInstance(int priority) const override { + return nullptr; + } + }; + + class NoStream : public Stream { + std::shared_ptr source; + duration_t duration; + public: + NoStream(std::shared_ptr source, bool keepSource) { + duration = source->getTotalDuration(); + if (keepSource) { + this->source = source; + } + } + + std::shared_ptr getSource() const override { + return source; + } + + void bindSpeaker(speakerid_t speaker) override { + } + + Speaker* createSpeaker(bool loop) override{ + return nullptr; + } + + speakerid_t getSpeaker() const override { + return 0; + } + + void update(double delta) override { + } + + void setTime(duration_t time) override { + } + }; + + class NoAudio : public Backend { + public: + ~NoAudio() {} + + Sound* createSound(std::shared_ptr pcm, bool keepPCM) override; + Stream* openStream(std::shared_ptr stream, bool keepSource) override; + + void setListener( + glm::vec3 position, + glm::vec3 velocity, + glm::vec3 at, + glm::vec3 up + ) override {} + + void update(double delta) override {} + + bool isDummy() const override { + return true; + } + + static NoAudio* create(); + }; +} + +#endif // AUDIO_NOAUDIO_H_ diff --git a/src/audio/audio.cpp b/src/audio/audio.cpp new file mode 100644 index 00000000..ecb5db2d --- /dev/null +++ b/src/audio/audio.cpp @@ -0,0 +1,310 @@ +#include "audio.h" + +#include +#include + +#include "NoAudio.h" +#include "AL/ALAudio.h" + +#include "../coders/wav.h" +#include "../coders/ogg.h" + +namespace audio { + static speakerid_t nextId = 1; + static Backend* backend; + static std::unordered_map> speakers; + static std::unordered_map> streams; +} + +using namespace audio; + +size_t PCMStream::readFully(char* buffer, size_t bufferSize, bool loop) { + if (!isOpen()) { + return 0; + } + long bytes = 0; + size_t size = 0; + do { + do { + bytes = read(buffer, bufferSize); + if (bytes < 0) { + return size; + } + size += bytes; + bufferSize -= bytes; + buffer += bytes; + } while (bytes > 0 && bufferSize > 0); + + if (bufferSize == 0) { + break; + } + + if (loop) { + seek(0); + } + if (bufferSize == 0) { + return size; + } + } while (loop); + return size; +} + +/// @brief pcm source that does not initialize buffer +class PCMVoidSource : public PCMStream { + size_t totalSamples; + size_t remain; + uint sampleRate; + bool seekable; + bool closed = false; +public: + PCMVoidSource(size_t totalSamples, uint sampleRate, bool seekable) + : totalSamples(totalSamples), + remain(totalSamples), + sampleRate(sampleRate), + seekable(seekable) + {} + + size_t read(char* buffer, size_t bufferSize) override { + if (closed) { + return 0; + } + if (!seekable) { + return bufferSize; + } + size_t n = std::min(bufferSize, totalSamples); + remain -= n; + return n; + } + + void close() override { + closed = true; + } + + bool isOpen() const override { + return !closed; + } + + size_t getTotalSamples() const override { + return totalSamples; + } + + duration_t getTotalDuration() const override { + return static_cast(totalSamples) / + static_cast(sampleRate); + } + + uint getChannels() const override { + return 1; + } + + uint getSampleRate() const override { + return sampleRate; + } + + uint getBitsPerSample() const override { + return 8; + } + + bool isSeekable() const override { + return seekable; + } + + void seek(size_t position) override { + if (closed || !seekable) { + return; + } + position %= totalSamples; + remain = totalSamples - position; + } +}; + +void audio::initialize(bool enabled) { + if (enabled) { + backend = ALAudio::create(); + } + if (backend == nullptr) { + std::cerr << "could not to initialize audio" << std::endl; + backend = NoAudio::create(); + } +} + +PCM* audio::loadPCM(const fs::path& file, bool headerOnly) { + if (!fs::exists(file)) { + throw std::runtime_error("file not found '"+file.u8string()+"'"); + } + std::string ext = file.extension().u8string(); + if (ext == ".wav" || ext == ".WAV") { + return wav::load_pcm(file, headerOnly); + } else if (ext == ".ogg" || ext == ".OGG") { + return ogg::load_pcm(file, headerOnly); + } + throw std::runtime_error("unsupported audio format"); +} + +Sound* audio::loadSound(const fs::path& file, bool keepPCM) { + std::shared_ptr pcm(loadPCM(file, !keepPCM && backend->isDummy())); + return createSound(pcm, keepPCM); +} + +Sound* audio::createSound(std::shared_ptr pcm, bool keepPCM) { + return backend->createSound(pcm, keepPCM); +} + +PCMStream* audio::openPCMStream(const fs::path& file) { + std::string ext = file.extension().u8string(); + if (ext == ".wav" || ext == ".WAV") { + return wav::create_stream(file); + } else if (ext == ".ogg" || ext == ".OGG") { + return ogg::create_stream(file); + } + throw std::runtime_error("unsupported audio stream format"); +} + +Stream* audio::openStream(const fs::path& file, bool keepSource) { + if (!keepSource && backend->isDummy()) { + auto header = loadPCM(file, true); + // using void source sized as audio instead of actual audio file + return openStream( + std::make_shared(header->totalSamples, header->sampleRate, header->seekable), + keepSource + ); + } + return openStream( + std::shared_ptr(openPCMStream(file)), + keepSource + ); +} + +Stream* audio::openStream(std::shared_ptr stream, bool keepSource) { + return backend->openStream(stream, keepSource); +} + + +void audio::setListener( + glm::vec3 position, + glm::vec3 velocity, + glm::vec3 lookAt, + glm::vec3 up +) { + backend->setListener(position, velocity, lookAt, up); +} + +void remove_lower_priority_speaker(int priority) { + for (auto it = speakers.begin(); it != speakers.end();) { + if (it->second->getPriority() < priority && it->second->isPaused()) { + it->second->stop(); + it = speakers.erase(it); + return; + } + it++; + } + for (auto it = speakers.begin(); it != speakers.end();) { + if (it->second->getPriority() < priority) { + it->second->stop(); + it = speakers.erase(it); + return; + } + it++; + } +} + +speakerid_t audio::play( + Sound* sound, + glm::vec3 position, + bool relative, + float volume, + float pitch, + bool loop, + int priority +) { + Speaker* speaker = sound->newInstance(priority); + if (speaker == nullptr) { + remove_lower_priority_speaker(priority); + speaker = sound->newInstance(priority); + } + if (speaker == nullptr) { + return 0; + } + speakerid_t id = nextId++; + speakers.emplace(id, speaker); + speaker->setPosition(position); + speaker->setVolume(volume); + speaker->setPitch(pitch); + speaker->setLoop(loop); + speaker->setRelative(relative); + speaker->play(); + return id; +} + +speakerid_t audio::play( + std::shared_ptr stream, + glm::vec3 position, + bool relative, + float volume, + float pitch, + bool loop +) { + Speaker* speaker = stream->createSpeaker(loop); + if (speaker == nullptr) { + remove_lower_priority_speaker(PRIORITY_HIGH); + speaker = stream->createSpeaker(loop); + } + if (speaker == nullptr) { + return 0; + } + speakerid_t id = nextId++; + streams.emplace(id, stream); + speakers.emplace(id, speaker); + stream->bindSpeaker(id); + + speaker->setPosition(position); + speaker->setVolume(volume); + speaker->setPitch(pitch); + speaker->setLoop(false); + speaker->setRelative(relative); + speaker->play(); + return id; +} + +speakerid_t audio::playStream( + const fs::path& file, + glm::vec3 position, + bool relative, + float volume, + float pitch, + bool loop +) { + std::shared_ptr stream (openStream(file, false)); + return play(stream, position, relative, volume, pitch, loop); +} + +Speaker* audio::get(speakerid_t id) { + auto found = speakers.find(id); + if (found == speakers.end()) { + return nullptr; + } + return found->second.get(); +} + +void audio::update(double delta) { + backend->update(delta); + + for (auto& entry : streams) { + entry.second->update(delta); + } + + for (auto it = speakers.begin(); it != speakers.end();) { + if (it->second->isStoppedManually()) { + streams.erase(it->first); + it = speakers.erase(it); + } else { + it++; + } + } +} + +void audio::close() { + speakers.clear(); + delete backend; + backend = nullptr; +} diff --git a/src/audio/audio.h b/src/audio/audio.h new file mode 100644 index 00000000..1bcb3512 --- /dev/null +++ b/src/audio/audio.h @@ -0,0 +1,404 @@ +#ifndef AUDIO_AUDIO_H_ +#define AUDIO_AUDIO_H_ + +#include +#include +#include +#include +#include "../typedefs.h" + +namespace fs = std::filesystem; + +namespace audio { + using speakerid_t = int64_t; + /// @brief duration unit is second + using duration_t = double; + + constexpr inline int PRIORITY_LOW = 0; + constexpr inline int PRIORITY_NORMAL = 5; + constexpr inline int PRIORITY_HIGH = 10; + + class Speaker; + + /// @brief Audio speaker states + enum class State { + playing, + paused, + stopped + }; + + /// @brief Pulse-code modulation data + struct PCM { + /// @brief May contain 8 bit and 16 bit PCM data + std::vector data; + size_t totalSamples; + uint8_t channels; + uint8_t bitsPerSample; + uint sampleRate; + bool seekable; + + PCM( + std::vector data, + size_t totalSamples, + uint8_t channels, + uint8_t bitsPerSample, + uint sampleRate, + bool seekable + ) : data(std::move(data)), + totalSamples(totalSamples), + channels(channels), + bitsPerSample(bitsPerSample), + sampleRate(sampleRate), + seekable(seekable) {} + + inline size_t countSamplesMono() const { + return totalSamples / channels; + } + + inline duration_t getDuration() const { + return static_cast(countSamplesMono()) / + static_cast(sampleRate); + } + }; + + /// @brief audio::PCMStream is a data source for audio::Stream + class PCMStream { + public: + virtual ~PCMStream() {}; + + /// @brief Read samples data to buffer + /// @param buffer destination buffer + /// @param bufferSize destination buffer size + /// @param loop loop stream (seek to start when end reached) + /// @return size of data received + /// (always equals bufferSize if seekable and looped) + virtual size_t readFully(char* buffer, size_t bufferSize, bool loop); + + virtual size_t read(char* buffer, size_t bufferSize) = 0; + + /// @brief Close stream + virtual void close()=0; + + /// @brief Check if stream is open + virtual bool isOpen() const=0; + + /// @brief Get total samples number if seekable or 0 + virtual size_t getTotalSamples() const=0; + + /// @brief Get total audio track duration if seekable or 0.0 + virtual duration_t getTotalDuration() const=0; + + /// @brief Get number of audio channels + /// @return 1 if mono, 2 if stereo + virtual uint getChannels() const=0; + + /// @brief Get audio sampling frequency + /// @return number of mono samples per second + virtual uint getSampleRate() const=0; + + /// @brief Get number of bits per mono sample + /// @return 8 or 16 + virtual uint getBitsPerSample() const=0; + + /// @brief Check if the stream does support seek feature + virtual bool isSeekable() const=0; + + /// @brief Move playhead to the selected sample number + /// @param position selected sample number + virtual void seek(size_t position) = 0; + }; + + /// @brief Audio streaming interface + class Stream { + public: + virtual ~Stream() {}; + + /// @brief Get pcm data source + /// @return PCM stream or nullptr if audio::openStream + /// keepSource argument is set to false + virtual std::shared_ptr getSource() const = 0; + + /// @brief Create new speaker bound to the Stream + /// and having high priority + /// @param loop is stream looped (required for correct buffers preload) + /// @return speaker id or 0 + virtual Speaker* createSpeaker(bool loop) = 0; + + /// @brief Unbind previous speaker and bind new speaker to the stream + /// @param speaker speaker id or 0 if all you need is unbind speaker + virtual void bindSpeaker(speakerid_t speaker) = 0; + + /// @brief Get id of the bound speaker + /// @return speaker id or 0 if no speaker bound + virtual speakerid_t getSpeaker() const = 0; + + /// @brief Update stream state (preload samples if needed) + /// @param delta time elapsed since the last update + virtual void update(double delta) = 0; + + /// @brief Set playhead to the selected time + /// @param time selected time + virtual void setTime(duration_t time) = 0; + }; + + /// @brief Sound is an audio asset that supposed to support many + /// simultaneously playing instances with different sources. + /// So it's audio data is stored in memory. + class Sound { + public: + virtual ~Sound() {} + + /// @brief Get sound duration + /// @return duration in seconds (>= 0.0) + virtual duration_t getDuration() const = 0; + + /// @brief Get sound PCM data + /// @return PCM data or nullptr + virtual std::shared_ptr getPCM() const = 0; + + /// @brief Create new sound instance + /// @param priority instance priority. High priority instance can + /// take out speaker from low priority instance + /// @return new speaker with sound bound or nullptr + /// if all speakers are in use + virtual Speaker* newInstance(int priority) const = 0; + }; + + /// @brief Audio source controller interface + class Speaker { + public: + virtual ~Speaker() {} + + /// @brief Get current speaker state + /// @return speaker state + virtual State getState() const = 0; + + /// @brief Get speaker audio gain + /// @return speaker audio gain value + virtual float getVolume() const = 0; + + /// @brief Set speaker audio gain (must be positive) + /// @param volume new gain value + virtual void setVolume(float volume) = 0; + + /// @brief Get speaker pitch multiplier + /// @return pitch multiplier + virtual float getPitch() const = 0; + + /// @brief Set speaker pitch multiplier + /// @param pitch new pitch multiplier (must be positive) + virtual void setPitch(float pitch) = 0; + + /// @brief Check if speaker audio is in loop + /// @return true if audio is in loop + virtual bool isLoop() const = 0; + + /// @brief Enable/disable audio loop + /// @param loop loop mode + virtual void setLoop(bool loop) = 0; + + /// @brief Play, replay or resume audio + virtual void play() = 0; + + /// @brief Pause playing audio and keep speaker alive + virtual void pause() = 0; + + /// @brief Stop and destroy speaker + virtual void stop() = 0; + + /// @brief Check if the speaker has stopped by calling stop() + virtual bool isStoppedManually() const = 0; + + /// @brief Get current time position of playing audio + /// @return time position in seconds + virtual duration_t getTime() const = 0; + + /// @brief Set playing audio time position + /// @param time time position in seconds + virtual void setTime(duration_t time) = 0; + + /// @brief Set speaker 3D position in the world + /// @param pos new position + virtual void setPosition(glm::vec3 pos) = 0; + + /// @brief Get speaker 3D position in the world + /// @return position + virtual glm::vec3 getPosition() const = 0; + + /// @brief Set speaker movement velocity used for Doppler effect + /// @param vel velocity vector + virtual void setVelocity(glm::vec3 vel) = 0; + + /// @brief Get speaker movement velocity used for Doppler effect + /// @return velocity vector + virtual glm::vec3 getVelocity() const = 0; + + /// @brief Get speaker priority + /// @return speaker priority value + virtual int getPriority() const = 0; + + /// @brief Determines if the position is relative to the listener + /// @param relative true - relative to the listener (default: false) + virtual void setRelative(bool relative) = 0; + + /// @brief Determines if the position is relative to the listener + virtual bool isRelative() const = 0; + + /// @brief Check if speaker is playing + inline bool isPlaying() const { + return getState() == State::playing; + } + + /// @brief Check if speaker is paused + inline bool isPaused() const { + return getState() == State::paused; + } + + /// @brief Check if speaker is stopped + inline bool isStopped() const { + return getState() == State::stopped; + } + }; + + class Backend { + public: + virtual ~Backend() {}; + + virtual Sound* createSound(std::shared_ptr pcm, bool keepPCM) = 0; + virtual Stream* openStream(std::shared_ptr stream, bool keepSource) = 0; + virtual void setListener( + glm::vec3 position, + glm::vec3 velocity, + glm::vec3 lookAt, + glm::vec3 up + ) = 0; + virtual void update(double delta) = 0; + + /// @brief Check if backend is an abstraction that does not internally + /// work with actual audio data or play anything + virtual bool isDummy() const = 0; + }; + + /// @brief Initialize audio system or use no audio mode + /// @param enabled try to initialize actual audio + extern void initialize(bool enabled); + + /// @brief Load audio file info and PCM data + /// @param file audio file + /// @param headerOnly read header only + /// @throws std::runtime_error if I/O error ocurred or format is unknown + /// @return PCM audio data + extern PCM* loadPCM(const fs::path& file, bool headerOnly); + + /// @brief Load sound from file + /// @param file audio file path + /// @param keepPCM store PCM data in sound to make it accessible with Sound::getPCM + /// @throws std::runtime_error if I/O error ocurred or format is unknown + /// @return new Sound instance + extern Sound* loadSound(const fs::path& file, bool keepPCM); + + /// @brief Create new sound from PCM data + /// @param pcm PCM data + /// @param keepPCM store PCM data in sound to make it accessible with Sound::getPCM + /// @return new Sound instance + extern Sound* createSound(std::shared_ptr pcm, bool keepPCM); + + /// @brief Open new PCM stream from file + /// @param file audio file path + /// @throws std::runtime_error if I/O error ocurred or format is unknown + /// @return new PCMStream instance + extern PCMStream* openPCMStream(const fs::path& file); + + /// @brief Open new audio stream from file + /// @param file audio file path + /// @param keepSource store PCMStream in stream to make it accessible with Stream::getSource + /// @return new Stream instance + extern Stream* openStream(const fs::path& file, bool keepSource); + + /// @brief Open new audio stream from source + /// @param stream PCM data source + /// @param keepSource store PCMStream in stream to make it accessible with Stream::getSource + /// @return new Stream instance + extern Stream* openStream(std::shared_ptr stream, bool keepSource); + + /// @brief Configure 3D listener + /// @param position listener position + /// @param velocity listener velocity (used for Doppler effect) + /// @param lookAt point the listener look at + /// @param up camera up vector + extern void setListener( + glm::vec3 position, + glm::vec3 velocity, + glm::vec3 lookAt, + glm::vec3 up + ); + + /// @brief Play 3D sound in the world + /// @param sound target sound + /// @param position sound world position + /// @param relative position speaker relative to listener + /// @param volume sound volume [0.0-1.0] + /// @param pitch sound pitch multiplier [0.0-...] + /// @param loop loop sound + /// @param priority sound priority + /// (PRIORITY_LOW, PRIORITY_NORMAL, PRIORITY_HIGH) + /// @return speaker id or 0 + extern speakerid_t play( + Sound* sound, + glm::vec3 position, + bool relative, + float volume, + float pitch, + bool loop, + int priority + ); + + /// @brief Play stream + /// @param stream target stream + /// @param position stream world position + /// @param relative position speaker relative to listener + /// @param volume stream volume [0.0-1.0] + /// @param pitch stream pitch multiplier [0.0-...] + /// @param loop loop stream + /// @return speaker id or 0 + extern speakerid_t play( + std::shared_ptr stream, + glm::vec3 position, + bool relative, + float volume, + float pitch, + bool loop + ); + + /// @brief Play stream from file + /// @param file audio file path + /// @param position stream world position + /// @param relative position speaker relative to listener + /// @param volume stream volume [0.0-1.0] + /// @param pitch stream pitch multiplier [0.0-...] + /// @param loop loop stream + /// @return speaker id or 0 + /// @return + extern speakerid_t playStream( + const fs::path& file, + glm::vec3 position, + bool relative, + float volume, + float pitch, + bool loop + ); + + /// @brief Get speaker by id + /// @param id speaker id + /// @return speaker or nullptr + extern Speaker* get(speakerid_t id); + + /// @brief Update audio streams and sound instanced + /// @param delta time elapsed since the last update (seconds) + extern void update(double delta); + + /// @brief Finalize audio system + extern void close(); +}; + +#endif // AUDIO_AUDIO_H_ diff --git a/src/audio/audioutil.cpp b/src/audio/audioutil.cpp deleted file mode 100644 index 17e3d99c..00000000 --- a/src/audio/audioutil.cpp +++ /dev/null @@ -1,207 +0,0 @@ -#include "audioutil.h" - -#include -#include -#include -#include -#include - -#ifdef __APPLE__ -#include -#include -#else -#include -#include -#endif - -bool is_big_endian(void){ - uint32_t ui32_v = 0x01020304; - char bytes[sizeof(uint32_t)]; - std::memcpy(bytes, &ui32_v, sizeof(uint32_t)); - - return bytes[0] == 1; -} - -std::int32_t convert_to_int(char* buffer, std::size_t len){ - std::int32_t a = 0; - if (!is_big_endian()) { - std::memcpy(&a, buffer, len); - } - else { - for (std::size_t i = 0; i < len; ++i) { - reinterpret_cast(&a)[3 - i] = buffer[i]; - } - } - return a; -} - -bool check_al_errors(const std::string& filename, const std::uint_fast32_t line){ - ALenum error = alGetError(); - if(error != AL_NO_ERROR){ - std::cerr << "OpenAL ERROR (" << filename << ": " << line << ")\n" ; - switch(error){ - case AL_INVALID_NAME: - std::cerr << "AL_INVALID_NAME: a bad name (ID) was passed to an OpenAL function"; - break; - case AL_INVALID_ENUM: - std::cerr << "AL_INVALID_ENUM: an invalid enum value was passed to an OpenAL function"; - break; - case AL_INVALID_VALUE: - std::cerr << "AL_INVALID_VALUE: an invalid value was passed to an OpenAL function"; - break; - case AL_INVALID_OPERATION: - std::cerr << "AL_INVALID_OPERATION: the requested operation is not valid"; - break; - case AL_OUT_OF_MEMORY: - std::cerr << "AL_OUT_OF_MEMORY: the requested operation resulted in OpenAL running out of memory"; - break; - default: - std::cerr << "UNKNOWN AL ERROR: " << error; - } - std::cerr << std::endl; - return false; - } - return true; -} - -bool load_wav_file_header(std::ifstream& file, - std::uint8_t& channels, - std::int32_t& sampleRate, - std::uint8_t& bitsPerSample, - ALsizei& size){ - char buffer[4]; - if(!file.is_open()) - return false; - - // the RIFF - if(!file.read(buffer, 4)){ - std::cerr << "ERROR: could not read RIFF" << std::endl; - return false; - } - if(std::strncmp(buffer, "RIFF", 4) != 0){ - std::cerr << "ERROR: file is not a valid WAVE file (header doesn't begin with RIFF)" << std::endl; - return false; - } - - // the size of the file - if(!file.read(buffer, 4)){ - std::cerr << "ERROR: could not read size of file" << std::endl; - return false; - } - - // the WAVE - if(!file.read(buffer, 4)){ - std::cerr << "ERROR: could not read WAVE" << std::endl; - return false; - } - if(std::strncmp(buffer, "WAVE", 4) != 0){ - std::cerr << "ERROR: file is not a valid WAVE file (header doesn't contain WAVE)" << std::endl; - return false; - } - - // "fmt/0" - if(!file.read(buffer, 4)){ - std::cerr << "ERROR: could not read fmt/0" << std::endl; - return false; - } - - // this is always 16, the size of the fmt data chunk - if(!file.read(buffer, 4)){ - std::cerr << "ERROR: could not read the 16" << std::endl; - return false; - } - - // PCM should be 1? - if(!file.read(buffer, 2)){ - std::cerr << "ERROR: could not read PCM" << std::endl; - return false; - } - - // the number of channels - if(!file.read(buffer, 2)){ - std::cerr << "ERROR: could not read number of channels" << std::endl; - return false; - } - channels = convert_to_int(buffer, 2); - - // sample rate - if(!file.read(buffer, 4)){ - std::cerr << "ERROR: could not read sample rate" << std::endl; - return false; - } - sampleRate = convert_to_int(buffer, 4); - - // (sampleRate * bitsPerSample * channels) / 8 - if(!file.read(buffer, 4)){ - std::cerr << "ERROR: could not read (sampleRate * bitsPerSample * channels) / 8" << std::endl; - return false; - } - - // ?? dafaq - if(!file.read(buffer, 2)){ - std::cerr << "ERROR: could not read dafaq" << std::endl; - return false; - } - - // bitsPerSample - if(!file.read(buffer, 2)){ - std::cerr << "ERROR: could not read bits per sample" << std::endl; - return false; - } - bitsPerSample = convert_to_int(buffer, 2); - - // data chunk header "data" - if(!file.read(buffer, 4)){ - std::cerr << "ERROR: could not read data chunk header" << std::endl; - return false; - } - if(std::strncmp(buffer, "data", 4) != 0){ - std::cerr << "ERROR: file is not a valid WAVE file (doesn't have 'data' tag)" << std::endl; - return false; - } - - // size of data - if(!file.read(buffer, 4)){ - std::cerr << "ERROR: could not read data size" << std::endl; - return false; - } - size = convert_to_int(buffer, 4); - - /* cannot be at the end of file */ - if(file.eof()){ - std::cerr << "ERROR: reached EOF on the file" << std::endl; - return false; - } - if(file.fail()){ - std::cerr << "ERROR: fail state set on the file" << std::endl; - return false; - } - return true; -} - -// after that user must free returned memory by himself! -char* load_wav(const std::string& filename, - std::uint8_t& channels, - std::int32_t& sampleRate, - std::uint8_t& bitsPerSample, - ALsizei& size){ - std::ifstream in(filename, std::ios::binary); - if(!in.is_open()){ - std::cerr << "ERROR: Could not open \"" << filename << "\"" << std::endl; - return nullptr; - } - if(!load_wav_file_header(in, channels, sampleRate, bitsPerSample, size)){ - std::cerr << "ERROR: Could not load wav header of \"" << filename << "\"" << std::endl; - return nullptr; - } - - std::unique_ptr data (new char[size]); - try { - in.read(data.get(), size); - return data.release(); - } - catch (const std::exception&) { - std::cerr << "ERROR: Could not load wav data of \"" << filename << "\"" << std::endl; - return nullptr; - } -} diff --git a/src/audio/audioutil.h b/src/audio/audioutil.h deleted file mode 100644 index 8f0357cd..00000000 --- a/src/audio/audioutil.h +++ /dev/null @@ -1,49 +0,0 @@ -#ifndef SRC_AUDIO_AUDIOUTIL_H_ -#define SRC_AUDIO_AUDIOUTIL_H_ - -#include -#include -#include - -#ifdef __APPLE__ -#include -#else -#include -#endif - -#define alCheckErrorsMacro() check_al_errors(__FILE__, __LINE__) - -bool check_al_errors(const std::string& filename, const std::uint_fast32_t line); - -bool load_wav_file_header(std::ifstream& file, - std::uint8_t& channels, - std::int32_t& sampleRate, - std::uint8_t& bitsPerSample, - ALsizei& size); - -char* load_wav(const std::string& filename, - std::uint8_t& channels, - std::int32_t& sampleRate, - std::uint8_t& bitsPerSample, - ALsizei& size); - -static inline ALenum to_al_format(short channels, short samples){ - bool stereo = (channels > 1); - - switch (samples) { - case 16: - if (stereo) - return AL_FORMAT_STEREO16; - else - return AL_FORMAT_MONO16; - case 8: - if (stereo) - return AL_FORMAT_STEREO8; - else - return AL_FORMAT_MONO8; - default: - return -1; - } -} - -#endif /* SRC_AUDIO_AUDIOUTIL_H_ */ diff --git a/src/coders/ogg.cpp b/src/coders/ogg.cpp new file mode 100644 index 00000000..2be7fe95 --- /dev/null +++ b/src/coders/ogg.cpp @@ -0,0 +1,149 @@ +#include "ogg.h" + +#include +#include +#include + +#include "../audio/audio.h" +#include "../typedefs.h" + +using namespace audio; + +static inline const char* vorbis_error_message(int code) { + switch (code) { + case 0: return "no error"; + case OV_EREAD: return "a read from media returned an error"; + case OV_ENOTVORBIS: return "bitstream does not contain any Vorbis data"; + case OV_EVERSION: return "vorbis version mismatch"; + case OV_EBADHEADER: return "invalid Vorbis bitstream header"; + case OV_EFAULT: return "internal logic fault"; + case OV_EINVAL: return "invalid read operation"; + default: + return "unknown"; + } +} + +audio::PCM* ogg::load_pcm(const std::filesystem::path& file, bool headerOnly) { + OggVorbis_File vf; + int code; + if ((code = ov_fopen(file.u8string().c_str(), &vf))) { + throw std::runtime_error(vorbis_error_message(code)); + } + std::vector data; + + vorbis_info* info = ov_info(&vf, -1); + uint channels = info->channels; + uint sampleRate = info->rate; + bool seekable = ov_seekable(&vf); + size_t totalSamples = seekable ? ov_pcm_total(&vf, -1) : 0; + + if (!headerOnly) { + const int bufferSize = 4096; + int section = 0; + char buffer[bufferSize]; + + bool eof = false; + while (!eof) { + long ret = ov_read(&vf, buffer, bufferSize, 0, 2, true, §ion); + if (ret == 0) { + eof = true; + } else if (ret < 0) { + std::cerr << "ogg::load_pcm: " << vorbis_error_message(ret) << std::endl; + } else { + data.insert(data.end(), std::begin(buffer), std::begin(buffer)+ret); + } + } + totalSamples = data.size(); + } + ov_clear(&vf); + return new PCM(std::move(data), totalSamples, channels, 16, sampleRate, seekable); +} + +class OggStream : public PCMStream { + OggVorbis_File vf; + bool closed = false; + uint channels; + uint sampleRate; + size_t totalSamples = 0; + bool seekable; +public: + OggStream(OggVorbis_File vf) : vf(std::move(vf)) { + vorbis_info* info = ov_info(&vf, -1); + channels = info->channels; + sampleRate = info->rate; + seekable = ov_seekable(&vf); + if (seekable) { + totalSamples = ov_pcm_total(&vf, -1); + } + } + + ~OggStream() { + if (!closed) { + close(); + } + } + + size_t read(char* buffer, size_t bufferSize) override { + if (closed) { + return 0; + } + int bitstream = 0; + long bytes = ov_read(&vf, buffer, bufferSize, 0, 2, true, &bitstream); + if (bytes < 0) { + std::cerr << "ogg::load_pcm: " << vorbis_error_message(bytes) << " " << bytes << std::endl; + return 0; + } + return bytes; + } + + void close() override { + if (!closed) { + ov_clear(&vf); + closed = true; + } + } + + bool isOpen() const override { + return !closed; + } + + size_t getTotalSamples() const override { + return totalSamples; + } + + duration_t getTotalDuration() const override { + return static_cast(totalSamples) / + static_cast(sampleRate); + } + + uint getChannels() const override { + return channels; + } + + uint getSampleRate() const override { + return sampleRate; + } + + uint getBitsPerSample() const override { + return 16; + } + + bool isSeekable() const override { + return seekable; + } + + void seek(size_t position) override { + if (!closed && seekable) { + ov_raw_seek(&vf, position); + } + } +}; + +PCMStream* ogg::create_stream(const std::filesystem::path& file) { + OggVorbis_File vf; + int code; + if ((code = ov_fopen(file.u8string().c_str(), &vf))) { + throw std::runtime_error(vorbis_error_message(code)); + } + return new OggStream(std::move(vf)); +} diff --git a/src/coders/ogg.h b/src/coders/ogg.h new file mode 100644 index 00000000..828f2be5 --- /dev/null +++ b/src/coders/ogg.h @@ -0,0 +1,16 @@ +#ifndef CODERS_OGG_H_ +#define CODERS_OGG_H_ + +#include + +namespace audio { + struct PCM; + class PCMStream; +} + +namespace ogg { + extern audio::PCM* load_pcm(const std::filesystem::path& file, bool headerOnly); + extern audio::PCMStream* create_stream(const std::filesystem::path& file); +} + +#endif // CODERS_OGG_H_ diff --git a/src/coders/wav.cpp b/src/coders/wav.cpp new file mode 100644 index 00000000..2ab78a7b --- /dev/null +++ b/src/coders/wav.cpp @@ -0,0 +1,236 @@ +#include "wav.h" + +#include +#include +#include +#include +#include + +#include "../audio/audio.h" + +bool is_big_endian() { + uint32_t ui32_v = 0x01020304; + char bytes[sizeof(uint32_t)]; + std::memcpy(bytes, &ui32_v, sizeof(uint32_t)); + return bytes[0] == 1; +} + +std::int32_t convert_to_int(char* buffer, std::size_t len){ + std::int32_t a = 0; + if (!is_big_endian()) { + std::memcpy(&a, buffer, len); + } + else { + for (std::size_t i = 0; i < len; ++i) { + reinterpret_cast(&a)[3 - i] = buffer[i]; + } + } + return a; +} + +/// @brief Seekable WAV-file PCM stream +class WavStream : public audio::PCMStream { + std::ifstream in; + uint channels; + uint bytesPerSample; + uint sampleRate; + size_t totalSize; + size_t totalSamples; + size_t initialPosition; +public: + WavStream( + std::ifstream in, + uint channels, + uint bitsPerSample, + uint sampleRate, + size_t size, + size_t initialPosition + ) : in(std::move(in)), + channels(channels), + bytesPerSample(bitsPerSample/8), + sampleRate(sampleRate), + totalSize(size) + { + totalSamples = totalSize / channels / bytesPerSample; + this->initialPosition = initialPosition; + } + + size_t read(char* buffer, size_t bufferSize) override { + if (!isOpen()) { + return 0; + } + in.read(buffer, bufferSize); + if (in.eof()) { + return 0; + } + if (in.fail()) { + std::cerr << "Wav::load_pcm: I/O error ocurred" << std::endl; + return -1; + } + return in.gcount(); + } + + void close() override { + if (!isOpen()) + return; + in.close(); + } + + bool isOpen() const override { + return in.is_open(); + } + + size_t getTotalSamples() const override { + return totalSamples; + } + + audio::duration_t getTotalDuration() const override { + return totalSamples / static_cast(sampleRate); + } + + uint getChannels() const override { + return channels; + } + + uint getSampleRate() const override { + return sampleRate; + } + + uint getBitsPerSample() const override { + return bytesPerSample * 8; + } + + bool isSeekable() const override { + return true; + } + + void seek(size_t position) override { + if (!isOpen()) + return; + position %= totalSamples; + in.clear(); + in.seekg(initialPosition + position * channels * bytesPerSample, std::ios_base::beg); + } +}; + +audio::PCMStream* wav::create_stream(const std::filesystem::path& file) { + std::ifstream in(file, std::ios::binary); + if(!in.is_open()){ + throw std::runtime_error("could not to open file '"+file.u8string()+"'"); + } + + char buffer[6]; + // the RIFF + if(!in.read(buffer, 4)){ + throw std::runtime_error("could not to read RIFF"); + } + if(std::strncmp(buffer, "RIFF", 4) != 0){ + throw std::runtime_error("file is not a valid WAVE file (header doesn't begin with RIFF)"); + } + // the size of the file + if(!in.read(buffer, 4)){ + throw std::runtime_error("could not read size of file"); + } + // the WAVE + if(!in.read(buffer, 4)){ + throw std::runtime_error("could not to read WAVE"); + } + if(std::strncmp(buffer, "WAVE", 4) != 0){ + throw std::runtime_error("file is not a valid WAVE file (header doesn't contain WAVE)"); + } + // "fmt/0" + if(!in.read(buffer, 4)){ + throw std::runtime_error("could not read fmt/0"); + } + // this is always 16, the size of the fmt data chunk + if(!in.read(buffer, 4)){ + throw std::runtime_error("could not read the 16"); + } + // PCM should be 1? + if(!in.read(buffer, 2)){ + throw std::runtime_error("could not read PCM"); + } + // the number of channels + if(!in.read(buffer, 2)){ + throw std::runtime_error("could not read number of channels"); + } + int channels = convert_to_int(buffer, 2); + // sample rate + if(!in.read(buffer, 4)){ + throw std::runtime_error("could not read sample rate"); + } + int sampleRate = convert_to_int(buffer, 4); + if (!in.read(buffer, 6)) { + throw std::runtime_error("could not to read WAV header"); + } + + // bitsPerSample + if(!in.read(buffer, 2)){ + throw std::runtime_error("could not read bits per sample"); + } + int bitsPerSample = convert_to_int(buffer, 2); + if (bitsPerSample >= 24) { + throw std::runtime_error(std::to_string(bitsPerSample)+" bit depth is not supported by OpenAL"); + } + + // data chunk header "data" + if(!in.read(buffer, 4)){ + throw std::runtime_error("could not read data chunk header"); + } + + size_t initialOffset = 44; + // skip garbage in WAV + if (std::strncmp(buffer, "LIST", 4) == 0) { + // chunk size + if(!in.read(buffer, 4)){ + throw std::runtime_error("could not read comment chunk size"); + } + int chunkSize = convert_to_int(buffer, 4); + in.seekg(chunkSize, std::ios_base::cur); + + initialOffset += chunkSize + 4; + + if(!in.read(buffer, 4)){ + throw std::runtime_error("could not read data chunk header"); + } + } + + if(std::strncmp(buffer, "data", 4) != 0){ + std::cerr << buffer << std::endl; + throw std::runtime_error("file is not a valid WAVE file (doesn't have 'data' tag)"); + } + + // size of data + if(!in.read(buffer, 4)){ + throw std::runtime_error("could not read data size"); + } + size_t size = convert_to_int(buffer, 4); + + /* cannot be at the end of file */ + if(in.eof()){ + throw std::runtime_error("reached EOF on the file"); + } + if(in.fail()){ + throw std::runtime_error("fail state set on the file"); + } + return new WavStream(std::move(in), channels, bitsPerSample, sampleRate, size, initialOffset); +} + +audio::PCM* wav::load_pcm(const std::filesystem::path& file, bool headerOnly) { + std::unique_ptr stream(wav::create_stream(file)); + + size_t totalSamples = stream->getTotalSamples(); + uint channels = stream->getChannels(); + uint bitsPerSample = stream->getBitsPerSample(); + uint sampleRate = stream->getSampleRate(); + + std::vector data; + if (!headerOnly) { + size_t size = stream->getTotalSamples() * + (stream->getBitsPerSample()/8) * + stream->getChannels(); + data.resize(size); + stream->readFully(data.data(), size, false); + } + return new audio::PCM(std::move(data), totalSamples, channels, bitsPerSample, sampleRate, true); +} diff --git a/src/coders/wav.h b/src/coders/wav.h new file mode 100644 index 00000000..5a8a2095 --- /dev/null +++ b/src/coders/wav.h @@ -0,0 +1,16 @@ +#ifndef CODERS_WAV_H_ +#define CODERS_WAV_H_ + +#include + +namespace audio { + struct PCM; + class PCMStream; +} + +namespace wav { + extern audio::PCM* load_pcm(const std::filesystem::path& file, bool headerOnly); + extern audio::PCMStream* create_stream(const std::filesystem::path& file); +} + +#endif // CODERS_WAV_H_ diff --git a/src/engine.cpp b/src/engine.cpp index a21bd8f7..e69c8458 100644 --- a/src/engine.cpp +++ b/src/engine.cpp @@ -10,7 +10,7 @@ #include #define GLEW_STATIC -#include "audio/Audio.h" +#include "audio/audio.h" #include "assets/Assets.h" #include "assets/AssetsLoader.h" #include "world/WorldGenerators.h" @@ -56,6 +56,7 @@ Engine::Engine(EngineSettings& settings, EnginePaths* paths) if (Window::initialize(settings.display)){ throw initialize_error("could not initialize window"); } + audio::initialize(true); auto resdir = paths->getResources(); scripting::initialize(this); @@ -66,7 +67,6 @@ Engine::Engine(EngineSettings& settings, EnginePaths* paths) resPaths = std::make_unique(resdir, roots); assets = std::make_unique(); - AssetsLoader loader(assets.get(), resPaths.get()); AssetsLoader::addDefaults(loader, nullptr); @@ -79,8 +79,6 @@ Engine::Engine(EngineSettings& settings, EnginePaths* paths) throw initialize_error("could not to load assets"); } } - - Audio::initialize(); gui = std::make_unique(); if (settings.ui.language == "auto") { settings.ui.language = langs::locale_by_envlocale(platform::detect_locale(), paths->getResources()); @@ -123,6 +121,8 @@ void Engine::mainloop() { assert(screen != nullptr); updateTimers(); updateHotkeys(); + + audio::update(delta); gui->act(delta); screen->update(delta); @@ -148,7 +148,7 @@ Engine::~Engine() { screen.reset(); content.reset(); - Audio::finalize(); + audio::close(); assets.reset(); scripting::close(); Window::terminate(); diff --git a/src/frontend/screens.cpp b/src/frontend/screens.cpp index 8427c000..badf8bf9 100644 --- a/src/frontend/screens.cpp +++ b/src/frontend/screens.cpp @@ -8,6 +8,7 @@ #include #include +#include "../audio/audio.h" #include "../window/Camera.h" #include "../window/Events.h" #include "../window/input.h" @@ -19,6 +20,7 @@ #include "../world/Level.h" #include "../world/World.h" #include "../objects/Player.h" +#include "../physics/Hitbox.h" #include "../logic/ChunksController.h" #include "../logic/LevelController.h" #include "../logic/scripting/scripting.h" @@ -149,6 +151,15 @@ void LevelScreen::update(float delta) { updateHotkeys(); } + auto player = controller->getPlayer(); + auto camera = player->camera; + audio::setListener( + camera->position, + player->hitbox->velocity, + camera->position+camera->dir, + camera->up + ); + // TODO: subscribe for setting change EngineSettings& settings = engine->getSettings(); controller->getPlayer()->camera->setFov(glm::radians(settings.camera.fov)); diff --git a/vcpkg.json b/vcpkg.json index 626e06e6..8a5f6049 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -9,6 +9,7 @@ "glm", "libspng", "zlib", - "luajit" + "luajit", + "libvorbis" ] }