Audio subsystem (wip)

This commit is contained in:
Nikita Lisitsa 2020-09-21 11:22:34 +03:00
parent 89a608e35f
commit 6549c2f907
7 changed files with 390 additions and 0 deletions

View file

@ -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)

View file

@ -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)

View file

@ -0,0 +1,19 @@
#pragma once
#include <string_view>
#include <cstdint>
#include <cstddef>
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() {}
};
}

View file

@ -0,0 +1,30 @@
#pragma once
#include <psemek/audio/effect.hpp>
#include <psemek/audio/stream.hpp>
#include <psemek/audio/sample.hpp>
#include <memory>
#include <string_view>
namespace psemek::audio
{
struct engine
{
engine();
~engine();
std::shared_ptr<sample> load_raw(std::int16_t const * data, std::size_t sample_count, bool copy = true);
std::shared_ptr<sample> load_wav(char const * data, std::size_t size);
std::shared_ptr<stream> play(std::shared_ptr<sample> s, bool loop = false);
private:
struct impl;
std::shared_ptr<impl> pimpl_;
struct impl & impl() { return *pimpl_; }
struct impl const & impl() const { return *pimpl_; }
};
}

View file

@ -0,0 +1,16 @@
#pragma once
#include <cstdint>
namespace psemek::audio
{
struct sample
{
virtual std::int16_t const * data() const = 0;
virtual std::size_t size() const = 0;
virtual ~sample(){}
};
}

View file

@ -0,0 +1,28 @@
#pragma once
#include <psemek/audio/effect.hpp>
#include <memory>
namespace psemek::audio
{
struct stream
{
// value \in [0, 1]
virtual void volume(float value) = 0;
virtual void push_effect(std::shared_ptr<effect> 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() {}
};
}

View file

@ -0,0 +1,270 @@
#include <psemek/audio/engine.hpp>
#include <psemek/sdl2/init.hpp>
#include <psemek/log/log.hpp>
#include <psemek/util/not_implemented.hpp>
#include <psemek/util/unused.hpp>
#include <SDL2/SDL.h>
#include <SDL2/SDL_mixer.h>
#include <mutex>
#include <vector>
#include <optional>
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<std::int16_t const *>(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_impl> sample;
bool loop;
stream_impl(int channel, std::shared_ptr<sample_impl> sample, bool loop)
: channel(channel)
, sample(sample)
, loop(loop)
{}
void volume(float value)
{
int v = std::min(128, std::max(0, static_cast<int>(std::round(value * MIX_MAX_VOLUME))));
Mix_Volume(channel, v);
}
void push_effect(std::shared_ptr<effect> 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<void> sdl_init;
sdl2_mixer_initializer mix_init;
struct channel
{
std::shared_ptr<stream_impl> stream;
};
std::mutex channels_mutex;
std::vector<channel> channels;
static std::mutex instance_mutex;
static std::weak_ptr<impl> instance_ptr;
static std::shared_ptr<impl> instance();
impl();
~impl();
std::shared_ptr<stream> play(std::shared_ptr<sample> s, bool loop);
static void channel_finished(int ch);
};
std::mutex engine::impl::instance_mutex;
std::weak_ptr<struct engine::impl> engine::impl::instance_ptr;
std::shared_ptr<struct engine::impl> engine::impl::instance()
{
std::lock_guard lock{instance_mutex};
if (auto p = instance_ptr.lock(); p)
return p;
auto p = std::make_shared<struct impl>();
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<stream> engine::impl::play(std::shared_ptr<sample> s, bool loop)
{
auto ss = std::dynamic_pointer_cast<sample_impl>(s);
if (!ss)
{
throw std::runtime_error("Failed to play sample: unknown sample type");
}
std::lock_guard lock{channels_mutex};
std::optional<int> ch;
int c;
for (c = 0; c < static_cast<int>(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<stream_impl>(*ch, std::move(ss), loop);
channels[*ch].stream = str;
return str;
}
void engine::impl::channel_finished(int ch)
{
std::shared_ptr<impl> 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<sample> engine::load_wav(char const * data, std::size_t size)
{
return std::make_shared<sample_impl>(Mix_LoadWAV_RW(SDL_RWFromConstMem(data, size), 1));
}
std::shared_ptr<sample> engine::load_raw(std::int16_t const * data, std::size_t sample_count, bool copy)
{
Mix_Chunk * chunk = static_cast<Mix_Chunk *>(malloc(sizeof(Mix_Chunk)));
chunk->allocated = copy ? 1 : 0;
chunk->alen = sample_count * 2;
chunk->volume = 128;
if (copy)
{
chunk->abuf = static_cast<Uint8 *>(malloc(sample_count * 2));
std::copy(data, data + sample_count, reinterpret_cast<std::int16_t *>(chunk->abuf));
}
else
{
chunk->abuf = const_cast<Uint8 *>(reinterpret_cast<Uint8 const *>(data));
}
return std::make_shared<sample_impl>(chunk);
}
std::shared_ptr<stream> engine::play(std::shared_ptr<sample> s, bool loop)
{
return impl().play(std::move(s), loop);
}
}