mirror of
https://gitlab.com/square-game-liberation-front/F.E.I.S.git
synced 2024-11-12 02:00:53 +01:00
Prettier Sync Window
This commit is contained in:
parent
8219aad65a
commit
a6f0c6e917
@ -1,898 +0,0 @@
|
||||
// FindTempo_standalone.cpp
|
||||
// Originally by Fietsemaker (Bram van de Wetering) and adapted to a standalone format by Nathan Stephenson
|
||||
|
||||
|
||||
#include "FindTempo_standalone.hpp"
|
||||
|
||||
namespace vortex {
|
||||
|
||||
using namespace std;
|
||||
|
||||
void FindOnsets(float* samples, int samplerate, int numFrames, int numThreads, Vector<Onset>* onsets) {
|
||||
int window = 1024; // window size
|
||||
int hop_size = window / 4;
|
||||
unsigned int read = 0; // bytes read each loop
|
||||
unsigned int total = 0; // total bytes read
|
||||
|
||||
fvec_t* in = new_fvec(hop_size); // input audio buffer
|
||||
fvec_t* out = new_fvec(2); // output position
|
||||
|
||||
aubio_onset_t* o = new_aubio_onset("complex", window, hop_size, samplerate);
|
||||
do {
|
||||
for (int i = 0; i < hop_size; i++) {
|
||||
// if (total + i >= numFrames) PrintOut("exceeding numFrames")
|
||||
in->data[i] = (total+i < numFrames) ? samples[total+i] : 0;
|
||||
}
|
||||
|
||||
aubio_onset_do(o, in, out);
|
||||
|
||||
// if (out->data[0] > 0) // if there is an onset
|
||||
if (out->data[0] > 0 && aubio_onset_get_last(o) >= 0) {
|
||||
// printf("%d\n", aubio_onset_get_last(o));
|
||||
onsets->append(Onset(aubio_onset_get_last(o), 1.0));
|
||||
}
|
||||
|
||||
total += hop_size;
|
||||
} while (total < numFrames);
|
||||
|
||||
// cleanup
|
||||
// del_aubio_source(source);
|
||||
del_aubio_onset(o);
|
||||
del_fvec(in);
|
||||
del_fvec(out);
|
||||
}
|
||||
|
||||
namespace find_tempo_cpp {
|
||||
|
||||
static const real_t MinimumBPM = 89.0;
|
||||
static const real_t MaximumBPM = 205.0;
|
||||
static const int IntervalDelta = 10;
|
||||
static const int IntervalDownsample = 3;
|
||||
static const int MaxThreads = 8;
|
||||
|
||||
// ================================================================================================
|
||||
// Helper structs.
|
||||
|
||||
struct TempoResult
|
||||
{
|
||||
TempoResult() : fitness(0.0f), bpm(0.0f), offset(0.0f) {}
|
||||
|
||||
float fitness;
|
||||
float bpm;
|
||||
float offset;
|
||||
|
||||
TempoResult(float a, float b, float c) {
|
||||
bpm = a; offset = b; fitness = c;
|
||||
}
|
||||
};
|
||||
|
||||
typedef Vector<TempoResult> TempoResults;
|
||||
typedef unsigned char uchar;
|
||||
|
||||
struct TempoSort {
|
||||
bool operator()(const TempoResult& a, const TempoResult& b) {
|
||||
return a.fitness > b.fitness;
|
||||
}
|
||||
};
|
||||
|
||||
struct SerializedTempo
|
||||
{
|
||||
SerializedTempo() : onsets(nullptr) {}
|
||||
|
||||
float* samples;
|
||||
int samplerate;
|
||||
int numFrames;
|
||||
int numThreads;
|
||||
Vector<Onset>* onsets;
|
||||
uchar* terminate;
|
||||
TempoResults result;
|
||||
};
|
||||
|
||||
struct GapData
|
||||
{
|
||||
GapData(int numThreads, int maxInterval, int downsample, int numOnsets, const Onset* onsets);
|
||||
~GapData();
|
||||
|
||||
const Onset* onsets;
|
||||
int* wrappedPos;
|
||||
real_t* wrappedOnsets;
|
||||
real_t* window;
|
||||
int bufferSize, numOnsets, windowSize, downsample;
|
||||
};
|
||||
|
||||
struct IntervalTester
|
||||
{
|
||||
IntervalTester(int samplerate, int numOnsets, const Onset* onsets);
|
||||
~IntervalTester();
|
||||
|
||||
int minInterval;
|
||||
int maxInterval;
|
||||
int numIntervals;
|
||||
int samplerate;
|
||||
int gapWindowSize;
|
||||
int numOnsets;
|
||||
const Onset* onsets;
|
||||
real_t* fitness;
|
||||
real_t coefs[4];
|
||||
};
|
||||
|
||||
// ================================================================================================
|
||||
// Audio processing
|
||||
|
||||
// Creates weights for a hamming window of length n.
|
||||
static void CreateHammingWindow(real_t* out, int n)
|
||||
{
|
||||
const real_t t = 6.2831853071795864 / (real_t)(n - 1);
|
||||
for (int i = 0; i < n; ++i) out[i] = 0.54 - 0.46 * cos((real_t)i * t);
|
||||
}
|
||||
|
||||
// Normalizes the given fitness value based the given 3rd order poly coefficients and interval.
|
||||
static void NormalizeFitness(real_t& fitness, const real_t* coefs, real_t interval)
|
||||
{
|
||||
real_t x = interval, x2 = x * x, x3 = x2 * x;
|
||||
fitness -= coefs[0] + coefs[1] * x + coefs[2] * x2 + coefs[3] * x3;
|
||||
}
|
||||
|
||||
// ================================================================================================
|
||||
// Gap confidence evaluation
|
||||
|
||||
GapData::GapData(int numThreads, int bufferSize, int downsample, int numOnsets, const Onset* onsets)
|
||||
: numOnsets(numOnsets)
|
||||
, onsets(onsets)
|
||||
, downsample(downsample)
|
||||
, windowSize(2048 >> downsample)
|
||||
, bufferSize(bufferSize)
|
||||
{
|
||||
window = AlignedMalloc(real_t, windowSize);
|
||||
wrappedPos = AlignedMalloc(int, numOnsets * numThreads);
|
||||
wrappedOnsets = AlignedMalloc(real_t, bufferSize * numThreads);
|
||||
CreateHammingWindow(window, windowSize);
|
||||
}
|
||||
|
||||
GapData::~GapData()
|
||||
{
|
||||
AlignedFree(window);
|
||||
AlignedFree(wrappedPos);
|
||||
AlignedFree(wrappedOnsets);
|
||||
}
|
||||
|
||||
// Returns the confidence value that indicates how many onsets are close to the given gap position.
|
||||
static real_t GapConfidence(const GapData& gapdata, int threadId, int gapPos, int interval)
|
||||
{
|
||||
int numOnsets = gapdata.numOnsets;
|
||||
int windowSize = gapdata.windowSize;
|
||||
int halfWindowSize = windowSize / 2;
|
||||
const real_t* window = gapdata.window;
|
||||
const real_t* wrappedOnsets = gapdata.wrappedOnsets + gapdata.bufferSize * threadId;
|
||||
real_t area = 0.0;
|
||||
|
||||
int beginOnset = gapPos - halfWindowSize;
|
||||
int endOnset = gapPos + halfWindowSize;
|
||||
|
||||
if (beginOnset < 0)
|
||||
{
|
||||
int wrappedBegin = beginOnset + interval;
|
||||
for (int i = wrappedBegin; i < interval; ++i)
|
||||
{
|
||||
int windowIndex = i - wrappedBegin;
|
||||
area += wrappedOnsets[i] * window[windowIndex];
|
||||
}
|
||||
beginOnset = 0;
|
||||
}
|
||||
if (endOnset > interval)
|
||||
{
|
||||
int wrappedEnd = endOnset - interval;
|
||||
int indexOffset = windowSize - wrappedEnd;
|
||||
for (int i = 0; i < wrappedEnd; ++i)
|
||||
{
|
||||
int windowIndex = i + indexOffset;
|
||||
area += wrappedOnsets[i] * window[windowIndex];
|
||||
}
|
||||
endOnset = interval;
|
||||
}
|
||||
for (int i = beginOnset; i < endOnset; ++i)
|
||||
{
|
||||
int windowIndex = i - beginOnset;
|
||||
area += wrappedOnsets[i] * window[windowIndex];
|
||||
}
|
||||
|
||||
return area;
|
||||
}
|
||||
|
||||
// Returns the confidence of the best gap value for the given interval.
|
||||
static real_t GetConfidenceForInterval(const GapData& gapdata, int threadId, int interval)
|
||||
{
|
||||
int downsample = gapdata.downsample;
|
||||
int numOnsets = gapdata.numOnsets;
|
||||
const Onset* onsets = gapdata.onsets;
|
||||
|
||||
int* wrappedPos = gapdata.wrappedPos + gapdata.numOnsets * threadId;
|
||||
real_t* wrappedOnsets = gapdata.wrappedOnsets + gapdata.bufferSize * threadId;
|
||||
memset(wrappedOnsets, 0, sizeof(real_t) * gapdata.bufferSize);
|
||||
|
||||
// Make a histogram of onset strengths for every position in the interval.
|
||||
int reducedInterval = interval >> downsample;
|
||||
for (int i = 0; i < numOnsets; ++i)
|
||||
{
|
||||
int pos = (onsets[i].pos % interval) >> downsample;
|
||||
wrappedPos[i] = pos;
|
||||
// printf("%f\n", onsets[i].strength);
|
||||
wrappedOnsets[pos] += onsets[i].strength;
|
||||
}
|
||||
|
||||
// Record the amount of support for each gap value.
|
||||
real_t highestConfidence = 0.0;
|
||||
for (int i = 0; i < numOnsets; ++i)
|
||||
{
|
||||
int pos = wrappedPos[i];
|
||||
real_t confidence = GapConfidence(gapdata, threadId, pos, reducedInterval);
|
||||
int offbeatPos = (pos + reducedInterval / 2) % reducedInterval;
|
||||
confidence += GapConfidence(gapdata, threadId, offbeatPos, reducedInterval) * 0.5;
|
||||
|
||||
if (confidence > highestConfidence)
|
||||
{
|
||||
highestConfidence = confidence;
|
||||
}
|
||||
}
|
||||
// printf("%f\n", highestConfidence);
|
||||
return highestConfidence;
|
||||
}
|
||||
|
||||
// Returns the confidence of the best gap value for the given BPM value.
|
||||
static real_t GetConfidenceForBPM(const GapData& gapdata, int threadId, IntervalTester& test, real_t bpm)
|
||||
{
|
||||
assert(bpm > 0);
|
||||
int numOnsets = gapdata.numOnsets;
|
||||
const Onset* onsets = gapdata.onsets;
|
||||
|
||||
int* wrappedPos = gapdata.wrappedPos + gapdata.numOnsets * threadId;
|
||||
real_t* wrappedOnsets = gapdata.wrappedOnsets + gapdata.bufferSize * threadId;
|
||||
// printf("%d\n\n", gapdata.bufferSize);
|
||||
memset(wrappedOnsets, 0, sizeof(real_t) * gapdata.bufferSize);
|
||||
|
||||
// Make a histogram of i strengths for every position in the interval.
|
||||
real_t intervalf = test.samplerate * 60.0 / bpm;
|
||||
int interval = (int)(intervalf + 0.5);
|
||||
for (int i = 0; i < numOnsets; ++i)
|
||||
{
|
||||
// printf("%d out of %d\n", i+1, numOnsets);
|
||||
// PrintOut(string(onsets[i]));
|
||||
int pos = (int)fmod((real_t)onsets[i].pos, intervalf);
|
||||
wrappedPos[i] = pos;
|
||||
// printf("%d\n", pos);
|
||||
wrappedOnsets[pos] += onsets[i].strength; // error here @ 252837
|
||||
}
|
||||
|
||||
// Record the amount of support for each gap value.
|
||||
real_t highestConfidence = 0.0;
|
||||
for (int i = 0; i < numOnsets; ++i)
|
||||
{
|
||||
int pos = wrappedPos[i];
|
||||
real_t confidence = GapConfidence(gapdata, threadId, pos, interval);
|
||||
int offbeatPos = (pos + interval / 2) % interval;
|
||||
confidence += GapConfidence(gapdata, threadId, offbeatPos, interval) * 0.5;
|
||||
|
||||
if (confidence > highestConfidence)
|
||||
{
|
||||
highestConfidence = confidence;
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize the confidence value.
|
||||
NormalizeFitness(highestConfidence, test.coefs, intervalf);
|
||||
|
||||
return highestConfidence;
|
||||
}
|
||||
|
||||
// ================================================================================================
|
||||
// Interval testing
|
||||
|
||||
IntervalTester::IntervalTester(int samplerate, int numOnsets, const Onset* onsets)
|
||||
: samplerate(samplerate)
|
||||
, numOnsets(numOnsets)
|
||||
, onsets(onsets)
|
||||
{
|
||||
minInterval = (int)(samplerate * 60.0 / MaximumBPM + 0.5);
|
||||
maxInterval = (int)(samplerate * 60.0 / MinimumBPM + 0.5);
|
||||
numIntervals = maxInterval - minInterval;
|
||||
|
||||
fitness = AlignedMalloc(real_t, numIntervals);
|
||||
}
|
||||
|
||||
IntervalTester::~IntervalTester()
|
||||
{
|
||||
AlignedFree(fitness);
|
||||
}
|
||||
|
||||
static real_t IntervalToBPM(const IntervalTester& test, int i)
|
||||
{
|
||||
return (test.samplerate * 60.0) / (i + test.minInterval);
|
||||
}
|
||||
|
||||
static void FillCoarseIntervals(IntervalTester& test, GapData& gapdata, int numThreads)
|
||||
{
|
||||
int numCoarseIntervals = (test.numIntervals + IntervalDelta - 1) / IntervalDelta;
|
||||
if (numThreads > 1)
|
||||
{
|
||||
struct IntervalThreads : public ParallelThreads
|
||||
{
|
||||
IntervalTester* test;
|
||||
GapData* gapdata;
|
||||
void execute(int i, int t) override
|
||||
{
|
||||
int index = i * IntervalDelta;
|
||||
// printf("%d\n", index);
|
||||
int interval = test->minInterval + index;
|
||||
test->fitness[index] = max(0.001, GetConfidenceForInterval(*gapdata, t, interval));
|
||||
}
|
||||
};
|
||||
IntervalThreads threads;
|
||||
threads.test = &test;
|
||||
threads.gapdata = &gapdata;
|
||||
threads.run(numCoarseIntervals, numThreads);
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int i = 0; i < numCoarseIntervals; ++i)
|
||||
{
|
||||
int index = i * IntervalDelta;
|
||||
int interval = test.minInterval + index;
|
||||
test.fitness[index] = max(0.001, GetConfidenceForInterval(gapdata, 0, interval));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static vec2i FillIntervalRange(IntervalTester& test, GapData& gapdata, int begin, int end)
|
||||
{
|
||||
begin = max(begin, 0);
|
||||
end = min(end, test.numIntervals);
|
||||
real_t* fit = test.fitness + begin;
|
||||
for (int i = begin, interval = test.minInterval + begin; i < end; ++i, ++interval, ++fit)
|
||||
{
|
||||
if (*fit == 0)
|
||||
{
|
||||
*fit = GetConfidenceForInterval(gapdata, 0, interval);
|
||||
NormalizeFitness(*fit, test.coefs, (real_t)interval);
|
||||
*fit = max(*fit, 0.1);
|
||||
}
|
||||
}
|
||||
return {begin, end};
|
||||
}
|
||||
|
||||
static int FindBestInterval(const real_t* fitness, int begin, int end)
|
||||
{
|
||||
int bestInterval = 0;
|
||||
real_t highestFitness = 0.0;
|
||||
for (int i = begin; i < end; ++i)
|
||||
{
|
||||
if (fitness[i] > highestFitness)
|
||||
{
|
||||
highestFitness = fitness[i];
|
||||
bestInterval = i;
|
||||
}
|
||||
}
|
||||
return bestInterval;
|
||||
}
|
||||
|
||||
// ================================================================================================
|
||||
// BPM testing
|
||||
|
||||
// Removes BPM values that are near-duplicates or multiples of a better BPM value.
|
||||
static void RemoveDuplicates(TempoResults& tempo)
|
||||
{
|
||||
for (int i = 0; i < tempo.size(); ++i)
|
||||
{
|
||||
real_t bpm = tempo[i].bpm, doubled = bpm * 2.0, halved = bpm * 0.5;
|
||||
for (int j = tempo.size() - 1; j > i; --j)
|
||||
{
|
||||
real_t v = tempo[j].bpm;
|
||||
if (min(min(abs(v - bpm), abs(v - doubled)), abs(v - halved)) < 0.1)
|
||||
{
|
||||
// printf("Erasing %f\n", tempo[j].bpm);
|
||||
tempo.erase(j);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Rounds BPM values that are close to integer values.
|
||||
static void RoundBPMValues(IntervalTester& test, GapData& gapdata, TempoResults& tempo)
|
||||
{
|
||||
for (auto& t : tempo)
|
||||
{
|
||||
real_t roundBPM = round(t.bpm);
|
||||
real_t diff = abs(t.bpm - roundBPM);
|
||||
if (diff < 0.01)
|
||||
{
|
||||
t.bpm = roundBPM;
|
||||
}
|
||||
else if (diff < 0.05)
|
||||
{
|
||||
real_t old = GetConfidenceForBPM(gapdata, 0, test, t.bpm);
|
||||
real_t cur = GetConfidenceForBPM(gapdata, 0, test, roundBPM);
|
||||
if (cur > old * 0.99) t.bpm = roundBPM;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Finds likely BPM candidates based on the given note onset values.
|
||||
static void CalculateBPM(SerializedTempo* data, Onset* onsets, int numOnsets)
|
||||
{
|
||||
auto& tempo = data->result;
|
||||
|
||||
// In order to determine the BPM, we need at least two onsets.
|
||||
if (numOnsets < 2)
|
||||
{
|
||||
PrintErr("Onset count is less than 2, skipping BPM calculation...");
|
||||
tempo.append(100.0, 0.0, 1.0);
|
||||
return;
|
||||
}
|
||||
|
||||
IntervalTester test(data->samplerate, numOnsets, onsets);
|
||||
GapData* gapdata = new GapData(data->numThreads, test.maxInterval, IntervalDownsample, numOnsets, onsets);
|
||||
|
||||
// Loop through every 10th possible BPM, later we will fill in those that look interesting.
|
||||
memset(test.fitness, 0, test.numIntervals * sizeof(real_t));
|
||||
FillCoarseIntervals(test, *gapdata, data->numThreads);
|
||||
int numCoarseIntervals = (test.numIntervals + IntervalDelta - 1) / IntervalDelta;
|
||||
MarkProgress(2, "Fill coarse intervals");
|
||||
|
||||
// Determine the polynomial coefficients to approximate the fitness curve and normalize the current fitness values.
|
||||
mathalgo::polyfit(3, test.coefs, test.fitness, numCoarseIntervals, test.minInterval);
|
||||
// polyfit(3, test.coefs, test.fitness, numCoarseIntervals, test.minInterval, IntervalDelta);
|
||||
real_t maxFitness = 0.001;
|
||||
for (int i = 0; i < test.numIntervals; i += IntervalDelta)
|
||||
{
|
||||
NormalizeFitness(test.fitness[i], test.coefs, (real_t)(test.minInterval + i));
|
||||
maxFitness = max(maxFitness, test.fitness[i]);
|
||||
}
|
||||
|
||||
// Refine the intervals around the best intervals.
|
||||
real_t fitnessThreshold = maxFitness * 0.4;
|
||||
for (int i = 0; i < test.numIntervals; i += IntervalDelta)
|
||||
{
|
||||
if (test.fitness[i] > fitnessThreshold)
|
||||
{
|
||||
vec2i range = FillIntervalRange(test, *gapdata, i - IntervalDelta, i + IntervalDelta);
|
||||
int best = FindBestInterval(test.fitness, range.x, range.y);
|
||||
tempo.append(IntervalToBPM(test, best), 0.0, test.fitness[best]);
|
||||
}
|
||||
}
|
||||
MarkProgress(3, "Refine intervals");
|
||||
|
||||
// At this point we stop the downsampling and upgrade to a more precise gap window.
|
||||
delete gapdata;
|
||||
gapdata = new GapData(data->numThreads, test.maxInterval, 0, numOnsets, onsets);
|
||||
|
||||
// Round BPM values to integers when possible, and remove weaker duplicates.
|
||||
std::stable_sort(tempo.begin(), tempo.end(), TempoSort());
|
||||
RemoveDuplicates(tempo);
|
||||
RoundBPMValues(test, *gapdata, tempo);
|
||||
|
||||
// If the fitness of the first and second option is very close, we ask for a second opinion.
|
||||
if (tempo.size() >= 2 && tempo[0].fitness / tempo[1].fitness < 1.05)
|
||||
{
|
||||
for (auto& t : tempo)
|
||||
t.fitness = GetConfidenceForBPM(*gapdata, 0, test, t.bpm);
|
||||
std::stable_sort(tempo.begin(), tempo.end(), TempoSort());
|
||||
}
|
||||
|
||||
// In all 300 test cases the correct BPM value was part of the top 3 choices,
|
||||
// so it seems reasonable to discard anything below the top 3 as irrelevant.
|
||||
if (tempo.size() > 3) tempo.resize(3);
|
||||
|
||||
// Cleanup.
|
||||
delete gapdata;
|
||||
}
|
||||
|
||||
// ================================================================================================
|
||||
// Offset testing
|
||||
|
||||
static void ComputeSlopes(const float* samples, real_t* out, int numFrames, int samplerate)
|
||||
{
|
||||
memset(out, 0, sizeof(real_t) * numFrames);
|
||||
|
||||
int wh = samplerate / 20;
|
||||
if (numFrames < wh * 2) return;
|
||||
|
||||
// Initial sums of the left/right side of the window.
|
||||
real_t sumL = 0, sumR = 0;
|
||||
for (int i = 0, j = wh; i < wh; ++i, ++j)
|
||||
{
|
||||
sumL += abs(samples[i]);
|
||||
sumR += abs(samples[j]);
|
||||
}
|
||||
|
||||
// Slide window over the samples.
|
||||
real_t scalar = 1.0 / (real_t)wh;
|
||||
for (int i = wh, end = numFrames - wh; i < end; ++i)
|
||||
{
|
||||
// Determine slope value.
|
||||
out[i] = max(0.0, (real_t)(sumR - sumL) * scalar);
|
||||
|
||||
// Move window.
|
||||
real_t cur = abs(samples[i]);
|
||||
sumL -= abs(samples[i - wh]);
|
||||
sumL += cur;
|
||||
sumR -= cur;
|
||||
sumR += abs(samples[i + wh]);
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the most promising offset for the given BPM value.
|
||||
static real_t GetBaseOffsetValue(const GapData& gapdata, int samplerate, real_t bpm)
|
||||
{
|
||||
int numOnsets = gapdata.numOnsets;
|
||||
const Onset* onsets = gapdata.onsets;
|
||||
|
||||
int* wrappedPos = gapdata.wrappedPos;
|
||||
real_t* wrappedOnsets = gapdata.wrappedOnsets;
|
||||
memset(wrappedOnsets, 0, sizeof(real_t) * gapdata.bufferSize);
|
||||
|
||||
// Make a histogram of onset strengths for every position in the interval.
|
||||
real_t intervalf = samplerate * 60.0 / bpm;
|
||||
int interval = (int)(intervalf + 0.5);
|
||||
memset(wrappedOnsets, 0, sizeof(real_t) * interval);
|
||||
for (int i = 0; i < numOnsets; ++i)
|
||||
{
|
||||
int pos = (int)fmod((real_t)onsets[i].pos, intervalf);
|
||||
wrappedPos[i] = pos;
|
||||
wrappedOnsets[pos] += 1.0;
|
||||
}
|
||||
|
||||
// Record the amount of support for each gap value.
|
||||
real_t highestConfidence = 0.0;
|
||||
int offsetPos = 0;
|
||||
for (int i = 0; i < numOnsets; ++i)
|
||||
{
|
||||
int pos = wrappedPos[i];
|
||||
real_t confidence = GapConfidence(gapdata, 0, pos, interval);
|
||||
int offbeatPos = (pos + interval / 2) % interval;
|
||||
confidence += GapConfidence(gapdata, 0, offbeatPos, interval) * 0.5;
|
||||
|
||||
if (confidence > highestConfidence)
|
||||
{
|
||||
highestConfidence = confidence;
|
||||
offsetPos = pos;
|
||||
}
|
||||
}
|
||||
|
||||
return (real_t)offsetPos / (real_t)samplerate;
|
||||
}
|
||||
|
||||
// Compares each offset to its corresponding offbeat value, and selects the most promising one.
|
||||
static real_t AdjustForOffbeats(SerializedTempo* data, real_t offset, real_t bpm)
|
||||
{
|
||||
int samplerate = data->samplerate;
|
||||
int numFrames = data->numFrames;
|
||||
|
||||
// Create a slope representation of the waveform.
|
||||
real_t* slopes = AlignedMalloc(real_t, numFrames);
|
||||
ComputeSlopes(data->samples, slopes, numFrames, samplerate);
|
||||
|
||||
// Determine the offbeat sample position.
|
||||
real_t secondsPerBeat = 60.0 / bpm;
|
||||
real_t offbeat = offset + secondsPerBeat * 0.5;
|
||||
if (offbeat > secondsPerBeat) offbeat -= secondsPerBeat;
|
||||
|
||||
// Calculate the support for both sample positions.
|
||||
real_t end = (real_t)numFrames;
|
||||
real_t interval = secondsPerBeat * samplerate;
|
||||
real_t posA = offset * samplerate, sumA = 0.0;
|
||||
real_t posB = offbeat * samplerate, sumB = 0.0;
|
||||
for (; posA < end && posB < end; posA += interval, posB += interval)
|
||||
{
|
||||
sumA += slopes[(int)posA];
|
||||
sumB += slopes[(int)posB];
|
||||
}
|
||||
AlignedFree(slopes);
|
||||
|
||||
// Return the offset with the highest support.
|
||||
return (sumA >= sumB) ? offset : offbeat;
|
||||
}
|
||||
|
||||
// Selects the best offset value for each of the BPM candidates.
|
||||
static void CalculateOffset(SerializedTempo* data, Onset* onsets, int numOnsets)
|
||||
{
|
||||
auto& tempo = data->result;
|
||||
int samplerate = data->samplerate;
|
||||
|
||||
// Create gapdata buffers for testing.
|
||||
real_t maxInterval = 0.0;
|
||||
for (auto& t : tempo) maxInterval = max(maxInterval, samplerate * 60.0 / t.bpm);
|
||||
GapData gapdata(1, (int)(maxInterval + 1.0), 1, numOnsets, onsets);
|
||||
|
||||
// Fill in onset values for each BPM.
|
||||
for (auto& t : tempo)
|
||||
t.offset = GetBaseOffsetValue(gapdata, samplerate, t.bpm);
|
||||
|
||||
// Test all onsets against their offbeat values, pick the best one.
|
||||
for (auto& t : tempo)
|
||||
t.offset = AdjustForOffbeats(data, t.offset, t.bpm);
|
||||
}
|
||||
|
||||
// ================================================================================================
|
||||
// BPM testing wrapper class
|
||||
|
||||
static const char* sProgressText[]
|
||||
{
|
||||
"[1/6] Looking for onsets",
|
||||
"[2/6] Scanning intervals",
|
||||
"[3/6] Refining intervals",
|
||||
"[4/6] Selecting BPM values",
|
||||
"[5/6] Calculating offsets",
|
||||
"BPM detection results"
|
||||
};
|
||||
|
||||
class TempoDetector //: public TempoDetector, public BackgroundThread
|
||||
{
|
||||
public:
|
||||
TempoDetector(Samples s, double time, double len);
|
||||
TempoDetector(Vector<Onset>* onsets, int sr);
|
||||
~TempoDetector();
|
||||
|
||||
void exec();
|
||||
|
||||
// bool hasSamples() { return (myData.samples != nullptr); }
|
||||
// const char* getProgress() const { return sProgressText[myData.progress]; }
|
||||
// bool hasResult() const { return isDone(); }
|
||||
const Vector<TempoResult>& getResult() const { return myData.result; }
|
||||
|
||||
private:
|
||||
SerializedTempo myData;
|
||||
};
|
||||
|
||||
TempoDetector::TempoDetector(Samples s, double time, double len)
|
||||
{
|
||||
// printf("%f, %f\n", time, len);
|
||||
auto& music = s;
|
||||
|
||||
// Check if the number of frames is non-zero.
|
||||
int firstFrame = max(0, (int)(time * music.getFrequency()));
|
||||
int numFrames = max(0, (int)(len * music.getFrequency()));
|
||||
numFrames = min(numFrames, music.getNumFrames() - firstFrame);
|
||||
if (numFrames <= 0)
|
||||
{
|
||||
PrintOut("There is no audio selected to perform BPM detection on.");
|
||||
return;
|
||||
}
|
||||
|
||||
this->myData = SerializedTempo();
|
||||
|
||||
// myData.terminate = &myTerminateFlag;
|
||||
|
||||
myData.numThreads = ParallelThreads::concurrency();
|
||||
myData.numFrames = numFrames;
|
||||
myData.samplerate = music.getFrequency();
|
||||
|
||||
myData.samples = AlignedMalloc(float, numFrames);
|
||||
if (!myData.samples)
|
||||
{
|
||||
PrintErr("Something bad happened...");
|
||||
return;
|
||||
}
|
||||
|
||||
// Copy the input samples.
|
||||
const short* l = music.samplesL() + firstFrame;
|
||||
const short* r = music.samplesR() + firstFrame;
|
||||
for (int i = 0; i < numFrames; ++i, ++l, ++r)
|
||||
{
|
||||
myData.samples[i] = (float)((int)*l + (int)*r) / 65536.0f;
|
||||
}
|
||||
}
|
||||
|
||||
TempoDetector::TempoDetector(Vector<Onset>* onsets, int sr)
|
||||
{
|
||||
this->myData = SerializedTempo();
|
||||
|
||||
myData.numThreads = 1;
|
||||
myData.onsets = onsets;
|
||||
myData.samples = nullptr;
|
||||
myData.samplerate = sr;
|
||||
}
|
||||
|
||||
TempoDetector::~TempoDetector()
|
||||
{
|
||||
if (myData.samples != nullptr)
|
||||
AlignedFree(myData.samples);
|
||||
}
|
||||
|
||||
void TempoDetector::exec()
|
||||
{
|
||||
SerializedTempo* data = &myData;
|
||||
|
||||
if (data->onsets != nullptr) {
|
||||
//printf("%d\n", data->onsets->size());
|
||||
CalculateBPM(data, data->onsets->data(), data->onsets->size());
|
||||
MarkProgress(4, "Find BPM");
|
||||
return;
|
||||
}
|
||||
|
||||
// Run the aubio onset tracker to find note onsets.
|
||||
Vector<Onset> onsets;
|
||||
FindOnsets(data->samples, data->samplerate, data->numFrames, 1, &onsets);
|
||||
|
||||
MarkProgress(1, "Find onsets");
|
||||
|
||||
// Calculate the strength of each onset.
|
||||
/*
|
||||
for (int i = 0; i < min(onsets.size(), 100); ++i)
|
||||
{
|
||||
int a = max(0, onsets[i].pos - 100);
|
||||
int b = min(data->numFrames, onsets[i].pos + 100);
|
||||
float v = 0.0f;
|
||||
for (int j = a; j < b; ++j)
|
||||
{
|
||||
v += abs(data->samples[j]);
|
||||
}
|
||||
v /= (float)max(1, b - a);
|
||||
onsets[i].strength = v;
|
||||
}
|
||||
*/
|
||||
|
||||
// Find BPM values.
|
||||
CalculateBPM(data, onsets.data(), onsets.size());
|
||||
MarkProgress(4, "Find BPM");
|
||||
|
||||
// Find offset values.
|
||||
CalculateOffset(data, onsets.data(), onsets.size());
|
||||
MarkProgress(5, "Find offsets");
|
||||
}
|
||||
|
||||
}; // namespace find_tempo_cpp
|
||||
using namespace find_tempo_cpp;
|
||||
|
||||
Samples::Samples(aubio_source_t* aud, int hop_size) {
|
||||
fmat_t* buf = new_fmat(2, hop_size); // create matrix
|
||||
unsigned int read = 0; // bytes read each loop
|
||||
unsigned int total = 0; // total bytes read
|
||||
|
||||
// set class variables
|
||||
frame_len = aubio_source_get_duration(aud);
|
||||
// printf("%d\n", frame_len);
|
||||
freq = aubio_source_get_samplerate(aud);
|
||||
// printf("%d\n", freq);
|
||||
|
||||
// split matrix into short arrays, left[] and right[]
|
||||
left = AlignedMalloc(short, frame_len);
|
||||
right = AlignedMalloc(short, frame_len);
|
||||
// mono = AlignedMalloc(float, frame_len);
|
||||
|
||||
int leftchan = 0;
|
||||
int rightchan = 1;
|
||||
do {
|
||||
// printf("%d\n", total);
|
||||
aubio_source_do_multi(aud, buf, &read);
|
||||
for (int i = 0; i < read; i++) {
|
||||
left[total+i] = (short)(buf->data[leftchan][i] * 32767); // float -> short, may have to change based on what the actual range is
|
||||
right[total+i] = (short)(buf->data[rightchan][i] * 32767);
|
||||
// mono[i] = buf->data[leftchan][total+i] + buf->data[rightchan][total+i];
|
||||
}
|
||||
total += read;
|
||||
} while (read == hop_size);
|
||||
|
||||
del_fmat(buf);
|
||||
}
|
||||
|
||||
// Samples::~Samples() {
|
||||
// AlignedFree(left);
|
||||
// AlignedFree(right);
|
||||
// }
|
||||
|
||||
void get_tempo(double* onsetsPtr, long freq) {
|
||||
|
||||
}
|
||||
|
||||
}; // namespace vortex
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
using namespace vortex;
|
||||
|
||||
if (argc < 2) {
|
||||
PrintErr("no arguments found");
|
||||
PrintErr("usage: " + string(argv[0]) + " <source_path> [start=0.0] [duration=60.0] [hop_size=256]\n");
|
||||
PrintErr("batch usage: " + string(argv[0]) + " --batch <source_folder> <output_csv>\n");
|
||||
return 2;
|
||||
}
|
||||
|
||||
if (string(argv[1]) == "--batch") {
|
||||
string pathname = string(argv[2]);
|
||||
|
||||
ofstream outfile;
|
||||
outfile.open(string(argv[3]));
|
||||
|
||||
for (const auto& entry : fs::directory_iterator(pathname)) {
|
||||
string fn = entry.path().string();
|
||||
string id = fn.substr(0, '.');
|
||||
|
||||
string artist;
|
||||
string title;
|
||||
int samplerate;
|
||||
string method;
|
||||
|
||||
ifstream input(fn);
|
||||
Vector<Onset> onsets;
|
||||
|
||||
if (input) {
|
||||
getline(input, artist);
|
||||
//printf("%s\n", artist.c_str());
|
||||
getline(input, title);
|
||||
//printf("%s\n", title.c_str());
|
||||
input >> samplerate;
|
||||
getline(input, method);
|
||||
getline(input, method);
|
||||
//printf("%s\n", method.c_str());
|
||||
double d;
|
||||
while (input >> d) {
|
||||
//printf("%f\n", d);
|
||||
onsets.append(Onset((int)d, 1.0));
|
||||
}
|
||||
} else { PrintErr("This text file is empty?"); continue; }
|
||||
|
||||
PrintOut(artist);
|
||||
PrintOut(title);
|
||||
|
||||
TempoDetector tempo = TempoDetector(&onsets, samplerate);
|
||||
tempo.exec();
|
||||
|
||||
TempoResults results = tempo.getResult();
|
||||
// loop through results, output to text file as csv
|
||||
int i = 0;
|
||||
#define QUOTE(A) "\"" + A + "\""
|
||||
#define DELIM ","
|
||||
// fix broken csv with regex: (\D|\d{1,4})\x0D, replace with \1
|
||||
// there are also broken quotes when the actual song title has double quotes so uhhh
|
||||
// (^.+?,)("[^,"]*)("[^,]*)(")([^,"]*") replace with \1\2\\\3\\\4\5
|
||||
// EXCEPT IT'S DOUBLE DOUBLE QUOTES NOT BACKSLASH QUOTE IN CSV, WHAT
|
||||
// fix the replace regex to be better; right now you can just replace \" with "" after the above regex replacement
|
||||
for (TempoResult result : results) {
|
||||
PrintStream(outfile, QUOTE(artist) + DELIM + QUOTE(title) + DELIM + QUOTE(id) + DELIM + QUOTE(method) + DELIM + to_string(++i) + DELIM + to_string(result.bpm) + DELIM + to_string(result.offset) + DELIM + to_string(result.fitness));
|
||||
}
|
||||
outfile.flush();
|
||||
onsets.clear();
|
||||
}
|
||||
|
||||
outfile.close();
|
||||
|
||||
} else {
|
||||
// load audio in
|
||||
unsigned int samplerate = 0;
|
||||
unsigned int win_s = 1024; // window size
|
||||
unsigned int hop_size = win_s / 4;
|
||||
double start = 0.0;
|
||||
double duration = 60.0;
|
||||
|
||||
char* source_path = argv[1];
|
||||
if (argc >= 3) start = atof(argv[2]);
|
||||
if (argc >= 4) duration = atof(argv[3]);
|
||||
if (argc >= 5) hop_size = atoi(argv[4]);
|
||||
|
||||
aubio_source_t* s = new_aubio_source(source_path, samplerate, hop_size);
|
||||
if (!s) {
|
||||
PrintErr("Source is not valid, exiting...");
|
||||
del_aubio_source(s);
|
||||
aubio_cleanup();
|
||||
return 1;
|
||||
}
|
||||
|
||||
Samples music = Samples(s, hop_size);
|
||||
MarkProgress(0, "Read audio samples");
|
||||
|
||||
// add Samples to function arguments
|
||||
TempoDetector tempo = TempoDetector(music, start, min(duration, (double)aubio_source_get_duration(s) / samplerate));
|
||||
tempo.exec();
|
||||
|
||||
TempoResults results = tempo.getResult();
|
||||
// loop through results, print result.bpm, result.offset and result.fitness
|
||||
for (TempoResult result : results)
|
||||
PrintOut("[RESULT] " + to_string(result.bpm) + " BPM, offset @ " + to_string(result.offset) + " sec, fitness " + to_string(result.fitness));
|
||||
// this function is bad and should be fixed
|
||||
|
||||
// delete tempo;
|
||||
// delete music;
|
||||
|
||||
del_aubio_source(s);
|
||||
aubio_cleanup();
|
||||
}
|
||||
return 0;
|
||||
}
|
@ -1,173 +0,0 @@
|
||||
#include <algorithm>
|
||||
#include <functional>
|
||||
#include <atomic>
|
||||
#include <iostream>
|
||||
#include <math.h>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <experimental/filesystem>
|
||||
#include <vector>
|
||||
#include <queue>
|
||||
#include <thread>
|
||||
#include <cassert>
|
||||
|
||||
#include <aubio/aubio.h>
|
||||
#include "polyfit.h"
|
||||
#include "Polyfit/PolyfitBoost.hpp"
|
||||
|
||||
#ifdef _WIN32
|
||||
#define AlignedMalloc(type, count) ((type*)_aligned_malloc((count) * sizeof(type), 16))
|
||||
#define AlignedFree(ptr) if (ptr){_aligned_free(ptr); ptr = nullptr;}
|
||||
#else
|
||||
#define AlignedMalloc(type, count) ((type*)aligned_alloc(16, (count) * sizeof(type)))
|
||||
#define AlignedFree(ptr) if (ptr){free(ptr); ptr = nullptr;}
|
||||
#endif
|
||||
|
||||
#define DEFAULT_THREAD_COUNT 2
|
||||
// #define MarkProgress(number, text) { if (*data->terminate) {return;} data->progress = number; }
|
||||
|
||||
namespace fs = std::experimental::filesystem;
|
||||
|
||||
typedef double real_t;
|
||||
|
||||
namespace vortex {
|
||||
|
||||
using namespace std;
|
||||
|
||||
void PrintOut(string s) {
|
||||
cout << s << endl;
|
||||
}
|
||||
|
||||
void MarkProgress(int step, string s) {
|
||||
cout << "[" << step << "] " << s << endl;
|
||||
}
|
||||
|
||||
void PrintErr(string s) {
|
||||
cerr << "[ERROR] " << s << endl;
|
||||
}
|
||||
|
||||
void PrintStream(ofstream& m, string s) {
|
||||
m << s << endl;
|
||||
}
|
||||
|
||||
struct vec2i {
|
||||
int x;
|
||||
int y;
|
||||
};
|
||||
|
||||
struct Onset {
|
||||
Onset(int);
|
||||
Onset(int, float);
|
||||
Onset() : pos(0), strength(0.0f) {}
|
||||
|
||||
int pos;
|
||||
float strength;
|
||||
|
||||
operator std::string() const
|
||||
{
|
||||
return "Onset(" + to_string(pos) + ", " + to_string(strength) + ")";
|
||||
}
|
||||
};
|
||||
|
||||
Onset::Onset(int i) : pos{i} {}
|
||||
Onset::Onset(int i, float s) : pos{i}, strength{s} {}
|
||||
|
||||
|
||||
template <typename T>
|
||||
class Vector : public vector<T> {
|
||||
public:
|
||||
// T &operator[](int i) {}
|
||||
void append(T obj) { this->vector<T>::push_back(obj); }
|
||||
void append(float a, float b, float c) { this->vector<T>::push_back({a, b, c}); }
|
||||
void append(double a, double b, double c) { this->vector<T>::push_back({static_cast<float>(a), static_cast<float>(b), static_cast<float>(c)}); }
|
||||
void erase(int i) { this->vector<T>::erase(this->vector<T>::begin() + i); }
|
||||
int size() { return (int)this->vector<T>::size(); }
|
||||
typename vector<T>::iterator begin() { return this->vector<T>::begin(); }
|
||||
};
|
||||
|
||||
class ParallelThreads {
|
||||
private:
|
||||
queue<int>* instanceQ; // contains the instances to be executed
|
||||
bool* statusList; // contains an index per thread with a bool indicating its status (false = waiting, true = in progress)
|
||||
|
||||
public:
|
||||
virtual void execute(int i, int t) {}; // t = thread
|
||||
|
||||
thread* _execute(int i, int t) {
|
||||
return new thread([this] (int i, int t) { execute(i, t); statusList[t] = false; }, i, t);
|
||||
}
|
||||
|
||||
void run(int numInstances, int maxThreads) {
|
||||
if (numInstances <= 0 || maxThreads <= 0) return;
|
||||
|
||||
// initialize bool per thread and a queue for every instance needed to run
|
||||
statusList = AlignedMalloc(bool, maxThreads);
|
||||
instanceQ = new queue<int>();
|
||||
|
||||
for (int i = 0; i < maxThreads; i++)
|
||||
statusList[i] = false;
|
||||
for (int i = 0; i < numInstances; i++)
|
||||
instanceQ->push(i);
|
||||
|
||||
// this code currently causes memory leaks, instead of having a status list (or WITH a status list) maybe have a pointer list instead so we can join everything
|
||||
|
||||
thread* tp; // thread pointer
|
||||
uint8_t t = 0; // current thread
|
||||
while (!instanceQ->empty()) {
|
||||
if (!statusList[t]) {
|
||||
statusList[t] = true;
|
||||
// printf("%d @ thread %d started\n", instanceQ->front(), t);
|
||||
tp = _execute(instanceQ->front(), t);
|
||||
instanceQ->pop();
|
||||
}
|
||||
t = (t + 1) % maxThreads;
|
||||
}
|
||||
|
||||
// wait for threads to finish
|
||||
int finished = 0; // number of finished threads
|
||||
while (finished < maxThreads) {
|
||||
finished = (statusList[t] ? 0 : finished + 1);
|
||||
t = (t + 1) % maxThreads;
|
||||
}
|
||||
|
||||
AlignedFree(statusList);
|
||||
delete instanceQ;
|
||||
}
|
||||
|
||||
static int concurrency() {
|
||||
int t = (int)std::thread::hardware_concurrency();
|
||||
return t > 0 ? t : DEFAULT_THREAD_COUNT;
|
||||
}
|
||||
};
|
||||
|
||||
class Samples {
|
||||
public:
|
||||
Samples(aubio_source_t* aud, int hop_size);
|
||||
const short* samplesL() {
|
||||
return left;
|
||||
}
|
||||
const short* samplesR() {
|
||||
return right;
|
||||
}
|
||||
/* const float* samplesFloat() {
|
||||
return mono;
|
||||
} */
|
||||
int getNumFrames() const {
|
||||
return frame_len;
|
||||
}
|
||||
int getFrequency() const {
|
||||
return freq;
|
||||
}
|
||||
|
||||
private:
|
||||
short* left;
|
||||
short* right;
|
||||
// float* mono;
|
||||
int frame_len;
|
||||
int freq;
|
||||
};
|
||||
|
||||
// destructor needs del_aubio_onset (o);
|
||||
// del_fvec (buf);
|
||||
|
||||
}
|
@ -1,31 +1,29 @@
|
||||
#include "editor_state.hpp"
|
||||
|
||||
#include <SFML/Audio/Music.hpp>
|
||||
#include <SFML/Audio/SoundSource.hpp>
|
||||
#include <SFML/Audio/SoundStream.hpp>
|
||||
#include <SFML/System/Vector2.hpp>
|
||||
#include <algorithm>
|
||||
#include <cfloat>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
|
||||
#include <fmt/core.h>
|
||||
#include <future>
|
||||
#include <imgui-SFML.h>
|
||||
#include <imgui.h>
|
||||
#include <imgui_internal.h>
|
||||
#include <imgui_stdlib.h>
|
||||
#include <initializer_list>
|
||||
#include <memory>
|
||||
#include <nowide/fstream.hpp>
|
||||
#include <sstream>
|
||||
#include <SFML/System/Time.hpp>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <tinyfiledialogs.h>
|
||||
#include <variant>
|
||||
|
||||
#include <fmt/core.h>
|
||||
#include <imgui.h>
|
||||
#include <imgui-SFML.h>
|
||||
#include <imgui_internal.h>
|
||||
#include <imgui_stdlib.h>
|
||||
#include <nowide/fstream.hpp>
|
||||
#include <SFML/Audio/Music.hpp>
|
||||
#include <SFML/Audio/SoundSource.hpp>
|
||||
#include <SFML/Audio/SoundStream.hpp>
|
||||
#include <SFML/System/Time.hpp>
|
||||
#include <SFML/System/Vector2.hpp>
|
||||
#include <tinyfiledialogs.h>
|
||||
|
||||
#include "better_note.hpp"
|
||||
#include "better_song.hpp"
|
||||
#include "chart_state.hpp"
|
||||
@ -38,9 +36,9 @@
|
||||
#include "long_note_dummy.hpp"
|
||||
#include "notifications_queue.hpp"
|
||||
#include "special_numeric_types.hpp"
|
||||
#include "src/better_metadata.hpp"
|
||||
#include "src/better_timing.hpp"
|
||||
#include "src/custom_sfml_audio/synced_sound_streams.hpp"
|
||||
#include "better_metadata.hpp"
|
||||
#include "better_timing.hpp"
|
||||
#include "custom_sfml_audio/synced_sound_streams.hpp"
|
||||
#include "utf8_sfml_redefinitions.hpp"
|
||||
#include "variant_visitor.hpp"
|
||||
#include "utf8_strings.hpp"
|
||||
@ -1144,34 +1142,57 @@ void EditorState::display_history() {
|
||||
history.display(show_history);
|
||||
}
|
||||
|
||||
void EditorState::display_timing_menu() {
|
||||
if (ImGui::Begin("Adjust Timing", &show_timing_menu)) {
|
||||
void EditorState::display_sync_menu() {
|
||||
if (ImGui::Begin("Adjust Sync", &show_sync_menu, ImGuiWindowFlags_AlwaysAutoResize)) {
|
||||
auto bpm = std::visit(
|
||||
[&](const auto& pos){return applicable_timing->bpm_at(pos);},
|
||||
playback_position
|
||||
);
|
||||
if (feis::InputDecimal("BPM", &bpm, ImGuiInputTextFlags_EnterReturnsTrue)) {
|
||||
ImGui::PushItemWidth(-70.0f);
|
||||
if (feis::InputDecimal("Initial BPM", &bpm, ImGuiInputTextFlags_EnterReturnsTrue)) {
|
||||
if (bpm > 0) {
|
||||
const auto before = *applicable_timing;
|
||||
applicable_timing->insert(better::BPMAtBeat{bpm, current_snaped_beats()});
|
||||
if (*applicable_timing != before) {
|
||||
reload_sounds_that_depend_on_timing();
|
||||
history.push(std::make_shared<ChangeTiming>(before, *applicable_timing, timing_origin()));
|
||||
}
|
||||
if (chart_state) {
|
||||
chart_state->density_graph.should_recompute = true;
|
||||
}
|
||||
auto new_timing = *applicable_timing;
|
||||
new_timing.insert(better::BPMAtBeat{bpm, current_snaped_beats()});
|
||||
replace_applicable_timing_with(new_timing);
|
||||
}
|
||||
}
|
||||
auto offset = applicable_timing->get_offset();
|
||||
if (feis::InputDecimal("beat zero offset", &offset, ImGuiInputTextFlags_EnterReturnsTrue)) {
|
||||
applicable_timing->set_offset(offset);
|
||||
reload_sounds_that_depend_on_timing();
|
||||
if (feis::InputDecimal("Offset", &offset, ImGuiInputTextFlags_EnterReturnsTrue)) {
|
||||
auto new_timing = *applicable_timing;
|
||||
new_timing.set_offset(offset);
|
||||
replace_applicable_timing_with(new_timing);
|
||||
set_playback_position(current_exact_beats());
|
||||
if (chart_state) {
|
||||
chart_state->density_graph.should_recompute = true;
|
||||
}
|
||||
reload_editable_range();
|
||||
}
|
||||
ImGui::PopItemWidth();
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted("Move first beat");
|
||||
ImGui::SameLine();
|
||||
const auto beat_duration = [](const better::Timing& t){return 60 / t.bpm_at(Fraction{0});};
|
||||
if (ImGui::Button("-1")) {
|
||||
auto new_timing = *applicable_timing;
|
||||
new_timing.set_offset(new_timing.get_offset() - beat_duration(new_timing));
|
||||
replace_applicable_timing_with(new_timing);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("-0.5")) {
|
||||
auto new_timing = *applicable_timing;
|
||||
const auto beat = 60 * new_timing.bpm_at(Fraction{0});
|
||||
new_timing.set_offset(new_timing.get_offset() - beat_duration(new_timing) / 2);
|
||||
replace_applicable_timing_with(new_timing);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("+0.5")) {
|
||||
auto new_timing = *applicable_timing;
|
||||
const auto beat = 60 * new_timing.bpm_at(Fraction{0});
|
||||
new_timing.set_offset(new_timing.get_offset() + beat_duration(new_timing) / 2);
|
||||
replace_applicable_timing_with(new_timing);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("+1")) {
|
||||
auto new_timing = *applicable_timing;
|
||||
const auto beat = 60 * new_timing.bpm_at(Fraction{0});
|
||||
new_timing.set_offset(new_timing.get_offset() + beat_duration(new_timing));
|
||||
replace_applicable_timing_with(new_timing);
|
||||
}
|
||||
ImGui::Separator();
|
||||
feis::CenteredText("Automatic BPM Detection");
|
||||
@ -1179,33 +1200,74 @@ void EditorState::display_timing_menu() {
|
||||
ImGui::BeginDisabled();
|
||||
}
|
||||
bool loading = tempo_candidates_loader.valid();
|
||||
if (loading) {
|
||||
ImGui::BeginDisabled();
|
||||
static std::optional<TempoCandidate> selected_tempo_candidate;
|
||||
if (ImGui::BeginChild("Candidate Child Window", ImVec2(0, 100), true, ImGuiWindowFlags_NoScrollbar)) {
|
||||
if (not tempo_candidates) {
|
||||
if (loading) {
|
||||
static std::size_t frame_counter = 0;
|
||||
frame_counter += 1;
|
||||
frame_counter %= 4*5;
|
||||
feis::CenteredText("Loading");
|
||||
ImGui::SameLine();
|
||||
ImGui::TextUnformatted(fmt::format("{:.>{}}", "", frame_counter / 5).c_str());
|
||||
}
|
||||
} else {
|
||||
if(
|
||||
ImGui::BeginTable(
|
||||
"Candidates",
|
||||
3,
|
||||
ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg
|
||||
)
|
||||
) {
|
||||
ImGui::TableSetupColumn("BPM");
|
||||
ImGui::TableSetupColumn("Offset");
|
||||
ImGui::TableSetupColumn("Fitness");
|
||||
ImGui::TableHeadersRow();
|
||||
std::for_each(tempo_candidates->crbegin(), tempo_candidates->crend(), [&](const auto& candidate){
|
||||
ImGui::TableNextRow();
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::PushID(&candidate);
|
||||
const auto label = fmt::format("{:.3f}##", static_cast<float>(candidate.bpm));
|
||||
const bool is_selected = selected_tempo_candidate.has_value() and *selected_tempo_candidate == candidate;
|
||||
if (ImGui::Selectable(label.c_str(), is_selected, ImGuiSelectableFlags_SpanAllColumns)) {
|
||||
selected_tempo_candidate = candidate;
|
||||
};
|
||||
ImGui::PopID();
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::TextUnformatted(fmt::format("{:.3f}s", static_cast<float>(candidate.offset_seconds)).c_str());
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::TextUnformatted(fmt::format("{:.2f}", candidate.fitness).c_str());
|
||||
});
|
||||
ImGui::EndTable();
|
||||
}
|
||||
}
|
||||
ImGui::EndChild();
|
||||
}
|
||||
if (ImGui::Button("Guess BPM")) {
|
||||
if (ImGui::Button("Guess BPM", {ImGui::GetContentRegionAvail().x * 0.5f, 0.0f})) {
|
||||
const auto path = full_audio_path();
|
||||
tempo_candidates.reset();
|
||||
if (path.has_value()) {
|
||||
tempo_candidates_loader = std::async(std::launch::async, guess_tempo, *path);
|
||||
}
|
||||
}
|
||||
if (loading) {
|
||||
ImGui::EndDisabled();
|
||||
ImGui::SameLine();
|
||||
static std::size_t frame_counter = 0;
|
||||
frame_counter += 1;
|
||||
frame_counter %= 4*5;
|
||||
ImGui::TextUnformatted(fmt::format("Loading{:.>{}}", "", frame_counter / 5).c_str());
|
||||
|
||||
ImGui::SameLine();
|
||||
const bool tempo_candidate_was_selected_before_pressing = selected_tempo_candidate.has_value();
|
||||
if (not tempo_candidate_was_selected_before_pressing) {
|
||||
ImGui::BeginDisabled();
|
||||
}
|
||||
if (tempo_candidates) {
|
||||
for (const auto& candidate: *tempo_candidates) {
|
||||
ImGui::TextUnformatted(fmt::format(
|
||||
"{:.3f} BPM @ {:.2f}s",
|
||||
static_cast<float>(candidate.bpm),
|
||||
static_cast<float>(candidate.offset_seconds)
|
||||
).c_str());
|
||||
if (ImGui::Button("Apply BPM", {-std::numeric_limits<float>::min(), 0.0f})) {
|
||||
if (selected_tempo_candidate) {
|
||||
const auto new_timing = better::Timing{
|
||||
{{convert_to_decimal(selected_tempo_candidate->bpm, 3), 0}},
|
||||
convert_to_decimal(selected_tempo_candidate->offset_seconds, 3)
|
||||
};
|
||||
replace_applicable_timing_with(new_timing);
|
||||
selected_tempo_candidate.reset();
|
||||
}
|
||||
}
|
||||
if (not tempo_candidate_was_selected_before_pressing) {
|
||||
ImGui::EndDisabled();
|
||||
}
|
||||
if (not music.has_value()) {
|
||||
ImGui::EndDisabled();
|
||||
}
|
||||
@ -1438,6 +1500,19 @@ void EditorState::frame_hook() {
|
||||
}
|
||||
}
|
||||
|
||||
void EditorState::replace_applicable_timing_with(const better::Timing& new_timing) {
|
||||
const auto before = *applicable_timing;
|
||||
applicable_timing = std::make_shared<better::Timing>(new_timing);
|
||||
if (*applicable_timing != before) {
|
||||
reload_sounds_that_depend_on_timing();
|
||||
reload_editable_range();
|
||||
history.push(std::make_shared<ChangeTiming>(before, *applicable_timing, timing_origin()));
|
||||
}
|
||||
if (chart_state) {
|
||||
chart_state->density_graph.should_recompute = true;
|
||||
}
|
||||
}
|
||||
|
||||
Interval<sf::Time> EditorState::choose_editable_range() {
|
||||
Interval<sf::Time> new_range{sf::Time::Zero, sf::Time::Zero};
|
||||
// In all cases, allow editing from beat zero (which might be at a negative
|
||||
|
@ -176,8 +176,8 @@ public:
|
||||
bool show_new_chart_dialog = false;
|
||||
bool show_chart_properties = false;
|
||||
|
||||
bool show_timing_menu = false;
|
||||
void display_timing_menu();
|
||||
bool show_sync_menu = false;
|
||||
void display_sync_menu();
|
||||
|
||||
enum class SaveOutcome {
|
||||
UserSaved,
|
||||
@ -246,6 +246,8 @@ public:
|
||||
|
||||
void frame_hook();
|
||||
|
||||
void replace_applicable_timing_with(const better::Timing& new_timing);
|
||||
|
||||
private:
|
||||
|
||||
int volume = 10; // 0 -> 10
|
||||
|
@ -32,6 +32,8 @@ struct TempoCandidate {
|
||||
Fraction bpm;
|
||||
Fraction offset_seconds;
|
||||
float fitness;
|
||||
|
||||
auto operator<=>(const TempoCandidate&) const = default;
|
||||
};
|
||||
|
||||
std::vector<TempoCandidate> guess_tempo(const std::filesystem::path& audio);
|
||||
|
@ -516,8 +516,8 @@ int main() {
|
||||
if (editor_state->show_editor_settings) {
|
||||
editor_state->display_editor_settings();
|
||||
}
|
||||
if (editor_state->show_timing_menu) {
|
||||
editor_state->display_timing_menu();
|
||||
if (editor_state->show_sync_menu) {
|
||||
editor_state->display_sync_menu();
|
||||
}
|
||||
} else {
|
||||
bg.render(window);
|
||||
@ -641,8 +641,8 @@ int main() {
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
if (ImGui::BeginMenu("Timing", editor_state.has_value())) {
|
||||
if (ImGui::MenuItem("Adjust Timing")) {
|
||||
editor_state->show_timing_menu = true;
|
||||
if (ImGui::MenuItem("Adjust Sync")) {
|
||||
editor_state->show_sync_menu = true;
|
||||
}
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user