• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2016 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"""Audio query delegates."""
6
7import subprocess
8
9import common
10from autotest_lib.client.common_lib import site_utils
11from autotest_lib.client.common_lib.feedback import client
12from autotest_lib.client.common_lib.feedback import tester_feedback_client
13
14import input_handlers
15import query_delegate
16import sequenced_request
17
18
19# Supported WAVE playback commands in decreasing order of preference.
20_KNOWN_WAV_PLAYBACK_METHODS = (
21        # Alsa command-line tool, most straightforward to use (if available).
22        ('aplay', ('aplay', '%(file)s')),
23        # Sox's play command.
24        ('play', ('play', '-q', '%(file)s')),
25        # VLC command-line tool.
26        ('cvlc', ('cvlc', '-q', '--play-and-exit', '%(file)s')),
27        # Mplayer; might choke when using Alsa and therefore least preferred.
28        ('mplayer', ('mplayer', '-quiet', '-novideo', '%(file)s')),
29)
30
31
32class PlaybackMixin(object):
33    """Mixin for adding playback capabilities to a query."""
34
35    # TODO(garnold) The provided audio file path is local to the test host,
36    # which isn't necessarily the same as the host running the feedback
37    # service. To support other use cases (Moblab, client-side testing) we'll
38    # need to properly identify such cases and fetch the file (b/26927734).
39    def _playback_wav_file(self, msg, audio_file):
40        """Plays a WAV file via user selected method.
41
42        Looks for available playback commands and presents them to the user to
43        choose from. Also lists "manual playback" as the last option.
44
45        @param msg: Introductory message to present to the user.
46        @param audio_file: The audio file to play.
47
48        @return: Whether playback was successful.
49        """
50        choices = []
51        cmds = []
52        for tool, cmd in _KNOWN_WAV_PLAYBACK_METHODS:
53            if site_utils.which(tool):
54                choices.append(tool)
55                cmds.append(cmd)
56        choices.append('Manual playback')
57
58        msg += (' The audio file is %s. Available playback methods include:' %
59                audio_file)
60        req = sequenced_request.SequencedFeedbackRequest(self.test, self.dut,
61                                                         None)
62        req.append_question(
63                msg,
64                input_handlers.MultipleChoiceInputHandler(choices, default=1),
65                prompt='Choose your playback method')
66        idx, _ = self._process_request(req)
67        if idx < len(choices) - 1:
68            cmd = [tok % {'file': audio_file} for tok in cmds[idx]]
69            return subprocess.call(cmd) == 0
70
71        return True
72
73
74class AudiblePlaybackQueryDelegate(query_delegate.OutputQueryDelegate,
75                                   PlaybackMixin):
76    """Query delegate for validating audible feedback."""
77
78    def _prepare_impl(self):
79        """Prepare for audio playback (interface override)."""
80        req = sequenced_request.SequencedFeedbackRequest(
81                self.test, self.dut, 'Audible playback')
82        req.append_question(
83                'Device %(dut)s will play a short audible sample. Please '
84                'prepare for listening to this playback and hit Enter to '
85                'continue...',
86                input_handlers.PauseInputHandler())
87        self._process_request(req)
88
89
90    def _validate_impl(self, audio_file=None):
91        """Validate playback (interface override).
92
93        @param audio_file: Name of audio file on the test host to validate
94                           against.
95        """
96        req = sequenced_request.SequencedFeedbackRequest(
97                self.test, self.dut, None)
98        msg = 'Playback finished on %(dut)s.'
99        if audio_file is None:
100            req.append_question(
101                    msg, input_handlers.YesNoInputHandler(default=True),
102                    prompt='Did you hear audible sound?')
103            err_msg = 'User did not hear audible feedback'
104        else:
105            if not self._playback_wav_file(msg, audio_file):
106                return (tester_feedback_client.QUERY_RET_ERROR,
107                        'Failed to playback recorded audio')
108            req.append_question(
109                    None, input_handlers.YesNoInputHandler(default=True),
110                    prompt=('Was the audio produced identical to the refernce '
111                            'audio file?'))
112            err_msg = ('Audio produced was not identical to the reference '
113                       'audio file')
114
115        if not self._process_request(req):
116            return (tester_feedback_client.QUERY_RET_FAIL, err_msg)
117
118
119class SilentPlaybackQueryDelegate(query_delegate.OutputQueryDelegate):
120    """Query delegate for validating silent feedback."""
121
122    def _prepare_impl(self):
123        """Prepare for silent playback (interface override)."""
124        req = sequenced_request.SequencedFeedbackRequest(
125                self.test, self.dut, 'Silent playback')
126        req.append_question(
127                'Device %(dut)s will play nothing for a short time. Please '
128                'prepare for listening to this silence and hit Enter to '
129                'continue...',
130                input_handlers.PauseInputHandler())
131        self._process_request(req)
132
133
134    def _validate_impl(self, audio_file=None):
135        """Validate silence (interface override).
136
137        @param audio_file: Name of audio file on the test host to validate
138                           against.
139        """
140        if audio_file is not None:
141            return (tester_feedback_client.QUERY_RET_ERROR,
142                    'Not expecting an audio file entry when validating silence')
143        req = sequenced_request.SequencedFeedbackRequest(
144                self.test, self.dut, None)
145        req.append_question(
146                'Silence playback finished on %(dut)s.',
147                input_handlers.YesNoInputHandler(default=True),
148                prompt='Did you hear silence?')
149        if not self._process_request(req):
150            return (tester_feedback_client.QUERY_RET_FAIL,
151                    'User did not hear silence')
152
153
154class RecordingQueryDelegate(query_delegate.InputQueryDelegate, PlaybackMixin):
155    """Query delegate for validating audible feedback."""
156
157    def _prepare_impl(self):
158        """Prepare for audio recording (interface override)."""
159        req = sequenced_request.SequencedFeedbackRequest(
160                self.test, self.dut, 'Audio recording')
161        # TODO(ralphnathan) Lift the restriction regarding recording time once
162        # the test allows recording for arbitrary periods of time (b/26924426).
163        req.append_question(
164                'Device %(dut)s will start recording audio for 10 seconds. '
165                'Please prepare for producing sound and hit Enter to '
166                'continue...',
167                input_handlers.PauseInputHandler())
168        self._process_request(req)
169
170
171    def _emit_impl(self):
172        """Emit sound for recording (interface override)."""
173        req = sequenced_request.SequencedFeedbackRequest(
174                self.test, self.dut, None)
175        req.append_question(
176                'Device %(dut)s is recording audio, hit Enter when done '
177                'producing sound...',
178                input_handlers.PauseInputHandler())
179        self._process_request(req)
180
181
182    def _validate_impl(self, captured_audio_file, sample_width,
183                       sample_rate=None, num_channels=None, peak_percent_min=1,
184                       peak_percent_max=100):
185        """Validate recording (interface override).
186
187        @param captured_audio_file: Path to the recorded WAV file.
188        @param sample_width: The recorded sample width.
189        @param sample_rate: The recorded sample rate.
190        @param num_channels: The number of recorded channels.
191        @peak_percent_min: Lower bound on peak recorded volume as percentage of
192                           max molume (0-100). Default is 1%.
193        @peak_percent_max: Upper bound on peak recorded volume as percentage of
194                           max molume (0-100). Default is 100% (no limit).
195        """
196        # Check the WAV file properties first.
197        try:
198            site_utils.check_wav_file(
199                    captured_audio_file, num_channels=num_channels,
200                    sample_rate=sample_rate, sample_width=sample_width)
201        except ValueError as e:
202            return (tester_feedback_client.QUERY_RET_FAIL,
203                    'Recorded audio file is invalid: %s' % e)
204
205        # Verify playback of the recorded audio.
206        props = ['has sample width of %d' % sample_width]
207        if sample_rate is not None:
208            props.append('has sample rate of %d' % sample_rate)
209        if num_channels is not None:
210            props.append('has %d recorded channels' % num_channels)
211        props_str = '%s%s%s' % (', '.join(props[:-1]),
212                                ', and ' if len(props) > 1 else '',
213                                props[-1])
214
215        msg = 'Recording finished on %%(dut)s. It %s.' % props_str
216        if not self._playback_wav_file(msg, captured_audio_file):
217            return (tester_feedback_client.QUERY_RET_ERROR,
218                    'Failed to playback recorded audio')
219
220        req = sequenced_request.SequencedFeedbackRequest(
221                self.test, self.dut, None)
222        req.append_question(
223                None,
224                input_handlers.YesNoInputHandler(default=True),
225                prompt='Did the recording capture the sound produced?')
226        if not self._process_request(req):
227            return (tester_feedback_client.QUERY_RET_FAIL,
228                    'Recorded audio is not identical to what the user produced')
229
230
231query_delegate.register_delegate_cls(client.QUERY_AUDIO_PLAYBACK_AUDIBLE,
232                                     AudiblePlaybackQueryDelegate)
233
234query_delegate.register_delegate_cls(client.QUERY_AUDIO_PLAYBACK_SILENT,
235                                     SilentPlaybackQueryDelegate)
236
237query_delegate.register_delegate_cls(client.QUERY_AUDIO_RECORDING,
238                                     RecordingQueryDelegate)
239