| # Copyright 2020 The Chromium Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| import collections |
| import logging |
| import os |
| import time |
| |
| from autotest_lib.client.bin import utils |
| from autotest_lib.client.common_lib import error |
| from autotest_lib.client.common_lib.cros import chrome |
| from autotest_lib.client.cros import constants as cros_constants |
| from autotest_lib.client.cros.audio import audio_helper |
| from autotest_lib.client.cros.audio import audio_test_data |
| from autotest_lib.client.cros.audio import check_quality |
| from autotest_lib.client.cros.audio import cmd_utils |
| from autotest_lib.client.cros.audio import cras_utils |
| from autotest_lib.client.cros.audio import sox_utils |
| from autotest_lib.client.cros.multimedia import audio_facade_native |
| |
| |
| CheckQualityArgsClass = collections.namedtuple( |
| 'args_type', ['filename', 'rate', 'channel', 'bit_width']) |
| |
| |
| class audio_AudioInputGain(audio_helper.cras_rms_test): |
| """Verifies input capture gain of chrome.audio API.""" |
| version = 1 |
| |
| ALOOP_CRAS_NODE_TYPE = 'ALSA_LOOPBACK' |
| ALOOP_MODULE_NAME = 'snd-aloop' |
| CAPTURE_DURATION = 1 |
| # 25: -20 dB |
| # 75: 10 dB |
| # Expected gain: 10 * sqrt(10) |
| LOW_GAIN = 25 |
| HIGH_GAIN = 75 |
| EXPECTED_GAIN = 31.62 |
| FREQ_TOLERANCE = 1 |
| SECOND_PEAK_RATIO_TOLERANCE = 0.05 |
| GAIN_TOLERANCE = 10 |
| |
| def run_once(self): |
| """Entry point of this test.""" |
| def cras_playback_record(gain_level): |
| """Do capture record at CRAS level. |
| |
| @param gain_level: The input gain level. |
| |
| @returns: A string for the recorded file path. |
| |
| """ |
| # Sine raw file lasts 5 seconds |
| raw_path = os.path.join(self.bindir, '5SEC.raw') |
| raw_file = audio_test_data.GenerateAudioTestData( |
| path=raw_path, |
| duration_secs=5, |
| frequencies=[440, 440], |
| volume_scale=0.05) |
| |
| recorded_file = os.path.join(self.resultsdir, |
| 'cras_recorded_%d.raw' % gain_level) |
| |
| # Note: we've found that a couple of seconds after Chrome is up, |
| # there may be a ~30-second-long output stream sourced from |
| # "What's New In Your Chromebook", and it plays no sound. |
| # Just ignore it and continue testing. |
| p = cmd_utils.popen(cras_utils.playback_cmd(raw_file.path)) |
| try: |
| cras_utils.capture(recorded_file, |
| duration=self.CAPTURE_DURATION) |
| # Make sure the audio is still playing. |
| if p.poll() != None: |
| raise error.TestError('playback stopped') |
| finally: |
| cmd_utils.kill_or_log_returncode(p) |
| raw_file.delete() |
| return recorded_file |
| |
| # Check CRAS server is alive. If not, restart it and wait a second to |
| # get server ready. |
| if utils.get_service_pid('cras') == 0: |
| logging.debug('CRAS server is down. Restart it.') |
| utils.start_service('cras', ignore_status=True) |
| time.sleep(1) |
| |
| utils.load_module(self.ALOOP_MODULE_NAME) |
| |
| try: |
| with chrome.Chrome( |
| extension_paths=[cros_constants.AUDIO_TEST_EXTENSION], |
| autotest_ext=True) as cr: |
| audio_facade = audio_facade_native.AudioFacadeNative(cr) |
| audio_facade.set_chrome_active_node_type( |
| self.ALOOP_CRAS_NODE_TYPE, self.ALOOP_CRAS_NODE_TYPE) |
| |
| rms_value = [] |
| for gain in [self.LOW_GAIN, self.HIGH_GAIN]: |
| logging.debug('Start testing loopback with gain %d.', gain) |
| audio_facade.set_chrome_active_input_gain(gain) |
| recorded_file = cras_playback_record(gain) |
| args = CheckQualityArgsClass(filename=recorded_file, |
| rate=48000, |
| channel=1, |
| bit_width=16) |
| raw_data, rate = check_quality.read_audio_file(args) |
| checker = check_quality.QualityChecker(raw_data, rate) |
| # The highest frequency recorded would be near 24 Khz |
| # as the max sample rate is 48000 in our tests. |
| # So let's set ignore_high_freq to be 48000. |
| checker.do_spectral_analysis(ignore_high_freq=48000, |
| check_quality=False, |
| quality_params=None) |
| spectra = checker._spectrals |
| primary_freq = float(spectra[0][0][0]) |
| if abs(primary_freq - 440.0) > self.FREQ_TOLERANCE: |
| raise error.TestFail( |
| 'Primary freq is beyond the expectation: ' |
| 'got %.2f, expected 440.00, tolerance %f' % |
| (primary_freq, self.FREQ_TOLERANCE)) |
| |
| if len(spectra[0]) > 1: |
| peak_ratio = (float(spectra[0][1][1]) / |
| float(spectra[0][0][1])) |
| if peak_ratio > self.SECOND_PEAK_RATIO_TOLERANCE: |
| raise error.TestFail( |
| 'The second peak is not negligible: ' |
| 'f %.2f, peak_ratio %f (tolerance %f)' % |
| (float(spectra[0][1][0]), |
| peak_ratio, |
| self.SECOND_PEAK_RATIO_TOLERANCE)) |
| |
| sox_stat = sox_utils.get_stat(input=recorded_file, |
| channels=1, |
| bits=16, |
| rate=48000) |
| rms_value.append(float(sox_stat.rms)) |
| logging.debug('signal RMS from sox = %f', rms_value[-1]) |
| |
| gain = rms_value[1] / rms_value[0] |
| if abs(gain - self.EXPECTED_GAIN) > self.GAIN_TOLERANCE: |
| raise error.TestFail( |
| 'Gain is beyond the expectation: ' |
| 'got %.2f, expected %.2f, tolerance %f' % |
| (gain, self.EXPECTED_GAIN, self.GAIN_TOLERANCE)) |
| finally: |
| utils.stop_service('cras', ignore_status=True) |
| utils.unload_module(self.ALOOP_MODULE_NAME) |
| utils.start_service('cras', ignore_status=True) |