| # Copyright 2016 The Chromium OS Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """Audio query delegates.""" |
| |
| import subprocess |
| |
| import common |
| from autotest_lib.client.common_lib import utils |
| from autotest_lib.client.common_lib.feedback import client |
| from autotest_lib.client.common_lib.feedback import tester_feedback_client |
| from autotest_lib.server.brillo import audio_utils |
| |
| import input_handlers |
| import query_delegate |
| import sequenced_request |
| |
| |
| # Supported WAVE playback commands in decreasing order of preference. |
| _KNOWN_WAV_PLAYBACK_METHODS = ( |
| # Alsa command-line tool, most straightforward to use (if available). |
| ('aplay', ('aplay', '%(file)s')), |
| # Sox's play command. |
| ('play', ('play', '-q', '%(file)s')), |
| # VLC command-line tool. |
| ('cvlc', ('cvlc', '-q', '--play-and-exit', '%(file)s')), |
| # Mplayer; might choke when using Alsa and therefore least preferred. |
| ('mplayer', ('mplayer', '-quiet', '-novideo', '%(file)s')), |
| ) |
| |
| |
| class PlaybackMixin(object): |
| """Mixin for adding playback capabilities to a query.""" |
| |
| # TODO(garnold) The provided audio file path is local to the test host, |
| # which isn't necessarily the same as the host running the feedback |
| # service. To support other use cases (Moblab, client-side testing) we'll |
| # need to properly identify such cases and fetch the file (b/26927734). |
| def _playback_wav_file(self, msg, audio_file): |
| """Plays a WAV file via user selected method. |
| |
| Looks for available playback commands and presents them to the user to |
| choose from. Also lists "manual playback" as the last option. |
| |
| @param msg: Introductory message to present to the user. |
| @param audio_file: The audio file to play. |
| |
| @return: Whether playback was successful. |
| """ |
| choices = [] |
| cmds = [] |
| for tool, cmd in _KNOWN_WAV_PLAYBACK_METHODS: |
| if utils.which(tool): |
| choices.append(tool) |
| cmds.append(cmd) |
| choices.append('Manual playback') |
| |
| msg += (' The audio file is %s. Available playback methods include:' % |
| audio_file) |
| req = sequenced_request.SequencedFeedbackRequest(self.test, self.dut, |
| None) |
| req.append_question( |
| msg, |
| input_handlers.MultipleChoiceInputHandler(choices, default=1), |
| prompt='Choose your playback method') |
| idx, _ = self._process_request(req) |
| if idx < len(choices) - 1: |
| cmd = [tok % {'file': audio_file} for tok in cmds[idx]] |
| return subprocess.call(cmd) == 0 |
| |
| return True |
| |
| |
| class AudiblePlaybackQueryDelegate(query_delegate.OutputQueryDelegate, |
| PlaybackMixin): |
| """Query delegate for validating audible feedback.""" |
| |
| def _prepare_impl(self, **kwargs): |
| """Prepare for audio playback (interface override).""" |
| req = sequenced_request.SequencedFeedbackRequest( |
| self.test, self.dut, 'Audible playback') |
| req.append_question( |
| 'Device %(dut)s will play a short audible sample. Please ' |
| 'prepare for listening to this playback and hit Enter to ' |
| 'continue...', |
| input_handlers.PauseInputHandler()) |
| self._process_request(req) |
| |
| |
| def _validate_impl(self, audio_file=None): |
| """Validate playback (interface override). |
| |
| @param audio_file: Name of audio file on the test host to validate |
| against. |
| """ |
| req = sequenced_request.SequencedFeedbackRequest( |
| self.test, self.dut, None) |
| msg = 'Playback finished on %(dut)s.' |
| if audio_file is None: |
| req.append_question( |
| msg, input_handlers.YesNoInputHandler(default=True), |
| prompt='Did you hear audible sound?') |
| err_msg = 'User did not hear audible feedback' |
| else: |
| if not self._playback_wav_file(msg, audio_file): |
| return (tester_feedback_client.QUERY_RET_ERROR, |
| 'Failed to playback recorded audio') |
| req.append_question( |
| None, input_handlers.YesNoInputHandler(default=True), |
| prompt=('Was the audio produced identical to the refernce ' |
| 'audio file?')) |
| err_msg = ('Audio produced was not identical to the reference ' |
| 'audio file') |
| |
| if not self._process_request(req): |
| return (tester_feedback_client.QUERY_RET_FAIL, err_msg) |
| |
| |
| class SilentPlaybackQueryDelegate(query_delegate.OutputQueryDelegate): |
| """Query delegate for validating silent feedback.""" |
| |
| def _prepare_impl(self, **kwargs): |
| """Prepare for silent playback (interface override).""" |
| req = sequenced_request.SequencedFeedbackRequest( |
| self.test, self.dut, 'Silent playback') |
| req.append_question( |
| 'Device %(dut)s will play nothing for a short time. Please ' |
| 'prepare for listening to this silence and hit Enter to ' |
| 'continue...', |
| input_handlers.PauseInputHandler()) |
| self._process_request(req) |
| |
| |
| def _validate_impl(self, audio_file=None): |
| """Validate silence (interface override). |
| |
| @param audio_file: Name of audio file on the test host to validate |
| against. |
| """ |
| if audio_file is not None: |
| return (tester_feedback_client.QUERY_RET_ERROR, |
| 'Not expecting an audio file entry when validating silence') |
| req = sequenced_request.SequencedFeedbackRequest( |
| self.test, self.dut, None) |
| req.append_question( |
| 'Silence playback finished on %(dut)s.', |
| input_handlers.YesNoInputHandler(default=True), |
| prompt='Did you hear silence?') |
| if not self._process_request(req): |
| return (tester_feedback_client.QUERY_RET_FAIL, |
| 'User did not hear silence') |
| |
| |
| class RecordingQueryDelegate(query_delegate.InputQueryDelegate, PlaybackMixin): |
| """Query delegate for validating audible feedback.""" |
| |
| def _prepare_impl(self, use_file, sample_width, sample_rate, |
| num_channels, frequency): |
| """Prepare for audio recording (interface override). |
| |
| @param use_file: If a file was used to produce audio. This is necessary |
| if audio of a particular frequency is being produced. |
| @param sample_width: The recorded sample width. |
| @param sample_rate: The recorded sample rate. |
| @param num_channels: The number of recorded channels. |
| @param frequency: Frequency of audio being produced. |
| """ |
| req = sequenced_request.SequencedFeedbackRequest( |
| self.test, self.dut, 'Audio recording') |
| # TODO(ralphnathan) Lift the restriction regarding recording time once |
| # the test allows recording for arbitrary periods of time (b/26924426). |
| req.append_question( |
| 'Device %(dut)s will start recording audio for 10 seconds. ' |
| 'Please prepare for producing sound and hit Enter to ' |
| 'continue...', |
| input_handlers.PauseInputHandler()) |
| self._process_request(req) |
| self._sample_width = sample_width |
| self._sample_rate = sample_rate |
| self._num_channels = num_channels |
| if use_file: |
| self._frequency = frequency |
| else: |
| self._frequency = None |
| |
| |
| def _emit_impl(self): |
| """Emit sound for recording (interface override).""" |
| req = sequenced_request.SequencedFeedbackRequest( |
| self.test, self.dut, None) |
| req.append_question( |
| 'Device %(dut)s is recording audio, hit Enter when done ' |
| 'producing sound...', |
| input_handlers.PauseInputHandler()) |
| self._process_request(req) |
| |
| |
| def _validate_impl(self, captured_audio_file): |
| """Validate recording (interface override). |
| |
| @param captured_audio_file: Path to the recorded WAV file. |
| """ |
| # Check the WAV file properties first. |
| try: |
| audio_utils.check_wav_file( |
| captured_audio_file, num_channels=self._num_channels, |
| sample_rate=self._sample_rate, |
| sample_width=self._sample_width) |
| except ValueError as e: |
| return (tester_feedback_client.QUERY_RET_FAIL, |
| 'Recorded audio file is invalid: %s' % e) |
| |
| |
| # Verify playback of the recorded audio. |
| props = ['as sample width of %d' % self._sample_width, |
| 'has sample rate of %d' % self._sample_rate, |
| 'has %d recorded channels' % self._num_channels] |
| if self._frequency is not None: |
| props.append('has frequency of %d Hz' % self._frequency) |
| props_str = '%s%s%s' % (', '.join(props[:-1]), |
| ', and ' if len(props) > 1 else '', |
| props[-1]) |
| |
| msg = 'Recording finished on %%(dut)s. It %s.' % props_str |
| if not self._playback_wav_file(msg, captured_audio_file): |
| return (tester_feedback_client.QUERY_RET_ERROR, |
| 'Failed to playback recorded audio') |
| |
| req = sequenced_request.SequencedFeedbackRequest( |
| self.test, self.dut, None) |
| req.append_question( |
| None, |
| input_handlers.YesNoInputHandler(default=True), |
| prompt='Did the recording capture the sound produced?') |
| if not self._process_request(req): |
| return (tester_feedback_client.QUERY_RET_FAIL, |
| 'Recorded audio is not identical to what the user produced') |
| |
| |
| query_delegate.register_delegate_cls(client.QUERY_AUDIO_PLAYBACK_AUDIBLE, |
| AudiblePlaybackQueryDelegate) |
| |
| query_delegate.register_delegate_cls(client.QUERY_AUDIO_PLAYBACK_SILENT, |
| SilentPlaybackQueryDelegate) |
| |
| query_delegate.register_delegate_cls(client.QUERY_AUDIO_RECORDING, |
| RecordingQueryDelegate) |