mirror of
https://gitlab.com/square-game-liberation-front/F.E.I.S.git
synced 2025-02-28 23:41:33 +01:00
Working beat tick + simpler stream change method
This commit is contained in:
parent
51c2bd8c37
commit
f09f6b66b3
116
src/custom_sfml_audio/beat_ticks.cpp
Normal file
116
src/custom_sfml_audio/beat_ticks.cpp
Normal 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;
|
||||||
|
}
|
34
src/custom_sfml_audio/beat_ticks.hpp
Normal file
34
src/custom_sfml_audio/beat_ticks.hpp
Normal 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;
|
||||||
|
};
|
@ -2,6 +2,7 @@ sources += files([
|
|||||||
'al_check.cpp',
|
'al_check.cpp',
|
||||||
'al_resource.cpp',
|
'al_resource.cpp',
|
||||||
'audio_device.cpp',
|
'audio_device.cpp',
|
||||||
|
'beat_ticks.cpp',
|
||||||
'note_claps.cpp',
|
'note_claps.cpp',
|
||||||
'open_music.cpp',
|
'open_music.cpp',
|
||||||
'open_sound_stream.cpp',
|
'open_sound_stream.cpp',
|
||||||
|
@ -61,48 +61,46 @@ SyncedSoundStreams::~SyncedSoundStreams() {
|
|||||||
|
|
||||||
|
|
||||||
void SyncedSoundStreams::add_stream(const std::string& name, std::shared_ptr<PreciseSoundStream> s) {
|
void SyncedSoundStreams::add_stream(const std::string& name, std::shared_ptr<PreciseSoundStream> s) {
|
||||||
stream_change_requests.try_enqueue(AddStream{name, s});
|
const auto oldStatus = getStatus();
|
||||||
if (not m_isStreaming) {
|
const auto position = getPrecisePlayingOffset();
|
||||||
// if we are not currently playing audio there should be no problem
|
stop();
|
||||||
// changing the streams right now
|
{
|
||||||
unsafe_update_streams();
|
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) {
|
void SyncedSoundStreams::remove_stream(const std::string& name) {
|
||||||
stream_change_requests.try_enqueue(RemoveStream{name});
|
const auto oldStatus = getStatus();
|
||||||
if (not m_isStreaming) {
|
const auto position = getPrecisePlayingOffset();
|
||||||
// if we are not currently playing audio there should be no problem
|
stop();
|
||||||
// changing the streams right now
|
{
|
||||||
unsafe_update_streams();
|
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() {
|
bool SyncedSoundStreams::contains_stream(const std::string& name) {
|
||||||
ChangeStreamsCommand c;
|
return streams.contains(name);
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void SyncedSoundStreams::play() {
|
void SyncedSoundStreams::play() {
|
||||||
@ -398,9 +396,6 @@ void SyncedSoundStreams::streamData() {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process stream change requests
|
|
||||||
unsafe_update_streams();
|
|
||||||
|
|
||||||
// Leave some time for the other threads if the stream is still playing
|
// Leave some time for the other threads if the stream is still playing
|
||||||
if (std::any_of(streams.begin(), streams.end(), [](auto& s){
|
if (std::any_of(streams.begin(), streams.end(), [](auto& s){
|
||||||
return s.second.stream->getStatus() != sf::SoundSource::Stopped;
|
return s.second.stream->getStatus() != sf::SoundSource::Stopped;
|
||||||
|
@ -14,7 +14,6 @@
|
|||||||
|
|
||||||
#include "al_resource.hpp"
|
#include "al_resource.hpp"
|
||||||
#include "precise_sound_stream.hpp"
|
#include "precise_sound_stream.hpp"
|
||||||
#include "readerwriterqueue.h"
|
|
||||||
#include "src/history_item.hpp"
|
#include "src/history_item.hpp"
|
||||||
|
|
||||||
|
|
||||||
@ -65,6 +64,7 @@ public:
|
|||||||
|
|
||||||
void add_stream(const std::string& name, std::shared_ptr<PreciseSoundStream> s);
|
void add_stream(const std::string& name, std::shared_ptr<PreciseSoundStream> s);
|
||||||
void remove_stream(const std::string& name);
|
void remove_stream(const std::string& name);
|
||||||
|
bool contains_stream(const std::string& name);
|
||||||
|
|
||||||
void play();
|
void play();
|
||||||
void pause();
|
void pause();
|
||||||
@ -92,7 +92,6 @@ private:
|
|||||||
void launchStreamingThread(sf::SoundSource::Status threadStartState);
|
void launchStreamingThread(sf::SoundSource::Status threadStartState);
|
||||||
void awaitStreamingThread();
|
void awaitStreamingThread();
|
||||||
|
|
||||||
moodycamel::ReaderWriterQueue<ChangeStreamsCommand> stream_change_requests{10};
|
|
||||||
void unsafe_update_streams();
|
void unsafe_update_streams();
|
||||||
void reload_sources();
|
void reload_sources();
|
||||||
|
|
||||||
|
@ -36,6 +36,7 @@
|
|||||||
|
|
||||||
EditorState::EditorState(const std::filesystem::path& assets_) :
|
EditorState::EditorState(const std::filesystem::path& assets_) :
|
||||||
note_claps(std::make_shared<NoteClaps>(nullptr, nullptr, assets_)),
|
note_claps(std::make_shared<NoteClaps>(nullptr, nullptr, assets_)),
|
||||||
|
beat_ticks(std::make_shared<BeatTicks>(nullptr, assets_)),
|
||||||
playfield(assets_),
|
playfield(assets_),
|
||||||
linear_view(assets_),
|
linear_view(assets_),
|
||||||
applicable_timing(song.timing),
|
applicable_timing(song.timing),
|
||||||
@ -54,6 +55,7 @@ EditorState::EditorState(
|
|||||||
song(song_),
|
song(song_),
|
||||||
song_path(song_path),
|
song_path(song_path),
|
||||||
note_claps(std::make_shared<NoteClaps>(nullptr, nullptr, assets_)),
|
note_claps(std::make_shared<NoteClaps>(nullptr, nullptr, assets_)),
|
||||||
|
beat_ticks(std::make_shared<BeatTicks>(nullptr, assets_)),
|
||||||
playfield(assets_),
|
playfield(assets_),
|
||||||
linear_view(assets_),
|
linear_view(assets_),
|
||||||
applicable_timing(song.timing),
|
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() {
|
void EditorState::play() {
|
||||||
audio.play();
|
audio.play();
|
||||||
}
|
}
|
||||||
@ -804,6 +814,7 @@ void EditorState::open_chart(const std::string& name) {
|
|||||||
reload_editable_range();
|
reload_editable_range();
|
||||||
reload_applicable_timing();
|
reload_applicable_timing();
|
||||||
note_claps->set_notes_and_timing(&chart.notes, &applicable_timing);
|
note_claps->set_notes_and_timing(&chart.notes, &applicable_timing);
|
||||||
|
beat_ticks->set_timing(&applicable_timing);
|
||||||
};
|
};
|
||||||
|
|
||||||
void EditorState::update_visible_notes() {
|
void EditorState::update_visible_notes() {
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
#include <SFML/Graphics.hpp>
|
#include <SFML/Graphics.hpp>
|
||||||
|
|
||||||
|
|
||||||
|
#include "custom_sfml_audio/beat_ticks.hpp"
|
||||||
#include "custom_sfml_audio/note_claps.hpp"
|
#include "custom_sfml_audio/note_claps.hpp"
|
||||||
#include "custom_sfml_audio/open_music.hpp"
|
#include "custom_sfml_audio/open_music.hpp"
|
||||||
#include "custom_sfml_audio/synced_sound_streams.hpp"
|
#include "custom_sfml_audio/synced_sound_streams.hpp"
|
||||||
@ -25,6 +26,7 @@
|
|||||||
|
|
||||||
const std::string music_stream = "music";
|
const std::string music_stream = "music";
|
||||||
const std::string note_clap_stream = "note_clap";
|
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
|
* The god class, holds everything there is to know about the currently open
|
||||||
@ -47,6 +49,7 @@ public:
|
|||||||
|
|
||||||
SyncedSoundStreams audio;
|
SyncedSoundStreams audio;
|
||||||
std::shared_ptr<NoteClaps> note_claps;
|
std::shared_ptr<NoteClaps> note_claps;
|
||||||
|
std::shared_ptr<BeatTicks> beat_ticks;
|
||||||
std::optional<std::shared_ptr<OpenMusic>> music = {};
|
std::optional<std::shared_ptr<OpenMusic>> music = {};
|
||||||
|
|
||||||
int get_volume() const;
|
int get_volume() const;
|
||||||
@ -77,6 +80,7 @@ public:
|
|||||||
const Interval<sf::Time>& get_editable_range();
|
const Interval<sf::Time>& get_editable_range();
|
||||||
|
|
||||||
void toggle_playback();
|
void toggle_playback();
|
||||||
|
void toggle_beat_ticks();
|
||||||
void play();
|
void play();
|
||||||
void pause();
|
void pause();
|
||||||
void stop();
|
void stop();
|
||||||
|
@ -481,12 +481,14 @@ int main() {
|
|||||||
}
|
}
|
||||||
if (editor_state->showSoundSettings) {
|
if (editor_state->showSoundSettings) {
|
||||||
ImGui::Begin("Sound Settings", &editor_state->showSoundSettings, ImGuiWindowFlags_AlwaysAutoResize); {
|
ImGui::Begin("Sound Settings", &editor_state->showSoundSettings, ImGuiWindowFlags_AlwaysAutoResize); {
|
||||||
if (ImGui::TreeNode("Note Clap")) {
|
ImGui::Text("Note Clap");
|
||||||
static auto play_chords = editor_state->note_claps->play_chords.load();
|
static auto play_chords = editor_state->note_claps->play_chords.load();
|
||||||
if (ImGui::Checkbox("Play on chords", &play_chords)) {
|
if (ImGui::Checkbox("Play on chords", &play_chords)) {
|
||||||
editor_state->note_claps->play_chords.store(play_chords);
|
editor_state->note_claps->play_chords.store(play_chords);
|
||||||
}
|
}
|
||||||
ImGui::TreePop();
|
ImGui::Text("Beat Tick");
|
||||||
|
if (ImGui::Button("Toggle")) {
|
||||||
|
editor_state->toggle_beat_ticks();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ImGui::End();
|
ImGui::End();
|
||||||
|
Loading…
x
Reference in New Issue
Block a user