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}