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}