# 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.audio import audio_helper
from autotest_lib.client.cros.chameleon import audio_widget
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
from autotest_lib.client.cros.chameleon import chameleon_info
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()
# 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. Finally, the MAC address
# of bluetooth module is queried through chameleon_info because
# it is not probeable on Chameleon board.
# 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._audio_board.get_bluetooth_controller(),
chameleon_info.get_bluetooth_mac_address(
self._chameleon_board))
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.
_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._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):
"""Creates a AudioWidget given port id string.
@param port_id: A port id string defined in chameleon_audio_ids.
@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':
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]
audio_board = self._chameleon_board.get_audio_board()
if audio_board:
jack_plugger = audio_board.get_jack_plugger()
else:
jack_plugger = None
if is_usb:
plug_handler = audio_widget.USBPlugHandler(self._usb_facade)
elif jack_plugger:
plug_handler = audio_widget.JackPluggerPlugHandler(jack_plugger)
else:
plug_handler = audio_widget.DummyPlugHandler()
if audio_port.role == 'sink':
if is_usb:
return audio_widget.CrosUSBInputWidgetHandler(
self._audio_facade, plug_handler)
else:
return audio_widget.CrosInputWidgetHandler(
self._audio_facade, plug_handler)
else:
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)
def compare_recorded_result(golden_file, recorder, method, parameters=None):
"""Check recoded audio in a AudioInputWidget against a golden file.
Compares recorded data with golden data by cross correlation method.
Refer to audio_helper.compare_data for details of comparison.
@param golden_file: An AudioTestData object that serves as golden data.
@param recorder: An AudioInputWidget that has recorded some audio data.
@param method: The method to compare recorded result. Currently,
'correlation' and 'frequency' are supported.
@param parameters: A dict containing parameters for method.
@returns: True if the recorded data and golden data are similar enough.
"""
logging.info('Comparing recorded data with golden file %s ...',
golden_file.path)
return audio_helper.compare_data(
golden_file.get_binary(), golden_file.data_format,
recorder.get_binary(), recorder.data_format, recorder.channel_map,
method, parameters)
@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.
E.g. with bind_widgets(binder):
do something on widget.
"""
try:
binder.connect()
yield
finally:
binder.disconnect()
binder.release()