001/**
002 * Copyright (c) 2011, The University of Southampton and the individual contributors.
003 * All rights reserved.
004 *
005 * Redistribution and use in source and binary forms, with or without modification,
006 * are permitted provided that the following conditions are met:
007 *
008 *   *  Redistributions of source code must retain the above copyright notice,
009 *      this list of conditions and the following disclaimer.
010 *
011 *   *  Redistributions in binary form must reproduce the above copyright notice,
012 *      this list of conditions and the following disclaimer in the documentation
013 *      and/or other materials provided with the distribution.
014 *
015 *   *  Neither the name of the University of Southampton nor the names of its
016 *      contributors may be used to endorse or promote products derived from this
017 *      software without specific prior written permission.
018 *
019 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
020 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
021 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
022 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
023 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
024 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
025 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
026 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
027 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
028 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
029 */
030/**
031 * 
032 */
033package org.openimaj.audio.analysis;
034
035import org.openimaj.audio.AudioFormat;
036import org.openimaj.audio.AudioStream;
037import org.openimaj.audio.SampleChunk;
038import org.openimaj.audio.processor.AudioProcessor;
039import org.openimaj.audio.samples.SampleBuffer;
040import org.openimaj.audio.timecode.AudioTimecode;
041
042/**
043 *      A beat detector that uses a 2nd order LP filter, followed by an envelope 
044 *      detector (thanks Bram), feeding a Schmitt trigger. The rising edge detector 
045 *      provides a 1-sample pulse each time a beat is detected. The class also 
046 *      provides beat detection per sample chunk.
047 * 
048 *      @see "http://www.musicdsp.org/showArchiveComment.php?ArchiveID=200"
049 *      @author David Dupplaw (dpd@ecs.soton.ac.uk)
050 *      
051 *      @created 30 Nov 2011
052 */
053public class BeatDetector extends AudioProcessor
054{
055        /** Filter coefficient */
056        private float kBeatFilter;
057
058        private float filter1Out, filter2Out;
059
060        /** Release time coefficient */
061        private float beatRelease;
062
063        /** Peak envelope follower */
064        private float peakEnv;
065
066        /** Schmitt trigger output */
067        private boolean beatTrigger;
068
069        /** Rising edge memory */
070        private boolean prevBeatPulse;
071
072        /** Beat detector output */
073        private boolean beatPulse;
074
075        /** The timecode of the detected beat */
076        private final AudioTimecode beatTimecode = new AudioTimecode(0);
077        
078        /** Whether a beat has been detected within a sample chunk */
079        private boolean beatDetected = false;
080
081        /** Low Pass filter frequency */
082        public static final float FREQ_LP_BEAT = 150.0f;
083
084        /** Low Pass filter time constant */
085        public static final float T_FILTER = (float) (1.0f / (2.0f * Math.PI 
086                        * BeatDetector.FREQ_LP_BEAT));
087
088        /** Release time of envelope detector in seconds */
089        public static final float BEAT_RTIME = 0.02f;
090        
091        /**
092         *      Default constructor
093         *      @param af The format of the incoming data.
094         */
095        public BeatDetector( final AudioFormat af )
096        {
097                this( null, af );
098        }
099        
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}