• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 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
26def 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
43class 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
394class RecorderError(Exception):
395    """Error in Recorder."""
396    pass
397
398
399class 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
455class PlayerError(Exception):
456    """Error in Player."""
457    pass
458
459
460class 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