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 7 import subprocess 8 9 import common 10 from autotest_lib.client.common_lib import utils 11 from autotest_lib.client.common_lib.feedback import client 12 from autotest_lib.client.common_lib.feedback import tester_feedback_client 13 from autotest_lib.server.brillo import audio_utils 14 15 import input_handlers 16 import query_delegate 17 import 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 33 class 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 75 class 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 120 class 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 155 class 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 241 query_delegate.register_delegate_cls(client.QUERY_AUDIO_PLAYBACK_AUDIBLE, 242 AudiblePlaybackQueryDelegate) 243 244 query_delegate.register_delegate_cls(client.QUERY_AUDIO_PLAYBACK_SILENT, 245 SilentPlaybackQueryDelegate) 246 247 query_delegate.register_delegate_cls(client.QUERY_AUDIO_RECORDING, 248 RecordingQueryDelegate) 249