Prettier Sync Window

This commit is contained in:
Stepland 2023-07-08 03:08:58 +02:00
parent 8219aad65a
commit a6f0c6e917
6 changed files with 141 additions and 1133 deletions

View File

@ -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;
}

View File

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

View File

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

View File

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

View File

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

View File

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