blob: 818679309a9264d37fb2548ded72f868604ecf6f [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
Hsinyu Chao10ef3b92012-07-02 22:42:30 +080018_DEFAULT_INPUT_DEVICE = 'hw:0,0'
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +080019_DEFAULT_RECORD_DURATION = 10
Hsinyu Chaof80337a2012-04-07 18:02:29 +080020_DEFAULT_SOX_FORMAT = '-t raw -b 16 -e signed -r 48000 -L'
21
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 '''
Hsinyu Chaof80337a2012-04-07 18:02:29 +080046 def __init__(self, test, sox_format=_DEFAULT_SOX_FORMAT,
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +080047 input_device=_DEFAULT_INPUT_DEVICE,
48 record_duration=_DEFAULT_RECORD_DURATION,
49 num_channels = _DEFAULT_NUM_CHANNELS):
Hsinyu Chao4b8300e2011-11-15 13:07:32 -080050 self._test = test
Hsinyu Chaof80337a2012-04-07 18:02:29 +080051 self._sox_format = sox_format
52 self._input_device = input_device
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +080053 self._record_duration = record_duration
54 self._num_channels = num_channels
Hsinyu Chao4b8300e2011-11-15 13:07:32 -080055
56 def setup_deps(self, deps):
57 '''
58 Sets up audio related dependencies.
59 '''
60 for dep in deps:
61 if dep == 'test_tones':
62 dep_dir = os.path.join(self._test.autodir, 'deps', dep)
63 self._test.job.install_pkg(dep, 'dep', dep_dir)
64 self.test_tones_path = os.path.join(dep_dir, 'src', dep)
65 elif dep == 'audioloop':
66 dep_dir = os.path.join(self._test.autodir, 'deps', dep)
67 self._test.job.install_pkg(dep, 'dep', dep_dir)
68 self.audioloop_path = os.path.join(dep_dir, 'src',
69 'looptest')
70 elif dep == 'sox':
71 dep_dir = os.path.join(self._test.autodir, 'deps', dep)
72 self._test.job.install_pkg(dep, 'dep', dep_dir)
73 self.sox_path = os.path.join(dep_dir, 'bin', dep)
74 self.sox_lib_path = os.path.join(dep_dir, 'lib')
75 if os.environ.has_key(LD_LIBRARY_PATH):
76 paths = os.environ[LD_LIBRARY_PATH].split(':')
77 if not self.sox_lib_path in paths:
78 paths.append(self.sox_lib_path)
79 os.environ[LD_LIBRARY_PATH] = ':'.join(paths)
80 else:
81 os.environ[LD_LIBRARY_PATH] = self.sox_lib_path
82
83 def cleanup_deps(self, deps):
84 '''
85 Cleans up environments which has been setup for dependencies.
86 '''
87 for dep in deps:
88 if dep == 'sox':
89 if (os.environ.has_key(LD_LIBRARY_PATH)
90 and hasattr(self, 'sox_lib_path')):
91 paths = filter(lambda x: x != self.sox_lib_path,
92 os.environ[LD_LIBRARY_PATH].split(':'))
93 os.environ[LD_LIBRARY_PATH] = ':'.join(paths)
94
Derek Basehoree973ce42012-07-10 23:38:32 -070095 def set_volume_levels(self, volume, capture):
96 '''
97 Sets the volume and capture gain through cras_test_client
98 '''
99 logging.info('Setting volume level to %d' % volume)
100 utils.system('/usr/bin/cras_test_client --volume %d' % volume)
101 logging.info('Setting capture gain to %d' % capture)
102 utils.system('/usr/bin/cras_test_client --capture_gain %d' % capture)
103 utils.system('/usr/bin/cras_test_client --dump_server_info')
104 utils.system('amixer -c 0 contents')
105
Hsin-Yu Chao8d093f42012-11-05 18:43:22 +0800106 def get_jack_status(self, jack_reg_exp):
107 '''
108 Gets the jack status.
109
110 Args:
111 jack_reg_exp: The regular expression to match jack control name.
112
113 Returns:
114 None if the control does not exist, return True if jack control
115 is detected plugged, return False otherwise.
116 '''
117 output = utils.system_output('amixer -c0 controls', retain_output=True)
118 numid = None
119 for line in output.split('\n'):
120 m = jack_reg_exp.match(line)
121 if m:
122 numid = m.group(1)
123 break
124 if numid is not None:
125 output = utils.system_output('amixer -c0 cget numid=%s' % numid)
126 for line in output.split('\n'):
127 if _JACK_VALUE_ON_RE.match(line):
128 return True
129 return False
130 else:
131 return None
132
133 def get_hp_jack_status(self):
134 return self.get_jack_status(_HP_JACK_CONTROL_RE)
135
136 def get_mic_jack_status(self):
137 return self.get_jack_status(_MIC_JACK_CONTROL_RE)
138
139 def check_loopback_dongle(self):
140 '''
141 Checks if loopback dongle is equipped correctly.
142 '''
143 # Check Mic Jack
144 mic_jack_status = self.get_mic_jack_status()
145 if mic_jack_status is None:
146 logging.warning('Found no Mic Jack control, skip check.')
147 elif not mic_jack_status:
148 logging.info('Mic jack is not plugged.')
149 return False
150 else:
151 logging.info('Mic jack is plugged.')
152
153 # Check Headphone Jack
154 hp_jack_status = self.get_hp_jack_status()
155 if hp_jack_status is None:
156 logging.warning('Found no Headphone Jack control, skip check.')
157 elif not hp_jack_status:
158 logging.info('Headphone jack is not plugged.')
159 return False
160 else:
161 logging.info('Headphone jack is plugged.')
162
163 return True
164
Hsinyu Chao4b8300e2011-11-15 13:07:32 -0800165 def set_mixer_controls(self, mixer_settings={}, card='0'):
166 '''
167 Sets all mixer controls listed in the mixer settings on card.
168 '''
169 logging.info('Setting mixer control values on %s' % card)
170 for item in mixer_settings:
171 logging.info('Setting %s to %s on card %s' %
172 (item['name'], item['value'], card))
173 cmd = 'amixer -c %s cset name=%s %s'
174 cmd = cmd % (card, item['name'], item['value'])
175 try:
176 utils.system(cmd)
177 except error.CmdError:
178 # A card is allowed not to support all the controls, so don't
179 # fail the test here if we get an error.
180 logging.info('amixer command failed: %s' % cmd)
Hsinyu Chaof80337a2012-04-07 18:02:29 +0800181
Hsinyu Chao2d64e1f2012-05-21 11:18:53 +0800182 def sox_stat_output(self, infile, channel):
Hsinyu Chaof80337a2012-04-07 18:02:29 +0800183 sox_mixer_cmd = self.get_sox_mixer_cmd(infile, channel)
184 stat_cmd = '%s -c 1 %s - -n stat 2>&1' % (self.sox_path,
185 self._sox_format)
186 sox_cmd = '%s | %s' % (sox_mixer_cmd, stat_cmd)
Hsinyu Chao2d64e1f2012-05-21 11:18:53 +0800187 return utils.system_output(sox_cmd, retain_output=True)
188
189 def get_audio_rms(self, sox_output):
Hsinyu Chaof80337a2012-04-07 18:02:29 +0800190 for rms_line in sox_output.split('\n'):
191 m = _SOX_RMS_AMPLITUDE_RE.match(rms_line)
192 if m is not None:
193 return float(m.group(1))
194
Hsinyu Chao2d64e1f2012-05-21 11:18:53 +0800195 def get_rough_freq(self, sox_output):
196 for rms_line in sox_output.split('\n'):
197 m = _SOX_ROUGH_FREQ_RE.match(rms_line)
198 if m is not None:
199 return int(m.group(1))
200
201
Hsinyu Chaof80337a2012-04-07 18:02:29 +0800202 def get_sox_mixer_cmd(self, infile, channel):
203 # Build up a pan value string for the sox command.
204 if channel == 0:
205 pan_values = '1'
206 else:
207 pan_values = '0'
208 for pan_index in range(1, self._num_channels):
209 if channel == pan_index:
210 pan_values = '%s%s' % (pan_values, ',1')
211 else:
212 pan_values = '%s%s' % (pan_values, ',0')
213
214 return '%s -c 2 %s %s -c 1 %s - mixer %s' % (self.sox_path,
215 self._sox_format, infile, self._sox_format, pan_values)
216
217 def noise_reduce_file(self, in_file, noise_file, out_file):
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +0800218 '''Runs the sox command to noise-reduce in_file using
Hsinyu Chaof80337a2012-04-07 18:02:29 +0800219 the noise profile from noise_file.
220
221 Args:
222 in_file: The file to noise reduce.
223 noise_file: The file containing the noise profile.
224 This can be created by recording silence.
225 out_file: The file contains the noise reduced sound.
226
227 Returns:
228 The name of the file containing the noise-reduced data.
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +0800229 '''
Hsinyu Chaof80337a2012-04-07 18:02:29 +0800230 prof_cmd = '%s -c 2 %s %s -n noiseprof' % (self.sox_path,
231 _SOX_FORMAT, noise_file)
232 reduce_cmd = ('%s -c 2 %s %s -c 2 %s %s noisered' %
233 (self.sox_path, _SOX_FORMAT, in_file, _SOX_FORMAT, out_file))
234 utils.system('%s | %s' % (prof_cmd, reduce_cmd))
235
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +0800236 def record_sample(self, tmpfile):
237 '''Records a sample from the default input device.
Hsinyu Chaof80337a2012-04-07 18:02:29 +0800238
239 Args:
240 duration: How long to record in seconds.
241 tmpfile: The file to record to.
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +0800242 '''
Hsinyu Chaof80337a2012-04-07 18:02:29 +0800243 cmd_rec = 'arecord -D %s -d %f -f dat %s' % (self._input_device,
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +0800244 self._record_duration, tmpfile)
245 logging.info('Command %s recording now (%fs)' % (cmd_rec,
246 self._record_duration))
Hsinyu Chaof80337a2012-04-07 18:02:29 +0800247 utils.system(cmd_rec)
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +0800248
249 def loopback_test_channels(self, noise_file, loopback_callback,
Hsinyu Chao2d64e1f2012-05-21 11:18:53 +0800250 check_recorded_callback):
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +0800251 '''Tests loopback on all channels.
252
253 Args:
254 noise_file: The file contains the pre-recorded noise.
255 loopback_callback: The callback to do the loopback for one channel.
256 check_recorded_callback: The callback function to check the
257 calculated RMS value.
258 '''
259 for channel in xrange(self._num_channels):
260 # Temp file for the final noise-reduced file.
261 with tempfile.NamedTemporaryFile(mode='w+t') as reduced_file:
262 # Temp file that records before noise reduction.
263 with tempfile.NamedTemporaryFile(mode='w+t') as tmpfile:
264 record_thread = RecordSampleThread(self, tmpfile.name)
265 record_thread.start()
266 loopback_callback(channel)
267 record_thread.join()
268
269 self.noise_reduce_file(tmpfile.name, noise_file.name,
270 reduced_file.name)
271
Hsinyu Chao2d64e1f2012-05-21 11:18:53 +0800272 sox_output = self.sox_stat_output(reduced_file.name, channel)
273 check_recorded_callback(sox_output)