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.processor;
034
035import org.openimaj.audio.AudioStream;
036import org.openimaj.audio.SampleChunk;
037
038/**
039 * Provides an audio processor that will process sample chunks of specific sizes
040 * when the incoming stream's sample chunk size is unknown.
041 * <p>
042 * This class has applications for FFT (for example) where the input sample size
043 * must be a power of 2 and the underlying audio stream reader may be returning
044 * sample chunks of any size.
045 * <p>
046 * The processor can also provide overlapping sample windows. Call
047 * {@link #setWindowStep(int)} to determine the slide of each sliding window. If
048 * this is set to 0 or below, the windows will be consecutive and will not
049 * overlap.
050 * <p>
051 * The only assumption made by the class about the samples is that they are
052 * whole numbers of bytes (8, 16, 24, 32 bits etc.). This is a pretty reasonable
053 * assumption.
054 *
055 * @author David Dupplaw (dpd@ecs.soton.ac.uk)
056 *
057 * @created 11 Jul 2011
058 */
059public class FixedSizeSampleAudioProcessor extends AudioProcessor
060{
061        /** The size of each required sample chunk */
062        private int requiredSampleSetSize = 512;
063
064        /** Our buffer of sample chunks stored between calls to process() */
065        private SampleChunk sampleBuffer = null;
066
067        /** The number of samples overlap required between each window */
068        private int windowStep = 0;
069
070        /** Whether or not the windows are overlapping */
071        private boolean overlapping = false;
072
073        /**
074         * Create processor that will process chunks of the given size.
075         *
076         * @param sizeRequired
077         *            The size of the chunks required (in samples)
078         */
079        public FixedSizeSampleAudioProcessor(final int sizeRequired)
080        {
081                this.requiredSampleSetSize = sizeRequired;
082        }
083
084        /**
085         * Create processor that will process chunks of the given size.
086         *
087         * @param stream
088         *            An audio stream to process
089         * @param sizeRequired
090         *            The size of the chunks required (in samples)
091         */
092        public FixedSizeSampleAudioProcessor(final AudioStream stream, final int sizeRequired)
093        {
094                super(stream);
095                this.requiredSampleSetSize = sizeRequired;
096        }
097
098        /**
099         * Constructor that takes the size of the window and the size of the window
100         * overlap.
101         *
102         * @param nSamplesInWindow
103         *            The number of samples in the window
104         * @param nSamplesOverlap
105         *            The size of the overlap
106         */
107        public FixedSizeSampleAudioProcessor(final int nSamplesInWindow,
108                        final int nSamplesOverlap)
109        {
110                this(nSamplesInWindow);
111                this.setWindowStep(nSamplesOverlap);
112        }
113
114        /**
115         * Chainable constructor that takes the size of the window and the number of
116         * samples overlap.
117         *
118         * @param as
119         *            The chained audio stream
120         * @param nSamplesInWindow
121         *            Samples in window
122         * @param nSamplesOverlap
123         *            Samples in window overlap
124         */
125        public FixedSizeSampleAudioProcessor(final AudioStream as, final int nSamplesInWindow,
126                        final int nSamplesOverlap)
127        {
128                this(as, nSamplesInWindow);
129                this.setWindowStep(nSamplesOverlap);
130        }
131
132        /**
133         * {@inheritDoc}
134         *
135         * @see org.openimaj.audio.processor.AudioProcessor#nextSampleChunk()
136         */
137        @Override
138        public SampleChunk nextSampleChunk()
139        {
140                // System.out.println( "Sample Buffer: "+(sampleBuffer != null?
141                // sampleBuffer.getNumberOfSamples() : "null"));
142
143                // Get the samples. If there's more samples than we need in the
144                // buffer, we'll just use that, otherwise we'll get a new sample
145                // chunk from the stream.
146                SampleChunk s = null;
147                if (this.sampleBuffer != null &&
148                                this.sampleBuffer.getNumberOfSamples() >= this.requiredSampleSetSize)
149                {
150                        s = this.sampleBuffer;
151                        this.sampleBuffer = null;
152                }
153                else
154                {
155                        s = this.getUnderlyingStream().nextSampleChunk();
156                        if (s != null)
157                                s = s.clone();
158
159                        // If we have something in our buffer, prepend it to the new
160                        // sample chunk
161                        if (this.sampleBuffer != null && this.sampleBuffer.getNumberOfSamples() > 0
162                                        && s != null)
163                        {
164                                // Prepend the contents of the sample buffer to the new sample
165                                // chunk
166                                s.prepend(this.sampleBuffer);
167                                this.sampleBuffer = null;
168                        }
169                }
170
171                // Sample buffer will always be null here
172                // It will be reinstated later with the left-overs after processing.
173                // From this point on we'll only work on the SampleChunk s.
174
175                // Catch the end of the stream. As the sample buffer is always empty
176                // at this point, the only time s can be null is that if the
177                // nextSampleChunk() above returned null. In which case, there's no
178                // more audio, so we return null.
179                if (s == null)
180                {
181                        if (this.sampleBuffer != null)
182                        {
183                                s = this.sampleBuffer;
184                                this.sampleBuffer = null;
185                                return s;
186                        }
187                        else
188                                return null;
189                }
190
191                // Now check how many samples we have to start with
192                int nSamples = s.getNumberOfSamples();
193
194                // If we don't have enough samples, we'll keep getting chunks until
195                // we have enough or until the end of the stream is reached.
196                boolean endOfStream = false;
197                while (!endOfStream && nSamples < this.requiredSampleSetSize)
198                {
199                        final SampleChunk nextSamples = this.getUnderlyingStream().nextSampleChunk();
200                        if (nextSamples != null)
201                        {
202                                // Append the new samples onto the end of the sample chunk
203                                s.append(nextSamples);
204
205                                // Check how many samples we now have.
206                                nSamples = s.getNumberOfSamples();
207                        }
208                        else
209                                endOfStream = true;
210                }
211
212                // If we have the right number of samples,
213                // or we've got to the end of the stream
214                // then we just return the chunk we have.
215                SampleChunk ss;
216                if (!endOfStream && (this.overlapping || nSamples > this.requiredSampleSetSize))
217                {
218                        // We must now have too many samples...
219                        // Store the excess back into the buffer
220                        int start = 0;
221                        if (this.overlapping)
222                                start = this.windowStep;
223                        else
224                                start = this.requiredSampleSetSize;
225
226                        // Store the rest into the sample buffer
227                        this.sampleBuffer = s.getSampleSlice(start, nSamples - start);
228
229                        // Process a slice of the sample chunk
230                        ss = s.getSampleSlice(0, this.requiredSampleSetSize);
231                }
232                else
233                {
234                        ss = s;
235
236                        if (ss.getNumberOfSamples() < this.requiredSampleSetSize)
237                                ss.pad(this.requiredSampleSetSize);
238                }
239
240                try
241                {
242                        // Return the processed samples
243                        return this.process(ss);
244                } catch (final Exception e)
245                {
246                        // If there's an error, log it and return the unprocessed samples
247                        e.printStackTrace();
248                        return ss;
249                }
250        }
251
252        /**
253         * Set the step of each overlapping window.
254         *
255         * @param overlap
256         *            The step of each overlapping window.
257         */
258        public void setWindowStep(final int overlap)
259        {
260                this.windowStep = overlap;
261                this.overlapping = true;
262                if (overlap <= 0)
263                        this.overlapping = false;
264        }
265
266        /**
267         * Returns the step of each overlapping window.
268         *
269         * @return The step of each overlapping window.
270         */
271        public int getWindowStep()
272        {
273                return this.windowStep;
274        }
275
276        /**
277         * Set the size of the window required. Should be called before the object
278         * has been used to process anything. The result is undefined if it's called
279         * during processing and will probably lead to some sort of bounds error.
280         *
281         * @param sizeRequired
282         *            The size required.
283         */
284        public void setWindowSize(final int sizeRequired)
285        {
286                this.requiredSampleSetSize = sizeRequired;
287        }
288
289        /**
290         * Returns the size of the sample window.
291         *
292         * @return The size of the sample window.
293         */
294        public int getWindowSize()
295        {
296                return this.requiredSampleSetSize;
297        }
298
299        /**
300         * Returns whether the windows are overlapping or not.
301         *
302         * @return whether the windows are overlapping or not.
303         */
304        public boolean isOverlapping()
305        {
306                return this.overlapping;
307        }
308
309        /**
310         * {@inheritDoc}
311         *
312         * The default operation of the {@link FixedSizeSampleAudioProcessor} is
313         * simply to change the shape of the sample chunk. You may override this
314         * method to process the samples directly.
315         *
316         * @see org.openimaj.audio.processor.AudioProcessor#process(org.openimaj.audio.SampleChunk)
317         */
318        @Override
319        public SampleChunk process(final SampleChunk sample) throws Exception
320        {
321                return sample;
322        }
323}