blob: 7831a8e0010391948021c27d4864396bf54e09b7 [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 threading
Hsin-Yu Chaof272d8e2013-04-05 03:28:50 +080010import time
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'
Hsin-Yu Chao4be6d182013-04-19 14:07:56 +080023
24# Minimum RMS value to pass when checking recorded file.
25_DEFAULT_SOX_RMS_THRESHOLD = 0.08
Hsinyu Chaof80337a2012-04-07 18:02:29 +080026
Hsin-Yu Chao8d093f42012-11-05 18:43:22 +080027_JACK_VALUE_ON_RE = re.compile('.*values=on')
28_HP_JACK_CONTROL_RE = re.compile('numid=(\d+).*Headphone\sJack')
29_MIC_JACK_CONTROL_RE = re.compile('numid=(\d+).*Mic\sJack')
30
Hsinyu Chaof80337a2012-04-07 18:02:29 +080031_SOX_RMS_AMPLITUDE_RE = re.compile('RMS\s+amplitude:\s+(.+)')
Hsinyu Chao2d64e1f2012-05-21 11:18:53 +080032_SOX_ROUGH_FREQ_RE = re.compile('Rough\s+frequency:\s+(.+)')
Hsinyu Chaof80337a2012-04-07 18:02:29 +080033
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
Hsin-Yu Chaof6bbc6c2013-08-20 19:22:05 +080038# Tools from platform/audiotest
39AUDIOFUNTEST_PATH = 'audiofuntest'
40AUDIOLOOP_PATH = 'looptest'
41LOOPBACK_LATENCY_PATH = 'loopback_latency'
42SOX_PATH = 'sox'
43TEST_TONES_PATH = 'test_tones'
44
45
Hsin-Yu Chao30561af2013-09-06 14:03:56 +080046def set_mixer_controls(mixer_settings={}, card='0'):
47 '''
48 Sets all mixer controls listed in the mixer settings on card.
49
50 @param mixer_settings: Mixer settings to set.
51 @param card: Index of audio card to set mixer settings for.
52 '''
53 logging.info('Setting mixer control values on %s', card)
54 for item in mixer_settings:
55 logging.info('Setting %s to %s on card %s',
56 item['name'], item['value'], card)
57 cmd = 'amixer -c %s cset name=%s %s'
58 cmd = cmd % (card, item['name'], item['value'])
59 try:
60 utils.system(cmd)
61 except error.CmdError:
62 # A card is allowed not to support all the controls, so don't
63 # fail the test here if we get an error.
64 logging.info('amixer command failed: %s', cmd)
65
66def set_volume_levels(volume, capture):
67 '''
68 Sets the volume and capture gain through cras_test_client
69
70 @param volume: The playback volume to set.
71 @param capture: The capture gain to set.
72 '''
73 logging.info('Setting volume level to %d', volume)
74 utils.system('/usr/bin/cras_test_client --volume %d' % volume)
75 logging.info('Setting capture gain to %d', capture)
76 utils.system('/usr/bin/cras_test_client --capture_gain %d' % capture)
77 utils.system('/usr/bin/cras_test_client --dump_server_info')
78 utils.system('/usr/bin/cras_test_client --mute 0')
79 utils.system('amixer -c 0 contents')
80
81def loopback_latency_check(**args):
82 '''
83 Checks loopback latency.
84
85 @param args: additional arguments for loopback_latency.
86
87 @return A tuple containing measured and reported latency in uS.
88 Return None if no audio detected.
89 '''
90 noise_threshold = str(args['n']) if args.has_key('n') else '400'
91
92 cmd = '%s -n %s' % (LOOPBACK_LATENCY_PATH, noise_threshold)
93
94 output = utils.system_output(cmd, retain_output=True)
95
96 # Sleep for a short while to make sure device is not busy anymore
97 # after called loopback_latency.
98 time.sleep(.1)
99
100 measured_latency = None
101 reported_latency = None
102 for line in output.split('\n'):
103 match = re.search(_MEASURED_LATENCY_RE, line, re.I)
104 if match:
105 measured_latency = int(match.group(1))
106 continue
107 match = re.search(_REPORTED_LATENCY_RE, line, re.I)
108 if match:
109 reported_latency = int(match.group(1))
110 continue
111 if re.search(_AUDIO_NOT_FOUND_RE, line, re.I):
112 return None
113 if measured_latency and reported_latency:
114 return (measured_latency, reported_latency)
115 else:
116 # Should not reach here, just in case.
117 return None
118
119def get_mixer_jack_status(jack_reg_exp):
120 '''
121 Gets the mixer jack status.
122
123 @param jack_reg_exp: The regular expression to match jack control name.
124
125 @return 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
135
136 # Proceed only when matched numid is not empty.
137 if numid:
138 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
146def get_hp_jack_status():
147 '''Gets the status of headphone jack'''
148 status = get_mixer_jack_status(_HP_JACK_CONTROL_RE)
149 if status is not None:
150 return status
151
152 # When headphone jack is not found in amixer, lookup input devices
153 # instead.
154 #
155 # TODO(hychao): Check hp/mic jack status dynamically from evdev. And
156 # possibly replace the existing check using amixer.
157 for evdev in glob('/dev/input/event*'):
158 device = InputDevice(evdev)
159 if device.is_hp_jack():
160 return device.get_headphone_insert()
161 else:
162 return None
163
164def get_mic_jack_status():
165 '''Gets the status of mic jack'''
166 status = get_mixer_jack_status(_MIC_JACK_CONTROL_RE)
167 if status is not None:
168 return status
169
170 # When mic jack is not found in amixer, lookup input devices instead.
171 for evdev in glob('/dev/input/event*'):
172 device = InputDevice(evdev)
173 if device.is_mic_jack():
174 return device.get_microphone_insert()
175 else:
176 return None
177
178def check_loopback_dongle():
179 '''
180 Checks if loopback dongle is equipped correctly.
181 '''
182 # Check Mic Jack
183 mic_jack_status = get_mic_jack_status()
184 if mic_jack_status is None:
185 logging.warning('Found no Mic Jack control, skip check.')
186 elif not mic_jack_status:
187 logging.info('Mic jack is not plugged.')
188 return False
189 else:
190 logging.info('Mic jack is plugged.')
191
192 # Check Headphone Jack
193 hp_jack_status = get_hp_jack_status()
194 if hp_jack_status is None:
195 logging.warning('Found no Headphone Jack control, skip check.')
196 elif not hp_jack_status:
197 logging.info('Headphone jack is not plugged.')
198 return False
199 else:
200 logging.info('Headphone jack is plugged.')
201
202 # Use latency check to test if audio can be captured through dongle.
203 # We only want to know the basic function of dongle, so no need to
204 # assert the latency accuracy here.
205 latency = loopback_latency_check(n=4000)
206 if latency:
207 logging.info('Got latency measured %d, reported %d',
208 latency[0], latency[1])
209 else:
210 logging.warning('Latency check fail.')
211 return False
212
213 return True
214
215# Functions to test audio palyback.
216def play_sound(duration_seconds=None, audio_file_path=None):
217 '''
218 Plays a sound file found at |audio_file_path| for |duration_seconds|.
219
220 If |audio_file_path|=None, plays a default audio file.
221 If |duration_seconds|=None, plays audio file in its entirety.
222
223 @param duration_seconds: Duration to play sound.
224 @param audio_file_path: Path to the audio file.
225 '''
226 if not audio_file_path:
227 audio_file_path = '/usr/local/autotest/cros/audio/sine440.wav'
228 duration_arg = ('-d %d' % duration_seconds) if duration_seconds else ''
229 utils.system('aplay %s %s' % (duration_arg, audio_file_path))
230
231def get_play_sine_args(channel, odev='default', freq=1000, duration=10,
232 sample_size=16):
233 '''Gets the command args to generate a sine wav to play to odev.
234
235 @param channel: 0 for left, 1 for right; otherwize, mono.
236 @param odev: alsa output device.
237 @param freq: frequency of the generated sine tone.
238 @param duration: duration of the generated sine tone.
239 @param sample_size: output audio sample size. Default to 16.
240 '''
241 cmdargs = [SOX_PATH, '-b', str(sample_size), '-n', '-t', 'alsa',
242 odev, 'synth', str(duration)]
243 if channel == 0:
244 cmdargs += ['sine', str(freq), 'sine', '0']
245 elif channel == 1:
246 cmdargs += ['sine', '0', 'sine', str(freq)]
247 else:
248 cmdargs += ['sine', str(freq)]
249
250 return cmdargs
251
252def play_sine(channel, odev='default', freq=1000, duration=10,
253 sample_size=16):
254 '''Generates a sine wave and plays to odev.
255
256 @param channel: 0 for left, 1 for right; otherwize, mono.
257 @param odev: alsa output device.
258 @param freq: frequency of the generated sine tone.
259 @param duration: duration of the generated sine tone.
260 @param sample_size: output audio sample size. Default to 16.
261 '''
262 cmdargs = get_play_sine_args(channel, odev, freq, duration, sample_size)
263 utils.system(' '.join(cmdargs))
264
Hsin-Yu Chao22bebdc2013-09-06 16:32:51 +0800265# Functions to compose customized sox command, execute it and process the
266# output of sox command.
267def get_sox_mixer_cmd(infile, channel,
268 num_channels=_DEFAULT_NUM_CHANNELS,
269 sox_format=_DEFAULT_SOX_FORMAT):
270 '''Gets sox mixer command to reduce channel.
271
272 @param infile: Input file name.
273 @param channel: The selected channel to take effect.
274 @param num_channels: The number of total channels to test.
275 @param sox_format: Format to generate sox command.
276 '''
277 # Build up a pan value string for the sox command.
278 if channel == 0:
279 pan_values = '1'
280 else:
281 pan_values = '0'
282 for pan_index in range(1, num_channels):
283 if channel == pan_index:
284 pan_values = '%s%s' % (pan_values, ',1')
285 else:
286 pan_values = '%s%s' % (pan_values, ',0')
287
288 return '%s -c 2 %s %s -c 1 %s - mixer %s' % (SOX_PATH,
289 sox_format, infile, sox_format, pan_values)
290
291def sox_stat_output(infile, channel,
292 num_channels=_DEFAULT_NUM_CHANNELS,
293 sox_format=_DEFAULT_SOX_FORMAT):
294 '''Executes sox stat command.
295
296 @param infile: Input file name.
297 @param channel: The selected channel.
298 @param num_channels: The number of total channels to test.
299 @param sox_format: Format to generate sox command.
300
301 @return The output of sox stat command
302 '''
303 sox_mixer_cmd = get_sox_mixer_cmd(infile, channel,
304 num_channels, sox_format)
305 stat_cmd = '%s -c 1 %s - -n stat 2>&1' % (SOX_PATH, sox_format)
306 sox_cmd = '%s | %s' % (sox_mixer_cmd, stat_cmd)
307 return utils.system_output(sox_cmd, retain_output=True)
308
309def get_audio_rms(sox_output):
310 '''Gets the audio RMS value from sox stat output
311
312 @param sox_output: Output of sox stat command.
313
314 @return The RMS value parsed from sox stat output.
315 '''
316 for rms_line in sox_output.split('\n'):
317 m = _SOX_RMS_AMPLITUDE_RE.match(rms_line)
318 if m is not None:
319 return float(m.group(1))
320
321def get_rough_freq(sox_output):
322 '''Gets the rough audio frequency from sox stat output
323
324 @param sox_output: Output of sox stat command.
325
326 @return The rough frequency value parsed from sox stat output.
327 '''
328 for rms_line in sox_output.split('\n'):
329 m = _SOX_ROUGH_FREQ_RE.match(rms_line)
330 if m is not None:
331 return int(m.group(1))
332
333def check_audio_rms(sox_output, sox_threshold=_DEFAULT_SOX_RMS_THRESHOLD):
334 """Checks if the calculated RMS value is expected.
335
336 @param sox_output: The output from sox stat command.
337 @param sox_threshold: The threshold to test RMS value against.
338
339 @raises error.TestError if RMS amplitude can't be parsed.
340 @raises error.TestFail if the RMS amplitude of the recording isn't above
341 the threshold.
342 """
343 rms_val = get_audio_rms(sox_output)
344
345 # In case we don't get a valid RMS value.
346 if rms_val is None:
347 raise error.TestError(
348 'Failed to generate an audio RMS value from playback.')
349
350 logging.info('Got audio RMS value of %f. Minimum pass is %f.',
351 rms_val, sox_threshold)
352 if rms_val < sox_threshold:
353 raise error.TestFail(
354 'Audio RMS value %f too low. Minimum pass is %f.' %
355 (rms_val, sox_threshold))
356
357def noise_reduce_file(in_file, noise_file, out_file,
358 sox_format=_DEFAULT_SOX_FORMAT):
359 '''Runs the sox command to noise-reduce in_file using
360 the noise profile from noise_file.
361
362 @param in_file: The file to noise reduce.
363 @param noise_file: The file containing the noise profile.
364 This can be created by recording silence.
365 @param out_file: The file contains the noise reduced sound.
366 @param sox_format: The sox format to generate sox command.
367 '''
368 prof_cmd = '%s -c 2 %s %s -n noiseprof' % (SOX_PATH,
369 sox_format, noise_file)
370 reduce_cmd = ('%s -c 2 %s %s -c 2 %s %s noisered' %
371 (SOX_PATH, sox_format, in_file, sox_format, out_file))
372 utils.system('%s | %s' % (prof_cmd, reduce_cmd))
373
Hsin-Yu Chao30561af2013-09-06 14:03:56 +0800374
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +0800375class RecordSampleThread(threading.Thread):
376 '''Wraps the execution of arecord in a thread.'''
377 def __init__(self, audio, recordfile):
378 threading.Thread.__init__(self)
379 self._audio = audio
380 self._recordfile = recordfile
381
382 def run(self):
383 self._audio.record_sample(self._recordfile)
384
385
Adrian Li689d3ff2013-07-15 15:11:06 -0700386class RecordMixThread(threading.Thread):
387 '''
388 Wraps the execution of recording the mixed loopback stream in
389 cras_test_client in a thread.
390 '''
391 def __init__(self, audio, recordfile):
392 threading.Thread.__init__(self)
393 self._audio = audio
394 self._recordfile = recordfile
395
396 def run(self):
397 self._audio.record_mix(self._recordfile)
398
399
Hsinyu Chao4b8300e2011-11-15 13:07:32 -0800400class AudioHelper(object):
401 '''
402 A helper class contains audio related utility functions.
403 '''
Dylan Reidbf9a5d42012-11-06 16:27:20 -0800404 def __init__(self, test,
Dylan Reidbf9a5d42012-11-06 16:27:20 -0800405 record_command = _DEFAULT_REC_COMMAND,
Adrian Li689d3ff2013-07-15 15:11:06 -0700406 num_channels = _DEFAULT_NUM_CHANNELS,
407 mix_command = None):
Hsinyu Chao4b8300e2011-11-15 13:07:32 -0800408 self._test = test
Dylan Reidbf9a5d42012-11-06 16:27:20 -0800409 self._rec_cmd = record_command
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +0800410 self._num_channels = num_channels
Adrian Li689d3ff2013-07-15 15:11:06 -0700411 self._mix_cmd = mix_command
Hsinyu Chao4b8300e2011-11-15 13:07:32 -0800412
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +0800413 def record_sample(self, tmpfile):
414 '''Records a sample from the default input device.
Hsinyu Chaof80337a2012-04-07 18:02:29 +0800415
Hsin-Yu Chao2ecbfe62013-04-06 05:49:27 +0800416 @param tmpfile: The file to record to.
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +0800417 '''
Dylan Reidbf9a5d42012-11-06 16:27:20 -0800418 cmd_rec = self._rec_cmd + ' %s' % tmpfile
Hsin-Yu Chao2ecbfe62013-04-06 05:49:27 +0800419 logging.info('Command %s recording now', cmd_rec)
Hsinyu Chaof80337a2012-04-07 18:02:29 +0800420 utils.system(cmd_rec)
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +0800421
Adrian Li689d3ff2013-07-15 15:11:06 -0700422 def record_mix(self, tmpfile):
423 '''Records a sample from the mixed loopback stream in cras_test_client.
424
425 @param tmpfile: The file to record to.
426 '''
427 cmd_mix = self._mix_cmd + ' %s' % tmpfile
428 logging.info('Command %s recording now', cmd_mix)
429 utils.system(cmd_mix)
430
Rohit Makasana8ef9e292013-05-02 19:02:15 -0700431 def loopback_test_channels(self, noise_file_name,
432 loopback_callback=None,
Hsin-Yu Chao22bebdc2013-09-06 16:32:51 +0800433 check_recorded_callback=check_audio_rms,
Rohit Makasana8ef9e292013-05-02 19:02:15 -0700434 preserve_test_file=True):
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +0800435 '''Tests loopback on all channels.
436
Hsin-Yu Chao2ecbfe62013-04-06 05:49:27 +0800437 @param noise_file_name: Name of the file contains pre-recorded noise.
438 @param loopback_callback: The callback to do the loopback for
439 one channel.
440 @param check_recorded_callback: The callback to check recorded file.
Rohit Makasana8ef9e292013-05-02 19:02:15 -0700441 @param preserve_test_file: Retain the recorded files for future debugging.
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +0800442 '''
443 for channel in xrange(self._num_channels):
Hsin-Yu Chao78c44b22013-04-06 05:33:58 +0800444 reduced_file_name = self.create_wav_file("reduced-%d" % channel)
445 record_file_name = self.create_wav_file("record-%d" % channel)
Hsin-Yu Chao78c44b22013-04-06 05:33:58 +0800446 record_thread = RecordSampleThread(self, record_file_name)
447 record_thread.start()
Adrian Li689d3ff2013-07-15 15:11:06 -0700448
449 if self._mix_cmd != None:
450 mix_file_name = self.create_wav_file("mix-%d" % channel)
451 mix_thread = RecordMixThread(self, mix_file_name)
452 mix_thread.start()
453
Hsin-Yu Chao78c44b22013-04-06 05:33:58 +0800454 if loopback_callback:
455 loopback_callback(channel)
Adrian Li689d3ff2013-07-15 15:11:06 -0700456
457 if self._mix_cmd != None:
458 mix_thread.join()
Hsin-Yu Chao22bebdc2013-09-06 16:32:51 +0800459 sox_output_mix = sox_stat_output(mix_file_name, channel)
460 rms_val_mix = get_audio_rms(sox_output_mix)
Adrian Li689d3ff2013-07-15 15:11:06 -0700461 logging.info('Got mixed audio RMS value of %f.', rms_val_mix)
462
Hsin-Yu Chao78c44b22013-04-06 05:33:58 +0800463 record_thread.join()
Hsin-Yu Chao22bebdc2013-09-06 16:32:51 +0800464 sox_output_record = sox_stat_output(record_file_name, channel)
465 rms_val_record = get_audio_rms(sox_output_record)
Adrian Li689d3ff2013-07-15 15:11:06 -0700466 logging.info('Got recorded audio RMS value of %f.', rms_val_record)
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +0800467
Hsin-Yu Chao22bebdc2013-09-06 16:32:51 +0800468 noise_reduce_file(record_file_name, noise_file_name,
469 reduced_file_name)
Hsin-Yu Chao11041d32012-11-13 15:46:13 +0800470
Hsin-Yu Chao22bebdc2013-09-06 16:32:51 +0800471 sox_output_reduced = sox_stat_output(reduced_file_name,
472 channel)
Hsin-Yu Chao78c44b22013-04-06 05:33:58 +0800473
Rohit Makasana8ef9e292013-05-02 19:02:15 -0700474 if not preserve_test_file:
475 os.unlink(reduced_file_name)
476 os.unlink(record_file_name)
Adrian Li689d3ff2013-07-15 15:11:06 -0700477 if self._mix_cmd != None:
478 os.unlink(mix_file_name)
Dylan Reid51f289c2012-11-06 17:16:24 -0800479
Hsin-Yu Chao22bebdc2013-09-06 16:32:51 +0800480 check_recorded_callback(sox_output_reduced)
Hsin-Yu Chao95ee3512012-11-05 20:43:10 +0800481
Hsin-Yu Chao78c44b22013-04-06 05:33:58 +0800482 def create_wav_file(self, prefix=""):
483 '''Creates a unique name for wav file.
484
Hsin-Yu Chao2ecbfe62013-04-06 05:49:27 +0800485 The created file name will be preserved in autotest result directory
486 for future analysis.
Hsin-Yu Chao78c44b22013-04-06 05:33:58 +0800487
Hsin-Yu Chao2ecbfe62013-04-06 05:49:27 +0800488 @param prefix: specified file name prefix.
Hsin-Yu Chao78c44b22013-04-06 05:33:58 +0800489 '''
490 filename = "%s-%s.wav" % (prefix, time.time())
491 return os.path.join(self._test.resultsdir, filename)