Implement fractional interval fitness computation

This commit is contained in:
Stepland 2023-07-05 01:39:13 +02:00
parent b494e3a356
commit edbf362dd8
2 changed files with 237 additions and 126 deletions

View File

@ -8,6 +8,7 @@
#include <functional>
#include <future>
#include <iostream>
#include <iterator>
#include <limits>
#include <numbers>
#include <numeric>
@ -17,76 +18,20 @@
#include <SFML/Config.hpp>
#include <SFML/System/Utf.hpp>
#include <eigen_polyfit.hpp>
#include <vector>
#include "Eigen/src/Core/Array.h"
#include "aubio_cpp.hpp"
#include "special_numeric_types.hpp"
std::vector<float> guess_tempo(const std::filesystem::path& path) {
std::vector<BPMCandidate> guess_tempo(const std::filesystem::path& audio) {
feis::InputSoundFile music;
music.open_from_path(path);
music.open_from_path(audio);
const auto onsets = detect_onsets(music);
estimate_bpm(onsets, music.getSampleRate());
// estimate offsets
}
void estimate_bpm(const std::set<std::size_t>& onsets, const std::size_t sample_rate) {
const auto fitness = broad_interval_test(onsets, sample_rate);
const auto corrected_fitness = correct_bias(fitness);
const auto max_fitness = corrected_fitness.maxCoeff();
std::vector<std::future<std::vector<IntervalFitness>>> futures;
for (std::size_t i = 0; i < corrected_fitness.size(); i++) {
if ((corrected_fitness[i] / max_fitness) > 0.4f) {
const auto interval = fitness.at(i).interval;
futures.emplace_back(
std::async(
std::launch::async,
std::bind(test_intervals, std::cref(onsets), interval - 9, 19, 1)
)
);
}
}
std::vector<IntervalFitness> candidates;
for (auto& future : futures) {
const auto results = future.get();
const auto it = std::ranges::max_element(results, {}, [](const IntervalFitness& f){return f.fitness;});
candidates.push_back(*it);
}
std::sort(
candidates.begin(),
candidates.end(),
[](const IntervalFitness& a, const IntervalFitness& b){return a.fitness < b.fitness;}
);
// remove multiples
std::vector<IntervalFitness> deduped_candidates;
while (not candidates.empty()) {
deduped_candidates.push_back(candidates.back());
candidates.pop_back();
const auto reference_interval = deduped_candidates.back().interval;
const Fraction reference_bpm = Fraction{60 * sample_rate} / Fraction{reference_interval};
std::erase_if(candidates, [&](const IntervalFitness& f){
const Fraction potential_multiple = round_fraction(Fraction(reference_interval, f.interval));
const Fraction candidate_bpm = Fraction{60 * sample_rate} / Fraction{f.interval};
Fraction diff = (reference_bpm * potential_multiple) - candidate_bpm;
if (diff < 0) {
diff *= -1;
}
return diff < 0.1f;
});
}
for (const auto& candidate: deduped_candidates) {
const Fraction bpm = Fraction{60 * sample_rate} / Fraction{candidate.interval};
const Fraction rounded_bpm = round_fraction(bpm);
Fraction diff = bpm - rounded_bpm;
if (diff < 0) {
diff *= -1;
}
if (diff < 0.05f) {
}
}
}
std::set<std::size_t> detect_onsets(feis::InputSoundFile& music) {
const auto sample_rate = music.getSampleRate();
const auto channel_count = music.getChannelCount();
@ -117,10 +62,79 @@ std::set<std::size_t> detect_onsets(feis::InputSoundFile& music) {
return onsets;
}
void estimate_bpm(const std::set<std::size_t>& onsets, const std::size_t sample_rate) {
const auto broad_fitness = broad_interval_test(onsets, sample_rate);
const auto corrected_fitness = correct_bias(broad_fitness);
const auto interval_candidates = narrow_interval_test(broad_fitness, corrected_fitness, onsets, sample_rate);
}
const float min_tested_bpm = 89.f;
const float max_tested_bpm = 205.f;
const std::size_t hamming_window_size = 1024;
std::vector<IntervalFitness> broad_interval_test(const std::set<std::size_t>& onsets, const std::size_t sample_rate) {
const std::size_t min_tested_interval = sample_rate * 60.f / max_tested_bpm;
const std::size_t max_tested_interval = sample_rate * 60.f / min_tested_bpm;
const std::size_t interval_range_width = max_tested_interval - min_tested_interval + 1;
const std::size_t stride = 10;
const std::size_t number_of_tested_intervals = (interval_range_width - 1) / stride + 1;
std::vector<IntervalFitness> fitness_values;
fitness_values.reserve(number_of_tested_intervals);
const auto threads = std::thread::hardware_concurrency();
const auto chunk_size = number_of_tested_intervals / threads;
std::vector<std::future<std::vector<IntervalFitness>>> futures;
for (std::size_t thread_index = 0; thread_index < threads; thread_index++) {
futures.emplace_back(
std::async(
std::launch::async,
std::bind(
fitness_of_interval_range,
std::cref(onsets),
min_tested_interval + thread_index * chunk_size * stride,
chunk_size,
stride
)
)
);
}
for (auto& future : futures) {
const auto new_values = future.get();
fitness_values.insert(fitness_values.end(), new_values.begin(), new_values.end());
}
return fitness_values;
}
std::vector<IntervalFitness> fitness_of_interval_range(const std::set<std::size_t>& onsets, const std::size_t start_interval, const std::size_t count, const std::size_t stride) {
std::vector<IntervalFitness> fitness_values;
fitness_values.reserve(count);
const auto max_tested_interval = start_interval + (count - 1) * stride;
Eigen::ArrayXf evidence_cache(max_tested_interval);
Eigen::ArrayXf confidence(max_tested_interval);
Eigen::ArrayX<std::size_t> int_histogram(max_tested_interval);
Eigen::ArrayXf histogram;
for (std::size_t interval_index = 0; interval_index < count; interval_index++) {
const auto tested_interval = start_interval + interval_index * stride;
int_histogram.setZero(tested_interval);
std::ranges::for_each(onsets, [&](std::size_t s){
int_histogram[s % tested_interval] += 1;
});
histogram = int_histogram.cast<float>();
evidence_cache.resize(tested_interval);
for (std::size_t onset = 0; onset < tested_interval; onset++) {
evidence_cache[onset] = evidence(histogram, onset);
}
confidence.resize(tested_interval);
confidence = evidence_cache;
const auto first_half_interval = tested_interval / 2;
const auto second_half_interval = tested_interval - first_half_interval;
confidence.head(first_half_interval) += evidence_cache.tail(first_half_interval) / 2;
confidence.tail(second_half_interval) += evidence_cache.head(second_half_interval) / 2;
fitness_values.push_back({tested_interval, confidence.maxCoeff()});
}
return fitness_values;
}
#ifndef EIGEN_VECTORIZE
#error eigen vectorization is off !
#endif
@ -133,8 +147,8 @@ Eigen::ArrayXf load_hamming_coefficients() {
const Eigen::ArrayXf hamming_coefficients = load_hamming_coefficients();
float evidence(const Eigen::ArrayXf& histogram, const std::size_t sample) {
const std::int64_t start_index = static_cast<std::int64_t>(sample) - hamming_window_size / 2;
const std::int64_t end_index = start_index + hamming_window_size; // exclusive
const std::int64_t start_index = static_cast<std::int64_t>(sample) - static_cast<std::int64_t>(hamming_window_size) / 2;
const std::int64_t end_index = start_index + static_cast<std::int64_t>(hamming_window_size); // exclusive
if (start_index < 0) {
const auto elements_before_first = std::abs(start_index);
return (
@ -165,68 +179,6 @@ float evidence(const Eigen::ArrayXf& histogram, const std::size_t sample) {
}
}
std::vector<IntervalFitness> broad_interval_test(const std::set<std::size_t>& onsets, const std::size_t sample_rate) {
const std::size_t min_tested_interval = sample_rate * 60.f / max_tested_bpm;
const std::size_t max_tested_interval = sample_rate * 60.f / min_tested_bpm;
const std::size_t interval_range_width = max_tested_interval - min_tested_interval + 1;
const std::size_t stride = 10;
const std::size_t number_of_tested_intervals = (interval_range_width - 1) / stride + 1;
std::vector<IntervalFitness> fitness_values;
fitness_values.reserve(number_of_tested_intervals);
const auto threads = std::thread::hardware_concurrency();
const auto chunk_size = number_of_tested_intervals / threads;
std::vector<std::future<std::vector<IntervalFitness>>> futures;
for (std::size_t thread_index = 0; thread_index < threads; thread_index++) {
futures.emplace_back(
std::async(
std::launch::async,
std::bind(
test_intervals,
std::cref(onsets),
min_tested_interval + thread_index * chunk_size * stride,
chunk_size,
stride
)
)
);
}
for (auto& future : futures) {
const auto new_values = future.get();
fitness_values.insert(fitness_values.end(), new_values.begin(), new_values.end());
}
return fitness_values;
}
std::vector<IntervalFitness> test_intervals(const std::set<std::size_t>& onsets, const std::size_t start_interval, const std::size_t count, const std::size_t stride) {
std::vector<IntervalFitness> fitness_values;
fitness_values.reserve(count);
const auto max_tested_interval = start_interval + (count - 1) * stride;
Eigen::ArrayXf evidence_cache(max_tested_interval);
Eigen::ArrayXf confidence(max_tested_interval);
Eigen::ArrayX<std::size_t> int_histogram(max_tested_interval);
Eigen::ArrayXf histogram;
for (std::size_t interval_index = 0; interval_index < count; interval_index++) {
const auto tested_interval = start_interval + interval_index * stride;
int_histogram.setZero(tested_interval);
std::ranges::for_each(onsets, [&](std::size_t s){
int_histogram[s % tested_interval] += 1;
});
histogram = int_histogram.cast<float>();
evidence_cache.resize(tested_interval);
for (std::size_t onset = 0; onset < tested_interval; onset++) {
evidence_cache[onset] = evidence(histogram, onset);
}
confidence.resize(tested_interval);
confidence = evidence_cache;
const auto first_half_interval = tested_interval / 2;
const auto second_half_interval = tested_interval - first_half_interval;
confidence.head(first_half_interval) += evidence_cache.tail(first_half_interval) / 2;
confidence.tail(second_half_interval) += evidence_cache.head(second_half_interval) / 2;
fitness_values.push_back({tested_interval, confidence.maxCoeff()});
}
return fitness_values;
}
Eigen::ArrayXf correct_bias(const std::vector<IntervalFitness>& fitness_results) {
std::vector<float> y_values(fitness_results.size());
std::transform(
@ -245,3 +197,138 @@ Eigen::ArrayXf correct_bias(const std::vector<IntervalFitness>& fitness_results)
+ coeffs[3] * fitness.pow(3)
);
}
std::vector<IntervalFitness> narrow_interval_test(
const std::vector<IntervalFitness>& broad_fitness,
const Eigen::ArrayXf& corrected_broad_fitness,
const std::set<std::size_t>& onsets,
const std::size_t sample_rate
) {
const auto max_fitness = corrected_broad_fitness.maxCoeff();
std::vector<std::future<std::vector<IntervalFitness>>> futures;
for (std::size_t i = 0; i < corrected_broad_fitness.size(); i++) {
if ((corrected_broad_fitness[i] / max_fitness) > 0.4f) {
const auto interval = broad_fitness.at(i).interval;
futures.emplace_back(
std::async(
std::launch::async,
std::bind(fitness_of_interval_range, std::cref(onsets), interval - 9, 19, 1)
)
);
}
}
std::vector<IntervalFitness> candidates;
for (auto& future : futures) {
const auto results = future.get();
const auto it = std::ranges::max_element(results, {}, [](const IntervalFitness& f){return f.fitness;});
candidates.push_back(*it);
}
return candidates;
}
std::vector<BPMFitness> select_bpm_candidates(
std::vector<IntervalFitness>& interval_candidates,
const std::size_t sample_rate,
const std::set<std::size_t>& onsets
) {
// Bram van de Wetering's original paper [BvdW] :
// "BPM candidates that differ by less than 0.1 from a multiple of a
// candidate with a higher fitness value are rejected."
std::sort(
interval_candidates.begin(),
interval_candidates.end(),
[](const IntervalFitness& a, const IntervalFitness& b){return a.fitness < b.fitness;}
);
std::vector<IntervalFitness> deduped_candidates;
while (not interval_candidates.empty()) {
deduped_candidates.push_back(interval_candidates.back());
interval_candidates.pop_back();
const auto reference_interval = deduped_candidates.back().interval;
const Fraction reference_bpm = Fraction{60 * sample_rate} / Fraction{reference_interval};
std::erase_if(interval_candidates, [&](const IntervalFitness& f){
const Fraction potential_multiple = round_fraction(Fraction(reference_interval, f.interval));
const Fraction candidate_bpm = Fraction{60 * sample_rate} / Fraction{f.interval};
Fraction diff = (reference_bpm * potential_multiple) - candidate_bpm;
if (diff < 0) {
diff *= -1;
}
return diff < 0.1f;
});
}
// [BvdW] :
// "BPM candidates that differ by less than 0.05 from an integer value are
// rounded and re-evaluated with a (possibly fractional) interval. If the
// fitness value of the rounded BPM exceeds 99% of the original fitness
// value, the rounded BPM is assumed to be correct and replaces the original
// BPM candidate."
std::vector<BPMFitness> bpm_candidates;
std::transform(
deduped_candidates.begin(),
deduped_candidates.end(),
std::back_inserter(bpm_candidates),
[&](const IntervalFitness& f){
return BPMFitness{
Fraction{60 * sample_rate} / Fraction{f.interval},
f.fitness
};
}
);
for (auto& candidate: bpm_candidates) {
const Fraction rounded_bpm = round_fraction(candidate.bpm);
Fraction diff = candidate.bpm - rounded_bpm;
if (diff < 0) {
diff *= -1;
}
if (diff < 0.05f) {
const auto rounded_bpm_fitness = fitness_of_bpm(onsets, sample_rate, rounded_bpm);
if (rounded_bpm_fitness > 0.99f * candidate.fitness) {
candidate.bpm = rounded_bpm;
candidate.fitness = rounded_bpm_fitness;
}
}
}
// At this point [BvdW] says :
//
// "if the ratio between the fitness values of the primary and secondary BPM
// candidates is less than 1.05, the fitness values of all BPM candidates
// are recomputed with accurate, fractional intervals."
//
// But I just really don't get why ... we've already recomputed the fitness
// of BPMs we changed at the previous step, and the other BPMs haven't
// changed so there's no reason to recompute their fitness either ...
// Instead I just sort the candidates by fitness and return them
std::sort(
bpm_candidates.begin(),
bpm_candidates.end(),
[](const BPMFitness& a, const BPMFitness& b){return a.fitness < b.fitness;}
);
return bpm_candidates;
}
float fitness_of_bpm(
const std::set<std::size_t>& onsets,
const std::size_t sample_rate,
const Fraction BPM
) {
const Fraction fractional_interval = Fraction(60) * Fraction(sample_rate) / Fraction(BPM);
const std::size_t rounded_interval = static_cast<std::size_t>(floor_fraction(fractional_interval)) + 1;
Eigen::ArrayXf evidence_cache(rounded_interval);
Eigen::ArrayXf confidence(rounded_interval);
Eigen::ArrayX<std::size_t> int_histogram = Eigen::ArrayX<std::size_t>::Zero(rounded_interval);
Eigen::ArrayXf histogram;
std::ranges::for_each(onsets, [&](std::size_t s){
const auto fractional_bin = Fraction{s} % fractional_interval;
const auto bin = static_cast<std::size_t>(floor_fraction(fractional_bin));
int_histogram[bin] += 1;
});
histogram = int_histogram.cast<float>();
for (std::size_t onset = 0; onset < rounded_interval; onset++) {
evidence_cache[onset] = evidence(histogram, onset);
}
confidence = evidence_cache;
const auto first_half_interval = rounded_interval / 2;
const auto second_half_interval = rounded_interval - first_half_interval;
confidence.head(first_half_interval) += evidence_cache.tail(first_half_interval) / 2;
confidence.tail(second_half_interval) += evidence_cache.head(second_half_interval) / 2;
return confidence.maxCoeff();
}

View File

@ -7,7 +7,7 @@
#include <Eigen/Dense>
#include <SFML/System/Time.hpp>
#include "Eigen/src/Core/Matrix.h"
#include "special_numeric_types.hpp"
#include "utf8_sfml_redefinitions.hpp"
@ -16,16 +16,40 @@ struct IntervalFitness {
float fitness;
};
std::vector<float> guess_tempo(const std::filesystem::path& audio);
struct BPMFitness {
Fraction bpm;
float fitness;
};
struct BPMCandidate {
Fraction bpm;
std::size_t offset;
float fitness;
};
std::vector<BPMCandidate> guess_tempo(const std::filesystem::path& audio);
std::set<std::size_t> detect_onsets(feis::InputSoundFile& music);
void estimate_bpm(const std::set<std::size_t>& onsets, const std::size_t sample_rate);
std::vector<IntervalFitness> broad_interval_test(const std::set<std::size_t>& onsets, const std::size_t sample_rate);
std::vector<IntervalFitness> test_intervals(
std::vector<IntervalFitness> fitness_of_interval_range(
const std::set<std::size_t>& onsets,
const std::size_t start_interval,
const std::size_t count,
const std::size_t stride
);
float evidence(const Eigen::ArrayXf& histogram, const std::size_t sample);
Eigen::ArrayXf correct_bias(const std::vector<IntervalFitness>& fitness_results);
std::vector<IntervalFitness> narrow_interval_test(
const std::vector<IntervalFitness>& broad_fitness,
const Eigen::ArrayXf& corrected_fitness,
const std::set<std::size_t>& onsets,
const std::size_t sample_rate
);
std::vector<BPMFitness> select_bpm_candidates(
std::vector<IntervalFitness>& interval_candidates,
const std::size_t sample_rate,
const std::set<std::size_t>& onsets
);
float fitness_of_bpm(const std::set<std::size_t>& onsets, const std::size_t sample_rate, const Fraction BPM);