#include "detail/metronome_player.hpp" #include #include #include #include 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 { auto result = std::vector(frame_count); auto const phase_step = (2.0 * pi * static_cast(frequency)) / static_cast(sample_rate); auto phase = 0.0; for (auto index = std::uint32_t{}; index < frame_count; ++index) { auto const progress = static_cast(index) / static_cast(frame_count); auto const envelope = (1.0 - progress) * (1.0 - progress); result[index] = static_cast(std::sin(phase) * envelope * static_cast(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(sample_rate) * static_cast(value.click_duration.count()) / 1000U; click_frames = std::clamp(click_frames, 1U, std::numeric_limits::max()); auto frames_per_beat = std::max(1U, static_cast(std::llround((static_cast(sample_rate) * 60.0) / static_cast(value.bpm)))); auto result = std::make_unique(); 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(click_frames), value.beat_frequency_hz, value.volume * 0.64F); result->downbeat_click = make_click_samples(sample_rate, static_cast(click_frames), value.downbeat_frequency_hz, value.volume); } publish(std::move(result)); } auto sonus::detail::metronome_player::stop() -> void { publish(std::make_unique()); } 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 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(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(frame_out[channel] + sample); frame_out[channel] = std::clamp(mixed, static_cast(-1.0F), static_cast(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; }