chameleon_audio_helper.py revision 17a2527ea44ed461cec2cf62577079f59e00a9b3
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.client.cros.chameleon import chameleon_audio_ids as ids
14
15
16class AudioPort(object):
17    """
18    This class abstracts an audio port in audio test framework. A port is
19    identified by its host and interface. Available hosts and interfaces
20    are listed in chameleon_audio_ids.
21
22    Properties:
23        port_id: The port id defined in chameleon_audio_ids.
24        host: The host of this audio port, e.g. 'Chameleon', 'Cros',
25              'Peripheral'.
26        interface: The interface of this audio port, e.g. 'HDMI', 'Headphone'.
27        direction: The direction of this audio port, that is, 'Input' or
28                   'Output'. Note that bidirectional interface like 3.5mm
29                   jack is separated to two interfaces 'Headphone' and
30                   'External Mic'.
31
32    """
33    def __init__(self, port_id):
34        """Initialize an AudioPort with port id string.
35
36        @param port_id: A port id string defined in chameleon_audio_ids.
37
38        """
39        logging.debug('Creating AudioPort with port_id: %s', port_id)
40        self.port_id = port_id
41        self.host = ids.get_host(port_id)
42        self.interface = ids.get_interface(port_id)
43        self.direction = ids.get_direction(port_id)
44        logging.debug('Created AudioPort: %s', self)
45
46
47    def __str__(self):
48        """String representation of audio port.
49
50        @returns: The string representation of audio port which is composed by
51                  host, interface, and direction.
52
53        """
54        return '( %s | %s | %s )' % (self.host, self.interface, self.direction)
55
56
57class AudioLinkFactoryError(Exception):
58    """Error in AudioLinkFactory."""
59    pass
60
61
62class AudioLinkFactory(object):
63    """
64    This class provides method to create link that connects widgets.
65    This is used by AudioWidgetFactory when user wants to create binder for
66    widgets.
67
68    Properties:
69        _audio_buses: A dict containing mapping from index number
70                      to object of AudioBusLink's subclass.
71
72    """
73
74    # Maps pair of widgets to widget link of different type.
75    LINK_TABLE = {
76        (ids.CrosIds.HDMI, ids.ChameleonIds.HDMI):
77                audio_widget_link.HDMIWidgetLink,
78        (ids.CrosIds.HEADPHONE, ids.ChameleonIds.LINEIN):
79                audio_widget_link.AudioBusToChameleonLink,
80        # TODO(cychiang): Add link for other widget pairs.
81    }
82
83    def __init__(self):
84        # There are two audio buses on audio board. Initializes them to
85        # None. They may be changed to objects of AudioBusLink's subclass.
86        self._audio_buses = {0: None, 1: None}
87
88
89    def _acquire_audio_bus_index(self):
90        """Acquires an available audio bus index that is not occupied yet.
91
92        @returns: A number.
93
94        @raises: AudioLinkFactoryError if there is no available
95                 audio bus.
96        """
97        for index, bus in self._audio_buses.iteritems():
98            if not (bus and bus.occupied):
99                return index
100
101        raise AudioLinkFactoryError('No available audio bus')
102
103
104    def create_link(self, source, sink):
105        """Creates a widget link for two audio widgets.
106
107        @param source: An AudioWidget.
108        @param sink: An AudioWidget.
109
110        @returns: An object of WidgetLink's subclass.
111
112        @raises: AudioLinkFactoryError if there is no link between
113            source and sink.
114
115        """
116        # Finds the available link types from LINK_TABLE.
117        link_type = self.LINK_TABLE.get((source.port_id, sink.port_id), None)
118        if not link_type:
119            raise AudioLinkFactoryError(
120                    'No supported link between %s and %s' % (
121                            source.pord_id, sink.port_id))
122
123        # There is only one dedicated HDMI cable, just use it.
124        if link_type == audio_widget_link.HDMIWidgetLink:
125            link = audio_widget_link.HDMIWidgetLink()
126
127        # Acquires audio bus if there is available bus.
128        # Creates a bus of AudioBusLink's subclass that is more
129        # specific than AudioBusLink.
130        elif issubclass(link_type, audio_widget_link.AudioBusLink):
131            bus_index = self._acquire_audio_bus_index()
132            link = link_type(bus_index)
133            self._audio_buses[bus_index] = link
134        else:
135            raise NotImplementedError('Link %s is not implemented' % link_type)
136
137        return link
138
139
140class AudioWidgetFactory(object):
141    """
142    This class provides methods to create widgets and binder of widgets.
143    User can use binder to setup audio paths. User can use widgets to control
144    record/playback on different ports on Cros device or Chameleon.
145
146    Properties:
147        _audio_facade: An AudioFacadeRemoteAdapter to access Cros device audio
148                       functionality. This is created by the
149                       'factory' argument passed to the constructor.
150        _chameleon_board: A ChameleonBoard object to access Chameleon
151                          functionality.
152        _link_factory: An AudioLinkFactory that creates link for widgets.
153
154    """
155    def __init__(self, chameleon_board, factory):
156        """Initializes a AudioWidgetFactory
157
158        @param chameleon_board: A ChameleonBoard object to access Chameleon
159                                functionality.
160        @param factory: A facade factory to access Cros device functionality.
161                        Currently only audio facade is used, but we can access
162                        other functionalities including display and video by
163                        facades created by this facade factory.
164
165        """
166        self._audio_facade = factory.create_audio_facade()
167        self._chameleon_board = chameleon_board
168        self._link_factory = AudioLinkFactory()
169
170
171    def create_widget(self, port_id):
172        """Creates a AudioWidget given port id string.
173
174        @param port_id: A port id string defined in chameleon_audio_ids.
175
176        @returns: An AudioWidget that is actually a
177                  (Chameleon/Cros/Peripheral)(Input/Output)Widget.
178
179        """
180        def _create_chameleon_handler(audio_port):
181            """Creates a ChameleonWidgetHandler for a given AudioPort.
182
183            @param audio_port: An AudioPort object.
184
185            @returns: A Chameleon(Input/Output)WidgetHandler depending on
186                      direction of audio_port.
187
188            """
189            if audio_port.direction == 'Input':
190                return audio_widget.ChameleonInputWidgetHandler(
191                        self._chameleon_board, audio_port.interface)
192            else:
193                return audio_widget.ChameleonOutputWidgetHandler(
194                        self._chameleon_board, audio_port.interface)
195
196
197        def _create_cros_handler(audio_port):
198            """Creates a CrosWidgetHandler for a given AudioPort.
199
200            @param audio_port: An AudioPort object.
201
202            @returns: A Cros(Input/Output)WidgetHandler depending on
203                      direction of audio_port.
204
205            """
206            if audio_port.direction == 'Input':
207                return audio_widget.CrosInputWidgetHandler(self._audio_facade)
208            else:
209                return audio_widget.CrosOutputWidgetHandler(self._audio_facade)
210
211
212        def _create_audio_widget(audio_port, handler):
213            """Creates an AudioWidget for given AudioPort using WidgetHandler.
214
215            Creates an AudioWidget with the direction of audio_port. Put
216            the widget handler into the widget so the widget can handle
217            action requests.
218
219            @param audio_port: An AudioPort object.
220            @param handler: A WidgetHandler object.
221
222            @returns: An Audio(Input/Output)Widget depending on
223                      direction of audio_port.
224
225            """
226            if audio_port.direction == 'Input':
227                return audio_widget.AudioInputWidget(audio_port, handler)
228            return audio_widget.AudioOutputWidget(audio_port, handler)
229
230
231        audio_port = AudioPort(port_id)
232        if audio_port.host == 'Chameleon':
233            handler = _create_chameleon_handler(audio_port)
234        elif audio_port.host == 'Cros':
235            handler = _create_cros_handler(audio_port)
236        elif audio_port.host == 'Peripheral':
237            handler = audio_widget.PeripheralWidgetHandler()
238
239        return _create_audio_widget(audio_port, handler)
240
241
242    def create_binder(self, source, sink):
243        """Creates a WidgetBinder for two AudioWidgets.
244
245        @param source: An AudioWidget.
246        @param sink: An AudioWidget.
247
248        @returns: A WidgetBinder object.
249
250        """
251        return audio_widget_link.WidgetBinder(
252                source, self._link_factory.create_link(source, sink), sink)
253
254
255def compare_recorded_result(golden_file, recorder, method):
256    """Check recoded audio in a AudioInputWidget against a golden file.
257
258    Compares recorded data with golden data by cross correlation method.
259    Refer to audio_helper.compare_data for details of comparison.
260
261    @param golden_file: An AudioTestData object that serves as golden data.
262    @param recorder: An AudioInputWidget that has recorded some audio data.
263    @param method: The method to compare recorded result. Currently,
264                   'correlation' and 'frequency' are supported.
265
266    @returns: True if the recorded data and golden data are similar enough.
267
268    """
269    logging.info('Comparing recorded data with golden file %s ...',
270                 golden_file.path)
271    return audio_helper.compare_data(
272            golden_file.get_binary(), golden_file.data_format,
273            recorder.get_binary(), recorder.data_format, recorder.channel_map,
274            method)
275
276
277@contextmanager
278def bind_widgets(binder):
279    """Context manager for widget binders.
280
281    Connects widgets in the beginning. Disconnects widgets and releases binder
282    in the end.
283
284    @param binder: A WidgetBinder object.
285
286    E.g. with bind_widgets(binder):
287             do something on widget.
288
289    """
290    binder.connect()
291    yield
292    binder.disconnect()
293    binder.release()
294