• 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._recorders = {}
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_availability(self):
103        """Returns the availability of chrome.audio API.
104
105        @returns: True if chrome.audio exists
106        """
107        return self._extension_handler.get_audio_api_availability()
108
109
110    def get_audio_devices(self):
111        """Returns the audio devices from chrome.audio API.
112
113        @returns: Checks docstring of get_audio_devices of AudioExtensionHandler.
114
115        """
116        return self._extension_handler.get_audio_devices()
117
118
119    def set_chrome_active_volume(self, volume):
120        """Sets the active audio output volume using chrome.audio API.
121
122        @param volume: Volume to set (0~100).
123
124        """
125        self._extension_handler.set_active_volume(volume)
126
127
128    def set_chrome_active_input_gain(self, gain):
129        """Sets the active audio input gain using chrome.audio API.
130
131        @param volume: Gain to set (0~100).
132
133        """
134        self._extension_handler.set_active_input_gain(gain)
135
136
137    def set_chrome_mute(self, mute):
138        """Mutes the active audio output using chrome.audio API.
139
140        @param mute: True to mute. False otherwise.
141
142        """
143        self._extension_handler.set_mute(mute)
144
145
146    def get_chrome_active_volume_mute(self):
147        """Gets the volume state of active audio output using chrome.audio API.
148
149        @param returns: A tuple (volume, mute), where volume is 0~100, and mute
150                        is True if node is muted, False otherwise.
151
152        """
153        return self._extension_handler.get_active_volume_mute()
154
155
156    def set_chrome_active_node_type(self, output_node_type, input_node_type):
157        """Sets active node type through chrome.audio API.
158
159        The node types are defined in cras_utils.CRAS_NODE_TYPES.
160        The current active node will be disabled first if the new active node
161        is different from the current one.
162
163        @param output_node_type: A node type defined in
164                                 cras_utils.CRAS_NODE_TYPES. None to skip.
165        @param input_node_type: A node type defined in
166                                 cras_utils.CRAS_NODE_TYPES. None to skip
167
168        """
169        if output_node_type:
170            node_id = cras_utils.get_node_id_from_node_type(
171                    output_node_type, False)
172            self._extension_handler.set_active_node_id(node_id)
173        if input_node_type:
174            node_id = cras_utils.get_node_id_from_node_type(
175                    input_node_type, True)
176            self._extension_handler.set_active_node_id(node_id)
177
178
179    def check_audio_stream_at_selected_device(self):
180        """Checks the audio output is at expected node"""
181        output_device_name = cras_utils.get_selected_output_device_name()
182        output_device_type = cras_utils.get_selected_output_device_type()
183        logging.info("Output device name is %s", output_device_name)
184        logging.info("Output device type is %s", output_device_type)
185        alsa_utils.check_audio_stream_at_selected_device(output_device_name,
186                                                         output_device_type)
187
188
189    def cleanup(self):
190        """Clean up the temporary files."""
191        for path in glob.glob('/tmp/playback_*'):
192            os.unlink(path)
193
194        for path in glob.glob('/tmp/capture_*'):
195            os.unlink(path)
196
197        for path in glob.glob('/tmp/listen_*'):
198            os.unlink(path)
199
200        if self._recorders:
201            for _, recorder in self._recorders:
202                recorder.cleanup()
203        self._recorders.clear()
204
205        if self._player:
206            self._player.cleanup()
207        if self._listener:
208            self._listener.cleanup()
209
210        if self._arc_resource:
211            self._arc_resource.cleanup()
212
213
214    def playback(self, file_path, data_format, blocking=False, node_type=None,
215                 block_size=None):
216        """Playback a file.
217
218        @param file_path: The path to the file.
219        @param data_format: A dict containing data format including
220                            file_type, sample_format, channel, and rate.
221                            file_type: file type e.g. 'raw' or 'wav'.
222                            sample_format: One of the keys in
223                                           audio_data.SAMPLE_FORMAT.
224                            channel: number of channels.
225                            rate: sampling rate.
226        @param blocking: Blocks this call until playback finishes.
227        @param node_type: A Cras node type defined in cras_utils.CRAS_NODE_TYPES
228                          that we like to pin at. None to have the playback on
229                          active selected device.
230        @param block_size: The number for frames per callback.
231
232        @returns: True.
233
234        @raises: AudioFacadeNativeError if data format is not supported.
235
236        """
237        logging.info('AudioFacadeNative playback file: %r. format: %r',
238                     file_path, data_format)
239
240        if data_format != self._PLAYBACK_DATA_FORMAT:
241            raise AudioFacadeNativeError(
242                    'data format %r is not supported' % data_format)
243
244        device_id = None
245        if node_type:
246            device_id = int(cras_utils.get_device_id_from_node_type(
247                    node_type, False))
248
249        self._player = Player()
250        self._player.start(file_path, blocking, device_id, block_size)
251
252        return True
253
254
255    def stop_playback(self):
256        """Stops playback process."""
257        self._player.stop()
258
259
260    def start_recording(self, data_format, node_type=None, block_size=None):
261        """Starts recording an audio file.
262
263        Currently the format specified in _CAPTURE_DATA_FORMATS is the only
264        formats.
265
266        @param data_format: A dict containing:
267                            file_type: 'raw'.
268                            sample_format: 'S16_LE' for 16-bit signed integer in
269                                           little-endian.
270                            channel: channel number.
271                            rate: sampling rate.
272        @param node_type: A Cras node type defined in cras_utils.CRAS_NODE_TYPES
273                          that we like to pin at. None to have the recording
274                          from active selected device.
275        @param block_size: The number for frames per callback.
276
277        @returns: True
278
279        @raises: AudioFacadeNativeError if data format is not supported, no
280                 active selected node or the specified node is occupied.
281
282        """
283        logging.info('AudioFacadeNative record format: %r', data_format)
284
285        if data_format not in self._CAPTURE_DATA_FORMATS:
286            raise AudioFacadeNativeError(
287                    'data format %r is not supported' % data_format)
288
289        if node_type is None:
290            device_id = None
291            node_type = cras_utils.get_selected_input_device_type()
292            if node_type is None:
293                raise AudioFacadeNativeError('No active selected input node.')
294        else:
295            device_id = int(cras_utils.get_device_id_from_node_type(
296                    node_type, True))
297
298        if node_type in self._recorders:
299            raise AudioFacadeNativeError(
300                    'Node %s is already ocuppied' % node_type)
301
302        self._recorders[node_type] = Recorder()
303        self._recorders[node_type].start(data_format, device_id, block_size)
304
305        return True
306
307
308    def stop_recording(self, node_type=None):
309        """Stops recording an audio file.
310        @param node_type: A Cras node type defined in cras_utils.CRAS_NODE_TYPES
311                          that we like to pin at. None to have the recording
312                          from active selected device.
313
314        @returns: The path to the recorded file.
315                  None if capture device is not functional.
316
317        @raises: AudioFacadeNativeError if no recording is started on
318                 corresponding node.
319        """
320        if node_type is None:
321            device_id = None
322            node_type = cras_utils.get_selected_input_device_type()
323            if node_type is None:
324                raise AudioFacadeNativeError('No active selected input node.')
325        else:
326            device_id = int(cras_utils.get_device_id_from_node_type(
327                    node_type, True))
328
329
330        if node_type not in self._recorders:
331            raise AudioFacadeNativeError(
332                    'No recording is started on node %s' % node_type)
333
334        recorder = self._recorders[node_type]
335        recorder.stop()
336        del self._recorders[node_type]
337
338        file_path = recorder.file_path
339        if file_contains_all_zeros(recorder.file_path):
340            logging.error('Recorded file contains all zeros. '
341                          'Capture device is not functional')
342            return None
343
344        return file_path
345
346
347    def start_listening(self, data_format):
348        """Starts listening to hotword for a given format.
349
350        Currently the format specified in _CAPTURE_DATA_FORMATS is the only
351        formats.
352
353        @param data_format: A dict containing:
354                            file_type: 'raw'.
355                            sample_format: 'S16_LE' for 16-bit signed integer in
356                                           little-endian.
357                            channel: channel number.
358                            rate: sampling rate.
359
360
361        @returns: True
362
363        @raises: AudioFacadeNativeError if data format is not supported.
364
365        """
366        logging.info('AudioFacadeNative record format: %r', data_format)
367
368        if data_format not in self._LISTEN_DATA_FORMATS:
369            raise AudioFacadeNativeError(
370                    'data format %r is not supported' % data_format)
371
372        self._listener = Listener()
373        self._listener.start(data_format)
374
375        return True
376
377
378    def stop_listening(self):
379        """Stops listening to hotword.
380
381        @returns: The path to the recorded file.
382                  None if hotwording is not functional.
383
384        """
385        self._listener.stop()
386        if file_contains_all_zeros(self._listener.file_path):
387            logging.error('Recorded file contains all zeros. '
388                          'Hotwording device is not functional')
389            return None
390        return self._listener.file_path
391
392
393    def set_selected_output_volume(self, volume):
394        """Sets the selected output volume.
395
396        @param volume: the volume to be set(0-100).
397
398        """
399        cras_utils.set_selected_output_node_volume(volume)
400
401
402    def set_selected_node_types(self, output_node_types, input_node_types):
403        """Set selected node types.
404
405        The node types are defined in cras_utils.CRAS_NODE_TYPES.
406
407        @param output_node_types: A list of output node types.
408                                  None to skip setting.
409        @param input_node_types: A list of input node types.
410                                 None to skip setting.
411
412        """
413        cras_utils.set_selected_node_types(output_node_types, input_node_types)
414
415
416    def get_selected_node_types(self):
417        """Gets the selected output and input node types.
418
419        @returns: A tuple (output_node_types, input_node_types) where each
420                  field is a list of selected node types defined in
421                  cras_utils.CRAS_NODE_TYPES.
422
423        """
424        return cras_utils.get_selected_node_types()
425
426
427    def get_plugged_node_types(self):
428        """Gets the plugged output and input node types.
429
430        @returns: A tuple (output_node_types, input_node_types) where each
431                  field is a list of plugged node types defined in
432                  cras_utils.CRAS_NODE_TYPES.
433
434        """
435        return cras_utils.get_plugged_node_types()
436
437
438    def dump_diagnostics(self, file_path):
439        """Dumps audio diagnostics results to a file.
440
441        @param file_path: The path to dump results.
442
443        """
444        audio_helper.dump_audio_diagnostics(file_path)
445
446
447    def start_counting_signal(self, signal_name):
448        """Starts counting DBus signal from Cras.
449
450        @param signal_name: Signal of interest.
451
452        """
453        if self._counter:
454            raise AudioFacadeNativeError('There is an ongoing counting.')
455        self._counter = cras_dbus_utils.CrasDBusBackgroundSignalCounter()
456        self._counter.start(signal_name)
457
458
459    def stop_counting_signal(self):
460        """Stops counting DBus signal from Cras.
461
462        @returns: Number of signals starting from last start_counting_signal
463                  call.
464
465        """
466        if not self._counter:
467            raise AudioFacadeNativeError('Should start counting signal first')
468        result = self._counter.stop()
469        self._counter = None
470        return result
471
472
473    def wait_for_unexpected_nodes_changed(self, timeout_secs):
474        """Waits for unexpected nodes changed signal.
475
476        @param timeout_secs: Timeout in seconds for waiting.
477
478        """
479        cras_dbus_utils.wait_for_unexpected_nodes_changed(timeout_secs)
480
481
482    @check_arc_resource
483    def start_arc_recording(self):
484        """Starts recording using microphone app in container."""
485        self._arc_resource.microphone.start_microphone_app()
486
487
488    @check_arc_resource
489    def stop_arc_recording(self):
490        """Checks the recording is stopped and gets the recorded path.
491
492        The recording duration of microphone app is fixed, so this method just
493        copies the recorded result from container to a path on Cros device.
494
495        """
496        _, file_path = tempfile.mkstemp(prefix='capture_', suffix='.amr-nb')
497        self._arc_resource.microphone.stop_microphone_app(file_path)
498        return file_path
499
500
501    @check_arc_resource
502    def set_arc_playback_file(self, file_path):
503        """Copies the audio file to be played into container.
504
505        User should call this method to put the file into container before
506        calling start_arc_playback.
507
508        @param file_path: Path to the file to be played on Cros host.
509
510        @returns: Path to the file in container.
511
512        """
513        return self._arc_resource.play_music.set_playback_file(file_path)
514
515
516    @check_arc_resource
517    def start_arc_playback(self, path):
518        """Start playback through Play Music app.
519
520        Before calling this method, user should call set_arc_playback_file to
521        put the file into container.
522
523        @param path: Path to the file in container.
524
525        """
526        self._arc_resource.play_music.start_playback(path)
527
528
529    @check_arc_resource
530    def stop_arc_playback(self):
531        """Stop playback through Play Music app."""
532        self._arc_resource.play_music.stop_playback()
533
534
535class RecorderError(Exception):
536    """Error in Recorder."""
537    pass
538
539
540class Recorder(object):
541    """The class to control recording subprocess.
542
543    Properties:
544        file_path: The path to recorded file. It should be accessed after
545                   stop() is called.
546
547    """
548    def __init__(self):
549        """Initializes a Recorder."""
550        _, self.file_path = tempfile.mkstemp(prefix='capture_', suffix='.raw')
551        self._capture_subprocess = None
552
553
554    def start(self, data_format, pin_device, block_size):
555        """Starts recording.
556
557        Starts recording subprocess. It can be stopped by calling stop().
558
559        @param data_format: A dict containing:
560                            file_type: 'raw'.
561                            sample_format: 'S16_LE' for 16-bit signed integer in
562                                           little-endian.
563                            channel: channel number.
564                            rate: sampling rate.
565        @param pin_device: A integer of device id to record from.
566        @param block_size: The number for frames per callback.
567        """
568        self._capture_subprocess = cmd_utils.popen(
569                cras_utils.capture_cmd(
570                        capture_file=self.file_path, duration=None,
571                        channels=data_format['channel'],
572                        rate=data_format['rate'],
573                        pin_device=pin_device, block_size=block_size))
574
575
576    def stop(self):
577        """Stops recording subprocess."""
578        if self._capture_subprocess.poll() is None:
579            self._capture_subprocess.terminate()
580        else:
581            raise RecorderError(
582                    'Recording process was terminated unexpectedly.')
583
584
585    def cleanup(self):
586        """Cleanup the resources.
587
588        Terminates the recording process if needed.
589
590        """
591        if self._capture_subprocess and self._capture_subprocess.poll() is None:
592            self._capture_subprocess.terminate()
593
594
595class PlayerError(Exception):
596    """Error in Player."""
597    pass
598
599
600class Player(object):
601    """The class to control audio playback subprocess.
602
603    Properties:
604        file_path: The path to the file to play.
605
606    """
607    def __init__(self):
608        """Initializes a Player."""
609        self._playback_subprocess = None
610
611
612    def start(self, file_path, blocking, pin_device, block_size):
613        """Starts playing.
614
615        Starts playing subprocess. It can be stopped by calling stop().
616
617        @param file_path: The path to the file.
618        @param blocking: Blocks this call until playback finishes.
619        @param pin_device: A integer of device id to play on.
620        @param block_size: The number for frames per callback.
621
622        """
623        self._playback_subprocess = cras_utils.playback(
624                blocking, playback_file=file_path, pin_device=pin_device,
625                block_size=block_size)
626
627
628    def stop(self):
629        """Stops playback subprocess."""
630        cmd_utils.kill_or_log_returncode(self._playback_subprocess)
631
632
633    def cleanup(self):
634        """Cleanup the resources.
635
636        Terminates the playback process if needed.
637
638        """
639        self.stop()
640
641
642class ListenerError(Exception):
643    """Error in Listener."""
644    pass
645
646
647class Listener(object):
648    """The class to control listening subprocess.
649
650    Properties:
651        file_path: The path to recorded file. It should be accessed after
652                   stop() is called.
653
654    """
655    def __init__(self):
656        """Initializes a Listener."""
657        _, self.file_path = tempfile.mkstemp(prefix='listen_', suffix='.raw')
658        self._capture_subprocess = None
659
660
661    def start(self, data_format):
662        """Starts listening.
663
664        Starts listening subprocess. It can be stopped by calling stop().
665
666        @param data_format: A dict containing:
667                            file_type: 'raw'.
668                            sample_format: 'S16_LE' for 16-bit signed integer in
669                                           little-endian.
670                            channel: channel number.
671                            rate: sampling rate.
672
673        @raises: ListenerError: If listening subprocess is terminated
674                 unexpectedly.
675
676        """
677        self._capture_subprocess = cmd_utils.popen(
678                cras_utils.listen_cmd(
679                        capture_file=self.file_path, duration=None,
680                        channels=data_format['channel'],
681                        rate=data_format['rate']))
682
683
684    def stop(self):
685        """Stops listening subprocess."""
686        if self._capture_subprocess.poll() is None:
687            self._capture_subprocess.terminate()
688        else:
689            raise ListenerError(
690                    'Listening process was terminated unexpectedly.')
691
692
693    def cleanup(self):
694        """Cleanup the resources.
695
696        Terminates the listening process if needed.
697
698        """
699        if self._capture_subprocess and self._capture_subprocess.poll() is None:
700            self._capture_subprocess.terminate()
701