blob: f75956fca79b5865aea24a92f2777fa16494f037 [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'
20
Hsin-Yu Chao8d093f42012-11-05 18:43:22 +080021_JACK_VALUE_ON_RE = re.compile('.*values=on')
22_HP_JACK_CONTROL_RE = re.compile('numid=(\d+).*Headphone\sJack')
23_MIC_JACK_CONTROL_RE = re.compile('numid=(\d+).*Mic\sJack')
24
Hsinyu Chaof80337a2012-04-07 18:02:29 +080025_SOX_RMS_AMPLITUDE_RE = re.compile('RMS\s+amplitude:\s+(.+)')
Hsinyu Chao2d64e1f2012-05-21 11:18:53 +080026_SOX_ROUGH_FREQ_RE = re.compile('Rough\s+frequency:\s+(.+)')
Hsinyu Chaof80337a2012-04-07 18:02:29 +080027_SOX_FORMAT = '-t raw -b 16 -e signed -r 48000 -L'
28
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +080029
30class RecordSampleThread(threading.Thread):
31 '''Wraps the execution of arecord in a thread.'''
32 def __init__(self, audio, recordfile):
33 threading.Thread.__init__(self)
34 self._audio = audio
35 self._recordfile = recordfile
36
37 def run(self):
38 self._audio.record_sample(self._recordfile)
39
40
Hsinyu Chao4b8300e2011-11-15 13:07:32 -080041class AudioHelper(object):
42 '''
43 A helper class contains audio related utility functions.
44 '''
Dylan Reidbf9a5d42012-11-06 16:27:20 -080045 def __init__(self, test,
46 sox_format = _DEFAULT_SOX_FORMAT,
47 record_command = _DEFAULT_REC_COMMAND,
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +080048 num_channels = _DEFAULT_NUM_CHANNELS):
Hsinyu Chao4b8300e2011-11-15 13:07:32 -080049 self._test = test
Hsinyu Chaof80337a2012-04-07 18:02:29 +080050 self._sox_format = sox_format
Dylan Reidbf9a5d42012-11-06 16:27:20 -080051 self._rec_cmd = record_command
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +080052 self._num_channels = num_channels
Hsinyu Chao4b8300e2011-11-15 13:07:32 -080053
54 def setup_deps(self, deps):
55 '''
56 Sets up audio related dependencies.
57 '''
58 for dep in deps:
59 if dep == 'test_tones':
60 dep_dir = os.path.join(self._test.autodir, 'deps', dep)
61 self._test.job.install_pkg(dep, 'dep', dep_dir)
62 self.test_tones_path = os.path.join(dep_dir, 'src', dep)
63 elif dep == 'audioloop':
64 dep_dir = os.path.join(self._test.autodir, 'deps', dep)
65 self._test.job.install_pkg(dep, 'dep', dep_dir)
66 self.audioloop_path = os.path.join(dep_dir, 'src',
67 'looptest')
68 elif dep == 'sox':
69 dep_dir = os.path.join(self._test.autodir, 'deps', dep)
70 self._test.job.install_pkg(dep, 'dep', dep_dir)
71 self.sox_path = os.path.join(dep_dir, 'bin', dep)
72 self.sox_lib_path = os.path.join(dep_dir, 'lib')
73 if os.environ.has_key(LD_LIBRARY_PATH):
74 paths = os.environ[LD_LIBRARY_PATH].split(':')
75 if not self.sox_lib_path in paths:
76 paths.append(self.sox_lib_path)
77 os.environ[LD_LIBRARY_PATH] = ':'.join(paths)
78 else:
79 os.environ[LD_LIBRARY_PATH] = self.sox_lib_path
80
81 def cleanup_deps(self, deps):
82 '''
83 Cleans up environments which has been setup for dependencies.
84 '''
85 for dep in deps:
86 if dep == 'sox':
87 if (os.environ.has_key(LD_LIBRARY_PATH)
88 and hasattr(self, 'sox_lib_path')):
89 paths = filter(lambda x: x != self.sox_lib_path,
90 os.environ[LD_LIBRARY_PATH].split(':'))
91 os.environ[LD_LIBRARY_PATH] = ':'.join(paths)
92
Derek Basehoree973ce42012-07-10 23:38:32 -070093 def set_volume_levels(self, volume, capture):
94 '''
95 Sets the volume and capture gain through cras_test_client
96 '''
97 logging.info('Setting volume level to %d' % volume)
98 utils.system('/usr/bin/cras_test_client --volume %d' % volume)
99 logging.info('Setting capture gain to %d' % capture)
100 utils.system('/usr/bin/cras_test_client --capture_gain %d' % capture)
101 utils.system('/usr/bin/cras_test_client --dump_server_info')
102 utils.system('amixer -c 0 contents')
103
Hsin-Yu Chao8d093f42012-11-05 18:43:22 +0800104 def get_jack_status(self, jack_reg_exp):
105 '''
106 Gets the jack status.
107
108 Args:
109 jack_reg_exp: The regular expression to match jack control name.
110
111 Returns:
112 None if the control does not exist, return True if jack control
113 is detected plugged, return False otherwise.
114 '''
115 output = utils.system_output('amixer -c0 controls', retain_output=True)
116 numid = None
117 for line in output.split('\n'):
118 m = jack_reg_exp.match(line)
119 if m:
120 numid = m.group(1)
121 break
122 if numid is not None:
123 output = utils.system_output('amixer -c0 cget numid=%s' % numid)
124 for line in output.split('\n'):
125 if _JACK_VALUE_ON_RE.match(line):
126 return True
127 return False
128 else:
129 return None
130
131 def get_hp_jack_status(self):
132 return self.get_jack_status(_HP_JACK_CONTROL_RE)
133
134 def get_mic_jack_status(self):
135 return self.get_jack_status(_MIC_JACK_CONTROL_RE)
136
137 def check_loopback_dongle(self):
138 '''
139 Checks if loopback dongle is equipped correctly.
140 '''
141 # Check Mic Jack
142 mic_jack_status = self.get_mic_jack_status()
143 if mic_jack_status is None:
144 logging.warning('Found no Mic Jack control, skip check.')
145 elif not mic_jack_status:
146 logging.info('Mic jack is not plugged.')
147 return False
148 else:
149 logging.info('Mic jack is plugged.')
150
151 # Check Headphone Jack
152 hp_jack_status = self.get_hp_jack_status()
153 if hp_jack_status is None:
154 logging.warning('Found no Headphone Jack control, skip check.')
155 elif not hp_jack_status:
156 logging.info('Headphone jack is not plugged.')
157 return False
158 else:
159 logging.info('Headphone jack is plugged.')
160
161 return True
162
Hsinyu Chao4b8300e2011-11-15 13:07:32 -0800163 def set_mixer_controls(self, mixer_settings={}, card='0'):
164 '''
165 Sets all mixer controls listed in the mixer settings on card.
166 '''
167 logging.info('Setting mixer control values on %s' % card)
168 for item in mixer_settings:
169 logging.info('Setting %s to %s on card %s' %
170 (item['name'], item['value'], card))
171 cmd = 'amixer -c %s cset name=%s %s'
172 cmd = cmd % (card, item['name'], item['value'])
173 try:
174 utils.system(cmd)
175 except error.CmdError:
176 # A card is allowed not to support all the controls, so don't
177 # fail the test here if we get an error.
178 logging.info('amixer command failed: %s' % cmd)
Hsinyu Chaof80337a2012-04-07 18:02:29 +0800179
Hsinyu Chao2d64e1f2012-05-21 11:18:53 +0800180 def sox_stat_output(self, infile, channel):
Hsinyu Chaof80337a2012-04-07 18:02:29 +0800181 sox_mixer_cmd = self.get_sox_mixer_cmd(infile, channel)
182 stat_cmd = '%s -c 1 %s - -n stat 2>&1' % (self.sox_path,
183 self._sox_format)
184 sox_cmd = '%s | %s' % (sox_mixer_cmd, stat_cmd)
Hsinyu Chao2d64e1f2012-05-21 11:18:53 +0800185 return utils.system_output(sox_cmd, retain_output=True)
186
187 def get_audio_rms(self, sox_output):
Hsinyu Chaof80337a2012-04-07 18:02:29 +0800188 for rms_line in sox_output.split('\n'):
189 m = _SOX_RMS_AMPLITUDE_RE.match(rms_line)
190 if m is not None:
191 return float(m.group(1))
192
Hsinyu Chao2d64e1f2012-05-21 11:18:53 +0800193 def get_rough_freq(self, sox_output):
194 for rms_line in sox_output.split('\n'):
195 m = _SOX_ROUGH_FREQ_RE.match(rms_line)
196 if m is not None:
197 return int(m.group(1))
198
199
Hsinyu Chaof80337a2012-04-07 18:02:29 +0800200 def get_sox_mixer_cmd(self, infile, channel):
201 # Build up a pan value string for the sox command.
202 if channel == 0:
203 pan_values = '1'
204 else:
205 pan_values = '0'
206 for pan_index in range(1, self._num_channels):
207 if channel == pan_index:
208 pan_values = '%s%s' % (pan_values, ',1')
209 else:
210 pan_values = '%s%s' % (pan_values, ',0')
211
212 return '%s -c 2 %s %s -c 1 %s - mixer %s' % (self.sox_path,
213 self._sox_format, infile, self._sox_format, pan_values)
214
215 def noise_reduce_file(self, in_file, noise_file, out_file):
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +0800216 '''Runs the sox command to noise-reduce in_file using
Hsinyu Chaof80337a2012-04-07 18:02:29 +0800217 the noise profile from noise_file.
218
219 Args:
220 in_file: The file to noise reduce.
221 noise_file: The file containing the noise profile.
222 This can be created by recording silence.
223 out_file: The file contains the noise reduced sound.
224
225 Returns:
226 The name of the file containing the noise-reduced data.
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +0800227 '''
Hsinyu Chaof80337a2012-04-07 18:02:29 +0800228 prof_cmd = '%s -c 2 %s %s -n noiseprof' % (self.sox_path,
229 _SOX_FORMAT, noise_file)
230 reduce_cmd = ('%s -c 2 %s %s -c 2 %s %s noisered' %
231 (self.sox_path, _SOX_FORMAT, in_file, _SOX_FORMAT, out_file))
232 utils.system('%s | %s' % (prof_cmd, reduce_cmd))
233
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +0800234 def record_sample(self, tmpfile):
235 '''Records a sample from the default input device.
Hsinyu Chaof80337a2012-04-07 18:02:29 +0800236
237 Args:
238 duration: How long to record in seconds.
239 tmpfile: The file to record to.
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +0800240 '''
Dylan Reidbf9a5d42012-11-06 16:27:20 -0800241 cmd_rec = self._rec_cmd + ' %s' % tmpfile
242 logging.info('Command %s recording now' % cmd_rec)
Hsinyu Chaof80337a2012-04-07 18:02:29 +0800243 utils.system(cmd_rec)
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +0800244
245 def loopback_test_channels(self, noise_file, loopback_callback,
Hsinyu Chao2d64e1f2012-05-21 11:18:53 +0800246 check_recorded_callback):
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +0800247 '''Tests loopback on all channels.
248
249 Args:
250 noise_file: The file contains the pre-recorded noise.
251 loopback_callback: The callback to do the loopback for one channel.
252 check_recorded_callback: The callback function to check the
253 calculated RMS value.
254 '''
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)
269 check_recorded_callback(sox_output)