# 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)