blob: 9a8368864116e4c15b8ef8ae0c02fc42543bf40a [file] [log] [blame]
Hsinyu Chao4b8300e2011-11-15 13:07:32 -08001#!/usr/bin/python
2# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6import logging
7import os
Hsinyu Chaof80337a2012-04-07 18:02:29 +08008import re
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +08009import tempfile
10import threading
Hsinyu Chao4b8300e2011-11-15 13:07:32 -080011
12from autotest_lib.client.bin import utils
13from autotest_lib.client.common_lib import error
14
15LD_LIBRARY_PATH = 'LD_LIBRARY_PATH'
16
Hsinyu Chaof80337a2012-04-07 18:02:29 +080017_DEFAULT_NUM_CHANNELS = 2
Dylan Reidbf9a5d42012-11-06 16:27:20 -080018_DEFAULT_REC_COMMAND = 'arecord -D hw:0,0 -d 10 -f dat'
Hsinyu Chaof80337a2012-04-07 18:02:29 +080019_DEFAULT_SOX_FORMAT = '-t raw -b 16 -e signed -r 48000 -L'
Dylan Reid51f289c2012-11-06 17:16:24 -080020_DEFAULT_SOX_RMS_THRESHOLD = 0.5
Hsinyu Chaof80337a2012-04-07 18:02:29 +080021
Hsin-Yu Chao8d093f42012-11-05 18:43:22 +080022_JACK_VALUE_ON_RE = re.compile('.*values=on')
23_HP_JACK_CONTROL_RE = re.compile('numid=(\d+).*Headphone\sJack')
24_MIC_JACK_CONTROL_RE = re.compile('numid=(\d+).*Mic\sJack')
25
Hsinyu Chaof80337a2012-04-07 18:02:29 +080026_SOX_RMS_AMPLITUDE_RE = re.compile('RMS\s+amplitude:\s+(.+)')
Hsinyu Chao2d64e1f2012-05-21 11:18:53 +080027_SOX_ROUGH_FREQ_RE = re.compile('Rough\s+frequency:\s+(.+)')
Hsinyu Chaof80337a2012-04-07 18:02:29 +080028_SOX_FORMAT = '-t raw -b 16 -e signed -r 48000 -L'
29
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +080030
31class RecordSampleThread(threading.Thread):
32 '''Wraps the execution of arecord in a thread.'''
33 def __init__(self, audio, recordfile):
34 threading.Thread.__init__(self)
35 self._audio = audio
36 self._recordfile = recordfile
37
38 def run(self):
39 self._audio.record_sample(self._recordfile)
40
41
Hsinyu Chao4b8300e2011-11-15 13:07:32 -080042class AudioHelper(object):
43 '''
44 A helper class contains audio related utility functions.
45 '''
Dylan Reidbf9a5d42012-11-06 16:27:20 -080046 def __init__(self, test,
47 sox_format = _DEFAULT_SOX_FORMAT,
Dylan Reid51f289c2012-11-06 17:16:24 -080048 sox_threshold = _DEFAULT_SOX_RMS_THRESHOLD,
Dylan Reidbf9a5d42012-11-06 16:27:20 -080049 record_command = _DEFAULT_REC_COMMAND,
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +080050 num_channels = _DEFAULT_NUM_CHANNELS):
Hsinyu Chao4b8300e2011-11-15 13:07:32 -080051 self._test = test
Dylan Reid51f289c2012-11-06 17:16:24 -080052 self._sox_threshold = sox_threshold
Hsinyu Chaof80337a2012-04-07 18:02:29 +080053 self._sox_format = sox_format
Dylan Reidbf9a5d42012-11-06 16:27:20 -080054 self._rec_cmd = record_command
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +080055 self._num_channels = num_channels
Hsinyu Chao4b8300e2011-11-15 13:07:32 -080056
57 def setup_deps(self, deps):
58 '''
59 Sets up audio related dependencies.
60 '''
61 for dep in deps:
62 if dep == 'test_tones':
63 dep_dir = os.path.join(self._test.autodir, 'deps', dep)
64 self._test.job.install_pkg(dep, 'dep', dep_dir)
65 self.test_tones_path = os.path.join(dep_dir, 'src', dep)
66 elif dep == 'audioloop':
67 dep_dir = os.path.join(self._test.autodir, 'deps', dep)
68 self._test.job.install_pkg(dep, 'dep', dep_dir)
69 self.audioloop_path = os.path.join(dep_dir, 'src',
70 'looptest')
71 elif dep == 'sox':
72 dep_dir = os.path.join(self._test.autodir, 'deps', dep)
73 self._test.job.install_pkg(dep, 'dep', dep_dir)
74 self.sox_path = os.path.join(dep_dir, 'bin', dep)
75 self.sox_lib_path = os.path.join(dep_dir, 'lib')
76 if os.environ.has_key(LD_LIBRARY_PATH):
77 paths = os.environ[LD_LIBRARY_PATH].split(':')
78 if not self.sox_lib_path in paths:
79 paths.append(self.sox_lib_path)
80 os.environ[LD_LIBRARY_PATH] = ':'.join(paths)
81 else:
82 os.environ[LD_LIBRARY_PATH] = self.sox_lib_path
83
84 def cleanup_deps(self, deps):
85 '''
86 Cleans up environments which has been setup for dependencies.
87 '''
88 for dep in deps:
89 if dep == 'sox':
90 if (os.environ.has_key(LD_LIBRARY_PATH)
91 and hasattr(self, 'sox_lib_path')):
92 paths = filter(lambda x: x != self.sox_lib_path,
93 os.environ[LD_LIBRARY_PATH].split(':'))
94 os.environ[LD_LIBRARY_PATH] = ':'.join(paths)
95
Derek Basehoree973ce42012-07-10 23:38:32 -070096 def set_volume_levels(self, volume, capture):
97 '''
98 Sets the volume and capture gain through cras_test_client
99 '''
100 logging.info('Setting volume level to %d' % volume)
101 utils.system('/usr/bin/cras_test_client --volume %d' % volume)
102 logging.info('Setting capture gain to %d' % capture)
103 utils.system('/usr/bin/cras_test_client --capture_gain %d' % capture)
104 utils.system('/usr/bin/cras_test_client --dump_server_info')
105 utils.system('amixer -c 0 contents')
106
Hsin-Yu Chao8d093f42012-11-05 18:43:22 +0800107 def get_jack_status(self, jack_reg_exp):
108 '''
109 Gets the jack status.
110
111 Args:
112 jack_reg_exp: The regular expression to match jack control name.
113
114 Returns:
115 None if the control does not exist, return True if jack control
116 is detected plugged, return False otherwise.
117 '''
118 output = utils.system_output('amixer -c0 controls', retain_output=True)
119 numid = None
120 for line in output.split('\n'):
121 m = jack_reg_exp.match(line)
122 if m:
123 numid = m.group(1)
124 break
125 if numid is not None:
126 output = utils.system_output('amixer -c0 cget numid=%s' % numid)
127 for line in output.split('\n'):
128 if _JACK_VALUE_ON_RE.match(line):
129 return True
130 return False
131 else:
132 return None
133
134 def get_hp_jack_status(self):
135 return self.get_jack_status(_HP_JACK_CONTROL_RE)
136
137 def get_mic_jack_status(self):
138 return self.get_jack_status(_MIC_JACK_CONTROL_RE)
139
140 def check_loopback_dongle(self):
141 '''
142 Checks if loopback dongle is equipped correctly.
143 '''
144 # Check Mic Jack
145 mic_jack_status = self.get_mic_jack_status()
146 if mic_jack_status is None:
147 logging.warning('Found no Mic Jack control, skip check.')
148 elif not mic_jack_status:
149 logging.info('Mic jack is not plugged.')
150 return False
151 else:
152 logging.info('Mic jack is plugged.')
153
154 # Check Headphone Jack
155 hp_jack_status = self.get_hp_jack_status()
156 if hp_jack_status is None:
157 logging.warning('Found no Headphone Jack control, skip check.')
158 elif not hp_jack_status:
159 logging.info('Headphone jack is not plugged.')
160 return False
161 else:
162 logging.info('Headphone jack is plugged.')
163
164 return True
165
Hsinyu Chao4b8300e2011-11-15 13:07:32 -0800166 def set_mixer_controls(self, mixer_settings={}, card='0'):
167 '''
168 Sets all mixer controls listed in the mixer settings on card.
169 '''
170 logging.info('Setting mixer control values on %s' % card)
171 for item in mixer_settings:
172 logging.info('Setting %s to %s on card %s' %
173 (item['name'], item['value'], card))
174 cmd = 'amixer -c %s cset name=%s %s'
175 cmd = cmd % (card, item['name'], item['value'])
176 try:
177 utils.system(cmd)
178 except error.CmdError:
179 # A card is allowed not to support all the controls, so don't
180 # fail the test here if we get an error.
181 logging.info('amixer command failed: %s' % cmd)
Hsinyu Chaof80337a2012-04-07 18:02:29 +0800182
Hsinyu Chao2d64e1f2012-05-21 11:18:53 +0800183 def sox_stat_output(self, infile, channel):
Hsinyu Chaof80337a2012-04-07 18:02:29 +0800184 sox_mixer_cmd = self.get_sox_mixer_cmd(infile, channel)
185 stat_cmd = '%s -c 1 %s - -n stat 2>&1' % (self.sox_path,
186 self._sox_format)
187 sox_cmd = '%s | %s' % (sox_mixer_cmd, stat_cmd)
Hsinyu Chao2d64e1f2012-05-21 11:18:53 +0800188 return utils.system_output(sox_cmd, retain_output=True)
189
190 def get_audio_rms(self, sox_output):
Hsinyu Chaof80337a2012-04-07 18:02:29 +0800191 for rms_line in sox_output.split('\n'):
192 m = _SOX_RMS_AMPLITUDE_RE.match(rms_line)
193 if m is not None:
194 return float(m.group(1))
195
Hsinyu Chao2d64e1f2012-05-21 11:18:53 +0800196 def get_rough_freq(self, sox_output):
197 for rms_line in sox_output.split('\n'):
198 m = _SOX_ROUGH_FREQ_RE.match(rms_line)
199 if m is not None:
200 return int(m.group(1))
201
202
Hsinyu Chaof80337a2012-04-07 18:02:29 +0800203 def get_sox_mixer_cmd(self, infile, channel):
204 # Build up a pan value string for the sox command.
205 if channel == 0:
206 pan_values = '1'
207 else:
208 pan_values = '0'
209 for pan_index in range(1, self._num_channels):
210 if channel == pan_index:
211 pan_values = '%s%s' % (pan_values, ',1')
212 else:
213 pan_values = '%s%s' % (pan_values, ',0')
214
215 return '%s -c 2 %s %s -c 1 %s - mixer %s' % (self.sox_path,
216 self._sox_format, infile, self._sox_format, pan_values)
217
218 def noise_reduce_file(self, in_file, noise_file, out_file):
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +0800219 '''Runs the sox command to noise-reduce in_file using
Hsinyu Chaof80337a2012-04-07 18:02:29 +0800220 the noise profile from noise_file.
221
222 Args:
223 in_file: The file to noise reduce.
224 noise_file: The file containing the noise profile.
225 This can be created by recording silence.
226 out_file: The file contains the noise reduced sound.
227
228 Returns:
229 The name of the file containing the noise-reduced data.
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +0800230 '''
Hsinyu Chaof80337a2012-04-07 18:02:29 +0800231 prof_cmd = '%s -c 2 %s %s -n noiseprof' % (self.sox_path,
232 _SOX_FORMAT, noise_file)
233 reduce_cmd = ('%s -c 2 %s %s -c 2 %s %s noisered' %
234 (self.sox_path, _SOX_FORMAT, in_file, _SOX_FORMAT, out_file))
235 utils.system('%s | %s' % (prof_cmd, reduce_cmd))
236
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +0800237 def record_sample(self, tmpfile):
238 '''Records a sample from the default input device.
Hsinyu Chaof80337a2012-04-07 18:02:29 +0800239
240 Args:
241 duration: How long to record in seconds.
242 tmpfile: The file to record to.
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +0800243 '''
Dylan Reidbf9a5d42012-11-06 16:27:20 -0800244 cmd_rec = self._rec_cmd + ' %s' % tmpfile
245 logging.info('Command %s recording now' % cmd_rec)
Hsinyu Chaof80337a2012-04-07 18:02:29 +0800246 utils.system(cmd_rec)
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +0800247
Dylan Reid51f289c2012-11-06 17:16:24 -0800248 def loopback_test_channels(self, noise_file, loopback_callback):
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +0800249 '''Tests loopback on all channels.
250
251 Args:
252 noise_file: The file contains the pre-recorded noise.
253 loopback_callback: The callback to do the loopback for one channel.
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +0800254 '''
255 for channel in xrange(self._num_channels):
256 # Temp file for the final noise-reduced file.
257 with tempfile.NamedTemporaryFile(mode='w+t') as reduced_file:
258 # Temp file that records before noise reduction.
259 with tempfile.NamedTemporaryFile(mode='w+t') as tmpfile:
260 record_thread = RecordSampleThread(self, tmpfile.name)
261 record_thread.start()
262 loopback_callback(channel)
263 record_thread.join()
264
265 self.noise_reduce_file(tmpfile.name, noise_file.name,
266 reduced_file.name)
267
Hsinyu Chao2d64e1f2012-05-21 11:18:53 +0800268 sox_output = self.sox_stat_output(reduced_file.name, channel)
Dylan Reid51f289c2012-11-06 17:16:24 -0800269 self.check_recorded(sox_output)
270
271 def check_recorded(self, sox_output):
272 """Checks if the calculated RMS value is expected.
273
274 Args:
275 sox_output: The output from sox stat command.
276
277 Raises:
278 error.TestFail if the RMS amplitude of the recording isn't above
279 the threshold.
280 """
281 rms_val = self.get_audio_rms(sox_output)
282
283 # In case we don't get a valid RMS value.
284 if rms_val is None:
285 raise error.TestError(
286 'Failed to generate an audio RMS value from playback.')
287
288 logging.info('Got audio RMS value of %f. Minimum pass is %f.' %
289 (rms_val, self._sox_threshold))
290 if rms_val < self._sox_threshold:
291 raise error.TestError(
292 'Audio RMS value %f too low. Minimum pass is %f.' %
293 (rms_val, self._sox_threshold))