Home | History | Annotate | Download | only in chameleon
      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 
      7 import logging
      8 from contextlib import contextmanager
      9 
     10 from autotest_lib.client.cros.audio import audio_helper
     11 from autotest_lib.client.cros.chameleon import audio_widget
     12 from autotest_lib.client.cros.chameleon import audio_widget_arc
     13 from autotest_lib.client.cros.chameleon import audio_widget_link
     14 from autotest_lib.server.cros.bluetooth import bluetooth_device
     15 from autotest_lib.client.cros.chameleon import chameleon_audio_ids as ids
     16 from autotest_lib.client.cros.chameleon import chameleon_info
     17 
     18 
     19 class AudioPort(object):
     20     """
     21     This class abstracts an audio port in audio test framework. A port is
     22     identified by its host and interface. Available hosts and interfaces
     23     are listed in chameleon_audio_ids.
     24 
     25     Properties:
     26         port_id: The port id defined in chameleon_audio_ids.
     27         host: The host of this audio port, e.g. 'Chameleon', 'Cros',
     28               'Peripheral'.
     29         interface: The interface of this audio port, e.g. 'HDMI', 'Headphone'.
     30         role: The role of this audio port, that is, 'source' or
     31               'sink'. Note that bidirectional interface like 3.5mm
     32               jack is separated to two interfaces 'Headphone' and
     33              'External Mic'.
     34 
     35     """
     36     def __init__(self, port_id):
     37         """Initialize an AudioPort with port id string.
     38 
     39         @param port_id: A port id string defined in chameleon_audio_ids.
     40 
     41         """
     42         logging.debug('Creating AudioPort with port_id: %s', port_id)
     43         self.port_id = port_id
     44         self.host = ids.get_host(port_id)
     45         self.interface = ids.get_interface(port_id)
     46         self.role = ids.get_role(port_id)
     47         logging.debug('Created AudioPort: %s', self)
     48 
     49 
     50     def __str__(self):
     51         """String representation of audio port.
     52 
     53         @returns: The string representation of audio port which is composed by
     54                   host, interface, and role.
     55 
     56         """
     57         return '( %s | %s | %s )' % (
     58                 self.host, self.interface, self.role)
     59 
     60 
     61 class AudioLinkFactoryError(Exception):
     62     """Error in AudioLinkFactory."""
     63     pass
     64 
     65 
     66 class AudioLinkFactory(object):
     67     """
     68     This class provides method to create link that connects widgets.
     69     This is used by AudioWidgetFactory when user wants to create binder for
     70     widgets.
     71 
     72     Properties:
     73         _audio_bus_links: A dict containing mapping from index number
     74                           to object of AudioBusLink's subclass.
     75         _audio_board: An AudioBoard object to access Chameleon
     76                       audio board functionality.
     77 
     78     """
     79 
     80     # Maps pair of widgets to widget link of different type.
     81     LINK_TABLE = {
     82         (ids.CrosIds.HDMI, ids.ChameleonIds.HDMI):
     83                 audio_widget_link.HDMIWidgetLink,
     84         (ids.CrosIds.HEADPHONE, ids.ChameleonIds.LINEIN):
     85                 audio_widget_link.AudioBusToChameleonLink,
     86         (ids.ChameleonIds.LINEOUT, ids.CrosIds.EXTERNAL_MIC):
     87                 audio_widget_link.AudioBusToCrosLink,
     88         (ids.ChameleonIds.LINEOUT, ids.PeripheralIds.SPEAKER):
     89                 audio_widget_link.AudioBusChameleonToPeripheralLink,
     90         (ids.PeripheralIds.MIC, ids.ChameleonIds.LINEIN):
     91                 audio_widget_link.AudioBusToChameleonLink,
     92         (ids.PeripheralIds.BLUETOOTH_DATA_RX,
     93          ids.ChameleonIds.LINEIN):
     94                 audio_widget_link.AudioBusToChameleonLink,
     95         (ids.ChameleonIds.LINEOUT,
     96          ids.PeripheralIds.BLUETOOTH_DATA_TX):
     97                 audio_widget_link.AudioBusChameleonToPeripheralLink,
     98         (ids.CrosIds.BLUETOOTH_HEADPHONE,
     99          ids.PeripheralIds.BLUETOOTH_DATA_RX):
    100                 audio_widget_link.BluetoothHeadphoneWidgetLink,
    101         (ids.PeripheralIds.BLUETOOTH_DATA_TX,
    102          ids.CrosIds.BLUETOOTH_MIC):
    103                 audio_widget_link.BluetoothMicWidgetLink,
    104         (ids.CrosIds.USBOUT, ids.ChameleonIds.USBIN):
    105                 audio_widget_link.USBToChameleonWidgetLink,
    106         (ids.ChameleonIds.USBOUT, ids.CrosIds.USBIN):
    107                 audio_widget_link.USBToCrosWidgetLink,
    108         # TODO(cychiang): Add link for other widget pairs.
    109     }
    110 
    111     def __init__(self, cros_host):
    112         """Initializes an AudioLinkFactory.
    113 
    114         @param cros_host: A CrosHost object to access Cros device.
    115 
    116         """
    117         # There are two audio buses on audio board. Initializes these links
    118         # to None. They may be changed to objects of AudioBusLink's subclass.
    119         self._audio_bus_links = {1: None, 2: None}
    120         self._cros_host = cros_host
    121         self._chameleon_board = cros_host.chameleon
    122         self._audio_board = self._chameleon_board.get_audio_board()
    123         self._bluetooth_device = None
    124         self._usb_ctrl = None
    125 
    126 
    127     def _acquire_audio_bus_index(self):
    128         """Acquires an available audio bus index that is not occupied yet.
    129 
    130         @returns: A number.
    131 
    132         @raises: AudioLinkFactoryError if there is no available
    133                  audio bus.
    134         """
    135         for index, bus in self._audio_bus_links.iteritems():
    136             if not (bus and bus.occupied):
    137                 return index
    138 
    139         raise AudioLinkFactoryError('No available audio bus')
    140 
    141 
    142     def create_link(self, source, sink):
    143         """Creates a widget link for two audio widgets.
    144 
    145         @param source: An AudioWidget.
    146         @param sink: An AudioWidget.
    147 
    148         @returns: An object of WidgetLink's subclass.
    149 
    150         @raises: AudioLinkFactoryError if there is no link between
    151             source and sink.
    152 
    153         """
    154         # Finds the available link types from LINK_TABLE.
    155         link_type = self.LINK_TABLE.get((source.port_id, sink.port_id), None)
    156         if not link_type:
    157             raise AudioLinkFactoryError(
    158                     'No supported link between %s and %s' % (
    159                             source.port_id, sink.port_id))
    160 
    161         # There is only one dedicated HDMI cable, just use it.
    162         if link_type == audio_widget_link.HDMIWidgetLink:
    163             link = audio_widget_link.HDMIWidgetLink(self._cros_host)
    164 
    165         # Acquires audio bus if there is available bus.
    166         # Creates a bus of AudioBusLink's subclass that is more
    167         # specific than AudioBusLink.
    168         # Controls this link using AudioBus object obtained from AudioBoard
    169         # object.
    170         elif issubclass(link_type, audio_widget_link.AudioBusLink):
    171             bus_index = self._acquire_audio_bus_index()
    172             link = link_type(self._audio_board.get_audio_bus(bus_index))
    173             self._audio_bus_links[bus_index] = link
    174         elif issubclass(link_type, audio_widget_link.BluetoothWidgetLink):
    175             # To connect bluetooth adapter on Cros device to bluetooth module on
    176             # chameleon board, we need to access bluetooth adapter on Cros host
    177             # using BluetoothDevice, and access bluetooth module on
    178             # audio board using BluetoothController. Finally, the MAC address
    179             # of bluetooth module is queried through chameleon_info because
    180             # it is not probeable on Chameleon board.
    181 
    182             # Initializes a BluetoothDevice object if needed. And reuse this
    183             # object for future bluetooth link usage.
    184             if not self._bluetooth_device:
    185                 self._bluetooth_device = bluetooth_device.BluetoothDevice(
    186                         self._cros_host)
    187 
    188             link = link_type(
    189                     self._bluetooth_device,
    190                     self._audio_board.get_bluetooth_controller(),
    191                     chameleon_info.get_bluetooth_mac_address(
    192                             self._chameleon_board))
    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 
    208 class AudioWidgetFactoryError(Exception):
    209     """Error in AudioWidgetFactory."""
    210     pass
    211 
    212 
    213 class 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 
    290         def _create_cros_handler(audio_port):
    291             """Creates a CrosWidgetHandler for a given AudioPort.
    292 
    293             @param audio_port: An AudioPort object.
    294 
    295             @returns: A Cros(Input/Output)WidgetHandler depending on
    296                       role of audio_port.
    297 
    298             """
    299             is_usb = audio_port.port_id in [ids.CrosIds.USBIN,
    300                                             ids.CrosIds.USBOUT]
    301             is_audio_jack = audio_port.port_id in [ids.CrosIds.HEADPHONE,
    302                                                    ids.CrosIds.EXTERNAL_MIC]
    303             is_internal_mic = audio_port.port_id == ids.CrosIds.INTERNAL_MIC
    304 
    305             # Determines the plug handler to be used.
    306             # By default, the plug handler is DummyPlugHandler.
    307             # If the port uses audio jack, and there is jack plugger available
    308             # through audio board, then JackPluggerPlugHandler should be used.
    309             audio_board = self._chameleon_board.get_audio_board()
    310             if audio_board:
    311                 jack_plugger = audio_board.get_jack_plugger()
    312             else:
    313                 jack_plugger = None
    314 
    315             if jack_plugger and is_audio_jack:
    316                 plug_handler = audio_widget.JackPluggerPlugHandler(jack_plugger)
    317             else:
    318                 plug_handler = audio_widget.DummyPlugHandler()
    319 
    320             if audio_port.role == 'sink':
    321                 if use_arc:
    322                     return audio_widget_arc.CrosInputWidgetARCHandler(
    323                             self._audio_facade, plug_handler)
    324                 elif is_usb:
    325                     return audio_widget.CrosUSBInputWidgetHandler(
    326                             self._audio_facade, plug_handler)
    327                 elif is_internal_mic:
    328                     return audio_widget.CrosIntMicInputWidgetHandler(
    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
    415 def 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