/* * Copyright (c) 2007 by Damien Di Fede <ddf@compartmental.net> * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as published * by the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program 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 Library General Public License for more details. * * You should have received a copy of the GNU Library General Public * License along with this program; if not, write to the Free Software * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */ package ddf.minim.analysis; import processing.core.PApplet; import ddf.minim.AudioBuffer; import ddf.minim.Minim; /** * The BeatDetect class allows you to analyze an audio stream for beats * (rhythmic onsets). <a * href="http://www.gamedev.net/reference/programming/features/beatdetection">Beat * Detection Algorithms</a> by Fr�d�ric Patin describes beats in the following * way: <blockquote> The human listening system determines the rhythm of music * by detecting a pseudo � periodical succession of beats. The signal which is * intercepted by the ear contains a certain energy, this energy is converted * into an electrical signal which the brain interprets. Obviously, The more * energy the sound transports, the louder the sound will seem. But a sound will * be heard as a <em>beat</em> only if his energy is largely superior to the * sound's energy history, that is to say if the brain detects a * <em>brutal variation * in sound energy</em>. Therefore if the ear intercepts * a monotonous sound with sometimes big energy peaks it will detect beats, * however, if you play a continuous loud sound you will not perceive any beats. * Thus, the beats are big variations of sound energy. </blockquote> In fact, * the two algorithms in this class are based on two algorithms described in * that paper. * <p> * To use this class, inside of <code>draw()</code> you must first call * <code>detect()</code>, passing the <code>AudioBuffer</code> you want to * analyze. You may then use the <code>isXXX</code> functions to find out * what beats have occurred in that frame. For example, you might use * <code>isKick()</code> to cause a circle to pulse. * <p> * BeatDetect has two modes: sound energy tracking and frequency energy * tracking. In sound energy mode, the level of the buffer, as returned by * <code>level()</code>, is used as the instant energy in each frame. Beats, * then, are spikes in this value, relative to the previous one second of sound. * In frequency energy mode, the same process is used but instead of tracking * the level of the buffer, an FFT is used to obtain a spectrum, which is then * divided into average bands using <code>logAverages()</code>, and each of * these bands is tracked individually. The result is that it is possible to * track sounds that occur in different parts of the frequency spectrum * independently (like the kick drum and snare drum). * <p> * In sound energy mode you use <code>isOnset()</code> to query the algorithm * and in frequency energy mode you use <code>isOnset(int i)</code>, * <code>isKick()</code>, <code>isSnare()</code>, and * <code>isRange()</code> to query particular frequnecy bands or ranges of * frequency bands. It should be noted that <code>isKick()</code>, * <code>isSnare()</code>, and <code>isHat()</code> merely call * <code>isRange()</code> with values determined by testing the algorithm * against music with a heavy beat and they may not be appropriate for all kinds * of music. If you find they are performing poorly with your music, you should * use <code>isRange()</code> directly to locate the bands that provide the * most meaningful information for you. * * @author Damien Di Fede * */ public class BeatDetect { /** Constant used to request frequency energy tracking mode. */ public static final int FREQ_ENERGY = 0; /** Constant used to request sound energy tracking mode. */ public static final int SOUND_ENERGY = 1; private int algorithm; private int sampleRate; private int timeSize; private int valCnt; private float[] valGraph; private int sensitivity; // for circular buffer support private int insertAt; // vars for sEnergy private boolean isOnset; private float[] eBuffer; private float[] dBuffer; private long timer; // vars for fEnergy private boolean[] fIsOnset; private FFT spect; private float[][] feBuffer; private float[][] fdBuffer; private long[] fTimer; private float[] varGraph; private int varCnt; /** * Create a BeatDetect object that is in SOUND_ENERGY mode. * <code>timeSize</code> and <code>sampleRate</code> will be set to 1024 * and 44100, respectively, so that it is possible to switch into FREQ_ENERGY * mode with meaningful values. * */ public BeatDetect() { sampleRate = 44100; timeSize = 1024; initSEResources(); initGraphs(); algorithm = SOUND_ENERGY; sensitivity = 10; } /** * Create a BeatDetect object that is in FREQ_ENERGY mode and expects a sample * buffer with the requested attributes. * * @param timeSize * the size of the buffer * @param sampleRate * the sample rate of the samples in the buffer */ public BeatDetect(int timeSize, float sampleRate) { this.sampleRate = (int) sampleRate; this.timeSize = timeSize; initFEResources(); initGraphs(); algorithm = FREQ_ENERGY; sensitivity = 10; } /** * Set the object to use the requested algorithm. If an invalid value is * passed, the function will report and error and default to * BeatDetect.SOUND_ENERGY * * @param algo * either BeatDetect.SOUND_ENERGY or BeatDetect.FREQ_ENERGY */ public void detectMode(int algo) { if (algo < 0 || algo > 1) { Minim .error("Unrecognized detect mode, defaulting to SOUND_ENERGY."); algo = SOUND_ENERGY; } if (algo == SOUND_ENERGY) { if (algorithm == FREQ_ENERGY) { releaseFEResources(); initSEResources(); initGraphs(); algorithm = algo; } } else { if (algorithm == SOUND_ENERGY) { releaseSEResources(); initFEResources(); initGraphs(); algorithm = FREQ_ENERGY; } } } private void initGraphs() { valCnt = varCnt = 0; valGraph = new float[512]; varGraph = new float[512]; } private void initSEResources() { isOnset = false; eBuffer = new float[sampleRate / timeSize]; dBuffer = new float[sampleRate / timeSize]; timer = System.currentTimeMillis(); insertAt = 0; } private void initFEResources() { spect = new FFT(timeSize, sampleRate); spect.logAverages(60, 3); int numAvg = spect.avgSize(); fIsOnset = new boolean[numAvg]; feBuffer = new float[numAvg][sampleRate / timeSize]; fdBuffer = new float[numAvg][sampleRate / timeSize]; fTimer = new long[numAvg]; long start = System.currentTimeMillis(); for (int i = 0; i < fTimer.length; i++) fTimer[i] = start; insertAt = 0; } private void releaseSEResources() { isOnset = false; eBuffer = null; dBuffer = null; timer = 0; } private void releaseFEResources() { spect = null; fIsOnset = null; feBuffer = null; fdBuffer = null; fTimer = null; } /** * Analyze the samples in <code>ab</code>. This is a cumlative process, so * you must call this function every frame. * * @param ab * the MAudioBuffer to analyze. */ public void detect(AudioBuffer ab) { switch (algorithm) { case SOUND_ENERGY: sEnergy(ab); break; case FREQ_ENERGY: fEnergy(ab); break; } } /** * In sound energy mode this returns true when a beat has been detected. In * frequency energy mode this always returns false. * * @return true if a beat has been detected. */ public boolean isOnset() { return isOnset; } /** * In frequency energy mode this returns true when a beat has been detect in * the <code>i<sup>th</sup></code> frequency band. In sound energy mode * this always returns false. * * @param i * the frequency band to query * @return true if a beat has been detected in the requested band */ public boolean isOnset(int i) { if (algorithm == SOUND_ENERGY) return false; return fIsOnset[i]; } /** * In frequency energy mode this returns true if a beat corresponding to the * frequency range of a kick drum has been detected. This has been tuned to * work well with dance / techno music and may not perform well with other * styles of music. In sound energy mode this always returns false. * * @return true if a kick drum beat has been detected */ public boolean isKick() { if (algorithm == SOUND_ENERGY) return false; int upper = 6 >= spect.avgSize() ? spect.avgSize() : 6; return isRange(1, upper, 2); } /** * In frequency energy mode this returns true if a beat corresponding to the * frequency range of a snare drum has been detected. This has been tuned to * work well with dance / techno music and may not perform well with other * styles of music. In sound energy mode this always returns false. * * @return true if a snare drum beat has been detected */ public boolean isSnare() { if (algorithm == SOUND_ENERGY) return false; int lower = 8 >= spect.avgSize() ? spect.avgSize() : 8; int upper = spect.avgSize() - 1; int thresh = (upper - lower) / 3 + 1; return isRange(lower, upper, thresh); } /** * In frequency energy mode this returns true if a beat corresponding to the * frequency range of a hi hat has been detected. This has been tuned to work * well with dance / techno music and may not perform well with other styles * of music. In sound energy mode this always returns false. * * @return true if a hi hat beat has been detected */ public boolean isHat() { if (algorithm == SOUND_ENERGY) return false; int lower = spect.avgSize() - 7 < 0 ? 0 : spect.avgSize() - 7; int upper = spect.avgSize() - 1; return isRange(lower, upper, 1); } /** * In frequency energy mode this returns true if at least * <code>threshold</code> bands of the bands included in the range * <code>[low, high]</code> have registered a beat. In sound energy mode * this always returns false. * * @param low * the index of the lower band * @param high * the index of the higher band * @param threshold * the smallest number of bands in the range <code>[low, high]</code> * that need to have registered a beat for this to return true * @return true if at least <code>threshold</code> bands of the bands * included in the range <code>[low, high]</code> have registered a * beat */ public boolean isRange(int low, int high, int threshold) { if (algorithm == SOUND_ENERGY) return false; int num = 0; for (int i = low; i < high + 1; i++) if (isOnset(i)) num++; return num >= threshold; } /** * Sets the sensitivity of the algorithm. After a beat has been detected, the * algorithm will wait for <code>s</code> milliseconds before allowing * another beat to be reported. You can use this to dampen the algorithm if it * is giving too many false-positives. The default value is 10, which is * essentially no damping. If you try to set the sensitivity to a negative * value, an error will be reported and it will be set to 10 instead. * * @param s * the sensitivity in milliseconds */ public void setSensitivity(int s) { if (s < 0) { Minim .error("BeatDetect: sensitivity cannot be less than zero. Defaulting to 10."); sensitivity = 10; } else { sensitivity = s; } } /** * Draws some debugging visuals in the passed PApplet. The visuals drawn when * in frequency energy mode are a good way to determine what values to use * with <code>inRange()</code> if the provided drum detecting functions * aren't what you need or aren't working well. * * @param p * the PApplet to draw in */ public void drawGraph(PApplet p) { if (algorithm == SOUND_ENERGY) { // draw valGraph for (int i = 0; i < valCnt; i++) { p.stroke(255); p.line(i, (p.height / 2) - valGraph[i], i, (p.height / 2) + valGraph[i]); } // draw varGraph for (int i = 0; i < varCnt - 1; i++) { p.stroke(255); p.line(i, p.height - varGraph[i], i + 1, p.height - varGraph[i + 1]); } } else { p.strokeWeight(5); for (int i = 0; i < fTimer.length; i++) { int c = (i % 3 == 0) ? p.color(255, 0, 0) : p.color(255); p.stroke(c); long clock = System.currentTimeMillis(); if (clock - fTimer[i] < sensitivity) { float h = PApplet.map(clock - fTimer[i], 0, sensitivity, 100, 0); p.line((i * 10), p.height - h, (i * 10), p.height); } } } } private void sEnergy(AudioBuffer in) { // otherwise, compute! float instant = in.level() * 100; // compute the average local energy float E = average(eBuffer); // compute the variance of the energies in eBuffer float V = variance(eBuffer, E); // compute C using a linear digression of C with V float C = (-0.0025714f * V) + 1.5142857f; // filter negaive values float diff = PApplet.max(instant - C * E, 0); pushVal(diff); // find the average of only the positive values in dBuffer float dAvg = specAverage(dBuffer); // filter negative values float diff2 = PApplet.max(diff - dAvg, 0); pushVar(diff2); // report false if it's been less than 'sensitivity' // milliseconds since the last true value if (System.currentTimeMillis() - timer < sensitivity) { isOnset = false; } // if we've made it this far then we're allowed to set a new // value, so set it true if it deserves to be, restart the timer else if (diff2 > 0 && instant > 2) { isOnset = true; timer = System.currentTimeMillis(); } // OMG it wasn't true! else { isOnset = false; } eBuffer[insertAt] = instant; dBuffer[insertAt] = diff; insertAt++; if (insertAt == eBuffer.length) insertAt = 0; // shift(eBuffer, instant); // shift(dBuffer, diff); } private void fEnergy(AudioBuffer in) { spect.forward(in); float instant, E, V, C, diff, dAvg, diff2; for (int i = 0; i < feBuffer.length; i++) { instant = spect.getAvg(i); E = average(feBuffer[i]); V = variance(feBuffer[i], E); C = (-0.0025714f * V) + 1.5142857f; diff = PApplet.max(instant - C * E, 0); dAvg = specAverage(fdBuffer[i]); diff2 = PApplet.max(diff - dAvg, 0); if (System.currentTimeMillis() - fTimer[i] < sensitivity) { fIsOnset[i] = false; } else if (diff2 > 0) { fIsOnset[i] = true; fTimer[i] = System.currentTimeMillis(); } else { fIsOnset[i] = false; } // shift(feBuffer[i], instant); // shift(fdBuffer[i], diff); feBuffer[i][insertAt] = instant; fdBuffer[i][insertAt] = diff; } insertAt++; if (insertAt == feBuffer[0].length) insertAt = 0; } private void pushVal(float v) { // println(valCnt); if (valCnt == valGraph.length) { valCnt = 0; valGraph = new float[valGraph.length]; } valGraph[valCnt] = v; valCnt++; } private void pushVar(float v) { // println(valCnt); if (varCnt == varGraph.length) { varCnt = 0; varGraph = new float[varGraph.length]; } varGraph[varCnt] = v; varCnt++; } private float average(float[] arr) { float avg = 0; for (int i = 0; i < arr.length; i++) avg += arr[i]; avg /= arr.length; return avg; } private float specAverage(float[] arr) { float avg = 0; float num = 0; for (int i = 0; i < arr.length; i++) { if (arr[i] > 0) { avg += arr[i]; num++; } } if (num > 0) avg /= num; return avg; } private float variance(float[] arr, float val) { float V = 0; for (int i = 0; i < arr.length; i++) V += PApplet.pow(arr[i] - val, 2); V /= arr.length; return V; } }