233 lines
5.4 KiB
C++
233 lines
5.4 KiB
C++
#include <psemek/audio/engine.hpp>
|
|
#include <psemek/audio/constants.hpp>
|
|
#include <psemek/sdl2/init.hpp>
|
|
#include <psemek/log/log.hpp>
|
|
#include <psemek/prof/profiler.hpp>
|
|
#include <psemek/util/at_scope_exit.hpp>
|
|
#include <psemek/util/to_string.hpp>
|
|
|
|
#include <SDL2/SDL.h>
|
|
|
|
#include <mutex>
|
|
#include <atomic>
|
|
#include <vector>
|
|
#include <optional>
|
|
#include <cmath>
|
|
|
|
namespace psemek::audio
|
|
{
|
|
|
|
namespace
|
|
{
|
|
|
|
struct track_data
|
|
{
|
|
util::span<float const> samples;
|
|
std::vector<float> storage;
|
|
};
|
|
|
|
struct track_stream_impl
|
|
: stream
|
|
{
|
|
track_stream_impl(std::shared_ptr<track_data> data)
|
|
: data_(std::move(data))
|
|
{}
|
|
|
|
std::optional<std::size_t> length() const override
|
|
{
|
|
return data_->samples.size();
|
|
}
|
|
|
|
std::size_t read(float * data, std::size_t sample_count) override
|
|
{
|
|
auto played = played_.load();
|
|
|
|
std::size_t result = std::min(sample_count, data_->samples.size() - played);
|
|
std::copy(data_->samples.data() + played, data_->samples.data() + played + result, data);
|
|
played_.fetch_add(result);
|
|
return result;
|
|
}
|
|
|
|
std::size_t played() const override
|
|
{
|
|
return played_.load();
|
|
}
|
|
|
|
private:
|
|
std::shared_ptr<track_data> data_;
|
|
std::atomic<std::size_t> played_{0};
|
|
};
|
|
|
|
struct track_impl
|
|
: track
|
|
{
|
|
track_impl(std::shared_ptr<track_data> data)
|
|
: data_(std::move(data))
|
|
{}
|
|
|
|
stream_ptr stream() const override
|
|
{
|
|
return std::make_shared<track_stream_impl>(data_);
|
|
}
|
|
|
|
std::optional<std::size_t> length() const override
|
|
{
|
|
return data_->samples.size();
|
|
}
|
|
|
|
private:
|
|
std::shared_ptr<track_data> data_;
|
|
};
|
|
|
|
std::vector<float> convert_audio(SDL_AudioSpec const & spec, std::uint8_t * samples, std::size_t length)
|
|
{
|
|
if (spec.channels > 2)
|
|
throw std::runtime_error(util::to_string("Can't convert audio with ", static_cast<int>(spec.channels), " channels"));
|
|
|
|
if (spec.freq != 44100)
|
|
throw std::runtime_error(util::to_string("Can't convert audio with frequency ", spec.freq));
|
|
|
|
if (spec.format != AUDIO_S16SYS)
|
|
throw std::runtime_error(util::to_string("Can't convert audio with format ", spec.format));
|
|
|
|
auto p = reinterpret_cast<std::int16_t *>(samples);
|
|
|
|
std::vector<float> result;
|
|
|
|
if (spec.channels == 1)
|
|
{
|
|
result.resize(length);
|
|
for (std::size_t i = 0; i < length / 2; ++i)
|
|
{
|
|
float v = (p[i] * 2.f + 1.f) / 65536.f;
|
|
result[2 * i + 0] = v;
|
|
result[2 * i + 1] = v;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
result.resize(length / 2);
|
|
for (std::size_t i = 0; i < length / 2; ++i)
|
|
result[i] = (p[i] * 2.f + 1.f) / 65536.f;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
}
|
|
|
|
struct engine::impl
|
|
{
|
|
std::shared_ptr<void> sdl_init;
|
|
|
|
SDL_AudioDeviceID device;
|
|
|
|
std::vector<float> buffer;
|
|
bool thread_registered = false;
|
|
|
|
channel_ptr output;
|
|
|
|
impl();
|
|
~impl();
|
|
|
|
static void callback(void * userdata, std::uint8_t * stream, int len);
|
|
};
|
|
|
|
engine::impl::impl()
|
|
: sdl_init(sdl2::init(SDL_INIT_AUDIO))
|
|
{
|
|
SDL_AudioSpec desired, obtained;
|
|
desired.freq = frequency;
|
|
desired.channels = 2;
|
|
desired.format = AUDIO_S16SYS;
|
|
desired.samples = 256;
|
|
desired.callback = &callback;
|
|
desired.userdata = this;
|
|
if (device = SDL_OpenAudioDevice(nullptr, 0, &desired, &obtained, 0); device == 0)
|
|
sdl2::fail("SDL_OpenAudioDevice failed:");
|
|
|
|
buffer.resize(obtained.samples * obtained.channels);
|
|
|
|
SDL_PauseAudioDevice(device, 0);
|
|
|
|
log::info() << "Initialized audio: " << static_cast<int>(obtained.channels) << " channels, " << obtained.freq << " Hz, " << obtained.samples << " samples";
|
|
|
|
output = std::make_shared<channel>();
|
|
}
|
|
|
|
engine::impl::~impl()
|
|
{
|
|
SDL_CloseAudioDevice(device);
|
|
}
|
|
|
|
void engine::impl::callback(void * userdata, std::uint8_t * dst_u8, int len)
|
|
{
|
|
static std::string const profiler_str = "audio";
|
|
prof::profiler prof(profiler_str);
|
|
|
|
auto self = static_cast<impl *>(userdata);
|
|
stream_ptr output = self->output->stream();
|
|
std::int16_t * dst = reinterpret_cast<std::int16_t *>(dst_u8);
|
|
|
|
if (!self->thread_registered)
|
|
{
|
|
log::register_thread("audio");
|
|
self->thread_registered = true;
|
|
}
|
|
|
|
std::size_t const size = len / 2;
|
|
std::size_t read = 0;
|
|
if (output)
|
|
read = output->read(self->buffer.data(), size);
|
|
std::fill(self->buffer.data() + read, self->buffer.data() + size, 0.f);
|
|
|
|
for (auto s : self->buffer)
|
|
*dst++ = static_cast<std::int16_t>(std::max(std::min((65535.f * s - 1.f) / 2.f, 32767.f), -32768.f));
|
|
}
|
|
|
|
engine::engine()
|
|
: pimpl_(make_impl())
|
|
{}
|
|
|
|
engine::~engine() = default;
|
|
|
|
track_ptr engine::load_raw(util::span<float const> data, bool copy)
|
|
{
|
|
if ((data.size() % 2) != 0)
|
|
throw std::runtime_error("bad sample count");
|
|
|
|
auto tdata = std::make_shared<track_data>();
|
|
if (copy)
|
|
{
|
|
tdata->storage.assign(data.begin(), data.end());
|
|
tdata->samples = tdata->storage;
|
|
}
|
|
else
|
|
tdata->samples = data;
|
|
|
|
return std::make_shared<track_impl>(std::move(tdata));
|
|
}
|
|
|
|
track_ptr engine::load_wav(util::span<char const> data)
|
|
{
|
|
SDL_AudioSpec spec;
|
|
std::uint8_t * samples;
|
|
std::uint32_t length;
|
|
if (!SDL_LoadWAV_RW(SDL_RWFromConstMem(data.data(), data.size()), 1, &spec, &samples, &length))
|
|
sdl2::fail("SDL_LoadWAV_RW failed:");
|
|
|
|
util::at_scope_exit release_samples([samples]{ SDL_FreeWAV(samples); });
|
|
|
|
auto tdata = std::make_shared<track_data>();
|
|
tdata->storage = convert_audio(spec, samples, length);
|
|
tdata->samples = tdata->storage;
|
|
|
|
return std::make_shared<track_impl>(std::move(tdata));
|
|
}
|
|
|
|
channel_ptr engine::output()
|
|
{
|
|
return impl().output;
|
|
}
|
|
|
|
}
|