1# Copyright 2015 The Chromium OS Authors. All rights reserved. 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5"""This module provides the test utilities for audio tests using chameleon.""" 6 7# TODO (cychiang) Move test utilities from chameleon_audio_helpers 8# to this module. 9 10import logging 11import multiprocessing 12import os 13import pprint 14import re 15from contextlib import contextmanager 16 17from autotest_lib.client.common_lib import error 18from autotest_lib.client.cros import constants 19from autotest_lib.client.cros.audio import audio_analysis 20from autotest_lib.client.cros.audio import audio_spec 21from autotest_lib.client.cros.audio import audio_data 22from autotest_lib.client.cros.audio import audio_helper 23from autotest_lib.client.cros.audio import audio_quality_measurement 24from autotest_lib.client.cros.chameleon import chameleon_audio_ids 25 26CHAMELEON_AUDIO_IDS_TO_CRAS_NODE_TYPES = { 27 chameleon_audio_ids.CrosIds.HDMI: 'HDMI', 28 chameleon_audio_ids.CrosIds.HEADPHONE: 'HEADPHONE', 29 chameleon_audio_ids.CrosIds.EXTERNAL_MIC: 'MIC', 30 chameleon_audio_ids.CrosIds.SPEAKER: 'INTERNAL_SPEAKER', 31 chameleon_audio_ids.CrosIds.INTERNAL_MIC: 'INTERNAL_MIC', 32 chameleon_audio_ids.CrosIds.BLUETOOTH_HEADPHONE: 'BLUETOOTH', 33 chameleon_audio_ids.CrosIds.BLUETOOTH_MIC: 'BLUETOOTH', 34 chameleon_audio_ids.CrosIds.USBIN: 'USB', 35 chameleon_audio_ids.CrosIds.USBOUT: 'USB', 36} 37 38 39def cros_port_id_to_cras_node_type(port_id): 40 """Gets Cras node type from Cros port id. 41 42 @param port_id: A port id defined in chameleon_audio_ids.CrosIds. 43 44 @returns: A Cras node type defined in cras_utils.CRAS_NODE_TYPES. 45 46 """ 47 return CHAMELEON_AUDIO_IDS_TO_CRAS_NODE_TYPES[port_id] 48 49 50def check_output_port(audio_facade, port_id): 51 """Checks selected output node on Cros device is correct for a port. 52 53 @param port_id: A port id defined in chameleon_audio_ids.CrosIds. 54 55 """ 56 output_node_type = cros_port_id_to_cras_node_type(port_id) 57 check_audio_nodes(audio_facade, ([output_node_type], None)) 58 59 60def check_input_port(audio_facade, port_id): 61 """Checks selected input node on Cros device is correct for a port. 62 63 @param port_id: A port id defined in chameleon_audio_ids.CrosIds. 64 65 """ 66 input_node_type = cros_port_id_to_cras_node_type(port_id) 67 check_audio_nodes(audio_facade, (None, [input_node_type])) 68 69 70def check_audio_nodes(audio_facade, audio_nodes): 71 """Checks the node selected by Cros device is correct. 72 73 @param audio_facade: A RemoteAudioFacade to access audio functions on 74 Cros device. 75 76 @param audio_nodes: A tuple (out_audio_nodes, in_audio_nodes) containing 77 expected selected output and input nodes. 78 79 @raises: error.TestFail if the nodes selected by Cros device are not expected. 80 81 """ 82 curr_out_nodes, curr_in_nodes = audio_facade.get_selected_node_types() 83 out_audio_nodes, in_audio_nodes = audio_nodes 84 if (in_audio_nodes != None and 85 sorted(curr_in_nodes) != sorted(in_audio_nodes)): 86 raise error.TestFail('Wrong input node(s) selected: %s ' 87 'expected: %s' % (str(curr_in_nodes), str(in_audio_nodes))) 88 89 # Treat line-out node as headphone node in Chameleon test since some 90 # Cros devices detect audio board as lineout. This actually makes sense 91 # because 3.5mm audio jack is connected to LineIn port on Chameleon. 92 if (out_audio_nodes == ['HEADPHONE'] and curr_out_nodes == ['LINEOUT']): 93 return 94 95 if (out_audio_nodes != None and 96 sorted(curr_out_nodes) != sorted(out_audio_nodes)): 97 raise error.TestFail('Wrong output node(s) selected %s ' 98 'expected: %s' % (str(curr_out_nodes), str(out_audio_nodes))) 99 100 101def check_plugged_nodes(audio_facade, audio_nodes): 102 """Checks the nodes that are currently plugged on Cros device are correct. 103 104 @param audio_facade: A RemoteAudioFacade to access audio functions on 105 Cros device. 106 107 @param audio_nodes: A tuple (out_audio_nodes, in_audio_nodes) containing 108 expected plugged output and input nodes. 109 110 @raises: error.TestFail if the plugged nodes on Cros device are not expected. 111 112 """ 113 curr_out_nodes, curr_in_nodes = audio_facade.get_plugged_node_types() 114 out_audio_nodes, in_audio_nodes = audio_nodes 115 if (in_audio_nodes != None and 116 sorted(curr_in_nodes) != sorted(in_audio_nodes)): 117 raise error.TestFail('Wrong input node(s) plugged: %s ' 118 'expected: %s!' % (str(curr_in_nodes), str(in_audio_nodes))) 119 if (out_audio_nodes != None and 120 sorted(curr_out_nodes) != sorted(out_audio_nodes)): 121 raise error.TestFail('Wrong output node(s) plugged: %s ' 122 'expected: %s!' % (str(curr_out_nodes), str(out_audio_nodes))) 123 124 125def bluetooth_nodes_plugged(audio_facade): 126 """Checks bluetooth nodes are plugged. 127 128 @param audio_facade: A RemoteAudioFacade to access audio functions on 129 Cros device. 130 131 @raises: error.TestFail if either input or output bluetooth node is 132 not plugged. 133 134 """ 135 curr_out_nodes, curr_in_nodes = audio_facade.get_plugged_node_types() 136 return 'BLUETOOTH' in curr_out_nodes and 'BLUETOOTH' in curr_in_nodes 137 138 139def _get_board_name(host): 140 """Gets the board name. 141 142 @param host: The CrosHost object. 143 144 @returns: The board name. 145 146 """ 147 return host.get_board().split(':')[1] 148 149 150def has_internal_speaker(host): 151 """Checks if the Cros device has speaker. 152 153 @param host: The CrosHost object. 154 155 @returns: True if Cros device has internal speaker. False otherwise. 156 157 """ 158 board_name = _get_board_name(host) 159 if not audio_spec.has_internal_speaker(host.get_board_type(), board_name): 160 logging.info('Board %s does not have speaker.', board_name) 161 return False 162 return True 163 164 165def has_internal_microphone(host): 166 """Checks if the Cros device has internal microphone. 167 168 @param host: The CrosHost object. 169 170 @returns: True if Cros device has internal microphone. False otherwise. 171 172 """ 173 board_name = _get_board_name(host) 174 if not audio_spec.has_internal_microphone(host.get_board_type()): 175 logging.info('Board %s does not have internal microphone.', board_name) 176 return False 177 return True 178 179 180def has_headphone(host): 181 """Checks if the Cros device has headphone. 182 183 @param host: The CrosHost object. 184 185 @returns: True if Cros device has headphone. False otherwise. 186 187 """ 188 board_name = _get_board_name(host) 189 if not audio_spec.has_headphone(host.get_board_type()): 190 logging.info('Board %s does not have headphone.', board_name) 191 return False 192 return True 193 194 195def suspend_resume(host, suspend_time_secs, resume_network_timeout_secs=50): 196 """Performs the suspend/resume on Cros device. 197 198 @param suspend_time_secs: Time in seconds to let Cros device suspend. 199 @resume_network_timeout_secs: Time in seconds to let Cros device resume and 200 obtain network. 201 """ 202 def action_suspend(): 203 """Calls the host method suspend.""" 204 host.suspend(suspend_time=suspend_time_secs) 205 206 boot_id = host.get_boot_id() 207 proc = multiprocessing.Process(target=action_suspend) 208 logging.info("Suspending...") 209 proc.daemon = True 210 proc.start() 211 host.test_wait_for_sleep(suspend_time_secs / 3) 212 logging.info("DUT suspended! Waiting to resume...") 213 host.test_wait_for_resume( 214 boot_id, suspend_time_secs + resume_network_timeout_secs) 215 logging.info("DUT resumed!") 216 217 218def dump_cros_audio_logs(host, audio_facade, directory, suffix=''): 219 """Dumps logs for audio debugging from Cros device. 220 221 @param host: The CrosHost object. 222 @param audio_facade: A RemoteAudioFacade to access audio functions on 223 Cros device. 224 @directory: The directory to dump logs. 225 226 """ 227 def get_file_path(name): 228 """Gets file path to dump logs. 229 230 @param name: The file name. 231 232 @returns: The file path with an optional suffix. 233 234 """ 235 file_name = '%s.%s' % (name, suffix) if suffix else name 236 file_path = os.path.join(directory, file_name) 237 return file_path 238 239 audio_facade.dump_diagnostics(get_file_path('audio_diagnostics.txt')) 240 241 host.get_file('/var/log/messages', get_file_path('messages')) 242 243 host.get_file(constants.MULTIMEDIA_XMLRPC_SERVER_LOG_FILE, 244 get_file_path('multimedia_xmlrpc_server.log')) 245 246 247def examine_audio_diagnostics(path): 248 """Examines audio diagnostic content. 249 250 @param path: Path to audio diagnostic file. 251 252 @returns: Warning messages or ''. 253 254 """ 255 warning_msgs = [] 256 line_number = 1 257 258 underrun_pattern = re.compile('num_underruns: (\d*)') 259 260 with open(path) as f: 261 for line in f.readlines(): 262 263 # Check for number of underruns. 264 search_result = underrun_pattern.search(line) 265 if search_result: 266 num_underruns = int(search_result.group(1)) 267 if num_underruns != 0: 268 warning_msgs.append( 269 'Found %d underrun at line %d: %s' % ( 270 num_underruns, line_number, line)) 271 272 # TODO(cychiang) add other check like maximum client reply delay. 273 line_number = line_number + 1 274 275 if warning_msgs: 276 return ('Found issue in audio diganostics result : %s' % 277 '\n'.join(warning_msgs)) 278 279 logging.info('audio_diagnostic result looks fine') 280 return '' 281 282 283@contextmanager 284def monitor_no_nodes_changed(audio_facade, callback=None): 285 """Context manager to monitor nodes changed signal on Cros device. 286 287 Starts the counter in the beginning. Stops the counter in the end to make 288 sure there is no NodesChanged signal during the try block. 289 290 E.g. with monitor_no_nodes_changed(audio_facade): 291 do something on playback/recording 292 293 @param audio_facade: A RemoteAudioFacade to access audio functions on 294 Cros device. 295 @param fail_callback: The callback to call before raising TestFail 296 when there is unexpected NodesChanged signals. 297 298 @raises: error.TestFail if there is NodesChanged signal on 299 Cros device during the context. 300 301 """ 302 try: 303 audio_facade.start_counting_signal('NodesChanged') 304 yield 305 finally: 306 count = audio_facade.stop_counting_signal() 307 if count: 308 message = 'Got %d unexpected NodesChanged signal' % count 309 logging.error(message) 310 if callback: 311 callback() 312 raise error.TestFail(message) 313 314 315# The second dominant frequency should have energy less than -26dB of the 316# first dominant frequency in the spectrum. 317_DEFAULT_SECOND_PEAK_RATIO = 0.05 318 319# Tolerate more noise for bluetooth audio using HSP. 320_HSP_SECOND_PEAK_RATIO = 0.2 321 322# Tolerate more noise for speaker. 323_SPEAKER_SECOND_PEAK_RATIO = 0.1 324 325# Tolerate more noise for internal microphone. 326_INTERNAL_MIC_SECOND_PEAK_RATIO = 0.2 327 328# maximum tolerant noise level 329DEFAULT_TOLERANT_NOISE_LEVEL = 0.01 330 331# If relative error of two durations is less than 0.2, 332# they will be considered equivalent. 333DEFAULT_EQUIVALENT_THRESHOLD = 0.2 334 335# The frequency at lower than _DC_FREQ_THRESHOLD should have coefficient 336# smaller than _DC_COEFF_THRESHOLD. 337_DC_FREQ_THRESHOLD = 0.001 338_DC_COEFF_THRESHOLD = 0.01 339 340def get_second_peak_ratio(source_id, recorder_id, is_hsp=False): 341 """Gets the second peak ratio suitable for use case. 342 343 @param source_id: ID defined in chameleon_audio_ids for source widget. 344 @param recorder_id: ID defined in chameleon_audio_ids for recorder widget. 345 @param is_hsp: For bluetooth HSP use case. 346 347 @returns: A float for proper second peak ratio to be used in 348 check_recorded_frequency. 349 """ 350 if is_hsp: 351 return _HSP_SECOND_PEAK_RATIO 352 elif source_id == chameleon_audio_ids.CrosIds.SPEAKER: 353 return _SPEAKER_SECOND_PEAK_RATIO 354 elif recorder_id == chameleon_audio_ids.CrosIds.INTERNAL_MIC: 355 return _INTERNAL_MIC_SECOND_PEAK_RATIO 356 else: 357 return _DEFAULT_SECOND_PEAK_RATIO 358 359 360# The deviation of estimated dominant frequency from golden frequency. 361DEFAULT_FREQUENCY_DIFF_THRESHOLD = 5 362 363def check_recorded_frequency( 364 golden_file, recorder, 365 second_peak_ratio=_DEFAULT_SECOND_PEAK_RATIO, 366 frequency_diff_threshold=DEFAULT_FREQUENCY_DIFF_THRESHOLD, 367 ignore_frequencies=None, check_anomaly=False, check_artifacts=False, 368 mute_durations=None, volume_changes=None, 369 tolerant_noise_level=DEFAULT_TOLERANT_NOISE_LEVEL): 370 """Checks if the recorded data contains sine tone of golden frequency. 371 372 @param golden_file: An AudioTestData object that serves as golden data. 373 @param recorder: An AudioWidget used in the test to record data. 374 @param second_peak_ratio: The test fails when the second dominant 375 frequency has coefficient larger than this 376 ratio of the coefficient of first dominant 377 frequency. 378 @param frequency_diff_threshold: The maximum difference between estimated 379 frequency of test signal and golden 380 frequency. This value should be small for 381 signal passed through line. 382 @param ignore_frequencies: A list of frequencies to be ignored. The 383 component in the spectral with frequency too 384 close to the frequency in the list will be 385 ignored. The comparison of frequencies uses 386 frequency_diff_threshold as well. 387 @param check_anomaly: True to check anomaly in the signal. 388 @param check_artifacts: True to check artifacts in the signal. 389 @param mute_durations: Each duration of mute in seconds in the signal. 390 @param volume_changes: A list containing alternative -1 for decreasing 391 volume and +1 for increasing volume. 392 @param tolerant_noise_level: The maximum noise level can be tolerated 393 394 @returns: A list containing tuples of (dominant_frequency, coefficient) for 395 valid channels. Coefficient can be a measure of signal magnitude 396 on that dominant frequency. Invalid channels where golden_channel 397 is None are ignored. 398 399 @raises error.TestFail if the recorded data does not contain sine tone of 400 golden frequency. 401 402 """ 403 if not ignore_frequencies: 404 ignore_frequencies = [] 405 406 # Also ignore harmonics of ignore frequencies. 407 ignore_frequencies_harmonics = [] 408 for ignore_freq in ignore_frequencies: 409 ignore_frequencies_harmonics += [ignore_freq * n for n in xrange(1, 4)] 410 411 data_format = recorder.data_format 412 recorded_data = audio_data.AudioRawData( 413 binary=recorder.get_binary(), 414 channel=data_format['channel'], 415 sample_format=data_format['sample_format']) 416 417 errors = [] 418 dominant_spectrals = [] 419 420 for test_channel, golden_channel in enumerate(recorder.channel_map): 421 if golden_channel is None: 422 logging.info('Skipped channel %d', test_channel) 423 continue 424 425 signal = recorded_data.channel_data[test_channel] 426 saturate_value = audio_data.get_maximum_value_from_sample_format( 427 data_format['sample_format']) 428 logging.debug('Channel %d max signal: %f', test_channel, max(signal)) 429 normalized_signal = audio_analysis.normalize_signal( 430 signal, saturate_value) 431 logging.debug('saturate_value: %f', saturate_value) 432 logging.debug('max signal after normalized: %f', max(normalized_signal)) 433 spectral = audio_analysis.spectral_analysis( 434 normalized_signal, data_format['rate']) 435 logging.debug('spectral: %s', spectral) 436 437 if not spectral: 438 errors.append( 439 'Channel %d: Can not find dominant frequency.' % 440 test_channel) 441 442 golden_frequency = golden_file.frequencies[golden_channel] 443 logging.debug('Checking channel %s spectral %s against frequency %s', 444 test_channel, spectral, golden_frequency) 445 446 dominant_frequency = spectral[0][0] 447 448 if (abs(dominant_frequency - golden_frequency) > 449 frequency_diff_threshold): 450 errors.append( 451 'Channel %d: Dominant frequency %s is away from golden %s' % 452 (test_channel, dominant_frequency, golden_frequency)) 453 454 if check_anomaly: 455 detected_anomaly = audio_analysis.anomaly_detection( 456 signal=normalized_signal, 457 rate=data_format['rate'], 458 freq=golden_frequency) 459 if detected_anomaly: 460 errors.append( 461 'Channel %d: Detect anomaly near these time: %s' % 462 (test_channel, detected_anomaly)) 463 else: 464 logging.info( 465 'Channel %d: Quality is good as there is no anomaly', 466 test_channel) 467 468 if check_artifacts or mute_durations or volume_changes: 469 result = audio_quality_measurement.quality_measurement( 470 normalized_signal, 471 data_format['rate'], 472 dominant_frequency=dominant_frequency) 473 logging.debug('Quality measurement result:\n%s', pprint.pformat(result)) 474 if check_artifacts: 475 if len(result['artifacts']['noise_before_playback']) > 0: 476 errors.append( 477 'Channel %d: Detects artifacts before playing near' 478 ' these time and duration: %s' % 479 (test_channel, 480 str(result['artifacts']['noise_before_playback']))) 481 482 if len(result['artifacts']['noise_after_playback']) > 0: 483 errors.append( 484 'Channel %d: Detects artifacts after playing near' 485 ' these time and duration: %s' % 486 (test_channel, 487 str(result['artifacts']['noise_after_playback']))) 488 489 if mute_durations: 490 delays = result['artifacts']['delay_during_playback'] 491 delay_durations = [] 492 for x in delays: 493 delay_durations.append(x[1]) 494 mute_matched, delay_matched = longest_common_subsequence( 495 mute_durations, 496 delay_durations, 497 DEFAULT_EQUIVALENT_THRESHOLD) 498 499 # updated delay list 500 new_delays = [delays[i] 501 for i in delay_matched if not delay_matched[i]] 502 503 result['artifacts']['delay_during_playback'] = new_delays 504 505 unmatched_mutes = [mute_durations[i] 506 for i in mute_matched if not mute_matched[i]] 507 508 if len(unmatched_mutes) > 0: 509 errors.append( 510 'Channel %d: Unmatched mute duration: %s' % 511 (test_channel, unmatched_mutes)) 512 513 if check_artifacts: 514 if len(result['artifacts']['delay_during_playback']) > 0: 515 errors.append( 516 'Channel %d: Detects delay during playing near' 517 ' these time and duration: %s' % 518 (test_channel, 519 result['artifacts']['delay_during_playback'])) 520 521 if len(result['artifacts']['burst_during_playback']) > 0: 522 errors.append( 523 'Channel %d: Detects burst/pop near these time: %s' % 524 (test_channel, 525 result['artifacts']['burst_during_playback'])) 526 527 if result['equivalent_noise_level'] > tolerant_noise_level: 528 errors.append( 529 'Channel %d: noise level is higher than tolerant' 530 ' noise level: %f > %f' % 531 (test_channel, 532 result['equivalent_noise_level'], 533 tolerant_noise_level)) 534 535 if volume_changes: 536 matched = True 537 volume_changing = result['volume_changes'] 538 if len(volume_changing) != len(volume_changes): 539 matched = False 540 else: 541 for i in xrange(len(volume_changing)): 542 if volume_changing[i][1] != volume_changes[i]: 543 matched = False 544 break 545 if not matched: 546 errors.append( 547 'Channel %d: volume changing is not as expected, ' 548 'found changing time and events are: %s while ' 549 'expected changing events are %s'% 550 (test_channel, 551 volume_changing, 552 volume_changes)) 553 554 # Filter out the harmonics resulted from imperfect sin wave. 555 # This list is different for different channels. 556 harmonics = [dominant_frequency * n for n in xrange(2, 10)] 557 558 def should_be_ignored(frequency): 559 """Checks if frequency is close to any frequency in ignore list. 560 561 The ignore list is harmonics of frequency to be ignored 562 (like power noise), plus harmonics of dominant frequencies, 563 plus DC. 564 565 @param frequency: The frequency to be tested. 566 567 @returns: True if the frequency should be ignored. False otherwise. 568 569 """ 570 for ignore_frequency in (ignore_frequencies_harmonics + harmonics 571 + [0.0]): 572 if (abs(frequency - ignore_frequency) < 573 frequency_diff_threshold): 574 logging.debug('Ignore frequency: %s', frequency) 575 return True 576 577 # Checks DC is small enough. 578 for freq, coeff in spectral: 579 if freq < _DC_FREQ_THRESHOLD and coeff > _DC_COEFF_THRESHOLD: 580 errors.append( 581 'Channel %d: Found large DC coefficient: ' 582 '(%f Hz, %f)' % (test_channel, freq, coeff)) 583 584 # Filter out the frequencies to be ignored. 585 spectral_post_ignore = [ 586 x for x in spectral if not should_be_ignored(x[0])] 587 588 if len(spectral_post_ignore) > 1: 589 first_coeff = spectral_post_ignore[0][1] 590 second_coeff = spectral_post_ignore[1][1] 591 if second_coeff > first_coeff * second_peak_ratio: 592 errors.append( 593 'Channel %d: Found large second dominant frequencies: ' 594 '%s' % (test_channel, spectral_post_ignore)) 595 596 if not spectral_post_ignore: 597 errors.append( 598 'Channel %d: No frequency left after removing unwanted ' 599 'frequencies. Spectral: %s; After removing unwanted ' 600 'frequencies: %s' % 601 (test_channel, spectral, spectral_post_ignore)) 602 603 else: 604 dominant_spectrals.append(spectral_post_ignore[0]) 605 606 if errors: 607 raise error.TestFail(', '.join(errors)) 608 609 return dominant_spectrals 610 611 612def longest_common_subsequence(list1, list2, equivalent_threshold): 613 """Finds longest common subsequence of list1 and list2 614 615 Such as list1: [0.3, 0.4], 616 list2: [0.001, 0.299, 0.002, 0.401, 0.001] 617 equivalent_threshold: 0.001 618 it will return matched1: [True, True], 619 matched2: [False, True, False, True, False] 620 621 @param list1: a list of integer or float value 622 @param list2: a list of integer or float value 623 @param equivalent_threshold: two values are considered equivalent if their 624 relative error is less than 625 equivalent_threshold. 626 627 @returns: a tuple of list (matched_1, matched_2) indicating each item 628 of list1 and list2 are matched or not. 629 630 """ 631 length1, length2 = len(list1), len(list2) 632 matching = [[0] * (length2 + 1)] * (length1 + 1) 633 # matching[i][j] is the maximum number of matched pairs for first i items 634 # in list1 and first j items in list2. 635 for i in xrange(length1): 636 for j in xrange(length2): 637 # Maximum matched pairs may be obtained without 638 # i-th item in list1 or without j-th item in list2 639 matching[i + 1][j + 1] = max(matching[i + 1][j], 640 matching[i][j + 1]) 641 diff = abs(list1[i] - list2[j]) 642 relative_error = diff / list1[i] 643 # If i-th item in list1 can be matched to j-th item in list2 644 if relative_error < equivalent_threshold: 645 matching[i + 1][j + 1] = matching[i][j] + 1 646 647 # Backtracking which item in list1 and list2 are matched 648 matched1 = [False] * length1 649 matched2 = [False] * length2 650 i, j = length1, length2 651 while i > 0 and j > 0: 652 # Maximum number is obtained by matching i-th item in list1 653 # and j-th one in list2. 654 if matching[i][j] == matching[i - 1][j - 1] + 1: 655 matched1[i - 1] = True 656 matched2[j - 1] = True 657 i, j = i - 1, j - 1 658 elif matching[i][j] == matching[i - 1][j]: 659 i -= 1 660 else: 661 j -= 1 662 return (matched1, matched2) 663 664 665def switch_to_hsp(audio_facade): 666 """Switches to HSP profile. 667 668 Selects bluetooth microphone and runs a recording process on Cros device. 669 This triggers bluetooth profile be switched from A2DP to HSP. 670 Note the user can call stop_recording on audio facade to stop the recording 671 process, or let multimedia_xmlrpc_server terminates it in its cleanup. 672 673 """ 674 audio_facade.set_chrome_active_node_type(None, 'BLUETOOTH') 675 check_audio_nodes(audio_facade, (None, ['BLUETOOTH'])) 676 audio_facade.start_recording( 677 dict(file_type='raw', sample_format='S16_LE', channel=2, 678 rate=48000)) 679 680 681def compare_recorded_correlation(golden_file, recorder, parameters=None): 682 """Checks recorded audio in an AudioInputWidget against a golden file. 683 684 Compares recorded data with golden data by cross correlation method. 685 Refer to audio_helper.compare_data for details of comparison. 686 687 @param golden_file: An AudioTestData object that serves as golden data. 688 @param recorder: An AudioInputWidget that has recorded some audio data. 689 @param parameters: A dict containing parameters for method. 690 691 """ 692 logging.info('Comparing recorded data with golden file %s ...', 693 golden_file.path) 694 audio_helper.compare_data_correlation( 695 golden_file.get_binary(), golden_file.data_format, 696 recorder.get_binary(), recorder.data_format, recorder.channel_map, 697 parameters) 698 699 700def check_and_set_chrome_active_node_types(audio_facade, output_type=None, 701 input_type=None): 702 """Check the target types are available, and set them to be active nodes. 703 704 @param audio_facade: An AudioFacadeNative or AudioFacadeAdapter object. 705 @output_type: An output node type defined in cras_utils.CRAS_NODE_TYPES. 706 None to skip. 707 @input_type: An input node type defined in cras_utils.CRAS_NODE_TYPES. 708 None to skip. 709 710 @raises: error.TestError if the expected node type is missing. We use 711 error.TestError here because usually this step is not the main 712 purpose of the test, but a setup step. 713 714 """ 715 output_types, input_types = audio_facade.get_plugged_node_types() 716 logging.debug('Plugged types: output: %r, input: %r', 717 output_types, input_types) 718 if output_type and output_type not in output_types: 719 raise error.TestError( 720 'Target output type %s not present' % output_type) 721 if input_type and input_type not in input_types: 722 raise error.TestError( 723 'Target input type %s not present' % input_type) 724 audio_facade.set_chrome_active_node_type(output_type, input_type) 725 726 727def check_hp_or_lineout_plugged(audio_facade): 728 """Checks whether line-out or headphone is plugged. 729 730 @param audio_facade: A RemoteAudioFacade to access audio functions on 731 Cros device. 732 733 @returns: 'LINEOUT' if line-out node is plugged. 734 'HEADPHONE' if headphone node is plugged. 735 736 @raises: error.TestFail if the plugged nodes does not contain one of 737 'LINEOUT' and 'HEADPHONE'. 738 739 """ 740 # Checks whether line-out or headphone is detected. 741 output_nodes, _ = audio_facade.get_plugged_node_types() 742 if 'LINEOUT' in output_nodes: 743 return 'LINEOUT' 744 if 'HEADPHONE' in output_nodes: 745 return 'HEADPHONE' 746 raise error.TestFail('Can not detect line-out or headphone') 747