diff --git a/src/custom_sfml_audio/chord_claps.cpp b/src/custom_sfml_audio/chord_claps.cpp new file mode 100644 index 0000000..67d74b9 --- /dev/null +++ b/src/custom_sfml_audio/chord_claps.cpp @@ -0,0 +1,90 @@ +#include "chord_claps.hpp" + +#include +#include +#include +#include +#include + +#include "../better_note.hpp" +#include "src/custom_sfml_audio/fake_pitched_sound_stream.hpp" +#include "src/custom_sfml_audio/sampler_callback.hpp" +#include "src/special_numeric_types.hpp" + +ChordClaps::ChordClaps( + const better::Notes* notes_, + const better::Timing* timing_, + const std::filesystem::path& assets, + float pitch_ +) : + FakePitchedSoundStream(assets / "sounds" / "chord.wav", pitch_), + notes(notes_), + timing(timing_) +{} + +ChordClaps::ChordClaps( + const better::Notes* notes_, + const better::Timing* timing_, + std::shared_ptr note_clap, + float pitch +) : + FakePitchedSoundStream(note_clap, pitch), + notes(notes_), + timing(timing_) +{} + +void ChordClaps::set_notes_and_timing(const better::Notes* notes_, const better::Timing* timing_) { + notes = notes_; + timing = timing_; +} + +std::shared_ptr ChordClaps::with_pitch(float new_pitch) { + return std::make_shared( + notes, + timing, + sample, + new_pitch + ); +} + +bool ChordClaps::onGetData(sf::SoundStream::Chunk& data) { + if (timing != nullptr and notes != nullptr) { + const auto absolute_buffer_start = first_sample_of_next_buffer; + const std::int64_t absolute_buffer_end = first_sample_of_next_buffer + static_cast(output_buffer.size()); + const auto start_time = samples_to_music_time(absolute_buffer_start); + const auto end_time = samples_to_music_time(absolute_buffer_end); + const auto start_beat = timing->beats_at(start_time); + const auto end_beat = timing->beats_at(end_time); + const auto count_clap_at = [&](const Fraction beat){ + const auto time = timing->time_at(beat); + const auto sample = static_cast(music_time_to_samples(time)); + // we don't want claps that *start* at the end sample since + // absolute_buffer_end is an *exculsive* end + if (sample < absolute_buffer_end) { + notes_at_sample[sample] += 1; + } + }; + + notes->in(start_beat, end_beat, [&](const better::Notes::const_iterator& it){ + count_clap_at(it->second.get_time()); + }); + std::erase_if(notes_at_sample, [](const auto& it){return it.second <= 1;}); + copy_sample_at_points( + sample, + output_buffer, + notes_at_sample, + absolute_buffer_start + ); + } + + data.samples = output_buffer.data(); + data.sampleCount = output_buffer.size(); + first_sample_of_next_buffer += output_buffer.size(); + + return true; +}; + +void ChordClaps::onSeek(sf::Time timeOffset) { + first_sample_of_next_buffer = music_time_to_samples(timeOffset); + notes_at_sample.clear(); +}; diff --git a/src/custom_sfml_audio/chord_claps.hpp b/src/custom_sfml_audio/chord_claps.hpp new file mode 100644 index 0000000..0a2d9b3 --- /dev/null +++ b/src/custom_sfml_audio/chord_claps.hpp @@ -0,0 +1,41 @@ +#pragma once + +#include +#include + +#include + +#include "../better_notes.hpp" +#include "../better_timing.hpp" +#include "fake_pitched_sound_stream.hpp" + +class ChordClaps: public FakePitchedSoundStream { +public: + ChordClaps( + const better::Notes* notes_, + const better::Timing* timing_, + const std::filesystem::path& assets, + float pitch_ + ); + + ChordClaps( + const better::Notes* notes_, + const better::Timing* timing_, + std::shared_ptr note_clap_, + float pitch_ + ); + + void set_notes_and_timing(const better::Notes* notes, const better::Timing* timing); + + std::shared_ptr with_pitch(float pitch); + +protected: + bool onGetData(Chunk& data) override; + void onSeek(sf::Time timeOffset) override; + +private: + std::map notes_at_sample; + + const better::Notes* notes; + const better::Timing* timing; +}; \ No newline at end of file diff --git a/src/custom_sfml_audio/meson.build b/src/custom_sfml_audio/meson.build index 3b5b1fa..d88786d 100644 --- a/src/custom_sfml_audio/meson.build +++ b/src/custom_sfml_audio/meson.build @@ -3,6 +3,7 @@ sources += files([ 'al_resource.cpp', 'audio_device.cpp', 'beat_ticks.cpp', + 'chord_claps.cpp', 'fake_pitched_sound_stream.cpp', 'note_claps.cpp', 'open_music.cpp', diff --git a/src/custom_sfml_audio/note_claps.cpp b/src/custom_sfml_audio/note_claps.cpp index 3bbb73b..badb5a1 100644 --- a/src/custom_sfml_audio/note_claps.cpp +++ b/src/custom_sfml_audio/note_claps.cpp @@ -9,14 +9,19 @@ #include "../better_note.hpp" #include "src/custom_sfml_audio/fake_pitched_sound_stream.hpp" #include "src/custom_sfml_audio/sampler_callback.hpp" +#include "src/special_numeric_types.hpp" NoteClaps::NoteClaps( const better::Notes* notes_, const better::Timing* timing_, const std::filesystem::path& assets, - float pitch_ + float pitch_, + bool play_chords_, + bool play_long_note_ends_ ) : FakePitchedSoundStream(assets / "sounds" / "note.wav", pitch_), + play_chords(play_chords_), + play_long_note_ends(play_long_note_ends_), notes(notes_), timing(timing_) {} @@ -25,9 +30,13 @@ NoteClaps::NoteClaps( const better::Notes* notes_, const better::Timing* timing_, std::shared_ptr note_clap, - float pitch + float pitch, + bool play_chords_, + bool play_long_note_ends_ ) : FakePitchedSoundStream(note_clap, pitch), + play_chords(play_chords_), + play_long_note_ends(play_long_note_ends_), notes(notes_), timing(timing_) {} @@ -37,12 +46,51 @@ void NoteClaps::set_notes_and_timing(const better::Notes* notes_, const better:: timing = timing_; } -std::shared_ptr NoteClaps::with_pitch(float pitch) { +std::shared_ptr NoteClaps::with_pitch(float new_pitch) { return std::make_shared( notes, timing, sample, - pitch + new_pitch, + play_chords, + play_long_note_ends + ); +} + +std::shared_ptr NoteClaps::with_chords(bool new_play_chords) { + return std::make_shared( + notes, + timing, + sample, + pitch, + new_play_chords, + play_long_note_ends + ); +} + +std::shared_ptr NoteClaps::with_long_note_ends(bool new_play_long_note_ends) { + return std::make_shared( + notes, + timing, + sample, + pitch, + play_chords, + new_play_long_note_ends + ); +} + +std::shared_ptr NoteClaps::with( + float pitch_, + bool play_chords_, + bool play_long_note_ends_ +) { + return std::make_shared( + notes, + timing, + sample, + pitch_, + play_chords_, + play_long_note_ends_ ); } @@ -54,22 +102,34 @@ bool NoteClaps::onGetData(sf::SoundStream::Chunk& data) { const auto end_time = samples_to_music_time(absolute_buffer_end); const auto start_beat = timing->beats_at(start_time); const auto end_beat = timing->beats_at(end_time); + const auto count_clap_at = [&](const Fraction beat){ + const auto time = timing->time_at(beat); + const auto sample = static_cast(music_time_to_samples(time)); + // we don't want claps that *start* at the end sample since + // absolute_buffer_end is an *exculsive* end + if (sample < absolute_buffer_end) { + notes_at_sample[sample] += 1; + } + }; + + const auto add_claps_of_note = VariantVisitor { + [&](const better::TapNote& t) { + count_clap_at(t.get_time()); + }, + [&](const better::LongNote& l) { + count_clap_at(l.get_time()); + if (play_long_note_ends) { + count_clap_at(l.get_end()); + } + }, + }; notes->in(start_beat, end_beat, [&](const better::Notes::const_iterator& it){ - const auto beat = it->second.get_time(); - // ignore long notes that started before the current buffer - if (beat < start_beat) { - return; - } - const auto time = timing->time_at(beat); - const auto sample = static_cast(music_time_to_samples(time)); - // interval_tree::in is inclusive of the upper bound but here we - // don't want claps that *start* at the end sample since - // it's an *exculsive* end - if (sample < absolute_buffer_end) { - notes_at_sample.insert(sample); - } + it->second.visit(add_claps_of_note); }); + if (not play_chords) { + std::erase_if(notes_at_sample, [](const auto& it){return it.second > 1;}); + } copy_sample_at_points( sample, output_buffer, diff --git a/src/custom_sfml_audio/note_claps.hpp b/src/custom_sfml_audio/note_claps.hpp index 52b619a..5a18d09 100644 --- a/src/custom_sfml_audio/note_claps.hpp +++ b/src/custom_sfml_audio/note_claps.hpp @@ -17,26 +17,43 @@ public: const better::Notes* notes_, const better::Timing* timing_, const std::filesystem::path& assets, - float pitch_ + float pitch_, + bool play_chords = true, + bool play_long_note_ends = false ); NoteClaps( const better::Notes* notes_, const better::Timing* timing_, std::shared_ptr note_clap_, - float time_factor_ + float pitch_, + bool play_chords = true, + bool play_long_note_ends = false ); void set_notes_and_timing(const better::Notes* notes, const better::Timing* timing); std::shared_ptr with_pitch(float pitch); + bool does_play_chords() const {return play_chords;}; + std::shared_ptr with_chords(bool play_chords); + + bool does_play_long_note_ends() const {return play_long_note_ends;}; + std::shared_ptr with_long_note_ends(bool play_long_note_ends); + + std::shared_ptr with( + float pitch, + bool play_chords, + bool play_long_note_ends + ); protected: bool onGetData(Chunk& data) override; void onSeek(sf::Time timeOffset) override; private: - std::set notes_at_sample; + std::map notes_at_sample; + bool play_chords = true; + bool play_long_note_ends = false; const better::Notes* notes; const better::Timing* timing; diff --git a/src/custom_sfml_audio/sampler_callback.hpp b/src/custom_sfml_audio/sampler_callback.hpp index ea63064..22ff74a 100644 --- a/src/custom_sfml_audio/sampler_callback.hpp +++ b/src/custom_sfml_audio/sampler_callback.hpp @@ -1,7 +1,9 @@ #pragma once #include +#include #include +#include #include #include @@ -12,4 +14,66 @@ void copy_sample_at_points( std::span output_buffer, std::set& starting_points, std::int64_t absolute_buffer_start -); \ No newline at end of file +); + +template +void copy_sample_at_points( + const std::shared_ptr& sample, + std::span output_buffer, + std::map& starting_points, + std::int64_t absolute_buffer_start +) { + std::ranges::fill(output_buffer, 0); + for (auto it = starting_points.begin(); it != starting_points.end();) { + const auto absolute_sample_start = it->first; + const auto absolute_buffer_end = absolute_buffer_start + static_cast(output_buffer.size()); + const auto absolute_sample_end = absolute_sample_start + static_cast(sample->getSampleCount()); + const auto absolute_sample_deoverlapped_end = std::min( + absolute_sample_end, + [&](const auto& it){ + const auto next = std::next(it); + if (next != starting_points.end()) { + return next->first; + } else { + return std::numeric_limits::max(); + } + }(it) + ); + const auto absolute_sample_slice_start = std::max( + absolute_sample_start, + absolute_buffer_start + ); + const auto absolute_sample_slice_end = std::min( + absolute_sample_deoverlapped_end, + absolute_buffer_end + ); + const auto slice_size = absolute_sample_slice_end - absolute_sample_slice_start; + const auto slice_start_relative_to_sample_start = absolute_sample_slice_start - absolute_sample_start; + const auto slice_start_relative_to_buffer_start = absolute_sample_slice_start - absolute_buffer_start; + + // Exit early in all the possible error cases I could think of + if ( + absolute_sample_deoverlapped_end <= absolute_buffer_start + or absolute_sample_start >= absolute_buffer_end + or slice_size <= 0 + ) { + it = starting_points.erase(it); + continue; + } + + const auto input_start = sample->getSamples() + slice_start_relative_to_sample_start; + const auto input_end = input_start + slice_size; + const auto output_start = output_buffer.begin() + slice_start_relative_to_buffer_start; + std::copy( + input_start, + input_end, + output_start + ); + // has this sample been fully played in this buffer ? + if (absolute_sample_deoverlapped_end <= absolute_buffer_end) { + it = starting_points.erase(it); + } else { + ++it; + } + } +} \ No newline at end of file diff --git a/src/custom_sfml_audio/synced_sound_streams.cpp b/src/custom_sfml_audio/synced_sound_streams.cpp index bc1c9f4..ee33406 100644 --- a/src/custom_sfml_audio/synced_sound_streams.cpp +++ b/src/custom_sfml_audio/synced_sound_streams.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -64,25 +65,30 @@ SyncedSoundStreams::~SyncedSoundStreams() { void SyncedSoundStreams::change_streams(std::function callback) { const auto oldStatus = getStatus(); + pause(); + setPitch(pitch); const auto position = getPlayingOffset(); stop(); callback(); reload_sources(); - setPitch(pitch); setPlayingOffset(position); if (oldStatus == sf::SoundSource::Playing) { play(); } } -void SyncedSoundStreams::update_streams(std::map new_streams) { +void SyncedSoundStreams::update_streams( + const std::map& to_add, + const std::initializer_list& to_remove +) { change_streams([&](){ - for (const auto& [name, new_stream] : new_streams) { - if (contains_stream(name)) { - remove_stream_internal(name); - } + for (const auto& name : to_remove) { + remove_stream_internal(name); + } + for (const auto& [name, new_stream] : to_add) { + remove_stream_internal(name); add_stream_internal(name, new_stream); } }); @@ -96,7 +102,7 @@ void SyncedSoundStreams::add_stream(const std::string& name, NewStream s) { } void SyncedSoundStreams::add_stream_internal(const std::string& name, NewStream s) { - InternalStream internal_stream{s.stream, {}, s.reconstruct_on_pitch_change}; + InternalStream internal_stream{s.stream, {}, s.bypasses_openal_pitch}; internal_stream.buffers.m_channelCount = s.stream->getChannelCount(); internal_stream.buffers.m_sampleRate = s.stream->getSampleRate(); internal_stream.buffers.m_format = AudioDevice::getFormatFromChannelCount(s.stream->getChannelCount()); @@ -116,7 +122,7 @@ void SyncedSoundStreams::remove_stream_internal(const std::string& name) { streams.erase(name); } -bool SyncedSoundStreams::contains_stream(const std::string& name) { +bool SyncedSoundStreams::contains_stream(const std::string& name) const { return streams.contains(name); } diff --git a/src/custom_sfml_audio/synced_sound_streams.hpp b/src/custom_sfml_audio/synced_sound_streams.hpp index 604e14d..01b3ae6 100644 --- a/src/custom_sfml_audio/synced_sound_streams.hpp +++ b/src/custom_sfml_audio/synced_sound_streams.hpp @@ -41,7 +41,7 @@ struct Buffers { struct NewStream { std::shared_ptr stream; - bool reconstruct_on_pitch_change; + bool bypasses_openal_pitch; }; struct InternalStream { @@ -57,10 +57,13 @@ public: SyncedSoundStreams(); ~SyncedSoundStreams(); - void update_streams(std::map new_streams); + void update_streams( + const std::map& to_add, + const std::initializer_list& to_remove = {} + ); void add_stream(const std::string& name, NewStream s); void remove_stream(const std::string& name); - bool contains_stream(const std::string& name); + bool contains_stream(const std::string& name) const; void play(); void pause(); diff --git a/src/editor_state.cpp b/src/editor_state.cpp index 9035d12..3c65f9f 100644 --- a/src/editor_state.cpp +++ b/src/editor_state.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -36,6 +37,7 @@ EditorState::EditorState(const std::filesystem::path& assets_) : note_claps(std::make_shared(nullptr, nullptr, assets_, 1.f)), + chord_claps(std::make_shared(nullptr, nullptr, assets_, 1.f)), beat_ticks(std::make_shared(nullptr, assets_, 1.f)), playfield(assets_), linear_view(assets_), @@ -55,6 +57,7 @@ EditorState::EditorState( song(song_), song_path(song_path), note_claps(std::make_shared(nullptr, nullptr, assets_, 1.f)), + chord_claps(std::make_shared(nullptr, nullptr, assets_, 1.f)), beat_ticks(std::make_shared(nullptr, assets_, 1.f)), playfield(assets_), linear_view(assets_), @@ -120,6 +123,60 @@ void EditorState::toggle_playback() { } } +void EditorState::toggle_note_claps() { + if ( + audio.contains_stream(note_clap_stream) + or audio.contains_stream(chord_clap_stream) + ) { + audio.update_streams({}, {note_clap_stream, chord_clap_stream}); + } else { + note_claps = note_claps->with( + get_pitch(), + not distinct_chord_clap, + clap_on_long_note_ends + ); + std::map streams = {{note_clap_stream, {note_claps, true}}}; + if (distinct_chord_clap) { + chord_claps = chord_claps->with_pitch(get_pitch()); + streams[chord_clap_stream] = {chord_claps, true}; + } + audio.update_streams(streams); + } +} + +void EditorState::toggle_clap_on_long_note_ends() { + clap_on_long_note_ends = not clap_on_long_note_ends; + note_claps = note_claps->with( + get_pitch(), + not distinct_chord_clap, + clap_on_long_note_ends + ); + audio.update_streams({{note_clap_stream, {note_claps, true}}}); +} + +void EditorState::toggle_distinct_chord_claps() { + distinct_chord_clap = not distinct_chord_clap; + note_claps = note_claps->with( + get_pitch(), + not distinct_chord_clap, + clap_on_long_note_ends + ); + if (distinct_chord_clap) { + chord_claps = chord_claps->with_pitch(get_pitch()); + audio.update_streams( + { + {note_clap_stream, {note_claps, true}}, + {chord_clap_stream, {chord_claps, true}} + } + ); + } else { + audio.update_streams( + {{note_clap_stream, {note_claps, true}}}, + {chord_clap_stream} + ); + } +} + void EditorState::toggle_beat_ticks() { if (audio.contains_stream(beat_tick_stream)) { audio.remove_stream(beat_tick_stream); @@ -155,6 +212,10 @@ void EditorState::set_pitch(float pitch) { beat_ticks = beat_ticks->with_pitch(pitch); update[beat_tick_stream] = {beat_ticks, true}; } + if (audio.contains_stream(chord_clap_stream)) { + chord_claps = chord_claps->with_pitch(pitch); + update[chord_clap_stream] = {chord_claps, true}; + } // setPitch has to be called before update_streams to avoid problems in // the internal call to setPlaybackPosition audio.setPitch(pitch); @@ -829,6 +890,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); + chord_claps->set_notes_and_timing(&chart.notes, &applicable_timing); beat_ticks->set_timing(&applicable_timing); }; diff --git a/src/editor_state.hpp b/src/editor_state.hpp index ad5253d..17d2cb9 100644 --- a/src/editor_state.hpp +++ b/src/editor_state.hpp @@ -9,6 +9,7 @@ #include "custom_sfml_audio/beat_ticks.hpp" +#include "custom_sfml_audio/chord_claps.hpp" #include "custom_sfml_audio/note_claps.hpp" #include "custom_sfml_audio/open_music.hpp" #include "custom_sfml_audio/synced_sound_streams.hpp" @@ -25,7 +26,8 @@ const std::string music_stream = "music"; -const std::string note_clap_stream = "aaa_note_clap"; +const std::string note_clap_stream = "note_clap"; +const std::string chord_clap_stream = "chord_clap"; const std::string beat_tick_stream = "beat_tick"; /* @@ -49,6 +51,7 @@ public: SyncedSoundStreams audio; std::shared_ptr note_claps; + std::shared_ptr chord_claps; std::shared_ptr beat_ticks; std::optional> music = {}; @@ -80,7 +83,14 @@ public: const Interval& get_editable_range(); void toggle_playback(); + void toggle_note_claps(); + bool note_claps_are_on() const {return audio.contains_stream(note_clap_stream);}; + void toggle_clap_on_long_note_ends(); + bool get_clap_on_long_note_ends() const {return clap_on_long_note_ends;}; + void toggle_distinct_chord_claps(); + bool get_distinct_chord_claps() const {return distinct_chord_clap;}; void toggle_beat_ticks(); + bool beat_ticks_are_on() const {return audio.contains_stream(beat_tick_stream);}; void play(); void pause(); void stop(); @@ -172,6 +182,9 @@ private: int volume = 10; // 0 -> 10 int speed = 10; // 1 -> 20 + bool clap_on_long_note_ends = false; + bool distinct_chord_clap = false; + /* sf::Time bounds (in the audio file "coordinates") which are accessible (and maybe editable) from the editor, can extend before and after diff --git a/src/main.cpp b/src/main.cpp index 7ecffc2..302a01f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -481,10 +481,27 @@ int main() { } if (editor_state->showSoundSettings) { ImGui::Begin("Sound Settings", &editor_state->showSoundSettings, ImGuiWindowFlags_AlwaysAutoResize); { - ImGui::Text("Beat Tick"); - if (ImGui::Button("Toggle")) { + bool beat_tick = editor_state->beat_ticks_are_on(); + if (ImGui::Checkbox("beat tick", &beat_tick)) { editor_state->toggle_beat_ticks(); } + bool note_clap = editor_state->note_claps_are_on(); + if (ImGui::Checkbox("note clap", ¬e_clap)) { + editor_state->toggle_note_claps(); + } + ImGui::BeginDisabled(not note_clap); { + ImGui::Indent(); + bool long_end = editor_state->get_clap_on_long_note_ends(); + if (ImGui::Checkbox("clap on long note ends", &long_end)) { + editor_state->toggle_clap_on_long_note_ends(); + } + bool chord_clap = editor_state->get_distinct_chord_claps(); + if (ImGui::Checkbox("distinct chord clap", &chord_clap)) { + editor_state->toggle_distinct_chord_claps(); + } + ImGui::Unindent(); + + } ImGui::EndDisabled(); } ImGui::End(); }