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