Audio library rewrite: re-implement engine using SDL_Audio & change API
This commit is contained in:
parent
d95807e320
commit
b3e583283a
23 changed files with 454 additions and 606 deletions
70
examples/audio.cpp
Normal file
70
examples/audio.cpp
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
#include <psemek/audio/engine.hpp>
|
||||
#include <psemek/audio/wave/sine.hpp>
|
||||
#include <psemek/audio/wave/sawtooth.hpp>
|
||||
#include <psemek/audio/wave/square.hpp>
|
||||
#include <psemek/audio/wave/triangle.hpp>
|
||||
#include <psemek/audio/mixer.hpp>
|
||||
#include <psemek/app/app.hpp>
|
||||
#include <psemek/app/main.hpp>
|
||||
|
||||
#include <map>
|
||||
|
||||
using namespace psemek;
|
||||
|
||||
static std::map<SDL_Keycode, int> const key_to_midi
|
||||
{
|
||||
{SDLK_a, 69},
|
||||
{SDLK_s, 70},
|
||||
{SDLK_d, 71},
|
||||
{SDLK_f, 72},
|
||||
{SDLK_g, 73},
|
||||
{SDLK_h, 74},
|
||||
{SDLK_j, 75},
|
||||
{SDLK_k, 76},
|
||||
{SDLK_l, 77},
|
||||
{SDLK_SEMICOLON, 78},
|
||||
{SDLK_QUOTE, 79},
|
||||
};
|
||||
|
||||
struct audio_app
|
||||
: app::app
|
||||
{
|
||||
audio_app()
|
||||
: app::app("Audio example")
|
||||
{
|
||||
mixer_ = audio::make_mixer();
|
||||
engine_.output(mixer_);
|
||||
}
|
||||
|
||||
void on_key_down(SDL_Keycode key) override
|
||||
{
|
||||
app::app::on_key_down(key);
|
||||
|
||||
if (key_to_midi.contains(key) && !channels_.contains(key))
|
||||
{
|
||||
int midi = key_to_midi.at(key);
|
||||
channels_[key] = mixer_->add(audio::sine_wave(440.f * std::pow(2.f, (midi - 69) / 12.f)));
|
||||
}
|
||||
}
|
||||
|
||||
void on_key_up(SDL_Keycode key) override
|
||||
{
|
||||
app::app::on_key_up(key);
|
||||
|
||||
if (channels_.contains(key))
|
||||
{
|
||||
channels_[key]->stop();
|
||||
channels_.erase(key);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
audio::engine engine_;
|
||||
audio::mixer_ptr mixer_;
|
||||
std::map<SDL_Keycode, audio::mixer::channel_ptr> channels_;
|
||||
};
|
||||
|
||||
int main()
|
||||
{
|
||||
return app::main<audio_app>();
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <psemek/audio/effect.hpp>
|
||||
#include <psemek/geom/point.hpp>
|
||||
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <mutex>
|
||||
|
||||
namespace psemek::audio
|
||||
{
|
||||
|
||||
struct listener_3d_mono
|
||||
{
|
||||
void set_position(geom::point<float, 3> const & position);
|
||||
|
||||
struct mono_effect
|
||||
: effect
|
||||
{
|
||||
virtual void set_position(geom::point<float, 3> const & position) = 0;
|
||||
};
|
||||
|
||||
virtual std::shared_ptr<mono_effect> make_effect(geom::point<float, 3> const & position, float distance_factor);
|
||||
|
||||
private:
|
||||
geom::point<float, 3> position_;
|
||||
std::mutex position_mutex_;
|
||||
};
|
||||
|
||||
}
|
||||
9
libs/audio/include/psemek/audio/constants.hpp
Normal file
9
libs/audio/include/psemek/audio/constants.hpp
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
#pragma once
|
||||
|
||||
namespace psemek::audio
|
||||
{
|
||||
|
||||
constexpr int frequency = 44100;
|
||||
constexpr float inv_frequency = 1.f / frequency;
|
||||
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <psemek/audio/effect.hpp>
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace psemek::audio
|
||||
{
|
||||
|
||||
std::shared_ptr<effect> echo(float delay, float volume);
|
||||
|
||||
}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <psemek/audio/sample.hpp>
|
||||
|
||||
#include <string_view>
|
||||
#include <cstddef>
|
||||
|
||||
namespace psemek::audio
|
||||
{
|
||||
|
||||
struct effect
|
||||
{
|
||||
virtual std::string_view name() const = 0;
|
||||
|
||||
virtual void init(int frequency, int channels) = 0;
|
||||
|
||||
virtual void operator()(sample * data, std::size_t count) = 0;
|
||||
|
||||
virtual ~effect() {}
|
||||
};
|
||||
|
||||
}
|
||||
|
|
@ -1,11 +1,10 @@
|
|||
#pragma once
|
||||
|
||||
#include <psemek/audio/sample.hpp>
|
||||
#include <psemek/audio/effect.hpp>
|
||||
#include <psemek/audio/stream.hpp>
|
||||
#include <psemek/audio/track.hpp>
|
||||
|
||||
#include <psemek/util/pimpl.hpp>
|
||||
#include <psemek/util/span.hpp>
|
||||
|
||||
#include <memory>
|
||||
#include <string_view>
|
||||
|
|
@ -19,31 +18,13 @@ namespace psemek::audio
|
|||
engine();
|
||||
~engine();
|
||||
|
||||
int frequency() const;
|
||||
int channels() const;
|
||||
track_ptr load(float const * data, std::size_t sample_count, bool copy = true);
|
||||
track_ptr load(util::span<float> data, bool copy = true);
|
||||
|
||||
std::shared_ptr<track> load_raw(sample const * data, std::size_t sample_count, bool copy = true);
|
||||
std::shared_ptr<track> load(char const * data, std::size_t size);
|
||||
|
||||
std::shared_ptr<track> load_raw(std::basic_string_view<sample> data, bool copy = true)
|
||||
{
|
||||
return load_raw(data.data(), data.size(), copy);
|
||||
}
|
||||
|
||||
std::shared_ptr<track> load(std::string_view data)
|
||||
{
|
||||
return load(data.data(), data.size());
|
||||
}
|
||||
|
||||
std::shared_ptr<track> load_raw(std::vector<sample> data);
|
||||
|
||||
std::shared_ptr<stream> play(std::shared_ptr<track> s, bool start = true, bool loop = false);
|
||||
|
||||
void lock();
|
||||
void unlock();
|
||||
stream_ptr output(stream_ptr stream);
|
||||
|
||||
private:
|
||||
psemek_declare_shared_pimpl
|
||||
psemek_declare_pimpl
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
|||
31
libs/audio/include/psemek/audio/mixer.hpp
Normal file
31
libs/audio/include/psemek/audio/mixer.hpp
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
#pragma once
|
||||
|
||||
#include <psemek/audio/stream.hpp>
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace psemek::audio
|
||||
{
|
||||
|
||||
struct mixer
|
||||
: stream
|
||||
{
|
||||
struct channel
|
||||
{
|
||||
virtual void stop() = 0;
|
||||
|
||||
virtual bool is_stopped() const = 0;
|
||||
|
||||
virtual ~channel() {}
|
||||
};
|
||||
|
||||
using channel_ptr = std::shared_ptr<channel>;
|
||||
|
||||
virtual channel_ptr add(stream_ptr stream) = 0;
|
||||
};
|
||||
|
||||
using mixer_ptr = std::shared_ptr<mixer>;
|
||||
|
||||
mixer_ptr make_mixer();
|
||||
|
||||
}
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <cstdint>
|
||||
#include <algorithm>
|
||||
|
||||
namespace psemek::audio
|
||||
{
|
||||
|
||||
using sample = std::int16_t;
|
||||
|
||||
inline sample clamp(float v)
|
||||
{
|
||||
static auto const min = std::numeric_limits<sample>::min();
|
||||
static auto const max = std::numeric_limits<sample>::max();
|
||||
|
||||
return static_cast<sample>(std::min<int>(max, std::max<int>(min, std::round(v))));
|
||||
}
|
||||
|
||||
// maps [-1, 1] range to samples
|
||||
inline sample pack(float v)
|
||||
{
|
||||
static auto const min = static_cast<float>(std::numeric_limits<sample>::min());
|
||||
static auto const max = static_cast<float>(std::numeric_limits<sample>::max());
|
||||
|
||||
return clamp(min + (v * 0.5f + 0.5f) * (max - min));
|
||||
}
|
||||
|
||||
// maps samples to [-1, 1] range
|
||||
inline float unpack(sample s)
|
||||
{
|
||||
static auto const min = static_cast<float>(std::numeric_limits<sample>::min());
|
||||
static auto const max = static_cast<float>(std::numeric_limits<sample>::max());
|
||||
|
||||
return 2.f * (s - min) / (max - min) - 1.f;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,9 +1,6 @@
|
|||
#pragma once
|
||||
|
||||
#include <psemek/audio/effect.hpp>
|
||||
|
||||
#include <psemek/util/function.hpp>
|
||||
|
||||
#include <cstddef>
|
||||
#include <memory>
|
||||
|
||||
namespace psemek::audio
|
||||
|
|
@ -11,22 +8,12 @@ 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 void on_stop(util::function<void()> callback) = 0;
|
||||
// Return value less than sample count means end of stream
|
||||
virtual std::size_t read(float * data, std::size_t sample_count) = 0;
|
||||
|
||||
virtual ~stream() {}
|
||||
};
|
||||
|
||||
using stream_ptr = std::shared_ptr<stream>;
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,18 @@
|
|||
#pragma once
|
||||
|
||||
#include <psemek/audio/sample.hpp>
|
||||
#include <psemek/audio/stream.hpp>
|
||||
|
||||
namespace psemek::audio
|
||||
{
|
||||
|
||||
struct track
|
||||
{
|
||||
virtual sample const * data() const = 0;
|
||||
virtual std::size_t size() const = 0;
|
||||
virtual stream_ptr stream() const = 0;
|
||||
virtual std::size_t length() const = 0;
|
||||
|
||||
virtual ~track(){}
|
||||
};
|
||||
|
||||
using track_ptr = std::shared_ptr<track>;
|
||||
|
||||
}
|
||||
|
|
|
|||
36
libs/audio/include/psemek/audio/wave/generator.hpp
Normal file
36
libs/audio/include/psemek/audio/wave/generator.hpp
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
#pragma once
|
||||
|
||||
#include <psemek/audio/stream.hpp>
|
||||
#include <psemek/audio/constants.hpp>
|
||||
|
||||
namespace psemek::audio
|
||||
{
|
||||
|
||||
template <typename Func>
|
||||
struct generator_stream
|
||||
: stream
|
||||
{
|
||||
generator_stream(Func func)
|
||||
: func_(std::move(func))
|
||||
, phase_(0.f)
|
||||
{}
|
||||
|
||||
std::size_t read(float * data, std::size_t sample_count) override
|
||||
{
|
||||
auto end = data + sample_count;
|
||||
for (auto p = data; p != end;)
|
||||
{
|
||||
float v = func_(phase_);
|
||||
*p++ = v;
|
||||
*p++ = v;
|
||||
phase_ += inv_frequency;
|
||||
}
|
||||
return sample_count;
|
||||
}
|
||||
|
||||
private:
|
||||
Func func_;
|
||||
float phase_;
|
||||
};
|
||||
|
||||
}
|
||||
10
libs/audio/include/psemek/audio/wave/sawtooth.hpp
Normal file
10
libs/audio/include/psemek/audio/wave/sawtooth.hpp
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
#pragma once
|
||||
|
||||
#include <psemek/audio/stream.hpp>
|
||||
|
||||
namespace psemek::audio
|
||||
{
|
||||
|
||||
stream_ptr sawtooth_wave(float frequency);
|
||||
|
||||
}
|
||||
10
libs/audio/include/psemek/audio/wave/sine.hpp
Normal file
10
libs/audio/include/psemek/audio/wave/sine.hpp
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
#pragma once
|
||||
|
||||
#include <psemek/audio/stream.hpp>
|
||||
|
||||
namespace psemek::audio
|
||||
{
|
||||
|
||||
stream_ptr sine_wave(float frequency);
|
||||
|
||||
}
|
||||
10
libs/audio/include/psemek/audio/wave/square.hpp
Normal file
10
libs/audio/include/psemek/audio/wave/square.hpp
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
#pragma once
|
||||
|
||||
#include <psemek/audio/stream.hpp>
|
||||
|
||||
namespace psemek::audio
|
||||
{
|
||||
|
||||
stream_ptr square_wave(float frequency);
|
||||
|
||||
}
|
||||
10
libs/audio/include/psemek/audio/wave/triangle.hpp
Normal file
10
libs/audio/include/psemek/audio/wave/triangle.hpp
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
#pragma once
|
||||
|
||||
#include <psemek/audio/stream.hpp>
|
||||
|
||||
namespace psemek::audio
|
||||
{
|
||||
|
||||
stream_ptr triangle_wave(float frequency);
|
||||
|
||||
}
|
||||
|
|
@ -1,83 +0,0 @@
|
|||
#include <psemek/audio/3d.hpp>
|
||||
#include <psemek/util/not_implemented.hpp>
|
||||
|
||||
#include <optional>
|
||||
#include <mutex>
|
||||
|
||||
namespace psemek::audio
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
struct mono_effect_impl
|
||||
: listener_3d_mono::mono_effect
|
||||
{
|
||||
mono_effect_impl(geom::point<float, 3> const & source_position, geom::point<float, 3> & target_position, std::mutex & target_position_mutex, float distance_factor)
|
||||
: source_position_(source_position)
|
||||
, target_position_(target_position)
|
||||
, cached_target_position_(target_position)
|
||||
, target_position_mutex_(target_position_mutex)
|
||||
, distance_factor_(distance_factor)
|
||||
{}
|
||||
|
||||
std::string_view name() const override
|
||||
{
|
||||
return "mono_3d";
|
||||
}
|
||||
|
||||
void init(int, int) override
|
||||
{ }
|
||||
|
||||
void operator()(sample * data, std::size_t count) override
|
||||
{
|
||||
{
|
||||
std::unique_lock lock{target_position_mutex_, std::try_to_lock};
|
||||
if (lock.owns_lock())
|
||||
cached_target_position_ = target_position_;
|
||||
}
|
||||
|
||||
{
|
||||
std::unique_lock lock{source_position_mutex_, std::try_to_lock};
|
||||
if (lock.owns_lock())
|
||||
cached_source_position_ = source_position_;
|
||||
}
|
||||
|
||||
float distance = geom::distance(cached_target_position_, cached_source_position_) / distance_factor_;
|
||||
float factor = std::min(1.f, 1.f / (1.f + distance * distance));
|
||||
|
||||
for (std::size_t i = 0; i < count; ++i)
|
||||
data[i] = pack(unpack(data[i]) * factor);
|
||||
}
|
||||
|
||||
void set_position(geom::point<float, 3> const & position) override
|
||||
{
|
||||
std::lock_guard lock{source_position_mutex_};
|
||||
source_position_ = position;
|
||||
}
|
||||
|
||||
private:
|
||||
std::mutex source_position_mutex_;
|
||||
geom::point<float, 3> source_position_;
|
||||
geom::point<float, 3> cached_source_position_;
|
||||
|
||||
geom::point<float, 3> & target_position_;
|
||||
geom::point<float, 3> cached_target_position_;
|
||||
std::mutex & target_position_mutex_;
|
||||
float distance_factor_;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
void listener_3d_mono::set_position(geom::point<float, 3> const & position)
|
||||
{
|
||||
std::lock_guard lock{position_mutex_};
|
||||
position_ = position;
|
||||
}
|
||||
|
||||
std::shared_ptr<listener_3d_mono::mono_effect> listener_3d_mono::make_effect(const geom::point<float, 3> &position, float distance_factor)
|
||||
{
|
||||
return std::make_shared<mono_effect_impl>(position, position_, position_mutex_, distance_factor);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,73 +0,0 @@
|
|||
#include <psemek/audio/echo.hpp>
|
||||
#include <psemek/audio/engine.hpp>
|
||||
#include <psemek/audio/sample.hpp>
|
||||
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
|
||||
namespace psemek::audio
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
struct echo_impl
|
||||
: audio::effect
|
||||
{
|
||||
echo_impl(float delay, float volume)
|
||||
: delay_(delay)
|
||||
, volume_(volume)
|
||||
{}
|
||||
|
||||
void init(int frequency, int channels) override
|
||||
{
|
||||
int sample_count = std::round(delay_ * frequency) * channels;
|
||||
samples_.reserve(sample_count);
|
||||
}
|
||||
|
||||
std::string_view name() const override
|
||||
{
|
||||
return "echo";
|
||||
}
|
||||
|
||||
void operator()(sample * data, std::size_t size) override
|
||||
{
|
||||
if (samples_.size() < samples_.capacity())
|
||||
{
|
||||
auto count = std::min(samples_.capacity() - samples_.size(), size);
|
||||
std::copy(data, data + count, std::back_inserter(samples_));
|
||||
pos_ += count;
|
||||
data += count;
|
||||
size -= count;
|
||||
|
||||
if (pos_ == samples_.capacity())
|
||||
pos_ = 0;
|
||||
}
|
||||
|
||||
for (; size > 0; ++data, --size)
|
||||
{
|
||||
float s = clamp((*data) + samples_[pos_] * volume_);
|
||||
samples_[pos_] = *data;
|
||||
*data = s;
|
||||
|
||||
++pos_;
|
||||
if (pos_ == samples_.capacity())
|
||||
pos_ = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<sample> samples_;
|
||||
std::size_t pos_ = 0;
|
||||
float delay_;
|
||||
float volume_;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
std::shared_ptr<effect> echo(float delay, float volume)
|
||||
{
|
||||
return std::make_shared<echo_impl>(delay, volume);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,11 +1,12 @@
|
|||
#include <psemek/audio/engine.hpp>
|
||||
#include <psemek/audio/constants.hpp>
|
||||
#include <psemek/sdl2/init.hpp>
|
||||
#include <psemek/log/log.hpp>
|
||||
|
||||
#include <SDL2/SDL.h>
|
||||
#include <SDL2/SDL_mixer.h>
|
||||
|
||||
#include <mutex>
|
||||
#include <atomic>
|
||||
#include <vector>
|
||||
#include <optional>
|
||||
#include <cmath>
|
||||
|
|
@ -13,349 +14,96 @@
|
|||
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 spec_t
|
||||
{
|
||||
int frequency = 44100;
|
||||
int channels = 2;
|
||||
};
|
||||
|
||||
struct track_impl
|
||||
: track
|
||||
{
|
||||
Mix_Chunk * chunk;
|
||||
std::vector<sample> buffer;
|
||||
|
||||
track_impl(Mix_Chunk * chunk)
|
||||
: chunk(chunk)
|
||||
{}
|
||||
|
||||
track_impl(Mix_Chunk * chunk, std::vector<sample> buffer)
|
||||
: chunk(chunk)
|
||||
, buffer(std::move(buffer))
|
||||
{}
|
||||
|
||||
sample const * data() const override
|
||||
{
|
||||
return reinterpret_cast<sample const *>(chunk->abuf);
|
||||
}
|
||||
|
||||
std::size_t size() const override
|
||||
{
|
||||
return chunk->alen / 2;
|
||||
}
|
||||
|
||||
~track_impl()
|
||||
{
|
||||
Mix_FreeChunk(chunk);
|
||||
}
|
||||
};
|
||||
|
||||
struct stream_impl
|
||||
: stream
|
||||
{
|
||||
bool playing = false;
|
||||
|
||||
int channel;
|
||||
std::shared_ptr<track_impl> track;
|
||||
bool loop;
|
||||
|
||||
spec_t spec;
|
||||
|
||||
std::vector<std::shared_ptr<effect>> effects;
|
||||
|
||||
util::function<void()> callback;
|
||||
|
||||
stream_impl(int channel, std::shared_ptr<track_impl> track, bool loop, spec_t spec)
|
||||
: channel(channel)
|
||||
, track(track)
|
||||
, loop(loop)
|
||||
, spec(spec)
|
||||
{}
|
||||
|
||||
void volume(float value) override
|
||||
{
|
||||
int v = std::min(MIX_MAX_VOLUME, 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
|
||||
{
|
||||
e->init(spec.frequency, spec.channels);
|
||||
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<sample *>(data), len / 2);
|
||||
}
|
||||
|
||||
void start() override
|
||||
{
|
||||
playing = true;
|
||||
Mix_PlayChannel(channel, track->chunk, loop ? -1 : 0);
|
||||
}
|
||||
|
||||
void pause() override
|
||||
{
|
||||
playing = false;
|
||||
Mix_Pause(channel);
|
||||
}
|
||||
|
||||
void resume() override
|
||||
{
|
||||
playing = true;
|
||||
Mix_Resume(channel);
|
||||
}
|
||||
|
||||
void stop() override
|
||||
{
|
||||
playing = false;
|
||||
Mix_HaltChannel(channel);
|
||||
}
|
||||
|
||||
bool is_playing() const override
|
||||
{
|
||||
return playing;
|
||||
}
|
||||
|
||||
void on_stop(util::function<void()> callback) override
|
||||
{
|
||||
this->callback = std::move(callback);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
struct engine::impl
|
||||
{
|
||||
std::shared_ptr<void> sdl_init;
|
||||
sdl2_mixer_initializer mix_init;
|
||||
|
||||
spec_t spec;
|
||||
SDL_AudioDeviceID device;
|
||||
|
||||
struct channel
|
||||
{
|
||||
std::shared_ptr<stream_impl> stream;
|
||||
};
|
||||
std::vector<float> buffer;
|
||||
bool thread_registered = false;
|
||||
|
||||
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();
|
||||
stream_ptr output;
|
||||
|
||||
impl();
|
||||
~impl();
|
||||
|
||||
std::shared_ptr<stream> play(std::shared_ptr<track> s, bool loop);
|
||||
|
||||
static void channel_finished(int ch);
|
||||
static void callback(void * userdata, std::uint8_t * stream, int len);
|
||||
};
|
||||
|
||||
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 = make_impl();
|
||||
instance_ptr = p;
|
||||
return p;
|
||||
}
|
||||
|
||||
engine::impl::impl()
|
||||
: sdl_init(sdl2::init(SDL_INIT_AUDIO))
|
||||
{
|
||||
if (Mix_OpenAudio(spec.frequency, AUDIO_S16SYS, spec.channels, 256) != 0)
|
||||
mix_fail("Mix_OpenAudio: ");
|
||||
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:");
|
||||
|
||||
Uint16 format;
|
||||
Mix_QuerySpec(&spec.frequency, &format, &spec.channels);
|
||||
buffer.resize(obtained.samples * obtained.channels);
|
||||
|
||||
if (format != AUDIO_S16SYS)
|
||||
throw std::runtime_error("Failed to initialize audio with int16 samples");
|
||||
SDL_PauseAudioDevice(device, 0);
|
||||
|
||||
Mix_ChannelFinished(&channel_finished);
|
||||
|
||||
log::info() << "Initialized audio: " << spec.channels << " channels, " << spec.frequency << " Hz";
|
||||
log::info() << "Initialized audio: " << static_cast<int>(obtained.channels) << " channels, " << obtained.freq << " Hz, " << obtained.samples << " samples";
|
||||
}
|
||||
|
||||
engine::impl::~impl()
|
||||
{
|
||||
for (auto & ch : channels)
|
||||
if (ch.stream)
|
||||
ch.stream->stop();
|
||||
Mix_CloseAudio();
|
||||
SDL_CloseAudioDevice(device);
|
||||
}
|
||||
|
||||
std::shared_ptr<stream> engine::impl::play(std::shared_ptr<track> s, bool loop)
|
||||
void engine::impl::callback(void * userdata, std::uint8_t * dst_u8, int len)
|
||||
{
|
||||
auto ss = std::dynamic_pointer_cast<track_impl>(s);
|
||||
if (!ss)
|
||||
auto self = static_cast<impl *>(userdata);
|
||||
stream_ptr output = std::atomic_load(&(self->output));
|
||||
std::int16_t * dst = reinterpret_cast<std::int16_t *>(dst_u8);
|
||||
|
||||
if (!self->thread_registered)
|
||||
{
|
||||
throw std::runtime_error("Failed to play sample: unknown sample type");
|
||||
log::register_thread("audio");
|
||||
self->thread_registered = true;
|
||||
}
|
||||
|
||||
std::lock_guard lock{channels_mutex};
|
||||
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);
|
||||
|
||||
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);
|
||||
log::info() << "Allocating " << channels.size() << " mixing channels";
|
||||
Mix_AllocateChannels(channels.size());
|
||||
ch = c;
|
||||
}
|
||||
|
||||
auto str = std::make_shared<stream_impl>(*ch, std::move(ss), loop, spec);
|
||||
|
||||
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;
|
||||
|
||||
util::function<void()> callback;
|
||||
{
|
||||
// callback will probably lock something as well,
|
||||
// leading to a deadlock, so we have to call the
|
||||
// callback outside of the lock
|
||||
std::lock_guard lock{self->channels_mutex};
|
||||
self->channels[ch].stream->playing = false;
|
||||
callback = std::move(self->channels[ch].stream->callback);
|
||||
self->channels[ch].stream = nullptr;
|
||||
}
|
||||
|
||||
if (callback)
|
||||
callback();
|
||||
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_{impl::instance()}
|
||||
: pimpl_(make_impl())
|
||||
{}
|
||||
|
||||
engine::~engine()
|
||||
{}
|
||||
engine::~engine() = default;
|
||||
|
||||
int engine::frequency() const
|
||||
track_ptr engine::load(float const * data, std::size_t sample_count, bool copy)
|
||||
{
|
||||
return impl::instance()->spec.frequency;
|
||||
if ((sample_count % 2) != 0)
|
||||
throw std::runtime_error("bad sample count");
|
||||
|
||||
(void)data;
|
||||
(void)sample_count;
|
||||
(void)copy;
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
int engine::channels() const
|
||||
track_ptr engine::load(util::span<float> data, bool copy)
|
||||
{
|
||||
return impl::instance()->spec.channels;
|
||||
return load(data.data(), data.size(), copy);
|
||||
}
|
||||
|
||||
std::shared_ptr<track> engine::load(char const * data, std::size_t size)
|
||||
stream_ptr engine::output(stream_ptr stream)
|
||||
{
|
||||
return std::make_shared<track_impl>(Mix_LoadWAV_RW(SDL_RWFromConstMem(data, size), 1));
|
||||
}
|
||||
|
||||
std::shared_ptr<track> engine::load_raw(sample 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(sample);
|
||||
chunk->volume = 128;
|
||||
if (copy)
|
||||
{
|
||||
chunk->abuf = static_cast<Uint8 *>(malloc(sample_count * sizeof(sample)));
|
||||
std::copy(data, data + sample_count, reinterpret_cast<sample *>(chunk->abuf));
|
||||
}
|
||||
else
|
||||
{
|
||||
chunk->abuf = const_cast<Uint8 *>(reinterpret_cast<Uint8 const *>(data));
|
||||
}
|
||||
return std::make_shared<track_impl>(chunk);
|
||||
}
|
||||
|
||||
std::shared_ptr<track> engine::load_raw(std::vector<sample> data)
|
||||
{
|
||||
Mix_Chunk * chunk = static_cast<Mix_Chunk *>(malloc(sizeof(Mix_Chunk)));
|
||||
chunk->allocated = 0;
|
||||
chunk->alen = data.size() * sizeof(sample);
|
||||
chunk->volume = 128;
|
||||
chunk->abuf = const_cast<Uint8 *>(reinterpret_cast<Uint8 const *>(data.data()));
|
||||
return std::make_shared<track_impl>(chunk, std::move(data));
|
||||
}
|
||||
|
||||
std::shared_ptr<stream> engine::play(std::shared_ptr<track> s, bool start, bool loop)
|
||||
{
|
||||
auto str = impl().play(std::move(s), loop);
|
||||
if (start) str->start();
|
||||
return str;
|
||||
}
|
||||
|
||||
void engine::lock()
|
||||
{
|
||||
SDL_LockAudio();
|
||||
}
|
||||
|
||||
void engine::unlock()
|
||||
{
|
||||
SDL_UnlockAudio();
|
||||
return std::atomic_exchange(&(impl().output), std::move(stream));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
118
libs/audio/source/mixer.cpp
Normal file
118
libs/audio/source/mixer.cpp
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
#include <psemek/audio/mixer.hpp>
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
#include <atomic>
|
||||
|
||||
namespace psemek::audio
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
struct channel_impl final
|
||||
: mixer::channel
|
||||
{
|
||||
stream_ptr stream;
|
||||
|
||||
void stop() override;
|
||||
|
||||
bool is_stopped() const override;
|
||||
};
|
||||
|
||||
void channel_impl::stop()
|
||||
{
|
||||
std::atomic_store(&stream, stream_ptr());
|
||||
}
|
||||
|
||||
bool channel_impl::is_stopped() const
|
||||
{
|
||||
return std::atomic_load(&stream) != nullptr;
|
||||
}
|
||||
|
||||
struct mixer_impl final
|
||||
: mixer
|
||||
, std::enable_shared_from_this<mixer_impl>
|
||||
{
|
||||
channel_ptr add(stream_ptr stream) override;
|
||||
|
||||
std::size_t read(float * data, std::size_t sample_count) override;
|
||||
|
||||
private:
|
||||
std::vector<std::shared_ptr<channel_impl>> channels_;
|
||||
std::vector<std::shared_ptr<channel_impl>> alive_channels_;
|
||||
|
||||
std::vector<float> buffer_;
|
||||
|
||||
std::mutex new_channels_mutex_;
|
||||
std::vector<std::shared_ptr<channel_impl>> new_channels_;
|
||||
};
|
||||
|
||||
mixer::channel_ptr mixer_impl::add(stream_ptr stream)
|
||||
{
|
||||
auto result = std::make_shared<channel_impl>();
|
||||
result->stream = std::move(stream);
|
||||
|
||||
{
|
||||
std::lock_guard lock{new_channels_mutex_};
|
||||
new_channels_.push_back(result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
std::size_t mixer_impl::read(float * data, std::size_t sample_count)
|
||||
{
|
||||
{
|
||||
std::vector<std::shared_ptr<channel_impl>> new_channels;
|
||||
{
|
||||
std::lock_guard lock{new_channels_mutex_};
|
||||
new_channels = std::move(new_channels_);
|
||||
}
|
||||
for (auto & ch : new_channels)
|
||||
channels_.push_back(std::move(ch));
|
||||
}
|
||||
|
||||
std::fill(data, data + sample_count, 0.f);
|
||||
|
||||
buffer_.resize(sample_count);
|
||||
|
||||
for (auto & ch : channels_)
|
||||
{
|
||||
auto stream = std::atomic_load(&(ch->stream));
|
||||
if (!stream)
|
||||
continue;
|
||||
|
||||
auto read = stream->read(buffer_.data(), sample_count);
|
||||
|
||||
{
|
||||
auto begin = buffer_.data();
|
||||
auto end = begin + read;
|
||||
auto dst = data;
|
||||
for (; begin < end; )
|
||||
*dst++ += *begin++;
|
||||
}
|
||||
|
||||
if (read < sample_count)
|
||||
{
|
||||
ch->stop();
|
||||
continue;
|
||||
}
|
||||
|
||||
alive_channels_.push_back(std::move(ch));
|
||||
}
|
||||
|
||||
std::swap(channels_, alive_channels_);
|
||||
alive_channels_.clear();
|
||||
|
||||
return sample_count;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
mixer_ptr make_mixer()
|
||||
{
|
||||
return std::make_shared<mixer_impl>();
|
||||
}
|
||||
|
||||
}
|
||||
21
libs/audio/source/wave/sawtooth.cpp
Normal file
21
libs/audio/source/wave/sawtooth.cpp
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
#include <psemek/audio/wave/sawtooth.hpp>
|
||||
#include <psemek/audio/wave/generator.hpp>
|
||||
#include <psemek/geom/math.hpp>
|
||||
#include <psemek/util/to_shared.hpp>
|
||||
|
||||
#include <cmath>
|
||||
|
||||
namespace psemek::audio
|
||||
{
|
||||
|
||||
stream_ptr sawtooth_wave(float frequency)
|
||||
{
|
||||
auto func = [frequency](float t){
|
||||
t *= frequency;
|
||||
return (t - std::floor(t)) * 2.f - 1.f;
|
||||
};
|
||||
|
||||
return util::to_shared(generator_stream(func));
|
||||
}
|
||||
|
||||
}
|
||||
22
libs/audio/source/wave/sine.cpp
Normal file
22
libs/audio/source/wave/sine.cpp
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
#include <psemek/audio/wave/sine.hpp>
|
||||
#include <psemek/audio/wave/generator.hpp>
|
||||
#include <psemek/geom/math.hpp>
|
||||
#include <psemek/util/to_shared.hpp>
|
||||
|
||||
#include <cmath>
|
||||
|
||||
namespace psemek::audio
|
||||
{
|
||||
|
||||
stream_ptr sine_wave(float frequency)
|
||||
{
|
||||
float angular_frequency = frequency * 2.f * geom::pi;
|
||||
|
||||
auto func = [angular_frequency](float t){
|
||||
return std::sin(angular_frequency * t);
|
||||
};
|
||||
|
||||
return util::to_shared(generator_stream(func));
|
||||
}
|
||||
|
||||
}
|
||||
21
libs/audio/source/wave/square.cpp
Normal file
21
libs/audio/source/wave/square.cpp
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
#include <psemek/audio/wave/square.hpp>
|
||||
#include <psemek/audio/wave/generator.hpp>
|
||||
#include <psemek/geom/math.hpp>
|
||||
#include <psemek/util/to_shared.hpp>
|
||||
|
||||
#include <cmath>
|
||||
|
||||
namespace psemek::audio
|
||||
{
|
||||
|
||||
stream_ptr square_wave(float frequency)
|
||||
{
|
||||
auto func = [frequency](float t){
|
||||
t *= frequency;
|
||||
return (t - std::floor(t)) < 0.5f ? 1.f : -1.f;
|
||||
};
|
||||
|
||||
return util::to_shared(generator_stream(func));
|
||||
}
|
||||
|
||||
}
|
||||
21
libs/audio/source/wave/triangle.cpp
Normal file
21
libs/audio/source/wave/triangle.cpp
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
#include <psemek/audio/wave/triangle.hpp>
|
||||
#include <psemek/audio/wave/generator.hpp>
|
||||
#include <psemek/geom/math.hpp>
|
||||
#include <psemek/util/to_shared.hpp>
|
||||
|
||||
#include <cmath>
|
||||
|
||||
namespace psemek::audio
|
||||
{
|
||||
|
||||
stream_ptr triangle_wave(float frequency)
|
||||
{
|
||||
auto func = [frequency](float t){
|
||||
t *= frequency;
|
||||
return 4.f * std::abs(t - std::floor(t + 0.5f)) - 1.f;
|
||||
};
|
||||
|
||||
return util::to_shared(generator_stream(func));
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue