blob: 42099bba27c211aea2bf175d08cf05167bd5b612 [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
Owen Linca365f82013-11-08 16:52:28 +08008import pipes
Hsinyu Chaof80337a2012-04-07 18:02:29 +08009import re
Owen Lin62492b02013-11-01 19:00:24 +080010import shlex
11import subprocess
Owen Lindae7a0d2013-12-05 13:34:06 +080012import tempfile
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +080013import threading
Hsin-Yu Chaof272d8e2013-04-05 03:28:50 +080014import time
Hsinyu Chao4b8300e2011-11-15 13:07:32 -080015
Hsin-Yu Chao084e9da2012-11-07 15:56:26 +080016from glob import glob
Hsinyu Chao4b8300e2011-11-15 13:07:32 -080017from autotest_lib.client.bin import utils
Hsin-Yu Chao084e9da2012-11-07 15:56:26 +080018from autotest_lib.client.bin.input.input_device import *
Hsinyu Chao4b8300e2011-11-15 13:07:32 -080019from autotest_lib.client.common_lib import error
Owen Lin7ab45a22013-11-19 17:26:33 +080020from autotest_lib.client.cros.audio import cmd_utils
21from autotest_lib.client.cros.audio import sox_utils
Hsinyu Chao4b8300e2011-11-15 13:07:32 -080022
23LD_LIBRARY_PATH = 'LD_LIBRARY_PATH'
24
Hsinyu Chaof80337a2012-04-07 18:02:29 +080025_DEFAULT_NUM_CHANNELS = 2
Dylan Reidbf9a5d42012-11-06 16:27:20 -080026_DEFAULT_REC_COMMAND = 'arecord -D hw:0,0 -d 10 -f dat'
Hsinyu Chaof80337a2012-04-07 18:02:29 +080027_DEFAULT_SOX_FORMAT = '-t raw -b 16 -e signed -r 48000 -L'
Hsin-Yu Chao4be6d182013-04-19 14:07:56 +080028
29# Minimum RMS value to pass when checking recorded file.
30_DEFAULT_SOX_RMS_THRESHOLD = 0.08
Hsinyu Chaof80337a2012-04-07 18:02:29 +080031
Hsin-Yu Chao8d093f42012-11-05 18:43:22 +080032_JACK_VALUE_ON_RE = re.compile('.*values=on')
33_HP_JACK_CONTROL_RE = re.compile('numid=(\d+).*Headphone\sJack')
34_MIC_JACK_CONTROL_RE = re.compile('numid=(\d+).*Mic\sJack')
35
Hsinyu Chaof80337a2012-04-07 18:02:29 +080036_SOX_RMS_AMPLITUDE_RE = re.compile('RMS\s+amplitude:\s+(.+)')
Hsinyu Chao2d64e1f2012-05-21 11:18:53 +080037_SOX_ROUGH_FREQ_RE = re.compile('Rough\s+frequency:\s+(.+)')
Hsinyu Chaof80337a2012-04-07 18:02:29 +080038
Hsin-Yu Chao95ee3512012-11-05 20:43:10 +080039_AUDIO_NOT_FOUND_RE = r'Audio\snot\sdetected'
40_MEASURED_LATENCY_RE = r'Measured\sLatency:\s(\d+)\suS'
41_REPORTED_LATENCY_RE = r'Reported\sLatency:\s(\d+)\suS'
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +080042
Hsin-Yu Chaof6bbc6c2013-08-20 19:22:05 +080043# Tools from platform/audiotest
44AUDIOFUNTEST_PATH = 'audiofuntest'
45AUDIOLOOP_PATH = 'looptest'
46LOOPBACK_LATENCY_PATH = 'loopback_latency'
47SOX_PATH = 'sox'
48TEST_TONES_PATH = 'test_tones'
49
50
Hsin-Yu Chao30561af2013-09-06 14:03:56 +080051def set_mixer_controls(mixer_settings={}, card='0'):
52 '''
53 Sets all mixer controls listed in the mixer settings on card.
54
55 @param mixer_settings: Mixer settings to set.
56 @param card: Index of audio card to set mixer settings for.
57 '''
58 logging.info('Setting mixer control values on %s', card)
59 for item in mixer_settings:
60 logging.info('Setting %s to %s on card %s',
61 item['name'], item['value'], card)
62 cmd = 'amixer -c %s cset name=%s %s'
63 cmd = cmd % (card, item['name'], item['value'])
64 try:
65 utils.system(cmd)
66 except error.CmdError:
67 # A card is allowed not to support all the controls, so don't
68 # fail the test here if we get an error.
69 logging.info('amixer command failed: %s', cmd)
70
71def set_volume_levels(volume, capture):
72 '''
73 Sets the volume and capture gain through cras_test_client
74
75 @param volume: The playback volume to set.
76 @param capture: The capture gain to set.
77 '''
78 logging.info('Setting volume level to %d', volume)
79 utils.system('/usr/bin/cras_test_client --volume %d' % volume)
80 logging.info('Setting capture gain to %d', capture)
81 utils.system('/usr/bin/cras_test_client --capture_gain %d' % capture)
82 utils.system('/usr/bin/cras_test_client --dump_server_info')
83 utils.system('/usr/bin/cras_test_client --mute 0')
84 utils.system('amixer -c 0 contents')
85
86def loopback_latency_check(**args):
87 '''
88 Checks loopback latency.
89
90 @param args: additional arguments for loopback_latency.
91
92 @return A tuple containing measured and reported latency in uS.
93 Return None if no audio detected.
94 '''
95 noise_threshold = str(args['n']) if args.has_key('n') else '400'
96
97 cmd = '%s -n %s' % (LOOPBACK_LATENCY_PATH, noise_threshold)
98
99 output = utils.system_output(cmd, retain_output=True)
100
101 # Sleep for a short while to make sure device is not busy anymore
102 # after called loopback_latency.
103 time.sleep(.1)
104
105 measured_latency = None
106 reported_latency = None
107 for line in output.split('\n'):
108 match = re.search(_MEASURED_LATENCY_RE, line, re.I)
109 if match:
110 measured_latency = int(match.group(1))
111 continue
112 match = re.search(_REPORTED_LATENCY_RE, line, re.I)
113 if match:
114 reported_latency = int(match.group(1))
115 continue
116 if re.search(_AUDIO_NOT_FOUND_RE, line, re.I):
117 return None
118 if measured_latency and reported_latency:
119 return (measured_latency, reported_latency)
120 else:
121 # Should not reach here, just in case.
122 return None
123
124def get_mixer_jack_status(jack_reg_exp):
125 '''
126 Gets the mixer jack status.
127
128 @param jack_reg_exp: The regular expression to match jack control name.
129
130 @return None if the control does not exist, return True if jack control
131 is detected plugged, return False otherwise.
132 '''
133 output = utils.system_output('amixer -c0 controls', retain_output=True)
134 numid = None
135 for line in output.split('\n'):
136 m = jack_reg_exp.match(line)
137 if m:
138 numid = m.group(1)
139 break
140
141 # Proceed only when matched numid is not empty.
142 if numid:
143 output = utils.system_output('amixer -c0 cget numid=%s' % numid)
144 for line in output.split('\n'):
145 if _JACK_VALUE_ON_RE.match(line):
146 return True
147 return False
148 else:
149 return None
150
151def get_hp_jack_status():
152 '''Gets the status of headphone jack'''
153 status = get_mixer_jack_status(_HP_JACK_CONTROL_RE)
154 if status is not None:
155 return status
156
157 # When headphone jack is not found in amixer, lookup input devices
158 # instead.
159 #
160 # TODO(hychao): Check hp/mic jack status dynamically from evdev. And
161 # possibly replace the existing check using amixer.
162 for evdev in glob('/dev/input/event*'):
163 device = InputDevice(evdev)
164 if device.is_hp_jack():
165 return device.get_headphone_insert()
166 else:
167 return None
168
169def get_mic_jack_status():
170 '''Gets the status of mic jack'''
171 status = get_mixer_jack_status(_MIC_JACK_CONTROL_RE)
172 if status is not None:
173 return status
174
175 # When mic jack is not found in amixer, lookup input devices instead.
176 for evdev in glob('/dev/input/event*'):
177 device = InputDevice(evdev)
178 if device.is_mic_jack():
179 return device.get_microphone_insert()
180 else:
181 return None
182
183def check_loopback_dongle():
184 '''
185 Checks if loopback dongle is equipped correctly.
186 '''
187 # Check Mic Jack
188 mic_jack_status = get_mic_jack_status()
189 if mic_jack_status is None:
190 logging.warning('Found no Mic Jack control, skip check.')
191 elif not mic_jack_status:
192 logging.info('Mic jack is not plugged.')
193 return False
194 else:
195 logging.info('Mic jack is plugged.')
196
197 # Check Headphone Jack
198 hp_jack_status = get_hp_jack_status()
199 if hp_jack_status is None:
200 logging.warning('Found no Headphone Jack control, skip check.')
201 elif not hp_jack_status:
202 logging.info('Headphone jack is not plugged.')
203 return False
204 else:
205 logging.info('Headphone jack is plugged.')
206
207 # Use latency check to test if audio can be captured through dongle.
208 # We only want to know the basic function of dongle, so no need to
209 # assert the latency accuracy here.
210 latency = loopback_latency_check(n=4000)
211 if latency:
212 logging.info('Got latency measured %d, reported %d',
213 latency[0], latency[1])
214 else:
215 logging.warning('Latency check fail.')
216 return False
217
218 return True
219
220# Functions to test audio palyback.
221def play_sound(duration_seconds=None, audio_file_path=None):
222 '''
223 Plays a sound file found at |audio_file_path| for |duration_seconds|.
224
225 If |audio_file_path|=None, plays a default audio file.
226 If |duration_seconds|=None, plays audio file in its entirety.
227
228 @param duration_seconds: Duration to play sound.
229 @param audio_file_path: Path to the audio file.
230 '''
231 if not audio_file_path:
232 audio_file_path = '/usr/local/autotest/cros/audio/sine440.wav'
233 duration_arg = ('-d %d' % duration_seconds) if duration_seconds else ''
234 utils.system('aplay %s %s' % (duration_arg, audio_file_path))
235
236def get_play_sine_args(channel, odev='default', freq=1000, duration=10,
237 sample_size=16):
238 '''Gets the command args to generate a sine wav to play to odev.
239
240 @param channel: 0 for left, 1 for right; otherwize, mono.
241 @param odev: alsa output device.
242 @param freq: frequency of the generated sine tone.
243 @param duration: duration of the generated sine tone.
244 @param sample_size: output audio sample size. Default to 16.
245 '''
246 cmdargs = [SOX_PATH, '-b', str(sample_size), '-n', '-t', 'alsa',
247 odev, 'synth', str(duration)]
248 if channel == 0:
249 cmdargs += ['sine', str(freq), 'sine', '0']
250 elif channel == 1:
251 cmdargs += ['sine', '0', 'sine', str(freq)]
252 else:
253 cmdargs += ['sine', str(freq)]
254
255 return cmdargs
256
257def play_sine(channel, odev='default', freq=1000, duration=10,
258 sample_size=16):
259 '''Generates a sine wave and plays to odev.
260
261 @param channel: 0 for left, 1 for right; otherwize, mono.
262 @param odev: alsa output device.
263 @param freq: frequency of the generated sine tone.
264 @param duration: duration of the generated sine tone.
265 @param sample_size: output audio sample size. Default to 16.
266 '''
267 cmdargs = get_play_sine_args(channel, odev, freq, duration, sample_size)
268 utils.system(' '.join(cmdargs))
269
Hsin-Yu Chao22bebdc2013-09-06 16:32:51 +0800270# Functions to compose customized sox command, execute it and process the
271# output of sox command.
272def get_sox_mixer_cmd(infile, channel,
273 num_channels=_DEFAULT_NUM_CHANNELS,
274 sox_format=_DEFAULT_SOX_FORMAT):
275 '''Gets sox mixer command to reduce channel.
276
277 @param infile: Input file name.
278 @param channel: The selected channel to take effect.
279 @param num_channels: The number of total channels to test.
280 @param sox_format: Format to generate sox command.
281 '''
282 # Build up a pan value string for the sox command.
283 if channel == 0:
284 pan_values = '1'
285 else:
286 pan_values = '0'
287 for pan_index in range(1, num_channels):
288 if channel == pan_index:
289 pan_values = '%s%s' % (pan_values, ',1')
290 else:
291 pan_values = '%s%s' % (pan_values, ',0')
292
293 return '%s -c 2 %s %s -c 1 %s - mixer %s' % (SOX_PATH,
294 sox_format, infile, sox_format, pan_values)
295
296def sox_stat_output(infile, channel,
297 num_channels=_DEFAULT_NUM_CHANNELS,
298 sox_format=_DEFAULT_SOX_FORMAT):
299 '''Executes sox stat command.
300
301 @param infile: Input file name.
302 @param channel: The selected channel.
303 @param num_channels: The number of total channels to test.
304 @param sox_format: Format to generate sox command.
305
306 @return The output of sox stat command
307 '''
308 sox_mixer_cmd = get_sox_mixer_cmd(infile, channel,
309 num_channels, sox_format)
310 stat_cmd = '%s -c 1 %s - -n stat 2>&1' % (SOX_PATH, sox_format)
311 sox_cmd = '%s | %s' % (sox_mixer_cmd, stat_cmd)
312 return utils.system_output(sox_cmd, retain_output=True)
313
314def get_audio_rms(sox_output):
315 '''Gets the audio RMS value from sox stat output
316
317 @param sox_output: Output of sox stat command.
318
319 @return The RMS value parsed from sox stat output.
320 '''
321 for rms_line in sox_output.split('\n'):
322 m = _SOX_RMS_AMPLITUDE_RE.match(rms_line)
323 if m is not None:
324 return float(m.group(1))
325
326def get_rough_freq(sox_output):
327 '''Gets the rough audio frequency from sox stat output
328
329 @param sox_output: Output of sox stat command.
330
331 @return The rough frequency value parsed from sox stat output.
332 '''
333 for rms_line in sox_output.split('\n'):
334 m = _SOX_ROUGH_FREQ_RE.match(rms_line)
335 if m is not None:
336 return int(m.group(1))
337
338def check_audio_rms(sox_output, sox_threshold=_DEFAULT_SOX_RMS_THRESHOLD):
339 """Checks if the calculated RMS value is expected.
340
341 @param sox_output: The output from sox stat command.
342 @param sox_threshold: The threshold to test RMS value against.
343
344 @raises error.TestError if RMS amplitude can't be parsed.
345 @raises error.TestFail if the RMS amplitude of the recording isn't above
346 the threshold.
347 """
348 rms_val = get_audio_rms(sox_output)
349
350 # In case we don't get a valid RMS value.
351 if rms_val is None:
352 raise error.TestError(
353 'Failed to generate an audio RMS value from playback.')
354
355 logging.info('Got audio RMS value of %f. Minimum pass is %f.',
356 rms_val, sox_threshold)
357 if rms_val < sox_threshold:
358 raise error.TestFail(
359 'Audio RMS value %f too low. Minimum pass is %f.' %
360 (rms_val, sox_threshold))
361
362def noise_reduce_file(in_file, noise_file, out_file,
363 sox_format=_DEFAULT_SOX_FORMAT):
364 '''Runs the sox command to noise-reduce in_file using
365 the noise profile from noise_file.
366
367 @param in_file: The file to noise reduce.
368 @param noise_file: The file containing the noise profile.
369 This can be created by recording silence.
370 @param out_file: The file contains the noise reduced sound.
371 @param sox_format: The sox format to generate sox command.
372 '''
373 prof_cmd = '%s -c 2 %s %s -n noiseprof' % (SOX_PATH,
374 sox_format, noise_file)
375 reduce_cmd = ('%s -c 2 %s %s -c 2 %s %s noisered' %
376 (SOX_PATH, sox_format, in_file, sox_format, out_file))
377 utils.system('%s | %s' % (prof_cmd, reduce_cmd))
378
Hsin-Yu Chao4be66782013-09-06 13:45:54 +0800379def record_sample(tmpfile, record_command=_DEFAULT_REC_COMMAND):
380 '''Records a sample from the default input device.
381
382 @param tmpfile: The file to record to.
383 @param record_command: The command to record audio.
384 '''
385 utils.system('%s %s' % (record_command, tmpfile))
386
Hsin-Yu Chao4be66782013-09-06 13:45:54 +0800387def create_wav_file(wav_dir, prefix=""):
388 '''Creates a unique name for wav file.
Adrian Li689d3ff2013-07-15 15:11:06 -0700389
Hsin-Yu Chao4be66782013-09-06 13:45:54 +0800390 The created file name will be preserved in autotest result directory
391 for future analysis.
392
393 @param prefix: specified file name prefix.
Hsinyu Chao4b8300e2011-11-15 13:07:32 -0800394 '''
Hsin-Yu Chao4be66782013-09-06 13:45:54 +0800395 filename = "%s-%s.wav" % (prefix, time.time())
396 return os.path.join(wav_dir, filename)
397
Owen Lin66cd9de2013-11-08 10:26:49 +0800398def run_in_parallel(*funs):
399 threads = []
400 for f in funs:
401 t = threading.Thread(target=f)
402 t.start()
403 threads.append(t)
404
405 for t in threads:
406 t.join()
407
Hsin-Yu Chao4be66782013-09-06 13:45:54 +0800408def loopback_test_channels(noise_file_name, wav_dir,
Owen Lin66cd9de2013-11-08 10:26:49 +0800409 playback_callback=None,
Hsin-Yu Chao4be66782013-09-06 13:45:54 +0800410 check_recorded_callback=check_audio_rms,
411 preserve_test_file=True,
412 num_channels = _DEFAULT_NUM_CHANNELS,
Owen Lin66cd9de2013-11-08 10:26:49 +0800413 record_callback=record_sample,
414 mix_callback=None):
Hsin-Yu Chao4be66782013-09-06 13:45:54 +0800415 '''Tests loopback on all channels.
416
417 @param noise_file_name: Name of the file contains pre-recorded noise.
Owen Lin66cd9de2013-11-08 10:26:49 +0800418 @param playback_callback: The callback to do the playback for
Hsin-Yu Chao4be66782013-09-06 13:45:54 +0800419 one channel.
Owen Lin66cd9de2013-11-08 10:26:49 +0800420 @param record_callback: The callback to do the recording.
Hsin-Yu Chao4be66782013-09-06 13:45:54 +0800421 @param check_recorded_callback: The callback to check recorded file.
422 @param preserve_test_file: Retain the recorded files for future debugging.
Hsinyu Chao4b8300e2011-11-15 13:07:32 -0800423 '''
Hsin-Yu Chao4be66782013-09-06 13:45:54 +0800424 for channel in xrange(num_channels):
Hsin-Yu Chao4be66782013-09-06 13:45:54 +0800425 record_file_name = create_wav_file(wav_dir,
426 "record-%d" % channel)
Owen Lin66cd9de2013-11-08 10:26:49 +0800427 functions = [lambda: record_callback(record_file_name)]
Hsinyu Chao4b8300e2011-11-15 13:07:32 -0800428
Owen Lin66cd9de2013-11-08 10:26:49 +0800429 if playback_callback:
430 functions.append(lambda: playback_callback(channel))
Hsinyu Chaof80337a2012-04-07 18:02:29 +0800431
Owen Lin66cd9de2013-11-08 10:26:49 +0800432 if mix_callback:
433 mix_file_name = create_wav_file(wav_dir, "mix-%d" % channel)
434 functions.append(lambda: mix_callback(mix_file_name))
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +0800435
Owen Lin66cd9de2013-11-08 10:26:49 +0800436 run_in_parallel(*functions)
437
438 if mix_callback:
Hsin-Yu Chao4be66782013-09-06 13:45:54 +0800439 sox_output_mix = sox_stat_output(mix_file_name, channel)
440 rms_val_mix = get_audio_rms(sox_output_mix)
441 logging.info('Got mixed audio RMS value of %f.', rms_val_mix)
Adrian Li689d3ff2013-07-15 15:11:06 -0700442
Hsin-Yu Chao4be66782013-09-06 13:45:54 +0800443 sox_output_record = sox_stat_output(record_file_name, channel)
444 rms_val_record = get_audio_rms(sox_output_record)
445 logging.info('Got recorded audio RMS value of %f.', rms_val_record)
Adrian Li689d3ff2013-07-15 15:11:06 -0700446
Owen Lin66cd9de2013-11-08 10:26:49 +0800447 reduced_file_name = create_wav_file(wav_dir,
448 "reduced-%d" % channel)
Hsin-Yu Chao4be66782013-09-06 13:45:54 +0800449 noise_reduce_file(record_file_name, noise_file_name,
450 reduced_file_name)
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +0800451
Owen Lin66cd9de2013-11-08 10:26:49 +0800452 sox_output_reduced = sox_stat_output(reduced_file_name, channel)
Adrian Li689d3ff2013-07-15 15:11:06 -0700453
Hsin-Yu Chao4be66782013-09-06 13:45:54 +0800454 if not preserve_test_file:
455 os.unlink(reduced_file_name)
456 os.unlink(record_file_name)
Owen Lin66cd9de2013-11-08 10:26:49 +0800457 if mix_callback:
Hsin-Yu Chao4be66782013-09-06 13:45:54 +0800458 os.unlink(mix_file_name)
Adrian Li689d3ff2013-07-15 15:11:06 -0700459
Hsin-Yu Chao4be66782013-09-06 13:45:54 +0800460 check_recorded_callback(sox_output_reduced)
Owen Lindae7a0d2013-12-05 13:34:06 +0800461
462
463def get_channel_sox_stat(
Owen Lin2013e462013-12-05 17:54:42 +0800464 input_audio, channel_index, channels=2, bits=16, rate=48000):
Owen Lindae7a0d2013-12-05 13:34:06 +0800465 """Gets the sox stat info of the selected channel in the input audio file.
466
467 @param input_audio: The input audio file to be analyzed.
468 @param channel_index: The index of the channel to be analyzed.
469 (1 for the first channel).
470 @param channels: The number of channels in the input audio.
471 @param bits: The number of bits of each audio sample.
472 @param rate: The sampling rate.
473 """
474 if channel_index <= 0 or channel_index > channels:
475 raise ValueError('incorrect channel_indexi: %d' % channel_index)
476
477 if channels == 1:
478 return sox_utils.get_stat(input_audio)
479
480 p1 = cmd_utils.popen(
481 sox_utils.extract_channel_cmd(
482 input_audio, '-', channel_index,
483 channels=channels, bits=bits, rate=rate),
484 stdout=subprocess.PIPE)
485 p2 = cmd_utils.popen(
486 sox_utils.stat_cmd('-', channels=1, bits=bits, rate=rate),
487 stdin=p1.stdout, stderr=subprocess.PIPE)
488 stat_output = p2.stderr.read()
489 cmd_utils.wait_and_check_returncode(p1, p2)
490 return sox_utils.parse_stat_output(stat_output)
491
492
493def reduce_noise_and_check_rms(
494 input_audio, noise_file, rms_threshold=_DEFAULT_SOX_RMS_THRESHOLD,
495 channels=1, bits=16, rate=48000):
496 with tempfile.NamedTemporaryFile() as reduced_file:
497 p1 = cmd_utils.popen(
498 sox_utils.noise_profile_cmd(
499 noise_file, '-', channels=channels, bits=bits,
500 rate=rate),
501 stdout=subprocess.PIPE)
502 p2 = cmd_utils.popen(
503 sox_utils.noise_reduce_cmd(
504 input_audio, reduced_file.name, '-',
505 channels=channels, bits=bits, rate=rate),
506 stdin=p1.stdout)
507 cmd_utils.wait_and_check_returncode(p1, p2)
508
509 stats = [get_channel_sox_stat(
510 reduced_file.name, i + 1, channels=channels, bits=bits,
511 rate=rate) for i in xrange(channels)]
512
513 logging.info('sox stat: %s', [str(s) for s in stats])
514
515 if any(s.rms < rms_threshold for s in stats):
516 raise error.TestFail('RMS: %s' % [s.rms for s in stats])