blob: a3645f929658be1b0a78ac27d66f88f2eaa5aaf7 [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
Hsin-Yu Chaof272d8e2013-04-05 03:28:50 +080011import time
Hsinyu Chao4b8300e2011-11-15 13:07:32 -080012
Hsin-Yu Chao084e9da2012-11-07 15:56:26 +080013from glob import glob
14
Hsinyu Chao4b8300e2011-11-15 13:07:32 -080015from autotest_lib.client.bin import utils
Hsin-Yu Chao084e9da2012-11-07 15:56:26 +080016from autotest_lib.client.bin.input.input_device import *
Hsinyu Chao4b8300e2011-11-15 13:07:32 -080017from autotest_lib.client.common_lib import error
18
19LD_LIBRARY_PATH = 'LD_LIBRARY_PATH'
20
Hsinyu Chaof80337a2012-04-07 18:02:29 +080021_DEFAULT_NUM_CHANNELS = 2
Dylan Reidbf9a5d42012-11-06 16:27:20 -080022_DEFAULT_REC_COMMAND = 'arecord -D hw:0,0 -d 10 -f dat'
Hsinyu Chaof80337a2012-04-07 18:02:29 +080023_DEFAULT_SOX_FORMAT = '-t raw -b 16 -e signed -r 48000 -L'
Dylan Reid51f289c2012-11-06 17:16:24 -080024_DEFAULT_SOX_RMS_THRESHOLD = 0.5
Hsinyu Chaof80337a2012-04-07 18:02:29 +080025
Hsin-Yu Chao8d093f42012-11-05 18:43:22 +080026_JACK_VALUE_ON_RE = re.compile('.*values=on')
27_HP_JACK_CONTROL_RE = re.compile('numid=(\d+).*Headphone\sJack')
28_MIC_JACK_CONTROL_RE = re.compile('numid=(\d+).*Mic\sJack')
29
Hsinyu Chaof80337a2012-04-07 18:02:29 +080030_SOX_RMS_AMPLITUDE_RE = re.compile('RMS\s+amplitude:\s+(.+)')
Hsinyu Chao2d64e1f2012-05-21 11:18:53 +080031_SOX_ROUGH_FREQ_RE = re.compile('Rough\s+frequency:\s+(.+)')
Hsinyu Chaof80337a2012-04-07 18:02:29 +080032_SOX_FORMAT = '-t raw -b 16 -e signed -r 48000 -L'
33
Hsin-Yu Chao95ee3512012-11-05 20:43:10 +080034_AUDIO_NOT_FOUND_RE = r'Audio\snot\sdetected'
35_MEASURED_LATENCY_RE = r'Measured\sLatency:\s(\d+)\suS'
36_REPORTED_LATENCY_RE = r'Reported\sLatency:\s(\d+)\suS'
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +080037
38class RecordSampleThread(threading.Thread):
39 '''Wraps the execution of arecord in a thread.'''
40 def __init__(self, audio, recordfile):
41 threading.Thread.__init__(self)
42 self._audio = audio
43 self._recordfile = recordfile
44
45 def run(self):
46 self._audio.record_sample(self._recordfile)
47
48
Hsinyu Chao4b8300e2011-11-15 13:07:32 -080049class AudioHelper(object):
50 '''
51 A helper class contains audio related utility functions.
52 '''
Dylan Reidbf9a5d42012-11-06 16:27:20 -080053 def __init__(self, test,
54 sox_format = _DEFAULT_SOX_FORMAT,
Dylan Reid51f289c2012-11-06 17:16:24 -080055 sox_threshold = _DEFAULT_SOX_RMS_THRESHOLD,
Dylan Reidbf9a5d42012-11-06 16:27:20 -080056 record_command = _DEFAULT_REC_COMMAND,
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +080057 num_channels = _DEFAULT_NUM_CHANNELS):
Hsinyu Chao4b8300e2011-11-15 13:07:32 -080058 self._test = test
Dylan Reid51f289c2012-11-06 17:16:24 -080059 self._sox_threshold = sox_threshold
Hsinyu Chaof80337a2012-04-07 18:02:29 +080060 self._sox_format = sox_format
Dylan Reidbf9a5d42012-11-06 16:27:20 -080061 self._rec_cmd = record_command
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +080062 self._num_channels = num_channels
Hsinyu Chao4b8300e2011-11-15 13:07:32 -080063
64 def setup_deps(self, deps):
65 '''
66 Sets up audio related dependencies.
67 '''
68 for dep in deps:
69 if dep == 'test_tones':
70 dep_dir = os.path.join(self._test.autodir, 'deps', dep)
71 self._test.job.install_pkg(dep, 'dep', dep_dir)
72 self.test_tones_path = os.path.join(dep_dir, 'src', dep)
73 elif dep == 'audioloop':
74 dep_dir = os.path.join(self._test.autodir, 'deps', dep)
75 self._test.job.install_pkg(dep, 'dep', dep_dir)
76 self.audioloop_path = os.path.join(dep_dir, 'src',
77 'looptest')
Hsin-Yu Chao95ee3512012-11-05 20:43:10 +080078 self.loopback_latency_path = os.path.join(dep_dir, 'src',
79 'loopback_latency')
Hsinyu Chao4b8300e2011-11-15 13:07:32 -080080 elif dep == 'sox':
81 dep_dir = os.path.join(self._test.autodir, 'deps', dep)
82 self._test.job.install_pkg(dep, 'dep', dep_dir)
83 self.sox_path = os.path.join(dep_dir, 'bin', dep)
84 self.sox_lib_path = os.path.join(dep_dir, 'lib')
85 if os.environ.has_key(LD_LIBRARY_PATH):
86 paths = os.environ[LD_LIBRARY_PATH].split(':')
87 if not self.sox_lib_path in paths:
88 paths.append(self.sox_lib_path)
89 os.environ[LD_LIBRARY_PATH] = ':'.join(paths)
90 else:
91 os.environ[LD_LIBRARY_PATH] = self.sox_lib_path
92
93 def cleanup_deps(self, deps):
94 '''
95 Cleans up environments which has been setup for dependencies.
96 '''
97 for dep in deps:
98 if dep == 'sox':
99 if (os.environ.has_key(LD_LIBRARY_PATH)
100 and hasattr(self, 'sox_lib_path')):
101 paths = filter(lambda x: x != self.sox_lib_path,
102 os.environ[LD_LIBRARY_PATH].split(':'))
103 os.environ[LD_LIBRARY_PATH] = ':'.join(paths)
104
Derek Basehoree973ce42012-07-10 23:38:32 -0700105 def set_volume_levels(self, volume, capture):
106 '''
107 Sets the volume and capture gain through cras_test_client
108 '''
109 logging.info('Setting volume level to %d' % volume)
110 utils.system('/usr/bin/cras_test_client --volume %d' % volume)
111 logging.info('Setting capture gain to %d' % capture)
112 utils.system('/usr/bin/cras_test_client --capture_gain %d' % capture)
113 utils.system('/usr/bin/cras_test_client --dump_server_info')
Dylan Reidc264e012012-11-06 18:26:12 -0800114 utils.system('/usr/bin/cras_test_client --mute 0')
Derek Basehoree973ce42012-07-10 23:38:32 -0700115 utils.system('amixer -c 0 contents')
116
Hsin-Yu Chao084e9da2012-11-07 15:56:26 +0800117 def get_mixer_jack_status(self, jack_reg_exp):
Hsin-Yu Chao8d093f42012-11-05 18:43:22 +0800118 '''
Hsin-Yu Chao084e9da2012-11-07 15:56:26 +0800119 Gets the mixer jack status.
Hsin-Yu Chao8d093f42012-11-05 18:43:22 +0800120
121 Args:
122 jack_reg_exp: The regular expression to match jack control name.
123
124 Returns:
125 None if the control does not exist, return True if jack control
126 is detected plugged, return False otherwise.
127 '''
128 output = utils.system_output('amixer -c0 controls', retain_output=True)
129 numid = None
130 for line in output.split('\n'):
131 m = jack_reg_exp.match(line)
132 if m:
133 numid = m.group(1)
134 break
Hsin-Yu Chao084e9da2012-11-07 15:56:26 +0800135
136 # Proceed only when matched numid is not empty.
137 if numid:
Hsin-Yu Chao8d093f42012-11-05 18:43:22 +0800138 output = utils.system_output('amixer -c0 cget numid=%s' % numid)
139 for line in output.split('\n'):
140 if _JACK_VALUE_ON_RE.match(line):
141 return True
142 return False
143 else:
144 return None
145
146 def get_hp_jack_status(self):
Hsin-Yu Chao084e9da2012-11-07 15:56:26 +0800147 status = self.get_mixer_jack_status(_HP_JACK_CONTROL_RE)
148 if status is not None:
149 return status
150
151 # When headphone jack is not found in amixer, lookup input devices
152 # instead.
153 #
154 # TODO(hychao): Check hp/mic jack status dynamically from evdev. And
155 # possibly replace the existing check using amixer.
156 for evdev in glob('/dev/input/event*'):
157 device = InputDevice(evdev)
158 if device.is_hp_jack():
159 return device.get_headphone_insert()
160 else:
161 return None
Hsin-Yu Chao8d093f42012-11-05 18:43:22 +0800162
163 def get_mic_jack_status(self):
Hsin-Yu Chao084e9da2012-11-07 15:56:26 +0800164 status = self.get_mixer_jack_status(_MIC_JACK_CONTROL_RE)
165 if status is not None:
166 return status
167
168 # When mic jack is not found in amixer, lookup input devices instead.
169 for evdev in glob('/dev/input/event*'):
170 device = InputDevice(evdev)
171 if device.is_mic_jack():
172 return device.get_microphone_insert()
173 else:
174 return None
Hsin-Yu Chao8d093f42012-11-05 18:43:22 +0800175
176 def check_loopback_dongle(self):
177 '''
178 Checks if loopback dongle is equipped correctly.
179 '''
180 # Check Mic Jack
181 mic_jack_status = self.get_mic_jack_status()
182 if mic_jack_status is None:
183 logging.warning('Found no Mic Jack control, skip check.')
184 elif not mic_jack_status:
185 logging.info('Mic jack is not plugged.')
186 return False
187 else:
188 logging.info('Mic jack is plugged.')
189
190 # Check Headphone Jack
191 hp_jack_status = self.get_hp_jack_status()
192 if hp_jack_status is None:
193 logging.warning('Found no Headphone Jack control, skip check.')
194 elif not hp_jack_status:
195 logging.info('Headphone jack is not plugged.')
196 return False
197 else:
198 logging.info('Headphone jack is plugged.')
199
Hsin-Yu Chao3953f522012-11-12 18:39:39 +0800200 # Use latency check to test if audio can be captured through dongle.
201 # We only want to know the basic function of dongle, so no need to
202 # assert the latency accuracy here.
203 latency = self.loopback_latency_check(n=4000)
204 if latency:
205 logging.info('Got latency measured %d, reported %d' %
206 (latency[0], latency[1]))
207 else:
208 logging.warning('Latency check fail.')
209 return False
210
Hsin-Yu Chao8d093f42012-11-05 18:43:22 +0800211 return True
212
Hsinyu Chao4b8300e2011-11-15 13:07:32 -0800213 def set_mixer_controls(self, mixer_settings={}, card='0'):
214 '''
215 Sets all mixer controls listed in the mixer settings on card.
216 '''
217 logging.info('Setting mixer control values on %s' % card)
218 for item in mixer_settings:
219 logging.info('Setting %s to %s on card %s' %
220 (item['name'], item['value'], card))
221 cmd = 'amixer -c %s cset name=%s %s'
222 cmd = cmd % (card, item['name'], item['value'])
223 try:
224 utils.system(cmd)
225 except error.CmdError:
226 # A card is allowed not to support all the controls, so don't
227 # fail the test here if we get an error.
228 logging.info('amixer command failed: %s' % cmd)
Hsinyu Chaof80337a2012-04-07 18:02:29 +0800229
Hsinyu Chao2d64e1f2012-05-21 11:18:53 +0800230 def sox_stat_output(self, infile, channel):
Hsinyu Chaof80337a2012-04-07 18:02:29 +0800231 sox_mixer_cmd = self.get_sox_mixer_cmd(infile, channel)
232 stat_cmd = '%s -c 1 %s - -n stat 2>&1' % (self.sox_path,
233 self._sox_format)
234 sox_cmd = '%s | %s' % (sox_mixer_cmd, stat_cmd)
Hsinyu Chao2d64e1f2012-05-21 11:18:53 +0800235 return utils.system_output(sox_cmd, retain_output=True)
236
237 def get_audio_rms(self, sox_output):
Hsinyu Chaof80337a2012-04-07 18:02:29 +0800238 for rms_line in sox_output.split('\n'):
239 m = _SOX_RMS_AMPLITUDE_RE.match(rms_line)
240 if m is not None:
241 return float(m.group(1))
242
Hsinyu Chao2d64e1f2012-05-21 11:18:53 +0800243 def get_rough_freq(self, sox_output):
244 for rms_line in sox_output.split('\n'):
245 m = _SOX_ROUGH_FREQ_RE.match(rms_line)
246 if m is not None:
247 return int(m.group(1))
248
249
Hsinyu Chaof80337a2012-04-07 18:02:29 +0800250 def get_sox_mixer_cmd(self, infile, channel):
251 # Build up a pan value string for the sox command.
252 if channel == 0:
253 pan_values = '1'
254 else:
255 pan_values = '0'
256 for pan_index in range(1, self._num_channels):
257 if channel == pan_index:
258 pan_values = '%s%s' % (pan_values, ',1')
259 else:
260 pan_values = '%s%s' % (pan_values, ',0')
261
262 return '%s -c 2 %s %s -c 1 %s - mixer %s' % (self.sox_path,
263 self._sox_format, infile, self._sox_format, pan_values)
264
265 def noise_reduce_file(self, in_file, noise_file, out_file):
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +0800266 '''Runs the sox command to noise-reduce in_file using
Hsinyu Chaof80337a2012-04-07 18:02:29 +0800267 the noise profile from noise_file.
268
269 Args:
270 in_file: The file to noise reduce.
271 noise_file: The file containing the noise profile.
272 This can be created by recording silence.
273 out_file: The file contains the noise reduced sound.
274
275 Returns:
276 The name of the file containing the noise-reduced data.
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +0800277 '''
Hsinyu Chaof80337a2012-04-07 18:02:29 +0800278 prof_cmd = '%s -c 2 %s %s -n noiseprof' % (self.sox_path,
279 _SOX_FORMAT, noise_file)
280 reduce_cmd = ('%s -c 2 %s %s -c 2 %s %s noisered' %
281 (self.sox_path, _SOX_FORMAT, in_file, _SOX_FORMAT, out_file))
282 utils.system('%s | %s' % (prof_cmd, reduce_cmd))
283
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +0800284 def record_sample(self, tmpfile):
285 '''Records a sample from the default input device.
Hsinyu Chaof80337a2012-04-07 18:02:29 +0800286
287 Args:
288 duration: How long to record in seconds.
289 tmpfile: The file to record to.
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +0800290 '''
Dylan Reidbf9a5d42012-11-06 16:27:20 -0800291 cmd_rec = self._rec_cmd + ' %s' % tmpfile
292 logging.info('Command %s recording now' % cmd_rec)
Hsinyu Chaof80337a2012-04-07 18:02:29 +0800293 utils.system(cmd_rec)
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +0800294
Hsin-Yu Chao11041d32012-11-13 15:46:13 +0800295 def loopback_test_channels(self, noise_file, loopback_callback,
296 check_recorded_callback=None):
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +0800297 '''Tests loopback on all channels.
298
299 Args:
300 noise_file: The file contains the pre-recorded noise.
301 loopback_callback: The callback to do the loopback for one channel.
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +0800302 '''
303 for channel in xrange(self._num_channels):
304 # Temp file for the final noise-reduced file.
305 with tempfile.NamedTemporaryFile(mode='w+t') as reduced_file:
306 # Temp file that records before noise reduction.
307 with tempfile.NamedTemporaryFile(mode='w+t') as tmpfile:
308 record_thread = RecordSampleThread(self, tmpfile.name)
309 record_thread.start()
310 loopback_callback(channel)
311 record_thread.join()
312
313 self.noise_reduce_file(tmpfile.name, noise_file.name,
314 reduced_file.name)
315
Hsinyu Chao2d64e1f2012-05-21 11:18:53 +0800316 sox_output = self.sox_stat_output(reduced_file.name, channel)
Hsin-Yu Chao11041d32012-11-13 15:46:13 +0800317
318 # Use injected check recorded callback if any.
319 if check_recorded_callback:
320 check_recorded_callback(sox_output)
321 else:
322 self.check_recorded(sox_output)
Dylan Reid51f289c2012-11-06 17:16:24 -0800323
324 def check_recorded(self, sox_output):
325 """Checks if the calculated RMS value is expected.
326
327 Args:
328 sox_output: The output from sox stat command.
329
330 Raises:
Hsin-Yu Chao84e86d22013-04-03 00:43:01 +0800331 error.TestError if RMS amplitude can't be parsed.
Dylan Reid51f289c2012-11-06 17:16:24 -0800332 error.TestFail if the RMS amplitude of the recording isn't above
333 the threshold.
334 """
335 rms_val = self.get_audio_rms(sox_output)
336
337 # In case we don't get a valid RMS value.
338 if rms_val is None:
339 raise error.TestError(
340 'Failed to generate an audio RMS value from playback.')
341
342 logging.info('Got audio RMS value of %f. Minimum pass is %f.' %
343 (rms_val, self._sox_threshold))
344 if rms_val < self._sox_threshold:
Hsin-Yu Chao84e86d22013-04-03 00:43:01 +0800345 raise error.TestFail(
Dylan Reid51f289c2012-11-06 17:16:24 -0800346 'Audio RMS value %f too low. Minimum pass is %f.' %
347 (rms_val, self._sox_threshold))
Hsin-Yu Chao95ee3512012-11-05 20:43:10 +0800348
349 def loopback_latency_check(self, **args):
350 '''
351 Checks loopback latency.
352
353 Args:
354 args: additional arguments for loopback_latency.
355
356 Returns:
357 A tuple containing measured and reported latency in uS.
358 Return None if no audio detected.
359 '''
360 noise_threshold = str(args['n']) if args.has_key('n') else '400'
361
362 cmd = '%s -n %s' % (self.loopback_latency_path, noise_threshold)
363
364 output = utils.system_output(cmd)
Hsin-Yu Chaof272d8e2013-04-05 03:28:50 +0800365
366 # Sleep for a short while to make sure device is not busy anymore
367 # after called loopback_latency.
368 time.sleep(.1)
369
Hsin-Yu Chao95ee3512012-11-05 20:43:10 +0800370 measured_latency = None
371 reported_latency = None
372 for line in output.split('\n'):
373 match = re.search(_MEASURED_LATENCY_RE, line, re.I)
374 if match:
375 measured_latency = int(match.group(1))
376 continue
377 match = re.search(_REPORTED_LATENCY_RE, line, re.I)
378 if match:
379 reported_latency = int(match.group(1))
380 continue
381 if re.search(_AUDIO_NOT_FOUND_RE, line, re.I):
382 return None
383 if measured_latency and reported_latency:
384 return (measured_latency, reported_latency)
385 else:
386 # Should not reach here, just in case.
387 return None
Simon Quea4be3442012-11-14 16:36:56 -0800388
389 def play_sound(self, duration_seconds=None, audio_file_path=None):
390 '''
391 Plays a sound file found at |audio_file_path| for |duration_seconds|.
392
393 If |audio_file_path|=None, plays a default audio file.
394 If |duration_seconds|=None, plays audio file in its entirety.
395 '''
396 if not audio_file_path:
397 audio_file_path = '/usr/local/autotest/cros/audio/sine440.wav'
398 duration_arg = ('-d %d' % duration_seconds) if duration_seconds else ''
399 utils.system('aplay %s %s' % (duration_arg, audio_file_path))
Hsin-Yu Chaob443f5d2013-03-12 18:36:18 +0800400
Hsin-Yu Chaoe6bb7932013-03-22 14:08:54 +0800401 def get_play_sine_args(self, channel, odev='default', freq=1000, duration=10,
Hsin-Yu Chaob443f5d2013-03-12 18:36:18 +0800402 sample_size=16):
Hsin-Yu Chaoe6bb7932013-03-22 14:08:54 +0800403 '''Gets the command args to generate a sine wav to play to odev.
Hsin-Yu Chaob443f5d2013-03-12 18:36:18 +0800404
405 Args:
Hsin-Yu Chaoe6bb7932013-03-22 14:08:54 +0800406 channel: 0 for left, 1 for right; otherwize, mono.
Hsin-Yu Chaob443f5d2013-03-12 18:36:18 +0800407 odev: alsa output device.
Hsin-Yu Chaoe6bb7932013-03-22 14:08:54 +0800408 freq: frequency of the generated sine tone.
409 duration: duration of the generated sine tone.
Hsin-Yu Chaob443f5d2013-03-12 18:36:18 +0800410 sample_size: output audio sample size. Default to 16.
411 '''
Hsin-Yu Chaoe6bb7932013-03-22 14:08:54 +0800412 cmdargs = [self.sox_path, '-b', str(sample_size), '-n', '-t', 'alsa',
413 odev, 'synth', str(duration)]
Hsin-Yu Chaob443f5d2013-03-12 18:36:18 +0800414 if channel == 0:
Hsin-Yu Chaoe6bb7932013-03-22 14:08:54 +0800415 cmdargs += ['sine', str(freq), 'sine', '0']
Hsin-Yu Chaob443f5d2013-03-12 18:36:18 +0800416 elif channel == 1:
Hsin-Yu Chaoe6bb7932013-03-22 14:08:54 +0800417 cmdargs += ['sine', '0', 'sine', str(freq)]
Hsin-Yu Chaob443f5d2013-03-12 18:36:18 +0800418 else:
Hsin-Yu Chaoe6bb7932013-03-22 14:08:54 +0800419 cmdargs += ['sine', str(freq)]
420
421 return cmdargs
422
423 def play_sine(self, channel, odev='default', freq=1000, duration=10,
424 sample_size=16):
425 '''Generates a sine wave and plays to odev.'''
Hsin-Yu Chao1b641072013-03-25 18:23:44 +0800426 cmdargs = self.get_play_sine_args(channel, odev, freq, duration, sample_size)
Hsin-Yu Chaoe6bb7932013-03-22 14:08:54 +0800427 utils.system(' '.join(cmdargs))