117 lines
4.6 KiB
C++
117 lines
4.6 KiB
C++
#include "detail/metronome_player.hpp"
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <limits>
|
|
#include <utility>
|
|
|
|
|
|
namespace {
|
|
|
|
constexpr auto pi = 3.141592653589793238462643383279502884;
|
|
|
|
auto make_click_samples(std::uint32_t sample_rate, std::uint32_t frame_count, std::float32_t frequency, std::float32_t gain) -> std::vector<std::float32_t> {
|
|
auto result = std::vector<std::float32_t>(frame_count);
|
|
auto const phase_step = (2.0 * pi * static_cast<double>(frequency)) / static_cast<double>(sample_rate);
|
|
auto phase = 0.0;
|
|
for (auto index = std::uint32_t{}; index < frame_count; ++index) {
|
|
auto const progress = static_cast<double>(index) / static_cast<double>(frame_count);
|
|
auto const envelope = (1.0 - progress) * (1.0 - progress);
|
|
result[index] = static_cast<std::float32_t>(std::sin(phase) * envelope * static_cast<double>(gain));
|
|
phase += phase_step;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
}
|
|
|
|
auto sonus::detail::metronome_player::configure(sonus::metronome_config value, std::uint32_t sample_rate, std::bool_t enable) -> void {
|
|
auto click_frames = static_cast<std::uint64_t>(sample_rate) * static_cast<std::uint64_t>(value.click_duration.count()) / 1000U;
|
|
click_frames = std::clamp<std::uint64_t>(click_frames, 1U, std::numeric_limits<std::uint32_t>::max());
|
|
auto frames_per_beat = std::max<std::uint64_t>(1U, static_cast<std::uint64_t>(std::llround((static_cast<double>(sample_rate) * 60.0) / static_cast<double>(value.bpm))));
|
|
auto result = std::make_unique<sonus::detail::metronome_render_config>();
|
|
result->enabled = enable;
|
|
result->beats_per_bar = value.beats_per_bar;
|
|
result->frames_per_beat = frames_per_beat;
|
|
if (enable) {
|
|
result->beat_click = make_click_samples(sample_rate, static_cast<std::uint32_t>(click_frames), value.beat_frequency_hz, value.volume * 0.64F);
|
|
result->downbeat_click = make_click_samples(sample_rate, static_cast<std::uint32_t>(click_frames), value.downbeat_frequency_hz, value.volume);
|
|
}
|
|
publish(std::move(result));
|
|
}
|
|
|
|
auto sonus::detail::metronome_player::stop() -> void {
|
|
publish(std::make_unique<sonus::detail::metronome_render_config>());
|
|
}
|
|
|
|
auto sonus::detail::metronome_player::active() const -> std::bool_t {
|
|
return metronome_enabled.load(std::memory_order_acquire);
|
|
}
|
|
|
|
auto sonus::detail::metronome_player::collect() -> void {
|
|
metronome_configs.collect();
|
|
}
|
|
|
|
auto sonus::detail::metronome_player::publish(std::unique_ptr<sonus::detail::metronome_render_config> next) -> void {
|
|
next->generation = next_metronome_generation++;
|
|
if (next_metronome_generation == 0U) {
|
|
next_metronome_generation = 1U;
|
|
}
|
|
auto const enabled = next->enabled;
|
|
metronome_configs.publish(std::move(next));
|
|
metronome_enabled.store(enabled, std::memory_order_release);
|
|
}
|
|
|
|
auto sonus::detail::metronome_player::render(std::float32_t *output, std::uint64_t frame_count, std::uint32_t channels) -> void {
|
|
auto guard = metronome_configs.read();
|
|
auto const *metro = guard.get();
|
|
if (metro == nullptr || !metro->enabled) {
|
|
if (metro != nullptr) {
|
|
observed_metronome_generation = metro->generation;
|
|
}
|
|
reset();
|
|
return;
|
|
}
|
|
if (observed_metronome_generation != metro->generation) {
|
|
observed_metronome_generation = metro->generation;
|
|
reset();
|
|
}
|
|
for (auto frame = std::uint64_t{}; frame < frame_count; ++frame) {
|
|
if (frames_until_next_beat == 0U) {
|
|
auto const downbeat = beat_index == 0U;
|
|
click_frame_index = 0;
|
|
click_is_downbeat = downbeat;
|
|
click_frames_remaining = downbeat ? metro->downbeat_click.size() : metro->beat_click.size();
|
|
beat_index = (beat_index + 1U) % std::max<std::uint32_t>(1U, metro->beats_per_bar);
|
|
frames_until_next_beat = metro->frames_per_beat;
|
|
}
|
|
auto sample = 0.0F;
|
|
if (click_frames_remaining > 0U) {
|
|
auto const &click = click_is_downbeat ? metro->downbeat_click : metro->beat_click;
|
|
if (click_frame_index < click.size()) {
|
|
sample = click[click_frame_index];
|
|
++click_frame_index;
|
|
--click_frames_remaining;
|
|
} else {
|
|
click_frames_remaining = 0;
|
|
}
|
|
}
|
|
if (sample != 0.0F) {
|
|
auto *frame_out = output + frame * channels;
|
|
for (auto channel = std::uint32_t{}; channel < channels; ++channel) {
|
|
auto const mixed = static_cast<std::float32_t>(frame_out[channel] + sample);
|
|
frame_out[channel] = std::clamp(mixed, static_cast<std::float32_t>(-1.0F), static_cast<std::float32_t>(1.0F));
|
|
}
|
|
}
|
|
--frames_until_next_beat;
|
|
}
|
|
}
|
|
|
|
auto sonus::detail::metronome_player::reset() -> void {
|
|
beat_index = 0;
|
|
frames_until_next_beat = 0;
|
|
click_frame_index = 0;
|
|
click_frames_remaining = 0;
|
|
click_is_downbeat = false;
|
|
}
|