blob: 8c24dad3d18d7a0ad4f22764f9cf14bfb76eb250 [file] [log] [blame]
# Lint as: python2, python3
# 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.
"""Server side Bluetooth audio tests."""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import logging
import os
import re
import subprocess
import time
import common
from autotest_lib.client.bin import utils
from autotest_lib.client.common_lib import error
from autotest_lib.client.cros.bluetooth.bluetooth_audio_test_data import (
A2DP, HFP_NBS, HFP_NBS_MEDIUM, HFP_WBS, HFP_WBS_MEDIUM,
AUDIO_DATA_TARBALL_PATH, VISQOL_BUFFER_LENGTH, DATA_DIR, VISQOL_PATH,
VISQOL_SIMILARITY_MODEL, VISQOL_TEST_DIR, AUDIO_RECORD_DIR,
audio_test_data, get_audio_test_data, get_visqol_binary)
from autotest_lib.server.cros.bluetooth.bluetooth_adapter_tests import (
BluetoothAdapterTests, test_retry_and_log)
from six.moves import range
class BluetoothAdapterAudioTests(BluetoothAdapterTests):
"""Server side Bluetooth adapter audio test class."""
DEVICE_TYPE = 'BLUETOOTH_AUDIO'
FREQUENCY_TOLERANCE_RATIO = 0.01
WAIT_DAEMONS_READY_SECS = 1
DEFAULT_CHUNK_IN_SECS = 1
IGNORE_LAST_FEW_CHUNKS = 2
# Useful constant for upsampling NBS files for compatibility with ViSQOL
MIN_VISQOL_SAMPLE_RATE = 16000
# The node types of the bluetooth output nodes in cras are the same for both
# A2DP and HFP.
CRAS_BLUETOOTH_OUTPUT_NODE_TYPE = 'BLUETOOTH'
CRAS_INTERNAL_SPEAKER_OUTPUT_NODE_TYPE = 'INTERNAL_SPEAKER'
# The node types of the bluetooth input nodes in cras are different for WBS
# and NBS.
CRAS_HFP_BLUETOOTH_INPUT_NODE_TYPE = {HFP_WBS: 'BLUETOOTH',
HFP_NBS: 'BLUETOOTH_NB_MIC'}
def _get_pulseaudio_bluez_source(self, get_source_method, device,
test_profile):
"""Get the specified bluez device number in the pulseaudio source list.
@param get_source_method: the method to get distinct bluez source
@param device: the bluetooth peer device
@param test_profile: the test profile used, A2DP, HFP_WBS or HFP_NBS
@returns: True if the specified bluez source is derived
"""
sources = device.ListSources(test_profile)
logging.debug('ListSources()\n%s', sources)
self.bluez_source = get_source_method(test_profile)
result = bool(self.bluez_source)
if result:
logging.debug('bluez_source device number: %s', self.bluez_source)
else:
logging.debug('waiting for bluez_source ready in pulseaudio...')
return result
def _get_pulseaudio_bluez_sink(self, get_sink_method, device, test_profile):
"""Get the specified bluez device number in the pulseaudio sink list.
@param get_sink_method: the method to get distinct bluez sink
@param device: the bluetooth peer device
@param test_profile: the test profile used, A2DP, HFP_WBS or HFP_NBS
@returns: True if the specified bluez sink is derived
"""
sinks = device.ListSinks(test_profile)
logging.debug('ListSinks()\n%s', sinks)
self.bluez_sink = get_sink_method(test_profile)
result = bool(self.bluez_sink)
if result:
logging.debug('bluez_sink device number: %s', self.bluez_sink)
else:
logging.debug('waiting for bluez_sink ready in pulseaudio...')
return result
def _get_pulseaudio_bluez_source_a2dp(self, device, test_profile):
"""Get the a2dp bluez source device number.
@param device: the bluetooth peer device
@param test_profile: the test profile used, A2DP, HFP_WBS or HFP_NBS
@returns: True if the specified a2dp bluez source is derived
"""
return self._get_pulseaudio_bluez_source(
device.GetBluezSourceA2DPDevice, device, test_profile)
def _get_pulseaudio_bluez_source_hfp(self, device, test_profile):
"""Get the hfp bluez source device number.
@param device: the bluetooth peer device
@param test_profile: the test profile used, A2DP, HFP_WBS or HFP_NBS
@returns: True if the specified hfp bluez source is derived
"""
return self._get_pulseaudio_bluez_source(
device.GetBluezSourceHFPDevice, device, test_profile)
def _get_pulseaudio_bluez_sink_hfp(self, device, test_profile):
"""Get the hfp bluez sink device number.
@param device: the bluetooth peer device
@param test_profile: the test profile used, A2DP, HFP_WBS or HFP_NBS
@returns: True if the specified hfp bluez sink is derived
"""
return self._get_pulseaudio_bluez_sink(
device.GetBluezSinkHFPDevice, device, test_profile)
def _check_audio_frames_legitimacy(self, audio_test_data, recording_device,
recorded_file=None):
"""Check if audio frames in the recorded file are legitimate.
For a wav file, a simple check is to make sure the recorded audio file
is not empty.
For a raw file, a simple check is to make sure the recorded audio file
are not all zeros.
@param audio_test_data: a dictionary about the audio test data
defined in client/cros/bluetooth/bluetooth_audio_test_data.py
@param recording_device: which device recorded the audio,
possible values are 'recorded_by_dut' or 'recorded_by_peer'
@param recorded_file: the recorded file name
@returns: True if audio frames are legitimate.
"""
result = self.bluetooth_facade.check_audio_frames_legitimacy(
audio_test_data, recording_device, recorded_file)
if not result:
self.results = {'audio_frames_legitimacy': 'empty or all zeros'}
logging.error('The recorded audio file is empty or all zeros.')
return result
def _check_frequency(self, test_profile, recorded_freq, expected_freq):
"""Check if the recorded frequency is within tolerance.
@param test_profile: the test profile used, A2DP, HFP_WBS or HFP_NBS
@param recorded_freq: the frequency of recorded audio
@param expected_freq: the expected frequency
@returns: True if the recoreded frequency falls within the tolerance of
the expected frequency
"""
tolerance = expected_freq * self.FREQUENCY_TOLERANCE_RATIO
return abs(expected_freq - recorded_freq) <= tolerance
def _check_primary_frequencies(self, test_profile, audio_test_data,
recording_device, recorded_file=None):
"""Check if the recorded frequencies meet expectation.
@param test_profile: the test profile used, A2DP, HFP_WBS or HFP_NBS
@param audio_test_data: a dictionary about the audio test data
defined in client/cros/bluetooth/bluetooth_audio_test_data.py
@param recording_device: which device recorded the audio,
possible values are 'recorded_by_dut' or 'recorded_by_peer'
@param recorded_file: the recorded file name
@returns: True if the recorded frequencies of all channels fall within
the tolerance of expected frequencies
"""
recorded_frequencies = self.bluetooth_facade.get_primary_frequencies(
audio_test_data, recording_device, recorded_file)
expected_frequencies = audio_test_data['frequencies']
final_result = True
self.results = dict()
if len(recorded_frequencies) < len(expected_frequencies):
logging.error('recorded_frequencies: %s, expected_frequencies: %s',
str(recorded_frequencies), str(expected_frequencies))
final_result = False
else:
for channel, expected_freq in enumerate(expected_frequencies):
recorded_freq = recorded_frequencies[channel]
ret_val = self._check_frequency(
test_profile, recorded_freq, expected_freq)
pass_fail_str = 'pass' if ret_val else 'fail'
result = ('primary frequency %d (expected %d): %s' %
(recorded_freq, expected_freq, pass_fail_str))
self.results['Channel %d' % channel] = result
logging.info('Channel %d: %s', channel, result)
if not ret_val:
final_result = False
logging.debug(str(self.results))
if not final_result:
logging.error('Failure at checking primary frequencies')
return final_result
def _poll_for_condition(self, condition, timeout=20, sleep_interval=1,
desc='waiting for condition'):
try:
utils.poll_for_condition(condition=condition,
timeout=timeout,
sleep_interval=sleep_interval,
desc=desc)
except Exception as e:
raise error.TestError('Exception occurred when %s (%s)' % (desc, e))
def _scp_to_dut(self, device, src_file, dest_file):
"""SCP file from peer device to DuT."""
ip = self.host.ip
# Localhost is unlikely to be the correct ip target so take the local
# host ip if it exists.
if self.host.ip == '127.0.0.1' and self.local_host_ip:
ip = self.local_host_ip
logging.info('Using local host ip = %s', ip)
device.ScpToDut(src_file, dest_file, ip)
def check_wbs_capability(self):
"""Check if the DUT supports WBS capability.
@raises: TestNAError if the dut does not support wbs.
"""
capabilities, err = self.bluetooth_facade.get_supported_capabilities()
return err is None and bool(capabilities.get('wide band speech'))
def initialize_bluetooth_audio(self, device, test_profile):
"""Initialize the Bluetooth audio task.
Note: pulseaudio is not stable. Need to restart it in the beginning.
@param device: the bluetooth peer device
@param test_profile: the test profile used, A2DP, HFP_WBS or HFP_NBS
"""
if not self.bluetooth_facade.create_audio_record_directory(
AUDIO_RECORD_DIR):
raise error.TestError('Failed to create %s on the DUT' %
AUDIO_RECORD_DIR)
if not device.StartPulseaudio(test_profile):
raise error.TestError('Failed to start pulseaudio.')
logging.debug('pulseaudio is started.')
if test_profile in (HFP_WBS, HFP_NBS, HFP_NBS_MEDIUM, HFP_WBS_MEDIUM):
if device.StartOfono():
logging.debug('ofono is started.')
else:
raise error.TestError('Failed to start ofono.')
elif device.StopOfono():
logging.debug('ofono is stopped.')
else:
logging.warning('Failed to stop ofono. Ignored.')
# Need time to complete starting services.
time.sleep(self.WAIT_DAEMONS_READY_SECS)
def cleanup_bluetooth_audio(self, device, test_profile):
"""Cleanup for Bluetooth audio.
@param device: the bluetooth peer device
@param test_profile: the test profile used, A2DP, HFP_WBS or HFP_NBS
"""
if device.StopPulseaudio():
logging.debug('pulseaudio is stopped.')
else:
logging.warning('Failed to stop pulseaudio. Ignored.')
if device.StopOfono():
logging.debug('ofono is stopped.')
else:
logging.warning('Failed to stop ofono. Ignored.')
def initialize_bluetooth_player(self, device):
"""Initialize the Bluetooth media player.
@param device: the Bluetooth peer device.
"""
if not device.ExportMediaPlayer():
raise error.TestError('Failed to export media player.')
logging.debug('mpris-proxy is started.')
# Wait for player to show up and observed by playerctl.
desc='waiting for media player'
self._poll_for_condition(
lambda: bool(device.GetExportedMediaPlayer()), desc=desc)
def cleanup_bluetooth_player(self, device):
"""Cleanup for Bluetooth media player.
@param device: the bluetooth peer device.
"""
device.UnexportMediaPlayer()
def parse_visqol_output(self, stdout, stderr):
"""
Parse stdout and stderr string from VISQOL output and parse into
a float score.
On error, stderr will contain the error message, otherwise will be None.
On success, stdout will be a string, first line will be
VISQOL version, followed by indication of speech mode. Followed by
paths to reference and degraded file, and a float MOS-LQO score, which
is what we're interested in. Followed by more detailed charts about
specific scoring by segments of the files. Stdout is None on error.
@param stdout: The stdout bytes from commandline output of VISQOL.
@param stderr: The stderr bytes from commandline output of VISQOL.
@returns: A tuple of a float score and string representation of the
srderr or None if there was no error.
"""
string_out = stdout.decode('utf-8') or ''
stderr = stderr.decode('utf-8')
# Log verbose VISQOL output:
log_file = os.path.join(VISQOL_TEST_DIR, 'VISQOL_LOG.txt')
with open(log_file, 'w+') as f:
f.write('String Error:\n{}\n'.format(stderr))
f.write('String Out:\n{}\n'.format(string_out))
# pattern matches first float or int after 'MOS-LQO:' in stdout,
# e.g. it would match the line 'MOS-LQO 2.3' in the stdout
score_pattern = re.compile(r'.*MOS-LQO:\s*(\d+.?\d*)')
score_search = re.search(score_pattern, string_out)
# re.search returns None if no pattern match found, otherwise the score
# would be in the match object's group 1 matches just the float score
score = float(score_search.group(1)) if score_search else -1.0
return stderr, score
def get_visqol_score(self, ref_file, deg_file, speech_mode=True,
verbose=True):
"""
Runs VISQOL using the subprocess library on the provided reference file
and degraded file and returns the VISQOL score.
@param ref_file: File path to the reference wav file.
@param deg_file: File path to the degraded wav file.
@param speech_mode: [Optional] Defaults to True, accepts 16k sample
rate files and ignores frequencies > 8kHz for scoring.
@param verbose: [Optional] Defaults to True, outputs more details.
@returns: A float score for the tested file.
"""
visqol_cmd = [VISQOL_PATH]
visqol_cmd += ['--reference_file', ref_file]
visqol_cmd += ['--degraded_file', deg_file]
visqol_cmd += ['--similarity_to_quality_model', VISQOL_SIMILARITY_MODEL]
if speech_mode:
visqol_cmd.append('--use_speech_mode')
if verbose:
visqol_cmd.append('--verbose')
visqol_process = subprocess.Popen(visqol_cmd, stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
stdout, stderr = visqol_process.communicate()
err, score = self.parse_visqol_output(stdout, stderr)
if err:
raise error.TestError(err)
elif score < 0.0:
raise error.TestError('Failed to parse score, got {}'.format(score))
return score
def get_ref_and_deg_files(self, trimmed_file, test_profile, test_data):
"""Return path for reference and degraded files to run visqol on.
@param trimmed_file: Path to the trimmed audio file on DUT.
@param test_profile: The test profile used HFP_WBS or HFP_NBS.
@param test_data: A dictionary about the audio test data defined in
client/cros/bluetooth/bluetooth_audio_test_data.py.
@returns: A tuple of path to the reference file and degraded file if
they exist, otherwise False for the files that aren't available.
"""
# Path in autotest server in ViSQOL folder to store degraded file from
# retrieved from the DUT
deg_file = os.path.join(VISQOL_TEST_DIR, os.path.split(trimmed_file)[1])
played_file = test_data['file']
# If profile is WBS, no resampling required
if test_profile == HFP_WBS:
self.host.get_file(trimmed_file, deg_file)
return played_file, deg_file
# On NBS, degraded and reference files need to be resampled to 16 kHz
# Build path for the upsampled (us) reference (ref) file on DUT
ref_file = '{}_us_ref{}'.format(*os.path.splitext(played_file))
# If resampled ref file already exists, don't need to do it again
if not os.path.isfile(ref_file):
if not self.bluetooth_facade.convert_audio_sample_rate(
played_file, ref_file, test_data,
self.MIN_VISQOL_SAMPLE_RATE):
return False, False
# Move upsampled reference file to autotest server
self.host.get_file(ref_file, ref_file)
# Build path for resampled degraded file on DUT
deg_on_dut = '{}_us{}'.format(*os.path.splitext(trimmed_file))
# Resample degraded file to 16 kHz and move to autotest server
if not self.bluetooth_facade.convert_audio_sample_rate(
trimmed_file, deg_on_dut, test_data,
self.MIN_VISQOL_SAMPLE_RATE):
return ref_file, False
self.host.get_file(deg_on_dut, deg_file)
return ref_file, deg_file
def format_recorded_file(self, test_data, test_profile, recording_device):
"""Format recorded files to be compatible with ViSQOL.
Convert raw files to wav if recorded file is a raw file, trim file to
duration, if required, resample the file, then lastly return the paths
for the reference file and degraded file on the autotest server.
@param test_data: A dictionary about the audio test data defined in
client/cros/bluetooth/bluetooth_audio_test_data.py.
@param test_profile: The test profile used, HFP_WBS or HFP_NBS.
@param recording_device: Which device recorded the audio, either
'recorded_by_dut' or 'recorded_by_peer'.
@returns: A tuple of path to the reference file and degraded file if
they exist, otherwise False for the files that aren't available.
"""
# Path to recorded file either on DUT or BT peer
recorded_file = test_data[recording_device]
untrimmed_file = recorded_file
if recorded_file.endswith('.raw'):
# build path for file converted from raw to wav, i.e. change the ext
untrimmed_file = os.path.splitext(recorded_file)[0] + '.wav'
if not self.bluetooth_facade.convert_raw_to_wav(
recorded_file, untrimmed_file, test_data):
raise error.TestError('Could not convert raw file to wav')
# Compute the duration of played file without added buffer
new_duration = (test_data['chunk_checking_duration'] -
VISQOL_BUFFER_LENGTH)
# build path for file resulting from trimming to desired duration
trimmed_file = '{}_t{}'.format(*os.path.splitext(untrimmed_file))
if not self.bluetooth_facade.trim_wav_file(
untrimmed_file, trimmed_file, new_duration, test_data):
raise error.TestError('Failed to trim recorded file')
return self.get_ref_and_deg_files(trimmed_file, test_profile, test_data)
def handle_one_chunk(self, device, chunk_in_secs, index, test_profile):
"""Handle one chunk of audio data by calling chameleon api."""
ip = self.host.ip
# Localhost is unlikely to be the correct ip target so take the local
# host ip if it exists.
if self.host.ip == '127.0.0.1' and self.local_host_ip:
ip = self.local_host_ip
logging.info('Using local host ip = %s', ip)
# TODO(b/207046142): Remove the old version fallback after the new
# Chameleon bundle is deployed.
try:
recorded_file = device.HandleOneChunk(chunk_in_secs, index, ip)
except Exception as e:
logging.debug("Unable to use new version of HandleOneChunk;"
"fall back to use the old one.")
try:
recorded_file = device.HandleOneChunk(chunk_in_secs, index,
test_profile, ip)
except Exception as e:
raise error.TestError('Failed to handle chunk (%s)', e)
return recorded_file
# ---------------------------------------------------------------
# Definitions of all bluetooth audio test cases
# ---------------------------------------------------------------
@test_retry_and_log(False)
def test_select_audio_input_device(self, device_name):
"""Select the audio input device for the DUT.
@param: device_name: the audio input device to be selected.
@returns: True on success. Raise otherwise.
"""
desc = 'waiting for cras to select audio input device'
logging.debug(desc)
self._poll_for_condition(
lambda: self.bluetooth_facade.select_input_device(device_name),
desc=desc)
return True
@test_retry_and_log(False)
def test_select_audio_output_node_bluetooth(self):
"""Select the Bluetooth device as output node.
@returns: True on success. False otherwise.
"""
return self._test_select_audio_output_node(
self.CRAS_BLUETOOTH_OUTPUT_NODE_TYPE)
@test_retry_and_log(False)
def test_select_audio_output_node_internal_speaker(self):
"""Select the internal speaker as output node.
@returns: True on success. False otherwise.
"""
return self._test_select_audio_output_node(
self.CRAS_INTERNAL_SPEAKER_OUTPUT_NODE_TYPE)
def _test_select_audio_output_node(self, node_type=None):
"""Select the audio output node through cras.
@param node_type: a str representing node type defined in
CRAS_NODE_TYPES.
@raises: error.TestError if failed.
@return True if select given node success.
"""
def node_type_selected(node_type):
"""Check if the given node type is selected."""
selected = self.bluetooth_facade.get_selected_output_device_type()
logging.debug('active output node type: %s, expected %s', selected,
node_type)
return selected == node_type
desc = 'waiting for bluetooth_facade.select_output_node()'
self._poll_for_condition(
lambda: self.bluetooth_facade.select_output_node(node_type),
desc=desc)
desc = 'waiting for %s as active cras audio output node type' % node_type
logging.debug(desc)
self._poll_for_condition(lambda: node_type_selected(node_type),
desc=desc)
return True
@test_retry_and_log(False)
def test_audio_is_alive_on_dut(self):
"""Test that if the audio stream is alive on the DUT.
@returns: True if the audio summary is found on the DUT.
"""
summary = self.bluetooth_facade.get_audio_thread_summary()
result = bool(summary)
# If we can find something starts with summary like: "Summary: Output
# device [Silent playback device.] 4096 48000 2 Summary: Output stream
# CRAS_CLIENT_TYPE_TEST CRAS_STREAM_TYPE_DEFAULT 480 240 0x0000 48000
# 2 0" this means that there's an audio stream alive on the DUT.
desc = " ".join(str(line) for line in summary)
logging.debug('find summary: %s', desc)
self.results = {'test_audio_is_alive_on_dut': result}
return all(self.results.values())
@test_retry_and_log(False)
def test_check_chunks(self,
device,
test_profile,
test_data,
duration,
check_legitimacy=True,
check_frequencies=True):
"""Check chunks of recorded streams and verify the primary frequencies.
@param device: the bluetooth peer device
@param test_profile: the a2dp test profile;
choices are A2DP and A2DP_LONG
@param test_data: the test data of the test profile
@param duration: the duration of the audio file to test
@param check_legitimacy: specify this to True to run
_check_audio_frames_legitimacy test
@param check_frequencies: specify this to True to run
_check_primary_frequencies test
@returns: True if all chunks pass the frequencies check.
"""
chunk_in_secs = test_data['chunk_in_secs']
if not bool(chunk_in_secs):
chunk_in_secs = self.DEFAULT_CHUNK_IN_SECS
nchunks = duration // chunk_in_secs
logging.info('Number of chunks: %d', nchunks)
check_audio_frames_legitimacy = True
check_primary_frequencies = True
for i in range(nchunks):
logging.debug('Check chunk %d', i)
recorded_file = self.handle_one_chunk(device, chunk_in_secs, i,
test_profile)
if recorded_file is None:
raise error.TestError('Failed to handle chunk %d' % i)
if check_legitimacy:
# Check if the audio frames in the recorded file are legitimate.
if not self._check_audio_frames_legitimacy(
test_data, 'recorded_by_peer', recorded_file=recorded_file):
if (i > self.IGNORE_LAST_FEW_CHUNKS and
i >= nchunks - self.IGNORE_LAST_FEW_CHUNKS):
logging.info('empty chunk %d ignored for last %d chunks',
i, self.IGNORE_LAST_FEW_CHUNKS)
else:
check_audio_frames_legitimacy = False
break
if check_frequencies:
# Check if the primary frequencies of the recorded file
# meet expectation.
if not self._check_primary_frequencies(
test_profile,
test_data,
'recorded_by_peer',
recorded_file=recorded_file):
if (i > self.IGNORE_LAST_FEW_CHUNKS and
i >= nchunks - self.IGNORE_LAST_FEW_CHUNKS):
msg = 'partially filled chunk %d ignored for last %d chunks'
logging.info(msg, i, self.IGNORE_LAST_FEW_CHUNKS)
else:
check_primary_frequencies = False
break
self.results = dict()
if check_legitimacy:
self.results['check_audio_frames_legitimacy'] = (
check_audio_frames_legitimacy)
if check_frequencies:
self.results['check_primary_frequencies'] = (
check_primary_frequencies)
return all(self.results.values())
@test_retry_and_log(False)
def test_check_empty_chunks(self, device, test_data, duration,
test_profile):
"""Check if all the chunks are empty.
@param device: The Bluetooth peer device.
@param test_data: The test data of the test profile.
@param duration: The duration of the audio file to test.
@param test_profile: Which audio profile is used. Profiles are defined
in bluetooth_audio_test_data.py.
@returns: True if all the chunks are empty.
"""
chunk_in_secs = test_data['chunk_in_secs']
if not bool(chunk_in_secs):
chunk_in_secs = self.DEFAULT_CHUNK_IN_SECS
nchunks = duration // chunk_in_secs
logging.info('Number of chunks: %d', nchunks)
all_chunks_empty = True
for i in range(nchunks):
logging.info('Check chunk %d', i)
recorded_file = self.handle_one_chunk(device, chunk_in_secs, i,
test_profile)
if recorded_file is None:
raise error.TestError('Failed to handle chunk %d' % i)
# Check if the audio frames in the recorded file are legitimate.
if self._check_audio_frames_legitimacy(
test_data, 'recorded_by_peer', recorded_file):
if (i > self.IGNORE_LAST_FEW_CHUNKS and
i >= nchunks - self.IGNORE_LAST_FEW_CHUNKS):
logging.info('empty chunk %d ignored for last %d chunks',
i, self.IGNORE_LAST_FEW_CHUNKS)
else:
all_chunks_empty = False
break
self.results = {'all chunks are empty': all_chunks_empty}
return all(self.results.values())
@test_retry_and_log(False)
def test_check_audio_file(self,
device,
test_profile,
test_data,
recording_device,
check_legitimacy=True,
check_frequencies=True):
"""Check the audio file and verify the primary frequencies.
@param device: the Bluetooth peer device.
@param test_profile: A2DP or HFP test profile.
@param test_data: the test data of the test profile.
@param recording_device: which device recorded the audio,
possible values are 'recorded_by_dut' or 'recorded_by_peer'.
@param check_legitimacy: if set this to True, run
_check_audio_frames_legitimacy test.
@param check_frequencies: if set this to True, run
_check_primary_frequencies test.
@returns: True if audio file passes the frequencies check.
"""
if recording_device == 'recorded_by_peer':
logging.debug('Scp to DUT')
try:
recorded_file = test_data[recording_device]
self._scp_to_dut(device, recorded_file, recorded_file)
logging.debug('Recorded {} successfully'.format(recorded_file))
except Exception as e:
raise error.TestError('Exception occurred when (%s)' % (e))
self.results = dict()
if check_legitimacy:
self.results['check_audio_frames_legitimacy'] = (
self._check_audio_frames_legitimacy(
test_data, recording_device))
if check_frequencies:
self.results['check_primary_frequencies'] = (
self._check_primary_frequencies(
test_profile, test_data, recording_device))
return all(self.results.values())
@test_retry_and_log(False)
def test_dut_to_start_playing_audio_subprocess(self,
test_data,
pin_device=None):
"""Start playing audio in a subprocess.
@param test_data: the audio test data
@returns: True on success. False otherwise.
"""
start_playing_audio = self.bluetooth_facade.start_playing_audio_subprocess(
test_data, pin_device)
self.results = {
'dut_to_start_playing_audio_subprocess': start_playing_audio
}
return all(self.results.values())
@test_retry_and_log(False)
def test_dut_to_stop_playing_audio_subprocess(self):
"""Stop playing audio in the subprocess.
@returns: True on success. False otherwise.
"""
stop_playing_audio = (
self.bluetooth_facade.stop_playing_audio_subprocess())
self.results = {
'dut_to_stop_playing_audio_subprocess': stop_playing_audio
}
return all(self.results.values())
@test_retry_and_log(False)
def test_dut_to_start_capturing_audio_subprocess(self, audio_data,
recording_device):
"""Start capturing audio in a subprocess.
@param audio_data: the audio test data
@param recording_device: which device recorded the audio,
possible values are 'recorded_by_dut' or 'recorded_by_peer'
@returns: True on success. False otherwise.
"""
# Let the dut capture audio stream until it is stopped explicitly by
# setting duration to None. This is required on some slower devices.
audio_data = audio_data.copy()
audio_data.update({'duration': None})
start_capturing_audio = self.bluetooth_facade.start_capturing_audio_subprocess(
audio_data, recording_device)
self.results = {
'dut_to_start_capturing_audio_subprocess':
start_capturing_audio
}
return all(self.results.values())
@test_retry_and_log(False)
def test_dut_to_stop_capturing_audio_subprocess(self):
"""Stop capturing audio.
@returns: True on success. False otherwise.
"""
stop_capturing_audio = (
self.bluetooth_facade.stop_capturing_audio_subprocess())
self.results = {
'dut_to_stop_capturing_audio_subprocess': stop_capturing_audio
}
return all(self.results.values())
@test_retry_and_log(False)
def test_device_to_start_playing_audio_subprocess(self, device,
test_profile, test_data):
"""Start playing the audio file in a subprocess.
@param device: the bluetooth peer device
@param test_data: the audio file to play and data about the file
@param audio_profile: the audio profile, either a2dp, hfp_wbs, or hfp_nbs
@returns: True on success. False otherwise.
"""
start_playing_audio = device.StartPlayingAudioSubprocess(
test_profile, test_data)
self.results = {
'device_to_start_playing_audio_subprocess': start_playing_audio
}
return all(self.results.values())
@test_retry_and_log(False)
def test_device_to_stop_playing_audio_subprocess(self, device):
"""Stop playing the audio file in a subprocess.
@param device: the bluetooth peer device
@returns: True on success. False otherwise.
"""
stop_playing_audio = device.StopPlayingAudioSubprocess()
self.results = {
'device_to_stop_playing_audio_subprocess': stop_playing_audio
}
return all(self.results.values())
@test_retry_and_log(False)
def test_device_to_start_recording_audio_subprocess(
self, device, test_profile, test_data):
"""Start recording audio in a subprocess.
@param device: the bluetooth peer device
@param test_profile: the audio profile used to get the recording settings
@param test_data: the details of the file being recorded
@returns: True on success. False otherwise.
"""
start_recording_audio = device.StartRecordingAudioSubprocess(
test_profile, test_data)
self.results = {
'device_to_start_recording_audio_subprocess':
start_recording_audio
}
return all(self.results.values())
@test_retry_and_log(False)
def test_device_to_stop_recording_audio_subprocess(self, device):
"""Stop the recording subprocess.
@returns: True on success. False otherwise.
"""
stop_recording_audio = device.StopRecordingingAudioSubprocess()
self.results = {
'device_to_stop_recording_audio_subprocess':
stop_recording_audio
}
return all(self.results.values())
@test_retry_and_log(False)
def test_device_a2dp_connected(self, device, timeout=15):
""" Tests a2dp profile is connected on device. """
self.results = {}
check_connection = lambda: self._get_pulseaudio_bluez_source_a2dp(
device, A2DP)
is_connected = self._wait_for_condition(check_connection,
'test_device_a2dp_connected',
timeout=timeout)
self.results['peer a2dp connected'] = is_connected
return all(self.results.values())
@test_retry_and_log(False)
def test_hfp_connected(self,
bluez_function,
device,
test_profile,
timeout=15):
"""Tests HFP profile is connected.
@param bluez_function: the appropriate bluez HFP function either
_get_pulseaudio_bluez_source_hfp or
_get_pulseaudio_bluez_sink_hfp depending on the role of the DUT.
@param device: the Bluetooth peer device.
@param test_profile: which test profile is used, HFP_WBS or HFP_NBS.
@param timeout: number of seconds to wait before giving up connecting
to HFP profile.
@returns: True on success. False otherwise.
"""
check_connection = lambda: bluez_function(device, test_profile)
is_connected = self._wait_for_condition(check_connection,
'test_hfp_connected',
timeout=timeout)
self.results = {'peer hfp connected': is_connected}
return all(self.results.values())
@test_retry_and_log(False)
def test_send_audio_to_dut_and_unzip(self):
"""Send the audio file to the DUT and unzip it.
@returns: True on success. False otherwise.
"""
try:
self.host.send_file(AUDIO_DATA_TARBALL_PATH,
AUDIO_DATA_TARBALL_PATH)
except Exception as e:
raise error.TestError('Fail to send file to the DUT: (%s)', e)
unzip_success = self.bluetooth_facade.unzip_audio_test_data(
AUDIO_DATA_TARBALL_PATH, DATA_DIR)
self.results = {'unzip audio file': unzip_success}
return all(self.results.values())
@test_retry_and_log(False)
def test_get_visqol_score(self, test_file, test_profile, recording_device):
"""Test that if the recorded audio file meets the passing score.
This function also records the visqol performance.
@param device: the Bluetooth peer device.
@param test_profile: which test profile is used, HFP_WBS or HFP_NBS.
@param recording_device: which device recorded the audio,
possible values are 'recorded_by_dut' or 'recorded_by_peer'.
@returns: True if the test files score at or above the
source_passing_score value as defined in
bluetooth_audio_test_data.py.
"""
dut_role = 'sink' if recording_device == 'recorded_by_dut' else 'source'
filename = os.path.split(test_file['file'])[1]
ref_file, deg_file = self.format_recorded_file(test_file, test_profile,
recording_device)
if not ref_file or not deg_file:
desc = 'Failed to get ref and deg file: ref {}, deg {}'.format(
ref_file, deg_file)
raise error.TestError(desc)
score = self.get_visqol_score(ref_file,
deg_file,
speech_mode=test_file['speech_mode'])
key = ''.join((dut_role, '_passing_score'))
logging.info('{} scored {}, min passing score: {}'.format(
filename, score, test_file[key]))
passed = score >= test_file[key]
self.results = {filename: passed}
# Track visqol performance
test_desc = '{}_{}_{}'.format(test_profile, dut_role,
test_file['reporting_type'])
self.write_perf_keyval({test_desc: score})
if not passed:
logging.warning('Failed: {}'.format(filename))
return all(self.results.values())
@test_retry_and_log(False)
def test_avrcp_commands(self, device):
"""Test Case: Test AVRCP commands issued by peer can be received at DUT
The very first AVRCP command (Linux evdev event) the DUT receives
contains extra information than just the AVRCP event, e.g. EV_REP
report used to specify delay settings. Send the first command before
the actual test starts to avoid dealing with them during test.
The peer device name is required to monitor the event reception on the
DUT. However, as the peer device itself already registered with the
kernel as an udev input device. The AVRCP profile will register as an
separate input device with the name pattern: name + (AVRCP), e.g.
RASPI_AUDIO (AVRCP). Using 'AVRCP' as device name to help search for
the device.
@param device: the Bluetooth peer device
@returns: True if the all AVRCP commands received by DUT, false
otherwise
"""
device.SendMediaPlayerCommand('play')
name = device.name
device.name = 'AVRCP'
result_pause = self.test_avrcp_event(device,
device.SendMediaPlayerCommand, 'pause')
result_play = self.test_avrcp_event(device,
device.SendMediaPlayerCommand, 'play')
result_stop = self.test_avrcp_event(device,
device.SendMediaPlayerCommand, 'stop')
result_next = self.test_avrcp_event(device,
device.SendMediaPlayerCommand, 'next')
result_previous = self.test_avrcp_event(device,
device.SendMediaPlayerCommand, 'previous')
device.name = name
self.results = {'pause': result_pause, 'play': result_play,
'stop': result_stop, 'next': result_next,
'previous': result_previous}
return all(self.results.values())
@test_retry_and_log(False)
def test_avrcp_media_info(self, device):
"""Test Case: Test AVRCP media info sent by DUT can be received by peer
The test update all media information twice to prevent previous
leftover data affect the current iteration of test. Then compare the
expected results against the information received on the peer device.
This test verifies media information including: playback status,
length, title, artist, and album. Position of the media is not
currently support as playerctl on the peer side cannot correctly
retrieve such information.
Length and position information are transmitted in the unit of
microsecond. However, BlueZ process those time data in the resolution
of millisecond. Discard microsecond detail when comparing those media
information.
@param device: the Bluetooth peer device
@returns: True if the all AVRCP media info received by DUT, false
otherwise
"""
# First round of updating media information to overwrite all leftovers.
init_status = 'stopped'
init_length = 20200414
init_position = 8686868
init_metadata = {'album': 'metadata_album_init',
'artist': 'metadata_artist_init',
'title': 'metadata_title_init'}
self.bluetooth_facade.set_player_playback_status(init_status)
self.bluetooth_facade.set_player_length(init_length)
self.bluetooth_facade.set_player_position(init_position)
self.bluetooth_facade.set_player_metadata(init_metadata)
# Second round of updating for actual testing.
expected_status = 'playing'
expected_length = 68686868
expected_position = 20200414
expected_metadata = {'album': 'metadata_album_expected',
'artist': 'metadata_artist_expected',
'title': 'metadata_title_expected'}
self.bluetooth_facade.set_player_playback_status(expected_status)
self.bluetooth_facade.set_player_length(expected_length)
self.bluetooth_facade.set_player_position(expected_position)
self.bluetooth_facade.set_player_metadata(expected_metadata)
received_media_info = device.GetMediaPlayerMediaInfo()
logging.debug(received_media_info)
try:
actual_length = int(received_media_info.get('length'))
except:
actual_length = 0
result_status = bool(expected_status ==
received_media_info.get('status').lower())
result_album = bool(expected_metadata['album'] ==
received_media_info.get('album'))
result_artist = bool(expected_metadata['artist'] ==
received_media_info.get('artist'))
result_title = bool(expected_metadata['title'] ==
received_media_info.get('title'))
# The AVRCP time information is in the unit of microseconds but with
# milliseconds resolution. Convert both send and received length into
# milliseconds for comparison.
result_length = bool(expected_length // 1000 == actual_length // 1000)
self.results = {'status': result_status, 'album': result_album,
'artist': result_artist, 'title': result_title,
'length': result_length}
return all(self.results.values())
# ---------------------------------------------------------------
# Definitions of all bluetooth audio test sequences
# ---------------------------------------------------------------
def test_a2dp_sinewaves(self, device, test_profile, duration):
"""Test Case: a2dp sinewaves
@param device: the bluetooth peer device
@param test_profile: the a2dp test profile;
choices are A2DP and A2DP_LONG
@param duration: the duration of the audio file to test
0 means to use the default value in the test profile
"""
# Make a copy since the test_data may be formatted with distinct
# arguments in the follow-up tests.
test_data = audio_test_data[test_profile].copy()
if bool(duration):
test_data['duration'] = duration
else:
duration = test_data['duration']
test_data['file'] %= duration
logging.info('%s test for %d seconds.', test_profile, duration)
# Wait for pulseaudio a2dp bluez source
self.test_device_a2dp_connected(device)
# Select audio output node so that we do not rely on chrome to do it.
self.test_select_audio_output_node_bluetooth()
# Start recording audio on the peer Bluetooth audio device.
self.test_device_to_start_recording_audio_subprocess(
device, test_profile, test_data)
# Play audio on the DUT in a non-blocked way and check the recorded
# audio stream in a real-time manner.
self.test_dut_to_start_playing_audio_subprocess(test_data)
# Check chunks of recorded streams and verify the primary frequencies.
# This is a blocking call until all chunks are completed.
self.test_check_chunks(device, test_profile, test_data, duration)
# Stop recording audio on the peer Bluetooth audio device.
self.test_device_to_stop_recording_audio_subprocess(device)
# Stop playing audio on DUT.
self.test_dut_to_stop_playing_audio_subprocess()
def playback_and_connect(self, device, test_profile):
"""Connect then disconnect an A2DP device while playing stream.
This test first plays the audio stream and then selects the BT device
as output node, checking if the stream has routed to the BT device.
After that, disconnect the BT device and also check whether the stream
closes on it gracefully.
@param device: the Bluetooth peer device.
@param test_profile: to select which A2DP test profile is used.
"""
test_data = audio_test_data[test_profile]
# TODO(b/207046142): Remove the old version fallback after the new
# Chameleon bundle is deployed.
# Currently the BT audio tests store test profile parameters in
# Chameleon bundle. However, we decide to move the test profiles to
# server test. During the transition, the new test code may interact
# with old/existing Chameleon bundle, which does not have A2DP_MEDIUM
# profile. We use a trick here: override the passing-in test_profile
# with A2DP so that Chameleon can look up the profile, and override the
# three parameters locally to make it a A2DP_MEDIUM profile.
test_profile = A2DP
test_data = audio_test_data[test_profile].copy()
test_data['duration'] = 60
test_data['chunk_checking_duration'] = 5
test_data['chunk_in_secs'] = 1
# Start playing audio on the Dut.
self.test_dut_to_start_playing_audio_subprocess(test_data)
# Connect the Bluetooth device.
self.test_device_set_discoverable(device, True)
self.test_discover_device(device.address)
self.test_pairing(device.address, device.pin, trusted=True)
self.test_connection_by_adapter(device.address)
self.test_device_a2dp_connected(device)
# Select Bluetooth as output node.
self.test_select_audio_output_node_bluetooth()
self.test_device_to_start_recording_audio_subprocess(
device, test_profile, test_data)
# Handle chunks of recorded streams and verify the primary frequencies.
# This is a blocking call until all chunks are completed.
self.test_check_chunks(device, test_profile, test_data,
test_data['chunk_checking_duration'])
self.test_device_to_stop_recording_audio_subprocess(device)
self.test_select_audio_output_node_internal_speaker()
# Check if the device disconnects successfully.
self.expect_test(False, self.test_device_a2dp_connected, device)
self.test_dut_to_stop_playing_audio_subprocess()
def playback_and_disconnect(self, device, test_profile):
"""Disconnect the Bluetooth device while the stream is playing.
This test will keep the stream playing and then disconnect the
Bluetooth device. The goal is to check the stream is still alive
after the Bluetooth device disconnected.
@param device: the Bluetooth peer device.
@param test_profile: to select which A2DP test profile is used.
"""
test_data = audio_test_data[test_profile]
# TODO(b/207046142): Remove the old version fallback after the new
# Chameleon bundle is deployed.
# Currently the BT audio tests store test profile parameters in
# Chameleon bundle. However, we decide to move the test profiles to
# server test. During the transition, the new test code may interact
# with old/existing Chameleon bundle, which does not have A2DP_MEDIUM
# profile. We use a trick here: override the passing-in test_profile
# with A2DP so that Chameleon can look up the profile, and override the
# three parameters locally to make it a A2DP_MEDIUM profile.
test_profile = A2DP
test_data = audio_test_data[test_profile].copy()
test_data['duration'] = 60
test_data['chunk_checking_duration'] = 5
test_data['chunk_in_secs'] = 1
# Connect the Bluetooth device.
self.test_device_set_discoverable(device, True)
self.test_discover_device(device.address)
self.test_pairing(device.address, device.pin, trusted=True)
self.test_connection_by_adapter(device.address)
self.test_device_a2dp_connected(device)
# Select Bluetooth as output node.
self.test_select_audio_output_node_bluetooth()
self.test_device_to_start_recording_audio_subprocess(
device, test_profile, test_data)
# Start playing audio on the DUT.
self.test_dut_to_start_playing_audio_subprocess(test_data)
# Handle chunks of recorded streams and verify the primary frequencies.
# This is a blocking call until all chunks are completed.
self.test_check_chunks(device, test_profile, test_data,
test_data['chunk_checking_duration'])
self.test_device_to_stop_recording_audio_subprocess(device)
# Disconnect the Bluetooth device.
self.test_disconnection_by_adapter(device.address)
# Obtain audio thread summary to check if the audio stream is still
# alive.
self.test_audio_is_alive_on_dut()
# Stop playing audio on the DUT.
self.test_dut_to_stop_playing_audio_subprocess()
def playback_back2back(self, device, test_profile):
"""Repeat to start and stop the playback stream several times.
This test repeats to start and stop the playback stream and verify
that the Bluetooth device receives the stream correctly.
@param device: the Bluetooth peer device.
@param test_profile: to select which A2DP test profile is used.
"""
test_data = audio_test_data[test_profile]
# TODO(b/207046142): Remove the old version fallback after the new
# Chameleon bundle is deployed.
# Currently the BT audio tests store test profile parameters in
# Chameleon bundle. However, we decide to move the test profiles to
# server test. During the transition, the new test code may interact
# with old/existing Chameleon bundle, which does not have A2DP_MEDIUM
# profile. We use a trick here: override the passing-in test_profile
# with A2DP so that Chameleon can look up the profile, and override the
# three parameters locally to make it a A2DP_MEDIUM profile.
test_profile = A2DP
test_data = audio_test_data[test_profile].copy()
test_data['duration'] = 60
test_data['chunk_checking_duration'] = 5
test_data['chunk_in_secs'] = 1
self.test_device_set_discoverable(device, True)
self.test_discover_device(device.address)
self.test_pairing(device.address, device.pin, trusted=True)
self.test_connection_by_adapter(device.address)
self.test_device_a2dp_connected(device)
self.test_select_audio_output_node_bluetooth()
for _ in range(3):
# TODO(b/208165757): In here if we record the audio stream before
# playing that will cause an audio blank about 1~2 sec in the
# beginning of the recorded file and make the chunks checking fail.
# Need to fix this problem in the future.
self.test_dut_to_start_playing_audio_subprocess(test_data)
self.test_device_to_start_recording_audio_subprocess(
device, test_profile, test_data)
self.test_check_chunks(device, test_profile, test_data,
test_data['chunk_checking_duration'])
self.test_dut_to_stop_playing_audio_subprocess()
self.test_device_to_stop_recording_audio_subprocess(device)
self.test_device_to_start_recording_audio_subprocess(
device, test_profile, test_data)
self.test_check_empty_chunks(device, test_data,
test_data['chunk_checking_duration'],
test_profile)
self.test_device_to_stop_recording_audio_subprocess(device)
self.test_disconnection_by_adapter(device.address)
def pinned_playback(self, device, test_profile):
"""Play an audio stream that is pinned to the Bluetooth device.
This test does not choose Bluetooth as the output node but directly
plays the sound that is pinned to the Bluetooth device and check
whether it receives the audio stream correctly.
@param device: the Bluetooth peer device.
@param test_profile: to select which A2DP test profile is used.
"""
test_data = audio_test_data[test_profile]
self.test_device_set_discoverable(device, True)
self.test_discover_device(device.address)
self.test_pairing(device.address, device.pin, trusted=True)
self.test_connection_by_adapter(device.address)
self.test_device_a2dp_connected(device)
self.test_device_to_start_recording_audio_subprocess(
device, test_profile, test_data)
# We do not select Bluetooth as output node but play audio pinned to
# the Bluetooth device straight forward.
device_id = self.bluetooth_facade.get_device_id_from_node_type(
self.CRAS_BLUETOOTH_OUTPUT_NODE_TYPE, False)
logging.info("Bluetooth device id for audio stream output: %s",
device_id)
self.test_dut_to_start_playing_audio_subprocess(test_data, device_id)
self.test_check_chunks(device, test_profile, test_data,
test_data['duration'])
self.test_dut_to_stop_playing_audio_subprocess()
self.test_device_to_stop_recording_audio_subprocess(device)
self.test_disconnection_by_adapter(device.address)
def hfp_dut_as_source_visqol_score(self, device, test_profile):
"""Test Case: HFP test files streaming from peer device to the DUT.
@param device: the Bluetooth peer device.
@param test_profile: which test profile is used, HFP_WBS or HFP_NBS.
"""
# list of test wav files
hfp_test_data = audio_test_data[test_profile]
test_files = hfp_test_data['visqol_test_files']
get_visqol_binary()
get_audio_test_data()
# Download test data to the DUT.
self.test_send_audio_to_dut_and_unzip()
for test_file in test_files:
filename = os.path.split(test_file['file'])[1]
logging.debug('Testing file: {}'.format(filename))
self.test_select_audio_input_device(device.name)
self.test_select_audio_output_node_bluetooth()
# Enable HFP profile.
self.test_dut_to_start_capturing_audio_subprocess(
test_file, 'recorded_by_peer')
# Wait for pulseaudio bluez hfp source/sink
self.test_hfp_connected(self._get_pulseaudio_bluez_source_hfp,
device, test_profile)
self.test_device_to_start_recording_audio_subprocess(
device, test_profile, test_file)
# Play audio on the DUT in a non-blocked way.
# If there are issues, cras_test_client playing back might be blocked
# forever. We would like to avoid the testing procedure from that.
self.test_dut_to_start_playing_audio_subprocess(test_file)
time.sleep(test_file['chunk_checking_duration'])
self.test_dut_to_stop_playing_audio_subprocess()
self.test_device_to_stop_recording_audio_subprocess(device)
# Disable HFP profile.
self.test_dut_to_stop_capturing_audio_subprocess()
# Copy the recorded audio file to the DUT for spectrum analysis.
recorded_file = test_file['recorded_by_peer']
self._scp_to_dut(device, recorded_file, recorded_file)
self.test_get_visqol_score(test_file, test_profile,
'recorded_by_peer')
def hfp_dut_as_sink_visqol_score(self, device, test_profile):
"""Test Case: HFP test files streaming from peer device to the DUT.
@param device: the Bluetooth peer device.
@param test_profile: which test profile is used, HFP_WBS or HFP_NBS.
"""
# list of test wav files
hfp_test_data = audio_test_data[test_profile]
test_files = hfp_test_data['visqol_test_files']
get_visqol_binary()
get_audio_test_data()
# Download test data to the DUT.
self.test_send_audio_to_dut_and_unzip()
for test_file in test_files:
filename = os.path.split(test_file['file'])[1]
logging.debug('Testing file: {}'.format(filename))
self.test_select_audio_input_device(device.name)
self.test_select_audio_output_node_bluetooth()
# Enable HFP profile.
self.test_dut_to_start_capturing_audio_subprocess(
test_file, 'recorded_by_dut')
# Wait for pulseaudio bluez hfp source/sink.
self.test_hfp_connected(self._get_pulseaudio_bluez_sink_hfp,
device, test_profile)
self.test_select_audio_input_device(device.name)
self.test_device_to_start_playing_audio_subprocess(
device, test_profile, test_file)
time.sleep(test_file['chunk_checking_duration'])
self.test_device_to_stop_playing_audio_subprocess(device)
# Disable HFP profile.
self.test_dut_to_stop_capturing_audio_subprocess()
logging.debug('Recorded {} successfully'.format(filename))
self.test_get_visqol_score(test_file, test_profile,
'recorded_by_dut')
def hfp_dut_as_source(self, device, test_profile):
"""Test Case: HFP sinewave streaming from the DUT to peer device.
@param device: the Bluetooth peer device.
@param test_profile: which test profile is used, HFP_WBS or HFP_NBS.
"""
hfp_test_data = audio_test_data[test_profile]
self.test_select_audio_input_device(device.name)
self.test_select_audio_output_node_bluetooth()
# Enable HFP profile.
self.test_dut_to_start_capturing_audio_subprocess(
hfp_test_data, 'recorded_by_peer')
# Wait for pulseaudio bluez hfp source/sink
self.test_hfp_connected(self._get_pulseaudio_bluez_source_hfp, device,
test_profile)
self.test_device_to_start_recording_audio_subprocess(
device, test_profile, hfp_test_data)
self.test_dut_to_start_playing_audio_subprocess(hfp_test_data)
time.sleep(hfp_test_data['chunk_checking_duration'])
self.test_dut_to_stop_playing_audio_subprocess()
self.test_device_to_stop_recording_audio_subprocess(device)
self.test_check_audio_file(device, test_profile, hfp_test_data,
'recorded_by_peer')
# Disable HFP profile.
self.test_dut_to_stop_capturing_audio_subprocess()
def hfp_dut_as_sink(self, device, test_profile):
"""Test Case: HFP sinewave streaming from peer device to the DUT.
@param device: the Bluetooth peer device.
@param test_profile: which test profile is used, HFP_WBS or HFP_NBS.
"""
hfp_test_data = audio_test_data[test_profile]
self.test_select_audio_input_device(device.name)
self.test_select_audio_output_node_bluetooth()
# Enable HFP profile.
self.test_dut_to_start_capturing_audio_subprocess(
hfp_test_data, 'recorded_by_dut')
# Wait for pulseaudio bluez hfp source/sink
self.test_hfp_connected(self._get_pulseaudio_bluez_sink_hfp, device,
test_profile)
self.test_select_audio_input_device(device.name)
self.test_device_to_start_playing_audio_subprocess(
device, test_profile, hfp_test_data)
time.sleep(hfp_test_data['chunk_checking_duration'])
self.test_device_to_stop_playing_audio_subprocess(device)
# Disable HFP profile.
self.test_dut_to_stop_capturing_audio_subprocess()
self.test_check_audio_file(device, test_profile, hfp_test_data,
'recorded_by_dut')
def hfp_dut_as_source_back2back(self, device, test_profile):
"""Play and stop the audio stream from DUT to Bluetooth peer device.
The test starts then stops the stream playback for three times. In each
iteration, it checks the Bluetooth device can successfully receive the
stream when it is played; also check the absence of the streama when
stop playing.
@param device: the Bluetooth peer device.
@param test_profile: which test profile is used, HFP_WBS or HFP_NBS.
"""
hfp_test_data = audio_test_data[test_profile]
# Select audio input device.
self.test_select_audio_input_device(device.name)
# Select audio output node so that we do not rely on chrome to do it.
self.test_select_audio_output_node_bluetooth()
# Enable HFP profile.
self.test_dut_to_start_capturing_audio_subprocess(hfp_test_data,
'recorded_by_peer')
# Wait for pulseaudio bluez hfp source/sink
self.test_hfp_connected(
self._get_pulseaudio_bluez_source_hfp, device, test_profile)
for _ in range(3):
# TODO(b/208165757): If we record the audio stream before playing
# that will cause an audio blank about 1~2 sec in the beginning of
# the recorded file and make the chunks checking fail. Need to fix
# this problem in the future.
self.test_dut_to_start_playing_audio_subprocess(hfp_test_data)
self.test_device_to_start_recording_audio_subprocess(
device, test_profile, hfp_test_data)
time.sleep(hfp_test_data['chunk_checking_duration'])
self.test_dut_to_stop_playing_audio_subprocess()
self.test_device_to_stop_recording_audio_subprocess(device)
self.test_check_audio_file(device, test_profile, hfp_test_data,
'recorded_by_peer')
self.test_device_to_start_recording_audio_subprocess(
device, test_profile, hfp_test_data)
time.sleep(hfp_test_data['chunk_checking_duration'])
self.test_device_to_stop_recording_audio_subprocess(device)
self.test_check_audio_file(device, test_profile, hfp_test_data,
recording_device='recorded_by_peer',
check_frequencies=False)
# Disable HFP profile.
self.test_dut_to_stop_capturing_audio_subprocess()
def a2dp_to_hfp_dut_as_source(self, device, test_profile):
"""Play the audio from DUT to Bluetooth device and switch the profile.
This test first uses A2DP profile and plays the audio stream on the
DUT, checking if the peer receives the audio stream correctly. And
then switch to the HFP_NBS profile and check the audio stream again.
@param device: the Bluetooth peer device.
@param test_profile: which test profile is used, HFP_WBS_MEDIUM or
HFP_NBS_MEDIUM.
"""
hfp_test_data = audio_test_data[test_profile]
# Wait for pulseaudio a2dp bluez source.
self.test_device_a2dp_connected(device)
# Select audio output node so that we do not rely on chrome to do it.
self.test_select_audio_output_node_bluetooth()
self.test_device_to_start_recording_audio_subprocess(
device, test_profile, hfp_test_data)
# Play audio on the DUT in a non-blocked way and check the recorded
# audio stream in a real-time manner.
self.test_dut_to_start_playing_audio_subprocess(hfp_test_data)
time.sleep(hfp_test_data['chunk_checking_duration'])
self.test_device_to_stop_recording_audio_subprocess(device)
self.test_check_audio_file(device, test_profile, hfp_test_data,
'recorded_by_peer')
self.test_select_audio_input_device(device.name)
# Enable HFP profile.
self.test_dut_to_start_capturing_audio_subprocess(hfp_test_data,
'recorded_by_peer')
# Wait for pulseaudio bluez hfp source/sink.
self.test_hfp_connected(
self._get_pulseaudio_bluez_source_hfp, device, test_profile)
self.test_device_to_start_recording_audio_subprocess(
device, test_profile, hfp_test_data)
time.sleep(hfp_test_data['chunk_checking_duration'])
self.test_dut_to_stop_playing_audio_subprocess()
self.test_device_to_stop_recording_audio_subprocess(device)
self.test_check_audio_file(device, test_profile, hfp_test_data,
'recorded_by_peer')
# Disable HFP profile.
self.test_dut_to_stop_capturing_audio_subprocess()
def hfp_to_a2dp_dut_as_source(self, device, test_profile):
"""Play the audio from DUT to Bluetooth peer in A2DP then switch to HFP.
This test first uses HFP profile and plays the audio stream on the DUT,
checking if the peer receives the audio stream correctly. And then
switch to the A2DP profile and check the audio stream again.
@param device: the Bluetooth peer device.
@param test_profile: which test profile is used,
HFP_NBS_MEDIUM or HFP_WBS_MEDIUM.
"""
hfp_test_data = audio_test_data[test_profile]
self.test_select_audio_input_device(device.name)
# Select audio output node so that we do not rely on chrome to do it.
self.test_select_audio_output_node_bluetooth()
# Enable HFP profile.
self.test_dut_to_start_capturing_audio_subprocess(hfp_test_data,
'recorded_by_peer')
# Wait for pulseaudio bluez hfp source/sink.
self.test_hfp_connected(
self._get_pulseaudio_bluez_source_hfp, device, test_profile)
# Play audio on the DUT in a non-blocked way and check the recorded
# audio stream in a real-time manner.
self.test_dut_to_start_playing_audio_subprocess(hfp_test_data)
self.test_device_to_start_recording_audio_subprocess(
device, test_profile, hfp_test_data)
time.sleep(hfp_test_data['chunk_checking_duration'])
self.test_device_to_stop_recording_audio_subprocess(device)
self.test_check_audio_file(device, test_profile, hfp_test_data,
'recorded_by_peer')
# Disable HFP profile.
self.test_dut_to_stop_capturing_audio_subprocess()
# Wait for pulseaudio a2dp bluez source.
self.test_device_a2dp_connected(device)
self.test_device_to_start_recording_audio_subprocess(
device, test_profile, hfp_test_data)
time.sleep(hfp_test_data['chunk_checking_duration'])
self.test_dut_to_stop_playing_audio_subprocess()
self.test_check_audio_file(device, test_profile, hfp_test_data,
'recorded_by_peer')
self.test_device_to_stop_recording_audio_subprocess(device)