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