| # Copyright 2014 The Chromium OS Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """This module provides the framework for audio tests using chameleon.""" |
| |
| import logging |
| from contextlib import contextmanager |
| |
| from autotest_lib.client.cros.chameleon import audio_widget |
| from autotest_lib.client.cros.chameleon import audio_widget_arc |
| from autotest_lib.client.cros.chameleon import audio_widget_link |
| from autotest_lib.server.cros.bluetooth import bluetooth_device |
| from autotest_lib.client.cros.chameleon import chameleon_audio_ids as ids |
| |
| class AudioPort(object): |
| """ |
| This class abstracts an audio port in audio test framework. A port is |
| identified by its host and interface. Available hosts and interfaces |
| are listed in chameleon_audio_ids. |
| |
| Properties: |
| port_id: The port id defined in chameleon_audio_ids. |
| host: The host of this audio port, e.g. 'Chameleon', 'Cros', |
| 'Peripheral'. |
| interface: The interface of this audio port, e.g. 'HDMI', 'Headphone'. |
| role: The role of this audio port, that is, 'source' or |
| 'sink'. Note that bidirectional interface like 3.5mm |
| jack is separated to two interfaces 'Headphone' and |
| 'External Mic'. |
| |
| """ |
| def __init__(self, port_id): |
| """Initialize an AudioPort with port id string. |
| |
| @param port_id: A port id string defined in chameleon_audio_ids. |
| |
| """ |
| logging.debug('Creating AudioPort with port_id: %s', port_id) |
| self.port_id = port_id |
| self.host = ids.get_host(port_id) |
| self.interface = ids.get_interface(port_id) |
| self.role = ids.get_role(port_id) |
| logging.debug('Created AudioPort: %s', self) |
| |
| |
| def __str__(self): |
| """String representation of audio port. |
| |
| @returns: The string representation of audio port which is composed by |
| host, interface, and role. |
| |
| """ |
| return '( %s | %s | %s )' % ( |
| self.host, self.interface, self.role) |
| |
| |
| class AudioLinkFactoryError(Exception): |
| """Error in AudioLinkFactory.""" |
| pass |
| |
| |
| class AudioLinkFactory(object): |
| """ |
| This class provides method to create link that connects widgets. |
| This is used by AudioWidgetFactory when user wants to create binder for |
| widgets. |
| |
| Properties: |
| _audio_bus_links: A dict containing mapping from index number |
| to object of AudioBusLink's subclass. |
| _audio_board: An AudioBoard object to access Chameleon |
| audio board functionality. |
| |
| """ |
| |
| # Maps pair of widgets to widget link of different type. |
| LINK_TABLE = { |
| (ids.CrosIds.HDMI, ids.ChameleonIds.HDMI): |
| audio_widget_link.HDMIWidgetLink, |
| (ids.CrosIds.HEADPHONE, ids.ChameleonIds.LINEIN): |
| audio_widget_link.AudioBusToChameleonLink, |
| (ids.ChameleonIds.LINEOUT, ids.CrosIds.EXTERNAL_MIC): |
| audio_widget_link.AudioBusToCrosLink, |
| (ids.ChameleonIds.LINEOUT, ids.PeripheralIds.SPEAKER): |
| audio_widget_link.AudioBusChameleonToPeripheralLink, |
| (ids.PeripheralIds.MIC, ids.ChameleonIds.LINEIN): |
| audio_widget_link.AudioBusToChameleonLink, |
| (ids.PeripheralIds.BLUETOOTH_DATA_RX, |
| ids.ChameleonIds.LINEIN): |
| audio_widget_link.AudioBusToChameleonLink, |
| (ids.ChameleonIds.LINEOUT, |
| ids.PeripheralIds.BLUETOOTH_DATA_TX): |
| audio_widget_link.AudioBusChameleonToPeripheralLink, |
| (ids.CrosIds.BLUETOOTH_HEADPHONE, |
| ids.PeripheralIds.BLUETOOTH_DATA_RX): |
| audio_widget_link.BluetoothHeadphoneWidgetLink, |
| (ids.PeripheralIds.BLUETOOTH_DATA_TX, |
| ids.CrosIds.BLUETOOTH_MIC): |
| audio_widget_link.BluetoothMicWidgetLink, |
| (ids.CrosIds.USBOUT, ids.ChameleonIds.USBIN): |
| audio_widget_link.USBToChameleonWidgetLink, |
| (ids.ChameleonIds.USBOUT, ids.CrosIds.USBIN): |
| audio_widget_link.USBToCrosWidgetLink, |
| # TODO(cychiang): Add link for other widget pairs. |
| } |
| |
| def __init__(self, cros_host): |
| """Initializes an AudioLinkFactory. |
| |
| @param cros_host: A CrosHost object to access Cros device. |
| |
| """ |
| # There are two audio buses on audio board. Initializes these links |
| # to None. They may be changed to objects of AudioBusLink's subclass. |
| self._audio_bus_links = {1: None, 2: None} |
| self._cros_host = cros_host |
| self._chameleon_board = cros_host.chameleon |
| self._audio_board = self._chameleon_board.get_audio_board() |
| self._bluetooth_device = None |
| self._usb_ctrl = None |
| |
| |
| def _acquire_audio_bus_index(self): |
| """Acquires an available audio bus index that is not occupied yet. |
| |
| @returns: A number. |
| |
| @raises: AudioLinkFactoryError if there is no available |
| audio bus. |
| """ |
| for index, bus in self._audio_bus_links.iteritems(): |
| if not (bus and bus.occupied): |
| return index |
| |
| raise AudioLinkFactoryError('No available audio bus') |
| |
| |
| def create_link(self, source, sink): |
| """Creates a widget link for two audio widgets. |
| |
| @param source: An AudioWidget. |
| @param sink: An AudioWidget. |
| |
| @returns: An object of WidgetLink's subclass. |
| |
| @raises: AudioLinkFactoryError if there is no link between |
| source and sink. |
| |
| """ |
| # Finds the available link types from LINK_TABLE. |
| link_type = self.LINK_TABLE.get((source.port_id, sink.port_id), None) |
| if not link_type: |
| raise AudioLinkFactoryError( |
| 'No supported link between %s and %s' % ( |
| source.port_id, sink.port_id)) |
| |
| # There is only one dedicated HDMI cable, just use it. |
| if link_type == audio_widget_link.HDMIWidgetLink: |
| link = audio_widget_link.HDMIWidgetLink(self._cros_host) |
| |
| # Acquires audio bus if there is available bus. |
| # Creates a bus of AudioBusLink's subclass that is more |
| # specific than AudioBusLink. |
| # Controls this link using AudioBus object obtained from AudioBoard |
| # object. |
| elif issubclass(link_type, audio_widget_link.AudioBusLink): |
| bus_index = self._acquire_audio_bus_index() |
| link = link_type(self._audio_board.get_audio_bus(bus_index)) |
| self._audio_bus_links[bus_index] = link |
| elif issubclass(link_type, audio_widget_link.BluetoothWidgetLink): |
| # To connect bluetooth adapter on Cros device to bluetooth module on |
| # chameleon board, we need to access bluetooth adapter on Cros host |
| # using BluetoothDevice, and access bluetooth module on |
| # audio board using BluetoothController. |
| |
| # Initializes a BluetoothDevice object if needed. And reuse this |
| # object for future bluetooth link usage. |
| if not self._bluetooth_device: |
| self._bluetooth_device = bluetooth_device.BluetoothDevice( |
| self._cros_host) |
| link = link_type( |
| self._bluetooth_device, |
| self._chameleon_board.get_bluetooth_ref_controller(), |
| self._chameleon_board.get_bluetooth_a2dp_sink().GetLocalBluetoothAddress()) |
| elif issubclass(link_type, audio_widget_link.USBWidgetLink): |
| # Aside from managing connection between USB audio gadget driver on |
| # Chameleon with Cros device, USBWidgetLink also handles changing |
| # the gadget driver's configurations, through the USBController that |
| # is passed to it at initialization. |
| if not self._usb_ctrl: |
| self._usb_ctrl = self._chameleon_board.get_usb_controller() |
| |
| link = link_type(self._usb_ctrl) |
| else: |
| raise NotImplementedError('Link %s is not implemented' % link_type) |
| |
| return link |
| |
| |
| class AudioWidgetFactoryError(Exception): |
| """Error in AudioWidgetFactory.""" |
| pass |
| |
| |
| class AudioWidgetFactory(object): |
| """ |
| This class provides methods to create widgets and binder of widgets. |
| User can use binder to setup audio paths. User can use widgets to control |
| record/playback on different ports on Cros device or Chameleon. |
| |
| Properties: |
| _audio_facade: An AudioFacadeRemoteAdapter to access Cros device audio |
| functionality. This is created by the |
| 'factory' argument passed to the constructor. |
| _display_facade: A DisplayFacadeRemoteAdapter to access Cros device |
| display functionality. This is created by the |
| 'factory' argument passed to the constructor. |
| _system_facade: A SystemFacadeRemoteAdapter to access Cros device |
| system functionality. This is created by the |
| 'factory' argument passed to the constructor. |
| _chameleon_board: A ChameleonBoard object to access Chameleon |
| functionality. |
| _link_factory: An AudioLinkFactory that creates link for widgets. |
| |
| """ |
| def __init__(self, factory, cros_host): |
| """Initializes a AudioWidgetFactory |
| |
| @param factory: A facade factory to access Cros device functionality. |
| Currently only audio facade is used, but we can access |
| other functionalities including display and video by |
| facades created by this facade factory. |
| @param cros_host: A CrosHost object to access Cros device. |
| |
| """ |
| self._audio_facade = factory.create_audio_facade() |
| self._display_facade = factory.create_display_facade() |
| self._system_facade = factory.create_system_facade() |
| self._usb_facade = factory.create_usb_facade() |
| self._cros_host = cros_host |
| self._chameleon_board = cros_host.chameleon |
| self._link_factory = AudioLinkFactory(cros_host) |
| |
| |
| def create_widget(self, port_id, use_arc=False): |
| """Creates a AudioWidget given port id string. |
| |
| @param port_id: A port id string defined in chameleon_audio_ids. |
| @param use_arc: For Cros widget, select if audio path exercises ARC. |
| Currently only input widget is supported. |
| |
| @returns: An AudioWidget that is actually a |
| (Chameleon/Cros/Peripheral)(Input/Output)Widget. |
| |
| """ |
| def _create_chameleon_handler(audio_port): |
| """Creates a ChameleonWidgetHandler for a given AudioPort. |
| |
| @param audio_port: An AudioPort object. |
| |
| @returns: A Chameleon(Input/Output)WidgetHandler depending on |
| role of audio_port. |
| |
| """ |
| if audio_port.role == 'sink': |
| if audio_port.port_id == ids.ChameleonIds.HDMI: |
| return audio_widget.ChameleonHDMIInputWidgetHandler( |
| self._chameleon_board, audio_port.interface, |
| self._display_facade) |
| else: |
| return audio_widget.ChameleonInputWidgetHandler( |
| self._chameleon_board, audio_port.interface) |
| else: |
| if audio_port.port_id == ids.ChameleonIds.LINEOUT: |
| return audio_widget.ChameleonLineOutOutputWidgetHandler( |
| self._chameleon_board, audio_port.interface) |
| else: |
| return audio_widget.ChameleonOutputWidgetHandler( |
| self._chameleon_board, audio_port.interface) |
| |
| |
| def _create_cros_handler(audio_port): |
| """Creates a CrosWidgetHandler for a given AudioPort. |
| |
| @param audio_port: An AudioPort object. |
| |
| @returns: A Cros(Input/Output)WidgetHandler depending on |
| role of audio_port. |
| |
| """ |
| is_usb = audio_port.port_id in [ids.CrosIds.USBIN, |
| ids.CrosIds.USBOUT] |
| is_audio_jack = audio_port.port_id in [ids.CrosIds.HEADPHONE, |
| ids.CrosIds.EXTERNAL_MIC] |
| is_internal_mic = audio_port.port_id == ids.CrosIds.INTERNAL_MIC |
| is_hotwording = audio_port.port_id == ids.CrosIds.HOTWORDING |
| |
| # Determines the plug handler to be used. |
| # By default, the plug handler is DummyPlugHandler. |
| # If the port uses audio jack, and there is jack plugger available |
| # through audio board, then JackPluggerPlugHandler should be used. |
| audio_board = self._chameleon_board.get_audio_board() |
| if audio_board: |
| jack_plugger = audio_board.get_jack_plugger() |
| else: |
| jack_plugger = None |
| |
| if jack_plugger and is_audio_jack: |
| plug_handler = audio_widget.JackPluggerPlugHandler(jack_plugger) |
| else: |
| plug_handler = audio_widget.DummyPlugHandler() |
| |
| if audio_port.role == 'sink': |
| if use_arc: |
| return audio_widget_arc.CrosInputWidgetARCHandler( |
| self._audio_facade, plug_handler) |
| elif is_usb: |
| return audio_widget.CrosUSBInputWidgetHandler( |
| self._audio_facade, plug_handler) |
| elif is_internal_mic: |
| return audio_widget.CrosIntMicInputWidgetHandler( |
| self._audio_facade, plug_handler, |
| self._system_facade) |
| elif is_hotwording: |
| return audio_widget.CrosHotwordingWidgetHandler( |
| self._audio_facade, plug_handler, |
| self._system_facade) |
| else: |
| return audio_widget.CrosInputWidgetHandler( |
| self._audio_facade, plug_handler) |
| else: |
| if use_arc: |
| return audio_widget_arc.CrosOutputWidgetARCHandler( |
| self._audio_facade, plug_handler) |
| return audio_widget.CrosOutputWidgetHandler(self._audio_facade, |
| plug_handler) |
| |
| |
| def _create_audio_widget(audio_port, handler): |
| """Creates an AudioWidget for given AudioPort using WidgetHandler. |
| |
| Creates an AudioWidget with the role of audio_port. Put |
| the widget handler into the widget so the widget can handle |
| action requests. |
| |
| @param audio_port: An AudioPort object. |
| @param handler: A WidgetHandler object. |
| |
| @returns: An Audio(Input/Output)Widget depending on |
| role of audio_port. |
| |
| @raises: AudioWidgetFactoryError if fail to create widget. |
| |
| """ |
| if audio_port.host in ['Chameleon', 'Cros']: |
| if audio_port.role == 'sink': |
| return audio_widget.AudioInputWidget(audio_port, handler) |
| else: |
| return audio_widget.AudioOutputWidget(audio_port, handler) |
| elif audio_port.host == 'Peripheral': |
| return audio_widget.PeripheralWidget(audio_port, handler) |
| else: |
| raise AudioWidgetFactoryError( |
| 'The host %s is not valid' % audio_port.host) |
| |
| |
| audio_port = AudioPort(port_id) |
| if audio_port.host == 'Chameleon': |
| handler = _create_chameleon_handler(audio_port) |
| elif audio_port.host == 'Cros': |
| handler = _create_cros_handler(audio_port) |
| elif audio_port.host == 'Peripheral': |
| handler = audio_widget.PeripheralWidgetHandler() |
| |
| return _create_audio_widget(audio_port, handler) |
| |
| |
| def _create_widget_binder(self, source, sink): |
| """Creates a WidgetBinder for two AudioWidgets. |
| |
| @param source: An AudioWidget. |
| @param sink: An AudioWidget. |
| |
| @returns: A WidgetBinder object. |
| |
| """ |
| return audio_widget_link.WidgetBinder( |
| source, self._link_factory.create_link(source, sink), sink) |
| |
| |
| def create_binder(self, *widgets): |
| """Creates a WidgetBinder or a WidgetChainBinder for AudioWidgets. |
| |
| @param widgets: A list of widgets that should be linked in a chain. |
| |
| @returns: A WidgetBinder for two widgets. A WidgetBinderChain object |
| for three or more widgets. |
| |
| """ |
| if len(widgets) == 2: |
| return self._create_widget_binder(widgets[0], widgets[1]) |
| binders = [] |
| for index in xrange(len(widgets) - 1): |
| binders.append( |
| self._create_widget_binder( |
| widgets[index], widgets[index + 1])) |
| |
| return audio_widget_link.WidgetBinderChain(binders) |
| |
| |
| @contextmanager |
| def bind_widgets(binder): |
| """Context manager for widget binders. |
| |
| Connects widgets in the beginning. Disconnects widgets and releases binder |
| in the end. |
| |
| @param binder: A WidgetBinder object or a WidgetBinderChain object. |
| If binder is None, then do nothing. This is for test user's |
| convenience to reuse test logic among paths using binder |
| and paths not using binder. |
| |
| E.g. with bind_widgets(binder): |
| do something on widget. |
| |
| """ |
| if not binder: |
| yield |
| else: |
| try: |
| binder.connect() |
| yield |
| finally: |
| binder.disconnect() |
| binder.release() |