• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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