blob: d6bf6dfd5dcc3071099e2965e15a40970ff9f3ae [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
Hsin-Yu Chao084e9da2012-11-07 15:56:26 +080012from glob import glob
13
Hsinyu Chao4b8300e2011-11-15 13:07:32 -080014from autotest_lib.client.bin import utils
Hsin-Yu Chao084e9da2012-11-07 15:56:26 +080015from autotest_lib.client.bin.input.input_device import *
Hsinyu Chao4b8300e2011-11-15 13:07:32 -080016from autotest_lib.client.common_lib import error
17
18LD_LIBRARY_PATH = 'LD_LIBRARY_PATH'
19
Hsinyu Chaof80337a2012-04-07 18:02:29 +080020_DEFAULT_NUM_CHANNELS = 2
Dylan Reidbf9a5d42012-11-06 16:27:20 -080021_DEFAULT_REC_COMMAND = 'arecord -D hw:0,0 -d 10 -f dat'
Hsinyu Chaof80337a2012-04-07 18:02:29 +080022_DEFAULT_SOX_FORMAT = '-t raw -b 16 -e signed -r 48000 -L'
Dylan Reid51f289c2012-11-06 17:16:24 -080023_DEFAULT_SOX_RMS_THRESHOLD = 0.5
Hsinyu Chaof80337a2012-04-07 18:02:29 +080024
Hsin-Yu Chao8d093f42012-11-05 18:43:22 +080025_JACK_VALUE_ON_RE = re.compile('.*values=on')
26_HP_JACK_CONTROL_RE = re.compile('numid=(\d+).*Headphone\sJack')
27_MIC_JACK_CONTROL_RE = re.compile('numid=(\d+).*Mic\sJack')
28
Hsinyu Chaof80337a2012-04-07 18:02:29 +080029_SOX_RMS_AMPLITUDE_RE = re.compile('RMS\s+amplitude:\s+(.+)')
Hsinyu Chao2d64e1f2012-05-21 11:18:53 +080030_SOX_ROUGH_FREQ_RE = re.compile('Rough\s+frequency:\s+(.+)')
Hsinyu Chaof80337a2012-04-07 18:02:29 +080031_SOX_FORMAT = '-t raw -b 16 -e signed -r 48000 -L'
32
Hsin-Yu Chao95ee3512012-11-05 20:43:10 +080033_AUDIO_NOT_FOUND_RE = r'Audio\snot\sdetected'
34_MEASURED_LATENCY_RE = r'Measured\sLatency:\s(\d+)\suS'
35_REPORTED_LATENCY_RE = r'Reported\sLatency:\s(\d+)\suS'
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +080036
37class RecordSampleThread(threading.Thread):
38 '''Wraps the execution of arecord in a thread.'''
39 def __init__(self, audio, recordfile):
40 threading.Thread.__init__(self)
41 self._audio = audio
42 self._recordfile = recordfile
43
44 def run(self):
45 self._audio.record_sample(self._recordfile)
46
47
Hsinyu Chao4b8300e2011-11-15 13:07:32 -080048class AudioHelper(object):
49 '''
50 A helper class contains audio related utility functions.
51 '''
Dylan Reidbf9a5d42012-11-06 16:27:20 -080052 def __init__(self, test,
53 sox_format = _DEFAULT_SOX_FORMAT,
Dylan Reid51f289c2012-11-06 17:16:24 -080054 sox_threshold = _DEFAULT_SOX_RMS_THRESHOLD,
Dylan Reidbf9a5d42012-11-06 16:27:20 -080055 record_command = _DEFAULT_REC_COMMAND,
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +080056 num_channels = _DEFAULT_NUM_CHANNELS):
Hsinyu Chao4b8300e2011-11-15 13:07:32 -080057 self._test = test
Dylan Reid51f289c2012-11-06 17:16:24 -080058 self._sox_threshold = sox_threshold
Hsinyu Chaof80337a2012-04-07 18:02:29 +080059 self._sox_format = sox_format
Dylan Reidbf9a5d42012-11-06 16:27:20 -080060 self._rec_cmd = record_command
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +080061 self._num_channels = num_channels
Hsinyu Chao4b8300e2011-11-15 13:07:32 -080062
63 def setup_deps(self, deps):
64 '''
65 Sets up audio related dependencies.
66 '''
67 for dep in deps:
68 if dep == 'test_tones':
69 dep_dir = os.path.join(self._test.autodir, 'deps', dep)
70 self._test.job.install_pkg(dep, 'dep', dep_dir)
71 self.test_tones_path = os.path.join(dep_dir, 'src', dep)
72 elif dep == 'audioloop':
73 dep_dir = os.path.join(self._test.autodir, 'deps', dep)
74 self._test.job.install_pkg(dep, 'dep', dep_dir)
75 self.audioloop_path = os.path.join(dep_dir, 'src',
76 'looptest')
Hsin-Yu Chao95ee3512012-11-05 20:43:10 +080077 self.loopback_latency_path = os.path.join(dep_dir, 'src',
78 'loopback_latency')
Hsinyu Chao4b8300e2011-11-15 13:07:32 -080079 elif dep == 'sox':
80 dep_dir = os.path.join(self._test.autodir, 'deps', dep)
81 self._test.job.install_pkg(dep, 'dep', dep_dir)
82 self.sox_path = os.path.join(dep_dir, 'bin', dep)
83 self.sox_lib_path = os.path.join(dep_dir, 'lib')
84 if os.environ.has_key(LD_LIBRARY_PATH):
85 paths = os.environ[LD_LIBRARY_PATH].split(':')
86 if not self.sox_lib_path in paths:
87 paths.append(self.sox_lib_path)
88 os.environ[LD_LIBRARY_PATH] = ':'.join(paths)
89 else:
90 os.environ[LD_LIBRARY_PATH] = self.sox_lib_path
91
92 def cleanup_deps(self, deps):
93 '''
94 Cleans up environments which has been setup for dependencies.
95 '''
96 for dep in deps:
97 if dep == 'sox':
98 if (os.environ.has_key(LD_LIBRARY_PATH)
99 and hasattr(self, 'sox_lib_path')):
100 paths = filter(lambda x: x != self.sox_lib_path,
101 os.environ[LD_LIBRARY_PATH].split(':'))
102 os.environ[LD_LIBRARY_PATH] = ':'.join(paths)
103
Derek Basehoree973ce42012-07-10 23:38:32 -0700104 def set_volume_levels(self, volume, capture):
105 '''
106 Sets the volume and capture gain through cras_test_client
107 '''
108 logging.info('Setting volume level to %d' % volume)
109 utils.system('/usr/bin/cras_test_client --volume %d' % volume)
110 logging.info('Setting capture gain to %d' % capture)
111 utils.system('/usr/bin/cras_test_client --capture_gain %d' % capture)
112 utils.system('/usr/bin/cras_test_client --dump_server_info')
Dylan Reidc264e012012-11-06 18:26:12 -0800113 utils.system('/usr/bin/cras_test_client --mute 0')
Derek Basehoree973ce42012-07-10 23:38:32 -0700114 utils.system('amixer -c 0 contents')
115
Hsin-Yu Chao084e9da2012-11-07 15:56:26 +0800116 def get_mixer_jack_status(self, jack_reg_exp):
Hsin-Yu Chao8d093f42012-11-05 18:43:22 +0800117 '''
Hsin-Yu Chao084e9da2012-11-07 15:56:26 +0800118 Gets the mixer jack status.
Hsin-Yu Chao8d093f42012-11-05 18:43:22 +0800119
120 Args:
121 jack_reg_exp: The regular expression to match jack control name.
122
123 Returns:
124 None if the control does not exist, return True if jack control
125 is detected plugged, return False otherwise.
126 '''
127 output = utils.system_output('amixer -c0 controls', retain_output=True)
128 numid = None
129 for line in output.split('\n'):
130 m = jack_reg_exp.match(line)
131 if m:
132 numid = m.group(1)
133 break
Hsin-Yu Chao084e9da2012-11-07 15:56:26 +0800134
135 # Proceed only when matched numid is not empty.
136 if numid:
Hsin-Yu Chao8d093f42012-11-05 18:43:22 +0800137 output = utils.system_output('amixer -c0 cget numid=%s' % numid)
138 for line in output.split('\n'):
139 if _JACK_VALUE_ON_RE.match(line):
140 return True
141 return False
142 else:
143 return None
144
145 def get_hp_jack_status(self):
Hsin-Yu Chao084e9da2012-11-07 15:56:26 +0800146 status = self.get_mixer_jack_status(_HP_JACK_CONTROL_RE)
147 if status is not None:
148 return status
149
150 # When headphone jack is not found in amixer, lookup input devices
151 # instead.
152 #
153 # TODO(hychao): Check hp/mic jack status dynamically from evdev. And
154 # possibly replace the existing check using amixer.
155 for evdev in glob('/dev/input/event*'):
156 device = InputDevice(evdev)
157 if device.is_hp_jack():
158 return device.get_headphone_insert()
159 else:
160 return None
Hsin-Yu Chao8d093f42012-11-05 18:43:22 +0800161
162 def get_mic_jack_status(self):
Hsin-Yu Chao084e9da2012-11-07 15:56:26 +0800163 status = self.get_mixer_jack_status(_MIC_JACK_CONTROL_RE)
164 if status is not None:
165 return status
166
167 # When mic jack is not found in amixer, lookup input devices instead.
168 for evdev in glob('/dev/input/event*'):
169 device = InputDevice(evdev)
170 if device.is_mic_jack():
171 return device.get_microphone_insert()
172 else:
173 return None
Hsin-Yu Chao8d093f42012-11-05 18:43:22 +0800174
175 def check_loopback_dongle(self):
176 '''
177 Checks if loopback dongle is equipped correctly.
178 '''
179 # Check Mic Jack
180 mic_jack_status = self.get_mic_jack_status()
181 if mic_jack_status is None:
182 logging.warning('Found no Mic Jack control, skip check.')
183 elif not mic_jack_status:
184 logging.info('Mic jack is not plugged.')
185 return False
186 else:
187 logging.info('Mic jack is plugged.')
188
189 # Check Headphone Jack
190 hp_jack_status = self.get_hp_jack_status()
191 if hp_jack_status is None:
192 logging.warning('Found no Headphone Jack control, skip check.')
193 elif not hp_jack_status:
194 logging.info('Headphone jack is not plugged.')
195 return False
196 else:
197 logging.info('Headphone jack is plugged.')
198
199 return True
200
Hsinyu Chao4b8300e2011-11-15 13:07:32 -0800201 def set_mixer_controls(self, mixer_settings={}, card='0'):
202 '''
203 Sets all mixer controls listed in the mixer settings on card.
204 '''
205 logging.info('Setting mixer control values on %s' % card)
206 for item in mixer_settings:
207 logging.info('Setting %s to %s on card %s' %
208 (item['name'], item['value'], card))
209 cmd = 'amixer -c %s cset name=%s %s'
210 cmd = cmd % (card, item['name'], item['value'])
211 try:
212 utils.system(cmd)
213 except error.CmdError:
214 # A card is allowed not to support all the controls, so don't
215 # fail the test here if we get an error.
216 logging.info('amixer command failed: %s' % cmd)
Hsinyu Chaof80337a2012-04-07 18:02:29 +0800217
Hsinyu Chao2d64e1f2012-05-21 11:18:53 +0800218 def sox_stat_output(self, infile, channel):
Hsinyu Chaof80337a2012-04-07 18:02:29 +0800219 sox_mixer_cmd = self.get_sox_mixer_cmd(infile, channel)
220 stat_cmd = '%s -c 1 %s - -n stat 2>&1' % (self.sox_path,
221 self._sox_format)
222 sox_cmd = '%s | %s' % (sox_mixer_cmd, stat_cmd)
Hsinyu Chao2d64e1f2012-05-21 11:18:53 +0800223 return utils.system_output(sox_cmd, retain_output=True)
224
225 def get_audio_rms(self, sox_output):
Hsinyu Chaof80337a2012-04-07 18:02:29 +0800226 for rms_line in sox_output.split('\n'):
227 m = _SOX_RMS_AMPLITUDE_RE.match(rms_line)
228 if m is not None:
229 return float(m.group(1))
230
Hsinyu Chao2d64e1f2012-05-21 11:18:53 +0800231 def get_rough_freq(self, sox_output):
232 for rms_line in sox_output.split('\n'):
233 m = _SOX_ROUGH_FREQ_RE.match(rms_line)
234 if m is not None:
235 return int(m.group(1))
236
237
Hsinyu Chaof80337a2012-04-07 18:02:29 +0800238 def get_sox_mixer_cmd(self, infile, channel):
239 # Build up a pan value string for the sox command.
240 if channel == 0:
241 pan_values = '1'
242 else:
243 pan_values = '0'
244 for pan_index in range(1, self._num_channels):
245 if channel == pan_index:
246 pan_values = '%s%s' % (pan_values, ',1')
247 else:
248 pan_values = '%s%s' % (pan_values, ',0')
249
250 return '%s -c 2 %s %s -c 1 %s - mixer %s' % (self.sox_path,
251 self._sox_format, infile, self._sox_format, pan_values)
252
253 def noise_reduce_file(self, in_file, noise_file, out_file):
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +0800254 '''Runs the sox command to noise-reduce in_file using
Hsinyu Chaof80337a2012-04-07 18:02:29 +0800255 the noise profile from noise_file.
256
257 Args:
258 in_file: The file to noise reduce.
259 noise_file: The file containing the noise profile.
260 This can be created by recording silence.
261 out_file: The file contains the noise reduced sound.
262
263 Returns:
264 The name of the file containing the noise-reduced data.
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +0800265 '''
Hsinyu Chaof80337a2012-04-07 18:02:29 +0800266 prof_cmd = '%s -c 2 %s %s -n noiseprof' % (self.sox_path,
267 _SOX_FORMAT, noise_file)
268 reduce_cmd = ('%s -c 2 %s %s -c 2 %s %s noisered' %
269 (self.sox_path, _SOX_FORMAT, in_file, _SOX_FORMAT, out_file))
270 utils.system('%s | %s' % (prof_cmd, reduce_cmd))
271
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +0800272 def record_sample(self, tmpfile):
273 '''Records a sample from the default input device.
Hsinyu Chaof80337a2012-04-07 18:02:29 +0800274
275 Args:
276 duration: How long to record in seconds.
277 tmpfile: The file to record to.
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +0800278 '''
Dylan Reidbf9a5d42012-11-06 16:27:20 -0800279 cmd_rec = self._rec_cmd + ' %s' % tmpfile
280 logging.info('Command %s recording now' % cmd_rec)
Hsinyu Chaof80337a2012-04-07 18:02:29 +0800281 utils.system(cmd_rec)
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +0800282
Dylan Reid51f289c2012-11-06 17:16:24 -0800283 def loopback_test_channels(self, noise_file, loopback_callback):
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +0800284 '''Tests loopback on all channels.
285
286 Args:
287 noise_file: The file contains the pre-recorded noise.
288 loopback_callback: The callback to do the loopback for one channel.
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +0800289 '''
290 for channel in xrange(self._num_channels):
291 # Temp file for the final noise-reduced file.
292 with tempfile.NamedTemporaryFile(mode='w+t') as reduced_file:
293 # Temp file that records before noise reduction.
294 with tempfile.NamedTemporaryFile(mode='w+t') as tmpfile:
295 record_thread = RecordSampleThread(self, tmpfile.name)
296 record_thread.start()
297 loopback_callback(channel)
298 record_thread.join()
299
300 self.noise_reduce_file(tmpfile.name, noise_file.name,
301 reduced_file.name)
302
Hsinyu Chao2d64e1f2012-05-21 11:18:53 +0800303 sox_output = self.sox_stat_output(reduced_file.name, channel)
Dylan Reid51f289c2012-11-06 17:16:24 -0800304 self.check_recorded(sox_output)
305
306 def check_recorded(self, sox_output):
307 """Checks if the calculated RMS value is expected.
308
309 Args:
310 sox_output: The output from sox stat command.
311
312 Raises:
313 error.TestFail if the RMS amplitude of the recording isn't above
314 the threshold.
315 """
316 rms_val = self.get_audio_rms(sox_output)
317
318 # In case we don't get a valid RMS value.
319 if rms_val is None:
320 raise error.TestError(
321 'Failed to generate an audio RMS value from playback.')
322
323 logging.info('Got audio RMS value of %f. Minimum pass is %f.' %
324 (rms_val, self._sox_threshold))
325 if rms_val < self._sox_threshold:
326 raise error.TestError(
327 'Audio RMS value %f too low. Minimum pass is %f.' %
328 (rms_val, self._sox_threshold))
Hsin-Yu Chao95ee3512012-11-05 20:43:10 +0800329
330 def loopback_latency_check(self, **args):
331 '''
332 Checks loopback latency.
333
334 Args:
335 args: additional arguments for loopback_latency.
336
337 Returns:
338 A tuple containing measured and reported latency in uS.
339 Return None if no audio detected.
340 '''
341 noise_threshold = str(args['n']) if args.has_key('n') else '400'
342
343 cmd = '%s -n %s' % (self.loopback_latency_path, noise_threshold)
344
345 output = utils.system_output(cmd)
346 measured_latency = None
347 reported_latency = None
348 for line in output.split('\n'):
349 match = re.search(_MEASURED_LATENCY_RE, line, re.I)
350 if match:
351 measured_latency = int(match.group(1))
352 continue
353 match = re.search(_REPORTED_LATENCY_RE, line, re.I)
354 if match:
355 reported_latency = int(match.group(1))
356 continue
357 if re.search(_AUDIO_NOT_FOUND_RE, line, re.I):
358 return None
359 if measured_latency and reported_latency:
360 return (measured_latency, reported_latency)
361 else:
362 # Should not reach here, just in case.
363 return None