View Javadoc

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 }