blob: 6fe5c5b373d7f279d28fb733d38a22654d255347 [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
Hsin-Yu Chaof6bbc6c2013-08-20 19:22:05 +080060
Hsin-Yu Chao30561af2013-09-06 14:03:56 +080061def set_mixer_controls(mixer_settings={}, card='0'):
62 '''
63 Sets all mixer controls listed in the mixer settings on card.
64
65 @param mixer_settings: Mixer settings to set.
66 @param card: Index of audio card to set mixer settings for.
67 '''
68 logging.info('Setting mixer control values on %s', card)
69 for item in mixer_settings:
70 logging.info('Setting %s to %s on card %s',
71 item['name'], item['value'], card)
72 cmd = 'amixer -c %s cset name=%s %s'
73 cmd = cmd % (card, item['name'], item['value'])
74 try:
75 utils.system(cmd)
76 except error.CmdError:
77 # A card is allowed not to support all the controls, so don't
78 # fail the test here if we get an error.
79 logging.info('amixer command failed: %s', cmd)
80
81def set_volume_levels(volume, capture):
82 '''
83 Sets the volume and capture gain through cras_test_client
84
85 @param volume: The playback volume to set.
86 @param capture: The capture gain to set.
87 '''
88 logging.info('Setting volume level to %d', volume)
89 utils.system('/usr/bin/cras_test_client --volume %d' % volume)
90 logging.info('Setting capture gain to %d', capture)
91 utils.system('/usr/bin/cras_test_client --capture_gain %d' % capture)
92 utils.system('/usr/bin/cras_test_client --dump_server_info')
93 utils.system('/usr/bin/cras_test_client --mute 0')
94 utils.system('amixer -c 0 contents')
95
96def loopback_latency_check(**args):
97 '''
98 Checks loopback latency.
99
100 @param args: additional arguments for loopback_latency.
101
102 @return A tuple containing measured and reported latency in uS.
103 Return None if no audio detected.
104 '''
105 noise_threshold = str(args['n']) if args.has_key('n') else '400'
106
107 cmd = '%s -n %s' % (LOOPBACK_LATENCY_PATH, noise_threshold)
108
109 output = utils.system_output(cmd, retain_output=True)
110
111 # Sleep for a short while to make sure device is not busy anymore
112 # after called loopback_latency.
113 time.sleep(.1)
114
115 measured_latency = None
116 reported_latency = None
117 for line in output.split('\n'):
118 match = re.search(_MEASURED_LATENCY_RE, line, re.I)
119 if match:
120 measured_latency = int(match.group(1))
121 continue
122 match = re.search(_REPORTED_LATENCY_RE, line, re.I)
123 if match:
124 reported_latency = int(match.group(1))
125 continue
126 if re.search(_AUDIO_NOT_FOUND_RE, line, re.I):
127 return None
128 if measured_latency and reported_latency:
129 return (measured_latency, reported_latency)
130 else:
131 # Should not reach here, just in case.
132 return None
133
134def get_mixer_jack_status(jack_reg_exp):
135 '''
136 Gets the mixer jack status.
137
138 @param jack_reg_exp: The regular expression to match jack control name.
139
140 @return None if the control does not exist, return True if jack control
141 is detected plugged, return False otherwise.
142 '''
143 output = utils.system_output('amixer -c0 controls', retain_output=True)
144 numid = None
145 for line in output.split('\n'):
146 m = jack_reg_exp.match(line)
147 if m:
148 numid = m.group(1)
149 break
150
151 # Proceed only when matched numid is not empty.
152 if numid:
153 output = utils.system_output('amixer -c0 cget numid=%s' % numid)
154 for line in output.split('\n'):
155 if _JACK_VALUE_ON_RE.match(line):
156 return True
157 return False
158 else:
159 return None
160
161def get_hp_jack_status():
162 '''Gets the status of headphone jack'''
163 status = get_mixer_jack_status(_HP_JACK_CONTROL_RE)
164 if status is not None:
165 return status
166
167 # When headphone jack is not found in amixer, lookup input devices
168 # instead.
169 #
170 # TODO(hychao): Check hp/mic jack status dynamically from evdev. And
171 # possibly replace the existing check using amixer.
172 for evdev in glob('/dev/input/event*'):
173 device = InputDevice(evdev)
174 if device.is_hp_jack():
175 return device.get_headphone_insert()
176 else:
177 return None
178
179def get_mic_jack_status():
180 '''Gets the status of mic jack'''
181 status = get_mixer_jack_status(_MIC_JACK_CONTROL_RE)
182 if status is not None:
183 return status
184
185 # When mic jack is not found in amixer, lookup input devices instead.
186 for evdev in glob('/dev/input/event*'):
187 device = InputDevice(evdev)
188 if device.is_mic_jack():
189 return device.get_microphone_insert()
190 else:
191 return None
192
Owen Lin56050862013-12-09 11:42:51 +0800193def log_loopback_dongle_status():
Hsin-Yu Chao30561af2013-09-06 14:03:56 +0800194 '''
Owen Lin56050862013-12-09 11:42:51 +0800195 Log the status of the loopback dongle to make sure it is equipped correctly.
Hsin-Yu Chao30561af2013-09-06 14:03:56 +0800196 '''
Owen Lin56050862013-12-09 11:42:51 +0800197 dongle_status_ok = True
198
Hsin-Yu Chao30561af2013-09-06 14:03:56 +0800199 # Check Mic Jack
200 mic_jack_status = get_mic_jack_status()
Owen Lin56050862013-12-09 11:42:51 +0800201 logging.info('Mic jack status: %s', mic_jack_status)
Owen Lin14bec7a2014-04-21 11:39:40 +0800202 dongle_status_ok &= bool(mic_jack_status)
Hsin-Yu Chao30561af2013-09-06 14:03:56 +0800203
204 # Check Headphone Jack
205 hp_jack_status = get_hp_jack_status()
Owen Lin56050862013-12-09 11:42:51 +0800206 logging.info('Headphone jack status: %s', hp_jack_status)
Owen Lin14bec7a2014-04-21 11:39:40 +0800207 dongle_status_ok &= bool(hp_jack_status)
Hsin-Yu Chao30561af2013-09-06 14:03:56 +0800208
209 # Use latency check to test if audio can be captured through dongle.
210 # We only want to know the basic function of dongle, so no need to
211 # assert the latency accuracy here.
212 latency = loopback_latency_check(n=4000)
213 if latency:
214 logging.info('Got latency measured %d, reported %d',
215 latency[0], latency[1])
216 else:
Owen Lin56050862013-12-09 11:42:51 +0800217 logging.info('Latency check fail.')
218 dongle_status_ok = False
Hsin-Yu Chao30561af2013-09-06 14:03:56 +0800219
Owen Lin56050862013-12-09 11:42:51 +0800220 logging.info('audio loopback dongle test: %s',
221 'PASS' if dongle_status_ok else 'FAIL')
Hsin-Yu Chao30561af2013-09-06 14:03:56 +0800222
223# Functions to test audio palyback.
224def play_sound(duration_seconds=None, audio_file_path=None):
225 '''
226 Plays a sound file found at |audio_file_path| for |duration_seconds|.
227
228 If |audio_file_path|=None, plays a default audio file.
229 If |duration_seconds|=None, plays audio file in its entirety.
230
231 @param duration_seconds: Duration to play sound.
232 @param audio_file_path: Path to the audio file.
233 '''
234 if not audio_file_path:
235 audio_file_path = '/usr/local/autotest/cros/audio/sine440.wav'
236 duration_arg = ('-d %d' % duration_seconds) if duration_seconds else ''
237 utils.system('aplay %s %s' % (duration_arg, audio_file_path))
238
239def get_play_sine_args(channel, odev='default', freq=1000, duration=10,
240 sample_size=16):
241 '''Gets the command args to generate a sine wav to play to odev.
242
243 @param channel: 0 for left, 1 for right; otherwize, mono.
244 @param odev: alsa output device.
245 @param freq: frequency of the generated sine tone.
246 @param duration: duration of the generated sine tone.
247 @param sample_size: output audio sample size. Default to 16.
248 '''
249 cmdargs = [SOX_PATH, '-b', str(sample_size), '-n', '-t', 'alsa',
250 odev, 'synth', str(duration)]
251 if channel == 0:
252 cmdargs += ['sine', str(freq), 'sine', '0']
253 elif channel == 1:
254 cmdargs += ['sine', '0', 'sine', str(freq)]
255 else:
256 cmdargs += ['sine', str(freq)]
257
258 return cmdargs
259
260def play_sine(channel, odev='default', freq=1000, duration=10,
261 sample_size=16):
262 '''Generates a sine wave and plays to odev.
263
264 @param channel: 0 for left, 1 for right; otherwize, mono.
265 @param odev: alsa output device.
266 @param freq: frequency of the generated sine tone.
267 @param duration: duration of the generated sine tone.
268 @param sample_size: output audio sample size. Default to 16.
269 '''
270 cmdargs = get_play_sine_args(channel, odev, freq, duration, sample_size)
271 utils.system(' '.join(cmdargs))
272
Hsin-Yu Chao22bebdc2013-09-06 16:32:51 +0800273# Functions to compose customized sox command, execute it and process the
274# output of sox command.
275def get_sox_mixer_cmd(infile, channel,
276 num_channels=_DEFAULT_NUM_CHANNELS,
277 sox_format=_DEFAULT_SOX_FORMAT):
278 '''Gets sox mixer command to reduce channel.
279
280 @param infile: Input file name.
281 @param channel: The selected channel to take effect.
282 @param num_channels: The number of total channels to test.
283 @param sox_format: Format to generate sox command.
284 '''
285 # Build up a pan value string for the sox command.
286 if channel == 0:
287 pan_values = '1'
288 else:
289 pan_values = '0'
290 for pan_index in range(1, num_channels):
291 if channel == pan_index:
292 pan_values = '%s%s' % (pan_values, ',1')
293 else:
294 pan_values = '%s%s' % (pan_values, ',0')
295
296 return '%s -c 2 %s %s -c 1 %s - mixer %s' % (SOX_PATH,
297 sox_format, infile, sox_format, pan_values)
298
299def sox_stat_output(infile, channel,
300 num_channels=_DEFAULT_NUM_CHANNELS,
301 sox_format=_DEFAULT_SOX_FORMAT):
302 '''Executes sox stat command.
303
304 @param infile: Input file name.
305 @param channel: The selected channel.
306 @param num_channels: The number of total channels to test.
307 @param sox_format: Format to generate sox command.
308
309 @return The output of sox stat command
310 '''
311 sox_mixer_cmd = get_sox_mixer_cmd(infile, channel,
312 num_channels, sox_format)
313 stat_cmd = '%s -c 1 %s - -n stat 2>&1' % (SOX_PATH, sox_format)
314 sox_cmd = '%s | %s' % (sox_mixer_cmd, stat_cmd)
315 return utils.system_output(sox_cmd, retain_output=True)
316
317def get_audio_rms(sox_output):
318 '''Gets the audio RMS value from sox stat output
319
320 @param sox_output: Output of sox stat command.
321
322 @return The RMS value parsed from sox stat output.
323 '''
324 for rms_line in sox_output.split('\n'):
325 m = _SOX_RMS_AMPLITUDE_RE.match(rms_line)
326 if m is not None:
327 return float(m.group(1))
328
329def get_rough_freq(sox_output):
330 '''Gets the rough audio frequency from sox stat output
331
332 @param sox_output: Output of sox stat command.
333
334 @return The rough frequency value parsed from sox stat output.
335 '''
336 for rms_line in sox_output.split('\n'):
337 m = _SOX_ROUGH_FREQ_RE.match(rms_line)
338 if m is not None:
339 return int(m.group(1))
340
341def check_audio_rms(sox_output, sox_threshold=_DEFAULT_SOX_RMS_THRESHOLD):
342 """Checks if the calculated RMS value is expected.
343
344 @param sox_output: The output from sox stat command.
345 @param sox_threshold: The threshold to test RMS value against.
346
347 @raises error.TestError if RMS amplitude can't be parsed.
348 @raises error.TestFail if the RMS amplitude of the recording isn't above
349 the threshold.
350 """
351 rms_val = get_audio_rms(sox_output)
352
353 # In case we don't get a valid RMS value.
354 if rms_val is None:
355 raise error.TestError(
356 'Failed to generate an audio RMS value from playback.')
357
358 logging.info('Got audio RMS value of %f. Minimum pass is %f.',
359 rms_val, sox_threshold)
360 if rms_val < sox_threshold:
361 raise error.TestFail(
362 'Audio RMS value %f too low. Minimum pass is %f.' %
363 (rms_val, sox_threshold))
364
365def noise_reduce_file(in_file, noise_file, out_file,
366 sox_format=_DEFAULT_SOX_FORMAT):
367 '''Runs the sox command to noise-reduce in_file using
368 the noise profile from noise_file.
369
370 @param in_file: The file to noise reduce.
371 @param noise_file: The file containing the noise profile.
372 This can be created by recording silence.
373 @param out_file: The file contains the noise reduced sound.
374 @param sox_format: The sox format to generate sox command.
375 '''
376 prof_cmd = '%s -c 2 %s %s -n noiseprof' % (SOX_PATH,
377 sox_format, noise_file)
378 reduce_cmd = ('%s -c 2 %s %s -c 2 %s %s noisered' %
379 (SOX_PATH, sox_format, in_file, sox_format, out_file))
380 utils.system('%s | %s' % (prof_cmd, reduce_cmd))
381
Hsin-Yu Chao4be66782013-09-06 13:45:54 +0800382def record_sample(tmpfile, record_command=_DEFAULT_REC_COMMAND):
383 '''Records a sample from the default input device.
384
385 @param tmpfile: The file to record to.
386 @param record_command: The command to record audio.
387 '''
388 utils.system('%s %s' % (record_command, tmpfile))
389
Hsin-Yu Chao4be66782013-09-06 13:45:54 +0800390def create_wav_file(wav_dir, prefix=""):
391 '''Creates a unique name for wav file.
Adrian Li689d3ff2013-07-15 15:11:06 -0700392
Hsin-Yu Chao4be66782013-09-06 13:45:54 +0800393 The created file name will be preserved in autotest result directory
394 for future analysis.
395
396 @param prefix: specified file name prefix.
Hsinyu Chao4b8300e2011-11-15 13:07:32 -0800397 '''
Hsin-Yu Chao4be66782013-09-06 13:45:54 +0800398 filename = "%s-%s.wav" % (prefix, time.time())
399 return os.path.join(wav_dir, filename)
400
Owen Lin66cd9de2013-11-08 10:26:49 +0800401def run_in_parallel(*funs):
402 threads = []
403 for f in funs:
404 t = threading.Thread(target=f)
405 t.start()
406 threads.append(t)
407
408 for t in threads:
409 t.join()
410
Hsin-Yu Chao4be66782013-09-06 13:45:54 +0800411def loopback_test_channels(noise_file_name, wav_dir,
Owen Lin66cd9de2013-11-08 10:26:49 +0800412 playback_callback=None,
Hsin-Yu Chao4be66782013-09-06 13:45:54 +0800413 check_recorded_callback=check_audio_rms,
414 preserve_test_file=True,
415 num_channels = _DEFAULT_NUM_CHANNELS,
Owen Lin66cd9de2013-11-08 10:26:49 +0800416 record_callback=record_sample,
417 mix_callback=None):
Hsin-Yu Chao4be66782013-09-06 13:45:54 +0800418 '''Tests loopback on all channels.
419
420 @param noise_file_name: Name of the file contains pre-recorded noise.
Owen Lin66cd9de2013-11-08 10:26:49 +0800421 @param playback_callback: The callback to do the playback for
Hsin-Yu Chao4be66782013-09-06 13:45:54 +0800422 one channel.
Owen Lin66cd9de2013-11-08 10:26:49 +0800423 @param record_callback: The callback to do the recording.
Hsin-Yu Chao4be66782013-09-06 13:45:54 +0800424 @param check_recorded_callback: The callback to check recorded file.
425 @param preserve_test_file: Retain the recorded files for future debugging.
Hsinyu Chao4b8300e2011-11-15 13:07:32 -0800426 '''
Hsin-Yu Chao4be66782013-09-06 13:45:54 +0800427 for channel in xrange(num_channels):
Hsin-Yu Chao4be66782013-09-06 13:45:54 +0800428 record_file_name = create_wav_file(wav_dir,
429 "record-%d" % channel)
Owen Lin66cd9de2013-11-08 10:26:49 +0800430 functions = [lambda: record_callback(record_file_name)]
Hsinyu Chao4b8300e2011-11-15 13:07:32 -0800431
Owen Lin66cd9de2013-11-08 10:26:49 +0800432 if playback_callback:
433 functions.append(lambda: playback_callback(channel))
Hsinyu Chaof80337a2012-04-07 18:02:29 +0800434
Owen Lin66cd9de2013-11-08 10:26:49 +0800435 if mix_callback:
436 mix_file_name = create_wav_file(wav_dir, "mix-%d" % channel)
437 functions.append(lambda: mix_callback(mix_file_name))
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +0800438
Owen Lin66cd9de2013-11-08 10:26:49 +0800439 run_in_parallel(*functions)
440
441 if mix_callback:
Hsin-Yu Chao4be66782013-09-06 13:45:54 +0800442 sox_output_mix = sox_stat_output(mix_file_name, channel)
443 rms_val_mix = get_audio_rms(sox_output_mix)
444 logging.info('Got mixed audio RMS value of %f.', rms_val_mix)
Adrian Li689d3ff2013-07-15 15:11:06 -0700445
Hsin-Yu Chao4be66782013-09-06 13:45:54 +0800446 sox_output_record = sox_stat_output(record_file_name, channel)
447 rms_val_record = get_audio_rms(sox_output_record)
448 logging.info('Got recorded audio RMS value of %f.', rms_val_record)
Adrian Li689d3ff2013-07-15 15:11:06 -0700449
Owen Lin66cd9de2013-11-08 10:26:49 +0800450 reduced_file_name = create_wav_file(wav_dir,
451 "reduced-%d" % channel)
Hsin-Yu Chao4be66782013-09-06 13:45:54 +0800452 noise_reduce_file(record_file_name, noise_file_name,
453 reduced_file_name)
Hsinyu Chao2a7e2f22012-04-18 16:46:43 +0800454
Owen Lin66cd9de2013-11-08 10:26:49 +0800455 sox_output_reduced = sox_stat_output(reduced_file_name, channel)
Adrian Li689d3ff2013-07-15 15:11:06 -0700456
Hsin-Yu Chao4be66782013-09-06 13:45:54 +0800457 if not preserve_test_file:
458 os.unlink(reduced_file_name)
459 os.unlink(record_file_name)
Owen Lin66cd9de2013-11-08 10:26:49 +0800460 if mix_callback:
Hsin-Yu Chao4be66782013-09-06 13:45:54 +0800461 os.unlink(mix_file_name)
Adrian Li689d3ff2013-07-15 15:11:06 -0700462
Hsin-Yu Chao4be66782013-09-06 13:45:54 +0800463 check_recorded_callback(sox_output_reduced)
Owen Lindae7a0d2013-12-05 13:34:06 +0800464
465
466def get_channel_sox_stat(
Owen Lin2013e462013-12-05 17:54:42 +0800467 input_audio, channel_index, channels=2, bits=16, rate=48000):
Owen Lindae7a0d2013-12-05 13:34:06 +0800468 """Gets the sox stat info of the selected channel in the input audio file.
469
470 @param input_audio: The input audio file to be analyzed.
471 @param channel_index: The index of the channel to be analyzed.
472 (1 for the first channel).
473 @param channels: The number of channels in the input audio.
474 @param bits: The number of bits of each audio sample.
475 @param rate: The sampling rate.
476 """
477 if channel_index <= 0 or channel_index > channels:
478 raise ValueError('incorrect channel_indexi: %d' % channel_index)
479
480 if channels == 1:
Owen Lin4a154c42013-12-26 11:03:20 +0800481 return sox_utils.get_stat(
482 input_audio, channels=channels, bits=bits, rate=rate)
Owen Lindae7a0d2013-12-05 13:34:06 +0800483
484 p1 = cmd_utils.popen(
485 sox_utils.extract_channel_cmd(
486 input_audio, '-', channel_index,
487 channels=channels, bits=bits, rate=rate),
Owen Linad6610a2013-12-13 11:20:48 +0800488 stdout=cmd_utils.PIPE)
Owen Lindae7a0d2013-12-05 13:34:06 +0800489 p2 = cmd_utils.popen(
490 sox_utils.stat_cmd('-', channels=1, bits=bits, rate=rate),
Owen Linad6610a2013-12-13 11:20:48 +0800491 stdin=p1.stdout, stderr=cmd_utils.PIPE)
Owen Lindae7a0d2013-12-05 13:34:06 +0800492 stat_output = p2.stderr.read()
493 cmd_utils.wait_and_check_returncode(p1, p2)
494 return sox_utils.parse_stat_output(stat_output)
495
496
Owen Lin942e04d2014-01-09 14:16:59 +0800497def get_rms(input_audio, channels=1, bits=16, rate=48000):
498 """Gets the RMS values of all channels of the input audio.
Owen Lin5bba6c02013-12-13 14:17:08 +0800499
Owen Lin5cbf5c32013-12-13 15:13:37 +0800500 @param input_audio: The input audio file to be checked.
Owen Lin5bba6c02013-12-13 14:17:08 +0800501 @param channels: The number of channels in the input audio.
502 @param bits: The number of bits of each audio sample.
503 @param rate: The sampling rate.
Owen Lin5bba6c02013-12-13 14:17:08 +0800504 """
505 stats = [get_channel_sox_stat(
506 input_audio, i + 1, channels=channels, bits=bits,
507 rate=rate) for i in xrange(channels)]
508
509 logging.info('sox stat: %s', [str(s) for s in stats])
Owen Lin942e04d2014-01-09 14:16:59 +0800510 return [s.rms for s in stats]
Owen Lin5bba6c02013-12-13 14:17:08 +0800511
512
Owen Lin942e04d2014-01-09 14:16:59 +0800513def reduce_noise_and_get_rms(
514 input_audio, noise_file, channels=1, bits=16, rate=48000):
515 """Reduces noise in the input audio by the given noise file and then gets
Owen Lin5cbf5c32013-12-13 15:13:37 +0800516 the RMS values of all channels of the input audio.
Owen Lin5bba6c02013-12-13 14:17:08 +0800517
518 @param input_audio: The input audio file to be analyzed.
519 @param noise_file: The noise file used to reduce noise in the input audio.
Owen Lin5bba6c02013-12-13 14:17:08 +0800520 @param channels: The number of channels in the input audio.
521 @param bits: The number of bits of each audio sample.
522 @param rate: The sampling rate.
Owen Lin5bba6c02013-12-13 14:17:08 +0800523 """
Owen Lindae7a0d2013-12-05 13:34:06 +0800524 with tempfile.NamedTemporaryFile() as reduced_file:
525 p1 = cmd_utils.popen(
526 sox_utils.noise_profile_cmd(
527 noise_file, '-', channels=channels, bits=bits,
528 rate=rate),
Owen Linad6610a2013-12-13 11:20:48 +0800529 stdout=cmd_utils.PIPE)
Owen Lindae7a0d2013-12-05 13:34:06 +0800530 p2 = cmd_utils.popen(
531 sox_utils.noise_reduce_cmd(
532 input_audio, reduced_file.name, '-',
533 channels=channels, bits=bits, rate=rate),
534 stdin=p1.stdout)
535 cmd_utils.wait_and_check_returncode(p1, p2)
Owen Lin942e04d2014-01-09 14:16:59 +0800536 return get_rms(reduced_file.name, channels, bits, rate)
Owen Lin56050862013-12-09 11:42:51 +0800537
538
Rohit Makasana06446562014-01-03 11:39:03 -0800539def skip_devices_to_test(*boards):
540 """Devices to skip due to hardware or test compatibility issues."""
541 # TODO(scottz): Remove this when crbug.com/220147 is fixed.
542 dut_board = utils.get_current_board()
543 if dut_board in boards:
544 raise error.TestNAError('This test is not available on %s' % dut_board)
545
546
Owen Lin56050862013-12-09 11:42:51 +0800547def cras_rms_test_setup():
548 """ Setups for the cras_rms_tests.
549
550 To make sure the line_out-to-mic_in path is all green.
551 """
552 # TODO(owenlin): Now, the nodes are choosed by chrome.
553 # We should do it here.
554 output_node, _ = cras_utils.get_selected_nodes()
555
556 cras_utils.set_system_volume(_DEFAULT_PLAYBACK_VOLUME)
557 cras_utils.set_node_volume(output_node, _DEFAULT_PLAYBACK_VOLUME)
558
559 cras_utils.set_capture_gain(_DEFAULT_CAPTURE_GAIN)
560
561 cras_utils.set_system_mute(False)
562 cras_utils.set_capture_mute(False)
563
564
Owen Lin63b214d2014-01-07 16:40:14 +0800565def generate_rms_postmortem():
566 try:
567 logging.info('audio postmortem report')
568 log_loopback_dongle_status()
569 logging.info(cmd_utils.execute(
570 [_AUDIO_DIAGNOSTICS_PATH], stdout=cmd_utils.PIPE))
Owen Lin942e04d2014-01-09 14:16:59 +0800571 except Exception:
Owen Lin63b214d2014-01-07 16:40:14 +0800572 logging.exception('Error while generating postmortem report')
573
574
Cheng-Yi Chiang7e49fb82014-09-01 20:02:08 +0800575def get_max_cross_correlation(signal_a, signal_b):
576 """Gets max cross-correlation and best time delay of two signals.
577
578 Computes cross-correlation function between two
579 signals and gets the maximum value and time delay.
580 The steps includes:
581 1. Compute cross-correlation function of X and Y and get Cxy.
582 The correlation function Cxy is an array where Cxy[k] is the
583 cross product of X and Y when Y is delayed by k.
584 Refer to manual of numpy.correlate for detail of correlation.
585 2. Find the maximum value C_max and index C_index in Cxy.
586 3. Compute L2 norm of X and Y to get norm(X) and norm(Y).
587 4. Divide C_max by norm(X)*norm(Y) to get max cross-correlation.
588
589 Max cross-correlation indicates the similarity of X and Y. The value
590 is 1 if X equals Y multiplied by a positive scalar.
591 The value is -1 if X equals Y multiplied by a negative scaler.
592 Any constant level shift will be regarded as distortion and will make
593 max cross-correlation value deviated from 1.
594 C_index is the best time delay of Y that make Y looks similar to X.
595 Refer to http://en.wikipedia.org/wiki/Cross-correlation.
596
597 @param signal_a: A list of numbers which contains the first signal.
598 @param signal_b: A list of numbers which contains the second signal.
599
600 @raises: ValueError if any number in signal_a or signal_b is not a float.
601 ValueError if norm of any array is less than _MINIMUM_NORM.
602
603 @returns: A tuple (correlation index, best delay). If there are more than
604 one best delay, just return the first one.
605 """
606 def check_list_contains_float(numbers):
607 """Checks the elements in a list are all float.
608
609 @param numbers: A list of numbers.
610
611 @raises: ValueError if there is any element which is not a float
612 in the list.
613 """
614 if any(not isinstance(x, float) for x in numbers):
615 raise ValueError('List contains number which is not a float')
616
617 check_list_contains_float(signal_a)
618 check_list_contains_float(signal_b)
619
620 norm_a = numpy.linalg.norm(signal_a)
621 norm_b = numpy.linalg.norm(signal_b)
622 logging.debug('norm_a: %f', norm_a)
623 logging.debug('norm_b: %f', norm_b)
624 if norm_a <= _MINIMUM_NORM or norm_b <= _MINIMUM_NORM:
625 raise ValueError('No meaningful data as norm is too small.')
626
627 correlation = numpy.correlate(signal_a, signal_b, 'full')
628 max_correlation = max(correlation)
629 best_delays = [i for i, j in enumerate(correlation) if j == max_correlation]
630 if len(best_delays) > 1:
631 logging.warning('There are more than one best delay: %r', best_delays)
632 return max_correlation / (norm_a * norm_b), best_delays[0]
633
634
635def trim_data(data, threshold=0):
636 """Trims a data by removing value that is too small in head and tail.
637
638 Removes elements in head and tail whose absolute value is smaller than
639 or equal to threshold.
640 E.g. trim_data([0.0, 0.1, 0.2, 0.3, 0.2, 0.1, 0.0], 0.2) =
641 ([0.2, 0.3, 0.2], 2)
642
643 @param data: A list of numbers.
644 @param threshold: The threshold to compare against.
645
646 @returns: A tuple (trimmed_data, valid_index), where valid_index is the
647 original index of the starting element in trimmed_data.
648 Returns ([], None) if there is no valid data.
649 """
650 indice_valid = [
651 i for i, j in enumerate(data) if abs(j) > threshold]
652 if not indice_valid:
653 logging.warning(
654 'There is no element with absolute value greater '
655 'than threshold %f' % threshold)
656 return [], None
657 logging.debug('Start and end of indice_valid: %d, %d',
658 indice_valid[0], indice_valid[-1])
659 return data[indice_valid[0] : indice_valid[-1] + 1], indice_valid[0]
660
661
662def get_one_channel_correlation(test_data, golden_data):
663 """Gets max cross-correlation of test_data and golden_data.
664
665 Trims test data and compute the max cross-correlation against golden_data.
666 Signal can be trimmed because those zero values in the head and tail of
667 a signal will not affect correlation computation.
668
669 @param test_data: A list containing the data to compare against golden data.
670 @param golden_data: A list containing the golden data.
671
672 @returns: A tuple (max cross-correlation, best_delay) if data is valid.
673 Otherwise returns (None, None). Refer to docstring of
674 get_max_cross_correlation.
675 """
676 trimmed_test_data, start_trimmed_length = trim_data(test_data)
677
678 def to_float(samples):
679 """Casts elements in the list to float.
680
681 @param samples: A list of numbers.
682
683 @returns: A list of original numbers casted to float.
684 """
685 samples_float = [float(x) for x in samples]
686 return samples_float
687
688 max_cross_correlation, best_delay = get_max_cross_correlation(
689 to_float(golden_data),
690 to_float(trimmed_test_data))
691
692 # Adds back the trimmed length in the head.
693 if max_cross_correlation:
694 return max_cross_correlation, best_delay + start_trimmed_length
695 else:
696 return None, None
697
698
699def compare_one_channel_correlation(test_data, golden_data):
700 """Compares two one-channel data by correlation.
701
702 @param test_data: A list containing the data to compare against golden data.
703 @param golden_data: A list containing the golden data.
704
705 @returns: A dict containing:
706 index: The index of similarity where 1 means they are different
707 only by a positive scale.
708 best_delay: The best delay of test data in relative to golden
709 data.
710 equal: A bool containing comparing result.
711 """
712 result_dict = dict()
713 max_cross_correlation, best_delay = get_one_channel_correlation(
714 test_data, golden_data)
715 result_dict['index'] = max_cross_correlation
716 result_dict['best_delay'] = best_delay
717 result_dict['equal'] = True if (
718 max_cross_correlation and
719 max_cross_correlation > _CORRELATION_INDEX_THRESHOLD) else False
720 logging.debug('result_dict: %r', result_dict)
721 return result_dict
722
723
724def compare_one_channel_data(test_data, golden_data, method):
725 """Compares two one-channel data.
726
727 @param test_data: A list containing the data to compare against golden data.
728 @param golden_data: A list containing the golden data.
729 @param method: The comparing method. Currently only 'correlation' is
730 supported.
731
732 @returns: A dict containing:
733 index: The index of similarity where 1 means they are different
734 only by a positive scale.
735 best_delay: The best delay of test data in relative to golden
736 data.
737 equal: A bool containing comparing result.
738
739 @raises: NotImplementedError if method is not supported.
740 """
741 if method == 'correlation':
742 return compare_one_channel_correlation(test_data, golden_data)
743 raise NotImplementedError('method %s is not implemented' % method)
744
745
746def compare_data(golden_data_binary, golden_data_format,
747 test_data_binary, test_data_format,
748 channel_map, method):
749 """Compares two raw data.
750
751 @param golden_data_binary: The binary containing golden data.
752 @param golden_data_format: The data format of golden data.
753 @param test_data_binary: The binary containing test data.
754 @param test_data_format: The data format of test data.
755 @param channel_map: A list containing channel mapping.
756 E.g. [1, 0, None, None, None, None, None, None] means
757 channel 0 of test data should map to channel 1 of
758 golden data. Channel 1 of test data should map to
759 channel 0 of golden data. Channel 2 to 7 of test data
760 should be skipped.
761 @param method: The method to compare data. Currently only correlation is
762 implemented.
763
764 @returns: A boolean contains compare result.
765
766 @raises: NotImplementedError if file type is not raw.
767 NotImplementedError if method is not correlation.
768 """
769 if (golden_data_format['file_type'] != 'raw' or
770 test_data_format['file_type'] != 'raw'):
771 raise NotImplementedError('Only support raw data in compare_data.')
772 golden_data = audio_data.AudioRawData(
773 binary=golden_data_binary,
774 channel=golden_data_format['channel'],
775 sample_format=golden_data_format['sample_format'])
776 test_data = audio_data.AudioRawData(
777 binary=test_data_binary,
778 channel=test_data_format['channel'],
779 sample_format=test_data_format['sample_format'])
780 compare_results = []
781 for test_channel, golden_channel in enumerate(channel_map):
782 if golden_channel is None:
783 logging.info('Skipped channel %d', test_channel)
784 continue
785 test_data_one_channel = test_data.channel_data[test_channel]
786 golden_data_one_channel = golden_data.channel_data[golden_channel]
787 result_dict = dict(test_channel=test_channel,
788 golden_channel=golden_channel)
789 result_dict.update(
790 compare_one_channel_data(
791 test_data_one_channel, golden_data_one_channel, method))
792 compare_results.append(result_dict)
793 logging.info('compare_results: %r', compare_results)
794 return_value = False if not compare_results else True
795 for result in compare_results:
796 if not result['equal']:
797 logging.error(
798 'Failed on test channel %d and golden channel %d',
799 result['test_channel'], result['golden_channel'])
800 return_value = False
801 # Also checks best delay are exactly the same.
802 if method == 'correlation':
803 best_delays = set([result['best_delay'] for result in compare_results])
804 if len(best_delays) > 1:
805 logging.error('There are more than one best delay.')
806 return_value = False
807 return return_value
808
809
Owen Lin942e04d2014-01-09 14:16:59 +0800810class _base_rms_test(test.test):
811 """ Base class for all rms_test """
Owen Lin56050862013-12-09 11:42:51 +0800812
Owen Lin942e04d2014-01-09 14:16:59 +0800813 def postprocess(self):
814 super(_base_rms_test, self).postprocess()
Owen Lin56050862013-12-09 11:42:51 +0800815
Owen Lin942e04d2014-01-09 14:16:59 +0800816 # Sum up the number of failed constraints in each iteration
817 if sum(len(x) for x in self.failed_constraints):
818 generate_rms_postmortem()
819
820
821class chrome_rms_test(_base_rms_test):
822 """ Base test class for audio RMS test with Chrome.
823
824 The chrome instance can be accessed by self.chrome.
Owen Lin56050862013-12-09 11:42:51 +0800825 """
Owen Lin942e04d2014-01-09 14:16:59 +0800826 def warmup(self):
827 skip_devices_to_test('x86-mario')
828 super(chrome_rms_test, self).warmup()
829
Owen Lin56050862013-12-09 11:42:51 +0800830 # Not all client of this file using telemetry.
831 # Just do the import here for those who really need it.
832 from autotest_lib.client.common_lib.cros import chrome
Owen Lin942e04d2014-01-09 14:16:59 +0800833
834 self.chrome = chrome.Chrome()
835
836 # The audio configuration could be changed when we
837 # restart chrome.
Owen Lin56050862013-12-09 11:42:51 +0800838 try:
Owen Lin942e04d2014-01-09 14:16:59 +0800839 cras_rms_test_setup()
840 except Exception:
841 self.chrome.browser.Close()
Owen Lin56050862013-12-09 11:42:51 +0800842 raise
Owen Lin56050862013-12-09 11:42:51 +0800843
844
Owen Lin942e04d2014-01-09 14:16:59 +0800845 def cleanup(self, *args):
846 try:
847 self.chrome.browser.Close()
848 finally:
849 super(chrome_rms_test, self).cleanup()
Owen Lin56050862013-12-09 11:42:51 +0800850
Owen Lin942e04d2014-01-09 14:16:59 +0800851class cras_rms_test(_base_rms_test):
852 """ Base test class for CRAS audio RMS test."""
853
854 def warmup(self):
Rohit Makasana06446562014-01-03 11:39:03 -0800855 skip_devices_to_test('x86-mario')
Owen Lin942e04d2014-01-09 14:16:59 +0800856 super(cras_rms_test, self).warmup()
Owen Lin56050862013-12-09 11:42:51 +0800857 cras_rms_test_setup()
Owen Lin56050862013-12-09 11:42:51 +0800858
859
Owen Lin942e04d2014-01-09 14:16:59 +0800860class alsa_rms_test(_base_rms_test):
861 """ Base test class for ALSA audio RMS test."""
Owen Lin56050862013-12-09 11:42:51 +0800862
Owen Lin942e04d2014-01-09 14:16:59 +0800863 def warmup(self):
Rohit Makasana06446562014-01-03 11:39:03 -0800864 skip_devices_to_test('x86-mario')
Owen Lin942e04d2014-01-09 14:16:59 +0800865 super(alsa_rms_test, self).warmup()
866
Owen Lin56050862013-12-09 11:42:51 +0800867 # TODO(owenlin): Don't use CRAS for setup.
868 cras_rms_test_setup()
869
870 # CRAS does not apply the volume and capture gain to ALSA util
871 # streams are added. Do that to ensure the values have been set.
Owen Linbd415142013-12-18 10:38:58 +0800872 cras_utils.playback('/dev/zero', duration=0.1)
Owen Lin56050862013-12-09 11:42:51 +0800873 cras_utils.capture('/dev/null', duration=0.1)