-
-
Notifications
You must be signed in to change notification settings - Fork 49
Expand file tree
/
Copy pathBeatDetector.java
More file actions
261 lines (228 loc) · 7.97 KB
/
BeatDetector.java
File metadata and controls
261 lines (228 loc) · 7.97 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
package processing.sound;
import com.jsyn.engine.SynthesisEngine;
import com.jsyn.ports.UnitInputPort;
import com.jsyn.ports.UnitOutputPort;
import com.jsyn.ports.UnitVariablePort;
import com.jsyn.unitgen.UnitGenerator;
import processing.core.PApplet;
/**
* The BeatDetector analyzer looks for spikes in the energy of an audio signal
* which are often associated with rhythmic musical beats and can be used to trigger a
* response whenever the incoming audio signal pulses. Note that this
* implementation does not return a tempo or BPM (beats per minute) value — it
* can only tell you whether the current moment of audio contains a beat or not.
*
* @webref Analysis:BeatDetector
* @webBrief Looks for spikes in the energy of an audio signal
* which are often associated with rhythmic musical beats and can be used to trigger a
* response whenever the incoming audio signal pulses.
**/
public class BeatDetector extends Analyzer {
private final BeatDetectorUGen detector;
/**
* @param parent Typically "this"
*/
public BeatDetector(PApplet parent) {
super(parent);
this.detector = new BeatDetectorUGen();
}
@Override
protected void removeInput() {
this.input = null;
}
@Override
protected void setInput(UnitOutputPort input) {
Engine.getEngine().add(this.detector);
this.detector.start();
this.detector.input.connect(input);
}
/**
* Returns <code>true</code> if the current moment of the audio signal
* contains a beat, <code>false</code> otherwise.<br />
* A "beat" is defined as a spike in the energy of the audio signal - it may
* or may not coincide exactly with a musical beat.
*
* @webref Analysis:BeatDetector
* @webBrief Returns whether or not the current moment of audio contains a beat or not.
*/
public boolean isBeat() {
return this.detector.current.getValue() == 1;
}
/**
* Sets the sensitivity, in milliseconds, of the beat detection algorithm.
* The sensitivity determines how long the detector will wait after detecting
* a beat to detect the next one. For example, a sensitivity of 10 will cause the
* detector to wait 10ms before returning any new beats.
*
* A higher sensitivity value means the algorithm will be less sensitive. You
* can tune this appropriately if you notice the detector returning too many
* false positive beats.
*
* @webref Analysis:BeatDetector
* @webBrief Sets the sensitivity, in milliseconds, of the beat detection algorithm.
*
* @param sensitivity Sensitivity in milliseconds. Must be a positive number.
*/
public void sensitivity(int sensitivity) {
this.detector.sensitivity.set(sensitivity);
}
/**
* Sets the sensitivity of the beat detector.
* @webref Analysis:BeatDetector
* @webBrief Sets the sensitivity of the beat detector.
*
* @return The sensitivity in milliseconds.
*/
public int sensitivity() {
return (int) this.detector.sensitivity.get();
}
public double[] getEnergyBuffer() {
return detector.getEnergyBuffer();
}
public int getEnergyCursor() {
return detector.getEnergyCursor();
}
public boolean[] getBeatBuffer() {
return detector.getBeatBuffer();
}
public class BeatDetectorUGen extends UnitGenerator {
private static final int CHUNK_SIZE = 1024;
public UnitInputPort input;
public UnitVariablePort current;
public UnitInputPort sensitivity;
public UnitOutputPort output;
private final double[] audioBuffer;
private double[] energyBuffer;
private double[] deltaBuffer;
private boolean[] beatBuffer;
private int audioBufferCursor;
private int energyBufferCursor;
private long detectTimeMillis;
private long sensitivityTimer;
public BeatDetectorUGen() {
this.addPort(this.input = new UnitInputPort("Input"));
this.addPort(this.current = new UnitVariablePort("Current"));
this.addPort(this.output = new UnitOutputPort("Output"));
this.addPort(this.sensitivity = new UnitInputPort("Sensitivity"));
sensitivity.set(10);
audioBuffer = new double[CHUNK_SIZE];
}
@Override
public void setSynthesisEngine(SynthesisEngine synthesisEngine) {
super.setSynthesisEngine(synthesisEngine);
int frameRate = synthesisEngine.getFrameRate();
int bufferSize = frameRate / CHUNK_SIZE;
energyBuffer = new double[bufferSize];
deltaBuffer = new double[bufferSize];
beatBuffer = new boolean[bufferSize];
}
public void generate(int start, int limit) {
double[] inputs = input.getValues();
double[] outputs = output.getValues();
for (int i = start; i < limit; i++) {
double inputValue = inputs[i];
audioBuffer[audioBufferCursor] = inputs[i];
++audioBufferCursor;
// When it is full, do the FFT.
if (audioBufferCursor == audioBuffer.length) {
boolean beatDetected = detect(audioBuffer);
current.set(beatDetected ? 1 : 0);
audioBufferCursor = 0;
}
outputs[i] = inputValue;
}
}
// This algorithm is adapted from Damien Quartz's Minim audio library
// http://code.compartmental.net/tools/minim/
private boolean detect(double[] samples) {
// compute the energy level
float level = 0;
for (int i = 0; i < samples.length; i++) {
level += (samples[i] * samples[i]);
}
level /= samples.length;
level = (float) Math.sqrt(level);
float instant = level * 100;
// compute the average local energy
float E = average(energyBuffer);
// compute the variance of the energies in eBuffer
float V = variance(energyBuffer, E);
// compute C using a linear digression of C with V
float C = (-0.0025714f * V) + 1.5142857f;
// filter negative values
float diff = Math.max(instant - C * E, 0);
// find the average of only the positive values in dBuffer
float dAvg = specAverage(deltaBuffer);
// filter negative values
float diff2 = Math.max(diff - dAvg, 0);
// report false if it's been less than 'sensitivity'
// milliseconds since the last true value
boolean beatDetected = false;
if (detectTimeMillis - sensitivityTimer < sensitivity.get()) {
beatDetected = 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) {
beatDetected = true;
sensitivityTimer = detectTimeMillis;
}
// OMG it wasn't true!
else {
beatDetected = false;
}
energyBuffer[energyBufferCursor] = instant;
deltaBuffer[energyBufferCursor] = diff;
beatBuffer[energyBufferCursor] = beatDetected;
energyBufferCursor++;
if (energyBufferCursor == energyBuffer.length) {
energyBufferCursor = 0;
}
// advance the current time by the number of milliseconds this buffer represents
detectTimeMillis += (long) (((float) samples.length / getFrameRate()) * 1000);
return beatDetected;
}
private float average(double[] arr) {
float avg = 0;
for (int i = 0; i < arr.length; i++) {
avg += arr[i];
}
avg /= arr.length;
return avg;
}
private float specAverage(double[] 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(double[] arr, float val) {
float v = 0;
for (int i = 0; i < arr.length; i++) {
v += (float) Math.pow(arr[i] - val, 2);
}
v /= arr.length;
return v;
}
public double[] getEnergyBuffer() {
return energyBuffer;
}
public double[] getDeltaBuffer() {
return deltaBuffer;
}
public boolean[] getBeatBuffer() {
return beatBuffer;
}
public int getEnergyCursor() {
return energyBufferCursor;
}
}
}