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