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 }