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.multimedia import audio_extension_handler 20 21 22class AudioFacadeNativeError(Exception): 23 """Error in AudioFacadeNative.""" 24 pass 25 26 27def check_arc_resource(func): 28 """Decorator function for ARC related functions in AudioFacadeNative.""" 29 @functools.wraps(func) 30 def wrapper(instance, *args, **kwargs): 31 """Wrapper for the methods to check _arc_resource. 32 33 @param instance: Object instance. 34 35 @raises: AudioFacadeNativeError if there is no ARC resource. 36 37 """ 38 if not instance._arc_resource: 39 raise AudioFacadeNativeError('There is no ARC resource.') 40 return func(instance, *args, **kwargs) 41 return wrapper 42 43 44def file_contains_all_zeros(path): 45 """Reads a file and checks whether the file contains all zeros.""" 46 with open(path) as f: 47 binary = f.read() 48 # Assume data is in 16 bit signed int format. The real format 49 # does not matter though since we only care if there is nonzero data. 50 np_array = np.fromstring(binary, dtype='<i2') 51 return not np.any(np_array) 52 53 54class AudioFacadeNative(object): 55 """Facede to access the audio-related functionality. 56 57 The methods inside this class only accept Python native types. 58 59 """ 60 _CAPTURE_DATA_FORMATS = [ 61 dict(file_type='raw', sample_format='S16_LE', 62 channel=1, rate=48000), 63 dict(file_type='raw', sample_format='S16_LE', 64 channel=2, rate=48000)] 65 66 _PLAYBACK_DATA_FORMAT = dict( 67 file_type='raw', sample_format='S16_LE', channel=2, rate=48000) 68 69 def __init__(self, resource, arc_resource=None): 70 """Initializes an audio facade. 71 72 @param resource: A FacadeResource object. 73 @param arc_resource: An ArcResource object. 74 75 """ 76 self._resource = resource 77 self._recorder = None 78 self._player = None 79 self._counter = None 80 self._loaded_extension_handler = None 81 self._arc_resource = arc_resource 82 83 84 @property 85 def _extension_handler(self): 86 """Multimedia test extension handler.""" 87 if not self._loaded_extension_handler: 88 extension = self._resource.get_extension( 89 constants.MULTIMEDIA_TEST_EXTENSION) 90 logging.debug('Loaded extension: %s', extension) 91 self._loaded_extension_handler = ( 92 audio_extension_handler.AudioExtensionHandler(extension)) 93 return self._loaded_extension_handler 94 95 96 def get_audio_devices(self): 97 """Returns the audio devices from chrome.audio API. 98 99 @returns: Checks docstring of get_audio_devices of AudioExtensionHandler. 100 101 """ 102 return self._extension_handler.get_audio_devices() 103 104 105 def set_chrome_active_volume(self, volume): 106 """Sets the active audio output volume using chrome.audio API. 107 108 @param volume: Volume to set (0~100). 109 110 """ 111 self._extension_handler.set_active_volume(volume) 112 113 114 def set_chrome_mute(self, mute): 115 """Mutes the active audio output using chrome.audio API. 116 117 @param mute: True to mute. False otherwise. 118 119 """ 120 self._extension_handler.set_mute(mute) 121 122 123 def get_chrome_active_volume_mute(self): 124 """Gets the volume state of active audio output using chrome.audio API. 125 126 @param returns: A tuple (volume, mute), where volume is 0~100, and mute 127 is True if node is muted, False otherwise. 128 129 """ 130 return self._extension_handler.get_active_volume_mute() 131 132 133 def set_chrome_active_node_type(self, output_node_type, input_node_type): 134 """Sets active node type through chrome.audio API. 135 136 The node types are defined in cras_utils.CRAS_NODE_TYPES. 137 The current active node will be disabled first if the new active node 138 is different from the current one. 139 140 @param output_node_type: A node type defined in 141 cras_utils.CRAS_NODE_TYPES. None to skip. 142 @param input_node_type: A node type defined in 143 cras_utils.CRAS_NODE_TYPES. None to skip 144 145 """ 146 if output_node_type: 147 node_id = cras_utils.get_node_id_from_node_type( 148 output_node_type, False) 149 self._extension_handler.set_active_node_id(node_id) 150 if input_node_type: 151 node_id = cras_utils.get_node_id_from_node_type( 152 input_node_type, True) 153 self._extension_handler.set_active_node_id(node_id) 154 155 156 def cleanup(self): 157 """Clean up the temporary files.""" 158 for path in glob.glob('/tmp/playback_*'): 159 os.unlink(path) 160 161 for path in glob.glob('/tmp/capture_*'): 162 os.unlink(path) 163 164 if self._recorder: 165 self._recorder.cleanup() 166 if self._player: 167 self._player.cleanup() 168 169 if self._arc_resource: 170 self._arc_resource.cleanup() 171 172 173 def playback(self, file_path, data_format, blocking=False): 174 """Playback a file. 175 176 @param file_path: The path to the file. 177 @param data_format: A dict containing data format including 178 file_type, sample_format, channel, and rate. 179 file_type: file type e.g. 'raw' or 'wav'. 180 sample_format: One of the keys in 181 audio_data.SAMPLE_FORMAT. 182 channel: number of channels. 183 rate: sampling rate. 184 @param blocking: Blocks this call until playback finishes. 185 186 @returns: True. 187 188 @raises: AudioFacadeNativeError if data format is not supported. 189 190 """ 191 logging.info('AudioFacadeNative playback file: %r. format: %r', 192 file_path, data_format) 193 194 if data_format != self._PLAYBACK_DATA_FORMAT: 195 raise AudioFacadeNativeError( 196 'data format %r is not supported' % data_format) 197 198 self._player = Player() 199 self._player.start(file_path, blocking) 200 201 return True 202 203 204 def stop_playback(self): 205 """Stops playback process.""" 206 self._player.stop() 207 208 209 def start_recording(self, data_format): 210 """Starts recording an audio file. 211 212 Currently the format specified in _CAPTURE_DATA_FORMATS is the only 213 formats. 214 215 @param data_format: A dict containing: 216 file_type: 'raw'. 217 sample_format: 'S16_LE' for 16-bit signed integer in 218 little-endian. 219 channel: channel number. 220 rate: sampling rate. 221 222 223 @returns: True 224 225 @raises: AudioFacadeNativeError if data format is not supported. 226 227 """ 228 logging.info('AudioFacadeNative record format: %r', data_format) 229 230 if data_format not in self._CAPTURE_DATA_FORMATS: 231 raise AudioFacadeNativeError( 232 'data format %r is not supported' % data_format) 233 234 self._recorder = Recorder() 235 self._recorder.start(data_format) 236 237 return True 238 239 240 def stop_recording(self): 241 """Stops recording an audio file. 242 243 @returns: The path to the recorded file. 244 None if capture device is not functional. 245 246 """ 247 self._recorder.stop() 248 if file_contains_all_zeros(self._recorder.file_path): 249 logging.error('Recorded file contains all zeros. ' 250 'Capture device is not functional') 251 return None 252 return self._recorder.file_path 253 254 255 def set_selected_output_volume(self, volume): 256 """Sets the selected output volume. 257 258 @param volume: the volume to be set(0-100). 259 260 """ 261 cras_utils.set_selected_output_node_volume(volume) 262 263 264 def set_input_gain(self, gain): 265 """Sets the system capture gain. 266 267 @param gain: the capture gain in db*100 (100 = 1dB) 268 269 """ 270 cras_utils.set_capture_gain(gain) 271 272 273 def set_selected_node_types(self, output_node_types, input_node_types): 274 """Set selected node types. 275 276 The node types are defined in cras_utils.CRAS_NODE_TYPES. 277 278 @param output_node_types: A list of output node types. 279 None to skip setting. 280 @param input_node_types: A list of input node types. 281 None to skip setting. 282 283 """ 284 cras_utils.set_selected_node_types(output_node_types, input_node_types) 285 286 287 def get_selected_node_types(self): 288 """Gets the selected output and input node types. 289 290 @returns: A tuple (output_node_types, input_node_types) where each 291 field is a list of selected node types defined in 292 cras_utils.CRAS_NODE_TYPES. 293 294 """ 295 return cras_utils.get_selected_node_types() 296 297 298 def get_plugged_node_types(self): 299 """Gets the plugged output and input node types. 300 301 @returns: A tuple (output_node_types, input_node_types) where each 302 field is a list of plugged node types defined in 303 cras_utils.CRAS_NODE_TYPES. 304 305 """ 306 return cras_utils.get_plugged_node_types() 307 308 309 def dump_diagnostics(self, file_path): 310 """Dumps audio diagnostics results to a file. 311 312 @param file_path: The path to dump results. 313 314 @returns: True 315 316 """ 317 with open(file_path, 'w') as f: 318 f.write(audio_helper.get_audio_diagnostics()) 319 return True 320 321 322 def start_counting_signal(self, signal_name): 323 """Starts counting DBus signal from Cras. 324 325 @param signal_name: Signal of interest. 326 327 """ 328 if self._counter: 329 raise AudioFacadeNativeError('There is an ongoing counting.') 330 self._counter = cras_dbus_utils.CrasDBusBackgroundSignalCounter() 331 self._counter.start(signal_name) 332 333 334 def stop_counting_signal(self): 335 """Stops counting DBus signal from Cras. 336 337 @returns: Number of signals starting from last start_counting_signal 338 call. 339 340 """ 341 if not self._counter: 342 raise AudioFacadeNativeError('Should start counting signal first') 343 result = self._counter.stop() 344 self._counter = None 345 return result 346 347 348 def wait_for_unexpected_nodes_changed(self, timeout_secs): 349 """Waits for unexpected nodes changed signal. 350 351 @param timeout_secs: Timeout in seconds for waiting. 352 353 """ 354 cras_dbus_utils.wait_for_unexpected_nodes_changed(timeout_secs) 355 356 357 @check_arc_resource 358 def start_arc_recording(self): 359 """Starts recording using microphone app in container.""" 360 self._arc_resource.microphone.start_microphone_app() 361 362 363 @check_arc_resource 364 def stop_arc_recording(self): 365 """Checks the recording is stopped and gets the recorded path. 366 367 The recording duration of microphone app is fixed, so this method just 368 copies the recorded result from container to a path on Cros device. 369 370 """ 371 _, file_path = tempfile.mkstemp(prefix='capture_', suffix='.amr-nb') 372 self._arc_resource.microphone.stop_microphone_app(file_path) 373 return file_path 374 375 376 @check_arc_resource 377 def set_arc_playback_file(self, file_path): 378 """Copies the audio file to be played into container. 379 380 User should call this method to put the file into container before 381 calling start_arc_playback. 382 383 @param file_path: Path to the file to be played on Cros host. 384 385 @returns: Path to the file in container. 386 387 """ 388 return self._arc_resource.play_music.set_playback_file(file_path) 389 390 391 @check_arc_resource 392 def start_arc_playback(self, path): 393 """Start playback through Play Music app. 394 395 Before calling this method, user should call set_arc_playback_file to 396 put the file into container. 397 398 @param path: Path to the file in container. 399 400 """ 401 self._arc_resource.play_music.start_playback(path) 402 403 404 @check_arc_resource 405 def stop_arc_playback(self): 406 """Stop playback through Play Music app.""" 407 self._arc_resource.play_music.stop_playback() 408 409 410class RecorderError(Exception): 411 """Error in Recorder.""" 412 pass 413 414 415class Recorder(object): 416 """The class to control recording subprocess. 417 418 Properties: 419 file_path: The path to recorded file. It should be accessed after 420 stop() is called. 421 422 """ 423 def __init__(self): 424 """Initializes a Recorder.""" 425 _, self.file_path = tempfile.mkstemp(prefix='capture_', suffix='.raw') 426 self._capture_subprocess = None 427 428 429 def start(self, data_format): 430 """Starts recording. 431 432 Starts recording subprocess. It can be stopped by calling stop(). 433 434 @param data_format: A dict containing: 435 file_type: 'raw'. 436 sample_format: 'S16_LE' for 16-bit signed integer in 437 little-endian. 438 channel: channel number. 439 rate: sampling rate. 440 441 @raises: RecorderError: If recording subprocess is terminated 442 unexpectedly. 443 444 """ 445 self._capture_subprocess = cmd_utils.popen( 446 cras_utils.capture_cmd( 447 capture_file=self.file_path, duration=None, 448 channels=data_format['channel'], 449 rate=data_format['rate'])) 450 451 452 def stop(self): 453 """Stops recording subprocess.""" 454 if self._capture_subprocess.poll() is None: 455 self._capture_subprocess.terminate() 456 else: 457 raise RecorderError( 458 'Recording process was terminated unexpectedly.') 459 460 461 def cleanup(self): 462 """Cleanup the resources. 463 464 Terminates the recording process if needed. 465 466 """ 467 if self._capture_subprocess and self._capture_subprocess.poll() is None: 468 self._capture_subprocess.terminate() 469 470 471class PlayerError(Exception): 472 """Error in Player.""" 473 pass 474 475 476class Player(object): 477 """The class to control audio playback subprocess. 478 479 Properties: 480 file_path: The path to the file to play. 481 482 """ 483 def __init__(self): 484 """Initializes a Player.""" 485 self._playback_subprocess = None 486 487 488 def start(self, file_path, blocking): 489 """Starts recording. 490 491 Starts recording subprocess. It can be stopped by calling stop(). 492 493 @param file_path: The path to the file. 494 @param blocking: Blocks this call until playback finishes. 495 496 """ 497 self._playback_subprocess = cras_utils.playback( 498 blocking, playback_file=file_path) 499 500 501 def stop(self): 502 """Stops playback subprocess.""" 503 cmd_utils.kill_or_log_returncode(self._playback_subprocess) 504 505 506 def cleanup(self): 507 """Cleanup the resources. 508 509 Terminates the playback process if needed. 510 511 """ 512 self.stop() 513