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_link
     13 from autotest_lib.server.cros.bluetooth import bluetooth_device
     14 from autotest_lib.client.cros.chameleon import chameleon_audio_ids as ids
     15 from autotest_lib.client.cros.chameleon import chameleon_info
     16 
     17 
     18 class 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 
     60 class AudioLinkFactoryError(Exception):
     61     """Error in AudioLinkFactory."""
     62     pass
     63 
     64 
     65 class 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 
    207 class AudioWidgetFactoryError(Exception):
    208     """Error in AudioWidgetFactory."""
    209     pass
    210 
    211 
    212 class 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 
    382 def 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
    406 def 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