• 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 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