diff --git a/src/custom_sfml_audio/beat_ticks.cpp b/src/custom_sfml_audio/beat_ticks.cpp new file mode 100644 index 0000000..b4b6c11 --- /dev/null +++ b/src/custom_sfml_audio/beat_ticks.cpp @@ -0,0 +1,116 @@ +#include "beat_ticks.hpp" + +#include +#include +#include + +#include "../better_note.hpp" + +BeatTicks::BeatTicks( + const better::Timing* timing_, + const std::filesystem::path& assets +) : + timing(timing_), + beat_tick() +{ + if (not beat_tick.loadFromFile(assets / "sounds" / "beat.wav")) { + throw std::runtime_error("Could not load beat tick audio file"); + } + sf::SoundStream::initialize(beat_tick.getChannelCount(), beat_tick.getSampleRate()); + samples.resize(timeToSamples(sf::milliseconds(200)), 0); +} + +void BeatTicks::set_timing(const better::Timing* timing_) { + timing = timing_; +} + +bool BeatTicks::onGetData(sf::SoundStream::Chunk& data) { + samples.assign(samples.size(), 0); + if (timing != nullptr) { + const auto start_sample = current_sample; + const auto end_sample = current_sample + static_cast(samples.size()); + const auto start_time = samplesToTime(start_sample); + const auto end_time = samplesToTime(end_sample); + const auto start_beat = timing->beats_at(start_time); + const auto end_beat = timing->beats_at(end_time); + + auto first_beat = static_cast(start_beat); + while (first_beat < start_beat) { + first_beat++; + } + for (std::int64_t beat = first_beat; beat < end_beat; beat++) { + const auto time = timing->time_at(beat); + const auto sample = static_cast(timeToSamples(time)); + beat_at_sample.insert(sample); + } + + for (auto it = beat_at_sample.begin(); it != beat_at_sample.end();) { + // Should we still be playing the clap ? + const auto next = std::next(it); + const auto last_audible_start = start_sample - static_cast(beat_tick.getSampleCount()); + if (*it <= last_audible_start) { + it = beat_at_sample.erase(it); + } else { + const auto full_tick_start_in_buffer = *it - static_cast(start_sample); + const auto slice_start_in_buffer = std::max(std::int64_t(0), full_tick_start_in_buffer); + const auto full_tick_end_in_buffer = full_tick_start_in_buffer + static_cast(beat_tick.getSampleCount()); + auto slice_end_in_buffer = full_tick_end_in_buffer; + bool tick_finished_playing_in_current_buffer = true; + if (next != beat_at_sample.end()) { + slice_end_in_buffer = std::min( + slice_end_in_buffer, + *next - static_cast(start_sample) + ); + } else if (slice_end_in_buffer > static_cast(samples.size())) { + tick_finished_playing_in_current_buffer = false; + slice_end_in_buffer = static_cast(samples.size()); + } + auto slice_start_in_tick = slice_start_in_buffer - full_tick_start_in_buffer; + auto slice_size = std::min( + slice_end_in_buffer - slice_start_in_buffer, + static_cast(beat_tick.getSampleCount()) - slice_start_in_tick + ); + const auto tick_pointer = beat_tick.getSamples() + slice_start_in_tick; + std::copy( + tick_pointer, + tick_pointer + slice_size, + samples.begin() + slice_start_in_buffer + ); + if (tick_finished_playing_in_current_buffer) { + it = beat_at_sample.erase(it); + } else { + ++it; + } + } + } + } + + data.samples = samples.data(); + data.sampleCount = samples.size(); + current_sample += samples.size(); + + return true; +}; + +void BeatTicks::onSeek(sf::Time timeOffset) { + current_sample = timeToSamples(timeOffset); + beat_at_sample.clear(); +}; + +std::int64_t BeatTicks::timeToSamples(sf::Time position) const { + // Always ROUND, no unchecked truncation, hence the addition in the numerator. + // This avoids most precision errors arising from "samples => Time => samples" conversions + // Original rounding calculation is ((Micros * Freq * Channels) / 1000000) + 0.5 + // We refactor it to keep Int64 as the data type throughout the whole operation. + return ((static_cast(position.asMicroseconds()) * beat_tick.getSampleRate() * beat_tick.getChannelCount()) + 500000) / 1000000; +} + +sf::Time BeatTicks::samplesToTime(std::int64_t samples) const { + sf::Time position = sf::Time::Zero; + + // Make sure we don't divide by 0 + if (beat_tick.getSampleRate() != 0 && beat_tick.getChannelCount() != 0) + position = sf::microseconds((samples * 1000000) / (beat_tick.getChannelCount() * beat_tick.getSampleRate())); + + return position; +} \ No newline at end of file diff --git a/src/custom_sfml_audio/beat_ticks.hpp b/src/custom_sfml_audio/beat_ticks.hpp new file mode 100644 index 0000000..5535c89 --- /dev/null +++ b/src/custom_sfml_audio/beat_ticks.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include + +#include + +#include "../better_timing.hpp" +#include "precise_sound_stream.hpp" + +class BeatTicks: public PreciseSoundStream { +public: + BeatTicks( + const better::Timing* timing_, + const std::filesystem::path& assets + ); + + void set_timing(const better::Timing* timing); + std::atomic play_chords = true; + +protected: + bool onGetData(Chunk& data) override; + void onSeek(sf::Time timeOffset) override; + +private: + std::vector samples; + std::int64_t current_sample = 0; + std::int64_t timeToSamples(sf::Time position) const; + sf::Time samplesToTime(std::int64_t samples) const; + + std::set beat_at_sample; + + const better::Timing* timing; + sf::SoundBuffer beat_tick; +}; \ No newline at end of file diff --git a/src/custom_sfml_audio/meson.build b/src/custom_sfml_audio/meson.build index 00d4772..26b26f3 100644 --- a/src/custom_sfml_audio/meson.build +++ b/src/custom_sfml_audio/meson.build @@ -2,6 +2,7 @@ sources += files([ 'al_check.cpp', 'al_resource.cpp', 'audio_device.cpp', + 'beat_ticks.cpp', 'note_claps.cpp', 'open_music.cpp', 'open_sound_stream.cpp', diff --git a/src/custom_sfml_audio/synced_sound_streams.cpp b/src/custom_sfml_audio/synced_sound_streams.cpp index 4fe7e48..ea40bfe 100644 --- a/src/custom_sfml_audio/synced_sound_streams.cpp +++ b/src/custom_sfml_audio/synced_sound_streams.cpp @@ -61,48 +61,46 @@ SyncedSoundStreams::~SyncedSoundStreams() { void SyncedSoundStreams::add_stream(const std::string& name, std::shared_ptr s) { - stream_change_requests.try_enqueue(AddStream{name, s}); - if (not m_isStreaming) { - // if we are not currently playing audio there should be no problem - // changing the streams right now - unsafe_update_streams(); + const auto oldStatus = getStatus(); + const auto position = getPrecisePlayingOffset(); + stop(); + { + InternalStream internal_stream{s, {}}; + internal_stream.buffers.m_channelCount = s->getChannelCount(); + internal_stream.buffers.m_sampleRate = s->getSampleRate(); + internal_stream.buffers.m_format = AudioDevice::getFormatFromChannelCount(s->getChannelCount()); + streams.emplace(name, internal_stream); + } + reload_sources(); + if (oldStatus != sf::SoundSource::Stopped) { + setPlayingOffset(position); + } + if (oldStatus == sf::SoundSource::Playing) { + play(); } } void SyncedSoundStreams::remove_stream(const std::string& name) { - stream_change_requests.try_enqueue(RemoveStream{name}); - if (not m_isStreaming) { - // if we are not currently playing audio there should be no problem - // changing the streams right now - unsafe_update_streams(); + const auto oldStatus = getStatus(); + const auto position = getPrecisePlayingOffset(); + stop(); + { + if (streams.contains(name)) { + streams.at(name).clear_queue(); + } + streams.erase(name); + } + reload_sources(); + if (oldStatus != sf::SoundSource::Stopped) { + setPlayingOffset(position); + } + if (oldStatus == sf::SoundSource::Playing) { + play(); } } -void SyncedSoundStreams::unsafe_update_streams() { - ChangeStreamsCommand c; - bool modified_stuff = false; - auto _do_request = VariantVisitor { - [this](const AddStream& a) { - InternalStream internal_stream{a.stream, {}}; - internal_stream.buffers.m_channelCount = a.stream->getChannelCount(); - internal_stream.buffers.m_sampleRate = a.stream->getSampleRate(); - internal_stream.buffers.m_format = AudioDevice::getFormatFromChannelCount(a.stream->getChannelCount()); - streams.emplace(a.name, internal_stream); - }, - [this](const RemoveStream& r) { - if (streams.contains(r.name)) { - streams.at(r.name).clear_queue(); - } - streams.erase(r.name); - }, - }; - while (stream_change_requests.try_dequeue(c)) { - std::visit(_do_request, c); - modified_stuff = true; - } - if (modified_stuff) { - reload_sources(); - } +bool SyncedSoundStreams::contains_stream(const std::string& name) { + return streams.contains(name); } void SyncedSoundStreams::play() { @@ -398,9 +396,6 @@ void SyncedSoundStreams::streamData() { break; } - // Process stream change requests - unsafe_update_streams(); - // Leave some time for the other threads if the stream is still playing if (std::any_of(streams.begin(), streams.end(), [](auto& s){ return s.second.stream->getStatus() != sf::SoundSource::Stopped; diff --git a/src/custom_sfml_audio/synced_sound_streams.hpp b/src/custom_sfml_audio/synced_sound_streams.hpp index d0f55e4..4bf5e2b 100644 --- a/src/custom_sfml_audio/synced_sound_streams.hpp +++ b/src/custom_sfml_audio/synced_sound_streams.hpp @@ -14,7 +14,6 @@ #include "al_resource.hpp" #include "precise_sound_stream.hpp" -#include "readerwriterqueue.h" #include "src/history_item.hpp" @@ -65,6 +64,7 @@ public: void add_stream(const std::string& name, std::shared_ptr s); void remove_stream(const std::string& name); + bool contains_stream(const std::string& name); void play(); void pause(); @@ -92,7 +92,6 @@ private: void launchStreamingThread(sf::SoundSource::Status threadStartState); void awaitStreamingThread(); - moodycamel::ReaderWriterQueue stream_change_requests{10}; void unsafe_update_streams(); void reload_sources(); diff --git a/src/editor_state.cpp b/src/editor_state.cpp index 4c6271c..af94cd4 100644 --- a/src/editor_state.cpp +++ b/src/editor_state.cpp @@ -36,6 +36,7 @@ EditorState::EditorState(const std::filesystem::path& assets_) : note_claps(std::make_shared(nullptr, nullptr, assets_)), + beat_ticks(std::make_shared(nullptr, assets_)), playfield(assets_), linear_view(assets_), applicable_timing(song.timing), @@ -54,6 +55,7 @@ EditorState::EditorState( song(song_), song_path(song_path), note_claps(std::make_shared(nullptr, nullptr, assets_)), + beat_ticks(std::make_shared(nullptr, assets_)), playfield(assets_), linear_view(assets_), applicable_timing(song.timing), @@ -118,6 +120,14 @@ void EditorState::toggle_playback() { } } +void EditorState::toggle_beat_ticks() { + if (audio.contains_stream(beat_tick_stream)) { + audio.remove_stream(beat_tick_stream); + } else { + audio.add_stream(beat_tick_stream, beat_ticks); + } +} + void EditorState::play() { audio.play(); } @@ -804,6 +814,7 @@ void EditorState::open_chart(const std::string& name) { reload_editable_range(); reload_applicable_timing(); note_claps->set_notes_and_timing(&chart.notes, &applicable_timing); + beat_ticks->set_timing(&applicable_timing); }; void EditorState::update_visible_notes() { diff --git a/src/editor_state.hpp b/src/editor_state.hpp index ecd0e05..694cc50 100644 --- a/src/editor_state.hpp +++ b/src/editor_state.hpp @@ -8,6 +8,7 @@ #include +#include "custom_sfml_audio/beat_ticks.hpp" #include "custom_sfml_audio/note_claps.hpp" #include "custom_sfml_audio/open_music.hpp" #include "custom_sfml_audio/synced_sound_streams.hpp" @@ -25,6 +26,7 @@ const std::string music_stream = "music"; const std::string note_clap_stream = "note_clap"; +const std::string beat_tick_stream = "beat_tick"; /* * The god class, holds everything there is to know about the currently open @@ -47,6 +49,7 @@ public: SyncedSoundStreams audio; std::shared_ptr note_claps; + std::shared_ptr beat_ticks; std::optional> music = {}; int get_volume() const; @@ -77,6 +80,7 @@ public: const Interval& get_editable_range(); void toggle_playback(); + void toggle_beat_ticks(); void play(); void pause(); void stop(); diff --git a/src/main.cpp b/src/main.cpp index bee2411..7864101 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -481,12 +481,14 @@ int main() { } if (editor_state->showSoundSettings) { ImGui::Begin("Sound Settings", &editor_state->showSoundSettings, ImGuiWindowFlags_AlwaysAutoResize); { - if (ImGui::TreeNode("Note Clap")) { - static auto play_chords = editor_state->note_claps->play_chords.load(); - if (ImGui::Checkbox("Play on chords", &play_chords)) { - editor_state->note_claps->play_chords.store(play_chords); - } - ImGui::TreePop(); + ImGui::Text("Note Clap"); + static auto play_chords = editor_state->note_claps->play_chords.load(); + if (ImGui::Checkbox("Play on chords", &play_chords)) { + editor_state->note_claps->play_chords.store(play_chords); + } + ImGui::Text("Beat Tick"); + if (ImGui::Button("Toggle")) { + editor_state->toggle_beat_ticks(); } } ImGui::End();