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; 034 035import java.util.ArrayList; 036import java.util.HashMap; 037import java.util.List; 038import java.util.Map; 039 040import org.openimaj.audio.processor.AudioProcessor; 041import org.openimaj.audio.processor.FixedSizeSampleAudioProcessor; 042import org.openimaj.audio.samples.SampleBuffer; 043import org.openimaj.audio.samples.SampleBufferFactory; 044import org.openimaj.audio.timecode.AudioTimecode; 045 046/** 047 * A basic audio mixer that takes a number of {@link AudioStream}s and mixes 048 * then with some gain compensation into a single audio stream. 049 * 050 * @author David Dupplaw (dpd@ecs.soton.ac.uk) 051 * @created 23rd November 2011 052 */ 053public class AudioMixer extends AudioStream 054{ 055 /** 056 * A listener for objects that wish to be informed of a mix event. The mix 057 * event provides the sample buffers of all the channels and the sample 058 * buffer of the mixed stream. It is called before the mixed stream chunk is 059 * returned from each mix event. 060 * 061 * @author David Dupplaw (dpd@ecs.soton.ac.uk) 062 * 063 * @created 29 Nov 2011 064 */ 065 public interface MixEventListener 066 { 067 /** 068 * Callback for a mix event. 069 * 070 * @param channels 071 * the channels being mixed 072 * @param mix 073 * the mixed channel 074 */ 075 public void mix(SampleBuffer[] channels, SampleBuffer mix); 076 } 077 078 /** A list of the audio streams to mix in this mixer */ 079 private final List<AudioStream> streams = new ArrayList<AudioStream>(); 080 private final List<Float> gain = new ArrayList<Float>(); 081 private final Map<AudioStream, AudioStream> streamMap = new HashMap<AudioStream, AudioStream>(); 082 083 /** The currently processed sample in the mixer */ 084 private SampleBuffer currentSample = null; 085 086 /** The size of each mix - the sample buffer size */ 087 private int bufferSize = 256; 088 089 /** 090 * If set to TRUE, this will cause the mixer to run even when there are not 091 * streams to play. It does this by returning empty sample chunks. 092 */ 093 private boolean alwaysRun = true; 094 095 /** The time the mixer started */ 096 private long startMillis = -1; 097 098 /** The timecode we're using */ 099 private AudioTimecode timecode = null; 100 101 /** Listeners of the mix event */ 102 private final List<MixEventListener> mixEventListeners = 103 new ArrayList<MixEventListener>(); 104 105 /** Whether or not to fire mix events */ 106 private boolean fireMixEvents = false; 107 108 /** 109 * Default constructor that takes the format for the samples. All streams 110 * added to this mixer must conform to that sample format. 111 * 112 * @param af 113 * The {@link AudioFormat} 114 */ 115 public AudioMixer(final AudioFormat af) 116 { 117 this.setFormat(af); 118 119 // Create the current sample chunk that we'll reuse 120 this.currentSample = SampleBufferFactory.createSampleBuffer(af, this.bufferSize); 121 122 this.timecode = new AudioTimecode(0); 123 } 124 125 /** 126 * The timecode object 127 * 128 * @param tc 129 * The timecode object. 130 */ 131 public void setTimecodeObject(final AudioTimecode tc) 132 { 133 this.timecode = tc; 134 } 135 136 /** 137 * Add an {@link AudioStream} to this mixer. It must conform to the same 138 * format as this mixer. If not, an {@link IllegalArgumentException} will be 139 * thrown. 140 * 141 * @param as The {@link AudioStream} to add to this mixer. 142 * @param defaultGain 143 * The default gain of this stream. 144 */ 145 public void addStream(final AudioStream as, final float defaultGain) 146 { 147 if (as.format.equals(this.getFormat())) 148 { 149 AudioStream stream = as; 150 151 // It's important that the incoming sample chunks from 152 // the input streams are equal in length, so we wrap them 153 // all in FixedSampleSizeAudioProcessor. However, before we 154 // do we check whether they already are fixed sized chunks. 155 // We can't check with just a instanceof because that will also 156 // be true for subclasses and we can't be sure they're doing more. 157 // So, we must check ONLY for instances of EXACTLY 158 // FixedSampleSizeAudioProcessors. 159 if (stream.getClass().getName().equals( 160 FixedSizeSampleAudioProcessor.class.getName())) 161 { 162 // Get the underlying stream. 163 stream = ((AudioProcessor) as).getUnderlyingStream(); 164 } 165 166 // Set the gain 167 this.gain.add(defaultGain); 168 169 // Add the stream wrapped in a fixed size audio processor. 170 synchronized (this.streams) 171 { 172 // Wrap the stream in a FixedSizeSampleAudioProcessor 173 final FixedSizeSampleAudioProcessor fssap = 174 new FixedSizeSampleAudioProcessor(stream, this.bufferSize); 175 this.streams.add(fssap); 176 this.streamMap.put(as, fssap); 177 } 178 } 179 else 180 throw new IllegalArgumentException("Format of added stream is " + 181 "incompatible with the mixer."); 182 } 183 184 /** 185 * {@inheritDoc} 186 * 187 * @see org.openimaj.audio.AudioStream#nextSampleChunk() 188 */ 189 @Override 190 synchronized public SampleChunk nextSampleChunk() 191 { 192 // If there are no streams attached to this mixer, then 193 // we return null - end of mixer stream. 194 if (this.streams.size() == 0 && !this.alwaysRun) 195 return null; 196 197 // Set the time the mixer started 198 if (this.startMillis == -1) 199 this.startMillis = System.currentTimeMillis(); 200 201 // Get the next sample chunk from each stream. 202 final SampleBuffer sb = this.currentSample; 203 SampleChunk sc = null; 204 205 final List<SampleBuffer> chunkList = new ArrayList<SampleBuffer>(); 206 synchronized (this.streams) 207 { 208 for (int stream = 0; stream < this.streams.size(); stream++) 209 { 210 // We can do this because the sample chunks from all the streams 211 // are forced to be the same size! 212 sc = this.streams.get(stream).nextSampleChunk(); 213 214 // Get the next chunk and add it to a list for going through 215 // later 216 if (sc != null) 217 { 218 // System.out.println( 219 // this+" Stream "+stream+" size "+sc.getNumberOfSamples() 220 // ); 221 chunkList.add(sc.getSampleBuffer()); 222 } 223 else 224 { 225 // Got to the end of the stream, so we'll remove it 226 this.streams.remove(stream); 227 this.gain.remove(stream); 228 } 229 } 230 231 // System.out.println( chunkList +" -> "+this.gain ); 232 233 // Now create the new sample chunk by averaging the samples 234 // at each point from all streams 235 for (int i = 0; i < sb.size(); i++) 236 { 237 float Z = 0; 238 for (int stream = 0; stream < chunkList.size(); stream++) 239 if (chunkList.get(stream) != null) 240 Z += chunkList.get(stream).get(i) * this.gain.get(stream); 241 242 // Set the value in the new sample buffer 243 sb.set(i, Z); 244 } 245 } 246 247 // Fire the mix event 248 if (this.fireMixEvents) 249 for (final MixEventListener mel : this.mixEventListeners) 250 mel.mix(chunkList.toArray(new SampleBuffer[0]), sb); 251 252 // Create a SampleChunk for our mix stream 253 sc = sb.getSampleChunk(); 254 255 this.timecode.setTimecodeInMilliseconds(System.currentTimeMillis() - 256 this.startMillis); 257 sc.setStartTimecode(this.timecode); 258 259 return sc; 260 } 261 262 /** 263 * {@inheritDoc} 264 * 265 * @see org.openimaj.audio.AudioStream#reset() 266 */ 267 @Override 268 public void reset() 269 { 270 // No implementation 271 } 272 273 /** 274 * Set the size of the buffer that the mixer will mix. Note that this must 275 * be done before any streams are added to the mixer. 276 * 277 * @param bufferSize 278 * The buffer size in samples per channel. 279 */ 280 public void setBufferSize(final int bufferSize) 281 { 282 this.bufferSize = bufferSize; 283 this.currentSample = SampleBufferFactory.createSampleBuffer( 284 this.format, bufferSize); 285 } 286 287 /** 288 * Whether to run the mixer when there are no audio streams to mix. 289 * 290 * @param alwaysRun 291 * TRUE to make the mixer always run. 292 */ 293 public void setAlwaysRun(final boolean alwaysRun) 294 { 295 this.alwaysRun = alwaysRun; 296 } 297 298 /** 299 * Add a mix event listener to this AudioMixer. 300 * 301 * @param mel 302 * The {@link MixEventListener} to add 303 */ 304 public void addMixEventListener(final MixEventListener mel) 305 { 306 this.mixEventListeners.add(mel); 307 } 308 309 /** 310 * Remove the given {@link MixEventListener} from this mixer. 311 * 312 * @param mel 313 * The {@link MixEventListener} to remove 314 */ 315 public void removeMixEventListener(final MixEventListener mel) 316 { 317 this.mixEventListeners.remove(mel); 318 } 319 320 /** 321 * {@inheritDoc} 322 * 323 * @see org.openimaj.audio.AudioStream#getLength() 324 */ 325 @Override 326 public long getLength() 327 { 328 return -1; 329 } 330 331 /** 332 * Whether to fire mix events or not (default is that the mixer doesn't) 333 * 334 * @param tf 335 * TRUE to fire mix events. 336 */ 337 public void setMixEvents(final boolean tf) 338 { 339 this.fireMixEvents = tf; 340 } 341 342 /** 343 * Remove the given audio stream from the mixer. 344 * 345 * @param as The audio stream to remove 346 */ 347 public void removeStream(final AudioStream as) 348 { 349 synchronized (this.streams) 350 { 351 AudioStream aas = this.streamMap.get(as); 352 if (aas == null) 353 aas = as; 354 355 System.out.println("Removing " + aas + " from " + this.streams); 356 this.gain.remove(this.streams.indexOf(aas)); 357 this.streams.remove(this.streams.indexOf(aas)); 358 } 359 } 360}