Slowly getting there but we still have some bugs

This commit is contained in:
Stepland 2022-10-07 01:47:45 +02:00
parent 68e6eaff32
commit beaea91380
11 changed files with 409 additions and 35 deletions

View File

@ -0,0 +1,90 @@
#include "chord_claps.hpp"
#include <SFML/Audio/SoundBuffer.hpp>
#include <SFML/System/Time.hpp>
#include <limits>
#include <memory>
#include <stdexcept>
#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<sf::SoundBuffer> 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> ChordClaps::with_pitch(float new_pitch) {
return std::make_shared<ChordClaps>(
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<std::int64_t>(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<std::int64_t>(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();
};

View File

@ -0,0 +1,41 @@
#pragma once
#include <map>
#include <memory>
#include <SFML/Audio/SoundBuffer.hpp>
#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<sf::SoundBuffer> note_clap_,
float pitch_
);
void set_notes_and_timing(const better::Notes* notes, const better::Timing* timing);
std::shared_ptr<ChordClaps> with_pitch(float pitch);
protected:
bool onGetData(Chunk& data) override;
void onSeek(sf::Time timeOffset) override;
private:
std::map<std::int64_t, unsigned int> notes_at_sample;
const better::Notes* notes;
const better::Timing* timing;
};

View File

@ -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',

View File

@ -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<sf::SoundBuffer> 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> NoteClaps::with_pitch(float pitch) {
std::shared_ptr<NoteClaps> NoteClaps::with_pitch(float new_pitch) {
return std::make_shared<NoteClaps>(
notes,
timing,
sample,
pitch
new_pitch,
play_chords,
play_long_note_ends
);
}
std::shared_ptr<NoteClaps> NoteClaps::with_chords(bool new_play_chords) {
return std::make_shared<NoteClaps>(
notes,
timing,
sample,
pitch,
new_play_chords,
play_long_note_ends
);
}
std::shared_ptr<NoteClaps> NoteClaps::with_long_note_ends(bool new_play_long_note_ends) {
return std::make_shared<NoteClaps>(
notes,
timing,
sample,
pitch,
play_chords,
new_play_long_note_ends
);
}
std::shared_ptr<NoteClaps> NoteClaps::with(
float pitch_,
bool play_chords_,
bool play_long_note_ends_
) {
return std::make_shared<NoteClaps>(
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<std::int64_t>(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<std::int64_t>(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,

View File

@ -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<sf::SoundBuffer> 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<NoteClaps> with_pitch(float pitch);
bool does_play_chords() const {return play_chords;};
std::shared_ptr<NoteClaps> with_chords(bool play_chords);
bool does_play_long_note_ends() const {return play_long_note_ends;};
std::shared_ptr<NoteClaps> with_long_note_ends(bool play_long_note_ends);
std::shared_ptr<NoteClaps> 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<std::int64_t> notes_at_sample;
std::map<std::int64_t, unsigned int> notes_at_sample;
bool play_chords = true;
bool play_long_note_ends = false;
const better::Notes* notes;
const better::Timing* timing;

View File

@ -1,7 +1,9 @@
#pragma once
#include <cstdint>
#include <map>
#include <memory>
#include <set>
#include <span>
#include <SFML/Audio/SoundBuffer.hpp>
@ -12,4 +14,66 @@ void copy_sample_at_points(
std::span<sf::Int16> output_buffer,
std::set<std::int64_t>& starting_points,
std::int64_t absolute_buffer_start
);
);
template<class T>
void copy_sample_at_points(
const std::shared_ptr<sf::SoundBuffer>& sample,
std::span<sf::Int16> output_buffer,
std::map<std::int64_t, T>& 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<std::int64_t>(output_buffer.size());
const auto absolute_sample_end = absolute_sample_start + static_cast<std::int64_t>(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<std::int64_t>::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;
}
}
}

View File

@ -2,6 +2,7 @@
#include <boost/math/constants/constants.hpp>
#include <cassert>
#include <initializer_list>
#include <iostream>
#include <memory>
#include <mutex>
@ -64,25 +65,30 @@ SyncedSoundStreams::~SyncedSoundStreams() {
void SyncedSoundStreams::change_streams(std::function<void()> 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<std::string, NewStream> new_streams) {
void SyncedSoundStreams::update_streams(
const std::map<std::string, NewStream>& to_add,
const std::initializer_list<std::string>& 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);
}

View File

@ -41,7 +41,7 @@ struct Buffers {
struct NewStream {
std::shared_ptr<PreciseSoundStream> stream;
bool reconstruct_on_pitch_change;
bool bypasses_openal_pitch;
};
struct InternalStream {
@ -57,10 +57,13 @@ public:
SyncedSoundStreams();
~SyncedSoundStreams();
void update_streams(std::map<std::string, NewStream> new_streams);
void update_streams(
const std::map<std::string, NewStream>& to_add,
const std::initializer_list<std::string>& 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();

View File

@ -12,6 +12,7 @@
#include <imgui.h>
#include <imgui_internal.h>
#include <imgui_stdlib.h>
#include <initializer_list>
#include <memory>
#include <nowide/fstream.hpp>
#include <sstream>
@ -36,6 +37,7 @@
EditorState::EditorState(const std::filesystem::path& assets_) :
note_claps(std::make_shared<NoteClaps>(nullptr, nullptr, assets_, 1.f)),
chord_claps(std::make_shared<ChordClaps>(nullptr, nullptr, assets_, 1.f)),
beat_ticks(std::make_shared<BeatTicks>(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<NoteClaps>(nullptr, nullptr, assets_, 1.f)),
chord_claps(std::make_shared<ChordClaps>(nullptr, nullptr, assets_, 1.f)),
beat_ticks(std::make_shared<BeatTicks>(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<std::string, NewStream> 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);
};

View File

@ -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<NoteClaps> note_claps;
std::shared_ptr<ChordClaps> chord_claps;
std::shared_ptr<BeatTicks> beat_ticks;
std::optional<std::shared_ptr<OpenMusic>> music = {};
@ -80,7 +83,14 @@ public:
const Interval<sf::Time>& 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

View File

@ -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", &note_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();
}