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 static org.junit.Assert.assertEquals;
20 import static org.junit.Assert.assertNotNull;
21 import static org.junit.Assert.assertTrue;
22 import static org.junit.Assert.fail;
23 
24 import android.app.Instrumentation;
25 import android.content.res.AssetFileDescriptor;
26 import android.content.res.Resources;
27 import android.media.MediaCodec;
28 import android.media.MediaExtractor;
29 import android.media.MediaFormat;
30 import android.media.cts.DecoderTest.AudioParameter;
31 import android.media.cts.R;
32 import android.util.Log;
33 
34 import androidx.test.InstrumentationRegistry;
35 
36 import org.junit.Before;
37 import org.junit.Test;
38 
39 import java.io.IOException;
40 import java.nio.ByteBuffer;
41 import java.util.ArrayList;
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
setUp()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
testDecodeAacDrcLevelM4a()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, null /*decoderName: use default decoder*/);
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
testDecodeAacDrcFullM4a()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, null /*decoderName: use default decoder*/);
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
testDecodeAacDrcHalfM4a()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, null /*decoderName: use default decoder*/);
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
testDecodeAacDrcOffM4a()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, null /*decoderName: use default decoder*/);
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
testDecodeAacDrcHeavyM4a()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, null /*decoderName: use default decoder*/);
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
testDecodeAacDrcClipM4a()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, null /*decoderName: use default decoder*/);
138         checkClipping(decSamples, decParams, 248.0f /* Hz */);
139     }
140 
141     /**
142      * Default decoder target level.
143      * The actual default value used by the decoder can differ between platforms, or even devices,
144      * but tests will measure energy relative to this value.
145      */
146     public static final int DEFAULT_DECODER_TARGET_LEVEL = 64; // -16.0 dBFs
147 
148     /**
149      * Test USAC decoder with different target loudness levels
150      */
151     @Test
testDecodeUsacLoudnessM4a()152     public void testDecodeUsacLoudnessM4a() throws Exception {
153         Log.v(TAG, "START testDecodeUsacLoudnessM4a");
154 
155         ArrayList<String> aacDecoderNames = DecoderTestXheAac.initAacDecoderNames();
156         assertTrue("No AAC decoder found", aacDecoderNames.size() > 0);
157 
158         for (String aacDecName : aacDecoderNames) {
159             // test default loudness
160             // decoderTargetLevel = 64 --> target output level = -16.0 dBFs
161             try {
162                 checkUsacLoudness(DEFAULT_DECODER_TARGET_LEVEL, 1, 1.0f, aacDecName);
163             } catch (Exception e) {
164                 Log.v(TAG, "testDecodeUsacLoudnessM4a for default loudness failed for " +
165                         aacDecName);
166                 throw new RuntimeException(e);
167             }
168 
169             // test loudness boost
170             // decoderTargetLevel = 40 --> target output level = -10.0 dBFs
171             // normFactor = 1/(10^(-6/10)) = 3.98f
172             //   where "-6" is the difference between the default level (-16), and -10 for this test
173             try {
174                 checkUsacLoudness(40, 1, (float)(1.0f/Math.pow(10.0f, -6.0f/10.0f)), aacDecName);
175             } catch (Exception e) {
176                 Log.v(TAG, "testDecodeUsacLoudnessM4a for loudness boost failed for " + aacDecName);
177                 throw new RuntimeException(e);
178             }
179 
180             // test loudness attenuation
181             // decoderTargetLevel = 96 --> target output level = -24.0 dBFs
182             // normFactor = 1/(10^(8/10)) = 0.15f
183             //     where 8 is the difference between the default level (-16), and -24 for this test
184             try {
185                 checkUsacLoudness(96, 0, (float)(1.0f/Math.pow(10.0f, 8.0f/10.0f)), aacDecName);
186             } catch (Exception e) {
187                 Log.v(TAG, "testDecodeUsacLoudnessM4a for loudness attenuation failed for "
188                         + aacDecName);
189                 throw new RuntimeException(e);
190             }
191         }
192     }
193 
194     /**
195      *  Internal utilities
196      */
197 
198     /**
199      * The test routine performs a THD+N (Total Harmonic Distortion + Noise) analysis on a given
200      * audio signal (decSamples). The THD+N value is defined here as harmonic distortion (+ noise)
201      * RMS over full signal RMS.
202      *
203      * After the energy measurement of the unprocessed signal the routine creates and applies a
204      * notch filter at the given frequency (sineFrequency). Afterwards the signal energy is
205      * measured again. Then the THD+N value is calculated as the ratio of the filtered and the full
206      * signal energy.
207      *
208      * The test passes if the THD+N value is lower than -60 dB. Otherwise it fails.
209      *
210      * @param decSamples the decoded audio samples to be tested
211      * @param decParams the audio parameters of the given audio samples (decSamples)
212      * @param sineFrequency frequency of the test signal tone used for testing
213      * @throws RuntimeException
214      */
checkClipping(short[] decSamples, AudioParameter decParams, float sineFrequency)215     private void checkClipping(short[] decSamples, AudioParameter decParams, float sineFrequency)
216             throws RuntimeException
217     {
218         final double threshold_clipping = -60.0; // dB
219         final int numChannels = decParams.getNumChannels();
220         final int startSample = 2 * 2048 * numChannels;          // exclude signal on- & offset to
221         final int stopSample = decSamples.length - startSample;  // ... measure only the stationary
222                                                                  // ... sine tone
223         // get full energy of signal (all channels)
224         double nrgFull = getEnergy(decSamples, startSample, stopSample);
225 
226         // create notch filter to suppress sine-tone at 248 Hz
227         Biquad filter = new Biquad(sineFrequency, decParams.getSamplingRate());
228         for (int channel = 0; channel < numChannels; channel++) {
229             // apply notch-filter on buffer for each channel to filter out the sine tone.
230             // only the harmonics (and noise) remain. */
231             filter.apply(decSamples, channel, numChannels);
232         }
233 
234         // get energy of harmonic distortion (signal without sine-tone)
235         double nrgHd = getEnergy(decSamples, startSample, stopSample);
236 
237         // Total Harmonic Distortion + Noise, defined here as harmonic distortion (+ noise) RMS
238         // over full signal RMS, given in dB
239         double THDplusN = 10 * Math.log10(nrgHd / nrgFull);
240         assertTrue("signal has clipping samples", THDplusN <= threshold_clipping);
241     }
242 
243     /**
244      * Measure the energy of a given signal over all channels within a given signal range.
245      * @param signal audio signal samples
246      * @param start start offset of the measuring range
247      * @param stop stop sample which is the last sample of the measuring range
248      * @return the signal energy in the given range
249      */
getEnergy(short[] signal, int start, int stop)250     private double getEnergy(short[] signal, int start, int stop) {
251         double nrg = 0.0;
252         for (int sample = start; sample < stop; sample++) {
253             double v = signal[sample];
254             nrg += v * v;
255         }
256         return nrg;
257     }
258 
259     // Notch filter implementation
260     private class Biquad {
261         // filter coefficients for biquad filter (2nd order IIR filter)
262         float[] a;
263         float[] b;
264         // filter states
265         float[] state_ff;
266         float[] state_fb;
267 
268         protected float alpha = 0.95f;
269 
Biquad(float f_notch, float f_s)270         public Biquad(float f_notch, float f_s) {
271             // Create filter coefficients of notch filter which suppresses a sine tone with f_notch
272             // Hz at sampling frequency f_s. Zeros placed at unit circle at f_notch, poles placed
273             // nearby the unit circle at f_notch.
274             state_ff = new float[2];
275             state_fb = new float[2];
276             state_ff[0] = state_ff[1] = state_fb[0] = state_fb[1] = 0.0f;
277 
278             a = new float[3];
279             b = new float[3];
280             double omega = 2.0 * Math.PI * f_notch / f_s;
281             a[0] = b[0] = b[2] = 1.0f;
282             a[1] = -2.0f * alpha * (float)Math.cos(omega);
283             a[2] = alpha * alpha;
284             b[1] = -2.0f * (float)Math.cos(omega);
285         }
286 
apply(short[] signal, int offset, int stride)287         public void apply(short[] signal, int offset, int stride) {
288             // reset states
289             state_ff[0] = state_ff[1] = 0.0f;
290             state_fb[0] = state_fb[1] = 0.0f;
291             // process 2nd order IIR filter in Direct Form I
292             float x_0, x_1, x_2, y_0, y_1, y_2;
293             x_2 = state_ff[0];  // x[n-2]
294             x_1 = state_ff[1];  // x[n-1]
295             y_2 = state_fb[0];  // y[n-2]
296             y_1 = state_fb[1];  // y[n-1]
297             for (int sample = offset; sample < signal.length; sample += stride) {
298                 x_0 = signal[sample];
299                 y_0 = b[0] * x_0 + b[1] * x_1 + b[2] * x_2
300                         - a[1] * y_1 - a[2] * y_2;
301                 x_2 = x_1;
302                 x_1 = x_0;
303                 y_2 = y_1;
304                 y_1 = y_0;
305                 signal[sample] = (short)y_0;
306             }
307             state_ff[0] = x_2;  // next x[n-2]
308             state_ff[1] = x_1;  // next x[n-1]
309             state_fb[0] = y_2;  // next y[n-2]
310             state_fb[1] = y_1;  // next y[n-1]
311         }
312     }
313 
314     /**
315      * USAC test DRC loudness
316      */
checkUsacLoudness(int decoderTargetLevel, int heavy, float normFactor, String decoderName)317     private void checkUsacLoudness(int decoderTargetLevel, int heavy, float normFactor,
318             String decoderName) throws Exception {
319         AudioParameter decParams = new AudioParameter();
320         DrcParams drcParams_def  = new DrcParams(127, 127, DEFAULT_DECODER_TARGET_LEVEL, 1);
321         DrcParams drcParams_test = new DrcParams(127, 127, decoderTargetLevel, heavy);
322 
323         short[] decSamples_def = decodeToMemory(decParams, R.raw.noise_2ch_48khz_aot42_19_lufs_mp4,
324                 -1, null, drcParams_def, decoderName);
325         short[] decSamples_test = decodeToMemory(decParams, R.raw.noise_2ch_48khz_aot42_19_lufs_mp4,
326                 -1, null, drcParams_test, decoderName);
327 
328         DecoderTestXheAac decTesterXheAac = new DecoderTestXheAac();
329         float[] nrg_def  = decTesterXheAac.checkEnergyUSAC(decSamples_def, decParams, 2, 1);
330         float[] nrg_test = decTesterXheAac.checkEnergyUSAC(decSamples_test, decParams, 2, 1);
331 
332         float[] nrgThreshold = {2602510595620.0f, 2354652443657.0f};
333 
334         // Check default loudness behavior
335         if (nrg_def[0] > nrgThreshold[0] || nrg_def[0] < nrgThreshold[1]) {
336             throw new Exception("Default loudness behavior not as expected");
337         }
338 
339         float nrgRatio = nrg_def[0]/nrg_test[0];
340 
341         // Check for loudness boost/attenuation if decoderTargetLevel deviates from default value
342         // used in these tests (note that the default target level can change from platform
343         // to platform, or device to device)
344         if ((decoderTargetLevel < DEFAULT_DECODER_TARGET_LEVEL) // boosted loudness
345                 && (nrg_def[0] > nrg_test[0])) {
346             throw new Exception("Signal not attenuated");
347         } else if ((decoderTargetLevel > DEFAULT_DECODER_TARGET_LEVEL) // attenuated loudness
348                 && (nrg_def[0] < nrg_test[0])) {
349             throw new Exception("Signal not boosted");
350         }
351         nrgRatio = nrgRatio * normFactor;
352 
353         // Check whether loudness behavior is as expected
354         if (nrgRatio > 1.05f || nrgRatio < 0.95f ){
355             throw new Exception("Loudness behavior not as expected");
356         }
357     }
358 
359 
360     /**
361      *  Class handling all MPEG-4 and MPEG-D Dynamic Range Control (DRC) parameter relevant for testing
362      */
363     protected static class DrcParams {
364         int mBoost;                          // scaling of boosting gains
365         int mCut;                            // scaling of compressing gains
366         int mDecoderTargetLevel;             // desired target output level (for normalization)
367         int mHeavy;                          // en-/disable heavy compression
368         int mEffectType;                     // MPEG-D DRC Effect Type
369 
DrcParams()370         public DrcParams() {
371             mBoost = 127;               // no scaling
372             mCut   = 127;               // no scaling
373             mHeavy = 1;                 // enabled
374         }
375 
DrcParams(int boost, int cut, int decoderTargetLevel, int heavy)376         public DrcParams(int boost, int cut, int decoderTargetLevel, int heavy) {
377             mBoost = boost;
378             mCut = cut;
379             mDecoderTargetLevel = decoderTargetLevel;
380             mHeavy = heavy;
381         }
382 
DrcParams(int boost, int cut, int decoderTargetLevel, int heavy, int effectType)383         public DrcParams(int boost, int cut, int decoderTargetLevel, int heavy, int effectType) {
384             mBoost = boost;
385             mCut = cut;
386             mDecoderTargetLevel = decoderTargetLevel;
387             mHeavy = heavy;
388             mEffectType = effectType;
389         }
390     }
391 
392 
393     // TODO: code is the same as in DecoderTest, differences are:
394     //          - addition of application of DRC parameters
395     //          - no need/use of resetMode, configMode
396     //       Split method so code can be shared
decodeToMemory(AudioParameter audioParams, int testinput, int eossample, List<Long> timestamps, DrcParams drcParams, String decoderName)397     private short[] decodeToMemory(AudioParameter audioParams, int testinput,
398             int eossample, List<Long> timestamps, DrcParams drcParams, String decoderName)
399             throws IOException
400     {
401         String localTag = TAG + "#decodeToMemory";
402         short [] decoded = new short[0];
403         int decodedIdx = 0;
404 
405         AssetFileDescriptor testFd = mResources.openRawResourceFd(testinput);
406 
407         MediaExtractor extractor;
408         MediaCodec codec;
409         ByteBuffer[] codecInputBuffers;
410         ByteBuffer[] codecOutputBuffers;
411 
412         extractor = new MediaExtractor();
413         extractor.setDataSource(testFd.getFileDescriptor(), testFd.getStartOffset(),
414                 testFd.getLength());
415         testFd.close();
416 
417         assertEquals("wrong number of tracks", 1, extractor.getTrackCount());
418         MediaFormat format = extractor.getTrackFormat(0);
419         String mime = format.getString(MediaFormat.KEY_MIME);
420         assertTrue("not an audio file", mime.startsWith("audio/"));
421 
422         MediaFormat configFormat = format;
423         if (decoderName == null) {
424             codec = MediaCodec.createDecoderByType(mime);
425         } else {
426             codec = MediaCodec.createByCodecName(decoderName);
427         }
428 
429         // set DRC parameters
430         if (drcParams != null) {
431             configFormat.setInteger(MediaFormat.KEY_AAC_DRC_BOOST_FACTOR, drcParams.mBoost);
432             configFormat.setInteger(MediaFormat.KEY_AAC_DRC_ATTENUATION_FACTOR, drcParams.mCut);
433             if (drcParams.mDecoderTargetLevel != 0) {
434                 configFormat.setInteger(MediaFormat.KEY_AAC_DRC_TARGET_REFERENCE_LEVEL,
435                         drcParams.mDecoderTargetLevel);
436             }
437             configFormat.setInteger(MediaFormat.KEY_AAC_DRC_HEAVY_COMPRESSION, drcParams.mHeavy);
438         }
439         Log.v(localTag, "configuring with " + configFormat);
440         codec.configure(configFormat, null /* surface */, null /* crypto */, 0 /* flags */);
441 
442         codec.start();
443         codecInputBuffers = codec.getInputBuffers();
444         codecOutputBuffers = codec.getOutputBuffers();
445 
446         extractor.selectTrack(0);
447 
448         // start decoding
449         final long kTimeOutUs = 5000;
450         MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
451         boolean sawInputEOS = false;
452         boolean sawOutputEOS = false;
453         int noOutputCounter = 0;
454         int samplecounter = 0;
455         while (!sawOutputEOS && noOutputCounter < 50) {
456             noOutputCounter++;
457             if (!sawInputEOS) {
458                 int inputBufIndex = codec.dequeueInputBuffer(kTimeOutUs);
459 
460                 if (inputBufIndex >= 0) {
461                     ByteBuffer dstBuf = codecInputBuffers[inputBufIndex];
462 
463                     int sampleSize =
464                         extractor.readSampleData(dstBuf, 0 /* offset */);
465 
466                     long presentationTimeUs = 0;
467 
468                     if (sampleSize < 0 && eossample > 0) {
469                         fail("test is broken: never reached eos sample");
470                     }
471                     if (sampleSize < 0) {
472                         Log.d(TAG, "saw input EOS.");
473                         sawInputEOS = true;
474                         sampleSize = 0;
475                     } else {
476                         if (samplecounter == eossample) {
477                             sawInputEOS = true;
478                         }
479                         samplecounter++;
480                         presentationTimeUs = extractor.getSampleTime();
481                     }
482                     codec.queueInputBuffer(
483                             inputBufIndex,
484                             0 /* offset */,
485                             sampleSize,
486                             presentationTimeUs,
487                             sawInputEOS ? MediaCodec.BUFFER_FLAG_END_OF_STREAM : 0);
488 
489                     if (!sawInputEOS) {
490                         extractor.advance();
491                     }
492                 }
493             }
494 
495             int res = codec.dequeueOutputBuffer(info, kTimeOutUs);
496 
497             if (res >= 0) {
498                 //Log.d(TAG, "got frame, size " + info.size + "/" + info.presentationTimeUs);
499 
500                 if (info.size > 0) {
501                     noOutputCounter = 0;
502                     if (timestamps != null) {
503                         timestamps.add(info.presentationTimeUs);
504                     }
505                 }
506 
507                 int outputBufIndex = res;
508                 ByteBuffer buf = codecOutputBuffers[outputBufIndex];
509 
510                 if (decodedIdx + (info.size / 2) >= decoded.length) {
511                     decoded = Arrays.copyOf(decoded, decodedIdx + (info.size / 2));
512                 }
513 
514                 buf.position(info.offset);
515                 for (int i = 0; i < info.size; i += 2) {
516                     decoded[decodedIdx++] = buf.getShort();
517                 }
518 
519                 codec.releaseOutputBuffer(outputBufIndex, false /* render */);
520 
521                 if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
522                     Log.d(TAG, "saw output EOS.");
523                     sawOutputEOS = true;
524                 }
525             } else if (res == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
526                 codecOutputBuffers = codec.getOutputBuffers();
527 
528                 Log.d(TAG, "output buffers have changed.");
529             } else if (res == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
530                 MediaFormat oformat = codec.getOutputFormat();
531                 audioParams.setNumChannels(oformat.getInteger(MediaFormat.KEY_CHANNEL_COUNT));
532                 audioParams.setSamplingRate(oformat.getInteger(MediaFormat.KEY_SAMPLE_RATE));
533                 Log.d(TAG, "output format has changed to " + oformat);
534             } else {
535                 Log.d(TAG, "dequeueOutputBuffer returned " + res);
536             }
537         }
538         if (noOutputCounter >= 50) {
539             fail("decoder stopped outputing data");
540         }
541 
542         codec.stop();
543         codec.release();
544         return decoded;
545     }
546 
547 }
548 
549