Working beat tick + simpler stream change method

This commit is contained in:
Stepland 2022-06-11 02:26:46 +02:00
parent 51c2bd8c37
commit f09f6b66b3
8 changed files with 208 additions and 46 deletions

View File

@ -0,0 +1,116 @@
#include "beat_ticks.hpp"
#include <SFML/System/Time.hpp>
#include <memory>
#include <stdexcept>
#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<std::int64_t>(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<std::int64_t>(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<std::int64_t>(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<std::int64_t>(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<std::int64_t>(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<std::int64_t>(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<std::int64_t>(start_sample)
);
} else if (slice_end_in_buffer > static_cast<std::int64_t>(samples.size())) {
tick_finished_playing_in_current_buffer = false;
slice_end_in_buffer = static_cast<std::int64_t>(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<std::int64_t>(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<std::int64_t>(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;
}

View File

@ -0,0 +1,34 @@
#pragma once
#include <set>
#include <SFML/Audio/SoundBuffer.hpp>
#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<bool> play_chords = true;
protected:
bool onGetData(Chunk& data) override;
void onSeek(sf::Time timeOffset) override;
private:
std::vector<sf::Int16> 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<std::int64_t> beat_at_sample;
const better::Timing* timing;
sf::SoundBuffer beat_tick;
};

View File

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

View File

@ -61,48 +61,46 @@ SyncedSoundStreams::~SyncedSoundStreams() {
void SyncedSoundStreams::add_stream(const std::string& name, std::shared_ptr<PreciseSoundStream> 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;

View File

@ -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<PreciseSoundStream> 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<ChangeStreamsCommand> stream_change_requests{10};
void unsafe_update_streams();
void reload_sources();

View File

@ -36,6 +36,7 @@
EditorState::EditorState(const std::filesystem::path& assets_) :
note_claps(std::make_shared<NoteClaps>(nullptr, nullptr, assets_)),
beat_ticks(std::make_shared<BeatTicks>(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<NoteClaps>(nullptr, nullptr, assets_)),
beat_ticks(std::make_shared<BeatTicks>(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() {

View File

@ -8,6 +8,7 @@
#include <SFML/Graphics.hpp>
#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<NoteClaps> note_claps;
std::shared_ptr<BeatTicks> beat_ticks;
std::optional<std::shared_ptr<OpenMusic>> music = {};
int get_volume() const;
@ -77,6 +80,7 @@ public:
const Interval<sf::Time>& get_editable_range();
void toggle_playback();
void toggle_beat_ticks();
void play();
void pause();
void stop();

View File

@ -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();