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