1# Copyright (c) 2013 The Chromium 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 cras audio utilities.""" 6 7import logging 8import re 9 10from autotest_lib.client.cros.audio import cmd_utils 11 12_CRAS_TEST_CLIENT = '/usr/bin/cras_test_client' 13 14 15class CrasUtilsError(Exception): 16 pass 17 18 19def playback(blocking=True, *args, **kargs): 20 """A helper function to execute the playback_cmd. 21 22 @param blocking: Blocks this call until playback finishes. 23 @param args: args passed to playback_cmd. 24 @param kargs: kargs passed to playback_cmd. 25 26 @returns: The process running the playback command. Note that if the 27 blocking parameter is true, this will return a finished process. 28 """ 29 process = cmd_utils.popen(playback_cmd(*args, **kargs)) 30 if blocking: 31 cmd_utils.wait_and_check_returncode(process) 32 return process 33 34 35def capture(*args, **kargs): 36 """A helper function to execute the capture_cmd. 37 38 @param args: args passed to capture_cmd. 39 @param kargs: kargs passed to capture_cmd. 40 41 """ 42 cmd_utils.execute(capture_cmd(*args, **kargs)) 43 44 45def playback_cmd(playback_file, block_size=None, duration=None, 46 channels=2, rate=48000): 47 """Gets a command to playback a file with given settings. 48 49 @param playback_file: the name of the file to play. '-' indicates to 50 playback raw audio from the stdin. 51 @param block_size: the number of frames per callback(dictates latency). 52 @param duration: seconds to playback 53 @param channels: number of channels. 54 @param rate: the sampling rate 55 56 @returns: The command args put in a list of strings. 57 58 """ 59 args = [_CRAS_TEST_CLIENT] 60 args += ['--playback_file', playback_file] 61 if block_size is not None: 62 args += ['--block_size', str(block_size)] 63 if duration is not None: 64 args += ['--duration', str(duration)] 65 args += ['--num_channels', str(channels)] 66 args += ['--rate', str(rate)] 67 return args 68 69 70def capture_cmd( 71 capture_file, block_size=None, duration=10, channels=1, rate=48000): 72 """Gets a command to capture the audio into the file with given settings. 73 74 @param capture_file: the name of file the audio to be stored in. 75 @param block_size: the number of frames per callback(dictates latency). 76 @param duration: seconds to record. If it is None, duration is not set, 77 and command will keep capturing audio until it is 78 terminated. 79 @param channels: number of channels. 80 @param rate: the sampling rate. 81 82 @returns: The command args put in a list of strings. 83 84 """ 85 args = [_CRAS_TEST_CLIENT] 86 args += ['--capture_file', capture_file] 87 if block_size is not None: 88 args += ['--block_size', str(block_size)] 89 if duration is not None: 90 args += ['--duration', str(duration)] 91 args += ['--num_channels', str(channels)] 92 args += ['--rate', str(rate)] 93 return args 94 95 96def loopback(*args, **kargs): 97 """A helper function to execute loopback_cmd. 98 99 @param args: args passed to loopback_cmd. 100 @param kargs: kargs passed to loopback_cmd. 101 102 """ 103 104 cmd_utils.execute(loopback_cmd(*args, **kargs)) 105 106 107def loopback_cmd(output_file, duration=10, channels=2, rate=48000): 108 """Gets a command to record the loopback. 109 110 @param output_file: The name of the file the loopback to be stored in. 111 @param channels: The number of channels of the recorded audio. 112 @param duration: seconds to record. 113 @param rate: the sampling rate. 114 115 @returns: The command args put in a list of strings. 116 117 """ 118 args = [_CRAS_TEST_CLIENT] 119 args += ['--loopback_file', output_file] 120 args += ['--duration_seconds', str(duration)] 121 args += ['--num_channels', str(channels)] 122 args += ['--rate', str(rate)] 123 return args 124 125 126def get_cras_nodes_cmd(): 127 """Gets a command to query the nodes from Cras. 128 129 @returns: The command to query nodes information from Cras using dbus-send. 130 131 """ 132 return ('dbus-send --system --type=method_call --print-reply ' 133 '--dest=org.chromium.cras /org/chromium/cras ' 134 'org.chromium.cras.Control.GetNodes') 135 136 137def set_system_volume(volume): 138 """Set the system volume. 139 140 @param volume: the system output vlume to be set(0 - 100). 141 142 """ 143 get_cras_control_interface().SetOutputVolume(volume) 144 145 146def set_node_volume(node_id, volume): 147 """Set the volume of the given output node. 148 149 @param node_id: the id of the output node to be set the volume. 150 @param volume: the volume to be set(0-100). 151 152 """ 153 get_cras_control_interface().SetOutputNodeVolume(node_id, volume) 154 155 156def set_capture_gain(gain): 157 """Set the system capture gain. 158 159 @param gain the capture gain in db*100 (100 = 1dB) 160 161 """ 162 get_cras_control_interface().SetInputGain(gain) 163 164 165def get_cras_control_interface(private=False): 166 """Gets Cras DBus control interface. 167 168 @param private: Set to True to use a new instance for dbus.SystemBus 169 instead of the shared instance. 170 171 @returns: A dBus.Interface object with Cras Control interface. 172 173 @raises: ImportError if this is not called on Cros device. 174 175 """ 176 try: 177 import dbus 178 except ImportError, e: 179 logging.exception( 180 'Can not import dbus: %s. This method should only be ' 181 'called on Cros device.', e) 182 raise 183 bus = dbus.SystemBus(private=private) 184 cras_object = bus.get_object('org.chromium.cras', '/org/chromium/cras') 185 return dbus.Interface(cras_object, 'org.chromium.cras.Control') 186 187 188def get_cras_nodes(): 189 """Gets nodes information from Cras. 190 191 @returns: A dict containing information of each node. 192 193 """ 194 return get_cras_control_interface().GetNodes() 195 196 197def get_selected_nodes(): 198 """Gets selected output nodes and input nodes. 199 200 @returns: A tuple (output_nodes, input_nodes) where each 201 field is a list of selected node IDs returned from Cras DBus API. 202 Note that there may be multiple output/input nodes being selected 203 at the same time. 204 205 """ 206 output_nodes = [] 207 input_nodes = [] 208 nodes = get_cras_nodes() 209 for node in nodes: 210 if node['Active']: 211 if node['IsInput']: 212 input_nodes.append(node['Id']) 213 else: 214 output_nodes.append(node['Id']) 215 return (output_nodes, input_nodes) 216 217 218def set_selected_output_node_volume(volume): 219 """Sets the selected output node volume. 220 221 @param volume: the volume to be set (0-100). 222 223 """ 224 selected_output_node_ids, _ = get_selected_nodes() 225 for node_id in selected_output_node_ids: 226 set_node_volume(node_id, volume) 227 228 229def get_active_stream_count(): 230 """Gets the number of active streams. 231 232 @returns: The number of active streams. 233 234 """ 235 return int(get_cras_control_interface().GetNumberOfActiveStreams()) 236 237 238def set_system_mute(is_mute): 239 """Sets the system mute switch. 240 241 @param is_mute: Set True to mute the system playback. 242 243 """ 244 get_cras_control_interface().SetOutputMute(is_mute) 245 246 247def set_capture_mute(is_mute): 248 """Sets the capture mute switch. 249 250 @param is_mute: Set True to mute the capture. 251 252 """ 253 get_cras_control_interface().SetInputMute(is_mute) 254 255 256def node_type_is_plugged(node_type, nodes_info): 257 """Determine if there is any node of node_type plugged. 258 259 This method is used in has_loopback_dongle in cros_host, where 260 the call is executed on autotest server. Use get_cras_nodes instead if 261 the call can be executed on Cros device. 262 263 Since Cras only reports the plugged node in GetNodes, we can 264 parse the return value to see if there is any node with the given type. 265 For example, if INTERNAL_MIC is of intereset, the pattern we are 266 looking for is: 267 268 dict entry( 269 string "Type" 270 variant string "INTERNAL_MIC" 271 ) 272 273 @param node_type: A str representing node type defined in CRAS_NODE_TYPES. 274 @param nodes_info: A str containing output of command get_nodes_cmd. 275 276 @returns: True if there is any node of node_type plugged. False otherwise. 277 278 """ 279 match = re.search(r'string "Type"\s+variant\s+string "%s"' % node_type, 280 nodes_info) 281 return True if match else False 282 283 284# Cras node types reported from Cras DBus control API. 285CRAS_OUTPUT_NODE_TYPES = ['HEADPHONE', 'INTERNAL_SPEAKER', 'HDMI', 'USB', 286 'BLUETOOTH', 'LINEOUT', 'UNKNOWN'] 287CRAS_INPUT_NODE_TYPES = ['MIC', 'INTERNAL_MIC', 'USB', 'BLUETOOTH', 288 'POST_DSP_LOOPBACK', 'POST_MIX_LOOPBACK', 'UNKNOWN', 289 'KEYBOARD_MIC', 'HOTWORD'] 290CRAS_NODE_TYPES = CRAS_OUTPUT_NODE_TYPES + CRAS_INPUT_NODE_TYPES 291 292 293def get_filtered_node_types(callback): 294 """Returns the pair of filtered output node types and input node types. 295 296 @param callback: A callback function which takes a node as input parameter 297 and filter the node based on its return value. 298 299 @returns: A tuple (output_node_types, input_node_types) where each 300 field is a list of node types defined in CRAS_NODE_TYPES, 301 and their 'attribute_name' is True. 302 303 """ 304 output_node_types = [] 305 input_node_types = [] 306 nodes = get_cras_nodes() 307 for node in nodes: 308 if callback(node): 309 node_type = str(node['Type']) 310 if node_type not in CRAS_NODE_TYPES: 311 raise RuntimeError( 312 'node type %s is not valid' % node_type) 313 if node['IsInput']: 314 input_node_types.append(node_type) 315 else: 316 output_node_types.append(node_type) 317 return (output_node_types, input_node_types) 318 319 320def get_selected_node_types(): 321 """Returns the pair of active output node types and input node types. 322 323 @returns: A tuple (output_node_types, input_node_types) where each 324 field is a list of selected node types defined in CRAS_NODE_TYPES. 325 326 """ 327 def is_selected(node): 328 """Checks if a node is selected. 329 330 A node is selected if its Active attribute is True. 331 332 @returns: True is a node is selected, False otherwise. 333 334 """ 335 return node['Active'] 336 337 return get_filtered_node_types(is_selected) 338 339 340def get_plugged_node_types(): 341 """Returns the pair of plugged output node types and input node types. 342 343 @returns: A tuple (output_node_types, input_node_types) where each 344 field is a list of plugged node types defined in CRAS_NODE_TYPES. 345 346 """ 347 def is_plugged(node): 348 """Checks if a node is plugged and is not unknown node. 349 350 Cras DBus API only reports plugged node, so every node reported by Cras 351 DBus API is plugged. However, we filter out UNKNOWN node here because 352 the existence of unknown node depends on the number of redundant 353 playback/record audio device created on audio card. Also, the user of 354 Cras will ignore unknown nodes. 355 356 @returns: True if a node is plugged and is not an UNKNOWN node. 357 358 """ 359 return node['Type'] != 'UNKNOWN' 360 361 return get_filtered_node_types(is_plugged) 362 363 364def set_selected_node_types(output_node_types, input_node_types): 365 """Sets selected node types. 366 367 @param output_node_types: A list of output node types. None to skip setting. 368 @param input_node_types: A list of input node types. None to skip setting. 369 370 """ 371 if len(output_node_types) == 1: 372 set_single_selected_output_node(output_node_types[0]) 373 elif output_node_types: 374 set_selected_output_nodes(output_node_types) 375 if len(input_node_types) == 1: 376 set_single_selected_input_node(input_node_types[0]) 377 elif input_node_types: 378 set_selected_input_nodes(input_node_types) 379 380 381def set_single_selected_output_node(node_type): 382 """Sets one selected output node. 383 384 Note that Chrome UI uses SetActiveOutputNode of Cras DBus API 385 to select one output node. 386 387 @param node_type: A node type. 388 389 """ 390 nodes = get_cras_nodes() 391 for node in nodes: 392 if node['IsInput']: 393 continue 394 if node['Type'] == node_type: 395 set_active_output_node(node['Id']) 396 397 398def set_single_selected_input_node(node_type): 399 """Sets one selected input node. 400 401 Note that Chrome UI uses SetActiveInputNode of Cras DBus API 402 to select one input node. 403 404 @param node_type: A node type. 405 406 """ 407 nodes = get_cras_nodes() 408 for node in nodes: 409 if not node['IsInput']: 410 continue 411 if node['Type'] == node_type: 412 set_active_input_node(node['Id']) 413 414 415def set_selected_output_nodes(types): 416 """Sets selected output node types. 417 418 Note that Chrome UI uses SetActiveOutputNode of Cras DBus API 419 to select one output node. Here we use add/remove active output node 420 to support multiple nodes. 421 422 @param types: A list of output node types. 423 424 """ 425 nodes = get_cras_nodes() 426 for node in nodes: 427 if node['IsInput']: 428 continue 429 if node['Type'] in types: 430 add_active_output_node(node['Id']) 431 elif node['Active']: 432 remove_active_output_node(node['Id']) 433 434 435def set_selected_input_nodes(types): 436 """Sets selected input node types. 437 438 Note that Chrome UI uses SetActiveInputNode of Cras DBus API 439 to select one input node. Here we use add/remove active input node 440 to support multiple nodes. 441 442 @param types: A list of input node types. 443 444 """ 445 nodes = get_cras_nodes() 446 for node in nodes: 447 if not node['IsInput']: 448 continue 449 if node['Type'] in types: 450 add_active_input_node(node['Id']) 451 elif node['Active']: 452 remove_active_input_node(node['Id']) 453 454 455def set_active_input_node(node_id): 456 """Sets one active input node. 457 458 @param node_id: node id. 459 460 """ 461 get_cras_control_interface().SetActiveInputNode(node_id) 462 463 464def set_active_output_node(node_id): 465 """Sets one active output node. 466 467 @param node_id: node id. 468 469 """ 470 get_cras_control_interface().SetActiveOutputNode(node_id) 471 472 473def add_active_output_node(node_id): 474 """Adds an active output node. 475 476 @param node_id: node id. 477 478 """ 479 get_cras_control_interface().AddActiveOutputNode(node_id) 480 481 482def add_active_input_node(node_id): 483 """Adds an active input node. 484 485 @param node_id: node id. 486 487 """ 488 get_cras_control_interface().AddActiveInputNode(node_id) 489 490 491def remove_active_output_node(node_id): 492 """Removes an active output node. 493 494 @param node_id: node id. 495 496 """ 497 get_cras_control_interface().RemoveActiveOutputNode(node_id) 498 499 500def remove_active_input_node(node_id): 501 """Removes an active input node. 502 503 @param node_id: node id. 504 505 """ 506 get_cras_control_interface().RemoveActiveInputNode(node_id) 507 508 509def get_node_id_from_node_type(node_type, is_input): 510 """Gets node id from node type. 511 512 @param types: A node type defined in CRAS_NODE_TYPES. 513 @param is_input: True if the node is input. False otherwise. 514 515 @returns: A string for node id. 516 517 @raises: CrasUtilsError: if unique node id can not be found. 518 519 """ 520 nodes = get_cras_nodes() 521 find_ids = [] 522 for node in nodes: 523 if node['Type'] == node_type and node['IsInput'] == is_input: 524 find_ids.append(node['Id']) 525 if len(find_ids) != 1: 526 raise CrasUtilsError( 527 'Can not find unique node id from node type %s' % node_type) 528 return find_ids[0] 529 530def get_active_node_volume(): 531 """Returns volume from active node. 532 533 @returns: int for volume 534 535 @raises: CrasUtilsError: if node volume cannot be found. 536 """ 537 nodes = get_cras_nodes() 538 for node in nodes: 539 if node['Active'] == 1 and node['IsInput'] == 0: 540 return int(node['NodeVolume']) 541 raise CrasUtilsError('Cannot find active node volume from nodes.') 542