chameleon_audio_helper.py revision d690b0a62b7b5e3732bd009eaa29369cab786c21
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 # TODO(cychiang): Add link for other widget pairs. 104 } 105 106 def __init__(self, cros_host): 107 """Initializes an AudioLinkFactory. 108 109 @param cros_host: A CrosHost object to access Cros device. 110 111 """ 112 # There are two audio buses on audio board. Initializes these links 113 # to None. They may be changed to objects of AudioBusLink's subclass. 114 self._audio_bus_links = {1: None, 2: None} 115 self._cros_host = cros_host 116 self._chameleon_board = cros_host.chameleon 117 self._audio_board = self._chameleon_board.get_audio_board() 118 self._bluetooth_device = None 119 120 121 def _acquire_audio_bus_index(self): 122 """Acquires an available audio bus index that is not occupied yet. 123 124 @returns: A number. 125 126 @raises: AudioLinkFactoryError if there is no available 127 audio bus. 128 """ 129 for index, bus in self._audio_bus_links.iteritems(): 130 if not (bus and bus.occupied): 131 return index 132 133 raise AudioLinkFactoryError('No available audio bus') 134 135 136 def create_link(self, source, sink): 137 """Creates a widget link for two audio widgets. 138 139 @param source: An AudioWidget. 140 @param sink: An AudioWidget. 141 142 @returns: An object of WidgetLink's subclass. 143 144 @raises: AudioLinkFactoryError if there is no link between 145 source and sink. 146 147 """ 148 # Finds the available link types from LINK_TABLE. 149 link_type = self.LINK_TABLE.get((source.port_id, sink.port_id), None) 150 if not link_type: 151 raise AudioLinkFactoryError( 152 'No supported link between %s and %s' % ( 153 source.port_id, sink.port_id)) 154 155 # There is only one dedicated HDMI cable, just use it. 156 if link_type == audio_widget_link.HDMIWidgetLink: 157 link = audio_widget_link.HDMIWidgetLink() 158 159 # Acquires audio bus if there is available bus. 160 # Creates a bus of AudioBusLink's subclass that is more 161 # specific than AudioBusLink. 162 # Controls this link using AudioBus object obtained from AudioBoard 163 # object. 164 # Plugs/unplugs 3.5mm jack using AudioJackPlugger object obtained from 165 # AudioBoard object. 166 elif issubclass(link_type, audio_widget_link.AudioBusLink): 167 bus_index = self._acquire_audio_bus_index() 168 link = link_type( 169 self._audio_board.get_audio_bus(bus_index), 170 self._audio_board.get_jack_plugger()) 171 self._audio_bus_links[bus_index] = link 172 elif issubclass(link_type, audio_widget_link.BluetoothWidgetLink): 173 # To connect bluetooth adapter on Cros device to bluetooth module on 174 # chameleon board, we need to access bluetooth adapter on Cros host 175 # using BluetoothDevice, and access bluetooth module on 176 # audio board using BluetoothController. Finally, the MAC address 177 # of bluetooth module is queried through chameleon_info because 178 # it is not probeable on Chameleon board. 179 180 # Initializes a BluetoothDevice object if needed. And reuse this 181 # object for future bluetooth link usage. 182 if not self._bluetooth_device: 183 self._bluetooth_device = bluetooth_device.BluetoothDevice( 184 self._cros_host) 185 186 link = link_type( 187 self._bluetooth_device, 188 self._audio_board.get_bluetooth_controller(), 189 chameleon_info.get_bluetooth_mac_address( 190 self._chameleon_board)) 191 else: 192 raise NotImplementedError('Link %s is not implemented' % link_type) 193 194 return link 195 196 197class AudioWidgetFactoryError(Exception): 198 """Error in AudioWidgetFactory.""" 199 pass 200 201 202class AudioWidgetFactory(object): 203 """ 204 This class provides methods to create widgets and binder of widgets. 205 User can use binder to setup audio paths. User can use widgets to control 206 record/playback on different ports on Cros device or Chameleon. 207 208 Properties: 209 _audio_facade: An AudioFacadeRemoteAdapter to access Cros device audio 210 functionality. This is created by the 211 'factory' argument passed to the constructor. 212 _chameleon_board: A ChameleonBoard object to access Chameleon 213 functionality. 214 _link_factory: An AudioLinkFactory that creates link for widgets. 215 216 """ 217 def __init__(self, factory, cros_host): 218 """Initializes a AudioWidgetFactory 219 220 @param factory: A facade factory to access Cros device functionality. 221 Currently only audio facade is used, but we can access 222 other functionalities including display and video by 223 facades created by this facade factory. 224 @param cros_host: A CrosHost object to access Cros device. 225 226 """ 227 self._audio_facade = factory.create_audio_facade() 228 self._cros_host = cros_host 229 self._chameleon_board = cros_host.chameleon 230 self._link_factory = AudioLinkFactory(cros_host) 231 232 233 def create_widget(self, port_id): 234 """Creates a AudioWidget given port id string. 235 236 @param port_id: A port id string defined in chameleon_audio_ids. 237 238 @returns: An AudioWidget that is actually a 239 (Chameleon/Cros/Peripheral)(Input/Output)Widget. 240 241 """ 242 def _create_chameleon_handler(audio_port): 243 """Creates a ChameleonWidgetHandler for a given AudioPort. 244 245 @param audio_port: An AudioPort object. 246 247 @returns: A Chameleon(Input/Output)WidgetHandler depending on 248 role of audio_port. 249 250 """ 251 if audio_port.role == 'sink': 252 return audio_widget.ChameleonInputWidgetHandler( 253 self._chameleon_board, audio_port.interface) 254 else: 255 return audio_widget.ChameleonOutputWidgetHandler( 256 self._chameleon_board, audio_port.interface) 257 258 259 def _create_cros_handler(audio_port): 260 """Creates a CrosWidgetHandler for a given AudioPort. 261 262 @param audio_port: An AudioPort object. 263 264 @returns: A Cros(Input/Output)WidgetHandler depending on 265 role of audio_port. 266 267 """ 268 if audio_port.role == 'sink': 269 return audio_widget.CrosInputWidgetHandler(self._audio_facade) 270 else: 271 return audio_widget.CrosOutputWidgetHandler(self._audio_facade) 272 273 274 def _create_audio_widget(audio_port, handler): 275 """Creates an AudioWidget for given AudioPort using WidgetHandler. 276 277 Creates an AudioWidget with the role of audio_port. Put 278 the widget handler into the widget so the widget can handle 279 action requests. 280 281 @param audio_port: An AudioPort object. 282 @param handler: A WidgetHandler object. 283 284 @returns: An Audio(Input/Output)Widget depending on 285 role of audio_port. 286 287 @raises: AudioWidgetFactoryError if fail to create widget. 288 289 """ 290 if audio_port.host in ['Chameleon', 'Cros']: 291 if audio_port.role == 'sink': 292 return audio_widget.AudioInputWidget(audio_port, handler) 293 else: 294 return audio_widget.AudioOutputWidget(audio_port, handler) 295 elif audio_port.host == 'Peripheral': 296 return audio_widget.PeripheralWidget(audio_port, handler) 297 else: 298 raise AudioWidgetFactoryError( 299 'The host %s is not valid' % audio_port.host) 300 301 302 audio_port = AudioPort(port_id) 303 if audio_port.host == 'Chameleon': 304 handler = _create_chameleon_handler(audio_port) 305 elif audio_port.host == 'Cros': 306 handler = _create_cros_handler(audio_port) 307 elif audio_port.host == 'Peripheral': 308 handler = audio_widget.PeripheralWidgetHandler() 309 310 return _create_audio_widget(audio_port, handler) 311 312 313 def _create_widget_binder(self, source, sink): 314 """Creates a WidgetBinder for two AudioWidgets. 315 316 @param source: An AudioWidget. 317 @param sink: An AudioWidget. 318 319 @returns: A WidgetBinder object. 320 321 """ 322 return audio_widget_link.WidgetBinder( 323 source, self._link_factory.create_link(source, sink), sink) 324 325 326 def create_binder(self, *widgets): 327 """Creates a WidgetBinder or a WidgetChainBinder for AudioWidgets. 328 329 @param widgets: A list of widgets that should be linked in a chain. 330 331 @returns: A WidgetBinder for two widgets. A WidgetBinderChain object 332 for three or more widgets. 333 334 """ 335 if len(widgets) == 2: 336 return self._create_widget_binder(widgets[0], widgets[1]) 337 binders = [] 338 for index in xrange(len(widgets) - 1): 339 binders.append( 340 self._create_widget_binder( 341 widgets[index], widgets[index + 1])) 342 343 return audio_widget_link.WidgetBinderChain(binders) 344 345 346def compare_recorded_result(golden_file, recorder, method): 347 """Check recoded audio in a AudioInputWidget against a golden file. 348 349 Compares recorded data with golden data by cross correlation method. 350 Refer to audio_helper.compare_data for details of comparison. 351 352 @param golden_file: An AudioTestData object that serves as golden data. 353 @param recorder: An AudioInputWidget that has recorded some audio data. 354 @param method: The method to compare recorded result. Currently, 355 'correlation' and 'frequency' are supported. 356 357 @returns: True if the recorded data and golden data are similar enough. 358 359 """ 360 logging.info('Comparing recorded data with golden file %s ...', 361 golden_file.path) 362 return audio_helper.compare_data( 363 golden_file.get_binary(), golden_file.data_format, 364 recorder.get_binary(), recorder.data_format, recorder.channel_map, 365 method) 366 367 368@contextmanager 369def bind_widgets(binder): 370 """Context manager for widget binders. 371 372 Connects widgets in the beginning. Disconnects widgets and releases binder 373 in the end. 374 375 @param binder: A WidgetBinder object or a WidgetBinderChain object. 376 377 E.g. with bind_widgets(binder): 378 do something on widget. 379 380 """ 381 try: 382 binder.connect() 383 yield 384 finally: 385 binder.disconnect() 386 binder.release() 387