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 AudioFacadeLocalError(Exception): 24 """Error in AudioFacadeLocal.""" 25 pass 26 27 28def check_arc_resource(func): 29 """Decorator function for ARC related functions in AudioFacadeLocal.""" 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: AudioFacadeLocalError if there is no ARC resource. 37 38 """ 39 if not instance._arc_resource: 40 raise AudioFacadeLocalError('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, 'rb') 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 AudioFacadeLocal(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: AudioFacadeLocalError if data format is not supported. 235 236 """ 237 logging.info('AudioFacadeLocal playback file: %r. format: %r', 238 file_path, data_format) 239 240 if data_format != self._PLAYBACK_DATA_FORMAT: 241 raise AudioFacadeLocalError( 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: AudioFacadeLocalError if data format is not supported, no 280 active selected node or the specified node is occupied. 281 282 """ 283 logging.info('AudioFacadeLocal record format: %r', data_format) 284 285 if data_format not in self._CAPTURE_DATA_FORMATS: 286 raise AudioFacadeLocalError( 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 AudioFacadeLocalError('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 AudioFacadeLocalError( 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: AudioFacadeLocalError 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 AudioFacadeLocalError('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 AudioFacadeLocalError( 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: AudioFacadeLocalError if data format is not supported. 364 365 """ 366 logging.info('AudioFacadeLocal record format: %r', data_format) 367 368 if data_format not in self._LISTEN_DATA_FORMATS: 369 raise AudioFacadeLocalError( 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 AudioFacadeLocalError('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 AudioFacadeLocalError('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 def get_noise_cancellation_supported(self): 483 """Gets whether the device supports Noise Cancellation. 484 485 @returns: True is supported; False otherwise. 486 487 """ 488 return cras_utils.get_noise_cancellation_supported() 489 490 491 def set_bypass_block_noise_cancellation(self, bypass): 492 """Sets CRAS to bypass the blocking logic of Noise Cancellation. 493 494 @param bypass: True for bypass; False for un-bypass. 495 496 """ 497 cras_utils.set_bypass_block_noise_cancellation(bypass) 498 499 500 def set_noise_cancellation_enabled(self, enabled): 501 """Sets the state to enable or disable Noise Cancellation. 502 503 @param enabled: True to enable; False to disable. 504 505 """ 506 cras_utils.set_noise_cancellation_enabled(enabled) 507 508 @check_arc_resource 509 def start_arc_recording(self): 510 """Starts recording using microphone app in container.""" 511 self._arc_resource.microphone.start_microphone_app() 512 513 514 @check_arc_resource 515 def stop_arc_recording(self): 516 """Checks the recording is stopped and gets the recorded path. 517 518 The recording duration of microphone app is fixed, so this method just 519 copies the recorded result from container to a path on Cros device. 520 521 """ 522 _, file_path = tempfile.mkstemp(prefix='capture_', suffix='.amr-nb') 523 self._arc_resource.microphone.stop_microphone_app(file_path) 524 return file_path 525 526 527 @check_arc_resource 528 def set_arc_playback_file(self, file_path): 529 """Copies the audio file to be played into container. 530 531 User should call this method to put the file into container before 532 calling start_arc_playback. 533 534 @param file_path: Path to the file to be played on Cros host. 535 536 @returns: Path to the file in container. 537 538 """ 539 return self._arc_resource.play_music.set_playback_file(file_path) 540 541 542 @check_arc_resource 543 def start_arc_playback(self, path): 544 """Start playback through Play Music app. 545 546 Before calling this method, user should call set_arc_playback_file to 547 put the file into container. 548 549 @param path: Path to the file in container. 550 551 """ 552 self._arc_resource.play_music.start_playback(path) 553 554 555 @check_arc_resource 556 def stop_arc_playback(self): 557 """Stop playback through Play Music app.""" 558 self._arc_resource.play_music.stop_playback() 559 560 561class RecorderError(Exception): 562 """Error in Recorder.""" 563 pass 564 565 566class Recorder(object): 567 """The class to control recording subprocess. 568 569 Properties: 570 file_path: The path to recorded file. It should be accessed after 571 stop() is called. 572 573 """ 574 def __init__(self): 575 """Initializes a Recorder.""" 576 _, self.file_path = tempfile.mkstemp(prefix='capture_', suffix='.raw') 577 self._capture_subprocess = None 578 579 580 def start(self, data_format, pin_device, block_size): 581 """Starts recording. 582 583 Starts recording subprocess. It can be stopped by calling stop(). 584 585 @param data_format: A dict containing: 586 file_type: 'raw'. 587 sample_format: 'S16_LE' for 16-bit signed integer in 588 little-endian. 589 channel: channel number. 590 rate: sampling rate. 591 @param pin_device: A integer of device id to record from. 592 @param block_size: The number for frames per callback. 593 """ 594 self._capture_subprocess = cmd_utils.popen( 595 cras_utils.capture_cmd( 596 capture_file=self.file_path, duration=None, 597 channels=data_format['channel'], 598 rate=data_format['rate'], 599 pin_device=pin_device, block_size=block_size)) 600 601 602 def stop(self): 603 """Stops recording subprocess.""" 604 if self._capture_subprocess.poll() is None: 605 self._capture_subprocess.terminate() 606 else: 607 raise RecorderError( 608 'Recording process was terminated unexpectedly.') 609 610 611 def cleanup(self): 612 """Cleanup the resources. 613 614 Terminates the recording process if needed. 615 616 """ 617 if self._capture_subprocess and self._capture_subprocess.poll() is None: 618 self._capture_subprocess.terminate() 619 620 621class PlayerError(Exception): 622 """Error in Player.""" 623 pass 624 625 626class Player(object): 627 """The class to control audio playback subprocess. 628 629 Properties: 630 file_path: The path to the file to play. 631 632 """ 633 def __init__(self): 634 """Initializes a Player.""" 635 self._playback_subprocess = None 636 637 638 def start(self, file_path, blocking, pin_device, block_size): 639 """Starts playing. 640 641 Starts playing subprocess. It can be stopped by calling stop(). 642 643 @param file_path: The path to the file. 644 @param blocking: Blocks this call until playback finishes. 645 @param pin_device: A integer of device id to play on. 646 @param block_size: The number for frames per callback. 647 648 """ 649 self._playback_subprocess = cras_utils.playback( 650 blocking, playback_file=file_path, pin_device=pin_device, 651 block_size=block_size) 652 653 654 def stop(self): 655 """Stops playback subprocess.""" 656 cmd_utils.kill_or_log_returncode(self._playback_subprocess) 657 658 659 def cleanup(self): 660 """Cleanup the resources. 661 662 Terminates the playback process if needed. 663 664 """ 665 self.stop() 666 667 668class ListenerError(Exception): 669 """Error in Listener.""" 670 pass 671 672 673class Listener(object): 674 """The class to control listening subprocess. 675 676 Properties: 677 file_path: The path to recorded file. It should be accessed after 678 stop() is called. 679 680 """ 681 def __init__(self): 682 """Initializes a Listener.""" 683 _, self.file_path = tempfile.mkstemp(prefix='listen_', suffix='.raw') 684 self._capture_subprocess = None 685 686 687 def start(self, data_format): 688 """Starts listening. 689 690 Starts listening subprocess. It can be stopped by calling stop(). 691 692 @param data_format: A dict containing: 693 file_type: 'raw'. 694 sample_format: 'S16_LE' for 16-bit signed integer in 695 little-endian. 696 channel: channel number. 697 rate: sampling rate. 698 699 @raises: ListenerError: If listening subprocess is terminated 700 unexpectedly. 701 702 """ 703 self._capture_subprocess = cmd_utils.popen( 704 cras_utils.listen_cmd( 705 capture_file=self.file_path, duration=None, 706 channels=data_format['channel'], 707 rate=data_format['rate'])) 708 709 710 def stop(self): 711 """Stops listening subprocess.""" 712 if self._capture_subprocess.poll() is None: 713 self._capture_subprocess.terminate() 714 else: 715 raise ListenerError( 716 'Listening process was terminated unexpectedly.') 717 718 719 def cleanup(self): 720 """Cleanup the resources. 721 722 Terminates the listening process if needed. 723 724 """ 725 if self._capture_subprocess and self._capture_subprocess.poll() is None: 726 self._capture_subprocess.terminate() 727