| # Copyright 2020 The Chromium OS Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """bluetooth audio test dat for A2DP, AVRCP, and HFP.""" |
| |
| import logging |
| import os |
| import subprocess |
| |
| import common |
| from autotest_lib.client.common_lib import error |
| from autotest_lib.client.bin import utils |
| |
| |
| DIST_FILES = 'gs://chromeos-localmirror/distfiles' |
| DOWNLOAD_TIMEOUT = 90 # timeout for gsutil downloads |
| DATA_DIR = '/tmp' |
| |
| |
| VISQOL_TARBALL = os.path.join(DIST_FILES, 'visqol-binary.tar.gz') |
| # Path to ViSQOL tarball in autotest server |
| VISQOL_TARBALL_LOCAL_PATH = os.path.join(DATA_DIR, |
| os.path.split(VISQOL_TARBALL)[1]) |
| VISQOL_FOLDER = os.path.join(DATA_DIR, 'visqol') |
| VISQOL_PATH = os.path.join(VISQOL_FOLDER, 'visqol') |
| # There are several available models for VISQOL, since these VISQOL based tests |
| # are primarily for voice quality, this model is more tuned for voice quality. |
| # experimentally, the scores have been fairly similar to the default model |
| # TODO b:169251326 terms below are set outside of this codebase |
| # and should be updated when possible. ("master" -> "main") |
| # 'libsvm_nu_svr_model.txt'. Details: github.com/google/visqol/tree/master/model |
| VISQOL_SIMILARITY_MODEL = os.path.join( |
| VISQOL_FOLDER, 'visqol.runfiles', '__main__', 'model', |
| 'tcdvoip_nu.568_c5.31474325639_g3.17773760038_model.txt') |
| VISQOL_TEST_DIR = os.path.join(VISQOL_FOLDER, 'bt-test-output') |
| |
| |
| AUDIO_TARBALL = os.path.join(DIST_FILES, 'chameleon-bundle', |
| 'audio-test-data.tar.gz') |
| AUDIO_TEST_DIR = '/usr/local/autotest/cros/audio/test_data' |
| AUDIO_RECORD_DIR = os.path.join(DATA_DIR, 'audio') |
| |
| # AUDIO_TARBALL_NAME is the name of the tarball, i.e. audio-test-data.tar.gz |
| AUDIO_TARBALL_NAME = os.path.split(AUDIO_TARBALL)[1] |
| # AUDIO_TEST_DATA_DIR is the path of the audio-test-data directory, |
| # i.e. /tmp/audio-test-data/ |
| AUDIO_TEST_DATA_DIR = os.path.join(DATA_DIR, |
| AUDIO_TARBALL_NAME.split('.', 1)[0]) |
| AUDIO_DATA_TARBALL_PATH = os.path.join(DATA_DIR, AUDIO_TARBALL_NAME) |
| |
| |
| A2DP = 'a2dp' |
| A2DP_LONG = 'a2dp_long' |
| AVRCP = 'avrcp' |
| HFP_NBS = 'hfp_nbs' |
| HFP_WBS = 'hfp_wbs' |
| VISQOL_BUFFER_LENGTH = 10.0 |
| |
| |
| common_test_data = { |
| 'bit_width': 16, |
| 'format': 'S16_LE', |
| 'duration': 5, |
| } |
| |
| |
| def download_file_from_bucket(dir, file_address, verify_download): |
| """Extract tarball specified by tar_path to directory dir. |
| |
| @param dir: Path to directory to download file to. |
| @param file_address: URL of the file to download. |
| @param verify_download: A function that accepts stdout, stderr, and the |
| process as args and verifies if the download succeeded. |
| |
| @retuns: The result of a call to verify_download. |
| """ |
| download_cmd = 'gsutil cp -r {0} {1}'.format(file_address, dir) |
| download_proc = subprocess.Popen(download_cmd.split(), |
| stdout=subprocess.PIPE, |
| stderr=subprocess.PIPE) |
| |
| try: |
| stdout, stderr = utils.poll_for_condition( |
| download_proc.communicate, |
| error.TestError('Failed to download'), timeout=DOWNLOAD_TIMEOUT, |
| desc='Downloading {}'.format(file_address)) |
| except Exception as e: |
| download_proc.terminate() |
| return False |
| else: |
| return verify_download(stdout, stderr, download_proc) |
| |
| |
| def extract_tarball(dir, tar_path, verify_extraction): |
| """Extract tarball specified by tar_path to directory dir. |
| |
| @param dir: Path to directory to extract to. |
| @param tar_path: Path to the tarball to extract. |
| @param verify_extraction: A function that accepts stdout, stderr, and the |
| process as args and verifies if the extraction succeeded. |
| |
| @retuns: The result of a call to verify_extraction. |
| """ |
| extract_cmd = 'tar -xf {0} -C {1}'.format(tar_path, dir) |
| extract_proc = subprocess.Popen(extract_cmd.split(), stdout=subprocess.PIPE, |
| stderr=subprocess.PIPE) |
| |
| try: |
| stdout, stderr = utils.poll_for_condition( |
| extract_proc.communicate, error.TestError('Failed to extract'), |
| timeout=DOWNLOAD_TIMEOUT, desc='Extracting {}'.format(tar_path)) |
| except Exception as e: |
| extract_proc.terminate() |
| return False |
| else: |
| return verify_extraction(stdout, stderr, extract_proc) |
| |
| |
| def verify_visqol_extraction(stdout, stderr, process): |
| """Verify all important components of VISQOL are present in expected |
| locations. |
| |
| @param stdout: Output of the extract process. |
| @param stderr: Error output of the extract process. |
| @param process: The Popen object of the extract process. |
| |
| @returns: True if all required components are present and extraction process |
| suceeded. |
| """ |
| return (not stderr and |
| os.path.isdir(VISQOL_FOLDER) and |
| os.path.isdir(VISQOL_TEST_DIR) and |
| os.path.exists(VISQOL_PATH) and |
| os.path.exists(VISQOL_SIMILARITY_MODEL)) |
| |
| |
| def get_visqol_binary(): |
| """Download visqol binary. |
| |
| If visqol binary not already available, download from DIST_FILES, otherwise |
| skip this step. |
| """ |
| logging.debug('Downloading ViSQOL binary on autotest server') |
| if verify_visqol_extraction(None, None, None): |
| logging.debug('VISQOL binary already exists, skipping') |
| return |
| |
| # download from VISQOL_TARBALL |
| if not download_file_from_bucket(DATA_DIR, VISQOL_TARBALL, |
| lambda _, __, p: p.returncode == 0): |
| raise error.TestError('Failed to download ViSQOL binary') |
| # Extract tarball tp DATA_DIR |
| if not extract_tarball(DATA_DIR, VISQOL_TARBALL_LOCAL_PATH, |
| verify_visqol_extraction): |
| raise error.TestError('Failed to extract ViSQOL binary') |
| |
| |
| def get_audio_test_data(): |
| """Download audio test data files |
| |
| Download and unzip audio files for audio tests from DIST_FILES. |
| """ |
| logging.debug('Downloading audio test data on autotest server') |
| |
| # download from AUDIO_TARBALL |
| if not download_file_from_bucket(DATA_DIR, AUDIO_TARBALL, |
| lambda _, __, p: p.returncode == 0): |
| raise error.TestError('Failed to download audio test data') |
| # Extract tarball to DATA_DIR |
| if not extract_tarball( |
| DATA_DIR, AUDIO_DATA_TARBALL_PATH, |
| lambda _, __, ___: os.path.isdir(AUDIO_TEST_DATA_DIR)): |
| raise error.TestError('Failed to extract audio test data') |
| |
| |
| # Audio test data for hfp narrow band speech |
| hfp_nbs_test_data = { |
| 'rate': 8000, |
| 'channels': 1, |
| 'frequencies': (3500,), |
| 'file': os.path.join(AUDIO_TEST_DIR, |
| 'sine_3500hz_rate8000_ch1_5secs.raw'), |
| 'recorded_by_peer': os.path.join(AUDIO_RECORD_DIR, |
| 'hfp_nbs_recorded_by_peer.wav'), |
| 'recorded_by_dut': os.path.join(AUDIO_RECORD_DIR, |
| 'hfp_nbs_recorded_by_dut.raw'), |
| 'visqol_test_files': [ |
| { |
| 'file': os.path.join(AUDIO_TEST_DATA_DIR, |
| 'voice_8k.wav'), |
| 'recorded_by_peer': os.path.join(AUDIO_RECORD_DIR, |
| 'voice_8k_deg_peer.wav'), |
| 'recorded_by_dut': os.path.join(AUDIO_RECORD_DIR, |
| 'voice_8k_deg_dut.raw'), |
| 'channels': 1, |
| 'rate': 8000, |
| 'duration': 26.112 + VISQOL_BUFFER_LENGTH, |
| 'bit_width': 16, |
| 'format': 'S16_LE', |
| # convenient way to differentiate ViSQOL tests from regular tests |
| 'visqol_test': True, |
| 'encoding': 'signed-integer', |
| 'speech_mode': True, |
| # Passing scored are determined mostly experimentally, the DUT as |
| # sink direction has issues and so for now the score set low. |
| # Ideally both scores should be set to >= 4.0 in fully functioning |
| # scenario. |
| 'sink_passing_score': 0.0, |
| 'source_passing_score': 4.0, |
| }, |
| { |
| 'file': os.path.join(AUDIO_TEST_DATA_DIR, |
| 'sine_3500hz_rate8000_ch1_5secs.wav'), |
| 'recorded_by_peer': os.path.join(AUDIO_RECORD_DIR, |
| 'sine_3k_deg_peer.wav'), |
| 'recorded_by_dut': os.path.join(AUDIO_RECORD_DIR, |
| 'sine_3k_deg_dut.raw'), |
| 'channels': 1, |
| 'rate': 8000, |
| 'duration': 5.0 + VISQOL_BUFFER_LENGTH, |
| 'bit_width': 16, |
| 'format': 'S16_LE', |
| # convenient way to differentiate ViSQOL tests from regular tests |
| 'visqol_test': True, |
| 'encoding': 'signed-integer', |
| 'speech_mode': True, |
| # Sine tones don't work very well with ViSQOL on the NBS tests, both |
| # directions score fairly low, however I've kept it in as a test |
| # file because its a good for reference, makes it easy to see |
| # degradation and verify that this is transmitting the frequency |
| # range we would expect |
| 'sink_passing_score': 1.0, |
| 'source_passing_score': 2.0, |
| } |
| ] |
| } |
| hfp_nbs_test_data.update(common_test_data) |
| |
| |
| # Audio test data for hfp wide band speech |
| hfp_wbs_test_data = { |
| 'rate': 16000, |
| 'channels': 1, |
| |
| 'frequencies': (7000,), |
| 'file': os.path.join(AUDIO_TEST_DIR, |
| 'sine_7000hz_rate16000_ch1_5secs.raw'), |
| 'recorded_by_peer': os.path.join(AUDIO_RECORD_DIR, |
| 'hfp_wbs_recorded_by_peer.wav'), |
| 'recorded_by_dut': os.path.join(AUDIO_RECORD_DIR, |
| 'hfp_wbs_recorded_by_dut.raw'), |
| 'visqol_test_files': [ |
| { |
| 'file': os.path.join(AUDIO_TEST_DATA_DIR, |
| 'voice.wav'), |
| 'recorded_by_peer': os.path.join(AUDIO_RECORD_DIR, |
| 'voice_deg_peer.wav'), |
| 'recorded_by_dut': os.path.join(AUDIO_RECORD_DIR, |
| 'voice_deg_dut.raw'), |
| 'channels': 1, |
| 'rate': 16000, |
| 'duration': 26.112 + VISQOL_BUFFER_LENGTH, |
| 'bit_width': 16, |
| 'format': 'S16_LE', |
| # convenient way to differentiate ViSQOL tests from regular tests |
| 'visqol_test': True, |
| 'encoding': 'signed-integer', |
| 'speech_mode': True, |
| # Passing scored are determined mostly experimentally, the DUT as |
| # sink direction has issues and so for now the score set low. |
| # Ideally both scores should be set to >= 4.0 in fully functioning |
| # scenario. |
| 'sink_passing_score': 0.0, |
| 'source_passing_score': 4.0, |
| }, |
| { |
| 'file': os.path.join(AUDIO_TEST_DATA_DIR, |
| 'sine_7000hz_rate16000_ch1_5secs.wav'), |
| 'recorded_by_peer': os.path.join(AUDIO_RECORD_DIR, |
| 'sine_7k_deg_peer.wav'), |
| 'recorded_by_dut': os.path.join(AUDIO_RECORD_DIR, |
| 'sine_7k_deg_dut.raw'), |
| 'channels': 1, |
| 'rate': 16000, |
| 'duration': 5.0 + VISQOL_BUFFER_LENGTH, |
| 'bit_width': 16, |
| 'format': 'S16_LE', |
| # convenient way to differentiate ViSQOL tests from regular tests |
| 'visqol_test': True, |
| 'encoding': 'signed-integer', |
| 'speech_mode': True, |
| # Passing scored are determined mostly experimentally, the DUT as |
| # sink direction has issues and so for now the score set low. |
| # Ideally both scores should be set to >= 4.0 in fully functioning |
| # scenario. |
| 'sink_passing_score': 0.0, |
| 'source_passing_score': 4.0, |
| } |
| ] |
| } |
| hfp_wbs_test_data.update(common_test_data) |
| |
| |
| # Audio test data for a2dp |
| a2dp_test_data = { |
| 'rate': 48000, |
| 'channels': 2, |
| 'frequencies': (440, 20000), |
| 'file': os.path.join(AUDIO_TEST_DIR, |
| 'binaural_sine_440hz_20000hz_rate48000_%dsecs.raw'), |
| 'recorded_by_peer': os.path.join(AUDIO_RECORD_DIR, |
| 'a2dp_recorded_by_peer.raw'), |
| 'chunk_in_secs': 5, |
| } |
| a2dp_test_data.update(common_test_data) |
| |
| |
| # Audio test data for a2dp long test. The file and duration attributes |
| # are dynamic and will be determined during run time. |
| a2dp_long_test_data = a2dp_test_data.copy() |
| a2dp_long_test_data.update({ |
| 'recorded_by_peer': os.path.join(AUDIO_RECORD_DIR, |
| 'a2dp_long_recorded_by_peer.raw'), |
| 'duration': 0, # determined at run time |
| 'chunk_in_secs': 1, |
| }) |
| |
| |
| audio_test_data = { |
| A2DP: a2dp_test_data, |
| A2DP_LONG: a2dp_long_test_data, |
| HFP_WBS: hfp_wbs_test_data, |
| HFP_NBS: hfp_nbs_test_data, |
| } |