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