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;
34  
35  import java.util.ArrayList;
36  import java.util.HashMap;
37  import java.util.List;
38  import java.util.Map;
39  
40  import org.openimaj.audio.processor.AudioProcessor;
41  import org.openimaj.audio.processor.FixedSizeSampleAudioProcessor;
42  import org.openimaj.audio.samples.SampleBuffer;
43  import org.openimaj.audio.samples.SampleBufferFactory;
44  import org.openimaj.audio.timecode.AudioTimecode;
45  
46  /**
47   * A basic audio mixer that takes a number of {@link AudioStream}s and mixes
48   * then with some gain compensation into a single audio stream.
49   * 
50   * @author David Dupplaw (dpd@ecs.soton.ac.uk)
51   * @created 23rd November 2011
52   */
53  public class AudioMixer extends AudioStream
54  {
55  	/**
56  	 * A listener for objects that wish to be informed of a mix event. The mix
57  	 * event provides the sample buffers of all the channels and the sample
58  	 * buffer of the mixed stream. It is called before the mixed stream chunk is
59  	 * returned from each mix event.
60  	 * 
61  	 * @author David Dupplaw (dpd@ecs.soton.ac.uk)
62  	 * 
63  	 * @created 29 Nov 2011
64  	 */
65  	public interface MixEventListener
66  	{
67  		/**
68  		 * Callback for a mix event.
69  		 * 
70  		 * @param channels
71  		 *            the channels being mixed
72  		 * @param mix
73  		 *            the mixed channel
74  		 */
75  		public void mix(SampleBuffer[] channels, SampleBuffer mix);
76  	}
77  
78  	/** A list of the audio streams to mix in this mixer */
79  	private final List<AudioStream> streams = new ArrayList<AudioStream>();
80  	private final List<Float> gain = new ArrayList<Float>();
81  	private final Map<AudioStream, AudioStream> streamMap = new HashMap<AudioStream, AudioStream>();
82  
83  	/** The currently processed sample in the mixer */
84  	private SampleBuffer currentSample = null;
85  
86  	/** The size of each mix - the sample buffer size */
87  	private int bufferSize = 256;
88  
89  	/**
90  	 * If set to TRUE, this will cause the mixer to run even when there are not
91  	 * streams to play. It does this by returning empty sample chunks.
92  	 */
93  	private boolean alwaysRun = true;
94  
95  	/** The time the mixer started */
96  	private long startMillis = -1;
97  
98  	/** The timecode we're using */
99  	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 }