• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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