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 link between audio widgets.""" 6 7import logging 8import time 9 10from autotest_lib.client.cros.chameleon import audio_level 11from autotest_lib.client.cros.chameleon import chameleon_bluetooth_audio 12 13 14class WidgetBinderError(Exception): 15 """Error in WidgetBinder.""" 16 pass 17 18 19class WidgetBinder(object): 20 """ 21 This class abstracts the binding controls between two audio widgets. 22 23 ________ __________________ ______ 24 | | | link | | | 25 | source |------->| input output |------->| sink | 26 |________| |__________________| |______| 27 28 Properties: 29 _source: An AudioWidget object. The audio source. This should be 30 an output widget. 31 _sink: An AudioWidget object. The audio sink. This should be an 32 input widget. 33 _link: An WidgetLink object to link source and sink. 34 _connected: True if this binder is connected. 35 _level_controller: A LevelController to set scale and balance levels of 36 source and sink. 37 """ 38 def __init__(self, source, link, sink): 39 """Initializes a WidgetBinder. 40 41 After initialization, the binder is not connected, but the link 42 is occupied until it is released. 43 After connection, the channel map of link will be set to the sink 44 widget, and it will remains the same until the sink widget is connected 45 to a different link. This is to make sure sink widget knows the channel 46 map of recorded data even after link is disconnected or released. 47 48 @param source: An AudioWidget object for audio source. 49 @param link: A WidgetLink object to connect source and sink. 50 @param sink: An AudioWidget object for audio sink. 51 52 """ 53 self._source = source 54 self._link = link 55 self._sink = sink 56 self._connected = False 57 self._link.occupied = True 58 self._level_controller = audio_level.LevelController( 59 self._source, self._sink) 60 61 62 def connect(self): 63 """Connects source and sink to link.""" 64 if self._connected: 65 return 66 67 logging.info('Connecting %s to %s', self._source.audio_port, 68 self._sink.audio_port) 69 self._link.connect(self._source, self._sink) 70 self._connected = True 71 # Sets channel map of link to the sink widget so 72 # sink widget knows the channel map of recorded data. 73 self._sink.channel_map = self._link.channel_map 74 self._level_controller.set_scale() 75 76 77 def disconnect(self): 78 """Disconnects source and sink from link.""" 79 if not self._connected: 80 return 81 82 logging.info('Disconnecting %s from %s', self._source.audio_port, 83 self._sink.audio_port) 84 self._link.disconnect(self._source, self._sink) 85 self._connected = False 86 self._level_controller.reset() 87 88 89 def release(self): 90 """Releases the link used by this binder. 91 92 @raises: WidgetBinderError if this binder is still connected. 93 94 """ 95 if self._connected: 96 raise WidgetBinderError('Can not release while connected') 97 self._link.occupied = False 98 99 100 def get_link(self): 101 """Returns the link controlled by this binder. 102 103 The link provides more controls than binder so user can do 104 more complicated tests. 105 106 @returns: An object of subclass of WidgetLink. 107 108 """ 109 return self._link 110 111 112class WidgetLinkError(Exception): 113 """Error in WidgetLink.""" 114 pass 115 116 117class WidgetLink(object): 118 """ 119 This class abstracts the link between two audio widgets. 120 121 Properties: 122 name: A string. The link name. 123 occupied: True if this widget is occupied by a widget binder. 124 channel_map: A list containing current channel map. Checks docstring 125 of channel_map method of AudioInputWidget for details. 126 127 """ 128 def __init__(self): 129 self.name = 'Unknown' 130 self.occupied = False 131 self.channel_map = None 132 133 134 def _check_widget_id(self, port_id, widget): 135 """Checks that the port id of a widget is expected. 136 137 @param port_id: An id defined in chameleon_audio_ids. 138 @param widget: An AudioWidget object. 139 140 @raises: WidgetLinkError if the port id of widget is not expected. 141 """ 142 if widget.audio_port.port_id != port_id: 143 raise WidgetLinkError( 144 'Link %s expects a %s widget, but gets a %s widget' % ( 145 self.name, port_id, widget.audio_port.port_id)) 146 147 148 def connect(self, source, sink): 149 """Connects source widget to sink widget. 150 151 @param source: An AudioWidget object. 152 @param sink: An AudioWidget object. 153 154 """ 155 self._plug_input(source) 156 self._plug_output(sink) 157 158 159 def disconnect(self, source, sink): 160 """Disconnects source widget from sink widget. 161 162 @param source: An AudioWidget object. 163 @param sink: An AudioWidget object. 164 165 """ 166 self._unplug_input(source) 167 self._unplug_output(sink) 168 169 170class AudioBusLink(WidgetLink): 171 """The abstraction of widget link using audio bus on audio board. 172 173 This class handles the Audio bus routing. 174 175 Properties: 176 _audio_bus: An AudioBus object. 177 178 """ 179 def __init__(self, audio_bus): 180 """Initializes an AudioBusLink. 181 182 @param audio_bus: An AudioBus object. 183 """ 184 super(AudioBusLink, self).__init__() 185 self._audio_bus = audio_bus 186 logging.debug('Create an AudioBusLink with bus index %d', 187 audio_bus.bus_index) 188 189 190 def _plug_input(self, widget): 191 """Plugs input of audio bus to the widget. 192 193 @param widget: An AudioWidget object. 194 195 """ 196 self._audio_bus.connect(widget.audio_port.port_id) 197 198 logging.info( 199 'Plugged audio board bus %d input to %s', 200 self._audio_bus.bus_index, widget.audio_port) 201 202 203 def _unplug_input(self, widget): 204 """Unplugs input of audio bus from the widget. 205 206 @param widget: An AudioWidget object. 207 208 """ 209 self._audio_bus.disconnect(widget.audio_port.port_id) 210 211 logging.info( 212 'Unplugged audio board bus %d input from %s', 213 self._audio_bus.bus_index, widget.audio_port) 214 215 216 def _plug_output(self, widget): 217 """Plugs output of audio bus to the widget. 218 219 @param widget: An AudioWidget object. 220 221 """ 222 self._audio_bus.connect(widget.audio_port.port_id) 223 224 logging.info( 225 'Plugged audio board bus %d output to %s', 226 self._audio_bus.bus_index, widget.audio_port) 227 228 229 def _unplug_output(self, widget): 230 """Unplugs output of audio bus from the widget. 231 232 @param widget: An AudioWidget object. 233 234 """ 235 self._audio_bus.disconnect(widget.audio_port.port_id) 236 logging.info( 237 'Unplugged audio board bus %d output from %s', 238 self._audio_bus.bus_index, widget.audio_port) 239 240 241 def disconnect_audio_bus(self): 242 """Disconnects all audio ports from audio bus. 243 244 A snapshot of audio bus is retained so we can reconnect audio bus 245 later. 246 This method is useful when user wants to let Cros device detects 247 audio jack after this link is connected. Some Cros devices 248 have sensitive audio jack detection mechanism such that plugger of 249 audio board can only be detected when audio bus is disconnected. 250 251 """ 252 self._audio_bus_snapshot = self._audio_bus.get_snapshot() 253 self._audio_bus.clear() 254 255 256 def reconnect_audio_bus(self): 257 """Reconnects audio ports to audio bus using snapshot.""" 258 self._audio_bus.restore_snapshot(self._audio_bus_snapshot) 259 260 261class AudioBusToChameleonLink(AudioBusLink): 262 """The abstraction for bus on audio board that is connected to Chameleon.""" 263 # This is the default channel map for 2-channel data recorded on 264 # Chameleon through audio board. 265 _DEFAULT_CHANNEL_MAP = [1, 0, None, None, None, None, None, None] 266 267 def __init__(self, *args, **kwargs): 268 super(AudioBusToChameleonLink, self).__init__( 269 *args, **kwargs) 270 self.name = ('Audio board bus %s to Chameleon' % 271 self._audio_bus.bus_index) 272 self.channel_map = self._DEFAULT_CHANNEL_MAP 273 logging.debug( 274 'Create an AudioBusToChameleonLink named %s with ' 275 'channel map %r', self.name, self.channel_map) 276 277 278class AudioBusChameleonToPeripheralLink(AudioBusLink): 279 """The abstraction for audio bus connecting Chameleon to peripheral.""" 280 # This is the channel map which maps 2-channel data at peripehral speaker 281 # to 8 channel data at Chameleon. 282 # The left channel at speaker comes from the second channel at Chameleon. 283 # The right channel at speaker comes from the first channel at Chameleon. 284 # Other channels at Chameleon are neglected. 285 _DEFAULT_CHANNEL_MAP = [1, 0] 286 287 def __init__(self, *args, **kwargs): 288 super(AudioBusChameleonToPeripheralLink, self).__init__( 289 *args, **kwargs) 290 self.name = 'Audio board bus %s to peripheral' % self._audio_bus.bus_index 291 self.channel_map = self._DEFAULT_CHANNEL_MAP 292 logging.debug( 293 'Create an AudioBusToPeripheralLink named %s with ' 294 'channel map %r', self.name, self.channel_map) 295 296 297class AudioBusToCrosLink(AudioBusLink): 298 """The abstraction for audio bus that is connected to Cros device.""" 299 # This is the default channel map for 1-channel data recorded on 300 # Cros device. 301 _DEFAULT_CHANNEL_MAP = [0] 302 303 def __init__(self, *args, **kwargs): 304 super(AudioBusToCrosLink, self).__init__( 305 *args, **kwargs) 306 self.name = 'Audio board bus %s to Cros' % self._audio_bus.bus_index 307 self.channel_map = self._DEFAULT_CHANNEL_MAP 308 logging.debug( 309 'Create an AudioBusToCrosLink named %s with ' 310 'channel map %r', self.name, self.channel_map) 311 312 313class USBWidgetLink(WidgetLink): 314 """The abstraction for USB Cable.""" 315 316 # This is the default channel map for 2-channel data 317 _DEFAULT_CHANNEL_MAP = [0, 1] 318 # Wait some time for Cros device to detect USB has been plugged. 319 _DELAY_AFTER_PLUGGING_SECS = 2.0 320 321 def __init__(self, usb_ctrl): 322 """Initializes a USBWidgetLink. 323 324 @param usb_ctrl: A USBController object. 325 326 """ 327 super(USBWidgetLink, self).__init__() 328 self.name = 'USB Cable' 329 self.channel_map = self._DEFAULT_CHANNEL_MAP 330 self._usb_ctrl = usb_ctrl 331 logging.debug( 332 'Create a USBWidgetLink. Do nothing because USB cable' 333 ' is dedicated') 334 335 336 def connect(self, source, sink): 337 """Connects source widget to sink widget. 338 339 @param source: An AudioWidget object. 340 @param sink: An AudioWidget object. 341 342 """ 343 source.handler.plug() 344 sink.handler.plug() 345 time.sleep(self._DELAY_AFTER_PLUGGING_SECS) 346 347 348 def disconnect(self, source, sink): 349 """Disconnects source widget from sink widget. 350 351 @param source: An AudioWidget object. 352 @param sink: An AudioWidget object. 353 354 """ 355 source.handler.unplug() 356 sink.handler.unplug() 357 358 359class USBToCrosWidgetLink(USBWidgetLink): 360 """The abstraction for the USB cable connected to the Cros device.""" 361 362 def __init__(self, *args, **kwargs): 363 """Initializes a USBToCrosWidgetLink.""" 364 super(USBToCrosWidgetLink, self).__init__(*args, **kwargs) 365 self.name = 'USB Cable to Cros' 366 logging.debug('Create a USBToCrosWidgetLink: %s', self.name) 367 368 369class USBToChameleonWidgetLink(USBWidgetLink): 370 """The abstraction for the USB cable connected to the Chameleon device.""" 371 372 def __init__(self, *args, **kwargs): 373 """Initializes a USBToChameleonWidgetLink.""" 374 super(USBToChameleonWidgetLink, self).__init__(*args, **kwargs) 375 self.name = 'USB Cable to Chameleon' 376 logging.debug('Create a USBToChameleonWidgetLink: %s', self.name) 377 378 379class HDMIWidgetLink(WidgetLink): 380 """The abstraction for HDMI cable.""" 381 382 # This is the default channel map for 2-channel data recorded on 383 # Chameleon through HDMI cable. 384 _DEFAULT_CHANNEL_MAP = [1, 0, None, None, None, None, None, None] 385 _DELAY_AFTER_PLUG_SECONDS = 6 386 387 def __init__(self, cros_host): 388 """Initializes a HDMI widget link. 389 390 @param cros_host: A CrosHost object to access Cros device. 391 392 """ 393 super(HDMIWidgetLink, self).__init__() 394 self.name = 'HDMI cable' 395 self.channel_map = self._DEFAULT_CHANNEL_MAP 396 self._cros_host = cros_host 397 logging.debug( 398 'Create an HDMIWidgetLink. Do nothing because HDMI cable' 399 ' is dedicated') 400 401 402 # TODO(cychiang) remove this when issue crbug.com/450101 is fixed. 403 def _correction_plug_unplug_for_audio(self, handler): 404 """Plugs/unplugs several times for Cros device to detect audio. 405 406 For issue crbug.com/450101, Exynos HDMI driver has problem recognizing 407 HDMI audio, while display can be detected. Do several plug/unplug and 408 wait as a workaround. Note that HDMI port will be in unplugged state 409 in the end if extra plug/unplug is needed. 410 We have seen this on Intel device(cyan, celes) too. 411 412 @param handler: A ChameleonHDMIInputWidgetHandler. 413 414 """ 415 board = self._cros_host.get_board().split(':')[1] 416 if board in ['cyan', 'celes', 'lars']: 417 logging.info('Need extra plug/unplug on board %s', board) 418 for _ in xrange(3): 419 handler.plug() 420 time.sleep(3) 421 handler.unplug() 422 time.sleep(3) 423 424 425 def connect(self, source, sink): 426 """Connects source widget to sink widget. 427 428 @param source: An AudioWidget object. 429 @param sink: An AudioWidget object. 430 431 """ 432 sink.handler.set_edid_for_audio() 433 self._correction_plug_unplug_for_audio(sink.handler) 434 sink.handler.plug() 435 time.sleep(self._DELAY_AFTER_PLUG_SECONDS) 436 437 438 def disconnect(self, source, sink): 439 """Disconnects source widget from sink widget. 440 441 @param source: An AudioWidget object. 442 @param sink: An AudioWidget object. 443 444 """ 445 sink.handler.unplug() 446 sink.handler.restore_edid() 447 448 449class BluetoothWidgetLink(WidgetLink): 450 """The abstraction for bluetooth link between Cros device and bt module.""" 451 # The delay after connection for cras to process the bluetooth connection 452 # event and enumerate the bluetooth nodes. 453 _DELAY_AFTER_CONNECT_SECONDS = 15 454 455 def __init__(self, bt_adapter, audio_board_bt_ctrl, mac_address): 456 """Initializes a BluetoothWidgetLink. 457 458 @param bt_adapter: A BluetoothDevice object to control bluetooth 459 adapter on Cros device. 460 @param audio_board_bt_ctrl: A BlueoothController object to control 461 bluetooth module on audio board. 462 @param mac_address: The MAC address of bluetooth module on audio board. 463 464 """ 465 super(BluetoothWidgetLink, self).__init__() 466 self._bt_adapter = bt_adapter 467 self._audio_board_bt_ctrl = audio_board_bt_ctrl 468 self._mac_address = mac_address 469 470 471 def connect(self, source, sink): 472 """Customizes the connecting sequence for bluetooth widget link. 473 474 We need to enable bluetooth module first, then start connecting 475 sequence from bluetooth adapter. 476 The arguments source and sink are not used because BluetoothWidgetLink 477 already has the access to bluetooth module on audio board and 478 bluetooth adapter on Cros device. 479 480 @param source: An AudioWidget object. 481 @param sink: An AudioWidget object. 482 483 """ 484 self.enable_bluetooth_module() 485 self._adapter_connect_sequence() 486 time.sleep(self._DELAY_AFTER_CONNECT_SECONDS) 487 488 489 def disconnect(self, source, sink): 490 """Customizes the disconnecting sequence for bluetooth widget link. 491 492 The arguments source and sink are not used because BluetoothWidgetLink 493 already has the access to bluetooth module on audio board and 494 bluetooth adapter on Cros device. 495 496 @param source: An AudioWidget object (unused). 497 @param sink: An AudioWidget object (unused). 498 499 """ 500 self.disable_bluetooth_module() 501 self.adapter_disconnect_module() 502 503 504 def enable_bluetooth_module(self): 505 """Reset bluetooth module if it is not enabled.""" 506 self._audio_board_bt_ctrl.reset() 507 508 509 def disable_bluetooth_module(self): 510 """Disables bluetooth module if it is enabled.""" 511 if self._audio_board_bt_ctrl.is_enabled(): 512 self._audio_board_bt_ctrl.disable() 513 514 515 def _adapter_connect_sequence(self): 516 """Scans, pairs, and connects bluetooth module to bluetooth adapter. 517 518 If the device is already connected, skip the connection sequence. 519 520 """ 521 if self._bt_adapter.device_is_connected(self._mac_address): 522 logging.debug( 523 '%s is already connected, skip the connection sequence', 524 self._mac_address) 525 return 526 chameleon_bluetooth_audio.connect_bluetooth_module_full_flow( 527 self._bt_adapter, self._mac_address) 528 529 530 def _disable_adapter(self): 531 """Turns off bluetooth adapter.""" 532 self._bt_adapter.reset_off() 533 534 535 def adapter_connect_module(self): 536 """Controls adapter to connect bluetooth module.""" 537 chameleon_bluetooth_audio.connect_bluetooth_module( 538 self._bt_adapter, self._mac_address) 539 540 def adapter_disconnect_module(self): 541 """Controls adapter to disconnect bluetooth module.""" 542 self._bt_adapter.disconnect_device(self._mac_address) 543 544 545class BluetoothHeadphoneWidgetLink(BluetoothWidgetLink): 546 """The abstraction for link from Cros device headphone to bt module Rx.""" 547 548 def __init__(self, *args, **kwargs): 549 """Initializes a BluetoothHeadphoneWidgetLink.""" 550 super(BluetoothHeadphoneWidgetLink, self).__init__(*args, **kwargs) 551 self.name = 'Cros bluetooth headphone to peripheral bluetooth module' 552 logging.debug('Create an BluetoothHeadphoneWidgetLink: %s', self.name) 553 554 555class BluetoothMicWidgetLink(BluetoothWidgetLink): 556 """The abstraction for link from bt module Tx to Cros device microphone.""" 557 558 # This is the default channel map for 1-channel data recorded on 559 # Cros device using bluetooth microphone. 560 _DEFAULT_CHANNEL_MAP = [0] 561 562 def __init__(self, *args, **kwargs): 563 """Initializes a BluetoothMicWidgetLink.""" 564 super(BluetoothMicWidgetLink, self).__init__(*args, **kwargs) 565 self.name = 'Peripheral bluetooth module to Cros bluetooth mic' 566 self.channel_map = self._DEFAULT_CHANNEL_MAP 567 logging.debug('Create an BluetoothMicWidgetLink: %s', self.name) 568 569 570class WidgetBinderChain(object): 571 """Abstracts a chain of binders. 572 573 This class supports connect, disconnect, release, just like WidgetBinder, 574 except that this class handles a chain of WidgetBinders. 575 576 """ 577 def __init__(self, binders): 578 """Initializes a WidgetBinderChain. 579 580 @param binders: A list of WidgetBinder. 581 582 """ 583 self._binders = binders 584 585 586 def connect(self): 587 """Asks all binders to connect.""" 588 for binder in self._binders: 589 binder.connect() 590 591 592 def disconnect(self): 593 """Asks all binders to disconnect.""" 594 for binder in self._binders: 595 binder.disconnect() 596 597 598 def release(self): 599 """Asks all binders to release.""" 600 for binder in self._binders: 601 binder.release() 602 603 604 def get_binders(self): 605 """Returns all the binders. 606 607 @returns: A list of binders. 608 609 """ 610 return self._binders 611