blob: 4a604d6f59bc44a22b8f7fe6e5d1d112c4d30e49 [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
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +080033
34class RecordSampleThread(threading.Thread):
35 '''Wraps the execution of arecord in a thread.'''
36 def __init__(self, audio, recordfile):
37 threading.Thread.__init__(self)
38 self._audio = audio
39 self._recordfile = recordfile
40
41 def run(self):
42 self._audio.record_sample(self._recordfile)
43
44
Hsinyu Chao4b8300e2011-11-15 13:07:32 -080045class AudioHelper(object):
46 '''
47 A helper class contains audio related utility functions.
48 '''
Dylan Reidbf9a5d42012-11-06 16:27:20 -080049 def __init__(self, test,
50 sox_format = _DEFAULT_SOX_FORMAT,
Dylan Reid51f289c2012-11-06 17:16:24 -080051 sox_threshold = _DEFAULT_SOX_RMS_THRESHOLD,
Dylan Reidbf9a5d42012-11-06 16:27:20 -080052 record_command = _DEFAULT_REC_COMMAND,
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +080053 num_channels = _DEFAULT_NUM_CHANNELS):
Hsinyu Chao4b8300e2011-11-15 13:07:32 -080054 self._test = test
Dylan Reid51f289c2012-11-06 17:16:24 -080055 self._sox_threshold = sox_threshold
Hsinyu Chaof80337a2012-04-07 18:02:29 +080056 self._sox_format = sox_format
Dylan Reidbf9a5d42012-11-06 16:27:20 -080057 self._rec_cmd = record_command
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +080058 self._num_channels = num_channels
Hsinyu Chao4b8300e2011-11-15 13:07:32 -080059
60 def setup_deps(self, deps):
61 '''
62 Sets up audio related dependencies.
63 '''
64 for dep in deps:
65 if dep == 'test_tones':
66 dep_dir = os.path.join(self._test.autodir, 'deps', dep)
67 self._test.job.install_pkg(dep, 'dep', dep_dir)
68 self.test_tones_path = os.path.join(dep_dir, 'src', dep)
69 elif dep == 'audioloop':
70 dep_dir = os.path.join(self._test.autodir, 'deps', dep)
71 self._test.job.install_pkg(dep, 'dep', dep_dir)
72 self.audioloop_path = os.path.join(dep_dir, 'src',
73 'looptest')
74 elif dep == 'sox':
75 dep_dir = os.path.join(self._test.autodir, 'deps', dep)
76 self._test.job.install_pkg(dep, 'dep', dep_dir)
77 self.sox_path = os.path.join(dep_dir, 'bin', dep)
78 self.sox_lib_path = os.path.join(dep_dir, 'lib')
79 if os.environ.has_key(LD_LIBRARY_PATH):
80 paths = os.environ[LD_LIBRARY_PATH].split(':')
81 if not self.sox_lib_path in paths:
82 paths.append(self.sox_lib_path)
83 os.environ[LD_LIBRARY_PATH] = ':'.join(paths)
84 else:
85 os.environ[LD_LIBRARY_PATH] = self.sox_lib_path
86
87 def cleanup_deps(self, deps):
88 '''
89 Cleans up environments which has been setup for dependencies.
90 '''
91 for dep in deps:
92 if dep == 'sox':
93 if (os.environ.has_key(LD_LIBRARY_PATH)
94 and hasattr(self, 'sox_lib_path')):
95 paths = filter(lambda x: x != self.sox_lib_path,
96 os.environ[LD_LIBRARY_PATH].split(':'))
97 os.environ[LD_LIBRARY_PATH] = ':'.join(paths)
98
Derek Basehoree973ce42012-07-10 23:38:32 -070099 def set_volume_levels(self, volume, capture):
100 '''
101 Sets the volume and capture gain through cras_test_client
102 '''
103 logging.info('Setting volume level to %d' % volume)
104 utils.system('/usr/bin/cras_test_client --volume %d' % volume)
105 logging.info('Setting capture gain to %d' % capture)
106 utils.system('/usr/bin/cras_test_client --capture_gain %d' % capture)
107 utils.system('/usr/bin/cras_test_client --dump_server_info')
Dylan Reidc264e012012-11-06 18:26:12 -0800108 utils.system('/usr/bin/cras_test_client --mute 0')
Derek Basehoree973ce42012-07-10 23:38:32 -0700109 utils.system('amixer -c 0 contents')
110
Hsin-Yu Chao084e9da2012-11-07 15:56:26 +0800111 def get_mixer_jack_status(self, jack_reg_exp):
Hsin-Yu Chao8d093f42012-11-05 18:43:22 +0800112 '''
Hsin-Yu Chao084e9da2012-11-07 15:56:26 +0800113 Gets the mixer jack status.
Hsin-Yu Chao8d093f42012-11-05 18:43:22 +0800114
115 Args:
116 jack_reg_exp: The regular expression to match jack control name.
117
118 Returns:
119 None if the control does not exist, return True if jack control
120 is detected plugged, return False otherwise.
121 '''
122 output = utils.system_output('amixer -c0 controls', retain_output=True)
123 numid = None
124 for line in output.split('\n'):
125 m = jack_reg_exp.match(line)
126 if m:
127 numid = m.group(1)
128 break
Hsin-Yu Chao084e9da2012-11-07 15:56:26 +0800129
130 # Proceed only when matched numid is not empty.
131 if numid:
Hsin-Yu Chao8d093f42012-11-05 18:43:22 +0800132 output = utils.system_output('amixer -c0 cget numid=%s' % numid)
133 for line in output.split('\n'):
134 if _JACK_VALUE_ON_RE.match(line):
135 return True
136 return False
137 else:
138 return None
139
140 def get_hp_jack_status(self):
Hsin-Yu Chao084e9da2012-11-07 15:56:26 +0800141 status = self.get_mixer_jack_status(_HP_JACK_CONTROL_RE)
142 if status is not None:
143 return status
144
145 # When headphone jack is not found in amixer, lookup input devices
146 # instead.
147 #
148 # TODO(hychao): Check hp/mic jack status dynamically from evdev. And
149 # possibly replace the existing check using amixer.
150 for evdev in glob('/dev/input/event*'):
151 device = InputDevice(evdev)
152 if device.is_hp_jack():
153 return device.get_headphone_insert()
154 else:
155 return None
Hsin-Yu Chao8d093f42012-11-05 18:43:22 +0800156
157 def get_mic_jack_status(self):
Hsin-Yu Chao084e9da2012-11-07 15:56:26 +0800158 status = self.get_mixer_jack_status(_MIC_JACK_CONTROL_RE)
159 if status is not None:
160 return status
161
162 # When mic jack is not found in amixer, lookup input devices instead.
163 for evdev in glob('/dev/input/event*'):
164 device = InputDevice(evdev)
165 if device.is_mic_jack():
166 return device.get_microphone_insert()
167 else:
168 return None
Hsin-Yu Chao8d093f42012-11-05 18:43:22 +0800169
170 def check_loopback_dongle(self):
171 '''
172 Checks if loopback dongle is equipped correctly.
173 '''
174 # Check Mic Jack
175 mic_jack_status = self.get_mic_jack_status()
176 if mic_jack_status is None:
177 logging.warning('Found no Mic Jack control, skip check.')
178 elif not mic_jack_status:
179 logging.info('Mic jack is not plugged.')
180 return False
181 else:
182 logging.info('Mic jack is plugged.')
183
184 # Check Headphone Jack
185 hp_jack_status = self.get_hp_jack_status()
186 if hp_jack_status is None:
187 logging.warning('Found no Headphone Jack control, skip check.')
188 elif not hp_jack_status:
189 logging.info('Headphone jack is not plugged.')
190 return False
191 else:
192 logging.info('Headphone jack is plugged.')
193
194 return True
195
Hsinyu Chao4b8300e2011-11-15 13:07:32 -0800196 def set_mixer_controls(self, mixer_settings={}, card='0'):
197 '''
198 Sets all mixer controls listed in the mixer settings on card.
199 '''
200 logging.info('Setting mixer control values on %s' % card)
201 for item in mixer_settings:
202 logging.info('Setting %s to %s on card %s' %
203 (item['name'], item['value'], card))
204 cmd = 'amixer -c %s cset name=%s %s'
205 cmd = cmd % (card, item['name'], item['value'])
206 try:
207 utils.system(cmd)
208 except error.CmdError:
209 # A card is allowed not to support all the controls, so don't
210 # fail the test here if we get an error.
211 logging.info('amixer command failed: %s' % cmd)
Hsinyu Chaof80337a2012-04-07 18:02:29 +0800212
Hsinyu Chao2d64e1f2012-05-21 11:18:53 +0800213 def sox_stat_output(self, infile, channel):
Hsinyu Chaof80337a2012-04-07 18:02:29 +0800214 sox_mixer_cmd = self.get_sox_mixer_cmd(infile, channel)
215 stat_cmd = '%s -c 1 %s - -n stat 2>&1' % (self.sox_path,
216 self._sox_format)
217 sox_cmd = '%s | %s' % (sox_mixer_cmd, stat_cmd)
Hsinyu Chao2d64e1f2012-05-21 11:18:53 +0800218 return utils.system_output(sox_cmd, retain_output=True)
219
220 def get_audio_rms(self, sox_output):
Hsinyu Chaof80337a2012-04-07 18:02:29 +0800221 for rms_line in sox_output.split('\n'):
222 m = _SOX_RMS_AMPLITUDE_RE.match(rms_line)
223 if m is not None:
224 return float(m.group(1))
225
Hsinyu Chao2d64e1f2012-05-21 11:18:53 +0800226 def get_rough_freq(self, sox_output):
227 for rms_line in sox_output.split('\n'):
228 m = _SOX_ROUGH_FREQ_RE.match(rms_line)
229 if m is not None:
230 return int(m.group(1))
231
232
Hsinyu Chaof80337a2012-04-07 18:02:29 +0800233 def get_sox_mixer_cmd(self, infile, channel):
234 # Build up a pan value string for the sox command.
235 if channel == 0:
236 pan_values = '1'
237 else:
238 pan_values = '0'
239 for pan_index in range(1, self._num_channels):
240 if channel == pan_index:
241 pan_values = '%s%s' % (pan_values, ',1')
242 else:
243 pan_values = '%s%s' % (pan_values, ',0')
244
245 return '%s -c 2 %s %s -c 1 %s - mixer %s' % (self.sox_path,
246 self._sox_format, infile, self._sox_format, pan_values)
247
248 def noise_reduce_file(self, in_file, noise_file, out_file):
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +0800249 '''Runs the sox command to noise-reduce in_file using
Hsinyu Chaof80337a2012-04-07 18:02:29 +0800250 the noise profile from noise_file.
251
252 Args:
253 in_file: The file to noise reduce.
254 noise_file: The file containing the noise profile.
255 This can be created by recording silence.
256 out_file: The file contains the noise reduced sound.
257
258 Returns:
259 The name of the file containing the noise-reduced data.
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +0800260 '''
Hsinyu Chaof80337a2012-04-07 18:02:29 +0800261 prof_cmd = '%s -c 2 %s %s -n noiseprof' % (self.sox_path,
262 _SOX_FORMAT, noise_file)
263 reduce_cmd = ('%s -c 2 %s %s -c 2 %s %s noisered' %
264 (self.sox_path, _SOX_FORMAT, in_file, _SOX_FORMAT, out_file))
265 utils.system('%s | %s' % (prof_cmd, reduce_cmd))
266
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +0800267 def record_sample(self, tmpfile):
268 '''Records a sample from the default input device.
Hsinyu Chaof80337a2012-04-07 18:02:29 +0800269
270 Args:
271 duration: How long to record in seconds.
272 tmpfile: The file to record to.
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +0800273 '''
Dylan Reidbf9a5d42012-11-06 16:27:20 -0800274 cmd_rec = self._rec_cmd + ' %s' % tmpfile
275 logging.info('Command %s recording now' % cmd_rec)
Hsinyu Chaof80337a2012-04-07 18:02:29 +0800276 utils.system(cmd_rec)
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +0800277
Dylan Reid51f289c2012-11-06 17:16:24 -0800278 def loopback_test_channels(self, noise_file, loopback_callback):
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +0800279 '''Tests loopback on all channels.
280
281 Args:
282 noise_file: The file contains the pre-recorded noise.
283 loopback_callback: The callback to do the loopback for one channel.
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +0800284 '''
285 for channel in xrange(self._num_channels):
286 # Temp file for the final noise-reduced file.
287 with tempfile.NamedTemporaryFile(mode='w+t') as reduced_file:
288 # Temp file that records before noise reduction.
289 with tempfile.NamedTemporaryFile(mode='w+t') as tmpfile:
290 record_thread = RecordSampleThread(self, tmpfile.name)
291 record_thread.start()
292 loopback_callback(channel)
293 record_thread.join()
294
295 self.noise_reduce_file(tmpfile.name, noise_file.name,
296 reduced_file.name)
297
Hsinyu Chao2d64e1f2012-05-21 11:18:53 +0800298 sox_output = self.sox_stat_output(reduced_file.name, channel)
Dylan Reid51f289c2012-11-06 17:16:24 -0800299 self.check_recorded(sox_output)
300
301 def check_recorded(self, sox_output):
302 """Checks if the calculated RMS value is expected.
303
304 Args:
305 sox_output: The output from sox stat command.
306
307 Raises:
308 error.TestFail if the RMS amplitude of the recording isn't above
309 the threshold.
310 """
311 rms_val = self.get_audio_rms(sox_output)
312
313 # In case we don't get a valid RMS value.
314 if rms_val is None:
315 raise error.TestError(
316 'Failed to generate an audio RMS value from playback.')
317
318 logging.info('Got audio RMS value of %f. Minimum pass is %f.' %
319 (rms_val, self._sox_threshold))
320 if rms_val < self._sox_threshold:
321 raise error.TestError(
322 'Audio RMS value %f too low. Minimum pass is %f.' %
323 (rms_val, self._sox_threshold))