286 lines
5.6 KiB
C++
286 lines
5.6 KiB
C++
#include <psemek/audio/engine.hpp>
|
|
#include <psemek/sdl2/init.hpp>
|
|
#include <psemek/log/log.hpp>
|
|
|
|
#include <SDL2/SDL.h>
|
|
#include <SDL2/SDL_mixer.h>
|
|
|
|
#include <mutex>
|
|
#include <vector>
|
|
#include <optional>
|
|
#include <cmath>
|
|
|
|
namespace psemek::audio
|
|
{
|
|
|
|
namespace
|
|
{
|
|
|
|
[[noreturn]] void mix_fail(std::string const & message)
|
|
{
|
|
throw std::runtime_error(message + Mix_GetError());
|
|
}
|
|
|
|
struct sdl2_mixer_initializer
|
|
{
|
|
sdl2_mixer_initializer()
|
|
{
|
|
if (Mix_Init(MIX_INIT_MP3) != MIX_INIT_MP3)
|
|
mix_fail("Failed to load MP3 support: ");
|
|
}
|
|
|
|
~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;
|
|
|
|
std::vector<std::shared_ptr<effect>> effects;
|
|
|
|
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
|
|
{
|
|
effects.push_back(e);
|
|
Mix_RegisterEffect(channel, &effect_func, nullptr, e.get());
|
|
}
|
|
|
|
void clear_effects() override
|
|
{
|
|
effects.clear();
|
|
Mix_UnregisterAllEffects(channel);
|
|
}
|
|
|
|
static void effect_func(int, void * data, int len, void * udata)
|
|
{
|
|
auto e = reinterpret_cast<effect *>(udata);
|
|
|
|
if ((len % 2) != 0)
|
|
{
|
|
log::error() << "Cannot apply " << e->name() << " effect: number of bytes not even";
|
|
return;
|
|
}
|
|
|
|
(*e)(reinterpret_cast<std::int16_t *>(data), len / 2);
|
|
}
|
|
|
|
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(engine::frequency, AUDIO_S16SYS, engine::channels, 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 * sizeof(std::int16_t);
|
|
chunk->volume = 128;
|
|
if (copy)
|
|
{
|
|
chunk->abuf = static_cast<Uint8 *>(malloc(sample_count * sizeof(std::int16_t)));
|
|
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 start, bool loop)
|
|
{
|
|
auto str = impl().play(std::move(s), loop);
|
|
if (start) str->start();
|
|
return str;
|
|
}
|
|
|
|
}
|