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