| // Copyright 2018 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "garnet/bin/media/audio_core/mixer/test/audio_result.h" |
| #include "garnet/bin/media/audio_core/mixer/test/mixer_tests_shared.h" |
| #include "lib/fxl/logging.h" |
| |
| namespace media::audio::test { |
| |
| // Convenience abbreviation within this source file to shorten names |
| using Resampler = ::media::audio::Mixer::Resampler; |
| |
| // Ideal dynamic range measurement is exactly equal to the reduction in gain. |
| // Ideal accompanying noise is ideal noise floor, minus the reduction in gain. |
| void MeasureSummaryDynamicRange(float gain_db, double* level_db, |
| double* sinad_db) { |
| MixerPtr mixer = SelectMixer(fuchsia::media::AudioSampleFormat::FLOAT, 1, |
| 48000, 1, 48000, Resampler::SampleAndHold); |
| |
| std::vector<float> source(kFreqTestBufSize); |
| std::vector<float> accum(kFreqTestBufSize); |
| |
| // Populate source buffer; mix it (pass-thru) to accumulation buffer |
| OverwriteCosine(source.data(), kFreqTestBufSize, |
| FrequencySet::kReferenceFreq); |
| |
| uint32_t dest_offset = 0; |
| int32_t frac_src_offset = 0; |
| |
| Bookkeeping info; |
| info.gain.SetSourceGain(gain_db); |
| |
| mixer->Mix(accum.data(), kFreqTestBufSize, &dest_offset, source.data(), |
| kFreqTestBufSize << kPtsFractionalBits, &frac_src_offset, false, |
| &info); |
| EXPECT_EQ(kFreqTestBufSize, dest_offset); |
| EXPECT_EQ(static_cast<int32_t>(kFreqTestBufSize << kPtsFractionalBits), |
| frac_src_offset); |
| |
| // Copy result to double-float buffer, FFT (freq-analyze) it at high-res |
| double magn_signal, magn_other; |
| MeasureAudioFreq(accum.data(), kFreqTestBufSize, FrequencySet::kReferenceFreq, |
| &magn_signal, &magn_other); |
| |
| *level_db = Gain::DoubleToDb(magn_signal); |
| *sinad_db = Gain::DoubleToDb(magn_signal / magn_other); |
| } |
| |
| // Measure dynamic range at two gain settings: less than 1.0 by the smallest |
| // increment possible, as well as the smallest increment detectable (the |
| // closest-to-1.0 gain that actually causes incoming data values to change). |
| TEST(DynamicRange, Epsilon) { |
| double unity_level_db, unity_sinad_db, level_db, sinad_db; |
| |
| MeasureSummaryDynamicRange(0.0f, &unity_level_db, &unity_sinad_db); |
| EXPECT_NEAR(unity_level_db, 0.0, AudioResult::kPrevLevelToleranceSourceFloat); |
| EXPECT_GE(unity_sinad_db, AudioResult::kPrevFloorSourceFloat); |
| AudioResult::LevelToleranceSourceFloat = |
| fmax(AudioResult::LevelToleranceSourceFloat, abs(unity_level_db)); |
| |
| // kMinGainDbUnity is the lowest (furthest-from-Unity) with no observable |
| // attenuation on float32 (i.e. the smallest indistinguishable from Unity). |
| // Just above the 'first detectable reduction' scale; should be same as unity. |
| MeasureSummaryDynamicRange(AudioResult::kMinGainDbUnity, &level_db, |
| &sinad_db); |
| EXPECT_EQ(level_db, unity_level_db); |
| EXPECT_EQ(sinad_db, unity_sinad_db); |
| |
| // kMaxGainDbNonUnity is the highest (closest-to-Unity) with observable effect |
| // on full-scale (i.e. largest sub-Unity AScale distinguishable from Unity). |
| // At this 'detectable reduction' scale, level and noise floor are reduced. |
| MeasureSummaryDynamicRange(AudioResult::kMaxGainDbNonUnity, |
| &AudioResult::LevelEpsilonDown, |
| &AudioResult::SinadEpsilonDown); |
| EXPECT_NEAR(AudioResult::LevelEpsilonDown, AudioResult::kPrevLevelEpsilonDown, |
| AudioResult::kPrevDynRangeTolerance); |
| AudioResult::DynRangeTolerance = fmax( |
| AudioResult::DynRangeTolerance, |
| abs(AudioResult::LevelEpsilonDown - AudioResult::kPrevLevelEpsilonDown)); |
| |
| EXPECT_LT(AudioResult::LevelEpsilonDown, unity_level_db); |
| EXPECT_GE(AudioResult::SinadEpsilonDown, AudioResult::kPrevSinadEpsilonDown); |
| } |
| |
| // Measure dynamic range (signal level, noise floor) when gain is -30dB. |
| TEST(DynamicRange, 30Down) { |
| MeasureSummaryDynamicRange(-30.0f, &AudioResult::Level30Down, |
| &AudioResult::Sinad30Down); |
| AudioResult::DynRangeTolerance = fmax(AudioResult::DynRangeTolerance, |
| abs(AudioResult::Level30Down + 30.0)); |
| |
| EXPECT_NEAR(AudioResult::Level30Down, -30.0, |
| AudioResult::kPrevDynRangeTolerance); |
| EXPECT_GE(AudioResult::Sinad30Down, AudioResult::kPrevSinad30Down); |
| } |
| |
| // Measure dynamic range (signal level, noise floor) when gain is -60dB. |
| TEST(DynamicRange, 60Down) { |
| MeasureSummaryDynamicRange(-60.0f, &AudioResult::Level60Down, |
| &AudioResult::Sinad60Down); |
| AudioResult::DynRangeTolerance = fmax(AudioResult::DynRangeTolerance, |
| abs(AudioResult::Level60Down + 60.0)); |
| |
| EXPECT_NEAR(AudioResult::Level60Down, -60.0, |
| AudioResult::kPrevDynRangeTolerance); |
| EXPECT_GE(AudioResult::Sinad60Down, AudioResult::kPrevSinad60Down); |
| } |
| |
| // Measure dynamic range (signal level, noise floor) when gain is -90dB. |
| TEST(DynamicRange, 90Down) { |
| MeasureSummaryDynamicRange(-90.0f, &AudioResult::Level90Down, |
| &AudioResult::Sinad90Down); |
| AudioResult::DynRangeTolerance = fmax(AudioResult::DynRangeTolerance, |
| abs(AudioResult::Level90Down + 90.0)); |
| |
| EXPECT_NEAR(AudioResult::Level90Down, -90.0, |
| AudioResult::kPrevDynRangeTolerance); |
| EXPECT_GE(AudioResult::Sinad90Down, AudioResult::kPrevSinad90Down); |
| } |
| |
| // Test our mix level and noise floor, when rechannelizing mono into stereo. |
| TEST(DynamicRange, MonoToStereo) { |
| MixerPtr mixer = SelectMixer(fuchsia::media::AudioSampleFormat::FLOAT, 1, |
| 48000, 2, 48000, Resampler::SampleAndHold); |
| |
| std::vector<float> source(kFreqTestBufSize); |
| std::vector<float> accum(kFreqTestBufSize * 2); |
| std::vector<float> left(kFreqTestBufSize); |
| |
| // Populate mono source buffer; mix it (no SRC/gain) to stereo accumulator |
| OverwriteCosine(source.data(), kFreqTestBufSize, |
| FrequencySet::kReferenceFreq); |
| |
| uint32_t dest_offset = 0; |
| int32_t frac_src_offset = 0; |
| |
| Bookkeeping info; |
| mixer->Mix(accum.data(), kFreqTestBufSize, &dest_offset, source.data(), |
| kFreqTestBufSize << kPtsFractionalBits, &frac_src_offset, false, |
| &info); |
| EXPECT_EQ(kFreqTestBufSize, dest_offset); |
| EXPECT_EQ(static_cast<int32_t>(kFreqTestBufSize << kPtsFractionalBits), |
| frac_src_offset); |
| |
| // Copy left result to double-float buffer, FFT (freq-analyze) it at high-res |
| for (uint32_t idx = 0; idx < kFreqTestBufSize; ++idx) { |
| EXPECT_EQ(accum[idx * 2], accum[(idx * 2) + 1]); |
| left[idx] = accum[idx * 2]; |
| } |
| |
| // Only need to analyze left side, since we verified that right is identical. |
| double magn_left_signal, magn_left_other, level_left_db, sinad_left_db; |
| MeasureAudioFreq(left.data(), kFreqTestBufSize, FrequencySet::kReferenceFreq, |
| &magn_left_signal, &magn_left_other); |
| |
| level_left_db = Gain::DoubleToDb(magn_left_signal); |
| sinad_left_db = Gain::DoubleToDb(magn_left_signal / magn_left_other); |
| |
| EXPECT_NEAR(level_left_db, 0.0, AudioResult::kPrevLevelToleranceSourceFloat); |
| AudioResult::LevelToleranceSourceFloat = |
| fmax(AudioResult::LevelToleranceSourceFloat, abs(level_left_db)); |
| |
| EXPECT_GE(sinad_left_db, AudioResult::kPrevFloorSourceFloat); |
| } |
| |
| // Test our mix level and noise floor, when rechannelizing stereo into mono. |
| TEST(DynamicRange, StereoToMono) { |
| MixerPtr mixer = SelectMixer(fuchsia::media::AudioSampleFormat::FLOAT, 2, |
| 48000, 1, 48000, Resampler::SampleAndHold); |
| |
| std::vector<float> mono(kFreqTestBufSize); |
| std::vector<float> source(kFreqTestBufSize * 2); |
| std::vector<float> accum(kFreqTestBufSize); |
| |
| // Populate a mono source buffer; copy it into left side of stereo buffer. |
| OverwriteCosine(mono.data(), kFreqTestBufSize, FrequencySet::kReferenceFreq, |
| kFullScaleFloatInputAmplitude); |
| for (uint32_t idx = 0; idx < kFreqTestBufSize; ++idx) { |
| source[idx * 2] = mono[idx]; |
| } |
| |
| // Populate a mono source buffer with same frequency and amplitude, phase- |
| // shifted by PI/2 (1/4 of a cycle); copy it into right side of stereo buffer. |
| OverwriteCosine(mono.data(), kFreqTestBufSize, FrequencySet::kReferenceFreq, |
| kFullScaleFloatInputAmplitude, M_PI / 2); |
| for (uint32_t idx = 0; idx < kFreqTestBufSize; ++idx) { |
| source[(idx * 2) + 1] = mono[idx]; |
| } |
| |
| uint32_t dest_offset = 0; |
| int32_t frac_src_offset = 0; |
| |
| Bookkeeping info; |
| mixer->Mix(accum.data(), kFreqTestBufSize, &dest_offset, source.data(), |
| kFreqTestBufSize << kPtsFractionalBits, &frac_src_offset, false, |
| &info); |
| EXPECT_EQ(kFreqTestBufSize, dest_offset); |
| EXPECT_EQ(static_cast<int32_t>(kFreqTestBufSize << kPtsFractionalBits), |
| frac_src_offset); |
| |
| // Copy result to double-float buffer, FFT (freq-analyze) it at high-res |
| double magn_signal, magn_other; |
| MeasureAudioFreq(accum.data(), kFreqTestBufSize, FrequencySet::kReferenceFreq, |
| &magn_signal, &magn_other); |
| |
| AudioResult::LevelStereoMono = Gain::DoubleToDb(magn_signal); |
| AudioResult::FloorStereoMono = |
| Gain::DoubleToDb(kFullScaleFloatAccumAmplitude / magn_other); |
| |
| // We added identical signals, so accuracy should be high. However, noise |
| // floor is doubled as well, so we expect 6dB reduction in sinad. |
| EXPECT_NEAR(AudioResult::LevelStereoMono, AudioResult::kPrevLevelStereoMono, |
| AudioResult::kPrevLevelToleranceStereoMono); |
| AudioResult::LevelToleranceStereoMono = fmax( |
| AudioResult::LevelToleranceStereoMono, |
| abs(AudioResult::LevelStereoMono - AudioResult::kPrevLevelStereoMono)); |
| |
| EXPECT_GE(AudioResult::FloorStereoMono, AudioResult::kPrevFloorStereoMono); |
| } |
| |
| // Test mix level and noise floor, when accumulating sources. |
| // |
| // Mix 2 full-scale streams with gain exactly 50% (audio out 100%, master 50%), |
| // then measure level and sinad. On systems with robust gain processing, a |
| // post-SUM master gain stage reduces noise along with level, for the same noise |
| // floor as a single FS signal with 100% gain (98,49 dB for 16,8 respectively). |
| // |
| // When summing two full-scale streams, signal should be approx +6dBFS, and |
| // noise floor should be related to the bitwidth of source and accumulator |
| // (whichever is more narrow). Because our accumulator is still normalized to |
| // 16 bits, we expect the single-stream noise floor to be approx. 98 dB. This |
| // test emulates the mixing of two streams, along with the application of a |
| // master gain which reduces the mixed result to 50%, which should result in a |
| // signal which is exactly full-scale. Summing the two streams will sum the |
| // inherent noise as well, leading to a noise floor of 91-92 dB before taking |
| // gain into account. Once our architecture contains a post-SUM master gain, |
| // after applying a 0.5 master gain scaling we would expect this 91-92 dB |
| // SINAD to be reduced to perhaps 98 dB. Today master gain is combined with |
| // audio out gain, so it is pre-Sum. |
| template <typename T> |
| void MeasureMixFloor(double* level_mix_db, double* sinad_mix_db) { |
| MixerPtr mixer; |
| double amplitude, expected_amplitude; |
| |
| // Why isn't expected_amplitude 1.0? int16 and int8 have more negative values |
| // than positive ones. To be linear without clipping, a full-scale signal |
| // reaches the max (such as 0x7FFF) but not the min (such as -0x8000). Thus, |
| // this magnitude is slightly less than the 1.0 we expect for float signals. |
| if (std::is_same<T, uint8_t>::value) { |
| mixer = SelectMixer(fuchsia::media::AudioSampleFormat::UNSIGNED_8, 1, 48000, |
| 1, 48000, Resampler::SampleAndHold); |
| amplitude = kFullScaleInt8InputAmplitude; |
| expected_amplitude = kFullScaleInt8AccumAmplitude; |
| } else if (std::is_same<T, int16_t>::value) { |
| mixer = SelectMixer(fuchsia::media::AudioSampleFormat::SIGNED_16, 1, 48000, |
| 1, 48000, Resampler::SampleAndHold); |
| amplitude = kFullScaleInt16InputAmplitude; |
| expected_amplitude = kFullScaleInt16AccumAmplitude; |
| } else if (std::is_same<T, int32_t>::value) { |
| mixer = SelectMixer(fuchsia::media::AudioSampleFormat::SIGNED_24_IN_32, 1, |
| 48000, 1, 48000, Resampler::SampleAndHold); |
| amplitude = kFullScaleInt24In32InputAmplitude; |
| expected_amplitude = kFullScaleInt24In32AccumAmplitude; |
| } else { |
| mixer = SelectMixer(fuchsia::media::AudioSampleFormat::FLOAT, 1, 48000, 1, |
| 48000, Resampler::SampleAndHold); |
| amplitude = kFullScaleFloatInputAmplitude; |
| expected_amplitude = kFullScaleFloatAccumAmplitude; |
| } |
| std::vector<T> source(kFreqTestBufSize); |
| std::vector<float> accum(kFreqTestBufSize); |
| |
| OverwriteCosine(source.data(), kFreqTestBufSize, FrequencySet::kReferenceFreq, |
| amplitude); |
| |
| uint32_t dest_offset = 0; |
| int32_t frac_src_offset = 0; |
| |
| // -6.0206 dB leads to 0.500 scale (exactly 50%), to be mixed with itself |
| Bookkeeping info; |
| info.gain.SetSourceGain(-6.0205999f); |
| |
| EXPECT_TRUE(mixer->Mix(accum.data(), kFreqTestBufSize, &dest_offset, |
| source.data(), kFreqTestBufSize << kPtsFractionalBits, |
| &frac_src_offset, false, &info)); |
| |
| // Accumulate the same (reference-frequency) wave. |
| dest_offset = 0; |
| frac_src_offset = 0; |
| |
| EXPECT_TRUE(mixer->Mix(accum.data(), kFreqTestBufSize, &dest_offset, |
| source.data(), kFreqTestBufSize << kPtsFractionalBits, |
| &frac_src_offset, true, &info)); |
| EXPECT_EQ(kFreqTestBufSize, dest_offset); |
| EXPECT_EQ(dest_offset << kPtsFractionalBits, |
| static_cast<uint32_t>(frac_src_offset)); |
| |
| // Copy result to double-float buffer, FFT (freq-analyze) it at high-res |
| double magn_signal, magn_other; |
| MeasureAudioFreq(accum.data(), kFreqTestBufSize, FrequencySet::kReferenceFreq, |
| &magn_signal, &magn_other); |
| |
| *level_mix_db = Gain::DoubleToDb(magn_signal / expected_amplitude); |
| *sinad_mix_db = Gain::DoubleToDb(expected_amplitude / magn_other); |
| } |
| |
| // Test our mix level and noise floor, when accumulating 8-bit sources. |
| TEST(DynamicRange, Mix_8) { |
| MeasureMixFloor<uint8_t>(&AudioResult::LevelMix8, &AudioResult::FloorMix8); |
| |
| EXPECT_NEAR(AudioResult::LevelMix8, 0.0, |
| AudioResult::kPrevLevelToleranceMix8); |
| AudioResult::LevelToleranceMix8 = |
| fmax(AudioResult::LevelToleranceMix8, abs(AudioResult::LevelMix8)); |
| |
| // 8-bit noise floor should be approx -48dBFS. Because 8-bit sources are |
| // normalized up to 16-bit level, they can take advantage of fractional |
| // "footroom"; hence we still expect sinad of ~48dB. |
| EXPECT_GE(AudioResult::FloorMix8, AudioResult::kPrevFloorMix8) |
| << std::setprecision(10) << AudioResult::FloorMix8; |
| } |
| |
| // Test our mix level and noise floor, when accumulating 16-bit sources. |
| TEST(DynamicRange, Mix_16) { |
| MeasureMixFloor<int16_t>(&AudioResult::LevelMix16, &AudioResult::FloorMix16); |
| |
| EXPECT_NEAR(AudioResult::LevelMix16, 0.0, |
| AudioResult::kPrevLevelToleranceMix16); |
| AudioResult::LevelToleranceMix16 = |
| fmax(AudioResult::LevelToleranceMix16, abs(AudioResult::LevelMix16)); |
| |
| // 16-bit noise floor should be approx -96dBFS. Noise is summed along with |
| // signal; therefore we expect sinad of ~90 dB. |
| EXPECT_GE(AudioResult::FloorMix16, AudioResult::kPrevFloorMix16) |
| << std::setprecision(10) << AudioResult::FloorMix16; |
| } |
| |
| // Test our mix level and noise floor, when accumulating 24-bit sources. |
| TEST(DynamicRange, Mix_24) { |
| MeasureMixFloor<int32_t>(&AudioResult::LevelMix24, &AudioResult::FloorMix24); |
| |
| EXPECT_NEAR(AudioResult::LevelMix24, 0.0, |
| AudioResult::kPrevLevelToleranceMix24); |
| AudioResult::LevelToleranceMix24 = |
| fmax(AudioResult::LevelToleranceMix24, abs(AudioResult::LevelMix24)); |
| |
| // 24-bit noise floor should be approx -144dBFS. Noise is summed along with |
| // signal; therefore we expect sinad of ~138 dB. |
| EXPECT_GE(AudioResult::FloorMix24, AudioResult::kPrevFloorMix24) |
| << std::setprecision(10) << AudioResult::FloorMix24; |
| } |
| |
| // Test our mix level and noise floor, when accumulating float sources. |
| TEST(DynamicRange, Mix_Float) { |
| MeasureMixFloor<float>(&AudioResult::LevelMixFloat, |
| &AudioResult::FloorMixFloat); |
| |
| EXPECT_NEAR(AudioResult::LevelMixFloat, 0.0, |
| AudioResult::kPrevLevelToleranceMixFloat); |
| AudioResult::LevelToleranceMixFloat = fmax( |
| AudioResult::LevelToleranceMixFloat, abs(AudioResult::LevelMixFloat)); |
| |
| // This should be same as 16-bit (~91dB), per accumulator precision. Once we |
| // increase accumulator precision, we expect this to improve, while Mix_16 |
| // would not, as precision will still be limited by its 16-bit source. |
| EXPECT_GE(AudioResult::FloorMixFloat, AudioResult::kPrevFloorMixFloat) |
| << std::setprecision(10) << AudioResult::FloorMixFloat; |
| } |
| |
| } // namespace media::audio::test |