public class BpmCalculator {
static final int[] DEFAULT_BAND_LIMITS = { 0, 200, 400, 800, 1600, 3200 };
private double bpmMin = 60;
private double bpmMax = 240;
private int[] bandLimits;
private int[] bandLimitsLower;
private int[] bandLimitsUpper;
public double calculateBpm(AudioData data) {
// aufgrund des Abtasttheorems, ist die maximale Frequenz im Signal die Haelfte
// der SampleRate.
int maxFrequency = data.getSampleRate() / 2;
// Um die BPM erkennen zu koennen, werden zwei Beats benoetigt.
// Will man mindestens 60 BPM (1 Beat pro Sekunde) erkennen, werden mindestens 2
// Sekunden an Daten benoetigt.
// Dazu noch 10 % Toleranz zur Sicherheit.
double duration = 1.1 * 2 * 60.0 / bpmMin;
double[] sample = getFrames(data, duration, 0.5); // wir nehmen das Sample aus der Mitte
setBandLimits(DEFAULT_BAND_LIMITS, maxFrequency, sample.length);
double[][][] bank = calculateFilterBank(sample);
double[][] hwnd = smooth(bank, 0.2, maxFrequency);
double[][] diffrect = differentiateAndRectify(hwnd);
double bpm = findTempo(diffrect, 2, bpmMin, bpmMax, maxFrequency);
bpm = findTempo(diffrect, .5, bpm - 0.5, bpm + 0.5, maxFrequency);
bpm = findTempo(diffrect, .1, bpm - 0.1, bpm + 0.1, maxFrequency);
bpm = findTempo(diffrect, .01, bpm - 0.1, bpm + 0.1, maxFrequency);
return bpm;
}
private void setBandLimits(int[] bandLimits, int maxFrequency, int nSamples) {
int n = bandLimits.length;
this.bandLimits = bandLimits;
bandLimitsLower = new int[n];
bandLimitsUpper = new int[n];
for (int i = 0; i < n - 1; i++) {
bandLimitsLower[i] = ((int) (bandLimits[i] / (double) maxFrequency * nSamples / 2.0)) + 1;
bandLimitsUpper[i] = ((int) (bandLimits[i + 1] / (double) maxFrequency * nSamples / 2.0));
}
bandLimitsLower[n - 1] = ((int) (bandLimits[n - 1] / (double) maxFrequency * nSamples / 2.0)) + 1;
bandLimitsUpper[n - 1] = nSamples / 2;
}
private static double[] getFrames(AudioData data, double duration, double positionFraction) {
int numFrames = (int) (duration * data.getSampleRate());
double[] frames = data.getFrames();
int offset = (int) (positionFraction * frames.length - numFrames / 2);
double[] result = new double[numFrames];
System.arraycopy(frames, offset, result, 0, numFrames);
return result;
}
private double[][][] calculateFilterBank(double[] samples) {
int n = samples.length;
double[] real = new double[n];
double[] imag = new double[n];
System.arraycopy(samples, 0, real, 0, samples.length);
Fft.transform(real, imag);
int nbands = bandLimits.length;
double[][][] bank = new double[nbands][][];
for (int i = 0; i < nbands; i++) {
double[][] bandData = new double[2][real.length];
int bl = bandLimitsLower[i] - 1;
int bu = bandLimitsUpper[i];
int bandLength = bu - bl;
System.arraycopy(real, bl, bandData[0], bl, bandLength);
System.arraycopy(imag, bl, bandData[1], bl, bandLength);
System.arraycopy(real, n - bu, bandData[0], n - bu, bandLength);
System.arraycopy(imag, n - bu, bandData[0], n - bu, bandLength);
bank[i] = bandData;
}
bank[0][0][0] = 0;
bank[0][1][0] = 0;
return bank;
}
private double[][] smooth(double[][][] signal, double winLength, int maxFrequency) {
int n = signal[0][0].length;
int nbands = bandLimitsLower.length;
double[][][] wave = new double[nbands][2][];
for (int i = 0; i < nbands; i++) {
double[] real = new double[n];
double[] imag = new double[n];
System.arraycopy(signal[i][0], 0, real, 0, n);
System.arraycopy(signal[i][1], 0, imag, 0, n);
Fft.inverseTransform(real, imag);
wave[i][0] = scaled(real);
wave[i][1] = new double[n];
}
for (int i = 0; i < nbands; i++) {
for (int j = 0; j < n; j++) {
if (wave[i][0][j] < 0)
wave[i][0][j] = -wave[i][0][j];
}
Fft.transform(wave[i][0], wave[i][1]);
}
double[][][] freq = wave;
wave = null;
double hannLen = winLength * 2 * maxFrequency;
double[] hann = new double[n];
for (int i = 0; i < n; i++) {
hann[i] = Math.pow(Math.cos((i + 1) * Math.PI / hannLen / 2), 2);
}
double[] imagHann = new double[n];
Fft.transform(hann, imagHann);
double[][] output = new double[nbands][];
for (int i = 0; i < nbands; i++) {
double[][] filtered = new double[2][n];
for (int j = 0; j < n; j++) {
filtered[0][j] = freq[i][0][j] * hann[j] - freq[i][1][j] * imagHann[j];
filtered[1][j] = freq[i][0][j] * imagHann[j] + freq[i][1][j] * hann[j];
}
Fft.inverseTransform(filtered[0], filtered[1]);
output[i] = scaled(filtered[0]);
}
return output;
}
static double[] scaled(double[] in) {
int n = in.length;
double[] result = new double[n];
for (int i = 0; i < n; i++) {
result[i] = in[i] * 1.0 / n;
}
return result;
}
private double[][] differentiateAndRectify(double[][] signal) {
int n = signal[0].length;
double[][] output = new double[signal.length][n];
for (int i = 0; i < signal.length; i++) {
for (int j = 5; j < n; j++) {
double diff = signal[i][j] - signal[i][j - 1];
if (diff > 0) {
output[i][j] = diff;
}
}
}
return output;
}
private double findTempo(double[][] signal, double acc, double bpmMin, double bpmMax, int maxFrequency) {
int pulses = 3;
int n = signal[0].length;
int nbands = signal.length;
double[][][] dft = new double[nbands][2][];
for (int i = 0; i < nbands; i++) {
double[] real = new double[n];
System.arraycopy(signal[i], 0, real, 0, n);
double[] imag = new double[n];
Fft.transform(real, imag);
dft[i][0] = real;
dft[i][1] = imag;
}
double e = 0.0;
double maxe = 0.0;
double result = bpmMin;
double bpm = bpmMin;
while (bpm <= bpmMax) {
e = 0.0;
double[][] fil = new double[2][n];
int nstep = (int) (120.0 / bpm * maxFrequency);
for (int a = 0; a < pulses; a++) {
fil[0][a * nstep] = 1;
}
Fft.transform(fil[0], fil[1]);
for (int i = 0; i < nbands; i++) {
for (int j = 0; j < n; j++) {
// (a+bi) * (c+di) = ac - bd + (ad + bc)i
double xr = fil[0][j] * dft[i][0][j] - fil[1][j] * dft[i][1][j];
double xi = fil[0][j] * dft[i][1][j] + fil[1][j] * dft[i][0][j];
double x = xr * xr + xi * xi;
e += x;
}
}
if (e > maxe) {
result = bpm;
maxe = e;
}
bpm += acc;
}
return result;
}
}