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