diff --git a/src/FindTempo_standalone.cpp b/src/FindTempo_standalone.cpp deleted file mode 100644 index 754de8a..0000000 --- a/src/FindTempo_standalone.cpp +++ /dev/null @@ -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* 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 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* 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* 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& 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* 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 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]) + " [start=0.0] [duration=60.0] [hop_size=256]\n"); - PrintErr("batch usage: " + string(argv[0]) + " --batch \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 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; -} \ No newline at end of file diff --git a/src/FindTempo_standalone.hpp b/src/FindTempo_standalone.hpp deleted file mode 100644 index 5d254fe..0000000 --- a/src/FindTempo_standalone.hpp +++ /dev/null @@ -1,173 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#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 -class Vector : public vector { - public: - // T &operator[](int i) {} - void append(T obj) { this->vector::push_back(obj); } - void append(float a, float b, float c) { this->vector::push_back({a, b, c}); } - void append(double a, double b, double c) { this->vector::push_back({static_cast(a), static_cast(b), static_cast(c)}); } - void erase(int i) { this->vector::erase(this->vector::begin() + i); } - int size() { return (int)this->vector::size(); } - typename vector::iterator begin() { return this->vector::begin(); } -}; - -class ParallelThreads { - private: - queue* 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(); - - 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); - -} \ No newline at end of file diff --git a/src/editor_state.cpp b/src/editor_state.cpp index ac2b9a3..607e09f 100644 --- a/src/editor_state.cpp +++ b/src/editor_state.cpp @@ -1,31 +1,29 @@ #include "editor_state.hpp" -#include -#include -#include -#include #include +#include #include #include #include #include - -#include #include -#include -#include -#include -#include #include #include -#include -#include -#include -#include -#include -#include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + #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(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 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(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(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(candidate.bpm), - static_cast(candidate.offset_seconds) - ).c_str()); + if (ImGui::Button("Apply BPM", {-std::numeric_limits::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(new_timing); + if (*applicable_timing != before) { + reload_sounds_that_depend_on_timing(); + reload_editable_range(); + history.push(std::make_shared(before, *applicable_timing, timing_origin())); + } + if (chart_state) { + chart_state->density_graph.should_recompute = true; + } +} + Interval EditorState::choose_editable_range() { Interval new_range{sf::Time::Zero, sf::Time::Zero}; // In all cases, allow editing from beat zero (which might be at a negative diff --git a/src/editor_state.hpp b/src/editor_state.hpp index f2b41f5..4786501 100644 --- a/src/editor_state.hpp +++ b/src/editor_state.hpp @@ -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 diff --git a/src/guess_tempo.hpp b/src/guess_tempo.hpp index 494a957..c1d025d 100644 --- a/src/guess_tempo.hpp +++ b/src/guess_tempo.hpp @@ -32,6 +32,8 @@ struct TempoCandidate { Fraction bpm; Fraction offset_seconds; float fitness; + + auto operator<=>(const TempoCandidate&) const = default; }; std::vector guess_tempo(const std::filesystem::path& audio); diff --git a/src/main.cpp b/src/main.cpp index 98e67c8..ae91f2b 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -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(); }