mirror of
				https://github.com/RetroDECK/Duckstation.git
				synced 2025-04-10 19:15:14 +00:00 
			
		
		
		
	
		
			
	
	
		
			574 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
		
		
			
		
	
	
			574 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
|   | ////////////////////////////////////////////////////////////////////////////////
 | ||
|  | ///
 | ||
|  | /// Beats-per-minute (BPM) detection routine.
 | ||
|  | ///
 | ||
|  | /// The beat detection algorithm works as follows:
 | ||
|  | /// - Use function 'inputSamples' to input a chunks of samples to the class for
 | ||
|  | ///   analysis. It's a good idea to enter a large sound file or stream in smallish
 | ||
|  | ///   chunks of around few kilosamples in order not to extinguish too much RAM memory.
 | ||
|  | /// - Inputted sound data is decimated to approx 500 Hz to reduce calculation burden,
 | ||
|  | ///   which is basically ok as low (bass) frequencies mostly determine the beat rate.
 | ||
|  | ///   Simple averaging is used for anti-alias filtering because the resulting signal
 | ||
|  | ///   quality isn't of that high importance.
 | ||
|  | /// - Decimated sound data is enveloped, i.e. the amplitude shape is detected by
 | ||
|  | ///   taking absolute value that's smoothed by sliding average. Signal levels that
 | ||
|  | ///   are below a couple of times the general RMS amplitude level are cut away to
 | ||
|  | ///   leave only notable peaks there.
 | ||
|  | /// - Repeating sound patterns (e.g. beats) are detected by calculating short-term 
 | ||
|  | ///   autocorrelation function of the enveloped signal.
 | ||
|  | /// - After whole sound data file has been analyzed as above, the bpm level is 
 | ||
|  | ///   detected by function 'getBpm' that finds the highest peak of the autocorrelation 
 | ||
|  | ///   function, calculates it's precise location and converts this reading to bpm's.
 | ||
|  | ///
 | ||
|  | /// Author        : Copyright (c) Olli Parviainen
 | ||
|  | /// Author e-mail : oparviai 'at' iki.fi
 | ||
|  | /// SoundTouch WWW: http://www.surina.net/soundtouch
 | ||
|  | ///
 | ||
|  | ////////////////////////////////////////////////////////////////////////////////
 | ||
|  | //
 | ||
|  | // License :
 | ||
|  | //
 | ||
|  | //  SoundTouch audio processing library
 | ||
|  | //  Copyright (c) Olli Parviainen
 | ||
|  | //
 | ||
|  | //  This library is free software; you can redistribute it and/or
 | ||
|  | //  modify it under the terms of the GNU Lesser General Public
 | ||
|  | //  License as published by the Free Software Foundation; either
 | ||
|  | //  version 2.1 of the License, or (at your option) any later version.
 | ||
|  | //
 | ||
|  | //  This library is distributed in the hope that it will be useful,
 | ||
|  | //  but WITHOUT ANY WARRANTY; without even the implied warranty of
 | ||
|  | //  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 | ||
|  | //  Lesser General Public License for more details.
 | ||
|  | //
 | ||
|  | //  You should have received a copy of the GNU Lesser General Public
 | ||
|  | //  License along with this library; if not, write to the Free Software
 | ||
|  | //  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 | ||
|  | //
 | ||
|  | ////////////////////////////////////////////////////////////////////////////////
 | ||
|  | 
 | ||
|  | #define _USE_MATH_DEFINES
 | ||
|  | 
 | ||
|  | #include <math.h>
 | ||
|  | #include <assert.h>
 | ||
|  | #include <string.h>
 | ||
|  | #include <stdio.h>
 | ||
|  | #include <cfloat>
 | ||
|  | #include "FIFOSampleBuffer.h"
 | ||
|  | #include "PeakFinder.h"
 | ||
|  | #include "BPMDetect.h"
 | ||
|  | 
 | ||
|  | using namespace soundtouch; | ||
|  | 
 | ||
|  | // algorithm input sample block size
 | ||
|  | static const int INPUT_BLOCK_SIZE = 2048; | ||
|  | 
 | ||
|  | // decimated sample block size
 | ||
|  | static const int DECIMATED_BLOCK_SIZE = 256; | ||
|  | 
 | ||
|  | /// Target sample rate after decimation
 | ||
|  | static const int TARGET_SRATE = 1000; | ||
|  | 
 | ||
|  | /// XCorr update sequence size, update in about 200msec chunks
 | ||
|  | static const int XCORR_UPDATE_SEQUENCE = (int)(TARGET_SRATE / 5); | ||
|  | 
 | ||
|  | /// Moving average N size
 | ||
|  | static const int MOVING_AVERAGE_N = 15; | ||
|  | 
 | ||
|  | /// XCorr decay time constant, decay to half in 30 seconds
 | ||
|  | /// If it's desired to have the system adapt quicker to beat rate 
 | ||
|  | /// changes within a continuing music stream, then the 
 | ||
|  | /// 'xcorr_decay_time_constant' value can be reduced, yet that
 | ||
|  | /// can increase possibility of glitches in bpm detection.
 | ||
|  | static const double XCORR_DECAY_TIME_CONSTANT = 30.0; | ||
|  | 
 | ||
|  | /// Data overlap factor for beat detection algorithm
 | ||
|  | static const int OVERLAP_FACTOR = 4; | ||
|  | 
 | ||
|  | static const double TWOPI = (2 * M_PI); | ||
|  | 
 | ||
|  | ////////////////////////////////////////////////////////////////////////////////
 | ||
|  | 
 | ||
|  | // Enable following define to create bpm analysis file:
 | ||
|  | 
 | ||
|  | //#define _CREATE_BPM_DEBUG_FILE
 | ||
|  | 
 | ||
|  | #ifdef _CREATE_BPM_DEBUG_FILE
 | ||
|  | 
 | ||
|  |     static void _SaveDebugData(const char *name, const float *data, int minpos, int maxpos, double coeff) | ||
|  |     { | ||
|  |         FILE *fptr = fopen(name, "wt"); | ||
|  |         int i; | ||
|  | 
 | ||
|  |         if (fptr) | ||
|  |         { | ||
|  |             printf("\nWriting BPM debug data into file %s\n", name); | ||
|  |             for (i = minpos; i < maxpos; i ++) | ||
|  |             { | ||
|  |                 fprintf(fptr, "%d\t%.1lf\t%f\n", i, coeff / (double)i, data[i]); | ||
|  |             } | ||
|  |             fclose(fptr); | ||
|  |         } | ||
|  |     } | ||
|  | 
 | ||
|  |     void _SaveDebugBeatPos(const char *name, const std::vector<BEAT> &beats) | ||
|  |     { | ||
|  |         printf("\nWriting beat detections data into file %s\n", name); | ||
|  | 
 | ||
|  |         FILE *fptr = fopen(name, "wt"); | ||
|  |         if (fptr) | ||
|  |         { | ||
|  |             for (uint i = 0; i < beats.size(); i++) | ||
|  |             { | ||
|  |                 BEAT b = beats[i]; | ||
|  |                 fprintf(fptr, "%lf\t%lf\n", b.pos, b.strength); | ||
|  |             } | ||
|  |             fclose(fptr); | ||
|  |         } | ||
|  |     } | ||
|  | #else
 | ||
|  |     #define _SaveDebugData(name, a,b,c,d)
 | ||
|  |     #define _SaveDebugBeatPos(name, b)
 | ||
|  | #endif
 | ||
|  | 
 | ||
|  | // Hamming window
 | ||
|  | void hamming(float *w, int N) | ||
|  | { | ||
|  |     for (int i = 0; i < N; i++) | ||
|  |     { | ||
|  |         w[i] = (float)(0.54 - 0.46 * cos(TWOPI * i / (N - 1))); | ||
|  |     } | ||
|  | 
 | ||
|  | } | ||
|  | 
 | ||
|  | ////////////////////////////////////////////////////////////////////////////////
 | ||
|  | //
 | ||
|  | // IIR2_filter - 2nd order IIR filter
 | ||
|  | 
 | ||
|  | IIR2_filter::IIR2_filter(const double *lpf_coeffs) | ||
|  | { | ||
|  |     memcpy(coeffs, lpf_coeffs, 5 * sizeof(double)); | ||
|  |     memset(prev, 0, sizeof(prev)); | ||
|  | } | ||
|  | 
 | ||
|  | 
 | ||
|  | float IIR2_filter::update(float x) | ||
|  | { | ||
|  |     prev[0] = x; | ||
|  |     double y = x * coeffs[0]; | ||
|  | 
 | ||
|  |     for (int i = 4; i >= 1; i--) | ||
|  |     { | ||
|  |         y += coeffs[i] * prev[i]; | ||
|  |         prev[i] = prev[i - 1]; | ||
|  |     } | ||
|  | 
 | ||
|  |     prev[3] = y; | ||
|  |     return (float)y; | ||
|  | } | ||
|  | 
 | ||
|  | 
 | ||
|  | // IIR low-pass filter coefficients, calculated with matlab/octave cheby2(2,40,0.05)
 | ||
|  | const double _LPF_coeffs[5] = { 0.00996655391939, -0.01944529148401, 0.00996655391939, 1.96867605796247, -0.96916387431724 }; | ||
|  | 
 | ||
|  | ////////////////////////////////////////////////////////////////////////////////
 | ||
|  | 
 | ||
|  | BPMDetect::BPMDetect(int numChannels, int aSampleRate) : | ||
|  |     beat_lpf(_LPF_coeffs) | ||
|  | { | ||
|  |     beats.reserve(250); // initial reservation to prevent frequent reallocation
 | ||
|  | 
 | ||
|  |     this->sampleRate = aSampleRate; | ||
|  |     this->channels = numChannels; | ||
|  | 
 | ||
|  |     decimateSum = 0; | ||
|  |     decimateCount = 0; | ||
|  | 
 | ||
|  |     // choose decimation factor so that result is approx. 1000 Hz
 | ||
|  |     decimateBy = sampleRate / TARGET_SRATE; | ||
|  |     if ((decimateBy <= 0) || (decimateBy * DECIMATED_BLOCK_SIZE < INPUT_BLOCK_SIZE)) | ||
|  |     { | ||
|  |         ST_THROW_RT_ERROR("Too small samplerate"); | ||
|  |     } | ||
|  | 
 | ||
|  |     // Calculate window length & starting item according to desired min & max bpms
 | ||
|  |     windowLen = (60 * sampleRate) / (decimateBy * MIN_BPM); | ||
|  |     windowStart = (60 * sampleRate) / (decimateBy * MAX_BPM_RANGE); | ||
|  | 
 | ||
|  |     assert(windowLen > windowStart); | ||
|  | 
 | ||
|  |     // allocate new working objects
 | ||
|  |     xcorr = new float[windowLen]; | ||
|  |     memset(xcorr, 0, windowLen * sizeof(float)); | ||
|  | 
 | ||
|  |     pos = 0; | ||
|  |     peakPos = 0; | ||
|  |     peakVal = 0; | ||
|  |     init_scaler = 1; | ||
|  |     beatcorr_ringbuffpos = 0; | ||
|  |     beatcorr_ringbuff = new float[windowLen]; | ||
|  |     memset(beatcorr_ringbuff, 0, windowLen * sizeof(float)); | ||
|  | 
 | ||
|  |     // allocate processing buffer
 | ||
|  |     buffer = new FIFOSampleBuffer(); | ||
|  |     // we do processing in mono mode
 | ||
|  |     buffer->setChannels(1); | ||
|  |     buffer->clear(); | ||
|  | 
 | ||
|  |     // calculate hamming windows
 | ||
|  |     hamw = new float[XCORR_UPDATE_SEQUENCE]; | ||
|  |     hamming(hamw, XCORR_UPDATE_SEQUENCE); | ||
|  |     hamw2 = new float[XCORR_UPDATE_SEQUENCE / 2]; | ||
|  |     hamming(hamw2, XCORR_UPDATE_SEQUENCE / 2); | ||
|  | } | ||
|  | 
 | ||
|  | 
 | ||
|  | BPMDetect::~BPMDetect() | ||
|  | { | ||
|  |     delete[] xcorr; | ||
|  |     delete[] beatcorr_ringbuff; | ||
|  |     delete[] hamw; | ||
|  |     delete[] hamw2; | ||
|  |     delete buffer; | ||
|  | } | ||
|  | 
 | ||
|  | 
 | ||
|  | /// convert to mono, low-pass filter & decimate to about 500 Hz. 
 | ||
|  | /// return number of outputted samples.
 | ||
|  | ///
 | ||
|  | /// Decimation is used to remove the unnecessary frequencies and thus to reduce 
 | ||
|  | /// the amount of data needed to be processed as calculating autocorrelation 
 | ||
|  | /// function is a very-very heavy operation.
 | ||
|  | ///
 | ||
|  | /// Anti-alias filtering is done simply by averaging the samples. This is really a 
 | ||
|  | /// poor-man's anti-alias filtering, but it's not so critical in this kind of application
 | ||
|  | /// (it'd also be difficult to design a high-quality filter with steep cut-off at very 
 | ||
|  | /// narrow band)
 | ||
|  | int BPMDetect::decimate(SAMPLETYPE *dest, const SAMPLETYPE *src, int numsamples) | ||
|  | { | ||
|  |     int count, outcount; | ||
|  |     LONG_SAMPLETYPE out; | ||
|  | 
 | ||
|  |     assert(channels > 0); | ||
|  |     assert(decimateBy > 0); | ||
|  |     outcount = 0; | ||
|  |     for (count = 0; count < numsamples; count ++)  | ||
|  |     { | ||
|  |         int j; | ||
|  | 
 | ||
|  |         // convert to mono and accumulate
 | ||
|  |         for (j = 0; j < channels; j ++) | ||
|  |         { | ||
|  |             decimateSum += src[j]; | ||
|  |         } | ||
|  |         src += j; | ||
|  | 
 | ||
|  |         decimateCount ++; | ||
|  |         if (decimateCount >= decimateBy)  | ||
|  |         { | ||
|  |             // Store every Nth sample only
 | ||
|  |             out = (LONG_SAMPLETYPE)(decimateSum / (decimateBy * channels)); | ||
|  |             decimateSum = 0; | ||
|  |             decimateCount = 0; | ||
|  | #ifdef SOUNDTOUCH_INTEGER_SAMPLES
 | ||
|  |             // check ranges for sure (shouldn't actually be necessary)
 | ||
|  |             if (out > 32767)  | ||
|  |             { | ||
|  |                 out = 32767; | ||
|  |             }  | ||
|  |             else if (out < -32768)  | ||
|  |             { | ||
|  |                 out = -32768; | ||
|  |             } | ||
|  | #endif // SOUNDTOUCH_INTEGER_SAMPLES
 | ||
|  |             dest[outcount] = (SAMPLETYPE)out; | ||
|  |             outcount ++; | ||
|  |         } | ||
|  |     } | ||
|  |     return outcount; | ||
|  | } | ||
|  | 
 | ||
|  | 
 | ||
|  | // Calculates autocorrelation function of the sample history buffer
 | ||
|  | void BPMDetect::updateXCorr(int process_samples) | ||
|  | { | ||
|  |     int offs; | ||
|  |     SAMPLETYPE *pBuffer; | ||
|  |      | ||
|  |     assert(buffer->numSamples() >= (uint)(process_samples + windowLen)); | ||
|  |     assert(process_samples == XCORR_UPDATE_SEQUENCE); | ||
|  | 
 | ||
|  |     pBuffer = buffer->ptrBegin(); | ||
|  | 
 | ||
|  |     // calculate decay factor for xcorr filtering
 | ||
|  |     float xcorr_decay = (float)pow(0.5, 1.0 / (XCORR_DECAY_TIME_CONSTANT * TARGET_SRATE / process_samples)); | ||
|  | 
 | ||
|  |     // prescale pbuffer
 | ||
|  |     float tmp[XCORR_UPDATE_SEQUENCE]; | ||
|  |     for (int i = 0; i < process_samples; i++) | ||
|  |     { | ||
|  |         tmp[i] = hamw[i] * hamw[i] * pBuffer[i]; | ||
|  |     } | ||
|  | 
 | ||
|  |     #pragma omp parallel for
 | ||
|  |     for (offs = windowStart; offs < windowLen; offs ++)  | ||
|  |     { | ||
|  |         float sum; | ||
|  |         int i; | ||
|  | 
 | ||
|  |         sum = 0; | ||
|  |         for (i = 0; i < process_samples; i ++)  | ||
|  |         { | ||
|  |             sum += tmp[i] * pBuffer[i + offs];  // scaling the sub-result shouldn't be necessary
 | ||
|  |         } | ||
|  |         xcorr[offs] *= xcorr_decay;   // decay 'xcorr' here with suitable time constant.
 | ||
|  | 
 | ||
|  |         xcorr[offs] += (float)fabs(sum); | ||
|  |     } | ||
|  | } | ||
|  | 
 | ||
|  | 
 | ||
|  | // Detect individual beat positions
 | ||
|  | void BPMDetect::updateBeatPos(int process_samples) | ||
|  | { | ||
|  |     SAMPLETYPE *pBuffer; | ||
|  | 
 | ||
|  |     assert(buffer->numSamples() >= (uint)(process_samples + windowLen)); | ||
|  | 
 | ||
|  |     pBuffer = buffer->ptrBegin(); | ||
|  |     assert(process_samples == XCORR_UPDATE_SEQUENCE / 2); | ||
|  | 
 | ||
|  |     //    static double thr = 0.0003;
 | ||
|  |     double posScale = (double)this->decimateBy / (double)this->sampleRate; | ||
|  |     int resetDur = (int)(0.12 / posScale + 0.5); | ||
|  | 
 | ||
|  |     // prescale pbuffer
 | ||
|  |     float tmp[XCORR_UPDATE_SEQUENCE / 2]; | ||
|  |     for (int i = 0; i < process_samples; i++) | ||
|  |     { | ||
|  |         tmp[i] = hamw2[i] * hamw2[i] * pBuffer[i]; | ||
|  |     } | ||
|  | 
 | ||
|  |     #pragma omp parallel for
 | ||
|  |     for (int offs = windowStart; offs < windowLen; offs++) | ||
|  |     { | ||
|  |         float sum = 0; | ||
|  |         for (int i = 0; i < process_samples; i++) | ||
|  |         { | ||
|  |             sum += tmp[i] * pBuffer[offs + i]; | ||
|  |         } | ||
|  |         beatcorr_ringbuff[(beatcorr_ringbuffpos + offs) % windowLen] += (float)((sum > 0) ? sum : 0); // accumulate only positive correlations
 | ||
|  |     } | ||
|  | 
 | ||
|  |     int skipstep = XCORR_UPDATE_SEQUENCE / OVERLAP_FACTOR; | ||
|  | 
 | ||
|  |     // compensate empty buffer at beginning by scaling coefficient
 | ||
|  |     float scale = (float)windowLen / (float)(skipstep * init_scaler); | ||
|  |     if (scale > 1.0f) | ||
|  |     { | ||
|  |         init_scaler++; | ||
|  |     } | ||
|  |     else | ||
|  |     { | ||
|  |         scale = 1.0f; | ||
|  |     } | ||
|  | 
 | ||
|  |     // detect beats
 | ||
|  |     for (int i = 0; i < skipstep; i++) | ||
|  |     { | ||
|  |         LONG_SAMPLETYPE max = 0; | ||
|  | 
 | ||
|  |         float sum = beatcorr_ringbuff[beatcorr_ringbuffpos]; | ||
|  |         sum -= beat_lpf.update(sum); | ||
|  | 
 | ||
|  |         if (sum > peakVal) | ||
|  |         { | ||
|  |             // found new local largest value
 | ||
|  |             peakVal = sum; | ||
|  |             peakPos = pos; | ||
|  |         } | ||
|  |         if (pos > peakPos + resetDur) | ||
|  |         { | ||
|  |             // largest value not updated for 200msec => accept as beat
 | ||
|  |             peakPos += skipstep; | ||
|  |             if (peakVal > 0) | ||
|  |             { | ||
|  |                 // add detected beat to end of "beats" vector
 | ||
|  |                 BEAT temp = { (float)(peakPos * posScale), (float)(peakVal * scale) }; | ||
|  |                 beats.push_back(temp); | ||
|  |             } | ||
|  | 
 | ||
|  |             peakVal = 0; | ||
|  |             peakPos = pos; | ||
|  |         } | ||
|  | 
 | ||
|  |         beatcorr_ringbuff[beatcorr_ringbuffpos] = 0; | ||
|  |         pos++; | ||
|  |         beatcorr_ringbuffpos = (beatcorr_ringbuffpos + 1) % windowLen; | ||
|  |     } | ||
|  | } | ||
|  | 
 | ||
|  | 
 | ||
|  | #define max(x,y) ((x) > (y) ? (x) : (y))
 | ||
|  | 
 | ||
|  | void BPMDetect::inputSamples(const SAMPLETYPE *samples, int numSamples) | ||
|  | { | ||
|  |     SAMPLETYPE decimated[DECIMATED_BLOCK_SIZE]; | ||
|  | 
 | ||
|  |     // iterate so that max INPUT_BLOCK_SAMPLES processed per iteration
 | ||
|  |     while (numSamples > 0) | ||
|  |     { | ||
|  |         int block; | ||
|  |         int decSamples; | ||
|  | 
 | ||
|  |         block = (numSamples > INPUT_BLOCK_SIZE) ? INPUT_BLOCK_SIZE : numSamples; | ||
|  | 
 | ||
|  |         // decimate. note that converts to mono at the same time
 | ||
|  |         decSamples = decimate(decimated, samples, block); | ||
|  |         samples += block * channels; | ||
|  |         numSamples -= block; | ||
|  | 
 | ||
|  |         buffer->putSamples(decimated, decSamples); | ||
|  |     } | ||
|  | 
 | ||
|  |     // when the buffer has enough samples for processing...
 | ||
|  |     int req = max(windowLen + XCORR_UPDATE_SEQUENCE, 2 * XCORR_UPDATE_SEQUENCE); | ||
|  |     while ((int)buffer->numSamples() >= req)  | ||
|  |     { | ||
|  |         // ... update autocorrelations...
 | ||
|  |         updateXCorr(XCORR_UPDATE_SEQUENCE); | ||
|  |         // ...update beat position calculation...
 | ||
|  |         updateBeatPos(XCORR_UPDATE_SEQUENCE / 2); | ||
|  |         // ... and remove proceessed samples from the buffer
 | ||
|  |         int n = XCORR_UPDATE_SEQUENCE / OVERLAP_FACTOR; | ||
|  |         buffer->receiveSamples(n); | ||
|  |     } | ||
|  | } | ||
|  | 
 | ||
|  | 
 | ||
|  | void BPMDetect::removeBias() | ||
|  | { | ||
|  |     int i; | ||
|  | 
 | ||
|  |     // Remove linear bias: calculate linear regression coefficient
 | ||
|  |     // 1. calc mean of 'xcorr' and 'i'
 | ||
|  |     double mean_i = 0; | ||
|  |     double mean_x = 0; | ||
|  |     for (i = windowStart; i < windowLen; i++) | ||
|  |     { | ||
|  |         mean_x += xcorr[i]; | ||
|  |     } | ||
|  |     mean_x /= (windowLen - windowStart); | ||
|  |     mean_i = 0.5 * (windowLen - 1 + windowStart); | ||
|  | 
 | ||
|  |     // 2. calculate linear regression coefficient
 | ||
|  |     double b = 0; | ||
|  |     double div = 0; | ||
|  |     for (i = windowStart; i < windowLen; i++) | ||
|  |     { | ||
|  |         double xt = xcorr[i] - mean_x; | ||
|  |         double xi = i - mean_i; | ||
|  |         b += xt * xi; | ||
|  |         div += xi * xi; | ||
|  |     } | ||
|  |     b /= div; | ||
|  | 
 | ||
|  |     // subtract linear regression and resolve min. value bias
 | ||
|  |     float minval = FLT_MAX;   // arbitrary large number
 | ||
|  |     for (i = windowStart; i < windowLen; i ++) | ||
|  |     { | ||
|  |         xcorr[i] -= (float)(b * i); | ||
|  |         if (xcorr[i] < minval) | ||
|  |         { | ||
|  |             minval = xcorr[i]; | ||
|  |         } | ||
|  |     } | ||
|  | 
 | ||
|  |     // subtract min.value
 | ||
|  |     for (i = windowStart; i < windowLen; i ++) | ||
|  |     { | ||
|  |         xcorr[i] -= minval; | ||
|  |     } | ||
|  | } | ||
|  | 
 | ||
|  | 
 | ||
|  | // Calculate N-point moving average for "source" values
 | ||
|  | void MAFilter(float *dest, const float *source, int start, int end, int N) | ||
|  | { | ||
|  |     for (int i = start; i < end; i++) | ||
|  |     { | ||
|  |         int i1 = i - N / 2; | ||
|  |         int i2 = i + N / 2 + 1; | ||
|  |         if (i1 < start) i1 = start; | ||
|  |         if (i2 > end)   i2 = end; | ||
|  | 
 | ||
|  |         double sum = 0; | ||
|  |         for (int j = i1; j < i2; j ++) | ||
|  |         {  | ||
|  |             sum += source[j]; | ||
|  |         } | ||
|  |         dest[i] = (float)(sum / (i2 - i1)); | ||
|  |     } | ||
|  | } | ||
|  | 
 | ||
|  | 
 | ||
|  | float BPMDetect::getBpm() | ||
|  | { | ||
|  |     double peakPos; | ||
|  |     double coeff; | ||
|  |     PeakFinder peakFinder; | ||
|  | 
 | ||
|  |     // remove bias from xcorr data
 | ||
|  |     removeBias(); | ||
|  | 
 | ||
|  |     coeff = 60.0 * ((double)sampleRate / (double)decimateBy); | ||
|  | 
 | ||
|  |     // save bpm debug data if debug data writing enabled
 | ||
|  |     _SaveDebugData("soundtouch-bpm-xcorr.txt", xcorr, windowStart, windowLen, coeff); | ||
|  | 
 | ||
|  |     // Smoothen by N-point moving-average
 | ||
|  |     float *data = new float[windowLen]; | ||
|  |     memset(data, 0, sizeof(float) * windowLen); | ||
|  |     MAFilter(data, xcorr, windowStart, windowLen, MOVING_AVERAGE_N); | ||
|  | 
 | ||
|  |     // find peak position
 | ||
|  |     peakPos = peakFinder.detectPeak(data, windowStart, windowLen); | ||
|  | 
 | ||
|  |     // save bpm debug data if debug data writing enabled
 | ||
|  |     _SaveDebugData("soundtouch-bpm-smoothed.txt", data, windowStart, windowLen, coeff); | ||
|  | 
 | ||
|  |     delete[] data; | ||
|  | 
 | ||
|  |     assert(decimateBy != 0); | ||
|  |     if (peakPos < 1e-9) return 0.0; // detection failed.
 | ||
|  | 
 | ||
|  |     _SaveDebugBeatPos("soundtouch-detected-beats.txt", beats); | ||
|  | 
 | ||
|  |     // calculate BPM
 | ||
|  |     float bpm = (float)(coeff / peakPos); | ||
|  |     return (bpm >= MIN_BPM && bpm <= MAX_BPM_VALID) ? bpm : 0; | ||
|  | } | ||
|  | 
 | ||
|  | 
 | ||
|  | /// Get beat position arrays. Note: The array includes also really low beat detection values 
 | ||
|  | /// in absence of clear strong beats. Consumer may wish to filter low values away.
 | ||
|  | /// - "pos" receive array of beat positions
 | ||
|  | /// - "values" receive array of beat detection strengths
 | ||
|  | /// - max_num indicates max.size of "pos" and "values" array.  
 | ||
|  | ///
 | ||
|  | /// You can query a suitable array sized by calling this with NULL in "pos" & "values".
 | ||
|  | ///
 | ||
|  | /// \return number of beats in the arrays.
 | ||
|  | int BPMDetect::getBeats(float *pos, float *values, int max_num) | ||
|  | { | ||
|  |     int num = (int)beats.size(); | ||
|  |     if ((!pos) || (!values)) return num;    // pos or values NULL, return just size
 | ||
|  | 
 | ||
|  |     for (int i = 0; (i < num) && (i < max_num); i++) | ||
|  |     { | ||
|  |         pos[i] = beats[i].pos; | ||
|  |         values[i] = beats[i].strength; | ||
|  |     } | ||
|  |     return num; | ||
|  | } |