Home | History | Annotate | Download | only in cts
      1 /*
      2  * Copyright (C) 2016 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package android.media.cts;
     18 
     19 import android.media.cts.R;
     20 
     21 import android.app.Instrumentation;
     22 import android.content.res.AssetFileDescriptor;
     23 import android.content.res.Resources;
     24 import android.media.cts.DecoderTest.AudioParameter;
     25 import android.media.MediaCodec;
     26 import android.media.MediaCodecInfo;
     27 import android.media.MediaCodecInfo.CodecCapabilities;
     28 import android.media.MediaExtractor;
     29 import android.media.MediaFormat;
     30 import android.support.test.InstrumentationRegistry;
     31 import android.util.Log;
     32 
     33 import com.android.compatibility.common.util.CtsAndroidTestCase;
     34 
     35 import static org.junit.Assert.*;
     36 import org.junit.Before;
     37 import org.junit.Rule;
     38 import org.junit.Test;
     39 
     40 import java.io.IOException;
     41 import java.nio.ByteBuffer;
     42 import java.util.Arrays;
     43 import java.util.List;
     44 
     45 public class DecoderTestAacDrc {
     46     private static final String TAG = "DecoderTestAacDrc";
     47 
     48     private Resources mResources;
     49 
     50     @Before
     51     public void setUp() throws Exception {
     52         final Instrumentation inst = InstrumentationRegistry.getInstrumentation();
     53         assertNotNull(inst);
     54         mResources = inst.getContext().getResources();
     55     }
     56 
     57     /**
     58      * Verify correct decoding of MPEG-4 AAC with output level normalization to -23dBFS.
     59      */
     60     @Test
     61     public void testDecodeAacDrcLevelM4a() throws Exception {
     62         AudioParameter decParams = new AudioParameter();
     63         // full boost, full cut, target ref level: -23dBFS, heavy compression: no
     64         DrcParams drcParams = new DrcParams(127, 127, 92, 0);
     65         short[] decSamples = decodeToMemory(decParams, R.raw.sine_2ch_48khz_aot5_drclevel_mp4,
     66                 -1, null, drcParams);
     67         DecoderTest decTester = new DecoderTest();
     68         decTester.checkEnergy(decSamples, decParams, 2, 0.70f);
     69     }
     70 
     71     /**
     72      * Verify correct decoding of MPEG-4 AAC with Dynamic Range Control (DRC) metadata.
     73      * Fully apply light compression DRC (default settings).
     74      */
     75     @Test
     76     public void testDecodeAacDrcFullM4a() throws Exception {
     77         AudioParameter decParams = new AudioParameter();
     78         short[] decSamples = decodeToMemory(decParams, R.raw.sine_2ch_48khz_aot5_drcfull_mp4,
     79                 -1, null, null);
     80         DecoderTest decTester = new DecoderTest();
     81         decTester.checkEnergy(decSamples, decParams, 2, 0.80f);
     82     }
     83 
     84     /**
     85      * Verify correct decoding of MPEG-4 AAC with Dynamic Range Control (DRC) metadata.
     86      * Apply only half of the light compression DRC and normalize to -20dBFS output level.
     87      */
     88     @Test
     89     public void testDecodeAacDrcHalfM4a() throws Exception {
     90         AudioParameter decParams = new AudioParameter();
     91         // half boost, half cut, target ref level: -20dBFS, heavy compression: no
     92         DrcParams drcParams = new DrcParams(63, 63, 80, 0);
     93         short[] decSamples = decodeToMemory(decParams, R.raw.sine_2ch_48khz_aot2_drchalf_mp4,
     94                 -1, null, drcParams);
     95         DecoderTest decTester = new DecoderTest();
     96         decTester.checkEnergy(decSamples, decParams, 2, 0.80f);
     97     }
     98 
     99     /**
    100      * Verify correct decoding of MPEG-4 AAC with Dynamic Range Control (DRC) metadata.
    101      * Disable light compression DRC to test if MediaFormat keys reach the decoder.
    102      */
    103     @Test
    104     public void testDecodeAacDrcOffM4a() throws Exception {
    105         AudioParameter decParams = new AudioParameter();
    106         // no boost, no cut, target ref level: -16dBFS, heavy compression: no
    107         DrcParams drcParams = new DrcParams(0, 0, 64, 0);       // normalize to -16dBFS
    108         short[] decSamples = decodeToMemory(decParams, R.raw.sine_2ch_48khz_aot5_drcoff_mp4,
    109                 -1, null, drcParams);
    110         DecoderTest decTester = new DecoderTest();
    111         decTester.checkEnergy(decSamples, decParams, 2, 0.80f);
    112     }
    113 
    114     /**
    115      * Verify correct decoding of MPEG-4 AAC with Dynamic Range Control (DRC) metadata.
    116      * Apply heavy compression gains and normalize to -16dBFS output level.
    117      */
    118     @Test
    119     public void testDecodeAacDrcHeavyM4a() throws Exception {
    120         AudioParameter decParams = new AudioParameter();
    121         // full boost, full cut, target ref level: -16dBFS, heavy compression: yes
    122         DrcParams drcParams = new DrcParams(127, 127, 64, 1);
    123         short[] decSamples = decodeToMemory(decParams, R.raw.sine_2ch_48khz_aot2_drcheavy_mp4,
    124                 -1, null, drcParams);
    125         DecoderTest decTester = new DecoderTest();
    126         decTester.checkEnergy(decSamples, decParams, 2, 0.80f);
    127     }
    128 
    129     /**
    130      * Test signal limiting (without clipping) of MPEG-4 AAC decoder with the help of DRC metadata.
    131      * Uses a two channel 248 Hz sine tone at 48 kHz sampling rate for input.
    132      */
    133     @Test
    134     public void testDecodeAacDrcClipM4a() throws Exception {
    135         AudioParameter decParams = new AudioParameter();
    136         short[] decSamples = decodeToMemory(decParams, R.raw.sine_2ch_48khz_aot5_drcclip_mp4,
    137                 -1, null, null);
    138         checkClipping(decSamples, decParams, 248.0f /* Hz */);
    139     }
    140 
    141 
    142     /**
    143      *  Internal utilities
    144      */
    145 
    146     /**
    147      * The test routine performs a THD+N (Total Harmonic Distortion + Noise) analysis on a given
    148      * audio signal (decSamples). The THD+N value is defined here as harmonic distortion (+ noise)
    149      * RMS over full signal RMS.
    150      *
    151      * After the energy measurement of the unprocessed signal the routine creates and applies a
    152      * notch filter at the given frequency (sineFrequency). Afterwards the signal energy is
    153      * measured again. Then the THD+N value is calculated as the ratio of the filtered and the full
    154      * signal energy.
    155      *
    156      * The test passes if the THD+N value is lower than -60 dB. Otherwise it fails.
    157      *
    158      * @param decSamples the decoded audio samples to be tested
    159      * @param decParams the audio parameters of the given audio samples (decSamples)
    160      * @param sineFrequency frequency of the test signal tone used for testing
    161      * @throws RuntimeException
    162      */
    163     private void checkClipping(short[] decSamples, AudioParameter decParams, float sineFrequency)
    164             throws RuntimeException
    165     {
    166         final double threshold_clipping = -60.0; // dB
    167         final int numChannels = decParams.getNumChannels();
    168         final int startSample = 2 * 2048 * numChannels;          // exclude signal on- & offset to
    169         final int stopSample = decSamples.length - startSample;  // ... measure only the stationary
    170                                                                  // ... sine tone
    171         // get full energy of signal (all channels)
    172         double nrgFull = getEnergy(decSamples, startSample, stopSample);
    173 
    174         // create notch filter to suppress sine-tone at 248 Hz
    175         Biquad filter = new Biquad(sineFrequency, decParams.getSamplingRate());
    176         for (int channel = 0; channel < numChannels; channel++) {
    177             // apply notch-filter on buffer for each channel to filter out the sine tone.
    178             // only the harmonics (and noise) remain. */
    179             filter.apply(decSamples, channel, numChannels);
    180         }
    181 
    182         // get energy of harmonic distortion (signal without sine-tone)
    183         double nrgHd = getEnergy(decSamples, startSample, stopSample);
    184 
    185         // Total Harmonic Distortion + Noise, defined here as harmonic distortion (+ noise) RMS
    186         // over full signal RMS, given in dB
    187         double THDplusN = 10 * Math.log10(nrgHd / nrgFull);
    188         assertTrue("signal has clipping samples", THDplusN <= threshold_clipping);
    189     }
    190 
    191     /**
    192      * Measure the energy of a given signal over all channels within a given signal range.
    193      * @param signal audio signal samples
    194      * @param start start offset of the measuring range
    195      * @param stop stop sample which is the last sample of the measuring range
    196      * @return the signal energy in the given range
    197      */
    198     private double getEnergy(short[] signal, int start, int stop) {
    199         double nrg = 0.0;
    200         for (int sample = start; sample < stop; sample++) {
    201             double v = signal[sample];
    202             nrg += v * v;
    203         }
    204         return nrg;
    205     }
    206 
    207     // Notch filter implementation
    208     private class Biquad {
    209         // filter coefficients for biquad filter (2nd order IIR filter)
    210         float[] a;
    211         float[] b;
    212         // filter states
    213         float[] state_ff;
    214         float[] state_fb;
    215 
    216         protected float alpha = 0.95f;
    217 
    218         public Biquad(float f_notch, float f_s) {
    219             // Create filter coefficients of notch filter which suppresses a sine tone with f_notch
    220             // Hz at sampling frequency f_s. Zeros placed at unit circle at f_notch, poles placed
    221             // nearby the unit circle at f_notch.
    222             state_ff = new float[2];
    223             state_fb = new float[2];
    224             state_ff[0] = state_ff[1] = state_fb[0] = state_fb[1] = 0.0f;
    225 
    226             a = new float[3];
    227             b = new float[3];
    228             double omega = 2.0 * Math.PI * f_notch / f_s;
    229             a[0] = b[0] = b[2] = 1.0f;
    230             a[1] = -2.0f * alpha * (float)Math.cos(omega);
    231             a[2] = alpha * alpha;
    232             b[1] = -2.0f * (float)Math.cos(omega);
    233         }
    234 
    235         public void apply(short[] signal, int offset, int stride) {
    236             // reset states
    237             state_ff[0] = state_ff[1] = 0.0f;
    238             state_fb[0] = state_fb[1] = 0.0f;
    239             // process 2nd order IIR filter in Direct Form I
    240             float x_0, x_1, x_2, y_0, y_1, y_2;
    241             x_2 = state_ff[0];  // x[n-2]
    242             x_1 = state_ff[1];  // x[n-1]
    243             y_2 = state_fb[0];  // y[n-2]
    244             y_1 = state_fb[1];  // y[n-1]
    245             for (int sample = offset; sample < signal.length; sample += stride) {
    246                 x_0 = signal[sample];
    247                 y_0 = b[0] * x_0 + b[1] * x_1 + b[2] * x_2
    248                         - a[1] * y_1 - a[2] * y_2;
    249                 x_2 = x_1;
    250                 x_1 = x_0;
    251                 y_2 = y_1;
    252                 y_1 = y_0;
    253                 signal[sample] = (short)y_0;
    254             }
    255             state_ff[0] = x_2;  // next x[n-2]
    256             state_ff[1] = x_1;  // next x[n-1]
    257             state_fb[0] = y_2;  // next y[n-2]
    258             state_fb[1] = y_1;  // next y[n-1]
    259         }
    260     }
    261 
    262 
    263     /**
    264      *  Class handling all MPEG-4 Dynamic Range Control (DRC) parameter relevant for testing
    265      */
    266     private class DrcParams {
    267         int boost;                          // scaling of boosting gains
    268         int cut;                            // scaling of compressing gains
    269         int decoderTargetLevel;             // desired target output level (for normalization)
    270         int heavy;                          // en-/disable heavy compression
    271 
    272         public DrcParams() {
    273             this.boost = 127;               // no scaling
    274             this.cut = 127;                 // no scaling
    275             this.decoderTargetLevel = 64;   // -16.0 dBFs
    276             this.heavy = 1;                 // enabled
    277         }
    278 
    279         public DrcParams(int boost, int cut, int decoderTargetLevel, int heavy) {
    280             this.boost = boost;
    281             this.cut = cut;
    282             this.decoderTargetLevel = decoderTargetLevel;
    283             this.heavy = heavy;
    284         }
    285     }
    286 
    287 
    288     // TODO: code is the same as in DecoderTest, differences are:
    289     //          - addition of application of DRC parameters
    290     //          - no need/use of resetMode, configMode
    291     //       Split method so code can be shared
    292     private short[] decodeToMemory(AudioParameter audioParams, int testinput,
    293             int eossample, List<Long> timestamps, DrcParams drcParams)
    294             throws IOException
    295     {
    296         String localTag = TAG + "#decodeToMemory";
    297         short [] decoded = new short[0];
    298         int decodedIdx = 0;
    299 
    300         AssetFileDescriptor testFd = mResources.openRawResourceFd(testinput);
    301 
    302         MediaExtractor extractor;
    303         MediaCodec codec;
    304         ByteBuffer[] codecInputBuffers;
    305         ByteBuffer[] codecOutputBuffers;
    306 
    307         extractor = new MediaExtractor();
    308         extractor.setDataSource(testFd.getFileDescriptor(), testFd.getStartOffset(),
    309                 testFd.getLength());
    310         testFd.close();
    311 
    312         assertEquals("wrong number of tracks", 1, extractor.getTrackCount());
    313         MediaFormat format = extractor.getTrackFormat(0);
    314         String mime = format.getString(MediaFormat.KEY_MIME);
    315         assertTrue("not an audio file", mime.startsWith("audio/"));
    316 
    317         MediaFormat configFormat = format;
    318         codec = MediaCodec.createDecoderByType(mime);
    319 
    320         // set DRC parameters
    321         if (drcParams != null) {
    322             configFormat.setInteger(MediaFormat.KEY_AAC_DRC_BOOST_FACTOR, drcParams.boost);
    323             configFormat.setInteger(MediaFormat.KEY_AAC_DRC_ATTENUATION_FACTOR, drcParams.cut);
    324             configFormat.setInteger(MediaFormat.KEY_AAC_DRC_TARGET_REFERENCE_LEVEL,
    325                     drcParams.decoderTargetLevel);
    326             configFormat.setInteger(MediaFormat.KEY_AAC_DRC_HEAVY_COMPRESSION, drcParams.heavy);
    327         }
    328         Log.v(localTag, "configuring with " + configFormat);
    329         codec.configure(configFormat, null /* surface */, null /* crypto */, 0 /* flags */);
    330 
    331         codec.start();
    332         codecInputBuffers = codec.getInputBuffers();
    333         codecOutputBuffers = codec.getOutputBuffers();
    334 
    335         extractor.selectTrack(0);
    336 
    337         // start decoding
    338         final long kTimeOutUs = 5000;
    339         MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
    340         boolean sawInputEOS = false;
    341         boolean sawOutputEOS = false;
    342         int noOutputCounter = 0;
    343         int samplecounter = 0;
    344         while (!sawOutputEOS && noOutputCounter < 50) {
    345             noOutputCounter++;
    346             if (!sawInputEOS) {
    347                 int inputBufIndex = codec.dequeueInputBuffer(kTimeOutUs);
    348 
    349                 if (inputBufIndex >= 0) {
    350                     ByteBuffer dstBuf = codecInputBuffers[inputBufIndex];
    351 
    352                     int sampleSize =
    353                         extractor.readSampleData(dstBuf, 0 /* offset */);
    354 
    355                     long presentationTimeUs = 0;
    356 
    357                     if (sampleSize < 0 && eossample > 0) {
    358                         fail("test is broken: never reached eos sample");
    359                     }
    360                     if (sampleSize < 0) {
    361                         Log.d(TAG, "saw input EOS.");
    362                         sawInputEOS = true;
    363                         sampleSize = 0;
    364                     } else {
    365                         if (samplecounter == eossample) {
    366                             sawInputEOS = true;
    367                         }
    368                         samplecounter++;
    369                         presentationTimeUs = extractor.getSampleTime();
    370                     }
    371                     codec.queueInputBuffer(
    372                             inputBufIndex,
    373                             0 /* offset */,
    374                             sampleSize,
    375                             presentationTimeUs,
    376                             sawInputEOS ? MediaCodec.BUFFER_FLAG_END_OF_STREAM : 0);
    377 
    378                     if (!sawInputEOS) {
    379                         extractor.advance();
    380                     }
    381                 }
    382             }
    383 
    384             int res = codec.dequeueOutputBuffer(info, kTimeOutUs);
    385 
    386             if (res >= 0) {
    387                 //Log.d(TAG, "got frame, size " + info.size + "/" + info.presentationTimeUs);
    388 
    389                 if (info.size > 0) {
    390                     noOutputCounter = 0;
    391                     if (timestamps != null) {
    392                         timestamps.add(info.presentationTimeUs);
    393                     }
    394                 }
    395 
    396                 int outputBufIndex = res;
    397                 ByteBuffer buf = codecOutputBuffers[outputBufIndex];
    398 
    399                 if (decodedIdx + (info.size / 2) >= decoded.length) {
    400                     decoded = Arrays.copyOf(decoded, decodedIdx + (info.size / 2));
    401                 }
    402 
    403                 buf.position(info.offset);
    404                 for (int i = 0; i < info.size; i += 2) {
    405                     decoded[decodedIdx++] = buf.getShort();
    406                 }
    407 
    408                 codec.releaseOutputBuffer(outputBufIndex, false /* render */);
    409 
    410                 if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
    411                     Log.d(TAG, "saw output EOS.");
    412                     sawOutputEOS = true;
    413                 }
    414             } else if (res == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
    415                 codecOutputBuffers = codec.getOutputBuffers();
    416 
    417                 Log.d(TAG, "output buffers have changed.");
    418             } else if (res == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
    419                 MediaFormat oformat = codec.getOutputFormat();
    420                 audioParams.setNumChannels(oformat.getInteger(MediaFormat.KEY_CHANNEL_COUNT));
    421                 audioParams.setSamplingRate(oformat.getInteger(MediaFormat.KEY_SAMPLE_RATE));
    422                 Log.d(TAG, "output format has changed to " + oformat);
    423             } else {
    424                 Log.d(TAG, "dequeueOutputBuffer returned " + res);
    425             }
    426         }
    427         if (noOutputCounter >= 50) {
    428             fail("decoder stopped outputing data");
    429         }
    430 
    431         codec.stop();
    432         codec.release();
    433         return decoded;
    434     }
    435 
    436 }
    437 
    438