blob: d254cea55ab412c80088bf6b325cf73e7742b599 [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
Cheng-Yi Chiang7e49fb82014-09-01 20:02:08 +08006
Hsinyu Chao4b8300e2011-11-15 13:07:32 -08007import logging
Cheng-Yi Chiang7e49fb82014-09-01 20:02:08 +08008import numpy
Hsinyu Chao4b8300e2011-11-15 13:07:32 -08009import os
Owen Linca365f82013-11-08 16:52:28 +080010import pipes
Hsinyu Chaof80337a2012-04-07 18:02:29 +080011import re
Owen Lin62492b02013-11-01 19:00:24 +080012import shlex
Owen Lindae7a0d2013-12-05 13:34:06 +080013import tempfile
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +080014import threading
Hsin-Yu Chaof272d8e2013-04-05 03:28:50 +080015import time
Hsinyu Chao4b8300e2011-11-15 13:07:32 -080016
Hsin-Yu Chao084e9da2012-11-07 15:56:26 +080017from glob import glob
Owen Lin942e04d2014-01-09 14:16:59 +080018from autotest_lib.client.bin import test, utils
Hsin-Yu Chao084e9da2012-11-07 15:56:26 +080019from autotest_lib.client.bin.input.input_device import *
Hsinyu Chao4b8300e2011-11-15 13:07:32 -080020from autotest_lib.client.common_lib import error
Owen Lin56050862013-12-09 11:42:51 +080021from autotest_lib.client.cros.audio import alsa_utils
Cheng-Yi Chiang7e49fb82014-09-01 20:02:08 +080022from autotest_lib.client.cros.audio import audio_data
Owen Lin7ab45a22013-11-19 17:26:33 +080023from autotest_lib.client.cros.audio import cmd_utils
Owen Lin56050862013-12-09 11:42:51 +080024from autotest_lib.client.cros.audio import cras_utils
Owen Lin7ab45a22013-11-19 17:26:33 +080025from autotest_lib.client.cros.audio import sox_utils
Hsinyu Chao4b8300e2011-11-15 13:07:32 -080026
27LD_LIBRARY_PATH = 'LD_LIBRARY_PATH'
28
Owen Lin410840b2013-12-17 17:02:57 +080029_AUDIO_DIAGNOSTICS_PATH = '/usr/bin/audio_diagnostics'
30
Hsinyu Chaof80337a2012-04-07 18:02:29 +080031_DEFAULT_NUM_CHANNELS = 2
Dylan Reidbf9a5d42012-11-06 16:27:20 -080032_DEFAULT_REC_COMMAND = 'arecord -D hw:0,0 -d 10 -f dat'
Hsinyu Chaof80337a2012-04-07 18:02:29 +080033_DEFAULT_SOX_FORMAT = '-t raw -b 16 -e signed -r 48000 -L'
Owen Lin56050862013-12-09 11:42:51 +080034_DEFAULT_PLAYBACK_VOLUME = 100
35_DEFAULT_CAPTURE_GAIN = 2500
Hsin-Yu Chao4be6d182013-04-19 14:07:56 +080036
37# Minimum RMS value to pass when checking recorded file.
38_DEFAULT_SOX_RMS_THRESHOLD = 0.08
Hsinyu Chaof80337a2012-04-07 18:02:29 +080039
Hsin-Yu Chao8d093f42012-11-05 18:43:22 +080040_JACK_VALUE_ON_RE = re.compile('.*values=on')
41_HP_JACK_CONTROL_RE = re.compile('numid=(\d+).*Headphone\sJack')
42_MIC_JACK_CONTROL_RE = re.compile('numid=(\d+).*Mic\sJack')
43
Hsinyu Chaof80337a2012-04-07 18:02:29 +080044_SOX_RMS_AMPLITUDE_RE = re.compile('RMS\s+amplitude:\s+(.+)')
Hsinyu Chao2d64e1f2012-05-21 11:18:53 +080045_SOX_ROUGH_FREQ_RE = re.compile('Rough\s+frequency:\s+(.+)')
Hsinyu Chaof80337a2012-04-07 18:02:29 +080046
Hsin-Yu Chao95ee3512012-11-05 20:43:10 +080047_AUDIO_NOT_FOUND_RE = r'Audio\snot\sdetected'
48_MEASURED_LATENCY_RE = r'Measured\sLatency:\s(\d+)\suS'
49_REPORTED_LATENCY_RE = r'Reported\sLatency:\s(\d+)\suS'
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +080050
Hsin-Yu Chaof6bbc6c2013-08-20 19:22:05 +080051# Tools from platform/audiotest
52AUDIOFUNTEST_PATH = 'audiofuntest'
53AUDIOLOOP_PATH = 'looptest'
54LOOPBACK_LATENCY_PATH = 'loopback_latency'
55SOX_PATH = 'sox'
56TEST_TONES_PATH = 'test_tones'
57
Cheng-Yi Chiang7e49fb82014-09-01 20:02:08 +080058_MINIMUM_NORM = 0.001
59_CORRELATION_INDEX_THRESHOLD = 0.999
Cheng-Yi Chiang17a25272014-11-28 19:05:13 +080060# The minimum difference of estimated frequencies between two sine waves.
Cheng-Yi Chiangb8728662015-05-05 11:18:23 -070061_FREQUENCY_DIFF_THRESHOLD = 20
Cheng-Yi Chiang17a25272014-11-28 19:05:13 +080062# The minimum RMS value of meaningful audio data.
63_MEANINGFUL_RMS_THRESHOLD = 0.001
Hsin-Yu Chaof6bbc6c2013-08-20 19:22:05 +080064
Hsin-Yu Chao30561af2013-09-06 14:03:56 +080065def set_mixer_controls(mixer_settings={}, card='0'):
66 '''
67 Sets all mixer controls listed in the mixer settings on card.
68
69 @param mixer_settings: Mixer settings to set.
70 @param card: Index of audio card to set mixer settings for.
71 '''
72 logging.info('Setting mixer control values on %s', card)
73 for item in mixer_settings:
74 logging.info('Setting %s to %s on card %s',
75 item['name'], item['value'], card)
76 cmd = 'amixer -c %s cset name=%s %s'
77 cmd = cmd % (card, item['name'], item['value'])
78 try:
79 utils.system(cmd)
80 except error.CmdError:
81 # A card is allowed not to support all the controls, so don't
82 # fail the test here if we get an error.
83 logging.info('amixer command failed: %s', cmd)
84
85def set_volume_levels(volume, capture):
86 '''
87 Sets the volume and capture gain through cras_test_client
88
89 @param volume: The playback volume to set.
90 @param capture: The capture gain to set.
91 '''
92 logging.info('Setting volume level to %d', volume)
93 utils.system('/usr/bin/cras_test_client --volume %d' % volume)
94 logging.info('Setting capture gain to %d', capture)
95 utils.system('/usr/bin/cras_test_client --capture_gain %d' % capture)
96 utils.system('/usr/bin/cras_test_client --dump_server_info')
97 utils.system('/usr/bin/cras_test_client --mute 0')
98 utils.system('amixer -c 0 contents')
99
100def loopback_latency_check(**args):
101 '''
102 Checks loopback latency.
103
104 @param args: additional arguments for loopback_latency.
105
106 @return A tuple containing measured and reported latency in uS.
107 Return None if no audio detected.
108 '''
109 noise_threshold = str(args['n']) if args.has_key('n') else '400'
110
111 cmd = '%s -n %s' % (LOOPBACK_LATENCY_PATH, noise_threshold)
112
113 output = utils.system_output(cmd, retain_output=True)
114
115 # Sleep for a short while to make sure device is not busy anymore
116 # after called loopback_latency.
117 time.sleep(.1)
118
119 measured_latency = None
120 reported_latency = None
121 for line in output.split('\n'):
122 match = re.search(_MEASURED_LATENCY_RE, line, re.I)
123 if match:
124 measured_latency = int(match.group(1))
125 continue
126 match = re.search(_REPORTED_LATENCY_RE, line, re.I)
127 if match:
128 reported_latency = int(match.group(1))
129 continue
130 if re.search(_AUDIO_NOT_FOUND_RE, line, re.I):
131 return None
132 if measured_latency and reported_latency:
133 return (measured_latency, reported_latency)
134 else:
135 # Should not reach here, just in case.
136 return None
137
138def get_mixer_jack_status(jack_reg_exp):
139 '''
140 Gets the mixer jack status.
141
142 @param jack_reg_exp: The regular expression to match jack control name.
143
144 @return None if the control does not exist, return True if jack control
145 is detected plugged, return False otherwise.
146 '''
147 output = utils.system_output('amixer -c0 controls', retain_output=True)
148 numid = None
149 for line in output.split('\n'):
150 m = jack_reg_exp.match(line)
151 if m:
152 numid = m.group(1)
153 break
154
155 # Proceed only when matched numid is not empty.
156 if numid:
157 output = utils.system_output('amixer -c0 cget numid=%s' % numid)
158 for line in output.split('\n'):
159 if _JACK_VALUE_ON_RE.match(line):
160 return True
161 return False
162 else:
163 return None
164
165def get_hp_jack_status():
166 '''Gets the status of headphone jack'''
167 status = get_mixer_jack_status(_HP_JACK_CONTROL_RE)
168 if status is not None:
169 return status
170
171 # When headphone jack is not found in amixer, lookup input devices
172 # instead.
173 #
174 # TODO(hychao): Check hp/mic jack status dynamically from evdev. And
175 # possibly replace the existing check using amixer.
176 for evdev in glob('/dev/input/event*'):
177 device = InputDevice(evdev)
178 if device.is_hp_jack():
179 return device.get_headphone_insert()
180 else:
181 return None
182
183def get_mic_jack_status():
184 '''Gets the status of mic jack'''
185 status = get_mixer_jack_status(_MIC_JACK_CONTROL_RE)
186 if status is not None:
187 return status
188
189 # When mic jack is not found in amixer, lookup input devices instead.
190 for evdev in glob('/dev/input/event*'):
191 device = InputDevice(evdev)
192 if device.is_mic_jack():
193 return device.get_microphone_insert()
194 else:
195 return None
196
Owen Lin56050862013-12-09 11:42:51 +0800197def log_loopback_dongle_status():
Hsin-Yu Chao30561af2013-09-06 14:03:56 +0800198 '''
Owen Lin56050862013-12-09 11:42:51 +0800199 Log the status of the loopback dongle to make sure it is equipped correctly.
Hsin-Yu Chao30561af2013-09-06 14:03:56 +0800200 '''
Owen Lin56050862013-12-09 11:42:51 +0800201 dongle_status_ok = True
202
Hsin-Yu Chao30561af2013-09-06 14:03:56 +0800203 # Check Mic Jack
204 mic_jack_status = get_mic_jack_status()
Owen Lin56050862013-12-09 11:42:51 +0800205 logging.info('Mic jack status: %s', mic_jack_status)
Owen Lin14bec7a2014-04-21 11:39:40 +0800206 dongle_status_ok &= bool(mic_jack_status)
Hsin-Yu Chao30561af2013-09-06 14:03:56 +0800207
208 # Check Headphone Jack
209 hp_jack_status = get_hp_jack_status()
Owen Lin56050862013-12-09 11:42:51 +0800210 logging.info('Headphone jack status: %s', hp_jack_status)
Owen Lin14bec7a2014-04-21 11:39:40 +0800211 dongle_status_ok &= bool(hp_jack_status)
Hsin-Yu Chao30561af2013-09-06 14:03:56 +0800212
213 # Use latency check to test if audio can be captured through dongle.
214 # We only want to know the basic function of dongle, so no need to
215 # assert the latency accuracy here.
216 latency = loopback_latency_check(n=4000)
217 if latency:
218 logging.info('Got latency measured %d, reported %d',
219 latency[0], latency[1])
220 else:
Owen Lin56050862013-12-09 11:42:51 +0800221 logging.info('Latency check fail.')
222 dongle_status_ok = False
Hsin-Yu Chao30561af2013-09-06 14:03:56 +0800223
Owen Lin56050862013-12-09 11:42:51 +0800224 logging.info('audio loopback dongle test: %s',
225 'PASS' if dongle_status_ok else 'FAIL')
Hsin-Yu Chao30561af2013-09-06 14:03:56 +0800226
227# Functions to test audio palyback.
228def play_sound(duration_seconds=None, audio_file_path=None):
229 '''
230 Plays a sound file found at |audio_file_path| for |duration_seconds|.
231
232 If |audio_file_path|=None, plays a default audio file.
233 If |duration_seconds|=None, plays audio file in its entirety.
234
235 @param duration_seconds: Duration to play sound.
236 @param audio_file_path: Path to the audio file.
237 '''
238 if not audio_file_path:
239 audio_file_path = '/usr/local/autotest/cros/audio/sine440.wav'
240 duration_arg = ('-d %d' % duration_seconds) if duration_seconds else ''
241 utils.system('aplay %s %s' % (duration_arg, audio_file_path))
242
243def get_play_sine_args(channel, odev='default', freq=1000, duration=10,
244 sample_size=16):
245 '''Gets the command args to generate a sine wav to play to odev.
246
247 @param channel: 0 for left, 1 for right; otherwize, mono.
248 @param odev: alsa output device.
249 @param freq: frequency of the generated sine tone.
250 @param duration: duration of the generated sine tone.
251 @param sample_size: output audio sample size. Default to 16.
252 '''
253 cmdargs = [SOX_PATH, '-b', str(sample_size), '-n', '-t', 'alsa',
254 odev, 'synth', str(duration)]
255 if channel == 0:
256 cmdargs += ['sine', str(freq), 'sine', '0']
257 elif channel == 1:
258 cmdargs += ['sine', '0', 'sine', str(freq)]
259 else:
260 cmdargs += ['sine', str(freq)]
261
262 return cmdargs
263
264def play_sine(channel, odev='default', freq=1000, duration=10,
265 sample_size=16):
266 '''Generates a sine wave and plays to odev.
267
268 @param channel: 0 for left, 1 for right; otherwize, mono.
269 @param odev: alsa output device.
270 @param freq: frequency of the generated sine tone.
271 @param duration: duration of the generated sine tone.
272 @param sample_size: output audio sample size. Default to 16.
273 '''
274 cmdargs = get_play_sine_args(channel, odev, freq, duration, sample_size)
275 utils.system(' '.join(cmdargs))
276
Hsin-Yu Chao22bebdc2013-09-06 16:32:51 +0800277# Functions to compose customized sox command, execute it and process the
278# output of sox command.
279def get_sox_mixer_cmd(infile, channel,
280 num_channels=_DEFAULT_NUM_CHANNELS,
281 sox_format=_DEFAULT_SOX_FORMAT):
282 '''Gets sox mixer command to reduce channel.
283
284 @param infile: Input file name.
285 @param channel: The selected channel to take effect.
286 @param num_channels: The number of total channels to test.
287 @param sox_format: Format to generate sox command.
288 '''
289 # Build up a pan value string for the sox command.
290 if channel == 0:
291 pan_values = '1'
292 else:
293 pan_values = '0'
294 for pan_index in range(1, num_channels):
295 if channel == pan_index:
296 pan_values = '%s%s' % (pan_values, ',1')
297 else:
298 pan_values = '%s%s' % (pan_values, ',0')
299
300 return '%s -c 2 %s %s -c 1 %s - mixer %s' % (SOX_PATH,
301 sox_format, infile, sox_format, pan_values)
302
303def sox_stat_output(infile, channel,
304 num_channels=_DEFAULT_NUM_CHANNELS,
305 sox_format=_DEFAULT_SOX_FORMAT):
306 '''Executes sox stat command.
307
308 @param infile: Input file name.
309 @param channel: The selected channel.
310 @param num_channels: The number of total channels to test.
311 @param sox_format: Format to generate sox command.
312
313 @return The output of sox stat command
314 '''
315 sox_mixer_cmd = get_sox_mixer_cmd(infile, channel,
316 num_channels, sox_format)
317 stat_cmd = '%s -c 1 %s - -n stat 2>&1' % (SOX_PATH, sox_format)
318 sox_cmd = '%s | %s' % (sox_mixer_cmd, stat_cmd)
319 return utils.system_output(sox_cmd, retain_output=True)
320
321def get_audio_rms(sox_output):
322 '''Gets the audio RMS value from sox stat output
323
324 @param sox_output: Output of sox stat command.
325
326 @return The RMS value parsed from sox stat output.
327 '''
328 for rms_line in sox_output.split('\n'):
329 m = _SOX_RMS_AMPLITUDE_RE.match(rms_line)
330 if m is not None:
331 return float(m.group(1))
332
333def get_rough_freq(sox_output):
334 '''Gets the rough audio frequency from sox stat output
335
336 @param sox_output: Output of sox stat command.
337
338 @return The rough frequency value parsed from sox stat output.
339 '''
340 for rms_line in sox_output.split('\n'):
341 m = _SOX_ROUGH_FREQ_RE.match(rms_line)
342 if m is not None:
343 return int(m.group(1))
344
345def check_audio_rms(sox_output, sox_threshold=_DEFAULT_SOX_RMS_THRESHOLD):
346 """Checks if the calculated RMS value is expected.
347
348 @param sox_output: The output from sox stat command.
349 @param sox_threshold: The threshold to test RMS value against.
350
351 @raises error.TestError if RMS amplitude can't be parsed.
352 @raises error.TestFail if the RMS amplitude of the recording isn't above
353 the threshold.
354 """
355 rms_val = get_audio_rms(sox_output)
356
357 # In case we don't get a valid RMS value.
358 if rms_val is None:
359 raise error.TestError(
360 'Failed to generate an audio RMS value from playback.')
361
362 logging.info('Got audio RMS value of %f. Minimum pass is %f.',
363 rms_val, sox_threshold)
364 if rms_val < sox_threshold:
365 raise error.TestFail(
366 'Audio RMS value %f too low. Minimum pass is %f.' %
367 (rms_val, sox_threshold))
368
369def noise_reduce_file(in_file, noise_file, out_file,
370 sox_format=_DEFAULT_SOX_FORMAT):
371 '''Runs the sox command to noise-reduce in_file using
372 the noise profile from noise_file.
373
374 @param in_file: The file to noise reduce.
375 @param noise_file: The file containing the noise profile.
376 This can be created by recording silence.
377 @param out_file: The file contains the noise reduced sound.
378 @param sox_format: The sox format to generate sox command.
379 '''
380 prof_cmd = '%s -c 2 %s %s -n noiseprof' % (SOX_PATH,
381 sox_format, noise_file)
382 reduce_cmd = ('%s -c 2 %s %s -c 2 %s %s noisered' %
383 (SOX_PATH, sox_format, in_file, sox_format, out_file))
384 utils.system('%s | %s' % (prof_cmd, reduce_cmd))
385
Hsin-Yu Chao4be66782013-09-06 13:45:54 +0800386def record_sample(tmpfile, record_command=_DEFAULT_REC_COMMAND):
387 '''Records a sample from the default input device.
388
389 @param tmpfile: The file to record to.
390 @param record_command: The command to record audio.
391 '''
392 utils.system('%s %s' % (record_command, tmpfile))
393
Hsin-Yu Chao4be66782013-09-06 13:45:54 +0800394def create_wav_file(wav_dir, prefix=""):
395 '''Creates a unique name for wav file.
Adrian Li689d3ff2013-07-15 15:11:06 -0700396
Hsin-Yu Chao4be66782013-09-06 13:45:54 +0800397 The created file name will be preserved in autotest result directory
398 for future analysis.
399
400 @param prefix: specified file name prefix.
Hsinyu Chao4b8300e2011-11-15 13:07:32 -0800401 '''
Hsin-Yu Chao4be66782013-09-06 13:45:54 +0800402 filename = "%s-%s.wav" % (prefix, time.time())
403 return os.path.join(wav_dir, filename)
404
Owen Lin66cd9de2013-11-08 10:26:49 +0800405def run_in_parallel(*funs):
406 threads = []
407 for f in funs:
408 t = threading.Thread(target=f)
409 t.start()
410 threads.append(t)
411
412 for t in threads:
413 t.join()
414
Hsin-Yu Chao4be66782013-09-06 13:45:54 +0800415def loopback_test_channels(noise_file_name, wav_dir,
Owen Lin66cd9de2013-11-08 10:26:49 +0800416 playback_callback=None,
Hsin-Yu Chao4be66782013-09-06 13:45:54 +0800417 check_recorded_callback=check_audio_rms,
418 preserve_test_file=True,
419 num_channels = _DEFAULT_NUM_CHANNELS,
Owen Lin66cd9de2013-11-08 10:26:49 +0800420 record_callback=record_sample,
421 mix_callback=None):
Hsin-Yu Chao4be66782013-09-06 13:45:54 +0800422 '''Tests loopback on all channels.
423
424 @param noise_file_name: Name of the file contains pre-recorded noise.
Owen Lin66cd9de2013-11-08 10:26:49 +0800425 @param playback_callback: The callback to do the playback for
Hsin-Yu Chao4be66782013-09-06 13:45:54 +0800426 one channel.
Owen Lin66cd9de2013-11-08 10:26:49 +0800427 @param record_callback: The callback to do the recording.
Hsin-Yu Chao4be66782013-09-06 13:45:54 +0800428 @param check_recorded_callback: The callback to check recorded file.
429 @param preserve_test_file: Retain the recorded files for future debugging.
Hsinyu Chao4b8300e2011-11-15 13:07:32 -0800430 '''
Hsin-Yu Chao4be66782013-09-06 13:45:54 +0800431 for channel in xrange(num_channels):
Hsin-Yu Chao4be66782013-09-06 13:45:54 +0800432 record_file_name = create_wav_file(wav_dir,
433 "record-%d" % channel)
Owen Lin66cd9de2013-11-08 10:26:49 +0800434 functions = [lambda: record_callback(record_file_name)]
Hsinyu Chao4b8300e2011-11-15 13:07:32 -0800435
Owen Lin66cd9de2013-11-08 10:26:49 +0800436 if playback_callback:
437 functions.append(lambda: playback_callback(channel))
Hsinyu Chaof80337a2012-04-07 18:02:29 +0800438
Owen Lin66cd9de2013-11-08 10:26:49 +0800439 if mix_callback:
440 mix_file_name = create_wav_file(wav_dir, "mix-%d" % channel)
441 functions.append(lambda: mix_callback(mix_file_name))
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +0800442
Owen Lin66cd9de2013-11-08 10:26:49 +0800443 run_in_parallel(*functions)
444
445 if mix_callback:
Hsin-Yu Chao4be66782013-09-06 13:45:54 +0800446 sox_output_mix = sox_stat_output(mix_file_name, channel)
447 rms_val_mix = get_audio_rms(sox_output_mix)
448 logging.info('Got mixed audio RMS value of %f.', rms_val_mix)
Adrian Li689d3ff2013-07-15 15:11:06 -0700449
Hsin-Yu Chao4be66782013-09-06 13:45:54 +0800450 sox_output_record = sox_stat_output(record_file_name, channel)
451 rms_val_record = get_audio_rms(sox_output_record)
452 logging.info('Got recorded audio RMS value of %f.', rms_val_record)
Adrian Li689d3ff2013-07-15 15:11:06 -0700453
Owen Lin66cd9de2013-11-08 10:26:49 +0800454 reduced_file_name = create_wav_file(wav_dir,
455 "reduced-%d" % channel)
Hsin-Yu Chao4be66782013-09-06 13:45:54 +0800456 noise_reduce_file(record_file_name, noise_file_name,
457 reduced_file_name)
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +0800458
Owen Lin66cd9de2013-11-08 10:26:49 +0800459 sox_output_reduced = sox_stat_output(reduced_file_name, channel)
Adrian Li689d3ff2013-07-15 15:11:06 -0700460
Hsin-Yu Chao4be66782013-09-06 13:45:54 +0800461 if not preserve_test_file:
462 os.unlink(reduced_file_name)
463 os.unlink(record_file_name)
Owen Lin66cd9de2013-11-08 10:26:49 +0800464 if mix_callback:
Hsin-Yu Chao4be66782013-09-06 13:45:54 +0800465 os.unlink(mix_file_name)
Adrian Li689d3ff2013-07-15 15:11:06 -0700466
Hsin-Yu Chao4be66782013-09-06 13:45:54 +0800467 check_recorded_callback(sox_output_reduced)
Owen Lindae7a0d2013-12-05 13:34:06 +0800468
469
470def get_channel_sox_stat(
Owen Lin2013e462013-12-05 17:54:42 +0800471 input_audio, channel_index, channels=2, bits=16, rate=48000):
Owen Lindae7a0d2013-12-05 13:34:06 +0800472 """Gets the sox stat info of the selected channel in the input audio file.
473
474 @param input_audio: The input audio file to be analyzed.
475 @param channel_index: The index of the channel to be analyzed.
476 (1 for the first channel).
477 @param channels: The number of channels in the input audio.
478 @param bits: The number of bits of each audio sample.
479 @param rate: The sampling rate.
480 """
481 if channel_index <= 0 or channel_index > channels:
482 raise ValueError('incorrect channel_indexi: %d' % channel_index)
483
484 if channels == 1:
Owen Lin4a154c42013-12-26 11:03:20 +0800485 return sox_utils.get_stat(
486 input_audio, channels=channels, bits=bits, rate=rate)
Owen Lindae7a0d2013-12-05 13:34:06 +0800487
488 p1 = cmd_utils.popen(
489 sox_utils.extract_channel_cmd(
490 input_audio, '-', channel_index,
491 channels=channels, bits=bits, rate=rate),
Owen Linad6610a2013-12-13 11:20:48 +0800492 stdout=cmd_utils.PIPE)
Owen Lindae7a0d2013-12-05 13:34:06 +0800493 p2 = cmd_utils.popen(
494 sox_utils.stat_cmd('-', channels=1, bits=bits, rate=rate),
Owen Linad6610a2013-12-13 11:20:48 +0800495 stdin=p1.stdout, stderr=cmd_utils.PIPE)
Owen Lindae7a0d2013-12-05 13:34:06 +0800496 stat_output = p2.stderr.read()
497 cmd_utils.wait_and_check_returncode(p1, p2)
498 return sox_utils.parse_stat_output(stat_output)
499
500
Owen Lin942e04d2014-01-09 14:16:59 +0800501def get_rms(input_audio, channels=1, bits=16, rate=48000):
502 """Gets the RMS values of all channels of the input audio.
Owen Lin5bba6c02013-12-13 14:17:08 +0800503
Owen Lin5cbf5c32013-12-13 15:13:37 +0800504 @param input_audio: The input audio file to be checked.
Owen Lin5bba6c02013-12-13 14:17:08 +0800505 @param channels: The number of channels in the input audio.
506 @param bits: The number of bits of each audio sample.
507 @param rate: The sampling rate.
Owen Lin5bba6c02013-12-13 14:17:08 +0800508 """
509 stats = [get_channel_sox_stat(
510 input_audio, 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])
Owen Lin942e04d2014-01-09 14:16:59 +0800514 return [s.rms for s in stats]
Owen Lin5bba6c02013-12-13 14:17:08 +0800515
516
Owen Lin942e04d2014-01-09 14:16:59 +0800517def reduce_noise_and_get_rms(
518 input_audio, noise_file, channels=1, bits=16, rate=48000):
519 """Reduces noise in the input audio by the given noise file and then gets
Owen Lin5cbf5c32013-12-13 15:13:37 +0800520 the RMS values of all channels of the input audio.
Owen Lin5bba6c02013-12-13 14:17:08 +0800521
522 @param input_audio: The input audio file to be analyzed.
523 @param noise_file: The noise file used to reduce noise in the input audio.
Owen Lin5bba6c02013-12-13 14:17:08 +0800524 @param channels: The number of channels in the input audio.
525 @param bits: The number of bits of each audio sample.
526 @param rate: The sampling rate.
Owen Lin5bba6c02013-12-13 14:17:08 +0800527 """
Owen Lindae7a0d2013-12-05 13:34:06 +0800528 with tempfile.NamedTemporaryFile() as reduced_file:
529 p1 = cmd_utils.popen(
530 sox_utils.noise_profile_cmd(
531 noise_file, '-', channels=channels, bits=bits,
532 rate=rate),
Owen Linad6610a2013-12-13 11:20:48 +0800533 stdout=cmd_utils.PIPE)
Owen Lindae7a0d2013-12-05 13:34:06 +0800534 p2 = cmd_utils.popen(
535 sox_utils.noise_reduce_cmd(
536 input_audio, reduced_file.name, '-',
537 channels=channels, bits=bits, rate=rate),
538 stdin=p1.stdout)
539 cmd_utils.wait_and_check_returncode(p1, p2)
Owen Lin942e04d2014-01-09 14:16:59 +0800540 return get_rms(reduced_file.name, channels, bits, rate)
Owen Lin56050862013-12-09 11:42:51 +0800541
542
Rohit Makasana06446562014-01-03 11:39:03 -0800543def skip_devices_to_test(*boards):
544 """Devices to skip due to hardware or test compatibility issues."""
545 # TODO(scottz): Remove this when crbug.com/220147 is fixed.
546 dut_board = utils.get_current_board()
547 if dut_board in boards:
548 raise error.TestNAError('This test is not available on %s' % dut_board)
549
550
Owen Lin56050862013-12-09 11:42:51 +0800551def cras_rms_test_setup():
552 """ Setups for the cras_rms_tests.
553
554 To make sure the line_out-to-mic_in path is all green.
555 """
556 # TODO(owenlin): Now, the nodes are choosed by chrome.
557 # We should do it here.
558 output_node, _ = cras_utils.get_selected_nodes()
559
560 cras_utils.set_system_volume(_DEFAULT_PLAYBACK_VOLUME)
561 cras_utils.set_node_volume(output_node, _DEFAULT_PLAYBACK_VOLUME)
562
563 cras_utils.set_capture_gain(_DEFAULT_CAPTURE_GAIN)
564
565 cras_utils.set_system_mute(False)
566 cras_utils.set_capture_mute(False)
567
568
Owen Lin63b214d2014-01-07 16:40:14 +0800569def generate_rms_postmortem():
570 try:
571 logging.info('audio postmortem report')
572 log_loopback_dongle_status()
Cheng-Yi Chiang88fdf252015-04-08 01:02:59 -0700573 logging.info(get_audio_diagnostics())
Owen Lin942e04d2014-01-09 14:16:59 +0800574 except Exception:
Owen Lin63b214d2014-01-07 16:40:14 +0800575 logging.exception('Error while generating postmortem report')
576
577
Cheng-Yi Chiang88fdf252015-04-08 01:02:59 -0700578def get_audio_diagnostics():
579 """Gets audio diagnostic results.
580
581 @returns: a string containing diagnostic results.
582
583 """
584 return cmd_utils.execute([_AUDIO_DIAGNOSTICS_PATH], stdout=cmd_utils.PIPE)
585
586
Cheng-Yi Chiang7e49fb82014-09-01 20:02:08 +0800587def get_max_cross_correlation(signal_a, signal_b):
588 """Gets max cross-correlation and best time delay of two signals.
589
590 Computes cross-correlation function between two
591 signals and gets the maximum value and time delay.
592 The steps includes:
593 1. Compute cross-correlation function of X and Y and get Cxy.
594 The correlation function Cxy is an array where Cxy[k] is the
595 cross product of X and Y when Y is delayed by k.
596 Refer to manual of numpy.correlate for detail of correlation.
597 2. Find the maximum value C_max and index C_index in Cxy.
598 3. Compute L2 norm of X and Y to get norm(X) and norm(Y).
599 4. Divide C_max by norm(X)*norm(Y) to get max cross-correlation.
600
601 Max cross-correlation indicates the similarity of X and Y. The value
602 is 1 if X equals Y multiplied by a positive scalar.
603 The value is -1 if X equals Y multiplied by a negative scaler.
604 Any constant level shift will be regarded as distortion and will make
605 max cross-correlation value deviated from 1.
606 C_index is the best time delay of Y that make Y looks similar to X.
607 Refer to http://en.wikipedia.org/wiki/Cross-correlation.
608
609 @param signal_a: A list of numbers which contains the first signal.
610 @param signal_b: A list of numbers which contains the second signal.
611
612 @raises: ValueError if any number in signal_a or signal_b is not a float.
613 ValueError if norm of any array is less than _MINIMUM_NORM.
614
615 @returns: A tuple (correlation index, best delay). If there are more than
616 one best delay, just return the first one.
617 """
618 def check_list_contains_float(numbers):
619 """Checks the elements in a list are all float.
620
621 @param numbers: A list of numbers.
622
623 @raises: ValueError if there is any element which is not a float
624 in the list.
625 """
626 if any(not isinstance(x, float) for x in numbers):
627 raise ValueError('List contains number which is not a float')
628
629 check_list_contains_float(signal_a)
630 check_list_contains_float(signal_b)
631
632 norm_a = numpy.linalg.norm(signal_a)
633 norm_b = numpy.linalg.norm(signal_b)
634 logging.debug('norm_a: %f', norm_a)
635 logging.debug('norm_b: %f', norm_b)
636 if norm_a <= _MINIMUM_NORM or norm_b <= _MINIMUM_NORM:
637 raise ValueError('No meaningful data as norm is too small.')
638
639 correlation = numpy.correlate(signal_a, signal_b, 'full')
640 max_correlation = max(correlation)
641 best_delays = [i for i, j in enumerate(correlation) if j == max_correlation]
642 if len(best_delays) > 1:
643 logging.warning('There are more than one best delay: %r', best_delays)
644 return max_correlation / (norm_a * norm_b), best_delays[0]
645
646
647def trim_data(data, threshold=0):
648 """Trims a data by removing value that is too small in head and tail.
649
650 Removes elements in head and tail whose absolute value is smaller than
651 or equal to threshold.
652 E.g. trim_data([0.0, 0.1, 0.2, 0.3, 0.2, 0.1, 0.0], 0.2) =
653 ([0.2, 0.3, 0.2], 2)
654
655 @param data: A list of numbers.
656 @param threshold: The threshold to compare against.
657
658 @returns: A tuple (trimmed_data, valid_index), where valid_index is the
659 original index of the starting element in trimmed_data.
660 Returns ([], None) if there is no valid data.
661 """
662 indice_valid = [
663 i for i, j in enumerate(data) if abs(j) > threshold]
664 if not indice_valid:
665 logging.warning(
666 'There is no element with absolute value greater '
Cheng-Yi Chiang17a25272014-11-28 19:05:13 +0800667 'than threshold %f', threshold)
Cheng-Yi Chiang7e49fb82014-09-01 20:02:08 +0800668 return [], None
669 logging.debug('Start and end of indice_valid: %d, %d',
670 indice_valid[0], indice_valid[-1])
671 return data[indice_valid[0] : indice_valid[-1] + 1], indice_valid[0]
672
673
674def get_one_channel_correlation(test_data, golden_data):
675 """Gets max cross-correlation of test_data and golden_data.
676
677 Trims test data and compute the max cross-correlation against golden_data.
678 Signal can be trimmed because those zero values in the head and tail of
679 a signal will not affect correlation computation.
680
681 @param test_data: A list containing the data to compare against golden data.
682 @param golden_data: A list containing the golden data.
683
684 @returns: A tuple (max cross-correlation, best_delay) if data is valid.
685 Otherwise returns (None, None). Refer to docstring of
686 get_max_cross_correlation.
687 """
688 trimmed_test_data, start_trimmed_length = trim_data(test_data)
689
690 def to_float(samples):
691 """Casts elements in the list to float.
692
693 @param samples: A list of numbers.
694
695 @returns: A list of original numbers casted to float.
696 """
697 samples_float = [float(x) for x in samples]
698 return samples_float
699
700 max_cross_correlation, best_delay = get_max_cross_correlation(
701 to_float(golden_data),
702 to_float(trimmed_test_data))
703
704 # Adds back the trimmed length in the head.
705 if max_cross_correlation:
706 return max_cross_correlation, best_delay + start_trimmed_length
707 else:
708 return None, None
709
710
711def compare_one_channel_correlation(test_data, golden_data):
712 """Compares two one-channel data by correlation.
713
714 @param test_data: A list containing the data to compare against golden data.
715 @param golden_data: A list containing the golden data.
716
717 @returns: A dict containing:
718 index: The index of similarity where 1 means they are different
719 only by a positive scale.
720 best_delay: The best delay of test data in relative to golden
721 data.
722 equal: A bool containing comparing result.
723 """
724 result_dict = dict()
725 max_cross_correlation, best_delay = get_one_channel_correlation(
726 test_data, golden_data)
727 result_dict['index'] = max_cross_correlation
728 result_dict['best_delay'] = best_delay
729 result_dict['equal'] = True if (
730 max_cross_correlation and
731 max_cross_correlation > _CORRELATION_INDEX_THRESHOLD) else False
732 logging.debug('result_dict: %r', result_dict)
733 return result_dict
734
735
Cheng-Yi Chiang17a25272014-11-28 19:05:13 +0800736def get_one_channel_stat(data, data_format):
737 """Gets statistic information of data.
738
739 @param data: A list containing one channel data.
740 @param data_format: A dict containing data format of data.
741
742 @return: The sox stat parsed result. An object containing
743 sameple_count: An int. Samples read.
744 length: A float. Length in seconds.
745 rms: A float. RMS amplitude.
746 rough_frequency: A float. Rough frequency.
747 """
748 if not data:
749 raise ValueError('Data is empty. Can not get stat')
750 raw_data = audio_data.AudioRawData(
751 binary=None, channel=1,
752 sample_format=data_format['sample_format'])
753 raw_data.copy_channel_data([data])
Cheng-Yi Chiang1a642ea2015-05-05 11:43:57 -0700754 with tempfile.NamedTemporaryFile() as raw_data_file:
755 raw_data_path = raw_data_file.name
756 raw_data.write_to_file(raw_data_path)
Cheng-Yi Chiang17a25272014-11-28 19:05:13 +0800757
Cheng-Yi Chiang1a642ea2015-05-05 11:43:57 -0700758 bits = 8 * (audio_data.SAMPLE_FORMATS[
759 data_format['sample_format']]['size_bytes'])
760 stat = sox_utils.get_stat(raw_data_path, channels=1, bits=bits,
761 rate=data_format['rate'])
762 return stat
Cheng-Yi Chiang17a25272014-11-28 19:05:13 +0800763
764
765def compare_one_channel_frequency(test_data, test_data_format,
766 golden_data, golden_data_format):
767 """Compares two one-channel data by frequency.
768
769 @param test_data: A list containing the data to compare against golden data.
770 @param test_data_format: A dict containing data format of test data.
771 @param golden_data: A list containing the golden data.
772 @param golden_data_format: A dict containing data format of golden data.
773
774 @returns: A dict containing:
775 test_data_frequency: test data frequency.
776 golden_data_frequency: golden data frequency.
777 equal: A bool containing comparing result.
778
779 """
780 result_dict = dict()
781 golden_data_stat = get_one_channel_stat(golden_data, golden_data_format)
782 logging.info('Get golden data one channel stat: %s', golden_data_stat)
783 test_data_stat = get_one_channel_stat(test_data, test_data_format)
784 logging.info('Get test data one channel stat: %s', test_data_stat)
785
786 result_dict['golden_data_frequency'] = golden_data_stat.rough_frequency
787 result_dict['test_data_frequency'] = test_data_stat.rough_frequency
788 result_dict['equal'] = True if (
789 abs(result_dict['test_data_frequency'] -
790 result_dict['golden_data_frequency']) < _FREQUENCY_DIFF_THRESHOLD
791 ) else False
792 if test_data_stat.rms < _MEANINGFUL_RMS_THRESHOLD:
793 logging.error('Recorded RMS %f is too small to be meaningful.',
794 test_data_stat.rms)
795 result_dict['equal'] = False
796 logging.debug('result_dict: %r', result_dict)
797 return result_dict
798
799
800def compare_one_channel_data(test_data, test_data_format,
801 golden_data, golden_data_format, method):
Cheng-Yi Chiang7e49fb82014-09-01 20:02:08 +0800802 """Compares two one-channel data.
803
804 @param test_data: A list containing the data to compare against golden data.
Cheng-Yi Chiang17a25272014-11-28 19:05:13 +0800805 @param test_data_format: The data format of test data.
Cheng-Yi Chiang7e49fb82014-09-01 20:02:08 +0800806 @param golden_data: A list containing the golden data.
Cheng-Yi Chiang17a25272014-11-28 19:05:13 +0800807 @param golden_data_format: The data format of golden data.
Cheng-Yi Chiang7e49fb82014-09-01 20:02:08 +0800808 @param method: The comparing method. Currently only 'correlation' is
809 supported.
810
811 @returns: A dict containing:
812 index: The index of similarity where 1 means they are different
813 only by a positive scale.
814 best_delay: The best delay of test data in relative to golden
815 data.
816 equal: A bool containing comparing result.
817
818 @raises: NotImplementedError if method is not supported.
819 """
820 if method == 'correlation':
821 return compare_one_channel_correlation(test_data, golden_data)
Cheng-Yi Chiang17a25272014-11-28 19:05:13 +0800822 if method == 'frequency':
823 return compare_one_channel_frequency(
824 test_data, test_data_format, golden_data, golden_data_format)
Cheng-Yi Chiang7e49fb82014-09-01 20:02:08 +0800825 raise NotImplementedError('method %s is not implemented' % method)
826
827
828def compare_data(golden_data_binary, golden_data_format,
829 test_data_binary, test_data_format,
830 channel_map, method):
831 """Compares two raw data.
832
833 @param golden_data_binary: The binary containing golden data.
834 @param golden_data_format: The data format of golden data.
835 @param test_data_binary: The binary containing test data.
836 @param test_data_format: The data format of test data.
837 @param channel_map: A list containing channel mapping.
838 E.g. [1, 0, None, None, None, None, None, None] means
839 channel 0 of test data should map to channel 1 of
840 golden data. Channel 1 of test data should map to
841 channel 0 of golden data. Channel 2 to 7 of test data
842 should be skipped.
Cheng-Yi Chiang17a25272014-11-28 19:05:13 +0800843 @param method: The method to compare data. Use 'correlation' to compare
844 general data. Use 'frequency' to compare data containing
845 sine wave.
Cheng-Yi Chiang7e49fb82014-09-01 20:02:08 +0800846
Cheng-Yi Chiang17a25272014-11-28 19:05:13 +0800847 @returns: A boolean for compare result.
Cheng-Yi Chiang7e49fb82014-09-01 20:02:08 +0800848
849 @raises: NotImplementedError if file type is not raw.
Cheng-Yi Chiang17a25272014-11-28 19:05:13 +0800850 NotImplementedError if sampling rates of two data are not the same.
Cheng-Yi Chiang7e49fb82014-09-01 20:02:08 +0800851 """
852 if (golden_data_format['file_type'] != 'raw' or
853 test_data_format['file_type'] != 'raw'):
854 raise NotImplementedError('Only support raw data in compare_data.')
Cheng-Yi Chiang17a25272014-11-28 19:05:13 +0800855 if (golden_data_format['rate'] != test_data_format['rate']):
856 raise NotImplementedError(
857 'Only support comparing data with the same sampling rate')
Cheng-Yi Chiang7e49fb82014-09-01 20:02:08 +0800858 golden_data = audio_data.AudioRawData(
859 binary=golden_data_binary,
860 channel=golden_data_format['channel'],
861 sample_format=golden_data_format['sample_format'])
862 test_data = audio_data.AudioRawData(
863 binary=test_data_binary,
864 channel=test_data_format['channel'],
865 sample_format=test_data_format['sample_format'])
866 compare_results = []
867 for test_channel, golden_channel in enumerate(channel_map):
868 if golden_channel is None:
869 logging.info('Skipped channel %d', test_channel)
870 continue
871 test_data_one_channel = test_data.channel_data[test_channel]
872 golden_data_one_channel = golden_data.channel_data[golden_channel]
873 result_dict = dict(test_channel=test_channel,
874 golden_channel=golden_channel)
875 result_dict.update(
876 compare_one_channel_data(
Cheng-Yi Chiang17a25272014-11-28 19:05:13 +0800877 test_data_one_channel, test_data_format,
878 golden_data_one_channel, golden_data_format, method))
Cheng-Yi Chiang7e49fb82014-09-01 20:02:08 +0800879 compare_results.append(result_dict)
880 logging.info('compare_results: %r', compare_results)
881 return_value = False if not compare_results else True
882 for result in compare_results:
883 if not result['equal']:
884 logging.error(
885 'Failed on test channel %d and golden channel %d',
886 result['test_channel'], result['golden_channel'])
887 return_value = False
888 # Also checks best delay are exactly the same.
889 if method == 'correlation':
890 best_delays = set([result['best_delay'] for result in compare_results])
891 if len(best_delays) > 1:
892 logging.error('There are more than one best delay.')
893 return_value = False
894 return return_value
895
896
Owen Lin942e04d2014-01-09 14:16:59 +0800897class _base_rms_test(test.test):
898 """ Base class for all rms_test """
Owen Lin56050862013-12-09 11:42:51 +0800899
Owen Lin942e04d2014-01-09 14:16:59 +0800900 def postprocess(self):
901 super(_base_rms_test, self).postprocess()
Owen Lin56050862013-12-09 11:42:51 +0800902
Owen Lin942e04d2014-01-09 14:16:59 +0800903 # Sum up the number of failed constraints in each iteration
904 if sum(len(x) for x in self.failed_constraints):
905 generate_rms_postmortem()
906
907
908class chrome_rms_test(_base_rms_test):
909 """ Base test class for audio RMS test with Chrome.
910
911 The chrome instance can be accessed by self.chrome.
Owen Lin56050862013-12-09 11:42:51 +0800912 """
Owen Lin942e04d2014-01-09 14:16:59 +0800913 def warmup(self):
914 skip_devices_to_test('x86-mario')
915 super(chrome_rms_test, self).warmup()
916
Owen Lin56050862013-12-09 11:42:51 +0800917 # Not all client of this file using telemetry.
918 # Just do the import here for those who really need it.
919 from autotest_lib.client.common_lib.cros import chrome
Owen Lin942e04d2014-01-09 14:16:59 +0800920
921 self.chrome = chrome.Chrome()
922
923 # The audio configuration could be changed when we
924 # restart chrome.
Owen Lin56050862013-12-09 11:42:51 +0800925 try:
Owen Lin942e04d2014-01-09 14:16:59 +0800926 cras_rms_test_setup()
927 except Exception:
928 self.chrome.browser.Close()
Owen Lin56050862013-12-09 11:42:51 +0800929 raise
Owen Lin56050862013-12-09 11:42:51 +0800930
931
Owen Lin942e04d2014-01-09 14:16:59 +0800932 def cleanup(self, *args):
933 try:
934 self.chrome.browser.Close()
935 finally:
936 super(chrome_rms_test, self).cleanup()
Owen Lin56050862013-12-09 11:42:51 +0800937
Owen Lin942e04d2014-01-09 14:16:59 +0800938class cras_rms_test(_base_rms_test):
939 """ Base test class for CRAS audio RMS test."""
940
941 def warmup(self):
Rohit Makasana06446562014-01-03 11:39:03 -0800942 skip_devices_to_test('x86-mario')
Owen Lin942e04d2014-01-09 14:16:59 +0800943 super(cras_rms_test, self).warmup()
Owen Lin56050862013-12-09 11:42:51 +0800944 cras_rms_test_setup()
Owen Lin56050862013-12-09 11:42:51 +0800945
946
Owen Lin942e04d2014-01-09 14:16:59 +0800947class alsa_rms_test(_base_rms_test):
948 """ Base test class for ALSA audio RMS test."""
Owen Lin56050862013-12-09 11:42:51 +0800949
Owen Lin942e04d2014-01-09 14:16:59 +0800950 def warmup(self):
Rohit Makasana06446562014-01-03 11:39:03 -0800951 skip_devices_to_test('x86-mario')
Owen Lin942e04d2014-01-09 14:16:59 +0800952 super(alsa_rms_test, self).warmup()
953
Owen Lin56050862013-12-09 11:42:51 +0800954 # TODO(owenlin): Don't use CRAS for setup.
955 cras_rms_test_setup()
956
957 # CRAS does not apply the volume and capture gain to ALSA util
958 # streams are added. Do that to ensure the values have been set.
Owen Linbd415142013-12-18 10:38:58 +0800959 cras_utils.playback('/dev/zero', duration=0.1)
Owen Lin56050862013-12-09 11:42:51 +0800960 cras_utils.capture('/dev/null', duration=0.1)