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