• 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_arc
13from autotest_lib.client.cros.chameleon import audio_widget_link
14from autotest_lib.server.cros.bluetooth import bluetooth_device
15from autotest_lib.client.cros.chameleon import chameleon_audio_ids as ids
16from autotest_lib.client.cros.chameleon import chameleon_info
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(self._cros_host)
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.
178
179            # Initializes a BluetoothDevice object if needed. And reuse this
180            # object for future bluetooth link usage.
181            if not self._bluetooth_device:
182                self._bluetooth_device = bluetooth_device.BluetoothDevice(
183                        self._cros_host)
184            link = link_type(
185                    self._bluetooth_device,
186                    self._chameleon_board.get_bluetooth_ref_controller(),
187                    self._chameleon_board.get_bluetooth_a2dp_sink().GetLocalBluetoothAddress())
188        elif issubclass(link_type, audio_widget_link.USBWidgetLink):
189            # Aside from managing connection between USB audio gadget driver on
190            # Chameleon with Cros device, USBWidgetLink also handles changing
191            # the gadget driver's configurations, through the USBController that
192            # is passed to it at initialization.
193            if not self._usb_ctrl:
194                self._usb_ctrl = self._chameleon_board.get_usb_controller()
195
196            link = link_type(self._usb_ctrl)
197        else:
198            raise NotImplementedError('Link %s is not implemented' % link_type)
199
200        return link
201
202
203class AudioWidgetFactoryError(Exception):
204    """Error in AudioWidgetFactory."""
205    pass
206
207
208class AudioWidgetFactory(object):
209    """
210    This class provides methods to create widgets and binder of widgets.
211    User can use binder to setup audio paths. User can use widgets to control
212    record/playback on different ports on Cros device or Chameleon.
213
214    Properties:
215        _audio_facade: An AudioFacadeRemoteAdapter to access Cros device audio
216                       functionality. This is created by the
217                       'factory' argument passed to the constructor.
218        _display_facade: A DisplayFacadeRemoteAdapter to access Cros device
219                         display functionality. This is created by the
220                         'factory' argument passed to the constructor.
221        _system_facade: A SystemFacadeRemoteAdapter to access Cros device
222                         system functionality. This is created by the
223                         'factory' argument passed to the constructor.
224        _chameleon_board: A ChameleonBoard object to access Chameleon
225                          functionality.
226        _link_factory: An AudioLinkFactory that creates link for widgets.
227
228    """
229    def __init__(self, factory, cros_host):
230        """Initializes a AudioWidgetFactory
231
232        @param factory: A facade factory to access Cros device functionality.
233                        Currently only audio facade is used, but we can access
234                        other functionalities including display and video by
235                        facades created by this facade factory.
236        @param cros_host: A CrosHost object to access Cros device.
237
238        """
239        self._audio_facade = factory.create_audio_facade()
240        self._display_facade = factory.create_display_facade()
241        self._system_facade = factory.create_system_facade()
242        self._usb_facade = factory.create_usb_facade()
243        self._cros_host = cros_host
244        self._chameleon_board = cros_host.chameleon
245        self._link_factory = AudioLinkFactory(cros_host)
246
247
248    def create_widget(self, port_id, use_arc=False):
249        """Creates a AudioWidget given port id string.
250
251        @param port_id: A port id string defined in chameleon_audio_ids.
252        @param use_arc: For Cros widget, select if audio path exercises ARC.
253                        Currently only input widget is supported.
254
255        @returns: An AudioWidget that is actually a
256                  (Chameleon/Cros/Peripheral)(Input/Output)Widget.
257
258        """
259        def _create_chameleon_handler(audio_port):
260            """Creates a ChameleonWidgetHandler for a given AudioPort.
261
262            @param audio_port: An AudioPort object.
263
264            @returns: A Chameleon(Input/Output)WidgetHandler depending on
265                      role of audio_port.
266
267            """
268            if audio_port.role == 'sink':
269                if audio_port.port_id == ids.ChameleonIds.HDMI:
270                    return audio_widget.ChameleonHDMIInputWidgetHandler(
271                            self._chameleon_board, audio_port.interface,
272                            self._display_facade)
273                else:
274                    return audio_widget.ChameleonInputWidgetHandler(
275                            self._chameleon_board, audio_port.interface)
276            else:
277                if audio_port.port_id == ids.ChameleonIds.LINEOUT:
278                    return audio_widget.ChameleonLineOutOutputWidgetHandler(
279                            self._chameleon_board, audio_port.interface)
280                else:
281                    return audio_widget.ChameleonOutputWidgetHandler(
282                            self._chameleon_board, audio_port.interface)
283
284
285        def _create_cros_handler(audio_port):
286            """Creates a CrosWidgetHandler for a given AudioPort.
287
288            @param audio_port: An AudioPort object.
289
290            @returns: A Cros(Input/Output)WidgetHandler depending on
291                      role of audio_port.
292
293            """
294            is_usb = audio_port.port_id in [ids.CrosIds.USBIN,
295                                            ids.CrosIds.USBOUT]
296            is_audio_jack = audio_port.port_id in [ids.CrosIds.HEADPHONE,
297                                                   ids.CrosIds.EXTERNAL_MIC]
298            is_internal_mic = audio_port.port_id == ids.CrosIds.INTERNAL_MIC
299            is_hotwording = audio_port.port_id == ids.CrosIds.HOTWORDING
300
301            # Determines the plug handler to be used.
302            # By default, the plug handler is DummyPlugHandler.
303            # If the port uses audio jack, and there is jack plugger available
304            # through audio board, then JackPluggerPlugHandler should be used.
305            audio_board = self._chameleon_board.get_audio_board()
306            if audio_board:
307                jack_plugger = audio_board.get_jack_plugger()
308            else:
309                jack_plugger = None
310
311            if jack_plugger and is_audio_jack:
312                plug_handler = audio_widget.JackPluggerPlugHandler(jack_plugger)
313            else:
314                plug_handler = audio_widget.DummyPlugHandler()
315
316            if audio_port.role == 'sink':
317                if use_arc:
318                    return audio_widget_arc.CrosInputWidgetARCHandler(
319                            self._audio_facade, plug_handler)
320                elif is_usb:
321                    return audio_widget.CrosUSBInputWidgetHandler(
322                            self._audio_facade, plug_handler)
323                elif is_internal_mic:
324                    return audio_widget.CrosIntMicInputWidgetHandler(
325                            self._audio_facade, plug_handler,
326                            self._system_facade)
327                elif is_hotwording:
328                    return audio_widget.CrosHotwordingWidgetHandler(
329                            self._audio_facade, plug_handler,
330                            self._system_facade)
331                else:
332                    return audio_widget.CrosInputWidgetHandler(
333                            self._audio_facade, plug_handler)
334            else:
335                if use_arc:
336                    return audio_widget_arc.CrosOutputWidgetARCHandler(
337                            self._audio_facade, plug_handler)
338                return audio_widget.CrosOutputWidgetHandler(self._audio_facade,
339                                                            plug_handler)
340
341
342        def _create_audio_widget(audio_port, handler):
343            """Creates an AudioWidget for given AudioPort using WidgetHandler.
344
345            Creates an AudioWidget with the role of audio_port. Put
346            the widget handler into the widget so the widget can handle
347            action requests.
348
349            @param audio_port: An AudioPort object.
350            @param handler: A WidgetHandler object.
351
352            @returns: An Audio(Input/Output)Widget depending on
353                      role of audio_port.
354
355            @raises: AudioWidgetFactoryError if fail to create widget.
356
357            """
358            if audio_port.host in ['Chameleon', 'Cros']:
359                if audio_port.role == 'sink':
360                    return audio_widget.AudioInputWidget(audio_port, handler)
361                else:
362                    return audio_widget.AudioOutputWidget(audio_port, handler)
363            elif audio_port.host == 'Peripheral':
364                return audio_widget.PeripheralWidget(audio_port, handler)
365            else:
366                raise AudioWidgetFactoryError(
367                        'The host %s is not valid' % audio_port.host)
368
369
370        audio_port = AudioPort(port_id)
371        if audio_port.host == 'Chameleon':
372            handler = _create_chameleon_handler(audio_port)
373        elif audio_port.host == 'Cros':
374            handler = _create_cros_handler(audio_port)
375        elif audio_port.host == 'Peripheral':
376            handler = audio_widget.PeripheralWidgetHandler()
377
378        return _create_audio_widget(audio_port, handler)
379
380
381    def _create_widget_binder(self, source, sink):
382        """Creates a WidgetBinder for two AudioWidgets.
383
384        @param source: An AudioWidget.
385        @param sink: An AudioWidget.
386
387        @returns: A WidgetBinder object.
388
389        """
390        return audio_widget_link.WidgetBinder(
391                source, self._link_factory.create_link(source, sink), sink)
392
393
394    def create_binder(self, *widgets):
395        """Creates a WidgetBinder or a WidgetChainBinder for AudioWidgets.
396
397        @param widgets: A list of widgets that should be linked in a chain.
398
399        @returns: A WidgetBinder for two widgets. A WidgetBinderChain object
400                  for three or more widgets.
401
402        """
403        if len(widgets) == 2:
404            return self._create_widget_binder(widgets[0], widgets[1])
405        binders = []
406        for index in xrange(len(widgets) - 1):
407            binders.append(
408                    self._create_widget_binder(
409                            widgets[index],  widgets[index + 1]))
410
411        return audio_widget_link.WidgetBinderChain(binders)
412
413
414@contextmanager
415def bind_widgets(binder):
416    """Context manager for widget binders.
417
418    Connects widgets in the beginning. Disconnects widgets and releases binder
419    in the end.
420
421    @param binder: A WidgetBinder object or a WidgetBinderChain object.
422                   If binder is None, then do nothing. This is for test user's
423                   convenience to reuse test logic among paths using binder
424                   and paths not using binder.
425
426    E.g. with bind_widgets(binder):
427             do something on widget.
428
429    """
430    if not binder:
431        yield
432    else:
433        try:
434            binder.connect()
435            yield
436        finally:
437            binder.disconnect()
438            binder.release()
439