973 lines
30 KiB
Python
973 lines
30 KiB
Python
# Lint as: python2, python3
|
|
# Copyright (c) 2013 The Chromium Authors. All rights reserved.
|
|
# Use of this source code is governed by a BSD-style license that can be
|
|
# found in the LICENSE file.
|
|
|
|
"""This module provides cras audio utilities."""
|
|
|
|
import logging
|
|
import re
|
|
import subprocess
|
|
|
|
from autotest_lib.client.bin import utils
|
|
from autotest_lib.client.cros.audio import cmd_utils
|
|
|
|
_CRAS_TEST_CLIENT = '/usr/bin/cras_test_client'
|
|
|
|
|
|
class CrasUtilsError(Exception):
|
|
"""Error in CrasUtils."""
|
|
pass
|
|
|
|
|
|
def playback(blocking=True, stdin=None, *args, **kargs):
|
|
"""A helper function to execute the playback_cmd.
|
|
|
|
@param blocking: Blocks this call until playback finishes.
|
|
@param stdin: the standard input of playback process
|
|
@param args: args passed to playback_cmd.
|
|
@param kargs: kargs passed to playback_cmd.
|
|
|
|
@returns: The process running the playback command. Note that if the
|
|
blocking parameter is true, this will return a finished process.
|
|
"""
|
|
process = cmd_utils.popen(playback_cmd(*args, **kargs), stdin=stdin)
|
|
if blocking:
|
|
cmd_utils.wait_and_check_returncode(process)
|
|
return process
|
|
|
|
|
|
def capture(*args, **kargs):
|
|
"""A helper function to execute the capture_cmd.
|
|
|
|
@param args: args passed to capture_cmd.
|
|
@param kargs: kargs passed to capture_cmd.
|
|
|
|
"""
|
|
cmd_utils.execute(capture_cmd(*args, **kargs))
|
|
|
|
|
|
def playback_cmd(playback_file, block_size=None, duration=None,
|
|
pin_device=None, channels=2, rate=48000):
|
|
"""Gets a command to playback a file with given settings.
|
|
|
|
@param playback_file: the name of the file to play. '-' indicates to
|
|
playback raw audio from the stdin.
|
|
@param pin_device: the device id to playback on.
|
|
@param block_size: the number of frames per callback(dictates latency).
|
|
@param duration: seconds to playback.
|
|
@param channels: number of channels.
|
|
@param rate: the sampling rate.
|
|
|
|
@returns: The command args put in a list of strings.
|
|
|
|
"""
|
|
args = [_CRAS_TEST_CLIENT]
|
|
args += ['--playback_file', playback_file]
|
|
if pin_device is not None:
|
|
args += ['--pin_device', str(pin_device)]
|
|
if block_size is not None:
|
|
args += ['--block_size', str(block_size)]
|
|
if duration is not None:
|
|
args += ['--duration', str(duration)]
|
|
args += ['--num_channels', str(channels)]
|
|
args += ['--rate', str(rate)]
|
|
return args
|
|
|
|
|
|
def capture_cmd(capture_file, block_size=None, duration=10,
|
|
sample_format='S16_LE',
|
|
pin_device=None, channels=1, rate=48000):
|
|
"""Gets a command to capture the audio into the file with given settings.
|
|
|
|
@param capture_file: the name of file the audio to be stored in.
|
|
@param block_size: the number of frames per callback(dictates latency).
|
|
@param duration: seconds to record. If it is None, duration is not set,
|
|
and command will keep capturing audio until it is
|
|
terminated.
|
|
@param sample_format: the sample format;
|
|
possible choices: 'S16_LE', 'S24_LE', and 'S32_LE'
|
|
default to S16_LE: signed 16 bits/sample,
|
|
little endian
|
|
@param pin_device: the device id to record from.
|
|
@param channels: number of channels.
|
|
@param rate: the sampling rate.
|
|
|
|
@returns: The command args put in a list of strings.
|
|
|
|
"""
|
|
args = [_CRAS_TEST_CLIENT]
|
|
args += ['--capture_file', capture_file]
|
|
if pin_device is not None:
|
|
args += ['--pin_device', str(pin_device)]
|
|
if block_size is not None:
|
|
args += ['--block_size', str(block_size)]
|
|
if duration is not None:
|
|
args += ['--duration', str(duration)]
|
|
args += ['--num_channels', str(channels)]
|
|
args += ['--rate', str(rate)]
|
|
args += ['--format', str(sample_format)]
|
|
return args
|
|
|
|
|
|
def listen_cmd(
|
|
capture_file, block_size=None, duration=10, channels=1, rate=48000):
|
|
"""Gets a command to listen on hotword and record audio into the file with
|
|
given settings.
|
|
|
|
@param capture_file: the name of file the audio to be stored in.
|
|
@param block_size: the number of frames per callback(dictates latency).
|
|
@param duration: seconds to record. If it is None, duration is not set,
|
|
and command will keep capturing audio until it is
|
|
terminated.
|
|
@param channels: number of channels.
|
|
@param rate: the sampling rate.
|
|
|
|
@returns: The command args put in a list of strings.
|
|
|
|
"""
|
|
args = [_CRAS_TEST_CLIENT]
|
|
args += ['--listen_for_hotword', capture_file]
|
|
if block_size is not None:
|
|
args += ['--block_size', str(block_size)]
|
|
if duration is not None:
|
|
args += ['--duration', str(duration)]
|
|
args += ['--num_channels', str(channels)]
|
|
args += ['--rate', str(rate)]
|
|
return args
|
|
|
|
|
|
def loopback(*args, **kargs):
|
|
"""A helper function to execute loopback_cmd.
|
|
|
|
@param args: args passed to loopback_cmd.
|
|
@param kargs: kargs passed to loopback_cmd.
|
|
|
|
"""
|
|
|
|
cmd_utils.execute(loopback_cmd(*args, **kargs))
|
|
|
|
|
|
def loopback_cmd(output_file, duration=10, channels=2, rate=48000):
|
|
"""Gets a command to record the loopback.
|
|
|
|
@param output_file: The name of the file the loopback to be stored in.
|
|
@param channels: The number of channels of the recorded audio.
|
|
@param duration: seconds to record.
|
|
@param rate: the sampling rate.
|
|
|
|
@returns: The command args put in a list of strings.
|
|
|
|
"""
|
|
args = [_CRAS_TEST_CLIENT]
|
|
args += ['--loopback_file', output_file]
|
|
args += ['--duration_seconds', str(duration)]
|
|
args += ['--num_channels', str(channels)]
|
|
args += ['--rate', str(rate)]
|
|
return args
|
|
|
|
|
|
def get_cras_nodes_cmd():
|
|
"""Gets a command to query the nodes from Cras.
|
|
|
|
@returns: The command to query nodes information from Cras using dbus-send.
|
|
|
|
"""
|
|
return ('dbus-send --system --type=method_call --print-reply '
|
|
'--dest=org.chromium.cras /org/chromium/cras '
|
|
'org.chromium.cras.Control.GetNodes')
|
|
|
|
|
|
def set_system_volume(volume):
|
|
"""Set the system volume.
|
|
|
|
@param volume: the system output vlume to be set(0 - 100).
|
|
|
|
"""
|
|
get_cras_control_interface().SetOutputVolume(volume)
|
|
|
|
|
|
def set_node_volume(node_id, volume):
|
|
"""Set the volume of the given output node.
|
|
|
|
@param node_id: the id of the output node to be set the volume.
|
|
@param volume: the volume to be set(0-100).
|
|
|
|
"""
|
|
get_cras_control_interface().SetOutputNodeVolume(node_id, volume)
|
|
|
|
|
|
def get_cras_control_interface(private=False):
|
|
"""Gets Cras DBus control interface.
|
|
|
|
@param private: Set to True to use a new instance for dbus.SystemBus
|
|
instead of the shared instance.
|
|
|
|
@returns: A dBus.Interface object with Cras Control interface.
|
|
|
|
@raises: ImportError if this is not called on Cros device.
|
|
|
|
"""
|
|
try:
|
|
import dbus
|
|
except ImportError as e:
|
|
logging.exception(
|
|
'Can not import dbus: %s. This method should only be '
|
|
'called on Cros device.', e)
|
|
raise
|
|
bus = dbus.SystemBus(private=private)
|
|
cras_object = bus.get_object('org.chromium.cras', '/org/chromium/cras')
|
|
return dbus.Interface(cras_object, 'org.chromium.cras.Control')
|
|
|
|
|
|
def get_cras_nodes():
|
|
"""Gets nodes information from Cras.
|
|
|
|
@returns: A dict containing information of each node.
|
|
|
|
"""
|
|
return get_cras_control_interface().GetNodes()
|
|
|
|
|
|
def get_selected_nodes():
|
|
"""Gets selected output nodes and input nodes.
|
|
|
|
@returns: A tuple (output_nodes, input_nodes) where each
|
|
field is a list of selected node IDs returned from Cras DBus API.
|
|
Note that there may be multiple output/input nodes being selected
|
|
at the same time.
|
|
|
|
"""
|
|
output_nodes = []
|
|
input_nodes = []
|
|
nodes = get_cras_nodes()
|
|
for node in nodes:
|
|
if node['Active']:
|
|
if node['IsInput']:
|
|
input_nodes.append(node['Id'])
|
|
else:
|
|
output_nodes.append(node['Id'])
|
|
return (output_nodes, input_nodes)
|
|
|
|
|
|
def set_selected_output_node_volume(volume):
|
|
"""Sets the selected output node volume.
|
|
|
|
@param volume: the volume to be set (0-100).
|
|
|
|
"""
|
|
selected_output_node_ids, _ = get_selected_nodes()
|
|
for node_id in selected_output_node_ids:
|
|
set_node_volume(node_id, volume)
|
|
|
|
|
|
def get_active_stream_count():
|
|
"""Gets the number of active streams.
|
|
|
|
@returns: The number of active streams.
|
|
|
|
"""
|
|
return int(get_cras_control_interface().GetNumberOfActiveStreams())
|
|
|
|
|
|
def set_system_mute(is_mute):
|
|
"""Sets the system mute switch.
|
|
|
|
@param is_mute: Set True to mute the system playback.
|
|
|
|
"""
|
|
get_cras_control_interface().SetOutputMute(is_mute)
|
|
|
|
|
|
def set_capture_mute(is_mute):
|
|
"""Sets the capture mute switch.
|
|
|
|
@param is_mute: Set True to mute the capture.
|
|
|
|
"""
|
|
get_cras_control_interface().SetInputMute(is_mute)
|
|
|
|
|
|
def node_type_is_plugged(node_type, nodes_info):
|
|
"""Determine if there is any node of node_type plugged.
|
|
|
|
This method is used in the AudioLoopbackDongleLabel class, where the
|
|
call is executed on autotest server. Use get_cras_nodes instead if
|
|
the call can be executed on Cros device.
|
|
|
|
Since Cras only reports the plugged node in GetNodes, we can
|
|
parse the return value to see if there is any node with the given type.
|
|
For example, if INTERNAL_MIC is of intereset, the pattern we are
|
|
looking for is:
|
|
|
|
dict entry(
|
|
string "Type"
|
|
variant string "INTERNAL_MIC"
|
|
)
|
|
|
|
@param node_type: A str representing node type defined in CRAS_NODE_TYPES.
|
|
@param nodes_info: A str containing output of command get_nodes_cmd.
|
|
|
|
@returns: True if there is any node of node_type plugged. False otherwise.
|
|
|
|
"""
|
|
match = re.search(r'string "Type"\s+variant\s+string "%s"' % node_type,
|
|
nodes_info)
|
|
return True if match else False
|
|
|
|
|
|
# Cras node types reported from Cras DBus control API.
|
|
CRAS_OUTPUT_NODE_TYPES = ['HEADPHONE', 'INTERNAL_SPEAKER', 'HDMI', 'USB',
|
|
'BLUETOOTH', 'LINEOUT', 'UNKNOWN', 'ALSA_LOOPBACK']
|
|
CRAS_INPUT_NODE_TYPES = ['MIC', 'INTERNAL_MIC', 'USB', 'BLUETOOTH',
|
|
'POST_DSP_LOOPBACK', 'POST_MIX_LOOPBACK', 'UNKNOWN',
|
|
'KEYBOARD_MIC', 'HOTWORD', 'FRONT_MIC', 'REAR_MIC',
|
|
'ECHO_REFERENCE']
|
|
CRAS_NODE_TYPES = CRAS_OUTPUT_NODE_TYPES + CRAS_INPUT_NODE_TYPES
|
|
|
|
|
|
def get_filtered_node_types(callback):
|
|
"""Returns the pair of filtered output node types and input node types.
|
|
|
|
@param callback: A callback function which takes a node as input parameter
|
|
and filter the node based on its return value.
|
|
|
|
@returns: A tuple (output_node_types, input_node_types) where each
|
|
field is a list of node types defined in CRAS_NODE_TYPES,
|
|
and their 'attribute_name' is True.
|
|
|
|
"""
|
|
output_node_types = []
|
|
input_node_types = []
|
|
nodes = get_cras_nodes()
|
|
for node in nodes:
|
|
if callback(node):
|
|
node_type = str(node['Type'])
|
|
if node_type not in CRAS_NODE_TYPES:
|
|
logging.warning('node type %s is not in known CRAS_NODE_TYPES',
|
|
node_type)
|
|
if node['IsInput']:
|
|
input_node_types.append(node_type)
|
|
else:
|
|
output_node_types.append(node_type)
|
|
return (output_node_types, input_node_types)
|
|
|
|
|
|
def get_selected_node_types():
|
|
"""Returns the pair of active output node types and input node types.
|
|
|
|
@returns: A tuple (output_node_types, input_node_types) where each
|
|
field is a list of selected node types defined in CRAS_NODE_TYPES.
|
|
|
|
"""
|
|
def is_selected(node):
|
|
"""Checks if a node is selected.
|
|
|
|
A node is selected if its Active attribute is True.
|
|
|
|
@returns: True is a node is selected, False otherwise.
|
|
|
|
"""
|
|
return node['Active']
|
|
|
|
return get_filtered_node_types(is_selected)
|
|
|
|
|
|
def get_selected_input_device_name():
|
|
"""Returns the device name of the active input node.
|
|
|
|
@returns: device name string. E.g. kbl_r5514_5663_max: :0,1
|
|
"""
|
|
nodes = get_cras_nodes()
|
|
for node in nodes:
|
|
if node['Active'] and node['IsInput']:
|
|
return node['DeviceName']
|
|
return None
|
|
|
|
|
|
def get_selected_input_device_type():
|
|
"""Returns the device type of the active input node.
|
|
|
|
@returns: device type string. E.g. INTERNAL_MICROPHONE
|
|
"""
|
|
nodes = get_cras_nodes()
|
|
for node in nodes:
|
|
if node['Active'] and node['IsInput']:
|
|
return node['Type']
|
|
return None
|
|
|
|
|
|
def get_selected_output_device_name():
|
|
"""Returns the device name of the active output node.
|
|
|
|
@returns: device name string. E.g. mtk-rt5650: :0,0
|
|
"""
|
|
nodes = get_cras_nodes()
|
|
for node in nodes:
|
|
if node['Active'] and not node['IsInput']:
|
|
return node['DeviceName']
|
|
return None
|
|
|
|
|
|
def get_selected_output_device_type():
|
|
"""Returns the device type of the active output node.
|
|
|
|
@returns: device type string. E.g. INTERNAL_SPEAKER
|
|
"""
|
|
nodes = get_cras_nodes()
|
|
for node in nodes:
|
|
if node['Active'] and not node['IsInput']:
|
|
return node['Type']
|
|
return None
|
|
|
|
|
|
def get_plugged_node_types():
|
|
"""Returns the pair of plugged output node types and input node types.
|
|
|
|
@returns: A tuple (output_node_types, input_node_types) where each
|
|
field is a list of plugged node types defined in CRAS_NODE_TYPES.
|
|
|
|
"""
|
|
def is_plugged(node):
|
|
"""Checks if a node is plugged and is not unknown node.
|
|
|
|
Cras DBus API only reports plugged node, so every node reported by Cras
|
|
DBus API is plugged. However, we filter out UNKNOWN node here because
|
|
the existence of unknown node depends on the number of redundant
|
|
playback/record audio device created on audio card. Also, the user of
|
|
Cras will ignore unknown nodes.
|
|
|
|
@returns: True if a node is plugged and is not an UNKNOWN node.
|
|
|
|
"""
|
|
return node['Type'] != 'UNKNOWN'
|
|
|
|
return get_filtered_node_types(is_plugged)
|
|
|
|
|
|
def set_selected_node_types(output_node_types, input_node_types):
|
|
"""Sets selected node types.
|
|
|
|
@param output_node_types: A list of output node types. None to skip setting.
|
|
@param input_node_types: A list of input node types. None to skip setting.
|
|
|
|
"""
|
|
if output_node_types is not None and len(output_node_types) == 1:
|
|
set_single_selected_output_node(output_node_types[0])
|
|
elif output_node_types:
|
|
set_selected_output_nodes(output_node_types)
|
|
if input_node_types is not None and len(input_node_types) == 1:
|
|
set_single_selected_input_node(input_node_types[0])
|
|
elif input_node_types:
|
|
set_selected_input_nodes(input_node_types)
|
|
|
|
|
|
def set_single_selected_output_node(node_type):
|
|
"""Sets one selected output node.
|
|
|
|
Note that Chrome UI uses SetActiveOutputNode of Cras DBus API
|
|
to select one output node.
|
|
|
|
@param node_type: A node type.
|
|
|
|
@returns: True if the output node type is found and set active.
|
|
"""
|
|
nodes = get_cras_nodes()
|
|
for node in nodes:
|
|
if node['IsInput']:
|
|
continue
|
|
if node['Type'] == node_type:
|
|
set_active_output_node(node['Id'])
|
|
return True
|
|
return False
|
|
|
|
|
|
def set_single_selected_input_node(node_type):
|
|
"""Sets one selected input node.
|
|
|
|
Note that Chrome UI uses SetActiveInputNode of Cras DBus API
|
|
to select one input node.
|
|
|
|
@param node_type: A node type.
|
|
|
|
@returns: True if the input node type is found and set active.
|
|
"""
|
|
nodes = get_cras_nodes()
|
|
for node in nodes:
|
|
if not node['IsInput']:
|
|
continue
|
|
if node['Type'] == node_type:
|
|
set_active_input_node(node['Id'])
|
|
return True
|
|
return False
|
|
|
|
|
|
def set_selected_output_nodes(types):
|
|
"""Sets selected output node types.
|
|
|
|
Note that Chrome UI uses SetActiveOutputNode of Cras DBus API
|
|
to select one output node. Here we use add/remove active output node
|
|
to support multiple nodes.
|
|
|
|
@param types: A list of output node types.
|
|
|
|
"""
|
|
nodes = get_cras_nodes()
|
|
for node in nodes:
|
|
if node['IsInput']:
|
|
continue
|
|
if node['Type'] in types:
|
|
add_active_output_node(node['Id'])
|
|
elif node['Active']:
|
|
remove_active_output_node(node['Id'])
|
|
|
|
|
|
def set_selected_input_nodes(types):
|
|
"""Sets selected input node types.
|
|
|
|
Note that Chrome UI uses SetActiveInputNode of Cras DBus API
|
|
to select one input node. Here we use add/remove active input node
|
|
to support multiple nodes.
|
|
|
|
@param types: A list of input node types.
|
|
|
|
"""
|
|
nodes = get_cras_nodes()
|
|
for node in nodes:
|
|
if not node['IsInput']:
|
|
continue
|
|
if node['Type'] in types:
|
|
add_active_input_node(node['Id'])
|
|
elif node['Active']:
|
|
remove_active_input_node(node['Id'])
|
|
|
|
|
|
def set_active_input_node(node_id):
|
|
"""Sets one active input node.
|
|
|
|
@param node_id: node id.
|
|
|
|
"""
|
|
get_cras_control_interface().SetActiveInputNode(node_id)
|
|
|
|
|
|
def set_active_output_node(node_id):
|
|
"""Sets one active output node.
|
|
|
|
@param node_id: node id.
|
|
|
|
"""
|
|
get_cras_control_interface().SetActiveOutputNode(node_id)
|
|
|
|
|
|
def add_active_output_node(node_id):
|
|
"""Adds an active output node.
|
|
|
|
@param node_id: node id.
|
|
|
|
"""
|
|
get_cras_control_interface().AddActiveOutputNode(node_id)
|
|
|
|
|
|
def add_active_input_node(node_id):
|
|
"""Adds an active input node.
|
|
|
|
@param node_id: node id.
|
|
|
|
"""
|
|
get_cras_control_interface().AddActiveInputNode(node_id)
|
|
|
|
|
|
def remove_active_output_node(node_id):
|
|
"""Removes an active output node.
|
|
|
|
@param node_id: node id.
|
|
|
|
"""
|
|
get_cras_control_interface().RemoveActiveOutputNode(node_id)
|
|
|
|
|
|
def remove_active_input_node(node_id):
|
|
"""Removes an active input node.
|
|
|
|
@param node_id: node id.
|
|
|
|
"""
|
|
get_cras_control_interface().RemoveActiveInputNode(node_id)
|
|
|
|
|
|
def get_node_id_from_node_type(node_type, is_input):
|
|
"""Gets node id from node type.
|
|
|
|
@param types: A node type defined in CRAS_NODE_TYPES.
|
|
@param is_input: True if the node is input. False otherwise.
|
|
|
|
@returns: A string for node id.
|
|
|
|
@raises: CrasUtilsError: if unique node id can not be found.
|
|
|
|
"""
|
|
nodes = get_cras_nodes()
|
|
find_ids = []
|
|
for node in nodes:
|
|
if node['Type'] == node_type and node['IsInput'] == is_input:
|
|
find_ids.append(node['Id'])
|
|
if len(find_ids) != 1:
|
|
raise CrasUtilsError(
|
|
'Can not find unique node id from node type %s' % node_type)
|
|
return find_ids[0]
|
|
|
|
|
|
def get_device_id_of(node_id):
|
|
"""Gets the device id of the node id.
|
|
|
|
The conversion logic is replicated from the CRAS's type definition at
|
|
third_party/adhd/cras/src/common/cras_types.h.
|
|
|
|
@param node_id: A string for node id.
|
|
|
|
@returns: A string for device id.
|
|
|
|
@raise: CrasUtilsError: if device id is invalid.
|
|
"""
|
|
device_id = str(int(node_id) >> 32)
|
|
if device_id == "0":
|
|
raise CrasUtilsError('Got invalid device_id: 0')
|
|
return device_id
|
|
|
|
|
|
def get_device_id_from_node_type(node_type, is_input):
|
|
"""Gets device id from node type.
|
|
|
|
@param types: A node type defined in CRAS_NODE_TYPES.
|
|
@param is_input: True if the node is input. False otherwise.
|
|
|
|
@returns: A string for device id.
|
|
|
|
"""
|
|
node_id = get_node_id_from_node_type(node_type, is_input)
|
|
return get_device_id_of(node_id)
|
|
|
|
|
|
def get_active_node_volume():
|
|
"""Returns volume from active node.
|
|
|
|
@returns: int for volume
|
|
|
|
@raises: CrasUtilsError: if node volume cannot be found.
|
|
"""
|
|
nodes = get_cras_nodes()
|
|
for node in nodes:
|
|
if node['Active'] == 1 and node['IsInput'] == 0:
|
|
return int(node['NodeVolume'])
|
|
raise CrasUtilsError('Cannot find active node volume from nodes.')
|
|
|
|
|
|
def get_active_output_node_max_supported_channels():
|
|
"""Returns max supported channels from active output node.
|
|
|
|
@returns: int for max supported channels.
|
|
|
|
@raises: CrasUtilsError: if node cannot be found.
|
|
"""
|
|
nodes = get_cras_nodes()
|
|
for node in nodes:
|
|
if node['Active'] == 1 and node['IsInput'] == 0:
|
|
return int(node['MaxSupportedChannels'])
|
|
raise CrasUtilsError('Cannot find active output node.')
|
|
|
|
|
|
class CrasTestClient(object):
|
|
"""An object to perform cras_test_client functions."""
|
|
|
|
BLOCK_SIZE = None
|
|
PIN_DEVICE = None
|
|
SAMPLE_FORMAT = 'S16_LE'
|
|
DURATION = 10
|
|
CHANNELS = 2
|
|
RATE = 48000
|
|
|
|
|
|
def __init__(self):
|
|
self._proc = None
|
|
self._capturing_proc = None
|
|
self._playing_proc = None
|
|
self._capturing_msg = 'capturing audio file'
|
|
self._playing_msg = 'playing audio file'
|
|
self._wbs_cmd = '%s --set_wbs_enabled ' % _CRAS_TEST_CLIENT
|
|
self._enable_wbs_cmd = ('%s 1' % self._wbs_cmd).split()
|
|
self._disable_wbs_cmd = ('%s 0' % self._wbs_cmd).split()
|
|
self._info_cmd = [_CRAS_TEST_CLIENT,]
|
|
self._select_input_cmd = '%s --select_input ' % _CRAS_TEST_CLIENT
|
|
|
|
|
|
def start_subprocess(self, proc, proc_cmd, filename, proc_msg):
|
|
"""Start a capture or play subprocess
|
|
|
|
@param proc: the process
|
|
@param proc_cmd: the process command and its arguments
|
|
@param filename: the file name to capture or play
|
|
@param proc_msg: the message to display in logging
|
|
|
|
@returns: True if the process is started successfully
|
|
"""
|
|
if proc is None:
|
|
try:
|
|
self._proc = subprocess.Popen(proc_cmd)
|
|
logging.debug('Start %s %s on the DUT', proc_msg, filename)
|
|
except Exception as e:
|
|
logging.error('Failed to popen: %s (%s)', proc_msg, e)
|
|
return False
|
|
else:
|
|
logging.error('cannot run the command twice: %s', proc_msg)
|
|
return False
|
|
return True
|
|
|
|
|
|
def stop_subprocess(self, proc, proc_msg):
|
|
"""Stop a subprocess
|
|
|
|
@param proc: the process to stop
|
|
@param proc_msg: the message to display in logging
|
|
|
|
@returns: True if the process is stopped successfully
|
|
"""
|
|
if proc is None:
|
|
logging.error('cannot run stop %s before starting it.', proc_msg)
|
|
return False
|
|
|
|
proc.terminate()
|
|
try:
|
|
utils.poll_for_condition(
|
|
condition=lambda: proc.poll() is not None,
|
|
exception=CrasUtilsError,
|
|
timeout=10,
|
|
sleep_interval=0.5,
|
|
desc='Waiting for subprocess to terminate')
|
|
except Exception:
|
|
logging.warn('Killing subprocess due to timeout')
|
|
proc.kill()
|
|
proc.wait()
|
|
|
|
logging.debug('stop %s on the DUT', proc_msg)
|
|
return True
|
|
|
|
|
|
def start_capturing_subprocess(self, capture_file, block_size=BLOCK_SIZE,
|
|
duration=DURATION, pin_device=PIN_DEVICE,
|
|
sample_format=SAMPLE_FORMAT,
|
|
channels=CHANNELS, rate=RATE):
|
|
"""Start capturing in a subprocess.
|
|
|
|
@param capture_file: the name of file the audio to be stored in
|
|
@param block_size: the number of frames per callback(dictates latency)
|
|
@param duration: seconds to record. If it is None, duration is not set,
|
|
and will keep capturing audio until terminated
|
|
@param sample_format: the sample format
|
|
@param pin_device: the device id to record from
|
|
@param channels: number of channels
|
|
@param rate: the sampling rate
|
|
|
|
@returns: True if the process is started successfully
|
|
"""
|
|
proc_cmd = capture_cmd(capture_file, block_size=block_size,
|
|
duration=duration, sample_format=sample_format,
|
|
pin_device=pin_device, channels=channels,
|
|
rate=rate)
|
|
result = self.start_subprocess(self._capturing_proc, proc_cmd,
|
|
capture_file, self._capturing_msg)
|
|
if result:
|
|
self._capturing_proc = self._proc
|
|
return result
|
|
|
|
|
|
def stop_capturing_subprocess(self):
|
|
"""Stop the capturing subprocess."""
|
|
result = self.stop_subprocess(self._capturing_proc, self._capturing_msg)
|
|
if result:
|
|
self._capturing_proc = None
|
|
return result
|
|
|
|
|
|
def start_playing_subprocess(self, audio_file, block_size=BLOCK_SIZE,
|
|
duration=DURATION, pin_device=PIN_DEVICE,
|
|
channels=CHANNELS, rate=RATE):
|
|
"""Start playing the audio file in a subprocess.
|
|
|
|
@param audio_file: the name of audio file to play
|
|
@param block_size: the number of frames per callback(dictates latency)
|
|
@param duration: seconds to play. If it is None, duration is not set,
|
|
and will keep playing audio until terminated
|
|
@param pin_device: the device id to play to
|
|
@param channels: number of channels
|
|
@param rate: the sampling rate
|
|
|
|
@returns: True if the process is started successfully
|
|
"""
|
|
proc_cmd = playback_cmd(audio_file, block_size, duration, pin_device,
|
|
channels, rate)
|
|
result = self.start_subprocess(self._playing_proc, proc_cmd,
|
|
audio_file, self._playing_msg)
|
|
if result:
|
|
self._playing_proc = self._proc
|
|
return result
|
|
|
|
|
|
def stop_playing_subprocess(self):
|
|
"""Stop the playing subprocess."""
|
|
result = self.stop_subprocess(self._playing_proc, self._playing_msg)
|
|
if result:
|
|
self._playing_proc = None
|
|
return result
|
|
|
|
|
|
def play(self, audio_file, block_size=BLOCK_SIZE, duration=DURATION,
|
|
pin_device=PIN_DEVICE, channels=CHANNELS, rate=RATE):
|
|
"""Play the audio file.
|
|
|
|
This method will get blocked until it has completed playing back.
|
|
If you do not want to get blocked, use start_playing_subprocess()
|
|
above instead.
|
|
|
|
@param audio_file: the name of audio file to play
|
|
@param block_size: the number of frames per callback(dictates latency)
|
|
@param duration: seconds to play. If it is None, duration is not set,
|
|
and will keep playing audio until terminated
|
|
@param pin_device: the device id to play to
|
|
@param channels: number of channels
|
|
@param rate: the sampling rate
|
|
|
|
@returns: True if the process is started successfully
|
|
"""
|
|
proc_cmd = playback_cmd(audio_file, block_size, duration, pin_device,
|
|
channels, rate)
|
|
try:
|
|
self._proc = subprocess.call(proc_cmd)
|
|
logging.debug('call "%s" on the DUT', proc_cmd)
|
|
except Exception as e:
|
|
logging.error('Failed to call: %s (%s)', proc_cmd, e)
|
|
return False
|
|
return True
|
|
|
|
|
|
def enable_wbs(self, value):
|
|
"""Enable or disable wideband speech (wbs) per the value.
|
|
|
|
@param value: True to enable wbs.
|
|
|
|
@returns: True if the operation succeeds.
|
|
"""
|
|
cmd = self._enable_wbs_cmd if value else self._disable_wbs_cmd
|
|
logging.debug('call "%s" on the DUT', cmd)
|
|
if subprocess.call(cmd):
|
|
logging.error('Failed to call: %s (%s)', cmd)
|
|
return False
|
|
return True
|
|
|
|
|
|
def select_input_device(self, device_name):
|
|
"""Select the audio input device.
|
|
|
|
@param device_name: the name of the Bluetooth peer device
|
|
|
|
@returns: True if the operation succeeds.
|
|
"""
|
|
logging.debug('to select input device for device_name: %s', device_name)
|
|
try:
|
|
info = subprocess.check_output(self._info_cmd)
|
|
logging.debug('info: %s', info)
|
|
except Exception as e:
|
|
logging.error('Failed to call: %s (%s)', self._info_cmd, e)
|
|
return False
|
|
|
|
flag_input_nodes = False
|
|
audio_input_node = None
|
|
for line in info.decode().splitlines():
|
|
if 'Input Nodes' in line:
|
|
flag_input_nodes = True
|
|
elif 'Attached clients' in line:
|
|
flag_input_nodes = False
|
|
|
|
if flag_input_nodes:
|
|
if device_name in line:
|
|
audio_input_node = line.split()[1]
|
|
logging.debug('%s', audio_input_node)
|
|
break
|
|
|
|
if audio_input_node is None:
|
|
logging.error('Failed to find audio input node: %s', device_name)
|
|
return False
|
|
|
|
select_input_cmd = (self._select_input_cmd + audio_input_node).split()
|
|
if subprocess.call(select_input_cmd):
|
|
logging.error('Failed to call: %s (%s)', select_input_cmd, e)
|
|
return False
|
|
|
|
logging.debug('call "%s" on the DUT', select_input_cmd)
|
|
return True
|
|
|
|
|
|
def set_player_playback_status(self, status):
|
|
"""Set playback status for the registered media player.
|
|
|
|
@param status: playback status in string.
|
|
|
|
"""
|
|
try:
|
|
get_cras_control_interface().SetPlayerPlaybackStatus(status)
|
|
except Exception as e:
|
|
logging.error('Failed to set player playback status: %s', e)
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
def set_player_position(self, position):
|
|
"""Set media position for the registered media player.
|
|
|
|
@param position: position in micro seconds.
|
|
|
|
"""
|
|
try:
|
|
get_cras_control_interface().SetPlayerPosition(position)
|
|
except Exception as e:
|
|
logging.error('Failed to set player position: %s', e)
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
def set_player_metadata(self, metadata):
|
|
"""Set title, artist, and album for the registered media player.
|
|
|
|
@param metadata: dictionary of media metadata.
|
|
|
|
"""
|
|
try:
|
|
get_cras_control_interface().SetPlayerMetadata(metadata)
|
|
except Exception as e:
|
|
logging.error('Failed to set player metadata: %s', e)
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
def set_player_length(self, length):
|
|
"""Set metadata length for the registered media player.
|
|
|
|
Media length is a part of metadata information. However, without
|
|
specify its type to int64. dbus-python will guess the variant type to
|
|
be int32 by default. Separate it from the metadata function to help
|
|
prepare the data differently.
|
|
|
|
@param metadata: DBUS dictionary that contains a variant of int64.
|
|
|
|
"""
|
|
try:
|
|
get_cras_control_interface().SetPlayerMetadata(length)
|
|
except Exception as e:
|
|
logging.error('Failed to set player length: %s', e)
|
|
return False
|
|
|
|
return True
|