1# Lint as: python2, python3 2# Copyright 2020 The Chromium OS Authors. All rights reserved. 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5 6"""bluetooth audio test dat for A2DP, AVRCP, and HFP.""" 7 8import logging 9import os 10import subprocess 11 12import common 13from autotest_lib.client.common_lib import error 14from autotest_lib.client.bin import utils 15 16 17# Chameleon device's data storing path. 18DEVICE_AUDIO_RECORD_DIR = '/tmp/audio' 19# Refer to TEST_DATA_DIR in the chameleon/deploy/deploy file. 20DEVICE_AUDIO_DATA_DIR = '/usr/share/autotest/audio-test-data' 21 22 23DIST_FILES = 'gs://chromeos-localmirror/distfiles' 24DOWNLOAD_TIMEOUT = 90 # timeout for gsutil downloads 25DATA_DIR = '/tmp' 26 27 28VISQOL_TARBALL = os.path.join(DIST_FILES, 'visqol-binary.tar.gz') 29# Path to ViSQOL tarball in autotest server 30VISQOL_TARBALL_LOCAL_PATH = os.path.join(DATA_DIR, 31 os.path.split(VISQOL_TARBALL)[1]) 32VISQOL_FOLDER = os.path.join(DATA_DIR, 'visqol') 33VISQOL_PATH = os.path.join(VISQOL_FOLDER, 'visqol') 34# There are several available models for VISQOL, since these VISQOL based tests 35# are primarily for voice quality, this model is more tuned for voice quality. 36# experimentally, the scores have been fairly similar to the default model 37# 'libsvm_nu_svr_model.txt'. Details: 38# github.com/google/visqol/tree/61cdced26b7a03098f0c78f7ab71c25dc2e461f5/model 39VISQOL_SIMILARITY_MODEL = os.path.join( 40 VISQOL_FOLDER, 'visqol.runfiles', '__main__', 'model', 41 'tcdvoip_nu.568_c5.31474325639_g3.17773760038_model.txt') 42VISQOL_TEST_DIR = os.path.join(VISQOL_FOLDER, 'bt-test-output') 43 44 45AUDIO_TARBALL = os.path.join(DIST_FILES, 'chameleon-bundle', 46 'audio-test-data.tar.gz') 47AUDIO_TEST_DIR = '/usr/local/autotest/cros/audio/test_data' 48AUDIO_RECORD_DIR = os.path.join(DATA_DIR, 'audio') 49 50# AUDIO_TARBALL_NAME is the name of the tarball, i.e. audio-test-data.tar.gz 51AUDIO_TARBALL_NAME = os.path.split(AUDIO_TARBALL)[1] 52# AUDIO_TEST_DATA_DIR is the path of the audio-test-data directory, 53# i.e. /tmp/audio-test-data/ 54AUDIO_TEST_DATA_DIR = os.path.join(DATA_DIR, 55 AUDIO_TARBALL_NAME.split('.', 1)[0]) 56AUDIO_DATA_TARBALL_PATH = os.path.join(DATA_DIR, AUDIO_TARBALL_NAME) 57 58 59A2DP = 'a2dp' 60A2DP_MEDIUM = 'a2dp_medium' 61A2DP_LONG = 'a2dp_long' 62AVRCP = 'avrcp' 63HFP_NBS = 'hfp_nbs' 64HFP_NBS_MEDIUM = 'hfp_nbs_medium' 65HFP_WBS = 'hfp_wbs' 66HFP_WBS_MEDIUM = 'hfp_wbs_medium' 67VISQOL_BUFFER_LENGTH = 10.0 68 69 70def download_file_from_bucket(dir, file_address, verify_download): 71 """Extract tarball specified by tar_path to directory dir. 72 73 @param dir: Path to directory to download file to. 74 @param file_address: URL of the file to download. 75 @param verify_download: A function that accepts stdout, stderr, and the 76 process as args and verifies if the download succeeded. 77 78 @retuns: The result of a call to verify_download. 79 """ 80 download_cmd = 'gsutil cp -r {0} {1}'.format(file_address, dir) 81 download_proc = subprocess.Popen(download_cmd.split(), 82 stdout=subprocess.PIPE, 83 stderr=subprocess.PIPE) 84 85 try: 86 stdout, stderr = utils.poll_for_condition( 87 download_proc.communicate, 88 error.TestError('Failed to download'), timeout=DOWNLOAD_TIMEOUT, 89 desc='Downloading {}'.format(file_address)) 90 except Exception as e: 91 download_proc.terminate() 92 return False 93 else: 94 return verify_download(stdout, stderr, download_proc) 95 96 97def extract_tarball(dir, tar_path, verify_extraction): 98 """Extract tarball specified by tar_path to directory dir. 99 100 @param dir: Path to directory to extract to. 101 @param tar_path: Path to the tarball to extract. 102 @param verify_extraction: A function that accepts stdout, stderr, and the 103 process as args and verifies if the extraction succeeded. 104 105 @retuns: The result of a call to verify_extraction. 106 """ 107 extract_cmd = 'tar -xf {0} -C {1}'.format(tar_path, dir) 108 extract_proc = subprocess.Popen(extract_cmd.split(), stdout=subprocess.PIPE, 109 stderr=subprocess.PIPE) 110 111 try: 112 stdout, stderr = utils.poll_for_condition( 113 extract_proc.communicate, error.TestError('Failed to extract'), 114 timeout=DOWNLOAD_TIMEOUT, desc='Extracting {}'.format(tar_path)) 115 except Exception as e: 116 extract_proc.terminate() 117 return False 118 else: 119 return verify_extraction(stdout, stderr, extract_proc) 120 121 122def verify_visqol_extraction(stdout, stderr, process): 123 """Verify all important components of VISQOL are present in expected 124 locations. 125 126 @param stdout: Output of the extract process. 127 @param stderr: Error output of the extract process. 128 @param process: The Popen object of the extract process. 129 130 @returns: True if all required components are present and extraction process 131 suceeded. 132 """ 133 return (not stderr and 134 os.path.isdir(VISQOL_FOLDER) and 135 os.path.isdir(VISQOL_TEST_DIR) and 136 os.path.exists(VISQOL_PATH) and 137 os.path.exists(VISQOL_SIMILARITY_MODEL)) 138 139 140def get_visqol_binary(): 141 """Download visqol binary. 142 143 If visqol binary not already available, download from DIST_FILES, otherwise 144 skip this step. 145 """ 146 logging.debug('Downloading ViSQOL binary on autotest server') 147 if verify_visqol_extraction(None, None, None): 148 logging.debug('VISQOL binary already exists, skipping') 149 return 150 151 # download from VISQOL_TARBALL 152 if not download_file_from_bucket(DATA_DIR, VISQOL_TARBALL, 153 lambda _, __, p: p.returncode == 0): 154 raise error.TestError('Failed to download ViSQOL binary') 155 # Extract tarball tp DATA_DIR 156 if not extract_tarball(DATA_DIR, VISQOL_TARBALL_LOCAL_PATH, 157 verify_visqol_extraction): 158 raise error.TestError('Failed to extract ViSQOL binary') 159 160 161def get_audio_test_data(): 162 """Download audio test data files 163 164 Download and unzip audio files for audio tests from DIST_FILES. 165 """ 166 logging.debug('Downloading audio test data on autotest server') 167 168 # download from AUDIO_TARBALL 169 if not download_file_from_bucket(DATA_DIR, AUDIO_TARBALL, 170 lambda _, __, p: p.returncode == 0): 171 raise error.TestError('Failed to download audio test data') 172 # Extract tarball to DATA_DIR 173 if not extract_tarball( 174 DATA_DIR, AUDIO_DATA_TARBALL_PATH, 175 lambda _, __, ___: os.path.isdir(AUDIO_TEST_DATA_DIR)): 176 raise error.TestError('Failed to extract audio test data') 177 178 179# Audio test data for hfp narrow band speech 180hfp_nbs_test_data = { 181 'rate': 8000, 182 'channels': 1, 183 'frequencies': (3500,), 184 'file': os.path.join(AUDIO_TEST_DIR, 185 'sine_3500hz_rate8000_ch1_5secs.raw'), 186 'recorded_by_peer': os.path.join(AUDIO_RECORD_DIR, 187 'hfp_nbs_recorded_by_peer.wav'), 188 'recorded_by_dut': os.path.join(AUDIO_RECORD_DIR, 189 'hfp_nbs_recorded_by_dut.raw'), 190 'chunk_in_secs': 1, 191 'bit_width': 16, 192 'format': 'S16_LE', 193 'duration': 5, 194 'chunk_checking_duration': 5, 195 196 # Device side data used by StartPlayingAudioSubprocess function in 197 # bluetooth_audio.py. 198 'device_file': os.path.join(DEVICE_AUDIO_DATA_DIR, 199 'sine_3500hz_rate8000_ch1_5secs.wav'), 200 201 # Device side data used by HandleOneChunk function in bluetooth_audio.py. 202 'chunk_file': os.path.join(DEVICE_AUDIO_RECORD_DIR, 203 'hfp_nbs_recorded_by_peer_%d.raw'), 204 205 'visqol_test_files': [ 206 { 207 'file': os.path.join(AUDIO_TEST_DATA_DIR, 208 'voice_8k.wav'), 209 'recorded_by_peer': os.path.join(AUDIO_RECORD_DIR, 210 'voice_8k_deg_peer.wav'), 211 'recorded_by_dut': os.path.join(AUDIO_RECORD_DIR, 212 'voice_8k_deg_dut.raw'), 213 'channels': 1, 214 'rate': 8000, 215 'duration': 26.112 + VISQOL_BUFFER_LENGTH, 216 'chunk_checking_duration': 26.112 + VISQOL_BUFFER_LENGTH, 217 'bit_width': 16, 218 'format': 'S16_LE', 219 # convenient way to differentiate ViSQOL tests from regular tests 220 'visqol_test': True, 221 'encoding': 'signed-integer', 222 'speech_mode': True, 223 # Passing scored are determined mostly experimentally. 224 # TODO(b/179501232) - NBS is currently not uniformly >= 4.0 on all 225 # devices so reduce the passing score. 226 'sink_passing_score': 3.5, 227 'source_passing_score': 3.5, 228 'reporting_type': 'voice-8k', 229 230 # Device side data used by StartPlayingAudioSubprocess function in 231 # bluetooth_audio.py. 232 'device_file': os.path.join(DEVICE_AUDIO_DATA_DIR, 233 'voice_8k.wav'), 234 }, 235 { 236 'file': os.path.join(AUDIO_TEST_DATA_DIR, 237 'sine_3500hz_rate8000_ch1_5secs.wav'), 238 'recorded_by_peer': os.path.join(AUDIO_RECORD_DIR, 239 'sine_3k_deg_peer.wav'), 240 'recorded_by_dut': os.path.join(AUDIO_RECORD_DIR, 241 'sine_3k_deg_dut.raw'), 242 'channels': 1, 243 'rate': 8000, 244 'duration': 5.0 + VISQOL_BUFFER_LENGTH, 245 'chunk_checking_duration': 5.0 + VISQOL_BUFFER_LENGTH, 246 'bit_width': 16, 247 'format': 'S16_LE', 248 # convenient way to differentiate ViSQOL tests from regular tests 249 'visqol_test': True, 250 'encoding': 'signed-integer', 251 'speech_mode': True, 252 # Sine tones don't work very well with ViSQOL on the NBS tests, both 253 # directions score fairly low, however I've kept it in as a test 254 # file because its a good for reference, makes it easy to see 255 # degradation and verify that this is transmitting the frequency 256 # range we would expect 257 # TODO(b/179501232) - NBS is currently not uniformly >= 2.0 on all 258 # devices so reduce the passing score. 259 'sink_passing_score': 1.0, 260 'source_passing_score': 1.0, 261 'reporting_type': 'sine-3.5k', 262 263 # Device side data used by StartPlayingAudioSubprocess function in 264 # bluetooth_audio.py. 265 'device_file': os.path.join(DEVICE_AUDIO_DATA_DIR, 266 'sine_3500hz_rate8000_ch1_5secs.wav'), 267 } 268 ] 269} 270 271 272# Audio test data for hfp wide band speech 273hfp_wbs_test_data = { 274 'rate': 16000, 275 'channels': 1, 276 277 'frequencies': (7000,), 278 'file': os.path.join(AUDIO_TEST_DIR, 279 'sine_7000hz_rate16000_ch1_5secs.raw'), 280 'recorded_by_peer': os.path.join(AUDIO_RECORD_DIR, 281 'hfp_wbs_recorded_by_peer.wav'), 282 'recorded_by_dut': os.path.join(AUDIO_RECORD_DIR, 283 'hfp_wbs_recorded_by_dut.raw'), 284 'chunk_in_secs': 1, 285 'bit_width': 16, 286 'format': 'S16_LE', 287 'duration': 5, 288 'chunk_checking_duration': 5, 289 290 # Device side data used by StartPlayingAudioSubprocess function in 291 # bluetooth_audio.py. 292 'device_file': os.path.join(DEVICE_AUDIO_DATA_DIR, 293 'sine_7000hz_rate16000_ch1_5secs.wav'), 294 295 # Device side data used by HandleOneChunk function in bluetooth_audio.py. 296 'chunk_file': os.path.join(DEVICE_AUDIO_RECORD_DIR, 297 'hfp_wbs_recorded_by_peer_%d.raw'), 298 299 'visqol_test_files': [ 300 { 301 'file': os.path.join(AUDIO_TEST_DATA_DIR, 302 'voice.wav'), 303 'recorded_by_peer': os.path.join(AUDIO_RECORD_DIR, 304 'voice_deg_peer.wav'), 305 'recorded_by_dut': os.path.join(AUDIO_RECORD_DIR, 306 'voice_deg_dut.raw'), 307 'channels': 1, 308 'rate': 16000, 309 'duration': 26.112 + VISQOL_BUFFER_LENGTH, 310 'chunk_checking_duration': 26.112 + VISQOL_BUFFER_LENGTH, 311 'bit_width': 16, 312 'format': 'S16_LE', 313 # convenient way to differentiate ViSQOL tests from regular tests 314 'visqol_test': True, 315 'encoding': 'signed-integer', 316 'speech_mode': True, 317 # Passing scored are determined mostly experimentally. 318 'sink_passing_score': 4.0, 319 'source_passing_score': 4.0, 320 'reporting_type': 'voice-16k', 321 322 # Device side data used by StartPlayingAudioSubprocess function in 323 # bluetooth_audio.py. 324 'device_file': os.path.join(DEVICE_AUDIO_DATA_DIR, 325 'voice.wav'), 326 }, 327 { 328 'file': os.path.join(AUDIO_TEST_DATA_DIR, 329 'sine_7000hz_rate16000_ch1_5secs.wav'), 330 'recorded_by_peer': os.path.join(AUDIO_RECORD_DIR, 331 'sine_7k_deg_peer.wav'), 332 'recorded_by_dut': os.path.join(AUDIO_RECORD_DIR, 333 'sine_7k_deg_dut.raw'), 334 'channels': 1, 335 'rate': 16000, 336 'duration': 5.0 + VISQOL_BUFFER_LENGTH, 337 'chunk_checking_duration': 5.0 + VISQOL_BUFFER_LENGTH, 338 'bit_width': 16, 339 'format': 'S16_LE', 340 # convenient way to differentiate ViSQOL tests from regular tests 341 'visqol_test': True, 342 'encoding': 'signed-integer', 343 'speech_mode': True, 344 # Passing scored are determined mostly experimentally. 345 'sink_passing_score': 4.0, 346 'source_passing_score': 4.0, 347 'reporting_type': 'sine-7k', 348 349 # Device side data used by StartPlayingAudioSubprocess function in 350 # bluetooth_audio.py. 351 'device_file': os.path.join(DEVICE_AUDIO_DATA_DIR, 352 'sine_7000hz_rate16000_ch1_5secs.wav'), 353 } 354 ] 355} 356 357# Audio test data for hfp nbs medium test. 358hfp_nbs_medium_test_data = { 359 'rate': 8000, 360 'channels': 1, 361 'frequencies': (3500,), 362 'file': os.path.join(AUDIO_TEST_DIR, 363 'sine_3500hz_rate8000_ch1_60secs.raw'), 364 'recorded_by_peer': os.path.join(AUDIO_RECORD_DIR, 365 'hfp_nbs_medium_recorded_by_peer.raw'), 366 'recorded_by_dut': os.path.join(AUDIO_RECORD_DIR, 367 'hfp_nbs_medium_recorded_by_dut.raw'), 368 'chunk_in_secs': 1, 369 'bit_width': 16, 370 'format': 'S16_LE', 371 'duration': 60, 372 'chunk_checking_duration': 5, 373 374 # Device side data used by StartPlayingAudioSubprocess function in 375 # bluetooth_audio.py. 376 'device_file': os.path.join(DEVICE_AUDIO_DATA_DIR, 377 'sine_3500hz_rate8000_ch1_60secs.wav'), 378 # Device side data used by HandleOneChunk function in bluetooth_audio.py. 379 'chunk_file': os.path.join(DEVICE_AUDIO_RECORD_DIR, 380 'hfp_nbs_medium_recorded_by_peer_%d.raw'), 381} 382 383 384# Audio test data for hfp wbs medium test. 385hfp_wbs_medium_test_data = { 386 'rate': 16000, 387 'channels': 1, 388 'frequencies': (7000,), 389 'file': os.path.join(AUDIO_TEST_DIR, 390 'sine_7000hz_rate16000_ch1_60secs.raw'), 391 'recorded_by_peer': os.path.join(AUDIO_RECORD_DIR, 392 'hfp_wbs_medium_recorded_by_peer.raw'), 393 'recorded_by_dut': os.path.join(AUDIO_RECORD_DIR, 394 'hfp_wbs_medium_recorded_by_dut.raw'), 395 'chunk_in_secs': 1, 396 'bit_width': 16, 397 'format': 'S16_LE', 398 'duration': 60, 399 'chunk_checking_duration': 5, 400 401 # Device side data used by StartPlayingAudioSubprocess function in 402 # bluetooth_audio.py. 403 'device_file': os.path.join(DEVICE_AUDIO_DATA_DIR, 404 'sine_7000hz_rate16000_ch1_60secs.wav'), 405 # Device side data used by HandleOneChunk function in bluetooth_audio.py. 406 'chunk_file': os.path.join(DEVICE_AUDIO_RECORD_DIR, 407 'hfp_wbs_medium_recorded_by_peer_%d.raw'), 408} 409 410 411# Audio test data for a2dp 412a2dp_test_data = { 413 'rate': 48000, 414 'channels': 2, 415 'frequencies': (440, 20000), 416 'file': os.path.join(AUDIO_TEST_DIR, 417 'binaural_sine_440hz_20000hz_rate48000_%dsecs.raw'), 418 'recorded_by_peer': os.path.join(AUDIO_RECORD_DIR, 419 'a2dp_recorded_by_peer.raw'), 420 'chunk_in_secs': 5, 421 'bit_width': 16, 422 'format': 'S16_LE', 423 'duration': 5, 424 425 # Device side data used by HandleOneChunk function in bluetooth_audio.py. 426 'chunk_file': os.path.join(DEVICE_AUDIO_RECORD_DIR, 427 'a2dp_recorded_by_peer_%d.raw'), 428} 429 430 431# Audio test data for a2dp long test. The file and duration attributes 432# are dynamic and will be determined during run time. 433a2dp_long_test_data = a2dp_test_data.copy() 434a2dp_long_test_data.update({ 435 'recorded_by_peer': os.path.join(AUDIO_RECORD_DIR, 436 'a2dp_long_recorded_by_peer.raw'), 437 'duration': 0, # determined at run time 438 'chunk_in_secs': 1, 439 # Device side data used by HandleOneChunk function in bluetooth_audio.py. 440 'chunk_file': os.path.join(DEVICE_AUDIO_RECORD_DIR, 441 'a2dp_long_recorded_by_peer_%d.raw'), 442}) 443 444 445# Audio test data for a2dp medium test. 446a2dp_medium_test_data = a2dp_test_data.copy() 447a2dp_medium_test_data.update({ 448 'recorded_by_peer': os.path.join(AUDIO_RECORD_DIR, 449 'a2dp_medium_recorded_by_peer.raw'), 450 'duration': 60, 451 'chunk_in_secs': 1, 452 'chunk_checking_duration': 5, 453 # Device side data used by HandleOneChunk function in bluetooth_audio.py. 454 'chunk_file': os.path.join(DEVICE_AUDIO_RECORD_DIR, 455 'a2dp_medium_recorded_by_peer_%d.raw'), 456}) 457 458 459audio_test_data = { 460 A2DP: a2dp_test_data, 461 A2DP_MEDIUM: a2dp_medium_test_data, 462 A2DP_LONG: a2dp_long_test_data, 463 HFP_WBS: hfp_wbs_test_data, 464 HFP_WBS_MEDIUM: hfp_wbs_medium_test_data, 465 HFP_NBS: hfp_nbs_test_data, 466 HFP_NBS_MEDIUM: hfp_nbs_medium_test_data, 467} 468