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_link 13from autotest_lib.server.cros.bluetooth import bluetooth_device 14from autotest_lib.client.cros.chameleon import chameleon_audio_ids as ids 15from autotest_lib.client.cros.chameleon import chameleon_info 16 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() 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. Finally, the MAC address 178 # of bluetooth module is queried through chameleon_info because 179 # it is not probeable on Chameleon board. 180 181 # Initializes a BluetoothDevice object if needed. And reuse this 182 # object for future bluetooth link usage. 183 if not self._bluetooth_device: 184 self._bluetooth_device = bluetooth_device.BluetoothDevice( 185 self._cros_host) 186 187 link = link_type( 188 self._bluetooth_device, 189 self._audio_board.get_bluetooth_controller(), 190 chameleon_info.get_bluetooth_mac_address( 191 self._chameleon_board)) 192 elif issubclass(link_type, audio_widget_link.USBWidgetLink): 193 # Aside from managing connection between USB audio gadget driver on 194 # Chameleon with Cros device, USBWidgetLink also handles changing 195 # the gadget driver's configurations, through the USBController that 196 # is passed to it at initialization. 197 if not self._usb_ctrl: 198 self._usb_ctrl = self._chameleon_board.get_usb_controller() 199 200 link = link_type(self._usb_ctrl) 201 else: 202 raise NotImplementedError('Link %s is not implemented' % link_type) 203 204 return link 205 206 207class AudioWidgetFactoryError(Exception): 208 """Error in AudioWidgetFactory.""" 209 pass 210 211 212class AudioWidgetFactory(object): 213 """ 214 This class provides methods to create widgets and binder of widgets. 215 User can use binder to setup audio paths. User can use widgets to control 216 record/playback on different ports on Cros device or Chameleon. 217 218 Properties: 219 _audio_facade: An AudioFacadeRemoteAdapter to access Cros device audio 220 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._usb_facade = factory.create_usb_facade() 239 self._cros_host = cros_host 240 self._chameleon_board = cros_host.chameleon 241 self._link_factory = AudioLinkFactory(cros_host) 242 243 244 def create_widget(self, port_id): 245 """Creates a AudioWidget given port id string. 246 247 @param port_id: A port id string defined in chameleon_audio_ids. 248 249 @returns: An AudioWidget that is actually a 250 (Chameleon/Cros/Peripheral)(Input/Output)Widget. 251 252 """ 253 def _create_chameleon_handler(audio_port): 254 """Creates a ChameleonWidgetHandler for a given AudioPort. 255 256 @param audio_port: An AudioPort object. 257 258 @returns: A Chameleon(Input/Output)WidgetHandler depending on 259 role of audio_port. 260 261 """ 262 if audio_port.role == 'sink': 263 return audio_widget.ChameleonInputWidgetHandler( 264 self._chameleon_board, audio_port.interface) 265 else: 266 if audio_port.port_id == ids.ChameleonIds.LINEOUT: 267 return audio_widget.ChameleonLineOutOutputWidgetHandler( 268 self._chameleon_board, audio_port.interface) 269 else: 270 return audio_widget.ChameleonOutputWidgetHandler( 271 self._chameleon_board, audio_port.interface) 272 273 274 def _create_cros_handler(audio_port): 275 """Creates a CrosWidgetHandler for a given AudioPort. 276 277 @param audio_port: An AudioPort object. 278 279 @returns: A Cros(Input/Output)WidgetHandler depending on 280 role of audio_port. 281 282 """ 283 is_usb = audio_port.port_id in [ids.CrosIds.USBIN, 284 ids.CrosIds.USBOUT] 285 audio_board = self._chameleon_board.get_audio_board() 286 if audio_board: 287 jack_plugger = audio_board.get_jack_plugger() 288 else: 289 jack_plugger = None 290 291 if is_usb: 292 plug_handler = audio_widget.USBPlugHandler(self._usb_facade) 293 elif jack_plugger: 294 plug_handler = audio_widget.JackPluggerPlugHandler(jack_plugger) 295 else: 296 plug_handler = audio_widget.DummyPlugHandler() 297 298 if audio_port.role == 'sink': 299 if is_usb: 300 return audio_widget.CrosUSBInputWidgetHandler( 301 self._audio_facade, plug_handler) 302 else: 303 return audio_widget.CrosInputWidgetHandler( 304 self._audio_facade, plug_handler) 305 else: 306 return audio_widget.CrosOutputWidgetHandler(self._audio_facade, 307 plug_handler) 308 309 310 def _create_audio_widget(audio_port, handler): 311 """Creates an AudioWidget for given AudioPort using WidgetHandler. 312 313 Creates an AudioWidget with the role of audio_port. Put 314 the widget handler into the widget so the widget can handle 315 action requests. 316 317 @param audio_port: An AudioPort object. 318 @param handler: A WidgetHandler object. 319 320 @returns: An Audio(Input/Output)Widget depending on 321 role of audio_port. 322 323 @raises: AudioWidgetFactoryError if fail to create widget. 324 325 """ 326 if audio_port.host in ['Chameleon', 'Cros']: 327 if audio_port.role == 'sink': 328 return audio_widget.AudioInputWidget(audio_port, handler) 329 else: 330 return audio_widget.AudioOutputWidget(audio_port, handler) 331 elif audio_port.host == 'Peripheral': 332 return audio_widget.PeripheralWidget(audio_port, handler) 333 else: 334 raise AudioWidgetFactoryError( 335 'The host %s is not valid' % audio_port.host) 336 337 338 audio_port = AudioPort(port_id) 339 if audio_port.host == 'Chameleon': 340 handler = _create_chameleon_handler(audio_port) 341 elif audio_port.host == 'Cros': 342 handler = _create_cros_handler(audio_port) 343 elif audio_port.host == 'Peripheral': 344 handler = audio_widget.PeripheralWidgetHandler() 345 346 return _create_audio_widget(audio_port, handler) 347 348 349 def _create_widget_binder(self, source, sink): 350 """Creates a WidgetBinder for two AudioWidgets. 351 352 @param source: An AudioWidget. 353 @param sink: An AudioWidget. 354 355 @returns: A WidgetBinder object. 356 357 """ 358 return audio_widget_link.WidgetBinder( 359 source, self._link_factory.create_link(source, sink), sink) 360 361 362 def create_binder(self, *widgets): 363 """Creates a WidgetBinder or a WidgetChainBinder for AudioWidgets. 364 365 @param widgets: A list of widgets that should be linked in a chain. 366 367 @returns: A WidgetBinder for two widgets. A WidgetBinderChain object 368 for three or more widgets. 369 370 """ 371 if len(widgets) == 2: 372 return self._create_widget_binder(widgets[0], widgets[1]) 373 binders = [] 374 for index in xrange(len(widgets) - 1): 375 binders.append( 376 self._create_widget_binder( 377 widgets[index], widgets[index + 1])) 378 379 return audio_widget_link.WidgetBinderChain(binders) 380 381 382def compare_recorded_result(golden_file, recorder, method, parameters=None): 383 """Check recoded audio in a AudioInputWidget against a golden file. 384 385 Compares recorded data with golden data by cross correlation method. 386 Refer to audio_helper.compare_data for details of comparison. 387 388 @param golden_file: An AudioTestData object that serves as golden data. 389 @param recorder: An AudioInputWidget that has recorded some audio data. 390 @param method: The method to compare recorded result. Currently, 391 'correlation' and 'frequency' are supported. 392 @param parameters: A dict containing parameters for method. 393 394 @returns: True if the recorded data and golden data are similar enough. 395 396 """ 397 logging.info('Comparing recorded data with golden file %s ...', 398 golden_file.path) 399 return audio_helper.compare_data( 400 golden_file.get_binary(), golden_file.data_format, 401 recorder.get_binary(), recorder.data_format, recorder.channel_map, 402 method, parameters) 403 404 405@contextmanager 406def bind_widgets(binder): 407 """Context manager for widget binders. 408 409 Connects widgets in the beginning. Disconnects widgets and releases binder 410 in the end. 411 412 @param binder: A WidgetBinder object or a WidgetBinderChain object. 413 414 E.g. with bind_widgets(binder): 415 do something on widget. 416 417 """ 418 try: 419 binder.connect() 420 yield 421 finally: 422 binder.disconnect() 423 binder.release() 424