chameleon_audio_helper.py revision 564ed0248723e84e1634cd2a39cf0a2da98292ba
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._usb_facade = factory.create_usb_facade()
229        self._cros_host = cros_host
230        self._chameleon_board = cros_host.chameleon
231        self._link_factory = AudioLinkFactory(cros_host)
232
233
234    def create_widget(self, port_id):
235        """Creates a AudioWidget given port id string.
236
237        @param port_id: A port id string defined in chameleon_audio_ids.
238
239        @returns: An AudioWidget that is actually a
240                  (Chameleon/Cros/Peripheral)(Input/Output)Widget.
241
242        """
243        def _create_chameleon_handler(audio_port):
244            """Creates a ChameleonWidgetHandler for a given AudioPort.
245
246            @param audio_port: An AudioPort object.
247
248            @returns: A Chameleon(Input/Output)WidgetHandler depending on
249                      role of audio_port.
250
251            """
252            if audio_port.role == 'sink':
253                return audio_widget.ChameleonInputWidgetHandler(
254                        self._chameleon_board, audio_port.interface)
255            else:
256                return audio_widget.ChameleonOutputWidgetHandler(
257                        self._chameleon_board, audio_port.interface)
258
259
260        def _create_cros_handler(audio_port):
261            """Creates a CrosWidgetHandler for a given AudioPort.
262
263            @param audio_port: An AudioPort object.
264
265            @returns: A Cros(Input/Output)WidgetHandler depending on
266                      role of audio_port.
267
268            """
269            if audio_port.role == 'sink':
270                return audio_widget.CrosInputWidgetHandler(self._audio_facade)
271            else:
272                return audio_widget.CrosOutputWidgetHandler(self._audio_facade)
273
274
275        def _create_audio_widget(audio_port, handler):
276            """Creates an AudioWidget for given AudioPort using WidgetHandler.
277
278            Creates an AudioWidget with the role of audio_port. Put
279            the widget handler into the widget so the widget can handle
280            action requests.
281
282            @param audio_port: An AudioPort object.
283            @param handler: A WidgetHandler object.
284
285            @returns: An Audio(Input/Output)Widget depending on
286                      role of audio_port.
287
288            @raises: AudioWidgetFactoryError if fail to create widget.
289
290            """
291            if audio_port.host in ['Chameleon', 'Cros']:
292                if audio_port.role == 'sink':
293                    return audio_widget.AudioInputWidget(audio_port, handler)
294                else:
295                    return audio_widget.AudioOutputWidget(audio_port, handler)
296            elif audio_port.host == 'Peripheral':
297                return audio_widget.PeripheralWidget(audio_port, handler)
298            else:
299                raise AudioWidgetFactoryError(
300                        'The host %s is not valid' % audio_port.host)
301
302
303        audio_port = AudioPort(port_id)
304        if audio_port.host == 'Chameleon':
305            handler = _create_chameleon_handler(audio_port)
306        elif audio_port.host == 'Cros':
307            handler = _create_cros_handler(audio_port)
308        elif audio_port.host == 'Peripheral':
309            handler = audio_widget.PeripheralWidgetHandler()
310
311        return _create_audio_widget(audio_port, handler)
312
313
314    def _create_widget_binder(self, source, sink):
315        """Creates a WidgetBinder for two AudioWidgets.
316
317        @param source: An AudioWidget.
318        @param sink: An AudioWidget.
319
320        @returns: A WidgetBinder object.
321
322        """
323        return audio_widget_link.WidgetBinder(
324                source, self._link_factory.create_link(source, sink), sink)
325
326
327    def create_binder(self, *widgets):
328        """Creates a WidgetBinder or a WidgetChainBinder for AudioWidgets.
329
330        @param widgets: A list of widgets that should be linked in a chain.
331
332        @returns: A WidgetBinder for two widgets. A WidgetBinderChain object
333                  for three or more widgets.
334
335        """
336        if len(widgets) == 2:
337            return self._create_widget_binder(widgets[0], widgets[1])
338        binders = []
339        for index in xrange(len(widgets) - 1):
340            binders.append(
341                    self._create_widget_binder(
342                            widgets[index],  widgets[index + 1]))
343
344        return audio_widget_link.WidgetBinderChain(binders)
345
346
347def compare_recorded_result(golden_file, recorder, method):
348    """Check recoded audio in a AudioInputWidget against a golden file.
349
350    Compares recorded data with golden data by cross correlation method.
351    Refer to audio_helper.compare_data for details of comparison.
352
353    @param golden_file: An AudioTestData object that serves as golden data.
354    @param recorder: An AudioInputWidget that has recorded some audio data.
355    @param method: The method to compare recorded result. Currently,
356                   'correlation' and 'frequency' are supported.
357
358    @returns: True if the recorded data and golden data are similar enough.
359
360    """
361    logging.info('Comparing recorded data with golden file %s ...',
362                 golden_file.path)
363    return audio_helper.compare_data(
364            golden_file.get_binary(), golden_file.data_format,
365            recorder.get_binary(), recorder.data_format, recorder.channel_map,
366            method)
367
368
369@contextmanager
370def bind_widgets(binder):
371    """Context manager for widget binders.
372
373    Connects widgets in the beginning. Disconnects widgets and releases binder
374    in the end.
375
376    @param binder: A WidgetBinder object or a WidgetBinderChain object.
377
378    E.g. with bind_widgets(binder):
379             do something on widget.
380
381    """
382    try:
383        binder.connect()
384        yield
385    finally:
386        binder.disconnect()
387        binder.release()
388