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