blob: 3904f5ec3dc378169597b59ec0963e141089f459 [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
Owen Lin62492b02013-11-01 19:00:24 +08009import shlex
10import subprocess
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +080011import threading
Hsin-Yu Chaof272d8e2013-04-05 03:28:50 +080012import time
Hsinyu Chao4b8300e2011-11-15 13:07:32 -080013
Hsin-Yu Chao084e9da2012-11-07 15:56:26 +080014from glob import glob
15
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
19
20LD_LIBRARY_PATH = 'LD_LIBRARY_PATH'
21
Hsinyu Chaof80337a2012-04-07 18:02:29 +080022_DEFAULT_NUM_CHANNELS = 2
Dylan Reidbf9a5d42012-11-06 16:27:20 -080023_DEFAULT_REC_COMMAND = 'arecord -D hw:0,0 -d 10 -f dat'
Hsinyu Chaof80337a2012-04-07 18:02:29 +080024_DEFAULT_SOX_FORMAT = '-t raw -b 16 -e signed -r 48000 -L'
Hsin-Yu Chao4be6d182013-04-19 14:07:56 +080025
26# Minimum RMS value to pass when checking recorded file.
27_DEFAULT_SOX_RMS_THRESHOLD = 0.08
Hsinyu Chaof80337a2012-04-07 18:02:29 +080028
Hsin-Yu Chao8d093f42012-11-05 18:43:22 +080029_JACK_VALUE_ON_RE = re.compile('.*values=on')
30_HP_JACK_CONTROL_RE = re.compile('numid=(\d+).*Headphone\sJack')
31_MIC_JACK_CONTROL_RE = re.compile('numid=(\d+).*Mic\sJack')
32
Hsinyu Chaof80337a2012-04-07 18:02:29 +080033_SOX_RMS_AMPLITUDE_RE = re.compile('RMS\s+amplitude:\s+(.+)')
Hsinyu Chao2d64e1f2012-05-21 11:18:53 +080034_SOX_ROUGH_FREQ_RE = re.compile('Rough\s+frequency:\s+(.+)')
Hsinyu Chaof80337a2012-04-07 18:02:29 +080035
Hsin-Yu Chao95ee3512012-11-05 20:43:10 +080036_AUDIO_NOT_FOUND_RE = r'Audio\snot\sdetected'
37_MEASURED_LATENCY_RE = r'Measured\sLatency:\s(\d+)\suS'
38_REPORTED_LATENCY_RE = r'Reported\sLatency:\s(\d+)\suS'
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +080039
Hsin-Yu Chaof6bbc6c2013-08-20 19:22:05 +080040# Tools from platform/audiotest
41AUDIOFUNTEST_PATH = 'audiofuntest'
42AUDIOLOOP_PATH = 'looptest'
43LOOPBACK_LATENCY_PATH = 'loopback_latency'
44SOX_PATH = 'sox'
45TEST_TONES_PATH = 'test_tones'
46
47
Hsin-Yu Chao30561af2013-09-06 14:03:56 +080048def set_mixer_controls(mixer_settings={}, card='0'):
49 '''
50 Sets all mixer controls listed in the mixer settings on card.
51
52 @param mixer_settings: Mixer settings to set.
53 @param card: Index of audio card to set mixer settings for.
54 '''
55 logging.info('Setting mixer control values on %s', card)
56 for item in mixer_settings:
57 logging.info('Setting %s to %s on card %s',
58 item['name'], item['value'], card)
59 cmd = 'amixer -c %s cset name=%s %s'
60 cmd = cmd % (card, item['name'], item['value'])
61 try:
62 utils.system(cmd)
63 except error.CmdError:
64 # A card is allowed not to support all the controls, so don't
65 # fail the test here if we get an error.
66 logging.info('amixer command failed: %s', cmd)
67
68def set_volume_levels(volume, capture):
69 '''
70 Sets the volume and capture gain through cras_test_client
71
72 @param volume: The playback volume to set.
73 @param capture: The capture gain to set.
74 '''
75 logging.info('Setting volume level to %d', volume)
76 utils.system('/usr/bin/cras_test_client --volume %d' % volume)
77 logging.info('Setting capture gain to %d', capture)
78 utils.system('/usr/bin/cras_test_client --capture_gain %d' % capture)
79 utils.system('/usr/bin/cras_test_client --dump_server_info')
80 utils.system('/usr/bin/cras_test_client --mute 0')
81 utils.system('amixer -c 0 contents')
82
83def loopback_latency_check(**args):
84 '''
85 Checks loopback latency.
86
87 @param args: additional arguments for loopback_latency.
88
89 @return A tuple containing measured and reported latency in uS.
90 Return None if no audio detected.
91 '''
92 noise_threshold = str(args['n']) if args.has_key('n') else '400'
93
94 cmd = '%s -n %s' % (LOOPBACK_LATENCY_PATH, noise_threshold)
95
96 output = utils.system_output(cmd, retain_output=True)
97
98 # Sleep for a short while to make sure device is not busy anymore
99 # after called loopback_latency.
100 time.sleep(.1)
101
102 measured_latency = None
103 reported_latency = None
104 for line in output.split('\n'):
105 match = re.search(_MEASURED_LATENCY_RE, line, re.I)
106 if match:
107 measured_latency = int(match.group(1))
108 continue
109 match = re.search(_REPORTED_LATENCY_RE, line, re.I)
110 if match:
111 reported_latency = int(match.group(1))
112 continue
113 if re.search(_AUDIO_NOT_FOUND_RE, line, re.I):
114 return None
115 if measured_latency and reported_latency:
116 return (measured_latency, reported_latency)
117 else:
118 # Should not reach here, just in case.
119 return None
120
121def get_mixer_jack_status(jack_reg_exp):
122 '''
123 Gets the mixer jack status.
124
125 @param jack_reg_exp: The regular expression to match jack control name.
126
127 @return None if the control does not exist, return True if jack control
128 is detected plugged, return False otherwise.
129 '''
130 output = utils.system_output('amixer -c0 controls', retain_output=True)
131 numid = None
132 for line in output.split('\n'):
133 m = jack_reg_exp.match(line)
134 if m:
135 numid = m.group(1)
136 break
137
138 # Proceed only when matched numid is not empty.
139 if numid:
140 output = utils.system_output('amixer -c0 cget numid=%s' % numid)
141 for line in output.split('\n'):
142 if _JACK_VALUE_ON_RE.match(line):
143 return True
144 return False
145 else:
146 return None
147
148def get_hp_jack_status():
149 '''Gets the status of headphone jack'''
150 status = get_mixer_jack_status(_HP_JACK_CONTROL_RE)
151 if status is not None:
152 return status
153
154 # When headphone jack is not found in amixer, lookup input devices
155 # instead.
156 #
157 # TODO(hychao): Check hp/mic jack status dynamically from evdev. And
158 # possibly replace the existing check using amixer.
159 for evdev in glob('/dev/input/event*'):
160 device = InputDevice(evdev)
161 if device.is_hp_jack():
162 return device.get_headphone_insert()
163 else:
164 return None
165
166def get_mic_jack_status():
167 '''Gets the status of mic jack'''
168 status = get_mixer_jack_status(_MIC_JACK_CONTROL_RE)
169 if status is not None:
170 return status
171
172 # When mic jack is not found in amixer, lookup input devices instead.
173 for evdev in glob('/dev/input/event*'):
174 device = InputDevice(evdev)
175 if device.is_mic_jack():
176 return device.get_microphone_insert()
177 else:
178 return None
179
180def check_loopback_dongle():
181 '''
182 Checks if loopback dongle is equipped correctly.
183 '''
184 # Check Mic Jack
185 mic_jack_status = get_mic_jack_status()
186 if mic_jack_status is None:
187 logging.warning('Found no Mic Jack control, skip check.')
188 elif not mic_jack_status:
189 logging.info('Mic jack is not plugged.')
190 return False
191 else:
192 logging.info('Mic jack is plugged.')
193
194 # Check Headphone Jack
195 hp_jack_status = get_hp_jack_status()
196 if hp_jack_status is None:
197 logging.warning('Found no Headphone Jack control, skip check.')
198 elif not hp_jack_status:
199 logging.info('Headphone jack is not plugged.')
200 return False
201 else:
202 logging.info('Headphone jack is plugged.')
203
204 # Use latency check to test if audio can be captured through dongle.
205 # We only want to know the basic function of dongle, so no need to
206 # assert the latency accuracy here.
207 latency = loopback_latency_check(n=4000)
208 if latency:
209 logging.info('Got latency measured %d, reported %d',
210 latency[0], latency[1])
211 else:
212 logging.warning('Latency check fail.')
213 return False
214
215 return True
216
217# Functions to test audio palyback.
218def play_sound(duration_seconds=None, audio_file_path=None):
219 '''
220 Plays a sound file found at |audio_file_path| for |duration_seconds|.
221
222 If |audio_file_path|=None, plays a default audio file.
223 If |duration_seconds|=None, plays audio file in its entirety.
224
225 @param duration_seconds: Duration to play sound.
226 @param audio_file_path: Path to the audio file.
227 '''
228 if not audio_file_path:
229 audio_file_path = '/usr/local/autotest/cros/audio/sine440.wav'
230 duration_arg = ('-d %d' % duration_seconds) if duration_seconds else ''
231 utils.system('aplay %s %s' % (duration_arg, audio_file_path))
232
233def get_play_sine_args(channel, odev='default', freq=1000, duration=10,
234 sample_size=16):
235 '''Gets the command args to generate a sine wav to play to odev.
236
237 @param channel: 0 for left, 1 for right; otherwize, mono.
238 @param odev: alsa output device.
239 @param freq: frequency of the generated sine tone.
240 @param duration: duration of the generated sine tone.
241 @param sample_size: output audio sample size. Default to 16.
242 '''
243 cmdargs = [SOX_PATH, '-b', str(sample_size), '-n', '-t', 'alsa',
244 odev, 'synth', str(duration)]
245 if channel == 0:
246 cmdargs += ['sine', str(freq), 'sine', '0']
247 elif channel == 1:
248 cmdargs += ['sine', '0', 'sine', str(freq)]
249 else:
250 cmdargs += ['sine', str(freq)]
251
252 return cmdargs
253
254def play_sine(channel, odev='default', freq=1000, duration=10,
255 sample_size=16):
256 '''Generates a sine wave and plays to odev.
257
258 @param channel: 0 for left, 1 for right; otherwize, mono.
259 @param odev: alsa output device.
260 @param freq: frequency of the generated sine tone.
261 @param duration: duration of the generated sine tone.
262 @param sample_size: output audio sample size. Default to 16.
263 '''
264 cmdargs = get_play_sine_args(channel, odev, freq, duration, sample_size)
265 utils.system(' '.join(cmdargs))
266
Hsin-Yu Chao22bebdc2013-09-06 16:32:51 +0800267# Functions to compose customized sox command, execute it and process the
268# output of sox command.
269def get_sox_mixer_cmd(infile, channel,
270 num_channels=_DEFAULT_NUM_CHANNELS,
271 sox_format=_DEFAULT_SOX_FORMAT):
272 '''Gets sox mixer command to reduce channel.
273
274 @param infile: Input file name.
275 @param channel: The selected channel to take effect.
276 @param num_channels: The number of total channels to test.
277 @param sox_format: Format to generate sox command.
278 '''
279 # Build up a pan value string for the sox command.
280 if channel == 0:
281 pan_values = '1'
282 else:
283 pan_values = '0'
284 for pan_index in range(1, num_channels):
285 if channel == pan_index:
286 pan_values = '%s%s' % (pan_values, ',1')
287 else:
288 pan_values = '%s%s' % (pan_values, ',0')
289
290 return '%s -c 2 %s %s -c 1 %s - mixer %s' % (SOX_PATH,
291 sox_format, infile, sox_format, pan_values)
292
293def sox_stat_output(infile, channel,
294 num_channels=_DEFAULT_NUM_CHANNELS,
295 sox_format=_DEFAULT_SOX_FORMAT):
296 '''Executes sox stat command.
297
298 @param infile: Input file name.
299 @param channel: The selected channel.
300 @param num_channels: The number of total channels to test.
301 @param sox_format: Format to generate sox command.
302
303 @return The output of sox stat command
304 '''
305 sox_mixer_cmd = get_sox_mixer_cmd(infile, channel,
306 num_channels, sox_format)
307 stat_cmd = '%s -c 1 %s - -n stat 2>&1' % (SOX_PATH, sox_format)
308 sox_cmd = '%s | %s' % (sox_mixer_cmd, stat_cmd)
309 return utils.system_output(sox_cmd, retain_output=True)
310
311def get_audio_rms(sox_output):
312 '''Gets the audio RMS value from sox stat output
313
314 @param sox_output: Output of sox stat command.
315
316 @return The RMS value parsed from sox stat output.
317 '''
318 for rms_line in sox_output.split('\n'):
319 m = _SOX_RMS_AMPLITUDE_RE.match(rms_line)
320 if m is not None:
321 return float(m.group(1))
322
323def get_rough_freq(sox_output):
324 '''Gets the rough audio frequency from sox stat output
325
326 @param sox_output: Output of sox stat command.
327
328 @return The rough frequency value parsed from sox stat output.
329 '''
330 for rms_line in sox_output.split('\n'):
331 m = _SOX_ROUGH_FREQ_RE.match(rms_line)
332 if m is not None:
333 return int(m.group(1))
334
335def check_audio_rms(sox_output, sox_threshold=_DEFAULT_SOX_RMS_THRESHOLD):
336 """Checks if the calculated RMS value is expected.
337
338 @param sox_output: The output from sox stat command.
339 @param sox_threshold: The threshold to test RMS value against.
340
341 @raises error.TestError if RMS amplitude can't be parsed.
342 @raises error.TestFail if the RMS amplitude of the recording isn't above
343 the threshold.
344 """
345 rms_val = get_audio_rms(sox_output)
346
347 # In case we don't get a valid RMS value.
348 if rms_val is None:
349 raise error.TestError(
350 'Failed to generate an audio RMS value from playback.')
351
352 logging.info('Got audio RMS value of %f. Minimum pass is %f.',
353 rms_val, sox_threshold)
354 if rms_val < sox_threshold:
355 raise error.TestFail(
356 'Audio RMS value %f too low. Minimum pass is %f.' %
357 (rms_val, sox_threshold))
358
359def noise_reduce_file(in_file, noise_file, out_file,
360 sox_format=_DEFAULT_SOX_FORMAT):
361 '''Runs the sox command to noise-reduce in_file using
362 the noise profile from noise_file.
363
364 @param in_file: The file to noise reduce.
365 @param noise_file: The file containing the noise profile.
366 This can be created by recording silence.
367 @param out_file: The file contains the noise reduced sound.
368 @param sox_format: The sox format to generate sox command.
369 '''
370 prof_cmd = '%s -c 2 %s %s -n noiseprof' % (SOX_PATH,
371 sox_format, noise_file)
372 reduce_cmd = ('%s -c 2 %s %s -c 2 %s %s noisered' %
373 (SOX_PATH, sox_format, in_file, sox_format, out_file))
374 utils.system('%s | %s' % (prof_cmd, reduce_cmd))
375
Hsin-Yu Chao4be66782013-09-06 13:45:54 +0800376def record_sample(tmpfile, record_command=_DEFAULT_REC_COMMAND):
377 '''Records a sample from the default input device.
378
379 @param tmpfile: The file to record to.
380 @param record_command: The command to record audio.
381 '''
382 utils.system('%s %s' % (record_command, tmpfile))
383
Hsin-Yu Chao30561af2013-09-06 14:03:56 +0800384
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +0800385class RecordSampleThread(threading.Thread):
386 '''Wraps the execution of arecord in a thread.'''
Hsin-Yu Chao4be66782013-09-06 13:45:54 +0800387 def __init__(self, recordfile, record_command=_DEFAULT_REC_COMMAND):
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +0800388 threading.Thread.__init__(self)
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +0800389 self._recordfile = recordfile
Hsin-Yu Chao4be66782013-09-06 13:45:54 +0800390 self._record_command = record_command
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +0800391
392 def run(self):
Hsin-Yu Chao4be66782013-09-06 13:45:54 +0800393 record_sample(self._recordfile, self._record_command)
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +0800394
395
Adrian Li689d3ff2013-07-15 15:11:06 -0700396class RecordMixThread(threading.Thread):
397 '''
398 Wraps the execution of recording the mixed loopback stream in
399 cras_test_client in a thread.
400 '''
Hsin-Yu Chao4be66782013-09-06 13:45:54 +0800401 def __init__(self, recordfile, mix_command):
Adrian Li689d3ff2013-07-15 15:11:06 -0700402 threading.Thread.__init__(self)
Hsin-Yu Chao4be66782013-09-06 13:45:54 +0800403 self._mix_command = mix_command
Adrian Li689d3ff2013-07-15 15:11:06 -0700404 self._recordfile = recordfile
405
406 def run(self):
Hsin-Yu Chao4be66782013-09-06 13:45:54 +0800407 utils.system('%s %s' % (self._mix_command, self._recordfile))
Adrian Li689d3ff2013-07-15 15:11:06 -0700408
Hsin-Yu Chao4be66782013-09-06 13:45:54 +0800409def create_wav_file(wav_dir, prefix=""):
410 '''Creates a unique name for wav file.
Adrian Li689d3ff2013-07-15 15:11:06 -0700411
Hsin-Yu Chao4be66782013-09-06 13:45:54 +0800412 The created file name will be preserved in autotest result directory
413 for future analysis.
414
415 @param prefix: specified file name prefix.
Hsinyu Chao4b8300e2011-11-15 13:07:32 -0800416 '''
Hsin-Yu Chao4be66782013-09-06 13:45:54 +0800417 filename = "%s-%s.wav" % (prefix, time.time())
418 return os.path.join(wav_dir, filename)
419
420def loopback_test_channels(noise_file_name, wav_dir,
421 loopback_callback=None,
422 check_recorded_callback=check_audio_rms,
423 preserve_test_file=True,
424 num_channels = _DEFAULT_NUM_CHANNELS,
425 record_command=_DEFAULT_REC_COMMAND,
426 mix_command=None):
427 '''Tests loopback on all channels.
428
429 @param noise_file_name: Name of the file contains pre-recorded noise.
430 @param loopback_callback: The callback to do the loopback for
431 one channel.
432 @param check_recorded_callback: The callback to check recorded file.
433 @param preserve_test_file: Retain the recorded files for future debugging.
Hsinyu Chao4b8300e2011-11-15 13:07:32 -0800434 '''
Hsin-Yu Chao4be66782013-09-06 13:45:54 +0800435 for channel in xrange(num_channels):
436 reduced_file_name = create_wav_file(wav_dir,
437 "reduced-%d" % channel)
438 record_file_name = create_wav_file(wav_dir,
439 "record-%d" % channel)
440 record_thread = RecordSampleThread(record_file_name,
441 record_command)
442 record_thread.start()
Hsinyu Chao4b8300e2011-11-15 13:07:32 -0800443
Hsin-Yu Chao4be66782013-09-06 13:45:54 +0800444 if mix_command:
445 mix_file_name = create_wav_file(wav_dir,
446 "mix-%d" % channel)
447 mix_thread = RecordMixThread(mix_file_name, mix_command)
448 mix_thread.start()
Hsinyu Chaof80337a2012-04-07 18:02:29 +0800449
Hsin-Yu Chao4be66782013-09-06 13:45:54 +0800450 if loopback_callback:
451 loopback_callback(channel)
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +0800452
Hsin-Yu Chao4be66782013-09-06 13:45:54 +0800453 if mix_command:
454 mix_thread.join()
455 sox_output_mix = sox_stat_output(mix_file_name, channel)
456 rms_val_mix = get_audio_rms(sox_output_mix)
457 logging.info('Got mixed audio RMS value of %f.', rms_val_mix)
Adrian Li689d3ff2013-07-15 15:11:06 -0700458
Hsin-Yu Chao4be66782013-09-06 13:45:54 +0800459 record_thread.join()
460 sox_output_record = sox_stat_output(record_file_name, channel)
461 rms_val_record = get_audio_rms(sox_output_record)
462 logging.info('Got recorded audio RMS value of %f.', rms_val_record)
Adrian Li689d3ff2013-07-15 15:11:06 -0700463
Hsin-Yu Chao4be66782013-09-06 13:45:54 +0800464 noise_reduce_file(record_file_name, noise_file_name,
465 reduced_file_name)
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +0800466
Hsin-Yu Chao4be66782013-09-06 13:45:54 +0800467 sox_output_reduced = sox_stat_output(reduced_file_name,
468 channel)
Adrian Li689d3ff2013-07-15 15:11:06 -0700469
Hsin-Yu Chao4be66782013-09-06 13:45:54 +0800470 if not preserve_test_file:
471 os.unlink(reduced_file_name)
472 os.unlink(record_file_name)
473 if mix_command:
474 os.unlink(mix_file_name)
Adrian Li689d3ff2013-07-15 15:11:06 -0700475
Hsin-Yu Chao4be66782013-09-06 13:45:54 +0800476 check_recorded_callback(sox_output_reduced)
Adrian Li689d3ff2013-07-15 15:11:06 -0700477
Owen Lin62492b02013-11-01 19:00:24 +0800478def find_hw_soundcard_name(cpuType=None):
479 '''Finds the name of the default hardware soundcard.
480
481 @param cpuType: (Optional) the cpu type.
482 '''
483
484 if not cpuType:
485 cpuType = utils.get_cpu_arch()
486
487 # On Intel platform, return the name "PCH".
488 if cpuType == 'x86_64' or cpuType == 'i386':
489 return 'PCH'
490
491 # On other platforms, if there is only one card, choose it; otherwise,
492 # choose the first card with controls named 'Speaker'
493 cmd = 'amixer -c %d scontrols'
494 id = 0
495 while True:
496 p = subprocess.Popen(shlex.split(cmd % id), stdout=subprocess.PIPE)
497 output, error = p.communicate()
498 if p.wait() != 0: # end of the card list
499 break;
500 if 'speaker' in output.lower():
501 return str(id)
502 id = id + 1
503
504 # If there is only one soundcard, return it, else return not found (None)
505 return '0' if id == 1 else None