diff --git a/cmake/modules/FindSDL2_mixer.cmake b/cmake/modules/FindSDL2_mixer.cmake new file mode 100644 index 00000000..14ae0ddd --- /dev/null +++ b/cmake/modules/FindSDL2_mixer.cmake @@ -0,0 +1,19 @@ +if(SDL2_MIXER_FOUND) + set(SDL2_MIXER_FIND_QUIETLY TRUE) +endif() + +find_path(SDL2_MIXER_INCLUDE_DIRS NAMES "SDL2/SDL_mixer.h" PATHS "${SDL2_MIXER_ROOT}/include") +find_library(SDL2_MIXER_LIBRARIES NAMES "SDL2_mixer" PATHS "${SDL2_MIXER_ROOT}/lib") + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(SDL2_MIXER DEFAULT_MSG SDL2_MIXER_INCLUDE_DIRS SDL2_MIXER_LIBRARIES) + +if(SDL2_MIXER_FOUND AND NOT TARGET SDL2_mixer) + add_library(SDL2_mixer SHARED IMPORTED) + set_target_properties(SDL2_mixer PROPERTIES + IMPORTED_LOCATION "${SDL2_MIXER_LIBRARIES}" + INTERFACE_INCLUDE_DIRECTORIES "${SDL2_MIXER_INCLUDE_DIRS}" + ) +endif() + +mark_as_advanced(SDL2_MIXER_INCLUDE_DIRS SDL2_MIXER_LIBRARIES) diff --git a/libs/audio/CMakeLists.txt b/libs/audio/CMakeLists.txt new file mode 100644 index 00000000..3689f27f --- /dev/null +++ b/libs/audio/CMakeLists.txt @@ -0,0 +1,8 @@ +find_package(SDL2_mixer REQUIRED) + +file(GLOB_RECURSE PSEMEK_AUDIO_HEADERS RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}" "include/*.hpp") +file(GLOB_RECURSE PSEMEK_AUDIO_SOURCES RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}" "source/*.cpp") + +add_library(psemek-audio ${PSEMEK_AUDIO_HEADERS} ${PSEMEK_AUDIO_SOURCES}) +target_include_directories(psemek-audio PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include") +target_link_libraries(psemek-audio PUBLIC psemek-sdl2 psemek-log psemek-util SDL2_mixer) diff --git a/libs/audio/include/psemek/audio/effect.hpp b/libs/audio/include/psemek/audio/effect.hpp new file mode 100644 index 00000000..f4bea74a --- /dev/null +++ b/libs/audio/include/psemek/audio/effect.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include +#include +#include + +namespace psemek::audio +{ + + struct effect + { + virtual std::string_view name() const = 0; + + virtual void operator()(std::int16_t * data, std::size_t count) = 0; + + virtual ~effect() {} + }; + +} diff --git a/libs/audio/include/psemek/audio/engine.hpp b/libs/audio/include/psemek/audio/engine.hpp new file mode 100644 index 00000000..f53fd4e6 --- /dev/null +++ b/libs/audio/include/psemek/audio/engine.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include +#include +#include + +#include +#include + +namespace psemek::audio +{ + + struct engine + { + engine(); + ~engine(); + + std::shared_ptr load_raw(std::int16_t const * data, std::size_t sample_count, bool copy = true); + std::shared_ptr load_wav(char const * data, std::size_t size); + + std::shared_ptr play(std::shared_ptr s, bool loop = false); + + private: + struct impl; + std::shared_ptr pimpl_; + struct impl & impl() { return *pimpl_; } + struct impl const & impl() const { return *pimpl_; } + }; + +} diff --git a/libs/audio/include/psemek/audio/sample.hpp b/libs/audio/include/psemek/audio/sample.hpp new file mode 100644 index 00000000..7ce76c72 --- /dev/null +++ b/libs/audio/include/psemek/audio/sample.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include + +namespace psemek::audio +{ + + struct sample + { + virtual std::int16_t const * data() const = 0; + virtual std::size_t size() const = 0; + + virtual ~sample(){} + }; + +} diff --git a/libs/audio/include/psemek/audio/stream.hpp b/libs/audio/include/psemek/audio/stream.hpp new file mode 100644 index 00000000..e78ae218 --- /dev/null +++ b/libs/audio/include/psemek/audio/stream.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include + +#include + +namespace psemek::audio +{ + + struct stream + { + // value \in [0, 1] + virtual void volume(float value) = 0; + + virtual void push_effect(std::shared_ptr e) = 0; + virtual void clear_effects() = 0; + + // NB: stop() effectively destroys the stream + virtual void start() = 0; + virtual void pause() = 0; + virtual void resume() = 0; + virtual void stop() = 0; + virtual bool is_playing() const = 0; + + virtual ~stream() {} + }; + +} diff --git a/libs/audio/source/engine.cpp b/libs/audio/source/engine.cpp new file mode 100644 index 00000000..5c1579b4 --- /dev/null +++ b/libs/audio/source/engine.cpp @@ -0,0 +1,270 @@ +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include + +namespace psemek::audio +{ + + namespace + { + + static const int frequency = 44100; + + [[noreturn]] void mix_fail(std::string const & message) + { + throw std::runtime_error(message + Mix_GetError()); + } + + struct sdl2_mixer_initializer + { + sdl2_mixer_initializer() + { + Mix_Init(0); + } + + ~sdl2_mixer_initializer() + { + Mix_Quit(); + } + }; + + struct sample_impl + : sample + { + Mix_Chunk * chunk; + + sample_impl(Mix_Chunk * chunk) + : chunk(chunk) + {} + + std::int16_t const * data() const override + { + return reinterpret_cast(chunk->abuf); + } + + std::size_t size() const override + { + return chunk->alen; + } + + ~sample_impl() + { + Mix_FreeChunk(chunk); + } + }; + + struct stream_impl + : stream + { + bool playing = false; + + int channel; + std::shared_ptr sample; + bool loop; + + stream_impl(int channel, std::shared_ptr sample, bool loop) + : channel(channel) + , sample(sample) + , loop(loop) + {} + + void volume(float value) + { + int v = std::min(128, std::max(0, static_cast(std::round(value * MIX_MAX_VOLUME)))); + Mix_Volume(channel, v); + } + + void push_effect(std::shared_ptr e) override + { + unused(e); + util::not_implemented(); + } + + void clear_effects() override + { + util::not_implemented(); + } + + void start() override + { + playing = true; + Mix_PlayChannel(channel, sample->chunk, loop ? -1 : 0); + } + + void pause() override + { + playing = false; + Mix_Pause(channel); + } + + void resume() override + { + Mix_Resume(channel); + } + + void stop() override + { + Mix_HaltChannel(channel); + } + + bool is_playing() const override + { + return playing; + } + }; + + } + + struct engine::impl + { + std::shared_ptr sdl_init; + sdl2_mixer_initializer mix_init; + + struct channel + { + std::shared_ptr stream; + }; + + std::mutex channels_mutex; + std::vector channels; + + static std::mutex instance_mutex; + static std::weak_ptr instance_ptr; + static std::shared_ptr instance(); + + impl(); + ~impl(); + + std::shared_ptr play(std::shared_ptr s, bool loop); + + static void channel_finished(int ch); + }; + + std::mutex engine::impl::instance_mutex; + std::weak_ptr engine::impl::instance_ptr; + + std::shared_ptr engine::impl::instance() + { + std::lock_guard lock{instance_mutex}; + + if (auto p = instance_ptr.lock(); p) + return p; + + auto p = std::make_shared(); + instance_ptr = p; + return p; + } + + engine::impl::impl() + : sdl_init(sdl2::init(SDL_INIT_AUDIO)) + { + if (Mix_OpenAudio(frequency, AUDIO_S16SYS, 2, 4096) != 0) + mix_fail("Mix_OpenAudio: "); + + Mix_ChannelFinished(&channel_finished); + + log::info() << "Initialized audio"; + } + + engine::impl::~impl() + { + for (auto & ch : channels) + if (ch.stream) + ch.stream->stop(); + Mix_CloseAudio(); + } + + std::shared_ptr engine::impl::play(std::shared_ptr s, bool loop) + { + auto ss = std::dynamic_pointer_cast(s); + if (!ss) + { + throw std::runtime_error("Failed to play sample: unknown sample type"); + } + + std::lock_guard lock{channels_mutex}; + + std::optional ch; + + int c; + for (c = 0; c < static_cast(channels.size()); ++c) + { + if (!channels[c].stream) + { + ch = c; + break; + } + } + + if (!ch) + { + channels.resize(channels.empty() ? 16 : channels.size() * 2); + Mix_AllocateChannels(channels.size()); + ch = c; + } + + auto str = std::make_shared(*ch, std::move(ss), loop); + + channels[*ch].stream = str; + + return str; + } + + void engine::impl::channel_finished(int ch) + { + std::shared_ptr self; + { + std::lock_guard lock{instance_mutex}; + self = instance_ptr.lock(); + } + if (!self) return; + + std::lock_guard lock{self->channels_mutex}; + self->channels[ch].stream = nullptr; + } + + engine::engine() + : pimpl_{impl::instance()} + {} + + engine::~engine() + {} + + std::shared_ptr engine::load_wav(char const * data, std::size_t size) + { + return std::make_shared(Mix_LoadWAV_RW(SDL_RWFromConstMem(data, size), 1)); + } + + std::shared_ptr engine::load_raw(std::int16_t const * data, std::size_t sample_count, bool copy) + { + Mix_Chunk * chunk = static_cast(malloc(sizeof(Mix_Chunk))); + chunk->allocated = copy ? 1 : 0; + chunk->alen = sample_count * 2; + chunk->volume = 128; + if (copy) + { + chunk->abuf = static_cast(malloc(sample_count * 2)); + std::copy(data, data + sample_count, reinterpret_cast(chunk->abuf)); + } + else + { + chunk->abuf = const_cast(reinterpret_cast(data)); + } + return std::make_shared(chunk); + } + + std::shared_ptr engine::play(std::shared_ptr s, bool loop) + { + return impl().play(std::move(s), loop); + } + +}