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.conversion;
034
035import org.openimaj.audio.AudioFormat;
036import org.openimaj.audio.AudioStream;
037import org.openimaj.audio.SampleChunk;
038import org.openimaj.audio.processor.AudioProcessor;
039import org.openimaj.audio.samples.SampleBuffer;
040import org.openimaj.audio.samples.SampleBufferFactory;
041import org.openimaj.math.util.Interpolation;
042
043/**
044 *      A sample rate conversion audio processing class. There is an enumerator
045 *      within the class that is publically available for determining the
046 *      algorithm for sample rate conversion. This defaults to
047 *      {@link SampleRateConversionAlgorithm#LINEAR_INTERPOLATION}.
048 *      <p>
049 *      To use the class, instantiate using the default constructor or the
050 *      chainable constructor. Both of these constructors take the algorithm for
051 *      sample rate conversion as well as the output format. The output format
052 *      must have the same number of bits and same number of channels as the
053 *      expected input otherwise the {@link #process(SampleChunk)} method
054 *      will throw {@link IllegalArgumentException}. The input format for the samples
055 *      is expected to be provided as part of the {@link SampleChunk}.
056 *      <p>
057 *      The class itself checks whether the output format and the input format
058 *      are the same (in which case the sample does not need to be resampled).
059 *      That means the algorithm implementation does not need to do this.
060 *
061 *  @author David Dupplaw (dpd@ecs.soton.ac.uk)
062 *
063 *      @created 18 Jun 2012
064 */
065public class SampleRateConverter extends AudioProcessor
066{
067        /**
068         *      An enumerator of the different sample rate conversion algorithms
069         *      available in this sample rate converter.
070         *
071         *  @author David Dupplaw (dpd@ecs.soton.ac.uk)
072         *
073         *      @created 18 Jun 2012
074         */
075        public enum SampleRateConversionAlgorithm
076        {
077                /**
078                 *      Performs linear interpolation between samples where the sample rate
079                 *      in the output format is greater than the sample rate of the input
080                 *      format. If the sample rate is less, then the nearest value
081                 *      of the input samples is used.
082                 */
083                LINEAR_INTERPOLATION
084                {
085                        @Override
086            public SampleChunk process( final SampleChunk s, final AudioFormat output)
087            {
088                                final AudioFormat input = s.getFormat();
089
090                                // Check to see if the input and output are the same
091                                if( input.getSampleRateKHz() == output.getSampleRateKHz() )
092                                        return s;
093
094                                // Work out the size of the output sample chunk
095                                final double scalar = input.getSampleRateKHz() / output.getSampleRateKHz();
096                                final SampleBuffer sbin = s.getSampleBuffer();
097                                final double size = sbin.size() / scalar;
098
099                                if( this.sbout == null || this.sbout.size() != (int)size )
100                                {
101                                        this.sbout = SampleBufferFactory.createSampleBuffer(
102                                                output, (int)size );
103                                        this.sbout.setFormat( output );
104                                }
105
106                                // If the input format has a greater sample rate than the
107                                // output format - down sampling (scalar > 1)
108                                if( scalar > 1 )
109                                {
110                                        for( int i = 0; i < this.sbout.size(); i++ )
111                                                this.sbout.set( i, sbin.get( (int)(i * scalar) ) );
112                                        return this.sbout.getSampleChunk();
113                                }
114                                // If the input format has a sample rate less than that
115                                // of the output - up sampling (scalar < 1)
116                                else
117                                {
118                                        // Linear interpolate each sample value
119                                        for( int i = 0; i < this.sbout.size()-1; i++ )
120                                        {
121                                                final int inputSampleX = (int)(i * scalar);
122                                                this.sbout.set( i, Interpolation.lerp( (float)(i*scalar),
123                                                                inputSampleX, sbin.get(inputSampleX),
124                                                                inputSampleX+1, sbin.get(inputSampleX+1) ) );
125                                        }
126                                        this.sbout.set( this.sbout.size()-1, sbin.get(sbin.size()-1) );
127                                        return this.sbout.getSampleChunk();
128                                }
129            }
130                };
131
132                protected SampleBuffer sbout = null;
133
134                /**
135                 *      Process a sample chunk and output a sample chunk in the given
136                 *      output format.
137                 *
138                 *  @param s The input sample chunk
139                 *  @param output The output format
140                 *  @return A resampled sample chunk.
141                 */
142                public abstract SampleChunk process( SampleChunk s, AudioFormat output );
143        }
144
145        /**
146         * Sample rate conversion defaults to
147         * {@link SampleRateConversionAlgorithm#LINEAR_INTERPOLATION}
148         */
149        private SampleRateConversionAlgorithm sampleConverter =
150                SampleRateConversionAlgorithm.LINEAR_INTERPOLATION;
151
152        /** The output format to which sample chunks will be converted */
153        private AudioFormat outputFormat = null;
154
155        /**
156         *      Default constructor that takes the input conversion
157         *  @param converter The converter to use
158         *  @param outputFormat The output format to convert to
159         */
160        public SampleRateConverter( final SampleRateConversionAlgorithm converter,
161                        final AudioFormat outputFormat )
162    {
163                this.sampleConverter = converter;
164                this.outputFormat = outputFormat;
165                this.setFormat( outputFormat );
166    }
167
168        /**
169         *      Chainable constructor.
170         *
171         *  @param as The audio stream to process
172         *  @param converter The converter to use
173         *  @param outputFormat The output format to convert to
174         */
175        public SampleRateConverter( final AudioStream as, final SampleRateConversionAlgorithm converter,
176                        final AudioFormat outputFormat )
177        {
178                super( as );
179                this.sampleConverter = converter;
180                this.outputFormat = outputFormat;
181                this.setFormat( outputFormat );
182        }
183
184        /**
185         *      {@inheritDoc}
186         *      @see org.openimaj.audio.processor.AudioProcessor#process(org.openimaj.audio.SampleChunk)
187         */
188        @Override
189        public SampleChunk process( final SampleChunk sample ) throws Exception
190        {
191                if( sample.getFormat().getNBits() != this.outputFormat.getNBits() )
192                        throw new IllegalArgumentException( "The number of bits in the " +
193                                        "output format is not the same as the sample chunk. Use a " +
194                                        "resampling conversion first before using the sample-rate " +
195                                        "converter." );
196
197                if( sample.getFormat().getNumChannels() != this.outputFormat.getNumChannels() )
198                        throw new IllegalArgumentException( "The number of channels in the " +
199                                        "output format is not the same as the sample chunk. Use a " +
200                                        "channel converter first before using the sample-rate " +
201                                        "converter." );
202
203                if( sample.getFormat().getSampleRateKHz() == this.outputFormat.getSampleRateKHz() )
204                        return sample;
205
206                final SampleChunk sc = this.sampleConverter.process( sample, this.outputFormat );
207                sc.setStartTimecode( sample.getStartTimecode() );
208                return sc;
209        }
210}