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._recorder = None 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_devices(self): 103 """Returns the audio devices from chrome.audio API. 104 105 @returns: Checks docstring of get_audio_devices of AudioExtensionHandler. 106 107 """ 108 return self._extension_handler.get_audio_devices() 109 110 111 def set_chrome_active_volume(self, volume): 112 """Sets the active audio output volume using chrome.audio API. 113 114 @param volume: Volume to set (0~100). 115 116 """ 117 self._extension_handler.set_active_volume(volume) 118 119 120 def set_chrome_mute(self, mute): 121 """Mutes the active audio output using chrome.audio API. 122 123 @param mute: True to mute. False otherwise. 124 125 """ 126 self._extension_handler.set_mute(mute) 127 128 129 def get_chrome_active_volume_mute(self): 130 """Gets the volume state of active audio output using chrome.audio API. 131 132 @param returns: A tuple (volume, mute), where volume is 0~100, and mute 133 is True if node is muted, False otherwise. 134 135 """ 136 return self._extension_handler.get_active_volume_mute() 137 138 139 def set_chrome_active_node_type(self, output_node_type, input_node_type): 140 """Sets active node type through chrome.audio API. 141 142 The node types are defined in cras_utils.CRAS_NODE_TYPES. 143 The current active node will be disabled first if the new active node 144 is different from the current one. 145 146 @param output_node_type: A node type defined in 147 cras_utils.CRAS_NODE_TYPES. None to skip. 148 @param input_node_type: A node type defined in 149 cras_utils.CRAS_NODE_TYPES. None to skip 150 151 """ 152 if output_node_type: 153 node_id = cras_utils.get_node_id_from_node_type( 154 output_node_type, False) 155 self._extension_handler.set_active_node_id(node_id) 156 if input_node_type: 157 node_id = cras_utils.get_node_id_from_node_type( 158 input_node_type, True) 159 self._extension_handler.set_active_node_id(node_id) 160 161 162 def check_audio_stream_at_selected_device(self): 163 """Checks the audio output is at expected node""" 164 output_device_name = cras_utils.get_selected_output_device_name() 165 output_device_type = cras_utils.get_selected_output_device_type() 166 logging.info("Output device name is %s", output_device_name) 167 logging.info("Output device type is %s", output_device_type) 168 alsa_utils.check_audio_stream_at_selected_device(output_device_name, 169 output_device_type) 170 171 172 def cleanup(self): 173 """Clean up the temporary files.""" 174 for path in glob.glob('/tmp/playback_*'): 175 os.unlink(path) 176 177 for path in glob.glob('/tmp/capture_*'): 178 os.unlink(path) 179 180 for path in glob.glob('/tmp/listen_*'): 181 os.unlink(path) 182 183 if self._recorder: 184 self._recorder.cleanup() 185 if self._player: 186 self._player.cleanup() 187 if self._listener: 188 self._listener.cleanup() 189 190 if self._arc_resource: 191 self._arc_resource.cleanup() 192 193 194 def playback(self, file_path, data_format, blocking=False): 195 """Playback a file. 196 197 @param file_path: The path to the file. 198 @param data_format: A dict containing data format including 199 file_type, sample_format, channel, and rate. 200 file_type: file type e.g. 'raw' or 'wav'. 201 sample_format: One of the keys in 202 audio_data.SAMPLE_FORMAT. 203 channel: number of channels. 204 rate: sampling rate. 205 @param blocking: Blocks this call until playback finishes. 206 207 @returns: True. 208 209 @raises: AudioFacadeNativeError if data format is not supported. 210 211 """ 212 logging.info('AudioFacadeNative playback file: %r. format: %r', 213 file_path, data_format) 214 215 if data_format != self._PLAYBACK_DATA_FORMAT: 216 raise AudioFacadeNativeError( 217 'data format %r is not supported' % data_format) 218 219 self._player = Player() 220 self._player.start(file_path, blocking) 221 222 return True 223 224 225 def stop_playback(self): 226 """Stops playback process.""" 227 self._player.stop() 228 229 230 def start_recording(self, data_format): 231 """Starts recording an audio file. 232 233 Currently the format specified in _CAPTURE_DATA_FORMATS is the only 234 formats. 235 236 @param data_format: A dict containing: 237 file_type: 'raw'. 238 sample_format: 'S16_LE' for 16-bit signed integer in 239 little-endian. 240 channel: channel number. 241 rate: sampling rate. 242 243 244 @returns: True 245 246 @raises: AudioFacadeNativeError if data format is not supported. 247 248 """ 249 logging.info('AudioFacadeNative record format: %r', data_format) 250 251 if data_format not in self._CAPTURE_DATA_FORMATS: 252 raise AudioFacadeNativeError( 253 'data format %r is not supported' % data_format) 254 255 self._recorder = Recorder() 256 self._recorder.start(data_format) 257 258 return True 259 260 261 def stop_recording(self): 262 """Stops recording an audio file. 263 264 @returns: The path to the recorded file. 265 None if capture device is not functional. 266 267 """ 268 self._recorder.stop() 269 if file_contains_all_zeros(self._recorder.file_path): 270 logging.error('Recorded file contains all zeros. ' 271 'Capture device is not functional') 272 return None 273 return self._recorder.file_path 274 275 276 def start_listening(self, data_format): 277 """Starts listening to hotword for a given format. 278 279 Currently the format specified in _CAPTURE_DATA_FORMATS is the only 280 formats. 281 282 @param data_format: A dict containing: 283 file_type: 'raw'. 284 sample_format: 'S16_LE' for 16-bit signed integer in 285 little-endian. 286 channel: channel number. 287 rate: sampling rate. 288 289 290 @returns: True 291 292 @raises: AudioFacadeNativeError if data format is not supported. 293 294 """ 295 logging.info('AudioFacadeNative record format: %r', data_format) 296 297 if data_format not in self._LISTEN_DATA_FORMATS: 298 raise AudioFacadeNativeError( 299 'data format %r is not supported' % data_format) 300 301 self._listener = Listener() 302 self._listener.start(data_format) 303 304 return True 305 306 307 def stop_listening(self): 308 """Stops listening to hotword. 309 310 @returns: The path to the recorded file. 311 None if hotwording is not functional. 312 313 """ 314 self._listener.stop() 315 if file_contains_all_zeros(self._listener.file_path): 316 logging.error('Recorded file contains all zeros. ' 317 'Hotwording device is not functional') 318 return None 319 return self._listener.file_path 320 321 322 def set_selected_output_volume(self, volume): 323 """Sets the selected output volume. 324 325 @param volume: the volume to be set(0-100). 326 327 """ 328 cras_utils.set_selected_output_node_volume(volume) 329 330 331 def set_input_gain(self, gain): 332 """Sets the system capture gain. 333 334 @param gain: the capture gain in db*100 (100 = 1dB) 335 336 """ 337 cras_utils.set_capture_gain(gain) 338 339 340 def set_selected_node_types(self, output_node_types, input_node_types): 341 """Set selected node types. 342 343 The node types are defined in cras_utils.CRAS_NODE_TYPES. 344 345 @param output_node_types: A list of output node types. 346 None to skip setting. 347 @param input_node_types: A list of input node types. 348 None to skip setting. 349 350 """ 351 cras_utils.set_selected_node_types(output_node_types, input_node_types) 352 353 354 def get_selected_node_types(self): 355 """Gets the selected output and input node types. 356 357 @returns: A tuple (output_node_types, input_node_types) where each 358 field is a list of selected node types defined in 359 cras_utils.CRAS_NODE_TYPES. 360 361 """ 362 return cras_utils.get_selected_node_types() 363 364 365 def get_plugged_node_types(self): 366 """Gets the plugged output and input node types. 367 368 @returns: A tuple (output_node_types, input_node_types) where each 369 field is a list of plugged node types defined in 370 cras_utils.CRAS_NODE_TYPES. 371 372 """ 373 return cras_utils.get_plugged_node_types() 374 375 376 def dump_diagnostics(self, file_path): 377 """Dumps audio diagnostics results to a file. 378 379 @param file_path: The path to dump results. 380 381 @returns: True 382 383 """ 384 with open(file_path, 'w') as f: 385 f.write(audio_helper.get_audio_diagnostics()) 386 return True 387 388 389 def start_counting_signal(self, signal_name): 390 """Starts counting DBus signal from Cras. 391 392 @param signal_name: Signal of interest. 393 394 """ 395 if self._counter: 396 raise AudioFacadeNativeError('There is an ongoing counting.') 397 self._counter = cras_dbus_utils.CrasDBusBackgroundSignalCounter() 398 self._counter.start(signal_name) 399 400 401 def stop_counting_signal(self): 402 """Stops counting DBus signal from Cras. 403 404 @returns: Number of signals starting from last start_counting_signal 405 call. 406 407 """ 408 if not self._counter: 409 raise AudioFacadeNativeError('Should start counting signal first') 410 result = self._counter.stop() 411 self._counter = None 412 return result 413 414 415 def wait_for_unexpected_nodes_changed(self, timeout_secs): 416 """Waits for unexpected nodes changed signal. 417 418 @param timeout_secs: Timeout in seconds for waiting. 419 420 """ 421 cras_dbus_utils.wait_for_unexpected_nodes_changed(timeout_secs) 422 423 424 @check_arc_resource 425 def start_arc_recording(self): 426 """Starts recording using microphone app in container.""" 427 self._arc_resource.microphone.start_microphone_app() 428 429 430 @check_arc_resource 431 def stop_arc_recording(self): 432 """Checks the recording is stopped and gets the recorded path. 433 434 The recording duration of microphone app is fixed, so this method just 435 copies the recorded result from container to a path on Cros device. 436 437 """ 438 _, file_path = tempfile.mkstemp(prefix='capture_', suffix='.amr-nb') 439 self._arc_resource.microphone.stop_microphone_app(file_path) 440 return file_path 441 442 443 @check_arc_resource 444 def set_arc_playback_file(self, file_path): 445 """Copies the audio file to be played into container. 446 447 User should call this method to put the file into container before 448 calling start_arc_playback. 449 450 @param file_path: Path to the file to be played on Cros host. 451 452 @returns: Path to the file in container. 453 454 """ 455 return self._arc_resource.play_music.set_playback_file(file_path) 456 457 458 @check_arc_resource 459 def start_arc_playback(self, path): 460 """Start playback through Play Music app. 461 462 Before calling this method, user should call set_arc_playback_file to 463 put the file into container. 464 465 @param path: Path to the file in container. 466 467 """ 468 self._arc_resource.play_music.start_playback(path) 469 470 471 @check_arc_resource 472 def stop_arc_playback(self): 473 """Stop playback through Play Music app.""" 474 self._arc_resource.play_music.stop_playback() 475 476 477class RecorderError(Exception): 478 """Error in Recorder.""" 479 pass 480 481 482class Recorder(object): 483 """The class to control recording subprocess. 484 485 Properties: 486 file_path: The path to recorded file. It should be accessed after 487 stop() is called. 488 489 """ 490 def __init__(self): 491 """Initializes a Recorder.""" 492 _, self.file_path = tempfile.mkstemp(prefix='capture_', suffix='.raw') 493 self._capture_subprocess = None 494 495 496 def start(self, data_format): 497 """Starts recording. 498 499 Starts recording subprocess. It can be stopped by calling stop(). 500 501 @param data_format: A dict containing: 502 file_type: 'raw'. 503 sample_format: 'S16_LE' for 16-bit signed integer in 504 little-endian. 505 channel: channel number. 506 rate: sampling rate. 507 508 @raises: RecorderError: If recording subprocess is terminated 509 unexpectedly. 510 511 """ 512 self._capture_subprocess = cmd_utils.popen( 513 cras_utils.capture_cmd( 514 capture_file=self.file_path, duration=None, 515 channels=data_format['channel'], 516 rate=data_format['rate'])) 517 518 519 def stop(self): 520 """Stops recording subprocess.""" 521 if self._capture_subprocess.poll() is None: 522 self._capture_subprocess.terminate() 523 else: 524 raise RecorderError( 525 'Recording process was terminated unexpectedly.') 526 527 528 def cleanup(self): 529 """Cleanup the resources. 530 531 Terminates the recording process if needed. 532 533 """ 534 if self._capture_subprocess and self._capture_subprocess.poll() is None: 535 self._capture_subprocess.terminate() 536 537 538class PlayerError(Exception): 539 """Error in Player.""" 540 pass 541 542 543class Player(object): 544 """The class to control audio playback subprocess. 545 546 Properties: 547 file_path: The path to the file to play. 548 549 """ 550 def __init__(self): 551 """Initializes a Player.""" 552 self._playback_subprocess = None 553 554 555 def start(self, file_path, blocking): 556 """Starts recording. 557 558 Starts recording subprocess. It can be stopped by calling stop(). 559 560 @param file_path: The path to the file. 561 @param blocking: Blocks this call until playback finishes. 562 563 """ 564 self._playback_subprocess = cras_utils.playback( 565 blocking, playback_file=file_path) 566 567 568 def stop(self): 569 """Stops playback subprocess.""" 570 cmd_utils.kill_or_log_returncode(self._playback_subprocess) 571 572 573 def cleanup(self): 574 """Cleanup the resources. 575 576 Terminates the playback process if needed. 577 578 """ 579 self.stop() 580 581 582class ListenerError(Exception): 583 """Error in Listener.""" 584 pass 585 586 587class Listener(object): 588 """The class to control listening subprocess. 589 590 Properties: 591 file_path: The path to recorded file. It should be accessed after 592 stop() is called. 593 594 """ 595 def __init__(self): 596 """Initializes a Listener.""" 597 _, self.file_path = tempfile.mkstemp(prefix='listen_', suffix='.raw') 598 self._capture_subprocess = None 599 600 601 def start(self, data_format): 602 """Starts listening. 603 604 Starts listening subprocess. It can be stopped by calling stop(). 605 606 @param data_format: A dict containing: 607 file_type: 'raw'. 608 sample_format: 'S16_LE' for 16-bit signed integer in 609 little-endian. 610 channel: channel number. 611 rate: sampling rate. 612 613 @raises: ListenerError: If listening subprocess is terminated 614 unexpectedly. 615 616 """ 617 self._capture_subprocess = cmd_utils.popen( 618 cras_utils.listen_cmd( 619 capture_file=self.file_path, duration=None, 620 channels=data_format['channel'], 621 rate=data_format['rate'])) 622 623 624 def stop(self): 625 """Stops listening subprocess.""" 626 if self._capture_subprocess.poll() is None: 627 self._capture_subprocess.terminate() 628 else: 629 raise ListenerError( 630 'Listening process was terminated unexpectedly.') 631 632 633 def cleanup(self): 634 """Cleanup the resources. 635 636 Terminates the listening process if needed. 637 638 """ 639 if self._capture_subprocess and self._capture_subprocess.poll() is None: 640 self._capture_subprocess.terminate() 641