chameleon_audio_helper.py revision d690b0a62b7b5e3732bd009eaa29369cab786c21
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"""This module provides the framework for audio tests using chameleon."""
6
7import logging
8from contextlib import contextmanager
9
10from autotest_lib.client.cros.audio import audio_helper
11from autotest_lib.client.cros.chameleon import audio_widget
12from autotest_lib.client.cros.chameleon import audio_widget_link
13from autotest_lib.server.cros.bluetooth import bluetooth_device
14from autotest_lib.client.cros.chameleon import chameleon_audio_ids as ids
15from autotest_lib.client.cros.chameleon import chameleon_info
16
17
18class AudioPort(object):
19    """
20    This class abstracts an audio port in audio test framework. A port is
21    identified by its host and interface. Available hosts and interfaces
22    are listed in chameleon_audio_ids.
23
24    Properties:
25        port_id: The port id defined in chameleon_audio_ids.
26        host: The host of this audio port, e.g. 'Chameleon', 'Cros',
27              'Peripheral'.
28        interface: The interface of this audio port, e.g. 'HDMI', 'Headphone'.
29        role: The role of this audio port, that is, 'source' or
30              'sink'. Note that bidirectional interface like 3.5mm
31              jack is separated to two interfaces 'Headphone' and
32             'External Mic'.
33
34    """
35    def __init__(self, port_id):
36        """Initialize an AudioPort with port id string.
37
38        @param port_id: A port id string defined in chameleon_audio_ids.
39
40        """
41        logging.debug('Creating AudioPort with port_id: %s', port_id)
42        self.port_id = port_id
43        self.host = ids.get_host(port_id)
44        self.interface = ids.get_interface(port_id)
45        self.role = ids.get_role(port_id)
46        logging.debug('Created AudioPort: %s', self)
47
48
49    def __str__(self):
50        """String representation of audio port.
51
52        @returns: The string representation of audio port which is composed by
53                  host, interface, and role.
54
55        """
56        return '( %s | %s | %s )' % (
57                self.host, self.interface, self.role)
58
59
60class AudioLinkFactoryError(Exception):
61    """Error in AudioLinkFactory."""
62    pass
63
64
65class AudioLinkFactory(object):
66    """
67    This class provides method to create link that connects widgets.
68    This is used by AudioWidgetFactory when user wants to create binder for
69    widgets.
70
71    Properties:
72        _audio_bus_links: A dict containing mapping from index number
73                          to object of AudioBusLink's subclass.
74        _audio_board: An AudioBoard object to access Chameleon
75                      audio board functionality.
76
77    """
78
79    # Maps pair of widgets to widget link of different type.
80    LINK_TABLE = {
81        (ids.CrosIds.HDMI, ids.ChameleonIds.HDMI):
82                audio_widget_link.HDMIWidgetLink,
83        (ids.CrosIds.HEADPHONE, ids.ChameleonIds.LINEIN):
84                audio_widget_link.AudioBusToChameleonLink,
85        (ids.ChameleonIds.LINEOUT, ids.CrosIds.EXTERNAL_MIC):
86                audio_widget_link.AudioBusToCrosLink,
87        (ids.ChameleonIds.LINEOUT, ids.PeripheralIds.SPEAKER):
88                audio_widget_link.AudioBusChameleonToPeripheralLink,
89        (ids.PeripheralIds.MIC, ids.ChameleonIds.LINEIN):
90                audio_widget_link.AudioBusToChameleonLink,
91        (ids.PeripheralIds.BLUETOOTH_DATA_RX,
92         ids.ChameleonIds.LINEIN):
93                audio_widget_link.AudioBusToChameleonLink,
94        (ids.ChameleonIds.LINEOUT,
95         ids.PeripheralIds.BLUETOOTH_DATA_TX):
96                audio_widget_link.AudioBusChameleonToPeripheralLink,
97        (ids.CrosIds.BLUETOOTH_HEADPHONE,
98         ids.PeripheralIds.BLUETOOTH_DATA_RX):
99                audio_widget_link.BluetoothHeadphoneWidgetLink,
100        (ids.PeripheralIds.BLUETOOTH_DATA_TX,
101         ids.CrosIds.BLUETOOTH_MIC):
102                audio_widget_link.BluetoothMicWidgetLink,
103        # TODO(cychiang): Add link for other widget pairs.
104    }
105
106    def __init__(self, cros_host):
107        """Initializes an AudioLinkFactory.
108
109        @param cros_host: A CrosHost object to access Cros device.
110
111        """
112        # There are two audio buses on audio board. Initializes these links
113        # to None. They may be changed to objects of AudioBusLink's subclass.
114        self._audio_bus_links = {1: None, 2: None}
115        self._cros_host = cros_host
116        self._chameleon_board = cros_host.chameleon
117        self._audio_board = self._chameleon_board.get_audio_board()
118        self._bluetooth_device = None
119
120
121    def _acquire_audio_bus_index(self):
122        """Acquires an available audio bus index that is not occupied yet.
123
124        @returns: A number.
125
126        @raises: AudioLinkFactoryError if there is no available
127                 audio bus.
128        """
129        for index, bus in self._audio_bus_links.iteritems():
130            if not (bus and bus.occupied):
131                return index
132
133        raise AudioLinkFactoryError('No available audio bus')
134
135
136    def create_link(self, source, sink):
137        """Creates a widget link for two audio widgets.
138
139        @param source: An AudioWidget.
140        @param sink: An AudioWidget.
141
142        @returns: An object of WidgetLink's subclass.
143
144        @raises: AudioLinkFactoryError if there is no link between
145            source and sink.
146
147        """
148        # Finds the available link types from LINK_TABLE.
149        link_type = self.LINK_TABLE.get((source.port_id, sink.port_id), None)
150        if not link_type:
151            raise AudioLinkFactoryError(
152                    'No supported link between %s and %s' % (
153                            source.port_id, sink.port_id))
154
155        # There is only one dedicated HDMI cable, just use it.
156        if link_type == audio_widget_link.HDMIWidgetLink:
157            link = audio_widget_link.HDMIWidgetLink()
158
159        # Acquires audio bus if there is available bus.
160        # Creates a bus of AudioBusLink's subclass that is more
161        # specific than AudioBusLink.
162        # Controls this link using AudioBus object obtained from AudioBoard
163        # object.
164        # Plugs/unplugs 3.5mm jack using AudioJackPlugger object obtained from
165        # AudioBoard object.
166        elif issubclass(link_type, audio_widget_link.AudioBusLink):
167            bus_index = self._acquire_audio_bus_index()
168            link = link_type(
169                    self._audio_board.get_audio_bus(bus_index),
170                    self._audio_board.get_jack_plugger())
171            self._audio_bus_links[bus_index] = link
172        elif issubclass(link_type, audio_widget_link.BluetoothWidgetLink):
173            # To connect bluetooth adapter on Cros device to bluetooth module on
174            # chameleon board, we need to access bluetooth adapter on Cros host
175            # using BluetoothDevice, and access bluetooth module on
176            # audio board using BluetoothController. Finally, the MAC address
177            # of bluetooth module is queried through chameleon_info because
178            # it is not probeable on Chameleon board.
179
180            # Initializes a BluetoothDevice object if needed. And reuse this
181            # object for future bluetooth link usage.
182            if not self._bluetooth_device:
183                self._bluetooth_device = bluetooth_device.BluetoothDevice(
184                        self._cros_host)
185
186            link = link_type(
187                    self._bluetooth_device,
188                    self._audio_board.get_bluetooth_controller(),
189                    chameleon_info.get_bluetooth_mac_address(
190                            self._chameleon_board))
191        else:
192            raise NotImplementedError('Link %s is not implemented' % link_type)
193
194        return link
195
196
197class AudioWidgetFactoryError(Exception):
198    """Error in AudioWidgetFactory."""
199    pass
200
201
202class AudioWidgetFactory(object):
203    """
204    This class provides methods to create widgets and binder of widgets.
205    User can use binder to setup audio paths. User can use widgets to control
206    record/playback on different ports on Cros device or Chameleon.
207
208    Properties:
209        _audio_facade: An AudioFacadeRemoteAdapter to access Cros device audio
210                       functionality. This is created by the
211                       'factory' argument passed to the constructor.
212        _chameleon_board: A ChameleonBoard object to access Chameleon
213                          functionality.
214        _link_factory: An AudioLinkFactory that creates link for widgets.
215
216    """
217    def __init__(self, factory, cros_host):
218        """Initializes a AudioWidgetFactory
219
220        @param factory: A facade factory to access Cros device functionality.
221                        Currently only audio facade is used, but we can access
222                        other functionalities including display and video by
223                        facades created by this facade factory.
224        @param cros_host: A CrosHost object to access Cros device.
225
226        """
227        self._audio_facade = factory.create_audio_facade()
228        self._cros_host = cros_host
229        self._chameleon_board = cros_host.chameleon
230        self._link_factory = AudioLinkFactory(cros_host)
231
232
233    def create_widget(self, port_id):
234        """Creates a AudioWidget given port id string.
235
236        @param port_id: A port id string defined in chameleon_audio_ids.
237
238        @returns: An AudioWidget that is actually a
239                  (Chameleon/Cros/Peripheral)(Input/Output)Widget.
240
241        """
242        def _create_chameleon_handler(audio_port):
243            """Creates a ChameleonWidgetHandler for a given AudioPort.
244
245            @param audio_port: An AudioPort object.
246
247            @returns: A Chameleon(Input/Output)WidgetHandler depending on
248                      role of audio_port.
249
250            """
251            if audio_port.role == 'sink':
252                return audio_widget.ChameleonInputWidgetHandler(
253                        self._chameleon_board, audio_port.interface)
254            else:
255                return audio_widget.ChameleonOutputWidgetHandler(
256                        self._chameleon_board, audio_port.interface)
257
258
259        def _create_cros_handler(audio_port):
260            """Creates a CrosWidgetHandler for a given AudioPort.
261
262            @param audio_port: An AudioPort object.
263
264            @returns: A Cros(Input/Output)WidgetHandler depending on
265                      role of audio_port.
266
267            """
268            if audio_port.role == 'sink':
269                return audio_widget.CrosInputWidgetHandler(self._audio_facade)
270            else:
271                return audio_widget.CrosOutputWidgetHandler(self._audio_facade)
272
273
274        def _create_audio_widget(audio_port, handler):
275            """Creates an AudioWidget for given AudioPort using WidgetHandler.
276
277            Creates an AudioWidget with the role of audio_port. Put
278            the widget handler into the widget so the widget can handle
279            action requests.
280
281            @param audio_port: An AudioPort object.
282            @param handler: A WidgetHandler object.
283
284            @returns: An Audio(Input/Output)Widget depending on
285                      role of audio_port.
286
287            @raises: AudioWidgetFactoryError if fail to create widget.
288
289            """
290            if audio_port.host in ['Chameleon', 'Cros']:
291                if audio_port.role == 'sink':
292                    return audio_widget.AudioInputWidget(audio_port, handler)
293                else:
294                    return audio_widget.AudioOutputWidget(audio_port, handler)
295            elif audio_port.host == 'Peripheral':
296                return audio_widget.PeripheralWidget(audio_port, handler)
297            else:
298                raise AudioWidgetFactoryError(
299                        'The host %s is not valid' % audio_port.host)
300
301
302        audio_port = AudioPort(port_id)
303        if audio_port.host == 'Chameleon':
304            handler = _create_chameleon_handler(audio_port)
305        elif audio_port.host == 'Cros':
306            handler = _create_cros_handler(audio_port)
307        elif audio_port.host == 'Peripheral':
308            handler = audio_widget.PeripheralWidgetHandler()
309
310        return _create_audio_widget(audio_port, handler)
311
312
313    def _create_widget_binder(self, source, sink):
314        """Creates a WidgetBinder for two AudioWidgets.
315
316        @param source: An AudioWidget.
317        @param sink: An AudioWidget.
318
319        @returns: A WidgetBinder object.
320
321        """
322        return audio_widget_link.WidgetBinder(
323                source, self._link_factory.create_link(source, sink), sink)
324
325
326    def create_binder(self, *widgets):
327        """Creates a WidgetBinder or a WidgetChainBinder for AudioWidgets.
328
329        @param widgets: A list of widgets that should be linked in a chain.
330
331        @returns: A WidgetBinder for two widgets. A WidgetBinderChain object
332                  for three or more widgets.
333
334        """
335        if len(widgets) == 2:
336            return self._create_widget_binder(widgets[0], widgets[1])
337        binders = []
338        for index in xrange(len(widgets) - 1):
339            binders.append(
340                    self._create_widget_binder(
341                            widgets[index],  widgets[index + 1]))
342
343        return audio_widget_link.WidgetBinderChain(binders)
344
345
346def compare_recorded_result(golden_file, recorder, method):
347    """Check recoded audio in a AudioInputWidget against a golden file.
348
349    Compares recorded data with golden data by cross correlation method.
350    Refer to audio_helper.compare_data for details of comparison.
351
352    @param golden_file: An AudioTestData object that serves as golden data.
353    @param recorder: An AudioInputWidget that has recorded some audio data.
354    @param method: The method to compare recorded result. Currently,
355                   'correlation' and 'frequency' are supported.
356
357    @returns: True if the recorded data and golden data are similar enough.
358
359    """
360    logging.info('Comparing recorded data with golden file %s ...',
361                 golden_file.path)
362    return audio_helper.compare_data(
363            golden_file.get_binary(), golden_file.data_format,
364            recorder.get_binary(), recorder.data_format, recorder.channel_map,
365            method)
366
367
368@contextmanager
369def bind_widgets(binder):
370    """Context manager for widget binders.
371
372    Connects widgets in the beginning. Disconnects widgets and releases binder
373    in the end.
374
375    @param binder: A WidgetBinder object or a WidgetBinderChain object.
376
377    E.g. with bind_widgets(binder):
378             do something on widget.
379
380    """
381    try:
382        binder.connect()
383        yield
384    finally:
385        binder.disconnect()
386        binder.release()
387