Beats per Minute (BPM berechnen)

MiMa

Top Contributor
Ich erstelle gerade meine neue Musiklasse mit jausdiotagger und möchte gerne die BPM meiern dateien berechnen lassen.
Im Netz finde ich kaum etwas und wenn das ist es ziemlich alt und funktioniert kaum mit der aktuellen Version 3.0.1

<!-- jAudioTagger -->
<dependency>
<groupId>net.jthink</groupId>
<artifactId>jaudiotagger</artifactId>
<version>3.0.1</version>
</dependency>
Ich frage hier mal, ob jemand erfahrung mit der verarbeitung vo Audiodateien wie mp3, flac usw.?

Was ich gefunden habe ich sowas
Java:
// Berechne BPM
int bpm = audioFile.getAudioHeader().getMp3AudioHeader().getBpm();
System.out.println("BPM der Audiodatei: " + bpm);
aber genau wie andere Beispiele funktioniert es leider nicht, weil die Funktionen so nicht enthalten sind?

Vielen Dank
 
Um die BPM zu berechnen mußt du etwas Signalverarbeitung betreiben. Ich würde es am Anfang mal damit versuchen, deine Audiodateien durch eine Fouriertransformation zu jagen und den unteren Teil des Frequenzspektrums über die Zeit zu analsysieren. Z.B. die 50Hz-Welle rausfiltern und durch eine Hysterese laufen lassen, um dann sagen zu können wann 50Hz ziemlich ausgeprägt da ist, und wann gar nicht. Und dann diese Ereignisse zählen.


PS: Ob Java dafür jetzt so bekannt, d.h. geeignet ist...wohl eher nicht. Für Python müßte es – auch wenn mich die Sprache anwidert – da aber was geben. Das wäre zumindest meine erste Anlaufstelle. (Nebst einem Tutorial, um erstmal die Pythonsyntax zu lernen...)
 
@Jw456
Die Bitrate ist nicht das gleiche wie BPM (Beats per Minute)

Ich hatte gedacht das es eventuelle Java Bibliotheken gibt die das können! jaudioTagger scheint tasächlich nur Tagbearbeitung zu können.
WAs ich noch gefunden hatte ist TarsosDSP. Mir ist aber inklar wie ich das in Maven implemenietre?
Depencity sagt mir mittlerweile was aber <repository> weis ich nicht wo das hin kommt??
Bin mir auch nicht sicher ob Audioprocessing das richtige ist, hört sich zumindest so an?
 
Der repositiry eintrag bleibt rot unterstrichen?
und bei den depencitys
Dependency 'be.tarsos.dsp:core:2.5' not found
 
hier schon geschaut https://stackoverflow.com/questions/79445/beats-per-minute-from-real-time-audio-input ?

du musst die aktuelle bass intensität herausfinden... der rest ist dann trivial.
Danke für deinen nutzlosen Versuch hier irgendwie den Eindruck zu vermitteln, du würdest du irgendwie wissen, was es hier geschrieben wird.

Aber ich denke schon, dass der Fragesteller hier an einer Java Lösung interessiert ist und nicht nach einer zufällig gefundenen Seite im Internet.

Ich denke man sollte es schon irgendwann mal eingesehen haben, dass man in einem Forum einfach komplett unwillkommen ist. Und das nichts mit dem Account zu tun hat, sondern mit dir als Person.
 
nach einer zufällig gefundenen Seite im Internet.
Ich finde den Link nicht schlecht 🙂 Über den Link des Fragestellers kommt man weiter zu https://www.clear.rice.edu/elec301/Projects01/beat_sync/beatalgo.html, mit relativ einfachem Matlab-Code.

Um den Code zu übersetzen, würde ich einfach mal mit einem Interface beginnen, das normalisierte Audiosamples (doubles zwischen -1 und +1) und die Samplerate liefert:
Java:
public interface AudioData {
    int getSampleRate();
    double[] getFrames();
}

Dann besorgt man sich eine FFT-Lib (https://www.nayuki.io/page/free-small-fft-in-multiple-languages) und doktort "ein wenig" rum:
Java:
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;
    }

}

Wer keine Lust hat, sich die FFT-Lib herunterzuladen:
Java:
/* 
 * Free FFT and convolution (Java)
 * 
 * Copyright (c) 2020 Project Nayuki. (MIT License)
 * https://www.nayuki.io/page/free-small-fft-in-multiple-languages
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 * - The above copyright notice and this permission notice shall be included in
 *   all copies or substantial portions of the Software.
 * - The Software is provided "as is", without warranty of any kind, express or
 *   implied, including but not limited to the warranties of merchantability,
 *   fitness for a particular purpose and noninfringement. In no event shall the
 *   authors or copyright holders be liable for any claim, damages or other
 *   liability, whether in an action of contract, tort or otherwise, arising from,
 *   out of or in connection with the Software or the use or other dealings in the
 *   Software.
 */


public final class Fft {
    
    /* 
     * Computes the discrete Fourier transform (DFT) of the given complex vector, storing the result back into the vector.
     * The vector can have any length. This is a wrapper function.
     */
    public static void transform(double[] real, double[] imag) {
        int n = real.length;
        if (n != imag.length)
            throw new IllegalArgumentException("Mismatched lengths");
        if (n == 0)
            return;
        else if ((n & (n - 1)) == 0)  // Is power of 2
            transformRadix2(real, imag);
        else  // More complicated algorithm for arbitrary sizes
            transformBluestein(real, imag);
    }
    
    
    /* 
     * Computes the inverse discrete Fourier transform (IDFT) of the given complex vector, storing the result back into the vector.
     * The vector can have any length. This is a wrapper function. This transform does not perform scaling, so the inverse is not a true inverse.
     */
    public static void inverseTransform(double[] real, double[] imag) {
        transform(imag, real);
    }
    
    
    /* 
     * Computes the discrete Fourier transform (DFT) of the given complex vector, storing the result back into the vector.
     * The vector's length must be a power of 2. Uses the Cooley-Tukey decimation-in-time radix-2 algorithm.
     */
    public static void transformRadix2(double[] real, double[] imag) {
        // Length variables
        int n = real.length;
        if (n != imag.length)
            throw new IllegalArgumentException("Mismatched lengths");
        int levels = 31 - Integer.numberOfLeadingZeros(n);  // Equal to floor(log2(n))
        if (1 << levels != n)
            throw new IllegalArgumentException("Length is not a power of 2");
        
        // Trigonometric tables
        double[] cosTable = new double[n / 2];
        double[] sinTable = new double[n / 2];
        for (int i = 0; i < n / 2; i++) {
            cosTable[i] = Math.cos(2 * Math.PI * i / n);
            sinTable[i] = Math.sin(2 * Math.PI * i / n);
        }
        
        // Bit-reversed addressing permutation
        for (int i = 0; i < n; i++) {
            int j = Integer.reverse(i) >>> (32 - levels);
            if (j > i) {
                double temp = real[i];
                real[i] = real[j];
                real[j] = temp;
                temp = imag[i];
                imag[i] = imag[j];
                imag[j] = temp;
            }
        }
        
        // Cooley-Tukey decimation-in-time radix-2 FFT
        for (int size = 2; size <= n; size *= 2) {
            int halfsize = size / 2;
            int tablestep = n / size;
            for (int i = 0; i < n; i += size) {
                for (int j = i, k = 0; j < i + halfsize; j++, k += tablestep) {
                    int l = j + halfsize;
                    double tpre =  real[l] * cosTable[k] + imag[l] * sinTable[k];
                    double tpim = -real[l] * sinTable[k] + imag[l] * cosTable[k];
                    real[l] = real[j] - tpre;
                    imag[l] = imag[j] - tpim;
                    real[j] += tpre;
                    imag[j] += tpim;
                }
            }
            if (size == n)  // Prevent overflow in 'size *= 2'
                break;
        }
    }
    
    
    /* 
     * Computes the discrete Fourier transform (DFT) of the given complex vector, storing the result back into the vector.
     * The vector can have any length. This requires the convolution function, which in turn requires the radix-2 FFT function.
     * Uses Bluestein's chirp z-transform algorithm.
     */
    public static void transformBluestein(double[] real, double[] imag) {
        // Find a power-of-2 convolution length m such that m >= n * 2 + 1
        int n = real.length;
        if (n != imag.length)
            throw new IllegalArgumentException("Mismatched lengths");
        if (n >= 0x20000000)
            throw new IllegalArgumentException("Array too large");
        int m = Integer.highestOneBit(n) * 4;
        
        // Trigonometric tables
        double[] cosTable = new double[n];
        double[] sinTable = new double[n];
        for (int i = 0; i < n; i++) {
            int j = (int)((long)i * i % (n * 2));  // This is more accurate than j = i * i
            cosTable[i] = Math.cos(Math.PI * j / n);
            sinTable[i] = Math.sin(Math.PI * j / n);
        }
        
        // Temporary vectors and preprocessing
        double[] areal = new double[m];
        double[] aimag = new double[m];
        for (int i = 0; i < n; i++) {
            areal[i] =  real[i] * cosTable[i] + imag[i] * sinTable[i];
            aimag[i] = -real[i] * sinTable[i] + imag[i] * cosTable[i];
        }
        double[] breal = new double[m];
        double[] bimag = new double[m];
        breal[0] = cosTable[0];
        bimag[0] = sinTable[0];
        for (int i = 1; i < n; i++) {
            breal[i] = breal[m - i] = cosTable[i];
            bimag[i] = bimag[m - i] = sinTable[i];
        }
        
        // Convolution
        double[] creal = new double[m];
        double[] cimag = new double[m];
        convolve(areal, aimag, breal, bimag, creal, cimag);
        
        // Postprocessing
        for (int i = 0; i < n; i++) {
            real[i] =  creal[i] * cosTable[i] + cimag[i] * sinTable[i];
            imag[i] = -creal[i] * sinTable[i] + cimag[i] * cosTable[i];
        }
    }
    
    
    /* 
     * Computes the circular convolution of the given real vectors. Each vector's length must be the same.
     */
    public static void convolve(double[] xvec, double[] yvec, double[] outvec) {
        int n = xvec.length;
        if (n != yvec.length || n != outvec.length)
            throw new IllegalArgumentException("Mismatched lengths");
        convolve(xvec, new double[n], yvec, new double[n], outvec, new double[n]);
    }
    
    
    /* 
     * Computes the circular convolution of the given complex vectors. Each vector's length must be the same.
     */
    public static void convolve(double[] xreal, double[] ximag,
            double[] yreal, double[] yimag, double[] outreal, double[] outimag) {
        
        int n = xreal.length;
        if (n != ximag.length || n != yreal.length || n != yimag.length
                || n != outreal.length || n != outimag.length)
            throw new IllegalArgumentException("Mismatched lengths");
        
        xreal = xreal.clone();
        ximag = ximag.clone();
        yreal = yreal.clone();
        yimag = yimag.clone();
        transform(xreal, ximag);
        transform(yreal, yimag);
        
        for (int i = 0; i < n; i++) {
            double temp = xreal[i] * yreal[i] - ximag[i] * yimag[i];
            ximag[i] = ximag[i] * yreal[i] + xreal[i] * yimag[i];
            xreal[i] = temp;
        }
        inverseTransform(xreal, ximag);
        
        for (int i = 0; i < n; i++) {  // Scaling (because this FFT implementation omits it)
            outreal[i] = xreal[i] / n;
            outimag[i] = ximag[i] / n;
        }
    }
    
}

Ja, nicht schön aber selten. Der Spaß ist auch nicht der schnellste. Bei einem Wave-File mit 22 kHz Samplerate, 16 Bit PCM, braucht meine Kiste ca. 4 Sekunden.

Zum Testen habe ich mit Audacity ein 80 BPM und ein 120 BPM Signal erstellt. Die Methode liefert als Ergebnis: 80.00000000000006 und 119.99000000000005 BPM. Sieht okay aus.
 
Ohje, das ist aber ganz schön aufwendig.
Ich habe selbst im Netz recherchiert, hatte aber nichts anständiges gefunden welches auf Java Basis anständig funktioniert. Ich habe aber auch kein Interesse um diese Funktion zu implementieren noch zig andere Abhängigkeiten zu integrieren. Viel versprechende Bibliotheken sind auch schon zig Jahre alt und funktionieren mit Java 20 auch nicht mehr, leider.
Ist das Thema heute so nutzlos geworden?
 
Könnte man das hiermit in einer einfachen Form implementieren?
BeatDetector
Von Signalverarbeitung habe ich keine Kenntnisse.

@mihe7
Du schreibst anspruchsvollen Code, vielen Dank dafür.
Ich bin weder Mathematiker noch Informatiker und verstehe daher leider den Code nicht.
 
Zuletzt bearbeitet:
Ich bin weder Mathematiker noch Informatiker und verstehe daher leider den Code nicht.
Der ursprüngliche Code ist auch nicht von mir, ich habe lediglich den Matlab-Code nach Java übersetzt. Auf der Seite ist der Spaß auch beschrieben.

Als "Anwender" musst Du lediglich die Methode calculateBpm mit einem Objekt aufrufen, das die AudioData-Schnittstelle implementiert, und bekommst die BPM zurück. D. h. es ist nicht sehr viel mehr als
Java:
double bpm = new BpmCalculator().calculateBpm(data);

Die AudioData-Schnittstelle deklariert einfach zwei Methoden: es wird die SampleRate benötigt und es werden die Audiodaten (unkomprimiert, versteht sich) benötigt. Die Audiodaten müssen auf den Bereich -1.0 bis +1.0 normiert sein.

Wenn Du an einer Stelle nicht weiterkommst oder etwas unklar ist, sag Bescheid.
 
Ist das Thema heute so nutzlos geworden?
Das nicht. Aber die, die sowas machen, benutzen normalerweise kein Java, sondern z.B. Matlab (teuer und auch nervig) und/oder z.B. Python.

Auch wenn ich – wie gesagt – von Python keine Ahnung habe würde mal davon ausgehen, das sowas jemand schonmal in Python implementiert hat. Vielleicht kannst du ja sowas nehmen und einen Javawrapper dafür schreiben.
 

Zurück
Oben