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.chameleon import audio_widget 11from autotest_lib.client.cros.chameleon import audio_widget_arc 12from autotest_lib.client.cros.chameleon import audio_widget_link 13from autotest_lib.server.cros.bluetooth import bluetooth_device 14from autotest_lib.client.cros.chameleon import chameleon_audio_ids as ids 15 16class AudioPort(object): 17 """ 18 This class abstracts an audio port in audio test framework. A port is 19 identified by its host and interface. Available hosts and interfaces 20 are listed in chameleon_audio_ids. 21 22 Properties: 23 port_id: The port id defined in chameleon_audio_ids. 24 host: The host of this audio port, e.g. 'Chameleon', 'Cros', 25 'Peripheral'. 26 interface: The interface of this audio port, e.g. 'HDMI', 'Headphone'. 27 role: The role of this audio port, that is, 'source' or 28 'sink'. Note that bidirectional interface like 3.5mm 29 jack is separated to two interfaces 'Headphone' and 30 'External Mic'. 31 32 """ 33 def __init__(self, port_id): 34 """Initialize an AudioPort with port id string. 35 36 @param port_id: A port id string defined in chameleon_audio_ids. 37 38 """ 39 logging.debug('Creating AudioPort with port_id: %s', port_id) 40 self.port_id = port_id 41 self.host = ids.get_host(port_id) 42 self.interface = ids.get_interface(port_id) 43 self.role = ids.get_role(port_id) 44 logging.debug('Created AudioPort: %s', self) 45 46 47 def __str__(self): 48 """String representation of audio port. 49 50 @returns: The string representation of audio port which is composed by 51 host, interface, and role. 52 53 """ 54 return '( %s | %s | %s )' % ( 55 self.host, self.interface, self.role) 56 57 58class AudioLinkFactoryError(Exception): 59 """Error in AudioLinkFactory.""" 60 pass 61 62 63class AudioLinkFactory(object): 64 """ 65 This class provides method to create link that connects widgets. 66 This is used by AudioWidgetFactory when user wants to create binder for 67 widgets. 68 69 Properties: 70 _audio_bus_links: A dict containing mapping from index number 71 to object of AudioBusLink's subclass. 72 _audio_board: An AudioBoard object to access Chameleon 73 audio board functionality. 74 75 """ 76 77 # Maps pair of widgets to widget link of different type. 78 LINK_TABLE = { 79 (ids.CrosIds.HDMI, ids.ChameleonIds.HDMI): 80 audio_widget_link.HDMIWidgetLink, 81 (ids.CrosIds.HEADPHONE, ids.ChameleonIds.LINEIN): 82 audio_widget_link.AudioBusToChameleonLink, 83 (ids.ChameleonIds.LINEOUT, ids.CrosIds.EXTERNAL_MIC): 84 audio_widget_link.AudioBusToCrosLink, 85 (ids.ChameleonIds.LINEOUT, ids.PeripheralIds.SPEAKER): 86 audio_widget_link.AudioBusChameleonToPeripheralLink, 87 (ids.PeripheralIds.MIC, ids.ChameleonIds.LINEIN): 88 audio_widget_link.AudioBusToChameleonLink, 89 (ids.PeripheralIds.BLUETOOTH_DATA_RX, 90 ids.ChameleonIds.LINEIN): 91 audio_widget_link.AudioBusToChameleonLink, 92 (ids.ChameleonIds.LINEOUT, 93 ids.PeripheralIds.BLUETOOTH_DATA_TX): 94 audio_widget_link.AudioBusChameleonToPeripheralLink, 95 (ids.CrosIds.BLUETOOTH_HEADPHONE, 96 ids.PeripheralIds.BLUETOOTH_DATA_RX): 97 audio_widget_link.BluetoothHeadphoneWidgetLink, 98 (ids.PeripheralIds.BLUETOOTH_DATA_TX, 99 ids.CrosIds.BLUETOOTH_MIC): 100 audio_widget_link.BluetoothMicWidgetLink, 101 (ids.CrosIds.USBOUT, ids.ChameleonIds.USBIN): 102 audio_widget_link.USBToChameleonWidgetLink, 103 (ids.ChameleonIds.USBOUT, ids.CrosIds.USBIN): 104 audio_widget_link.USBToCrosWidgetLink, 105 # TODO(cychiang): Add link for other widget pairs. 106 } 107 108 def __init__(self, cros_host): 109 """Initializes an AudioLinkFactory. 110 111 @param cros_host: A CrosHost object to access Cros device. 112 113 """ 114 # There are two audio buses on audio board. Initializes these links 115 # to None. They may be changed to objects of AudioBusLink's subclass. 116 self._audio_bus_links = {1: None, 2: None} 117 self._cros_host = cros_host 118 self._chameleon_board = cros_host.chameleon 119 self._audio_board = self._chameleon_board.get_audio_board() 120 self._bluetooth_device = None 121 self._usb_ctrl = None 122 123 124 def _acquire_audio_bus_index(self): 125 """Acquires an available audio bus index that is not occupied yet. 126 127 @returns: A number. 128 129 @raises: AudioLinkFactoryError if there is no available 130 audio bus. 131 """ 132 for index, bus in self._audio_bus_links.iteritems(): 133 if not (bus and bus.occupied): 134 return index 135 136 raise AudioLinkFactoryError('No available audio bus') 137 138 139 def create_link(self, source, sink): 140 """Creates a widget link for two audio widgets. 141 142 @param source: An AudioWidget. 143 @param sink: An AudioWidget. 144 145 @returns: An object of WidgetLink's subclass. 146 147 @raises: AudioLinkFactoryError if there is no link between 148 source and sink. 149 150 """ 151 # Finds the available link types from LINK_TABLE. 152 link_type = self.LINK_TABLE.get((source.port_id, sink.port_id), None) 153 if not link_type: 154 raise AudioLinkFactoryError( 155 'No supported link between %s and %s' % ( 156 source.port_id, sink.port_id)) 157 158 # There is only one dedicated HDMI cable, just use it. 159 if link_type == audio_widget_link.HDMIWidgetLink: 160 link = audio_widget_link.HDMIWidgetLink(self._cros_host) 161 162 # Acquires audio bus if there is available bus. 163 # Creates a bus of AudioBusLink's subclass that is more 164 # specific than AudioBusLink. 165 # Controls this link using AudioBus object obtained from AudioBoard 166 # object. 167 elif issubclass(link_type, audio_widget_link.AudioBusLink): 168 bus_index = self._acquire_audio_bus_index() 169 link = link_type(self._audio_board.get_audio_bus(bus_index)) 170 self._audio_bus_links[bus_index] = link 171 elif issubclass(link_type, audio_widget_link.BluetoothWidgetLink): 172 # To connect bluetooth adapter on Cros device to bluetooth module on 173 # chameleon board, we need to access bluetooth adapter on Cros host 174 # using BluetoothDevice, and access bluetooth module on 175 # audio board using BluetoothController. 176 177 # Initializes a BluetoothDevice object if needed. And reuse this 178 # object for future bluetooth link usage. 179 if not self._bluetooth_device: 180 self._bluetooth_device = bluetooth_device.BluetoothDevice( 181 self._cros_host) 182 link = link_type( 183 self._bluetooth_device, 184 self._chameleon_board.get_bluetooth_ref_controller(), 185 self._chameleon_board.get_bluetooth_a2dp_sink().GetLocalBluetoothAddress()) 186 elif issubclass(link_type, audio_widget_link.USBWidgetLink): 187 # Aside from managing connection between USB audio gadget driver on 188 # Chameleon with Cros device, USBWidgetLink also handles changing 189 # the gadget driver's configurations, through the USBController that 190 # is passed to it at initialization. 191 if not self._usb_ctrl: 192 self._usb_ctrl = self._chameleon_board.get_usb_controller() 193 194 link = link_type(self._usb_ctrl) 195 else: 196 raise NotImplementedError('Link %s is not implemented' % link_type) 197 198 return link 199 200 201class AudioWidgetFactoryError(Exception): 202 """Error in AudioWidgetFactory.""" 203 pass 204 205 206class AudioWidgetFactory(object): 207 """ 208 This class provides methods to create widgets and binder of widgets. 209 User can use binder to setup audio paths. User can use widgets to control 210 record/playback on different ports on Cros device or Chameleon. 211 212 Properties: 213 _audio_facade: An AudioFacadeRemoteAdapter to access Cros device audio 214 functionality. This is created by the 215 'factory' argument passed to the constructor. 216 _display_facade: A DisplayFacadeRemoteAdapter to access Cros device 217 display functionality. This is created by the 218 'factory' argument passed to the constructor. 219 _system_facade: A SystemFacadeRemoteAdapter to access Cros device 220 system functionality. This is created by the 221 'factory' argument passed to the constructor. 222 _chameleon_board: A ChameleonBoard object to access Chameleon 223 functionality. 224 _link_factory: An AudioLinkFactory that creates link for widgets. 225 226 """ 227 def __init__(self, factory, cros_host): 228 """Initializes a AudioWidgetFactory 229 230 @param factory: A facade factory to access Cros device functionality. 231 Currently only audio facade is used, but we can access 232 other functionalities including display and video by 233 facades created by this facade factory. 234 @param cros_host: A CrosHost object to access Cros device. 235 236 """ 237 self._audio_facade = factory.create_audio_facade() 238 self._display_facade = factory.create_display_facade() 239 self._system_facade = factory.create_system_facade() 240 self._usb_facade = factory.create_usb_facade() 241 self._cros_host = cros_host 242 self._chameleon_board = cros_host.chameleon 243 self._link_factory = AudioLinkFactory(cros_host) 244 245 246 def create_widget(self, port_id, use_arc=False): 247 """Creates a AudioWidget given port id string. 248 249 @param port_id: A port id string defined in chameleon_audio_ids. 250 @param use_arc: For Cros widget, select if audio path exercises ARC. 251 Currently only input widget is supported. 252 253 @returns: An AudioWidget that is actually a 254 (Chameleon/Cros/Peripheral)(Input/Output)Widget. 255 256 """ 257 def _create_chameleon_handler(audio_port): 258 """Creates a ChameleonWidgetHandler for a given AudioPort. 259 260 @param audio_port: An AudioPort object. 261 262 @returns: A Chameleon(Input/Output)WidgetHandler depending on 263 role of audio_port. 264 265 """ 266 if audio_port.role == 'sink': 267 if audio_port.port_id == ids.ChameleonIds.HDMI: 268 return audio_widget.ChameleonHDMIInputWidgetHandler( 269 self._chameleon_board, audio_port.interface, 270 self._display_facade) 271 else: 272 return audio_widget.ChameleonInputWidgetHandler( 273 self._chameleon_board, audio_port.interface) 274 else: 275 if audio_port.port_id == ids.ChameleonIds.LINEOUT: 276 return audio_widget.ChameleonLineOutOutputWidgetHandler( 277 self._chameleon_board, audio_port.interface) 278 else: 279 return audio_widget.ChameleonOutputWidgetHandler( 280 self._chameleon_board, audio_port.interface) 281 282 def _create_cros_handler(audio_port): 283 """Creates a CrosWidgetHandler for a given AudioPort. 284 285 @param audio_port: An AudioPort object. 286 287 @returns: A Cros(Input/Output)WidgetHandler depending on 288 role of audio_port. 289 290 """ 291 is_usb = audio_port.port_id in [ 292 ids.CrosIds.USBIN, ids.CrosIds.USBOUT 293 ] 294 is_internal_mic = audio_port.port_id == ids.CrosIds.INTERNAL_MIC 295 is_hotwording = audio_port.port_id == ids.CrosIds.HOTWORDING 296 297 if audio_port.role == 'sink': 298 if use_arc: 299 return audio_widget_arc.CrosInputWidgetARCHandler( 300 self._audio_facade) 301 elif is_usb: 302 return audio_widget.CrosUSBInputWidgetHandler( 303 self._audio_facade) 304 elif is_hotwording: 305 return audio_widget.CrosHotwordingWidgetHandler( 306 self._audio_facade, self._system_facade) 307 else: 308 return audio_widget.CrosInputWidgetHandler( 309 self._audio_facade) 310 else: 311 if use_arc: 312 return audio_widget_arc.CrosOutputWidgetARCHandler( 313 self._audio_facade) 314 return audio_widget.CrosOutputWidgetHandler( 315 self._audio_facade) 316 317 def _create_audio_widget(audio_port, handler): 318 """Creates an AudioWidget for given AudioPort using WidgetHandler. 319 320 Creates an AudioWidget with the role of audio_port. Put 321 the widget handler into the widget so the widget can handle 322 action requests. 323 324 @param audio_port: An AudioPort object. 325 @param handler: A WidgetHandler object. 326 327 @returns: An Audio(Input/Output)Widget depending on 328 role of audio_port. 329 330 @raises: AudioWidgetFactoryError if fail to create widget. 331 332 """ 333 if audio_port.host in ['Chameleon', 'Cros']: 334 if audio_port.role == 'sink': 335 return audio_widget.AudioInputWidget(audio_port, handler) 336 else: 337 return audio_widget.AudioOutputWidget(audio_port, handler) 338 elif audio_port.host == 'Peripheral': 339 return audio_widget.PeripheralWidget(audio_port, handler) 340 else: 341 raise AudioWidgetFactoryError( 342 'The host %s is not valid' % audio_port.host) 343 344 345 audio_port = AudioPort(port_id) 346 if audio_port.host == 'Chameleon': 347 handler = _create_chameleon_handler(audio_port) 348 elif audio_port.host == 'Cros': 349 handler = _create_cros_handler(audio_port) 350 elif audio_port.host == 'Peripheral': 351 handler = audio_widget.PeripheralWidgetHandler() 352 353 return _create_audio_widget(audio_port, handler) 354 355 356 def _create_widget_binder(self, source, sink): 357 """Creates a WidgetBinder for two AudioWidgets. 358 359 @param source: An AudioWidget. 360 @param sink: An AudioWidget. 361 362 @returns: A WidgetBinder object. 363 364 """ 365 return audio_widget_link.WidgetBinder( 366 source, self._link_factory.create_link(source, sink), sink) 367 368 369 def create_binder(self, *widgets): 370 """Creates a WidgetBinder or a WidgetChainBinder for AudioWidgets. 371 372 @param widgets: A list of widgets that should be linked in a chain. 373 374 @returns: A WidgetBinder for two widgets. A WidgetBinderChain object 375 for three or more widgets. 376 377 """ 378 if len(widgets) == 2: 379 return self._create_widget_binder(widgets[0], widgets[1]) 380 binders = [] 381 for index in xrange(len(widgets) - 1): 382 binders.append( 383 self._create_widget_binder( 384 widgets[index], widgets[index + 1])) 385 386 return audio_widget_link.WidgetBinderChain(binders) 387 388 389@contextmanager 390def bind_widgets(binder): 391 """Context manager for widget binders. 392 393 Connects widgets in the beginning. Disconnects widgets and releases binder 394 in the end. 395 396 @param binder: A WidgetBinder object or a WidgetBinderChain object. 397 If binder is None, then do nothing. This is for test user's 398 convenience to reuse test logic among paths using binder 399 and paths not using binder. 400 401 E.g. with bind_widgets(binder): 402 do something on widget. 403 404 """ 405 if not binder: 406 yield 407 else: 408 try: 409 binder.connect() 410 yield 411 finally: 412 binder.disconnect() 413 binder.release() 414