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 site_utils 11 from autotest_lib.client.common_lib.feedback import client 12 from autotest_lib.client.common_lib.feedback import tester_feedback_client 13 14 import input_handlers 15 import query_delegate 16 import 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 32 class 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 74 class 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 119 class 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 154 class 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 231 query_delegate.register_delegate_cls(client.QUERY_AUDIO_PLAYBACK_AUDIBLE, 232 AudiblePlaybackQueryDelegate) 233 234 query_delegate.register_delegate_cls(client.QUERY_AUDIO_PLAYBACK_SILENT, 235 SilentPlaybackQueryDelegate) 236 237 query_delegate.register_delegate_cls(client.QUERY_AUDIO_RECORDING, 238 RecordingQueryDelegate) 239