1# Copyright 2014 The Chromium OS Authors. All rights reserved. 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5"""Facade to access the audio-related functionality.""" 6 7import functools 8import glob 9import logging 10import numpy as np 11import os 12import tempfile 13 14from autotest_lib.client.cros import constants 15from autotest_lib.client.cros.audio import audio_helper 16from autotest_lib.client.cros.audio import cmd_utils 17from autotest_lib.client.cros.audio import cras_dbus_utils 18from autotest_lib.client.cros.audio import cras_utils 19from autotest_lib.client.cros.audio import alsa_utils 20from autotest_lib.client.cros.multimedia import audio_extension_handler 21 22 23class AudioFacadeNativeError(Exception): 24 """Error in AudioFacadeNative.""" 25 pass 26 27 28def check_arc_resource(func): 29 """Decorator function for ARC related functions in AudioFacadeNative.""" 30 @functools.wraps(func) 31 def wrapper(instance, *args, **kwargs): 32 """Wrapper for the methods to check _arc_resource. 33 34 @param instance: Object instance. 35 36 @raises: AudioFacadeNativeError if there is no ARC resource. 37 38 """ 39 if not instance._arc_resource: 40 raise AudioFacadeNativeError('There is no ARC resource.') 41 return func(instance, *args, **kwargs) 42 return wrapper 43 44 45def file_contains_all_zeros(path): 46 """Reads a file and checks whether the file contains all zeros.""" 47 with open(path) as f: 48 binary = f.read() 49 # Assume data is in 16 bit signed int format. The real format 50 # does not matter though since we only care if there is nonzero data. 51 np_array = np.fromstring(binary, dtype='<i2') 52 return not np.any(np_array) 53 54 55class AudioFacadeNative(object): 56 """Facede to access the audio-related functionality. 57 58 The methods inside this class only accept Python native types. 59 60 """ 61 _CAPTURE_DATA_FORMATS = [ 62 dict(file_type='raw', sample_format='S16_LE', 63 channel=1, rate=48000), 64 dict(file_type='raw', sample_format='S16_LE', 65 channel=2, rate=48000)] 66 67 _PLAYBACK_DATA_FORMAT = dict( 68 file_type='raw', sample_format='S16_LE', channel=2, rate=48000) 69 70 _LISTEN_DATA_FORMATS = [ 71 dict(file_type='raw', sample_format='S16_LE', 72 channel=1, rate=16000)] 73 74 def __init__(self, resource, arc_resource=None): 75 """Initializes an audio facade. 76 77 @param resource: A FacadeResource object. 78 @param arc_resource: An ArcResource object. 79 80 """ 81 self._resource = resource 82 self._listener = None 83 self._recorders = {} 84 self._player = None 85 self._counter = None 86 self._loaded_extension_handler = None 87 self._arc_resource = arc_resource 88 89 90 @property 91 def _extension_handler(self): 92 """Multimedia test extension handler.""" 93 if not self._loaded_extension_handler: 94 extension = self._resource.get_extension( 95 constants.AUDIO_TEST_EXTENSION) 96 logging.debug('Loaded extension: %s', extension) 97 self._loaded_extension_handler = ( 98 audio_extension_handler.AudioExtensionHandler(extension)) 99 return self._loaded_extension_handler 100 101 102 def get_audio_availability(self): 103 """Returns the availability of chrome.audio API. 104 105 @returns: True if chrome.audio exists 106 """ 107 return self._extension_handler.get_audio_api_availability() 108 109 110 def get_audio_devices(self): 111 """Returns the audio devices from chrome.audio API. 112 113 @returns: Checks docstring of get_audio_devices of AudioExtensionHandler. 114 115 """ 116 return self._extension_handler.get_audio_devices() 117 118 119 def set_chrome_active_volume(self, volume): 120 """Sets the active audio output volume using chrome.audio API. 121 122 @param volume: Volume to set (0~100). 123 124 """ 125 self._extension_handler.set_active_volume(volume) 126 127 128 def set_chrome_active_input_gain(self, gain): 129 """Sets the active audio input gain using chrome.audio API. 130 131 @param volume: Gain to set (0~100). 132 133 """ 134 self._extension_handler.set_active_input_gain(gain) 135 136 137 def set_chrome_mute(self, mute): 138 """Mutes the active audio output using chrome.audio API. 139 140 @param mute: True to mute. False otherwise. 141 142 """ 143 self._extension_handler.set_mute(mute) 144 145 146 def get_chrome_active_volume_mute(self): 147 """Gets the volume state of active audio output using chrome.audio API. 148 149 @param returns: A tuple (volume, mute), where volume is 0~100, and mute 150 is True if node is muted, False otherwise. 151 152 """ 153 return self._extension_handler.get_active_volume_mute() 154 155 156 def set_chrome_active_node_type(self, output_node_type, input_node_type): 157 """Sets active node type through chrome.audio API. 158 159 The node types are defined in cras_utils.CRAS_NODE_TYPES. 160 The current active node will be disabled first if the new active node 161 is different from the current one. 162 163 @param output_node_type: A node type defined in 164 cras_utils.CRAS_NODE_TYPES. None to skip. 165 @param input_node_type: A node type defined in 166 cras_utils.CRAS_NODE_TYPES. None to skip 167 168 """ 169 if output_node_type: 170 node_id = cras_utils.get_node_id_from_node_type( 171 output_node_type, False) 172 self._extension_handler.set_active_node_id(node_id) 173 if input_node_type: 174 node_id = cras_utils.get_node_id_from_node_type( 175 input_node_type, True) 176 self._extension_handler.set_active_node_id(node_id) 177 178 179 def check_audio_stream_at_selected_device(self): 180 """Checks the audio output is at expected node""" 181 output_device_name = cras_utils.get_selected_output_device_name() 182 output_device_type = cras_utils.get_selected_output_device_type() 183 logging.info("Output device name is %s", output_device_name) 184 logging.info("Output device type is %s", output_device_type) 185 alsa_utils.check_audio_stream_at_selected_device(output_device_name, 186 output_device_type) 187 188 189 def cleanup(self): 190 """Clean up the temporary files.""" 191 for path in glob.glob('/tmp/playback_*'): 192 os.unlink(path) 193 194 for path in glob.glob('/tmp/capture_*'): 195 os.unlink(path) 196 197 for path in glob.glob('/tmp/listen_*'): 198 os.unlink(path) 199 200 if self._recorders: 201 for _, recorder in self._recorders: 202 recorder.cleanup() 203 self._recorders.clear() 204 205 if self._player: 206 self._player.cleanup() 207 if self._listener: 208 self._listener.cleanup() 209 210 if self._arc_resource: 211 self._arc_resource.cleanup() 212 213 214 def playback(self, file_path, data_format, blocking=False, node_type=None, 215 block_size=None): 216 """Playback a file. 217 218 @param file_path: The path to the file. 219 @param data_format: A dict containing data format including 220 file_type, sample_format, channel, and rate. 221 file_type: file type e.g. 'raw' or 'wav'. 222 sample_format: One of the keys in 223 audio_data.SAMPLE_FORMAT. 224 channel: number of channels. 225 rate: sampling rate. 226 @param blocking: Blocks this call until playback finishes. 227 @param node_type: A Cras node type defined in cras_utils.CRAS_NODE_TYPES 228 that we like to pin at. None to have the playback on 229 active selected device. 230 @param block_size: The number for frames per callback. 231 232 @returns: True. 233 234 @raises: AudioFacadeNativeError if data format is not supported. 235 236 """ 237 logging.info('AudioFacadeNative playback file: %r. format: %r', 238 file_path, data_format) 239 240 if data_format != self._PLAYBACK_DATA_FORMAT: 241 raise AudioFacadeNativeError( 242 'data format %r is not supported' % data_format) 243 244 device_id = None 245 if node_type: 246 device_id = int(cras_utils.get_device_id_from_node_type( 247 node_type, False)) 248 249 self._player = Player() 250 self._player.start(file_path, blocking, device_id, block_size) 251 252 return True 253 254 255 def stop_playback(self): 256 """Stops playback process.""" 257 self._player.stop() 258 259 260 def start_recording(self, data_format, node_type=None, block_size=None): 261 """Starts recording an audio file. 262 263 Currently the format specified in _CAPTURE_DATA_FORMATS is the only 264 formats. 265 266 @param data_format: A dict containing: 267 file_type: 'raw'. 268 sample_format: 'S16_LE' for 16-bit signed integer in 269 little-endian. 270 channel: channel number. 271 rate: sampling rate. 272 @param node_type: A Cras node type defined in cras_utils.CRAS_NODE_TYPES 273 that we like to pin at. None to have the recording 274 from active selected device. 275 @param block_size: The number for frames per callback. 276 277 @returns: True 278 279 @raises: AudioFacadeNativeError if data format is not supported, no 280 active selected node or the specified node is occupied. 281 282 """ 283 logging.info('AudioFacadeNative record format: %r', data_format) 284 285 if data_format not in self._CAPTURE_DATA_FORMATS: 286 raise AudioFacadeNativeError( 287 'data format %r is not supported' % data_format) 288 289 if node_type is None: 290 device_id = None 291 node_type = cras_utils.get_selected_input_device_type() 292 if node_type is None: 293 raise AudioFacadeNativeError('No active selected input node.') 294 else: 295 device_id = int(cras_utils.get_device_id_from_node_type( 296 node_type, True)) 297 298 if node_type in self._recorders: 299 raise AudioFacadeNativeError( 300 'Node %s is already ocuppied' % node_type) 301 302 self._recorders[node_type] = Recorder() 303 self._recorders[node_type].start(data_format, device_id, block_size) 304 305 return True 306 307 308 def stop_recording(self, node_type=None): 309 """Stops recording an audio file. 310 @param node_type: A Cras node type defined in cras_utils.CRAS_NODE_TYPES 311 that we like to pin at. None to have the recording 312 from active selected device. 313 314 @returns: The path to the recorded file. 315 None if capture device is not functional. 316 317 @raises: AudioFacadeNativeError if no recording is started on 318 corresponding node. 319 """ 320 if node_type is None: 321 device_id = None 322 node_type = cras_utils.get_selected_input_device_type() 323 if node_type is None: 324 raise AudioFacadeNativeError('No active selected input node.') 325 else: 326 device_id = int(cras_utils.get_device_id_from_node_type( 327 node_type, True)) 328 329 330 if node_type not in self._recorders: 331 raise AudioFacadeNativeError( 332 'No recording is started on node %s' % node_type) 333 334 recorder = self._recorders[node_type] 335 recorder.stop() 336 del self._recorders[node_type] 337 338 file_path = recorder.file_path 339 if file_contains_all_zeros(recorder.file_path): 340 logging.error('Recorded file contains all zeros. ' 341 'Capture device is not functional') 342 return None 343 344 return file_path 345 346 347 def start_listening(self, data_format): 348 """Starts listening to hotword for a given format. 349 350 Currently the format specified in _CAPTURE_DATA_FORMATS is the only 351 formats. 352 353 @param data_format: A dict containing: 354 file_type: 'raw'. 355 sample_format: 'S16_LE' for 16-bit signed integer in 356 little-endian. 357 channel: channel number. 358 rate: sampling rate. 359 360 361 @returns: True 362 363 @raises: AudioFacadeNativeError if data format is not supported. 364 365 """ 366 logging.info('AudioFacadeNative record format: %r', data_format) 367 368 if data_format not in self._LISTEN_DATA_FORMATS: 369 raise AudioFacadeNativeError( 370 'data format %r is not supported' % data_format) 371 372 self._listener = Listener() 373 self._listener.start(data_format) 374 375 return True 376 377 378 def stop_listening(self): 379 """Stops listening to hotword. 380 381 @returns: The path to the recorded file. 382 None if hotwording is not functional. 383 384 """ 385 self._listener.stop() 386 if file_contains_all_zeros(self._listener.file_path): 387 logging.error('Recorded file contains all zeros. ' 388 'Hotwording device is not functional') 389 return None 390 return self._listener.file_path 391 392 393 def set_selected_output_volume(self, volume): 394 """Sets the selected output volume. 395 396 @param volume: the volume to be set(0-100). 397 398 """ 399 cras_utils.set_selected_output_node_volume(volume) 400 401 402 def set_selected_node_types(self, output_node_types, input_node_types): 403 """Set selected node types. 404 405 The node types are defined in cras_utils.CRAS_NODE_TYPES. 406 407 @param output_node_types: A list of output node types. 408 None to skip setting. 409 @param input_node_types: A list of input node types. 410 None to skip setting. 411 412 """ 413 cras_utils.set_selected_node_types(output_node_types, input_node_types) 414 415 416 def get_selected_node_types(self): 417 """Gets the selected output and input node types. 418 419 @returns: A tuple (output_node_types, input_node_types) where each 420 field is a list of selected node types defined in 421 cras_utils.CRAS_NODE_TYPES. 422 423 """ 424 return cras_utils.get_selected_node_types() 425 426 427 def get_plugged_node_types(self): 428 """Gets the plugged output and input node types. 429 430 @returns: A tuple (output_node_types, input_node_types) where each 431 field is a list of plugged node types defined in 432 cras_utils.CRAS_NODE_TYPES. 433 434 """ 435 return cras_utils.get_plugged_node_types() 436 437 438 def dump_diagnostics(self, file_path): 439 """Dumps audio diagnostics results to a file. 440 441 @param file_path: The path to dump results. 442 443 """ 444 audio_helper.dump_audio_diagnostics(file_path) 445 446 447 def start_counting_signal(self, signal_name): 448 """Starts counting DBus signal from Cras. 449 450 @param signal_name: Signal of interest. 451 452 """ 453 if self._counter: 454 raise AudioFacadeNativeError('There is an ongoing counting.') 455 self._counter = cras_dbus_utils.CrasDBusBackgroundSignalCounter() 456 self._counter.start(signal_name) 457 458 459 def stop_counting_signal(self): 460 """Stops counting DBus signal from Cras. 461 462 @returns: Number of signals starting from last start_counting_signal 463 call. 464 465 """ 466 if not self._counter: 467 raise AudioFacadeNativeError('Should start counting signal first') 468 result = self._counter.stop() 469 self._counter = None 470 return result 471 472 473 def wait_for_unexpected_nodes_changed(self, timeout_secs): 474 """Waits for unexpected nodes changed signal. 475 476 @param timeout_secs: Timeout in seconds for waiting. 477 478 """ 479 cras_dbus_utils.wait_for_unexpected_nodes_changed(timeout_secs) 480 481 482 @check_arc_resource 483 def start_arc_recording(self): 484 """Starts recording using microphone app in container.""" 485 self._arc_resource.microphone.start_microphone_app() 486 487 488 @check_arc_resource 489 def stop_arc_recording(self): 490 """Checks the recording is stopped and gets the recorded path. 491 492 The recording duration of microphone app is fixed, so this method just 493 copies the recorded result from container to a path on Cros device. 494 495 """ 496 _, file_path = tempfile.mkstemp(prefix='capture_', suffix='.amr-nb') 497 self._arc_resource.microphone.stop_microphone_app(file_path) 498 return file_path 499 500 501 @check_arc_resource 502 def set_arc_playback_file(self, file_path): 503 """Copies the audio file to be played into container. 504 505 User should call this method to put the file into container before 506 calling start_arc_playback. 507 508 @param file_path: Path to the file to be played on Cros host. 509 510 @returns: Path to the file in container. 511 512 """ 513 return self._arc_resource.play_music.set_playback_file(file_path) 514 515 516 @check_arc_resource 517 def start_arc_playback(self, path): 518 """Start playback through Play Music app. 519 520 Before calling this method, user should call set_arc_playback_file to 521 put the file into container. 522 523 @param path: Path to the file in container. 524 525 """ 526 self._arc_resource.play_music.start_playback(path) 527 528 529 @check_arc_resource 530 def stop_arc_playback(self): 531 """Stop playback through Play Music app.""" 532 self._arc_resource.play_music.stop_playback() 533 534 535class RecorderError(Exception): 536 """Error in Recorder.""" 537 pass 538 539 540class Recorder(object): 541 """The class to control recording subprocess. 542 543 Properties: 544 file_path: The path to recorded file. It should be accessed after 545 stop() is called. 546 547 """ 548 def __init__(self): 549 """Initializes a Recorder.""" 550 _, self.file_path = tempfile.mkstemp(prefix='capture_', suffix='.raw') 551 self._capture_subprocess = None 552 553 554 def start(self, data_format, pin_device, block_size): 555 """Starts recording. 556 557 Starts recording subprocess. It can be stopped by calling stop(). 558 559 @param data_format: A dict containing: 560 file_type: 'raw'. 561 sample_format: 'S16_LE' for 16-bit signed integer in 562 little-endian. 563 channel: channel number. 564 rate: sampling rate. 565 @param pin_device: A integer of device id to record from. 566 @param block_size: The number for frames per callback. 567 """ 568 self._capture_subprocess = cmd_utils.popen( 569 cras_utils.capture_cmd( 570 capture_file=self.file_path, duration=None, 571 channels=data_format['channel'], 572 rate=data_format['rate'], 573 pin_device=pin_device, block_size=block_size)) 574 575 576 def stop(self): 577 """Stops recording subprocess.""" 578 if self._capture_subprocess.poll() is None: 579 self._capture_subprocess.terminate() 580 else: 581 raise RecorderError( 582 'Recording process was terminated unexpectedly.') 583 584 585 def cleanup(self): 586 """Cleanup the resources. 587 588 Terminates the recording process if needed. 589 590 """ 591 if self._capture_subprocess and self._capture_subprocess.poll() is None: 592 self._capture_subprocess.terminate() 593 594 595class PlayerError(Exception): 596 """Error in Player.""" 597 pass 598 599 600class Player(object): 601 """The class to control audio playback subprocess. 602 603 Properties: 604 file_path: The path to the file to play. 605 606 """ 607 def __init__(self): 608 """Initializes a Player.""" 609 self._playback_subprocess = None 610 611 612 def start(self, file_path, blocking, pin_device, block_size): 613 """Starts playing. 614 615 Starts playing subprocess. It can be stopped by calling stop(). 616 617 @param file_path: The path to the file. 618 @param blocking: Blocks this call until playback finishes. 619 @param pin_device: A integer of device id to play on. 620 @param block_size: The number for frames per callback. 621 622 """ 623 self._playback_subprocess = cras_utils.playback( 624 blocking, playback_file=file_path, pin_device=pin_device, 625 block_size=block_size) 626 627 628 def stop(self): 629 """Stops playback subprocess.""" 630 cmd_utils.kill_or_log_returncode(self._playback_subprocess) 631 632 633 def cleanup(self): 634 """Cleanup the resources. 635 636 Terminates the playback process if needed. 637 638 """ 639 self.stop() 640 641 642class ListenerError(Exception): 643 """Error in Listener.""" 644 pass 645 646 647class Listener(object): 648 """The class to control listening subprocess. 649 650 Properties: 651 file_path: The path to recorded file. It should be accessed after 652 stop() is called. 653 654 """ 655 def __init__(self): 656 """Initializes a Listener.""" 657 _, self.file_path = tempfile.mkstemp(prefix='listen_', suffix='.raw') 658 self._capture_subprocess = None 659 660 661 def start(self, data_format): 662 """Starts listening. 663 664 Starts listening subprocess. It can be stopped by calling stop(). 665 666 @param data_format: A dict containing: 667 file_type: 'raw'. 668 sample_format: 'S16_LE' for 16-bit signed integer in 669 little-endian. 670 channel: channel number. 671 rate: sampling rate. 672 673 @raises: ListenerError: If listening subprocess is terminated 674 unexpectedly. 675 676 """ 677 self._capture_subprocess = cmd_utils.popen( 678 cras_utils.listen_cmd( 679 capture_file=self.file_path, duration=None, 680 channels=data_format['channel'], 681 rate=data_format['rate'])) 682 683 684 def stop(self): 685 """Stops listening subprocess.""" 686 if self._capture_subprocess.poll() is None: 687 self._capture_subprocess.terminate() 688 else: 689 raise ListenerError( 690 'Listening process was terminated unexpectedly.') 691 692 693 def cleanup(self): 694 """Cleanup the resources. 695 696 Terminates the listening process if needed. 697 698 """ 699 if self._capture_subprocess and self._capture_subprocess.poll() is None: 700 self._capture_subprocess.terminate() 701