desktopui_AudioFeedback.py revision f80337a4fc85e970400240fb8ff62aedafdbcc66
1# Copyright (c) 2012 The Chromium 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
5import logging, threading, tempfile
6
7from autotest_lib.client.bin import test
8from autotest_lib.client.common_lib import error
9from autotest_lib.client.cros import cros_ui_test, httpd
10from autotest_lib.client.cros.audio import audio_helper
11
12# Names of mixer controls.
13_CONTROL_MASTER = "'Master Playback Volume'"
14_CONTROL_HEADPHONE = "'Headphone Playback Volume'"
15_CONTROL_SPEAKER = "'Speaker Playback Volume'"
16_CONTROL_SPEAKER_HP = "'HP/Speakers'"
17_CONTROL_MIC_BOOST = "'Mic Boost Volume'"
18_CONTROL_CAPTURE = "'Capture Volume'"
19_CONTROL_PCM = "'PCM Playback Volume'"
20_CONTROL_DIGITAL = "'Digital Capture Volume'"
21_CONTROL_CAPTURE_SWITCH = "'Capture Switch'"
22
23# Default test configuration.
24_DEFAULT_CARD = '0'
25_DEFAULT_MIXER_SETTINGS = [{'name': _CONTROL_MASTER, 'value': "100%"},
26                           {'name': _CONTROL_HEADPHONE, 'value': "100%"},
27                           {'name': _CONTROL_MIC_BOOST, 'value': "50%"},
28                           {'name': _CONTROL_PCM, 'value': "100%"},
29                           {'name': _CONTROL_DIGITAL, 'value': "100%"},
30                           {'name': _CONTROL_CAPTURE, 'value': "100%"},
31                           {'name': _CONTROL_CAPTURE_SWITCH, 'value': "on"}]
32
33_CONTROL_SPEAKER_DEVICE = ['x86-alex', 'x86-mario', 'x86-zgb']
34_CONTROL_SPEAKER_DEVICE_HP = ['stumpy', 'lumpy']
35
36_DEFAULT_NUM_CHANNELS = 2
37_DEFAULT_RECORD_DURATION = 15
38# Minimum RMS value to consider a "pass".
39_DEFAULT_SOX_RMS_THRESHOLD = 0.30
40
41
42class RecordSampleThread(threading.Thread):
43    """Wraps the execution of arecord in a thread."""
44    def __init__(self, audio, duration, recordfile):
45        threading.Thread.__init__(self)
46        self._audio = audio
47        self._duration = duration
48        self._recordfile = recordfile
49
50    def run(self):
51        self._audio.record_sample(self._duration, self._recordfile)
52
53
54class desktopui_AudioFeedback(cros_ui_test.UITest):
55    version = 1
56
57    def initialize(self,
58                   card=_DEFAULT_CARD,
59                   mixer_settings=_DEFAULT_MIXER_SETTINGS,
60                   num_channels=_DEFAULT_NUM_CHANNELS,
61                   record_duration=_DEFAULT_RECORD_DURATION,
62                   sox_min_rms=_DEFAULT_SOX_RMS_THRESHOLD):
63        """Setup the deps for the test.
64
65        Args:
66            card: The index of the sound card to use.
67            mixer_settings: Alsa control settings to apply to the mixer before
68                starting the test.
69            num_channels: The number of channels on the device to test.
70            record_duration: How long of a sample to record.
71            sox_min_rms: The minimum RMS value to consider a pass.
72
73        Raises:
74            error.TestError if the deps can't be run.
75        """
76        self._card = card
77        self._mixer_settings = mixer_settings
78        self._num_channels = num_channels
79        self._record_duration = record_duration
80        self._sox_min_rms = sox_min_rms
81
82        self._ah = audio_helper.AudioHelper(self)
83        self._ah.setup_deps(['sox'])
84
85        super(desktopui_AudioFeedback, self).initialize()
86        self._test_url = 'http://localhost:8000/youtube.html'
87        self._testServer = httpd.HTTPListener(8000, docroot=self.bindir)
88        self._testServer.run()
89
90    def run_once(self):
91        # Speaker control settings may differ from device to device.
92        if self.pyauto.ChromeOSBoard() in _CONTROL_SPEAKER_DEVICE:
93            self._mixer_settings.append({'name': _CONTROL_SPEAKER,
94                                         'value': "0%"})
95        elif self.pyauto.ChromeOSBoard() in _CONTROL_SPEAKER_DEVICE_HP:
96            self._mixer_settings.append({'name': _CONTROL_SPEAKER_HP,
97                                         'value': "0%"})
98        self._ah.set_mixer_controls(self._mixer_settings, self._card)
99
100        # Record a sample of "silence" to use as a noise profile.
101        with tempfile.NamedTemporaryFile(mode='w+t') as noise_file:
102            logging.info('Noise file: %s' % noise_file.name)
103            self._ah.record_sample(1, noise_file.name)
104
105            # Test each channel separately. Assume two channels.
106            for channel in xrange(0, self._num_channels):
107                self.loopback_test_one_channel(channel, noise_file.name)
108
109    def play_video(self):
110        """Plays a Youtube video to record audio samples.
111
112           Skipping initial 60 seconds so we can ignore initial silence
113           in the video.
114        """
115        logging.info('Playing back youtube media file %s.' % self._test_url)
116        self.pyauto.NavigateToURL(self._test_url)
117        if not self.pyauto.WaitUntil(lambda: self.pyauto.ExecuteJavascript("""
118                    player_status = document.getElementById('player_status');
119                    window.domAutomationController.send(player_status.innerHTML);
120               """), expect_retval='player ready'):
121            raise error.TestError('Failed to load the Youtube player')
122        self.pyauto.ExecuteJavascript("""
123            ytplayer.pauseVideo();
124            ytplayer.seekTo(60, true);
125            ytplayer.playVideo();
126            window.domAutomationController.send('');
127        """)
128
129    def loopback_test_one_channel(self, channel, noise_file):
130        """Test loopback for a given channel.
131
132        Args:
133            channel: The channel to test loopback on.
134            noise_file: Noise profile to use for filtering, None to skip noise
135                filtering.
136        """
137        with tempfile.NamedTemporaryFile(mode='w+t') as reduced_file:
138            with tempfile.NamedTemporaryFile(mode='w+t') as tmpfile:
139                record_thread = RecordSampleThread(self._ah,
140                        self._record_duration, tmpfile.name)
141                self.play_video()
142                record_thread.start()
143                record_thread.join()
144
145                self._ah.noise_reduce_file(tmpfile.name, noise_file,
146                        reduced_file.name)
147            self.check_recorded_audio(reduced_file.name, channel)
148
149    def check_recorded_audio(self, infile, channel):
150        """Runs the sox command to check if we captured audio.
151
152        Note: if we captured any sufficient loud audio which can generate
153        the rms_value greater than the threshold value, test will pass.
154        TODO (rohitbm) : find a way to compare the recorded audio with
155                         an actual sample file.
156
157        Args:
158            infile: The file is to test for (strong) audio content via the RMS
159                    method.
160            channel: The audio channel to test.
161
162        Raises:
163            error.TestFail if the RMS amplitude of the recording isn't above
164                the threshold.
165        """
166        rms_val = self._ah.get_audio_rms(infile, channel)
167        # In case sox didn't return an RMS value.
168        if rms_val is None:
169            raise error.TestError(
170                'Failed to generate an audio RMS value from playback.')
171
172        logging.info('Got audio RMS value of %f. Minimum pass is %f.' %
173                     (rms_val, self._sox_min_rms))
174        if rms_val < self._sox_min_rms:
175                raise error.TestError(
176                    'Audio RMS value %f too low. Minimum pass is %f.' %
177                    (rms_val, self._sox_min_rms))
178