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 18class 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 60class AudioLinkFactoryError(Exception): 61 """Error in AudioLinkFactory.""" 62 pass 63 64 65class 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(self._cros_host) 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. 178 179 # Initializes a BluetoothDevice object if needed. And reuse this 180 # object for future bluetooth link usage. 181 if not self._bluetooth_device: 182 self._bluetooth_device = bluetooth_device.BluetoothDevice( 183 self._cros_host) 184 link = link_type( 185 self._bluetooth_device, 186 self._chameleon_board.get_bluetooth_ref_controller(), 187 self._chameleon_board.get_bluetooth_a2dp_sink().GetLocalBluetoothAddress()) 188 elif issubclass(link_type, audio_widget_link.USBWidgetLink): 189 # Aside from managing connection between USB audio gadget driver on 190 # Chameleon with Cros device, USBWidgetLink also handles changing 191 # the gadget driver's configurations, through the USBController that 192 # is passed to it at initialization. 193 if not self._usb_ctrl: 194 self._usb_ctrl = self._chameleon_board.get_usb_controller() 195 196 link = link_type(self._usb_ctrl) 197 else: 198 raise NotImplementedError('Link %s is not implemented' % link_type) 199 200 return link 201 202 203class AudioWidgetFactoryError(Exception): 204 """Error in AudioWidgetFactory.""" 205 pass 206 207 208class AudioWidgetFactory(object): 209 """ 210 This class provides methods to create widgets and binder of widgets. 211 User can use binder to setup audio paths. User can use widgets to control 212 record/playback on different ports on Cros device or Chameleon. 213 214 Properties: 215 _audio_facade: An AudioFacadeRemoteAdapter to access Cros device audio 216 functionality. This is created by the 217 'factory' argument passed to the constructor. 218 _display_facade: A DisplayFacadeRemoteAdapter to access Cros device 219 display functionality. This is created by the 220 'factory' argument passed to the constructor. 221 _system_facade: A SystemFacadeRemoteAdapter to access Cros device 222 system functionality. This is created by the 223 'factory' argument passed to the constructor. 224 _chameleon_board: A ChameleonBoard object to access Chameleon 225 functionality. 226 _link_factory: An AudioLinkFactory that creates link for widgets. 227 228 """ 229 def __init__(self, factory, cros_host): 230 """Initializes a AudioWidgetFactory 231 232 @param factory: A facade factory to access Cros device functionality. 233 Currently only audio facade is used, but we can access 234 other functionalities including display and video by 235 facades created by this facade factory. 236 @param cros_host: A CrosHost object to access Cros device. 237 238 """ 239 self._audio_facade = factory.create_audio_facade() 240 self._display_facade = factory.create_display_facade() 241 self._system_facade = factory.create_system_facade() 242 self._usb_facade = factory.create_usb_facade() 243 self._cros_host = cros_host 244 self._chameleon_board = cros_host.chameleon 245 self._link_factory = AudioLinkFactory(cros_host) 246 247 248 def create_widget(self, port_id, use_arc=False): 249 """Creates a AudioWidget given port id string. 250 251 @param port_id: A port id string defined in chameleon_audio_ids. 252 @param use_arc: For Cros widget, select if audio path exercises ARC. 253 Currently only input widget is supported. 254 255 @returns: An AudioWidget that is actually a 256 (Chameleon/Cros/Peripheral)(Input/Output)Widget. 257 258 """ 259 def _create_chameleon_handler(audio_port): 260 """Creates a ChameleonWidgetHandler for a given AudioPort. 261 262 @param audio_port: An AudioPort object. 263 264 @returns: A Chameleon(Input/Output)WidgetHandler depending on 265 role of audio_port. 266 267 """ 268 if audio_port.role == 'sink': 269 if audio_port.port_id == ids.ChameleonIds.HDMI: 270 return audio_widget.ChameleonHDMIInputWidgetHandler( 271 self._chameleon_board, audio_port.interface, 272 self._display_facade) 273 else: 274 return audio_widget.ChameleonInputWidgetHandler( 275 self._chameleon_board, audio_port.interface) 276 else: 277 if audio_port.port_id == ids.ChameleonIds.LINEOUT: 278 return audio_widget.ChameleonLineOutOutputWidgetHandler( 279 self._chameleon_board, audio_port.interface) 280 else: 281 return audio_widget.ChameleonOutputWidgetHandler( 282 self._chameleon_board, audio_port.interface) 283 284 285 def _create_cros_handler(audio_port): 286 """Creates a CrosWidgetHandler for a given AudioPort. 287 288 @param audio_port: An AudioPort object. 289 290 @returns: A Cros(Input/Output)WidgetHandler depending on 291 role of audio_port. 292 293 """ 294 is_usb = audio_port.port_id in [ids.CrosIds.USBIN, 295 ids.CrosIds.USBOUT] 296 is_audio_jack = audio_port.port_id in [ids.CrosIds.HEADPHONE, 297 ids.CrosIds.EXTERNAL_MIC] 298 is_internal_mic = audio_port.port_id == ids.CrosIds.INTERNAL_MIC 299 is_hotwording = audio_port.port_id == ids.CrosIds.HOTWORDING 300 301 # Determines the plug handler to be used. 302 # By default, the plug handler is DummyPlugHandler. 303 # If the port uses audio jack, and there is jack plugger available 304 # through audio board, then JackPluggerPlugHandler should be used. 305 audio_board = self._chameleon_board.get_audio_board() 306 if audio_board: 307 jack_plugger = audio_board.get_jack_plugger() 308 else: 309 jack_plugger = None 310 311 if jack_plugger and is_audio_jack: 312 plug_handler = audio_widget.JackPluggerPlugHandler(jack_plugger) 313 else: 314 plug_handler = audio_widget.DummyPlugHandler() 315 316 if audio_port.role == 'sink': 317 if use_arc: 318 return audio_widget_arc.CrosInputWidgetARCHandler( 319 self._audio_facade, plug_handler) 320 elif is_usb: 321 return audio_widget.CrosUSBInputWidgetHandler( 322 self._audio_facade, plug_handler) 323 elif is_internal_mic: 324 return audio_widget.CrosIntMicInputWidgetHandler( 325 self._audio_facade, plug_handler, 326 self._system_facade) 327 elif is_hotwording: 328 return audio_widget.CrosHotwordingWidgetHandler( 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