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