Home | History | Annotate | Download | only in tester_feedback
      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