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