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 glob 8import logging 9import multiprocessing 10import os 11import tempfile 12 13from autotest_lib.client.cros import constants 14from autotest_lib.client.cros.audio import audio_helper 15from autotest_lib.client.cros.audio import cmd_utils 16from autotest_lib.client.cros.audio import cras_dbus_utils 17from autotest_lib.client.cros.audio import cras_utils 18from autotest_lib.client.cros.multimedia import audio_extension_handler 19 20 21class AudioFacadeNativeError(Exception): 22 """Error in AudioFacadeNative.""" 23 pass 24 25 26class AudioFacadeNative(object): 27 """Facede to access the audio-related functionality. 28 29 The methods inside this class only accept Python native types. 30 31 """ 32 _CAPTURE_DATA_FORMATS = [ 33 dict(file_type='raw', sample_format='S16_LE', 34 channel=1, rate=48000), 35 dict(file_type='raw', sample_format='S16_LE', 36 channel=2, rate=48000)] 37 38 _PLAYBACK_DATA_FORMAT = dict( 39 file_type='raw', sample_format='S16_LE', channel=2, rate=48000) 40 41 def __init__(self, resource): 42 """Initializes an audio facade. 43 44 @param resource: A FacadeResource object. 45 46 """ 47 self._resource = resource 48 self._recorder = None 49 self._counter = None 50 self._extension_handler = None 51 self._create_extension_handler() 52 53 54 def _create_extension_handler(self): 55 """Loads multimedia test extension and creates extension handler.""" 56 extension = self._resource.get_extension( 57 constants.MULTIMEDIA_TEST_EXTENSION) 58 logging.debug('Loaded extension: %s', extension) 59 self._extension_handler = audio_extension_handler.AudioExtensionHandler( 60 extension) 61 62 63 def get_audio_info(self): 64 """Returns the audio info from chrome.audio API. 65 66 @returns: Checks docstring of get_audio_info of AudioExtensionHandler. 67 68 """ 69 return self._extension_handler.get_audio_info() 70 71 72 def set_chrome_active_volume(self, volume): 73 """Sets the active audio output volume using chrome.audio API. 74 75 @param volume: Volume to set (0~100). 76 77 """ 78 self._extension_handler.set_active_volume(volume) 79 80 81 def set_chrome_mute(self, mute): 82 """Mutes the active audio output using chrome.audio API. 83 84 @param mute: True to mute. False otherwise. 85 86 """ 87 self._extension_handler.set_mute(mute) 88 89 90 def get_chrome_active_volume_mute(self): 91 """Gets the volume state of active audio output using chrome.audio API. 92 93 @param returns: A tuple (volume, mute), where volume is 0~100, and mute 94 is True if node is muted, False otherwise. 95 96 """ 97 return self._extension_handler.get_active_volume_mute() 98 99 100 def set_chrome_active_node_type(self, output_node_type, input_node_type): 101 """Sets active node type through chrome.audio API. 102 103 The node types are defined in cras_utils.CRAS_NODE_TYPES. 104 The current active node will be disabled first if the new active node 105 is different from the current one. 106 107 @param output_node_type: A node type defined in 108 cras_utils.CRAS_NODE_TYPES. None to skip. 109 @param input_node_type: A node type defined in 110 cras_utils.CRAS_NODE_TYPES. None to skip 111 112 """ 113 if output_node_type: 114 node_id = cras_utils.get_node_id_from_node_type( 115 output_node_type, False) 116 self._extension_handler.set_active_node_id(node_id) 117 if input_node_type: 118 node_id = cras_utils.get_node_id_from_node_type( 119 input_node_type, True) 120 self._extension_handler.set_active_node_id(node_id) 121 122 123 def cleanup(self): 124 """Clean up the temporary files.""" 125 for path in glob.glob('/tmp/playback_*'): 126 os.unlink(path) 127 128 for path in glob.glob('/tmp/capture_*'): 129 os.unlink(path) 130 131 132 def playback(self, file_path, data_format, blocking=False): 133 """Playback a file. 134 135 @param file_path: The path to the file. 136 @param data_format: A dict containing data format including 137 file_type, sample_format, channel, and rate. 138 file_type: file type e.g. 'raw' or 'wav'. 139 sample_format: One of the keys in 140 audio_data.SAMPLE_FORMAT. 141 channel: number of channels. 142 rate: sampling rate. 143 @param blocking: Blocks this call until playback finishes. 144 145 @returns: True. 146 147 @raises: AudioFacadeNativeError if data format is not supported. 148 149 """ 150 logging.info('AudioFacadeNative playback file: %r. format: %r', 151 file_path, data_format) 152 153 if data_format != self._PLAYBACK_DATA_FORMAT: 154 raise AudioFacadeNativeError( 155 'data format %r is not supported' % data_format) 156 157 def _playback(): 158 """Playback using cras utility.""" 159 cras_utils.playback(playback_file=file_path) 160 161 if blocking: 162 _playback() 163 else: 164 p = multiprocessing.Process(target=_playback) 165 p.daemon = True 166 p.start() 167 168 return True 169 170 171 def start_recording(self, data_format): 172 """Starts recording an audio file. 173 174 Currently the format specified in _CAPTURE_DATA_FORMATS is the only 175 formats. 176 177 @param data_format: A dict containing: 178 file_type: 'raw'. 179 sample_format: 'S16_LE' for 16-bit signed integer in 180 little-endian. 181 channel: channel number. 182 rate: sampling rate. 183 184 185 @returns: True 186 187 @raises: AudioFacadeNativeError if data format is not supported. 188 189 """ 190 logging.info('AudioFacadeNative record format: %r', data_format) 191 192 if data_format not in self._CAPTURE_DATA_FORMATS: 193 raise AudioFacadeNativeError( 194 'data format %r is not supported' % data_format) 195 196 self._recorder = Recorder() 197 self._recorder.start(data_format) 198 199 return True 200 201 202 def stop_recording(self): 203 """Stops recording an audio file. 204 205 @returns: The path to the recorded file. 206 207 """ 208 self._recorder.stop() 209 return self._recorder.file_path 210 211 212 def set_selected_output_volume(self, volume): 213 """Sets the selected output volume. 214 215 @param volume: the volume to be set(0-100). 216 217 """ 218 cras_utils.set_selected_output_node_volume(volume) 219 220 221 def set_selected_node_types(self, output_node_types, input_node_types): 222 """Set selected node types. 223 224 The node types are defined in cras_utils.CRAS_NODE_TYPES. 225 226 @param output_node_types: A list of output node types. 227 None to skip setting. 228 @param input_node_types: A list of input node types. 229 None to skip setting. 230 231 """ 232 cras_utils.set_selected_node_types(output_node_types, input_node_types) 233 234 235 def get_selected_node_types(self): 236 """Gets the selected output and input node types. 237 238 @returns: A tuple (output_node_types, input_node_types) where each 239 field is a list of selected node types defined in 240 cras_utils.CRAS_NODE_TYPES. 241 242 """ 243 return cras_utils.get_selected_node_types() 244 245 246 def get_plugged_node_types(self): 247 """Gets the plugged output and input node types. 248 249 @returns: A tuple (output_node_types, input_node_types) where each 250 field is a list of plugged node types defined in 251 cras_utils.CRAS_NODE_TYPES. 252 253 """ 254 return cras_utils.get_plugged_node_types() 255 256 257 def dump_diagnostics(self, file_path): 258 """Dumps audio diagnostics results to a file. 259 260 @param file_path: The path to dump results. 261 262 @returns: True 263 264 """ 265 with open(file_path, 'w') as f: 266 f.write(audio_helper.get_audio_diagnostics()) 267 return True 268 269 270 def start_counting_signal(self, signal_name): 271 """Starts counting DBus signal from Cras. 272 273 @param signal_name: Signal of interest. 274 275 """ 276 if self._counter: 277 raise AudioFacadeNativeError('There is an ongoing counting.') 278 self._counter = cras_dbus_utils.CrasDBusBackgroundSignalCounter() 279 self._counter.start(signal_name) 280 281 282 def stop_counting_signal(self): 283 """Stops counting DBus signal from Cras. 284 285 @returns: Number of signals starting from last start_counting_signal 286 call. 287 288 """ 289 if not self._counter: 290 raise AudioFacadeNativeError('Should start counting signal first') 291 result = self._counter.stop() 292 self._counter = None 293 return result 294 295 296 def wait_for_unexpected_nodes_changed(self, timeout_secs): 297 """Waits for unexpected nodes changed signal. 298 299 @param timeout_secs: Timeout in seconds for waiting. 300 301 """ 302 cras_dbus_utils.wait_for_unexpected_nodes_changed(timeout_secs) 303 304 305 306class RecorderError(Exception): 307 """Error in Recorder.""" 308 pass 309 310 311class Recorder(object): 312 """The class to control recording subprocess. 313 314 Properties: 315 file_path: The path to recorded file. It should be accessed after 316 stop() is called. 317 318 """ 319 def __init__(self): 320 """Initializes a Recorder.""" 321 _, self.file_path = tempfile.mkstemp(prefix='capture_', suffix='.raw') 322 self._capture_subprocess = None 323 324 325 def start(self, data_format): 326 """Starts recording. 327 328 Starts recording subprocess. It can be stopped by calling stop(). 329 330 @param data_format: A dict containing: 331 file_type: 'raw'. 332 sample_format: 'S16_LE' for 16-bit signed integer in 333 little-endian. 334 channel: channel number. 335 rate: sampling rate. 336 337 @raises: RecorderError: If recording subprocess is terminated 338 unexpectedly. 339 340 """ 341 self._capture_subprocess = cmd_utils.popen( 342 cras_utils.capture_cmd( 343 capture_file=self.file_path, duration=None, 344 channels=data_format['channel'], 345 rate=data_format['rate'])) 346 347 348 def stop(self): 349 """Stops recording subprocess.""" 350 if self._capture_subprocess.poll() is None: 351 self._capture_subprocess.terminate() 352 else: 353 raise RecorderError( 354 'Recording process was terminated unexpectedly.') 355