blob: 269c279111b4ca5d11eab5aa5b08ac26356cf6f0 [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.
Owen Lin56050862013-12-09 11:42:51 +0800558 cras_utils.set_system_volume(_DEFAULT_PLAYBACK_VOLUME)
Cheng-Yi Chiang79b9ada2015-05-27 14:46:56 +0800559 cras_utils.set_selected_output_node_volume(_DEFAULT_PLAYBACK_VOLUME)
Owen Lin56050862013-12-09 11:42:51 +0800560
561 cras_utils.set_capture_gain(_DEFAULT_CAPTURE_GAIN)
562
563 cras_utils.set_system_mute(False)
564 cras_utils.set_capture_mute(False)
565
566
Owen Lin63b214d2014-01-07 16:40:14 +0800567def generate_rms_postmortem():
568 try:
569 logging.info('audio postmortem report')
570 log_loopback_dongle_status()
Cheng-Yi Chiang88fdf252015-04-08 01:02:59 -0700571 logging.info(get_audio_diagnostics())
Owen Lin942e04d2014-01-09 14:16:59 +0800572 except Exception:
Owen Lin63b214d2014-01-07 16:40:14 +0800573 logging.exception('Error while generating postmortem report')
574
575
Cheng-Yi Chiang88fdf252015-04-08 01:02:59 -0700576def get_audio_diagnostics():
577 """Gets audio diagnostic results.
578
579 @returns: a string containing diagnostic results.
580
581 """
582 return cmd_utils.execute([_AUDIO_DIAGNOSTICS_PATH], stdout=cmd_utils.PIPE)
583
584
Cheng-Yi Chiang7e49fb82014-09-01 20:02:08 +0800585def get_max_cross_correlation(signal_a, signal_b):
586 """Gets max cross-correlation and best time delay of two signals.
587
588 Computes cross-correlation function between two
589 signals and gets the maximum value and time delay.
590 The steps includes:
591 1. Compute cross-correlation function of X and Y and get Cxy.
592 The correlation function Cxy is an array where Cxy[k] is the
593 cross product of X and Y when Y is delayed by k.
594 Refer to manual of numpy.correlate for detail of correlation.
595 2. Find the maximum value C_max and index C_index in Cxy.
596 3. Compute L2 norm of X and Y to get norm(X) and norm(Y).
597 4. Divide C_max by norm(X)*norm(Y) to get max cross-correlation.
598
599 Max cross-correlation indicates the similarity of X and Y. The value
600 is 1 if X equals Y multiplied by a positive scalar.
601 The value is -1 if X equals Y multiplied by a negative scaler.
602 Any constant level shift will be regarded as distortion and will make
603 max cross-correlation value deviated from 1.
604 C_index is the best time delay of Y that make Y looks similar to X.
605 Refer to http://en.wikipedia.org/wiki/Cross-correlation.
606
607 @param signal_a: A list of numbers which contains the first signal.
608 @param signal_b: A list of numbers which contains the second signal.
609
610 @raises: ValueError if any number in signal_a or signal_b is not a float.
611 ValueError if norm of any array is less than _MINIMUM_NORM.
612
613 @returns: A tuple (correlation index, best delay). If there are more than
614 one best delay, just return the first one.
615 """
616 def check_list_contains_float(numbers):
617 """Checks the elements in a list are all float.
618
619 @param numbers: A list of numbers.
620
621 @raises: ValueError if there is any element which is not a float
622 in the list.
623 """
624 if any(not isinstance(x, float) for x in numbers):
625 raise ValueError('List contains number which is not a float')
626
627 check_list_contains_float(signal_a)
628 check_list_contains_float(signal_b)
629
630 norm_a = numpy.linalg.norm(signal_a)
631 norm_b = numpy.linalg.norm(signal_b)
632 logging.debug('norm_a: %f', norm_a)
633 logging.debug('norm_b: %f', norm_b)
634 if norm_a <= _MINIMUM_NORM or norm_b <= _MINIMUM_NORM:
635 raise ValueError('No meaningful data as norm is too small.')
636
637 correlation = numpy.correlate(signal_a, signal_b, 'full')
638 max_correlation = max(correlation)
639 best_delays = [i for i, j in enumerate(correlation) if j == max_correlation]
640 if len(best_delays) > 1:
641 logging.warning('There are more than one best delay: %r', best_delays)
642 return max_correlation / (norm_a * norm_b), best_delays[0]
643
644
645def trim_data(data, threshold=0):
646 """Trims a data by removing value that is too small in head and tail.
647
648 Removes elements in head and tail whose absolute value is smaller than
649 or equal to threshold.
650 E.g. trim_data([0.0, 0.1, 0.2, 0.3, 0.2, 0.1, 0.0], 0.2) =
651 ([0.2, 0.3, 0.2], 2)
652
653 @param data: A list of numbers.
654 @param threshold: The threshold to compare against.
655
656 @returns: A tuple (trimmed_data, valid_index), where valid_index is the
657 original index of the starting element in trimmed_data.
658 Returns ([], None) if there is no valid data.
659 """
660 indice_valid = [
661 i for i, j in enumerate(data) if abs(j) > threshold]
662 if not indice_valid:
663 logging.warning(
664 'There is no element with absolute value greater '
Cheng-Yi Chiang17a25272014-11-28 19:05:13 +0800665 'than threshold %f', threshold)
Cheng-Yi Chiang7e49fb82014-09-01 20:02:08 +0800666 return [], None
667 logging.debug('Start and end of indice_valid: %d, %d',
668 indice_valid[0], indice_valid[-1])
669 return data[indice_valid[0] : indice_valid[-1] + 1], indice_valid[0]
670
671
672def get_one_channel_correlation(test_data, golden_data):
673 """Gets max cross-correlation of test_data and golden_data.
674
675 Trims test data and compute the max cross-correlation against golden_data.
676 Signal can be trimmed because those zero values in the head and tail of
677 a signal will not affect correlation computation.
678
679 @param test_data: A list containing the data to compare against golden data.
680 @param golden_data: A list containing the golden data.
681
682 @returns: A tuple (max cross-correlation, best_delay) if data is valid.
683 Otherwise returns (None, None). Refer to docstring of
684 get_max_cross_correlation.
685 """
686 trimmed_test_data, start_trimmed_length = trim_data(test_data)
687
688 def to_float(samples):
689 """Casts elements in the list to float.
690
691 @param samples: A list of numbers.
692
693 @returns: A list of original numbers casted to float.
694 """
695 samples_float = [float(x) for x in samples]
696 return samples_float
697
698 max_cross_correlation, best_delay = get_max_cross_correlation(
699 to_float(golden_data),
700 to_float(trimmed_test_data))
701
702 # Adds back the trimmed length in the head.
703 if max_cross_correlation:
704 return max_cross_correlation, best_delay + start_trimmed_length
705 else:
706 return None, None
707
708
709def compare_one_channel_correlation(test_data, golden_data):
710 """Compares two one-channel data by correlation.
711
712 @param test_data: A list containing the data to compare against golden data.
713 @param golden_data: A list containing the golden data.
714
715 @returns: A dict containing:
716 index: The index of similarity where 1 means they are different
717 only by a positive scale.
718 best_delay: The best delay of test data in relative to golden
719 data.
720 equal: A bool containing comparing result.
721 """
722 result_dict = dict()
723 max_cross_correlation, best_delay = get_one_channel_correlation(
724 test_data, golden_data)
725 result_dict['index'] = max_cross_correlation
726 result_dict['best_delay'] = best_delay
727 result_dict['equal'] = True if (
728 max_cross_correlation and
729 max_cross_correlation > _CORRELATION_INDEX_THRESHOLD) else False
730 logging.debug('result_dict: %r', result_dict)
731 return result_dict
732
733
Cheng-Yi Chiang17a25272014-11-28 19:05:13 +0800734def get_one_channel_stat(data, data_format):
735 """Gets statistic information of data.
736
737 @param data: A list containing one channel data.
738 @param data_format: A dict containing data format of data.
739
740 @return: The sox stat parsed result. An object containing
741 sameple_count: An int. Samples read.
742 length: A float. Length in seconds.
743 rms: A float. RMS amplitude.
744 rough_frequency: A float. Rough frequency.
745 """
746 if not data:
747 raise ValueError('Data is empty. Can not get stat')
748 raw_data = audio_data.AudioRawData(
749 binary=None, channel=1,
750 sample_format=data_format['sample_format'])
751 raw_data.copy_channel_data([data])
Cheng-Yi Chiang1a642ea2015-05-05 11:43:57 -0700752 with tempfile.NamedTemporaryFile() as raw_data_file:
753 raw_data_path = raw_data_file.name
754 raw_data.write_to_file(raw_data_path)
Cheng-Yi Chiang17a25272014-11-28 19:05:13 +0800755
Cheng-Yi Chiang1a642ea2015-05-05 11:43:57 -0700756 bits = 8 * (audio_data.SAMPLE_FORMATS[
757 data_format['sample_format']]['size_bytes'])
758 stat = sox_utils.get_stat(raw_data_path, channels=1, bits=bits,
759 rate=data_format['rate'])
760 return stat
Cheng-Yi Chiang17a25272014-11-28 19:05:13 +0800761
762
763def compare_one_channel_frequency(test_data, test_data_format,
764 golden_data, golden_data_format):
765 """Compares two one-channel data by frequency.
766
767 @param test_data: A list containing the data to compare against golden data.
768 @param test_data_format: A dict containing data format of test data.
769 @param golden_data: A list containing the golden data.
770 @param golden_data_format: A dict containing data format of golden data.
771
772 @returns: A dict containing:
773 test_data_frequency: test data frequency.
774 golden_data_frequency: golden data frequency.
775 equal: A bool containing comparing result.
776
777 """
778 result_dict = dict()
779 golden_data_stat = get_one_channel_stat(golden_data, golden_data_format)
780 logging.info('Get golden data one channel stat: %s', golden_data_stat)
781 test_data_stat = get_one_channel_stat(test_data, test_data_format)
782 logging.info('Get test data one channel stat: %s', test_data_stat)
783
784 result_dict['golden_data_frequency'] = golden_data_stat.rough_frequency
785 result_dict['test_data_frequency'] = test_data_stat.rough_frequency
786 result_dict['equal'] = True if (
787 abs(result_dict['test_data_frequency'] -
788 result_dict['golden_data_frequency']) < _FREQUENCY_DIFF_THRESHOLD
789 ) else False
790 if test_data_stat.rms < _MEANINGFUL_RMS_THRESHOLD:
791 logging.error('Recorded RMS %f is too small to be meaningful.',
792 test_data_stat.rms)
793 result_dict['equal'] = False
794 logging.debug('result_dict: %r', result_dict)
795 return result_dict
796
797
798def compare_one_channel_data(test_data, test_data_format,
799 golden_data, golden_data_format, method):
Cheng-Yi Chiang7e49fb82014-09-01 20:02:08 +0800800 """Compares two one-channel data.
801
802 @param test_data: A list containing the data to compare against golden data.
Cheng-Yi Chiang17a25272014-11-28 19:05:13 +0800803 @param test_data_format: The data format of test data.
Cheng-Yi Chiang7e49fb82014-09-01 20:02:08 +0800804 @param golden_data: A list containing the golden data.
Cheng-Yi Chiang17a25272014-11-28 19:05:13 +0800805 @param golden_data_format: The data format of golden data.
Cheng-Yi Chiang7e49fb82014-09-01 20:02:08 +0800806 @param method: The comparing method. Currently only 'correlation' is
807 supported.
808
809 @returns: A dict containing:
810 index: The index of similarity where 1 means they are different
811 only by a positive scale.
812 best_delay: The best delay of test data in relative to golden
813 data.
814 equal: A bool containing comparing result.
815
816 @raises: NotImplementedError if method is not supported.
817 """
818 if method == 'correlation':
819 return compare_one_channel_correlation(test_data, golden_data)
Cheng-Yi Chiang17a25272014-11-28 19:05:13 +0800820 if method == 'frequency':
821 return compare_one_channel_frequency(
822 test_data, test_data_format, golden_data, golden_data_format)
Cheng-Yi Chiang7e49fb82014-09-01 20:02:08 +0800823 raise NotImplementedError('method %s is not implemented' % method)
824
825
826def compare_data(golden_data_binary, golden_data_format,
827 test_data_binary, test_data_format,
828 channel_map, method):
829 """Compares two raw data.
830
831 @param golden_data_binary: The binary containing golden data.
832 @param golden_data_format: The data format of golden data.
833 @param test_data_binary: The binary containing test data.
834 @param test_data_format: The data format of test data.
835 @param channel_map: A list containing channel mapping.
836 E.g. [1, 0, None, None, None, None, None, None] means
837 channel 0 of test data should map to channel 1 of
838 golden data. Channel 1 of test data should map to
839 channel 0 of golden data. Channel 2 to 7 of test data
840 should be skipped.
Cheng-Yi Chiang17a25272014-11-28 19:05:13 +0800841 @param method: The method to compare data. Use 'correlation' to compare
842 general data. Use 'frequency' to compare data containing
843 sine wave.
Cheng-Yi Chiang7e49fb82014-09-01 20:02:08 +0800844
Cheng-Yi Chiang17a25272014-11-28 19:05:13 +0800845 @returns: A boolean for compare result.
Cheng-Yi Chiang7e49fb82014-09-01 20:02:08 +0800846
847 @raises: NotImplementedError if file type is not raw.
Cheng-Yi Chiang17a25272014-11-28 19:05:13 +0800848 NotImplementedError if sampling rates of two data are not the same.
Cheng-Yi Chiang7e49fb82014-09-01 20:02:08 +0800849 """
850 if (golden_data_format['file_type'] != 'raw' or
851 test_data_format['file_type'] != 'raw'):
852 raise NotImplementedError('Only support raw data in compare_data.')
Cheng-Yi Chiang17a25272014-11-28 19:05:13 +0800853 if (golden_data_format['rate'] != test_data_format['rate']):
854 raise NotImplementedError(
855 'Only support comparing data with the same sampling rate')
Cheng-Yi Chiang7e49fb82014-09-01 20:02:08 +0800856 golden_data = audio_data.AudioRawData(
857 binary=golden_data_binary,
858 channel=golden_data_format['channel'],
859 sample_format=golden_data_format['sample_format'])
860 test_data = audio_data.AudioRawData(
861 binary=test_data_binary,
862 channel=test_data_format['channel'],
863 sample_format=test_data_format['sample_format'])
864 compare_results = []
865 for test_channel, golden_channel in enumerate(channel_map):
866 if golden_channel is None:
867 logging.info('Skipped channel %d', test_channel)
868 continue
869 test_data_one_channel = test_data.channel_data[test_channel]
870 golden_data_one_channel = golden_data.channel_data[golden_channel]
871 result_dict = dict(test_channel=test_channel,
872 golden_channel=golden_channel)
873 result_dict.update(
874 compare_one_channel_data(
Cheng-Yi Chiang17a25272014-11-28 19:05:13 +0800875 test_data_one_channel, test_data_format,
876 golden_data_one_channel, golden_data_format, method))
Cheng-Yi Chiang7e49fb82014-09-01 20:02:08 +0800877 compare_results.append(result_dict)
878 logging.info('compare_results: %r', compare_results)
879 return_value = False if not compare_results else True
880 for result in compare_results:
881 if not result['equal']:
882 logging.error(
883 'Failed on test channel %d and golden channel %d',
884 result['test_channel'], result['golden_channel'])
885 return_value = False
886 # Also checks best delay are exactly the same.
887 if method == 'correlation':
888 best_delays = set([result['best_delay'] for result in compare_results])
889 if len(best_delays) > 1:
890 logging.error('There are more than one best delay.')
891 return_value = False
892 return return_value
893
894
Owen Lin942e04d2014-01-09 14:16:59 +0800895class _base_rms_test(test.test):
896 """ Base class for all rms_test """
Owen Lin56050862013-12-09 11:42:51 +0800897
Owen Lin942e04d2014-01-09 14:16:59 +0800898 def postprocess(self):
899 super(_base_rms_test, self).postprocess()
Owen Lin56050862013-12-09 11:42:51 +0800900
Owen Lin942e04d2014-01-09 14:16:59 +0800901 # Sum up the number of failed constraints in each iteration
902 if sum(len(x) for x in self.failed_constraints):
903 generate_rms_postmortem()
904
905
906class chrome_rms_test(_base_rms_test):
907 """ Base test class for audio RMS test with Chrome.
908
909 The chrome instance can be accessed by self.chrome.
Owen Lin56050862013-12-09 11:42:51 +0800910 """
Owen Lin942e04d2014-01-09 14:16:59 +0800911 def warmup(self):
912 skip_devices_to_test('x86-mario')
913 super(chrome_rms_test, self).warmup()
914
Owen Lin56050862013-12-09 11:42:51 +0800915 # Not all client of this file using telemetry.
916 # Just do the import here for those who really need it.
917 from autotest_lib.client.common_lib.cros import chrome
Owen Lin942e04d2014-01-09 14:16:59 +0800918
919 self.chrome = chrome.Chrome()
920
921 # The audio configuration could be changed when we
922 # restart chrome.
Owen Lin56050862013-12-09 11:42:51 +0800923 try:
Owen Lin942e04d2014-01-09 14:16:59 +0800924 cras_rms_test_setup()
925 except Exception:
926 self.chrome.browser.Close()
Owen Lin56050862013-12-09 11:42:51 +0800927 raise
Owen Lin56050862013-12-09 11:42:51 +0800928
929
Owen Lin942e04d2014-01-09 14:16:59 +0800930 def cleanup(self, *args):
931 try:
932 self.chrome.browser.Close()
933 finally:
934 super(chrome_rms_test, self).cleanup()
Owen Lin56050862013-12-09 11:42:51 +0800935
Owen Lin942e04d2014-01-09 14:16:59 +0800936class cras_rms_test(_base_rms_test):
937 """ Base test class for CRAS audio RMS test."""
938
939 def warmup(self):
Rohit Makasana06446562014-01-03 11:39:03 -0800940 skip_devices_to_test('x86-mario')
Owen Lin942e04d2014-01-09 14:16:59 +0800941 super(cras_rms_test, self).warmup()
Owen Lin56050862013-12-09 11:42:51 +0800942 cras_rms_test_setup()
Owen Lin56050862013-12-09 11:42:51 +0800943
944
Owen Lin942e04d2014-01-09 14:16:59 +0800945class alsa_rms_test(_base_rms_test):
946 """ Base test class for ALSA audio RMS test."""
Owen Lin56050862013-12-09 11:42:51 +0800947
Owen Lin942e04d2014-01-09 14:16:59 +0800948 def warmup(self):
Rohit Makasana06446562014-01-03 11:39:03 -0800949 skip_devices_to_test('x86-mario')
Owen Lin942e04d2014-01-09 14:16:59 +0800950 super(alsa_rms_test, self).warmup()
951
Owen Lin56050862013-12-09 11:42:51 +0800952 # TODO(owenlin): Don't use CRAS for setup.
953 cras_rms_test_setup()
954
955 # CRAS does not apply the volume and capture gain to ALSA util
956 # streams are added. Do that to ensure the values have been set.
Owen Linbd415142013-12-18 10:38:58 +0800957 cras_utils.playback('/dev/zero', duration=0.1)
Owen Lin56050862013-12-09 11:42:51 +0800958 cras_utils.capture('/dev/null', duration=0.1)