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 7import logging 8from contextlib import contextmanager 9 10from autotest_lib.client.cros.audio import audio_helper 11from autotest_lib.client.cros.chameleon import audio_widget 12from autotest_lib.client.cros.chameleon import audio_widget_arc 13from autotest_lib.client.cros.chameleon import audio_widget_link 14from autotest_lib.server.cros.bluetooth import bluetooth_device 15from autotest_lib.client.cros.chameleon import chameleon_audio_ids as ids 16from autotest_lib.client.cros.chameleon import chameleon_info 17 18 19class 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 61class AudioLinkFactoryError(Exception): 62 """Error in AudioLinkFactory.""" 63 pass 64 65 66class 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 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 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 415def 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