Refactor claps and ticks a bit

This commit is contained in:
Stepland 2022-10-04 22:32:11 +02:00
parent ce5f162b25
commit 54aef06e04
9 changed files with 234 additions and 209 deletions

View File

@ -5,35 +5,25 @@
#include <stdexcept>
#include "../better_note.hpp"
#include "sampler_callback.hpp"
BeatTicks::BeatTicks(
const better::Timing* timing_,
const std::filesystem::path& assets,
float pitch_
float pitch
) :
pitch(pitch_),
timing(timing_),
beat_tick(std::make_shared<sf::SoundBuffer>())
{
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::seconds(1)), 0);
}
FakePitchedSoundStream(assets / "sounds" / "beat.wav", pitch),
timing(timing_)
{}
BeatTicks::BeatTicks(
const better::Timing* timing_,
std::shared_ptr<sf::SoundBuffer> beat_tick_,
float pitch_
std::shared_ptr<sf::SoundBuffer> beat_tick,
float pitch
) :
pitch(pitch_),
timing(timing_),
beat_tick(beat_tick_)
{
sf::SoundStream::initialize(beat_tick_->getChannelCount(), beat_tick_->getSampleRate());
samples.resize(timeToSamples(sf::seconds(1)), 0);
}
FakePitchedSoundStream(beat_tick, pitch),
timing(timing_)
{}
void BeatTicks::set_timing(const better::Timing* timing_) {
timing = timing_;
@ -42,18 +32,17 @@ void BeatTicks::set_timing(const better::Timing* timing_) {
std::shared_ptr<BeatTicks> BeatTicks::with_pitch(float pitch) {
return std::make_shared<BeatTicks>(
timing,
beat_tick,
sample,
pitch
);
}
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 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);
@ -63,77 +52,26 @@ bool BeatTicks::onGetData(sf::SoundStream::Chunk& data) {
}
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));
const auto sample = static_cast<std::int64_t>(music_time_to_samples(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;
}
}
}
copy_sample_at_points(
sample,
output_buffer,
beat_at_sample,
absolute_buffer_start
);
}
data.samples = samples.data();
data.sampleCount = samples.size();
current_sample += samples.size();
data.samples = output_buffer.data();
data.sampleCount = output_buffer.size();
first_sample_of_next_buffer += output_buffer.size();
return true;
};
void BeatTicks::onSeek(sf::Time timeOffset) {
current_sample = timeToSamples(timeOffset);
first_sample_of_next_buffer = music_time_to_samples(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 / pitch).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 * pitch;
}
};

View File

@ -6,9 +6,9 @@
#include <SFML/Audio/SoundBuffer.hpp>
#include "../better_timing.hpp"
#include "precise_sound_stream.hpp"
#include "fake_pitched_sound_stream.hpp"
class BeatTicks: public PreciseSoundStream {
class BeatTicks: public FakePitchedSoundStream {
public:
BeatTicks(
const better::Timing* timing_,
@ -31,14 +31,7 @@ protected:
void onSeek(sf::Time timeOffset) override;
private:
float pitch = 1.f;
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;
std::shared_ptr<sf::SoundBuffer> beat_tick;
};

View File

@ -0,0 +1,60 @@
#include "fake_pitched_sound_stream.hpp"
#include <memory>
#include <fmt/core.h>
#include <SFML/Audio/SoundBuffer.hpp>
FakePitchedSoundStream::FakePitchedSoundStream(
const std::filesystem::path& path_to_sample,
float pitch_
) :
pitch(pitch_),
sample(std::make_shared<sf::SoundBuffer>())
{
if (not sample->loadFromFile(path_to_sample)) {
throw std::runtime_error(fmt::format("Could not load audio sample : {}", path_to_sample.string()));
}
finish_initializing_the_sample();
}
FakePitchedSoundStream::FakePitchedSoundStream(
std::shared_ptr<sf::SoundBuffer> sample_,
float pitch_
) :
pitch(pitch_),
sample(sample_)
{
finish_initializing_the_sample();
}
void FakePitchedSoundStream::finish_initializing_the_sample() {
sf::SoundStream::initialize(sample->getChannelCount(), sample->getSampleRate());
output_buffer.resize(openAL_time_to_samples(sf::seconds(1)), 0);
}
std::int64_t FakePitchedSoundStream::openAL_time_to_samples(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()) * sample->getSampleRate() * sample->getChannelCount()) + 500000) / 1000000;
}
sf::Time FakePitchedSoundStream::samples_to_openAL_time(std::int64_t samples) const {
sf::Time position = sf::Time::Zero;
// Make sure we don't divide by 0
if (sample->getSampleRate() != 0 && sample->getChannelCount() != 0)
position = sf::microseconds((samples * 1000000) / (sample->getChannelCount() * sample->getSampleRate()));
return position;
}
std::int64_t FakePitchedSoundStream::music_time_to_samples(sf::Time position) const {
return openAL_time_to_samples(position / pitch);
}
sf::Time FakePitchedSoundStream::samples_to_music_time(std::int64_t samples) const {
return samples_to_openAL_time(samples) * pitch;
}

View File

@ -0,0 +1,38 @@
#pragma once
#include <SFML/Audio/SoundBuffer.hpp>
#include "precise_sound_stream.hpp"
/*
SoundStream that doesn't pitch-shift but differenciates between the current
offset in the music file played alongside (the music time) and the current
offset it reports to openAL (the OpenAL time)
This allows note claps and beat ticks not to get pitch-shifted when the music
is.
*/
class FakePitchedSoundStream : public PreciseSoundStream {
public:
FakePitchedSoundStream(
const std::filesystem::path& path_to_sample,
float pitch_
);
FakePitchedSoundStream(
std::shared_ptr<sf::SoundBuffer> sample_,
float pitch_
);
protected:
void finish_initializing_the_sample();
float pitch = 1.f;
std::shared_ptr<sf::SoundBuffer> sample;
std::vector<sf::Int16> output_buffer;
std::int64_t first_sample_of_next_buffer = 0;
std::int64_t openAL_time_to_samples(sf::Time position) const;
sf::Time samples_to_openAL_time(std::int64_t samples) const;
std::int64_t music_time_to_samples(sf::Time position) const;
sf::Time samples_to_music_time(std::int64_t samples) const;
};

View File

@ -3,9 +3,11 @@ sources += files([
'al_resource.cpp',
'audio_device.cpp',
'beat_ticks.cpp',
'fake_pitched_sound_stream.cpp',
'note_claps.cpp',
'open_music.cpp',
'open_sound_stream.cpp',
'precise_sound_stream.cpp',
'sampler_callback.cpp',
'synced_sound_streams.cpp'
])

View File

@ -7,6 +7,8 @@
#include <stdexcept>
#include "../better_note.hpp"
#include "src/custom_sfml_audio/fake_pitched_sound_stream.hpp"
#include "src/custom_sfml_audio/sampler_callback.hpp"
NoteClaps::NoteClaps(
const better::Notes* notes_,
@ -14,32 +16,21 @@ NoteClaps::NoteClaps(
const std::filesystem::path& assets,
float pitch_
) :
pitch(pitch_),
FakePitchedSoundStream(assets / "sounds" / "note.wav", pitch_),
notes(notes_),
timing(timing_),
note_clap(std::make_shared<sf::SoundBuffer>())
{
if (not note_clap->loadFromFile(assets / "sounds" / "note.wav")) {
throw std::runtime_error("Could not load note clap audio file");
}
sf::SoundStream::initialize(note_clap->getChannelCount(), note_clap->getSampleRate());
samples.resize(openAL_time_to_samples(sf::seconds(1)), 0);
}
timing(timing_)
{}
NoteClaps::NoteClaps(
const better::Notes* notes_,
const better::Timing* timing_,
std::shared_ptr<sf::SoundBuffer> note_clap_,
float pitch_
std::shared_ptr<sf::SoundBuffer> note_clap,
float pitch
) :
pitch(pitch_),
FakePitchedSoundStream(note_clap, pitch),
notes(notes_),
timing(timing_),
note_clap(note_clap_)
{
sf::SoundStream::initialize(note_clap->getChannelCount(), note_clap->getSampleRate());
samples.resize(openAL_time_to_samples(sf::seconds(1)), 0);
}
timing(timing_)
{}
void NoteClaps::set_notes_and_timing(const better::Notes* notes_, const better::Timing* timing_) {
notes = notes_;
@ -50,16 +41,15 @@ std::shared_ptr<NoteClaps> NoteClaps::with_pitch(float pitch) {
return std::make_shared<NoteClaps>(
notes,
timing,
note_clap,
sample,
pitch
);
}
bool NoteClaps::onGetData(sf::SoundStream::Chunk& data) {
samples.assign(samples.size(), 0);
if (timing != nullptr and notes != nullptr) {
const auto absolute_buffer_start = current_sample;
const std::int64_t absolute_buffer_end = current_sample + static_cast<std::int64_t>(samples.size());
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);
@ -77,90 +67,25 @@ bool NoteClaps::onGetData(sf::SoundStream::Chunk& data) {
// don't want claps that *start* at the end sample since
// it's an *exculsive* end
if (sample < absolute_buffer_end) {
notes_at_sample[sample] += 1;
notes_at_sample.insert(sample);
}
});
for (auto it = notes_at_sample.begin(); it != notes_at_sample.end();) {
const auto absolute_clap_start = it->first;
const auto absolute_clap_end = absolute_clap_start + static_cast<std::int64_t>(note_clap->getSampleCount());
const auto absolute_clap_slice_start = std::max(
absolute_clap_start,
absolute_buffer_start
);
const auto absolute_clap_deoverlapped_end = std::min(
absolute_clap_end,
[&](const auto& it){
const auto next = std::next(it);
if (next != notes_at_sample.end()) {
return next->first;
} else {
return std::numeric_limits<std::int64_t>::max();
}
}(it)
);
if (absolute_clap_deoverlapped_end <= absolute_buffer_start) {
it = notes_at_sample.erase(it);
continue;
}
const auto absolute_clap_slice_end = std::min(
absolute_clap_deoverlapped_end,
absolute_buffer_end
);
const auto slice_size = absolute_clap_slice_end - absolute_clap_slice_start;
const auto slice_start_relative_to_clap_start = absolute_clap_slice_start - absolute_clap_start;
const auto slice_start_relative_to_buffer_start = absolute_clap_slice_start - absolute_buffer_start;
const auto input_start = note_clap->getSamples() + slice_start_relative_to_clap_start;
const auto input_end = input_start + slice_size;
const auto output_start = samples.begin() + slice_start_relative_to_buffer_start;
std::copy(
input_start,
input_end,
output_start
);
// has this clap been fully played in this buffer ?
if (absolute_clap_deoverlapped_end <= absolute_buffer_end) {
it = notes_at_sample.erase(it);
} else {
++it;
}
}
copy_sample_at_points(
sample,
output_buffer,
notes_at_sample,
absolute_buffer_start
);
}
data.samples = samples.data();
data.sampleCount = samples.size();
current_sample += samples.size();
data.samples = output_buffer.data();
data.sampleCount = output_buffer.size();
first_sample_of_next_buffer += output_buffer.size();
return true;
};
void NoteClaps::onSeek(sf::Time timeOffset) {
current_sample = music_time_to_samples(timeOffset);
first_sample_of_next_buffer = music_time_to_samples(timeOffset);
notes_at_sample.clear();
};
std::int64_t NoteClaps::openAL_time_to_samples(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()) * note_clap->getSampleRate() * note_clap->getChannelCount()) + 500000) / 1000000;
}
sf::Time NoteClaps::samples_to_openAL_time(std::int64_t samples) const {
sf::Time position = sf::Time::Zero;
// Make sure we don't divide by 0
if (note_clap->getSampleRate() != 0 && note_clap->getChannelCount() != 0)
position = sf::microseconds((samples * 1000000) / (note_clap->getChannelCount() * note_clap->getSampleRate()));
return position;
}
std::int64_t NoteClaps::music_time_to_samples(sf::Time position) const {
return openAL_time_to_samples(position / pitch);
}
sf::Time NoteClaps::samples_to_music_time(std::int64_t samples) const {
return samples_to_openAL_time(samples) * pitch;
}

View File

@ -8,9 +8,10 @@
#include "../better_notes.hpp"
#include "../better_timing.hpp"
#include "fake_pitched_sound_stream.hpp"
#include "precise_sound_stream.hpp"
class NoteClaps: public PreciseSoundStream {
class NoteClaps: public FakePitchedSoundStream {
public:
NoteClaps(
const better::Notes* notes_,
@ -35,17 +36,8 @@ protected:
void onSeek(sf::Time timeOffset) override;
private:
float pitch = 1.f;
std::vector<sf::Int16> samples;
std::int64_t current_sample = 0;
std::int64_t openAL_time_to_samples(sf::Time position) const;
sf::Time samples_to_openAL_time(std::int64_t samples) const;
std::int64_t music_time_to_samples(sf::Time position) const;
sf::Time samples_to_music_time(std::int64_t samples) const;
std::map<std::int64_t, unsigned int> notes_at_sample;
std::set<std::int64_t> notes_at_sample;
const better::Notes* notes;
const better::Timing* timing;
std::shared_ptr<sf::SoundBuffer> note_clap;
};

View File

@ -0,0 +1,62 @@
#include "sampler_callback.hpp"
void copy_sample_at_points(
const std::shared_ptr<sf::SoundBuffer>& sample,
std::span<sf::Int16> output_buffer,
std::set<std::int64_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;
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;
} 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 case 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

@ -0,0 +1,15 @@
#pragma once
#include <cstdint>
#include <memory>
#include <span>
#include <SFML/Audio/SoundBuffer.hpp>
#include <SFML/Config.hpp>
void copy_sample_at_points(
const std::shared_ptr<sf::SoundBuffer>& sample,
std::span<sf::Int16> output_buffer,
std::set<std::int64_t>& starting_points,
std::int64_t absolute_buffer_start
);