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