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