• 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"""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        (ids.CrosIds.USBOUT, ids.ChameleonIds.USBIN):
104                audio_widget_link.USBToChameleonWidgetLink,
105        (ids.ChameleonIds.USBOUT, ids.CrosIds.USBIN):
106                audio_widget_link.USBToCrosWidgetLink,
107        # TODO(cychiang): Add link for other widget pairs.
108    }
109
110    def __init__(self, cros_host):
111        """Initializes an AudioLinkFactory.
112
113        @param cros_host: A CrosHost object to access Cros device.
114
115        """
116        # There are two audio buses on audio board. Initializes these links
117        # to None. They may be changed to objects of AudioBusLink's subclass.
118        self._audio_bus_links = {1: None, 2: None}
119        self._cros_host = cros_host
120        self._chameleon_board = cros_host.chameleon
121        self._audio_board = self._chameleon_board.get_audio_board()
122        self._bluetooth_device = None
123        self._usb_ctrl = None
124
125
126    def _acquire_audio_bus_index(self):
127        """Acquires an available audio bus index that is not occupied yet.
128
129        @returns: A number.
130
131        @raises: AudioLinkFactoryError if there is no available
132                 audio bus.
133        """
134        for index, bus in self._audio_bus_links.iteritems():
135            if not (bus and bus.occupied):
136                return index
137
138        raise AudioLinkFactoryError('No available audio bus')
139
140
141    def create_link(self, source, sink):
142        """Creates a widget link for two audio widgets.
143
144        @param source: An AudioWidget.
145        @param sink: An AudioWidget.
146
147        @returns: An object of WidgetLink's subclass.
148
149        @raises: AudioLinkFactoryError if there is no link between
150            source and sink.
151
152        """
153        # Finds the available link types from LINK_TABLE.
154        link_type = self.LINK_TABLE.get((source.port_id, sink.port_id), None)
155        if not link_type:
156            raise AudioLinkFactoryError(
157                    'No supported link between %s and %s' % (
158                            source.port_id, sink.port_id))
159
160        # There is only one dedicated HDMI cable, just use it.
161        if link_type == audio_widget_link.HDMIWidgetLink:
162            link = audio_widget_link.HDMIWidgetLink()
163
164        # Acquires audio bus if there is available bus.
165        # Creates a bus of AudioBusLink's subclass that is more
166        # specific than AudioBusLink.
167        # Controls this link using AudioBus object obtained from AudioBoard
168        # object.
169        elif issubclass(link_type, audio_widget_link.AudioBusLink):
170            bus_index = self._acquire_audio_bus_index()
171            link = link_type(self._audio_board.get_audio_bus(bus_index))
172            self._audio_bus_links[bus_index] = link
173        elif issubclass(link_type, audio_widget_link.BluetoothWidgetLink):
174            # To connect bluetooth adapter on Cros device to bluetooth module on
175            # chameleon board, we need to access bluetooth adapter on Cros host
176            # using BluetoothDevice, and access bluetooth module on
177            # audio board using BluetoothController. Finally, the MAC address
178            # of bluetooth module is queried through chameleon_info because
179            # it is not probeable on Chameleon board.
180
181            # Initializes a BluetoothDevice object if needed. And reuse this
182            # object for future bluetooth link usage.
183            if not self._bluetooth_device:
184                self._bluetooth_device = bluetooth_device.BluetoothDevice(
185                        self._cros_host)
186
187            link = link_type(
188                    self._bluetooth_device,
189                    self._audio_board.get_bluetooth_controller(),
190                    chameleon_info.get_bluetooth_mac_address(
191                            self._chameleon_board))
192        elif issubclass(link_type, audio_widget_link.USBWidgetLink):
193            # Aside from managing connection between USB audio gadget driver on
194            # Chameleon with Cros device, USBWidgetLink also handles changing
195            # the gadget driver's configurations, through the USBController that
196            # is passed to it at initialization.
197            if not self._usb_ctrl:
198                self._usb_ctrl = self._chameleon_board.get_usb_controller()
199
200            link = link_type(self._usb_ctrl)
201        else:
202            raise NotImplementedError('Link %s is not implemented' % link_type)
203
204        return link
205
206
207class AudioWidgetFactoryError(Exception):
208    """Error in AudioWidgetFactory."""
209    pass
210
211
212class AudioWidgetFactory(object):
213    """
214    This class provides methods to create widgets and binder of widgets.
215    User can use binder to setup audio paths. User can use widgets to control
216    record/playback on different ports on Cros device or Chameleon.
217
218    Properties:
219        _audio_facade: An AudioFacadeRemoteAdapter to access Cros device audio
220                       functionality. This is created by the
221                       'factory' argument passed to the constructor.
222        _chameleon_board: A ChameleonBoard object to access Chameleon
223                          functionality.
224        _link_factory: An AudioLinkFactory that creates link for widgets.
225
226    """
227    def __init__(self, factory, cros_host):
228        """Initializes a AudioWidgetFactory
229
230        @param factory: A facade factory to access Cros device functionality.
231                        Currently only audio facade is used, but we can access
232                        other functionalities including display and video by
233                        facades created by this facade factory.
234        @param cros_host: A CrosHost object to access Cros device.
235
236        """
237        self._audio_facade = factory.create_audio_facade()
238        self._usb_facade = factory.create_usb_facade()
239        self._cros_host = cros_host
240        self._chameleon_board = cros_host.chameleon
241        self._link_factory = AudioLinkFactory(cros_host)
242
243
244    def create_widget(self, port_id):
245        """Creates a AudioWidget given port id string.
246
247        @param port_id: A port id string defined in chameleon_audio_ids.
248
249        @returns: An AudioWidget that is actually a
250                  (Chameleon/Cros/Peripheral)(Input/Output)Widget.
251
252        """
253        def _create_chameleon_handler(audio_port):
254            """Creates a ChameleonWidgetHandler for a given AudioPort.
255
256            @param audio_port: An AudioPort object.
257
258            @returns: A Chameleon(Input/Output)WidgetHandler depending on
259                      role of audio_port.
260
261            """
262            if audio_port.role == 'sink':
263                return audio_widget.ChameleonInputWidgetHandler(
264                        self._chameleon_board, audio_port.interface)
265            else:
266                if audio_port.port_id == ids.ChameleonIds.LINEOUT:
267                    return audio_widget.ChameleonLineOutOutputWidgetHandler(
268                            self._chameleon_board, audio_port.interface)
269                else:
270                    return audio_widget.ChameleonOutputWidgetHandler(
271                            self._chameleon_board, audio_port.interface)
272
273
274        def _create_cros_handler(audio_port):
275            """Creates a CrosWidgetHandler for a given AudioPort.
276
277            @param audio_port: An AudioPort object.
278
279            @returns: A Cros(Input/Output)WidgetHandler depending on
280                      role of audio_port.
281
282            """
283            is_usb = audio_port.port_id in [ids.CrosIds.USBIN,
284                                            ids.CrosIds.USBOUT]
285            audio_board = self._chameleon_board.get_audio_board()
286            if audio_board:
287                jack_plugger = audio_board.get_jack_plugger()
288            else:
289                jack_plugger = None
290
291            if is_usb:
292                plug_handler = audio_widget.USBPlugHandler(self._usb_facade)
293            elif jack_plugger:
294                plug_handler = audio_widget.JackPluggerPlugHandler(jack_plugger)
295            else:
296                plug_handler = audio_widget.DummyPlugHandler()
297
298            if audio_port.role == 'sink':
299                if is_usb:
300                    return audio_widget.CrosUSBInputWidgetHandler(
301                            self._audio_facade, plug_handler)
302                else:
303                    return audio_widget.CrosInputWidgetHandler(
304                            self._audio_facade, plug_handler)
305            else:
306                return audio_widget.CrosOutputWidgetHandler(self._audio_facade,
307                                                            plug_handler)
308
309
310        def _create_audio_widget(audio_port, handler):
311            """Creates an AudioWidget for given AudioPort using WidgetHandler.
312
313            Creates an AudioWidget with the role of audio_port. Put
314            the widget handler into the widget so the widget can handle
315            action requests.
316
317            @param audio_port: An AudioPort object.
318            @param handler: A WidgetHandler object.
319
320            @returns: An Audio(Input/Output)Widget depending on
321                      role of audio_port.
322
323            @raises: AudioWidgetFactoryError if fail to create widget.
324
325            """
326            if audio_port.host in ['Chameleon', 'Cros']:
327                if audio_port.role == 'sink':
328                    return audio_widget.AudioInputWidget(audio_port, handler)
329                else:
330                    return audio_widget.AudioOutputWidget(audio_port, handler)
331            elif audio_port.host == 'Peripheral':
332                return audio_widget.PeripheralWidget(audio_port, handler)
333            else:
334                raise AudioWidgetFactoryError(
335                        'The host %s is not valid' % audio_port.host)
336
337
338        audio_port = AudioPort(port_id)
339        if audio_port.host == 'Chameleon':
340            handler = _create_chameleon_handler(audio_port)
341        elif audio_port.host == 'Cros':
342            handler = _create_cros_handler(audio_port)
343        elif audio_port.host == 'Peripheral':
344            handler = audio_widget.PeripheralWidgetHandler()
345
346        return _create_audio_widget(audio_port, handler)
347
348
349    def _create_widget_binder(self, source, sink):
350        """Creates a WidgetBinder for two AudioWidgets.
351
352        @param source: An AudioWidget.
353        @param sink: An AudioWidget.
354
355        @returns: A WidgetBinder object.
356
357        """
358        return audio_widget_link.WidgetBinder(
359                source, self._link_factory.create_link(source, sink), sink)
360
361
362    def create_binder(self, *widgets):
363        """Creates a WidgetBinder or a WidgetChainBinder for AudioWidgets.
364
365        @param widgets: A list of widgets that should be linked in a chain.
366
367        @returns: A WidgetBinder for two widgets. A WidgetBinderChain object
368                  for three or more widgets.
369
370        """
371        if len(widgets) == 2:
372            return self._create_widget_binder(widgets[0], widgets[1])
373        binders = []
374        for index in xrange(len(widgets) - 1):
375            binders.append(
376                    self._create_widget_binder(
377                            widgets[index],  widgets[index + 1]))
378
379        return audio_widget_link.WidgetBinderChain(binders)
380
381
382def compare_recorded_result(golden_file, recorder, method, parameters=None):
383    """Check recoded audio in a AudioInputWidget against a golden file.
384
385    Compares recorded data with golden data by cross correlation method.
386    Refer to audio_helper.compare_data for details of comparison.
387
388    @param golden_file: An AudioTestData object that serves as golden data.
389    @param recorder: An AudioInputWidget that has recorded some audio data.
390    @param method: The method to compare recorded result. Currently,
391                   'correlation' and 'frequency' are supported.
392    @param parameters: A dict containing parameters for method.
393
394    @returns: True if the recorded data and golden data are similar enough.
395
396    """
397    logging.info('Comparing recorded data with golden file %s ...',
398                 golden_file.path)
399    return audio_helper.compare_data(
400            golden_file.get_binary(), golden_file.data_format,
401            recorder.get_binary(), recorder.data_format, recorder.channel_map,
402            method, parameters)
403
404
405@contextmanager
406def bind_widgets(binder):
407    """Context manager for widget binders.
408
409    Connects widgets in the beginning. Disconnects widgets and releases binder
410    in the end.
411
412    @param binder: A WidgetBinder object or a WidgetBinderChain object.
413
414    E.g. with bind_widgets(binder):
415             do something on widget.
416
417    """
418    try:
419        binder.connect()
420        yield
421    finally:
422        binder.disconnect()
423        binder.release()
424