1 /**
2 * Copyright (c) 2011, The University of Southampton and the individual contributors.
3 * All rights reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without modification,
6 * are permitted provided that the following conditions are met:
7 *
8 * * Redistributions of source code must retain the above copyright notice,
9 * this list of conditions and the following disclaimer.
10 *
11 * * Redistributions in binary form must reproduce the above copyright notice,
12 * this list of conditions and the following disclaimer in the documentation
13 * and/or other materials provided with the distribution.
14 *
15 * * Neither the name of the University of Southampton nor the names of its
16 * contributors may be used to endorse or promote products derived from this
17 * software without specific prior written permission.
18 *
19 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
20 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
21 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
23 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
24 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
25 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
26 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
28 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 */
30 /**
31 *
32 */
33 package org.openimaj.audio.analysis;
34
35 import org.openimaj.audio.AudioFormat;
36 import org.openimaj.audio.AudioStream;
37 import org.openimaj.audio.SampleChunk;
38 import org.openimaj.audio.processor.AudioProcessor;
39 import org.openimaj.audio.samples.SampleBuffer;
40 import org.openimaj.audio.timecode.AudioTimecode;
41
42 /**
43 * A beat detector that uses a 2nd order LP filter, followed by an envelope
44 * detector (thanks Bram), feeding a Schmitt trigger. The rising edge detector
45 * provides a 1-sample pulse each time a beat is detected. The class also
46 * provides beat detection per sample chunk.
47 *
48 * @see "http://www.musicdsp.org/showArchiveComment.php?ArchiveID=200"
49 * @author David Dupplaw (dpd@ecs.soton.ac.uk)
50 *
51 * @created 30 Nov 2011
52 */
53 public class BeatDetector extends AudioProcessor
54 {
55 /** Filter coefficient */
56 private float kBeatFilter;
57
58 private float filter1Out, filter2Out;
59
60 /** Release time coefficient */
61 private float beatRelease;
62
63 /** Peak envelope follower */
64 private float peakEnv;
65
66 /** Schmitt trigger output */
67 private boolean beatTrigger;
68
69 /** Rising edge memory */
70 private boolean prevBeatPulse;
71
72 /** Beat detector output */
73 private boolean beatPulse;
74
75 /** The timecode of the detected beat */
76 private final AudioTimecode beatTimecode = new AudioTimecode(0);
77
78 /** Whether a beat has been detected within a sample chunk */
79 private boolean beatDetected = false;
80
81 /** Low Pass filter frequency */
82 public static final float FREQ_LP_BEAT = 150.0f;
83
84 /** Low Pass filter time constant */
85 public static final float T_FILTER = (float) (1.0f / (2.0f * Math.PI
86 * BeatDetector.FREQ_LP_BEAT));
87
88 /** Release time of envelope detector in seconds */
89 public static final float BEAT_RTIME = 0.02f;
90
91 /**
92 * Default constructor
93 * @param af The format of the incoming data.
94 */
95 public BeatDetector( final AudioFormat af )
96 {
97 this( null, af );
98 }
99
100 /**
101 * Chainable constructor
102 * @param as The audio stream to process
103 */
104 public BeatDetector( final AudioStream as )
105 {
106 this( as, as.getFormat() );
107 }
108
109 /**
110 * Chainable constructor.
111 * @param as The audio stream to process
112 * @param af The format to process.
113 */
114 protected BeatDetector( final AudioStream as, final AudioFormat af )
115 {
116 super( as );
117 this.filter1Out = 0.0f;
118 this.filter2Out = 0.0f;
119 this.peakEnv = 0.0f;
120 this.beatTrigger = false;
121 this.prevBeatPulse = false;
122 this.format = af;
123 this.setSampleRate( (float)(af.getSampleRateKHz()*1000f) );
124 }
125
126
127 /**
128 * Set the sample rate of the incoming data.
129 * @param sampleRate The sample rate
130 */
131 private void setSampleRate( final float sampleRate )
132 {
133 this.kBeatFilter = (float) (1.0 / (sampleRate * BeatDetector.T_FILTER));
134 this.beatRelease = (float) Math.exp( -1.0f / (sampleRate * BeatDetector.BEAT_RTIME) );
135 }
136
137 /**
138 * {@inheritDoc}
139 * @see org.openimaj.audio.processor.AudioProcessor#process(org.openimaj.audio.SampleChunk)
140 */
141 @Override
142 public SampleChunk process( final SampleChunk samples )
143 {
144 // Detect beats. Note that we stop as soon as we detect a beat.
145 this.beatDetected = false;
146 final SampleBuffer sb = samples.getSampleBuffer();
147 int i = 0;
148 for(; i < sb.size(); i++ )
149 {
150 if( this.beatDetected = this.processSample( sb.get(i) ) )
151 break;
152 }
153
154 if( this.beatDetected() )
155 this.beatTimecode.setTimecodeInMilliseconds( (long)(
156 samples.getStartTimecode().getTimecodeInMilliseconds() +
157 i * this.format.getSampleRateKHz() ) );
158
159 // System.out.println( beatDetected );
160
161 // We return the samples unaltered
162 return samples;
163 }
164
165 /**
166 * Process a given sample.
167 * @param input The sample to process.
168 * @return TRUE if a beat was detected at this sample
169 */
170 private boolean processSample( final float in )
171 {
172 float EnvIn;
173
174 final float input = in / Integer.MAX_VALUE;
175
176 // Step 1 : 2nd order low pass filter (made of two 1st order RC filter)
177 this.filter1Out = this.filter1Out + (this.kBeatFilter * (input - this.filter1Out));
178 this.filter2Out = this.filter2Out + (this.kBeatFilter * (this.filter1Out - this.filter2Out));
179
180 // Step 2 : peak detector
181 EnvIn = Math.abs( this.filter2Out );
182 if( EnvIn > this.peakEnv )
183 this.peakEnv = EnvIn; // Attack time = 0
184 else
185 {
186 this.peakEnv *= this.beatRelease;
187 this.peakEnv += (1.0f - this.beatRelease) * EnvIn;
188 }
189
190 // Step 3 : Schmitt trigger
191 if( !this.beatTrigger )
192 {
193 if( this.peakEnv > 0.3 ) this.beatTrigger = true;
194 }
195 else
196 {
197 if( this.peakEnv < 0.15 ) this.beatTrigger = false;
198 }
199
200 // Step 4 : rising edge detector
201 this.beatPulse = false;
202 if( (this.beatTrigger) && (!this.prevBeatPulse) ) this.beatPulse = true;
203 this.prevBeatPulse = this.beatTrigger;
204
205 return this.beatPulse;
206 }
207
208 /**
209 * Returns whether a beat was detected within this sample chunk.
210 * @return TRUE if a beat was detected
211 */
212 public boolean beatDetected()
213 {
214 return this.beatDetected;
215 }
216
217 /**
218 * Returns the timecode at which the first beat in this sample chunk
219 * was detected. Note that this class reuses this timecode class, so if
220 * you wish to use it afterwards you should clone it immediately after
221 * calling this function.
222 *
223 * @return The beat timecode.
224 */
225 public AudioTimecode getBeatTimecode()
226 {
227 return this.beatTimecode;
228 }
229 }