xref: /aosp_15_r20/external/autotest/client/cros/audio/audio_helper.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
1*9c5db199SXin Li#!/usr/bin/python3
2*9c5db199SXin Li# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
3*9c5db199SXin Li# Use of this source code is governed by a BSD-style license that can be
4*9c5db199SXin Li# found in the LICENSE file.
5*9c5db199SXin Li
6*9c5db199SXin Li
7*9c5db199SXin Lifrom __future__ import absolute_import
8*9c5db199SXin Lifrom __future__ import division
9*9c5db199SXin Lifrom __future__ import print_function
10*9c5db199SXin Li
11*9c5db199SXin Liimport logging
12*9c5db199SXin Liimport numpy
13*9c5db199SXin Liimport os
14*9c5db199SXin Liimport re
15*9c5db199SXin Liimport subprocess
16*9c5db199SXin Liimport tempfile
17*9c5db199SXin Liimport threading
18*9c5db199SXin Liimport time
19*9c5db199SXin Li
20*9c5db199SXin Lifrom glob import glob
21*9c5db199SXin Lifrom autotest_lib.client.bin import test, utils
22*9c5db199SXin Lifrom autotest_lib.client.bin.input.input_device import *
23*9c5db199SXin Lifrom autotest_lib.client.common_lib import error
24*9c5db199SXin Lifrom autotest_lib.client.cros.audio import audio_data
25*9c5db199SXin Lifrom autotest_lib.client.cros.audio import cmd_utils
26*9c5db199SXin Lifrom autotest_lib.client.cros.audio import cras_utils
27*9c5db199SXin Lifrom autotest_lib.client.cros.audio import sox_utils
28*9c5db199SXin Lifrom six.moves import range
29*9c5db199SXin Li
30*9c5db199SXin LiLD_LIBRARY_PATH = 'LD_LIBRARY_PATH'
31*9c5db199SXin Li
32*9c5db199SXin Li_AUDIO_DIAGNOSTICS_PATH = '/usr/bin/audio_diagnostics'
33*9c5db199SXin Li
34*9c5db199SXin Li_DEFAULT_NUM_CHANNELS = 2
35*9c5db199SXin Li_DEFAULT_REC_COMMAND = 'arecord -D hw:0,0 -d 10 -f dat'
36*9c5db199SXin Li_DEFAULT_SOX_FORMAT = '-t raw -b 16 -e signed -r 48000 -L'
37*9c5db199SXin Li_DEFAULT_PLAYBACK_VOLUME = 100
38*9c5db199SXin Li_DEFAULT_CAPTURE_GAIN = 2500
39*9c5db199SXin Li_DEFAULT_ALSA_MAX_VOLUME = '100%'
40*9c5db199SXin Li_DEFAULT_ALSA_CAPTURE_GAIN = '25dB'
41*9c5db199SXin Li_DEFAULT_VOLUME_LEVEL = 75
42*9c5db199SXin Li_DEFAULT_MIC_GAIN = 75
43*9c5db199SXin Li
44*9c5db199SXin Li# Minimum RMS value to pass when checking recorded file.
45*9c5db199SXin Li_DEFAULT_SOX_RMS_THRESHOLD = 0.08
46*9c5db199SXin Li
47*9c5db199SXin Li_JACK_VALUE_ON_RE = re.compile(r'.*values=on')
48*9c5db199SXin Li_HP_JACK_CONTROL_RE = re.compile(r'numid=(\d+).*Headphone\sJack')
49*9c5db199SXin Li_MIC_JACK_CONTROL_RE = re.compile(r'numid=(\d+).*Mic\sJack')
50*9c5db199SXin Li
51*9c5db199SXin Li_SOX_RMS_AMPLITUDE_RE = re.compile(r'RMS\s+amplitude:\s+(.+)')
52*9c5db199SXin Li_SOX_ROUGH_FREQ_RE = re.compile(r'Rough\s+frequency:\s+(.+)')
53*9c5db199SXin Li
54*9c5db199SXin Li_AUDIO_NOT_FOUND_RE = r'Audio\snot\sdetected'
55*9c5db199SXin Li_MEASURED_LATENCY_RE = r'Measured\sLatency:\s(\d+)\suS'
56*9c5db199SXin Li_REPORTED_LATENCY_RE = r'Reported\sLatency:\s(\d+)\suS'
57*9c5db199SXin Li
58*9c5db199SXin Li# Tools from platform/audiotest
59*9c5db199SXin LiAUDIOFUNTEST_PATH = 'audiofuntest'
60*9c5db199SXin LiAUDIOLOOP_PATH = 'looptest'
61*9c5db199SXin LiLOOPBACK_LATENCY_PATH = 'loopback_latency'
62*9c5db199SXin LiSOX_PATH = 'sox'
63*9c5db199SXin LiTEST_TONES_PATH = 'test_tones'
64*9c5db199SXin Li
65*9c5db199SXin Li_MINIMUM_NORM = 0.001
66*9c5db199SXin Li_CORRELATION_INDEX_THRESHOLD = 0.999
67*9c5db199SXin Li# The minimum difference of estimated frequencies between two sine waves.
68*9c5db199SXin Li_FREQUENCY_DIFF_THRESHOLD = 20
69*9c5db199SXin Li# The minimum RMS value of meaningful audio data.
70*9c5db199SXin Li_MEANINGFUL_RMS_THRESHOLD = 0.001
71*9c5db199SXin Li
72*9c5db199SXin Lidef set_mixer_controls(mixer_settings={}, card='0'):
73*9c5db199SXin Li    """Sets all mixer controls listed in the mixer settings on card.
74*9c5db199SXin Li
75*9c5db199SXin Li    @param mixer_settings: Mixer settings to set.
76*9c5db199SXin Li    @param card: Index of audio card to set mixer settings for.
77*9c5db199SXin Li    """
78*9c5db199SXin Li    logging.info('Setting mixer control values on %s', card)
79*9c5db199SXin Li    for item in mixer_settings:
80*9c5db199SXin Li        logging.info('Setting %s to %s on card %s',
81*9c5db199SXin Li                     item['name'], item['value'], card)
82*9c5db199SXin Li        cmd = 'amixer -c %s cset name=%s %s'
83*9c5db199SXin Li        cmd = cmd % (card, item['name'], item['value'])
84*9c5db199SXin Li        try:
85*9c5db199SXin Li            utils.system(cmd)
86*9c5db199SXin Li        except error.CmdError:
87*9c5db199SXin Li            # A card is allowed not to support all the controls, so don't
88*9c5db199SXin Li            # fail the test here if we get an error.
89*9c5db199SXin Li            logging.info('amixer command failed: %s', cmd)
90*9c5db199SXin Li
91*9c5db199SXin Lidef set_default_volume_levels():
92*9c5db199SXin Li    """Sets the default volume and default capture gain through cras_test_client.
93*9c5db199SXin Li
94*9c5db199SXin Li    """
95*9c5db199SXin Li    logging.info('Setting audio levels to their defaults')
96*9c5db199SXin Li    set_volume_levels(_DEFAULT_VOLUME_LEVEL, _DEFAULT_MIC_GAIN)
97*9c5db199SXin Li
98*9c5db199SXin Lidef set_volume_levels(volume, capture):
99*9c5db199SXin Li    """Sets the volume and capture gain through cras_test_client.
100*9c5db199SXin Li
101*9c5db199SXin Li    @param volume: The playback volume to set.
102*9c5db199SXin Li    @param capture: The capture gain to set.
103*9c5db199SXin Li    """
104*9c5db199SXin Li    logging.info('Setting volume: %d capture: %d', volume, capture)
105*9c5db199SXin Li    try:
106*9c5db199SXin Li        utils.system('/usr/bin/cras_test_client --volume %d' % volume)
107*9c5db199SXin Li        utils.system('/usr/bin/cras_test_client --capture_gain %d' % capture)
108*9c5db199SXin Li        utils.system('/usr/bin/cras_test_client --dump_server_info')
109*9c5db199SXin Li        utils.system('/usr/bin/cras_test_client --mute 0')
110*9c5db199SXin Li    except error.CmdError as e:
111*9c5db199SXin Li        raise error.TestError(
112*9c5db199SXin Li                '*** Can not tune volume through CRAS. *** (' + str(e) + ')')
113*9c5db199SXin Li
114*9c5db199SXin Li    try:
115*9c5db199SXin Li        utils.system('amixer -c 0 contents')
116*9c5db199SXin Li    except error.CmdError as e:
117*9c5db199SXin Li        logging.info('amixer command failed: %s', str(e))
118*9c5db199SXin Li
119*9c5db199SXin Lidef loopback_latency_check(**args):
120*9c5db199SXin Li    """Checks loopback latency.
121*9c5db199SXin Li
122*9c5db199SXin Li    @param args: additional arguments for loopback_latency.
123*9c5db199SXin Li
124*9c5db199SXin Li    @return A tuple containing measured and reported latency in uS.
125*9c5db199SXin Li        Return None if no audio detected.
126*9c5db199SXin Li    """
127*9c5db199SXin Li    noise_threshold = str(args['n']) if 'n' in args else '400'
128*9c5db199SXin Li
129*9c5db199SXin Li    cmd = '%s -n %s -c' % (LOOPBACK_LATENCY_PATH, noise_threshold)
130*9c5db199SXin Li
131*9c5db199SXin Li    output = utils.system_output(cmd, retain_output=True)
132*9c5db199SXin Li
133*9c5db199SXin Li    # Sleep for a short while to make sure device is not busy anymore
134*9c5db199SXin Li    # after called loopback_latency.
135*9c5db199SXin Li    time.sleep(.1)
136*9c5db199SXin Li
137*9c5db199SXin Li    measured_latency = None
138*9c5db199SXin Li    reported_latency = None
139*9c5db199SXin Li    for line in output.split('\n'):
140*9c5db199SXin Li        match = re.search(_MEASURED_LATENCY_RE, line, re.I)
141*9c5db199SXin Li        if match:
142*9c5db199SXin Li            measured_latency = int(match.group(1))
143*9c5db199SXin Li            continue
144*9c5db199SXin Li        match = re.search(_REPORTED_LATENCY_RE, line, re.I)
145*9c5db199SXin Li        if match:
146*9c5db199SXin Li            reported_latency = int(match.group(1))
147*9c5db199SXin Li            continue
148*9c5db199SXin Li        if re.search(_AUDIO_NOT_FOUND_RE, line, re.I):
149*9c5db199SXin Li            return None
150*9c5db199SXin Li    if measured_latency and reported_latency:
151*9c5db199SXin Li        return (measured_latency, reported_latency)
152*9c5db199SXin Li    else:
153*9c5db199SXin Li        # Should not reach here, just in case.
154*9c5db199SXin Li        return None
155*9c5db199SXin Li
156*9c5db199SXin Lidef get_mixer_jack_status(jack_reg_exp):
157*9c5db199SXin Li    """Gets the mixer jack status.
158*9c5db199SXin Li
159*9c5db199SXin Li    @param jack_reg_exp: The regular expression to match jack control name.
160*9c5db199SXin Li
161*9c5db199SXin Li    @return None if the control does not exist, return True if jack control
162*9c5db199SXin Li        is detected plugged, return False otherwise.
163*9c5db199SXin Li    """
164*9c5db199SXin Li    output = utils.system_output('amixer -c0 controls', retain_output=True)
165*9c5db199SXin Li    numid = None
166*9c5db199SXin Li    for line in output.split('\n'):
167*9c5db199SXin Li        m = jack_reg_exp.match(line)
168*9c5db199SXin Li        if m:
169*9c5db199SXin Li            numid = m.group(1)
170*9c5db199SXin Li            break
171*9c5db199SXin Li
172*9c5db199SXin Li    # Proceed only when matched numid is not empty.
173*9c5db199SXin Li    if numid:
174*9c5db199SXin Li        output = utils.system_output('amixer -c0 cget numid=%s' % numid)
175*9c5db199SXin Li        for line in output.split('\n'):
176*9c5db199SXin Li            if _JACK_VALUE_ON_RE.match(line):
177*9c5db199SXin Li                return True
178*9c5db199SXin Li        return False
179*9c5db199SXin Li    else:
180*9c5db199SXin Li        return None
181*9c5db199SXin Li
182*9c5db199SXin Lidef get_hp_jack_status():
183*9c5db199SXin Li    """Gets the status of headphone jack."""
184*9c5db199SXin Li    status = get_mixer_jack_status(_HP_JACK_CONTROL_RE)
185*9c5db199SXin Li    if status is not None:
186*9c5db199SXin Li        return status
187*9c5db199SXin Li
188*9c5db199SXin Li    # When headphone jack is not found in amixer, lookup input devices
189*9c5db199SXin Li    # instead.
190*9c5db199SXin Li    #
191*9c5db199SXin Li    # TODO(hychao): Check hp/mic jack status dynamically from evdev. And
192*9c5db199SXin Li    # possibly replace the existing check using amixer.
193*9c5db199SXin Li    for evdev in glob('/dev/input/event*'):
194*9c5db199SXin Li        device = InputDevice(evdev)
195*9c5db199SXin Li        if device.is_hp_jack():
196*9c5db199SXin Li            return device.get_headphone_insert()
197*9c5db199SXin Li    else:
198*9c5db199SXin Li        return None
199*9c5db199SXin Li
200*9c5db199SXin Lidef get_mic_jack_status():
201*9c5db199SXin Li    """Gets the status of mic jack."""
202*9c5db199SXin Li    status = get_mixer_jack_status(_MIC_JACK_CONTROL_RE)
203*9c5db199SXin Li    if status is not None:
204*9c5db199SXin Li        return status
205*9c5db199SXin Li
206*9c5db199SXin Li    # When mic jack is not found in amixer, lookup input devices instead.
207*9c5db199SXin Li    for evdev in glob('/dev/input/event*'):
208*9c5db199SXin Li        device = InputDevice(evdev)
209*9c5db199SXin Li        if device.is_mic_jack():
210*9c5db199SXin Li            return device.get_microphone_insert()
211*9c5db199SXin Li    else:
212*9c5db199SXin Li        return None
213*9c5db199SXin Li
214*9c5db199SXin Li# Functions to test audio palyback.
215*9c5db199SXin Lidef play_sound(duration_seconds=None, audio_file_path=None):
216*9c5db199SXin Li    """Plays a sound file found at |audio_file_path| for |duration_seconds|.
217*9c5db199SXin Li
218*9c5db199SXin Li    If |audio_file_path|=None, plays a default audio file.
219*9c5db199SXin Li    If |duration_seconds|=None, plays audio file in its entirety.
220*9c5db199SXin Li
221*9c5db199SXin Li    @param duration_seconds: Duration to play sound.
222*9c5db199SXin Li    @param audio_file_path: Path to the audio file.
223*9c5db199SXin Li    """
224*9c5db199SXin Li    if not audio_file_path:
225*9c5db199SXin Li        audio_file_path = '/usr/local/autotest/cros/audio/sine440.wav'
226*9c5db199SXin Li    duration_arg = ('-d %d' % duration_seconds) if duration_seconds else ''
227*9c5db199SXin Li    utils.system('aplay %s %s' % (duration_arg, audio_file_path))
228*9c5db199SXin Li
229*9c5db199SXin Lidef get_play_sine_args(channel, odev='default', freq=1000, duration=10,
230*9c5db199SXin Li                       sample_size=16):
231*9c5db199SXin Li    """Gets the command args to generate a sine wav to play to odev.
232*9c5db199SXin Li
233*9c5db199SXin Li    @param channel: 0 for left, 1 for right; otherwize, mono.
234*9c5db199SXin Li    @param odev: alsa output device.
235*9c5db199SXin Li    @param freq: frequency of the generated sine tone.
236*9c5db199SXin Li    @param duration: duration of the generated sine tone.
237*9c5db199SXin Li    @param sample_size: output audio sample size. Default to 16.
238*9c5db199SXin Li    """
239*9c5db199SXin Li    cmdargs = [SOX_PATH, '-b', str(sample_size), '-n', '-t', 'alsa',
240*9c5db199SXin Li               odev, 'synth', str(duration)]
241*9c5db199SXin Li    if channel == 0:
242*9c5db199SXin Li        cmdargs += ['sine', str(freq), 'sine', '0']
243*9c5db199SXin Li    elif channel == 1:
244*9c5db199SXin Li        cmdargs += ['sine', '0', 'sine', str(freq)]
245*9c5db199SXin Li    else:
246*9c5db199SXin Li        cmdargs += ['sine', str(freq)]
247*9c5db199SXin Li
248*9c5db199SXin Li    return cmdargs
249*9c5db199SXin Li
250*9c5db199SXin Lidef play_sine(channel, odev='default', freq=1000, duration=10,
251*9c5db199SXin Li              sample_size=16):
252*9c5db199SXin Li    """Generates a sine wave and plays to odev.
253*9c5db199SXin Li
254*9c5db199SXin Li    @param channel: 0 for left, 1 for right; otherwize, mono.
255*9c5db199SXin Li    @param odev: alsa output device.
256*9c5db199SXin Li    @param freq: frequency of the generated sine tone.
257*9c5db199SXin Li    @param duration: duration of the generated sine tone.
258*9c5db199SXin Li    @param sample_size: output audio sample size. Default to 16.
259*9c5db199SXin Li    """
260*9c5db199SXin Li    cmdargs = get_play_sine_args(channel, odev, freq, duration, sample_size)
261*9c5db199SXin Li    utils.system(' '.join(cmdargs))
262*9c5db199SXin Li
263*9c5db199SXin Lidef get_audio_rms(sox_output):
264*9c5db199SXin Li    """Gets the audio RMS value from sox stat output
265*9c5db199SXin Li
266*9c5db199SXin Li    @param sox_output: Output of sox stat command.
267*9c5db199SXin Li
268*9c5db199SXin Li    @return The RMS value parsed from sox stat output.
269*9c5db199SXin Li    """
270*9c5db199SXin Li    for rms_line in sox_output.split('\n'):
271*9c5db199SXin Li        m = _SOX_RMS_AMPLITUDE_RE.match(rms_line)
272*9c5db199SXin Li        if m is not None:
273*9c5db199SXin Li            return float(m.group(1))
274*9c5db199SXin Li
275*9c5db199SXin Lidef get_rough_freq(sox_output):
276*9c5db199SXin Li    """Gets the rough audio frequency from sox stat output
277*9c5db199SXin Li
278*9c5db199SXin Li    @param sox_output: Output of sox stat command.
279*9c5db199SXin Li
280*9c5db199SXin Li    @return The rough frequency value parsed from sox stat output.
281*9c5db199SXin Li    """
282*9c5db199SXin Li    for rms_line in sox_output.split('\n'):
283*9c5db199SXin Li        m = _SOX_ROUGH_FREQ_RE.match(rms_line)
284*9c5db199SXin Li        if m is not None:
285*9c5db199SXin Li            return int(m.group(1))
286*9c5db199SXin Li
287*9c5db199SXin Lidef check_audio_rms(sox_output, sox_threshold=_DEFAULT_SOX_RMS_THRESHOLD):
288*9c5db199SXin Li    """Checks if the calculated RMS value is expected.
289*9c5db199SXin Li
290*9c5db199SXin Li    @param sox_output: The output from sox stat command.
291*9c5db199SXin Li    @param sox_threshold: The threshold to test RMS value against.
292*9c5db199SXin Li
293*9c5db199SXin Li    @raises error.TestError if RMS amplitude can't be parsed.
294*9c5db199SXin Li    @raises error.TestFail if the RMS amplitude of the recording isn't above
295*9c5db199SXin Li            the threshold.
296*9c5db199SXin Li    """
297*9c5db199SXin Li    rms_val = get_audio_rms(sox_output)
298*9c5db199SXin Li
299*9c5db199SXin Li    # In case we don't get a valid RMS value.
300*9c5db199SXin Li    if rms_val is None:
301*9c5db199SXin Li        raise error.TestError(
302*9c5db199SXin Li            'Failed to generate an audio RMS value from playback.')
303*9c5db199SXin Li
304*9c5db199SXin Li    logging.info('Got audio RMS value of %f. Minimum pass is %f.',
305*9c5db199SXin Li                 rms_val, sox_threshold)
306*9c5db199SXin Li    if rms_val < sox_threshold:
307*9c5db199SXin Li        raise error.TestFail(
308*9c5db199SXin Li            'Audio RMS value %f too low. Minimum pass is %f.' %
309*9c5db199SXin Li            (rms_val, sox_threshold))
310*9c5db199SXin Li
311*9c5db199SXin Lidef noise_reduce_file(in_file, noise_file, out_file,
312*9c5db199SXin Li                      sox_format=_DEFAULT_SOX_FORMAT):
313*9c5db199SXin Li    """Runs the sox command to reduce noise.
314*9c5db199SXin Li
315*9c5db199SXin Li    Runs the sox command to noise-reduce in_file using the noise
316*9c5db199SXin Li    profile from noise_file.
317*9c5db199SXin Li
318*9c5db199SXin Li    @param in_file: The file to noise reduce.
319*9c5db199SXin Li    @param noise_file: The file containing the noise profile.
320*9c5db199SXin Li        This can be created by recording silence.
321*9c5db199SXin Li    @param out_file: The file contains the noise reduced sound.
322*9c5db199SXin Li    @param sox_format: The  sox format to generate sox command.
323*9c5db199SXin Li    """
324*9c5db199SXin Li    prof_cmd = '%s -c 2 %s %s -n noiseprof' % (SOX_PATH,
325*9c5db199SXin Li               sox_format, noise_file)
326*9c5db199SXin Li    reduce_cmd = ('%s -c 2 %s %s -c 2 %s %s noisered' %
327*9c5db199SXin Li            (SOX_PATH, sox_format, in_file, sox_format, out_file))
328*9c5db199SXin Li    utils.system('%s | %s' % (prof_cmd, reduce_cmd))
329*9c5db199SXin Li
330*9c5db199SXin Lidef record_sample(tmpfile, record_command=_DEFAULT_REC_COMMAND):
331*9c5db199SXin Li    """Records a sample from the default input device.
332*9c5db199SXin Li
333*9c5db199SXin Li    @param tmpfile: The file to record to.
334*9c5db199SXin Li    @param record_command: The command to record audio.
335*9c5db199SXin Li    """
336*9c5db199SXin Li    utils.system('%s %s' % (record_command, tmpfile))
337*9c5db199SXin Li
338*9c5db199SXin Lidef create_wav_file(wav_dir, prefix=""):
339*9c5db199SXin Li    """Creates a unique name for wav file.
340*9c5db199SXin Li
341*9c5db199SXin Li    The created file name will be preserved in autotest result directory
342*9c5db199SXin Li    for future analysis.
343*9c5db199SXin Li
344*9c5db199SXin Li    @param wav_dir: The directory of created wav file.
345*9c5db199SXin Li    @param prefix: specified file name prefix.
346*9c5db199SXin Li    """
347*9c5db199SXin Li    filename = "%s-%s.wav" % (prefix, time.time())
348*9c5db199SXin Li    return os.path.join(wav_dir, filename)
349*9c5db199SXin Li
350*9c5db199SXin Lidef run_in_parallel(*funs):
351*9c5db199SXin Li    """Runs methods in parallel.
352*9c5db199SXin Li
353*9c5db199SXin Li    @param funs: methods to run.
354*9c5db199SXin Li    """
355*9c5db199SXin Li    threads = []
356*9c5db199SXin Li    for f in funs:
357*9c5db199SXin Li        t = threading.Thread(target=f)
358*9c5db199SXin Li        t.start()
359*9c5db199SXin Li        threads.append(t)
360*9c5db199SXin Li
361*9c5db199SXin Li    for t in threads:
362*9c5db199SXin Li        t.join()
363*9c5db199SXin Li
364*9c5db199SXin Lidef get_channel_sox_stat(
365*9c5db199SXin Li        input_audio, channel_index, channels=2, bits=16, rate=48000):
366*9c5db199SXin Li    """Gets the sox stat info of the selected channel in the input audio file.
367*9c5db199SXin Li
368*9c5db199SXin Li    @param input_audio: The input audio file to be analyzed.
369*9c5db199SXin Li    @param channel_index: The index of the channel to be analyzed.
370*9c5db199SXin Li                          (1 for the first channel).
371*9c5db199SXin Li    @param channels: The number of channels in the input audio.
372*9c5db199SXin Li    @param bits: The number of bits of each audio sample.
373*9c5db199SXin Li    @param rate: The sampling rate.
374*9c5db199SXin Li    """
375*9c5db199SXin Li    if channel_index <= 0 or channel_index > channels:
376*9c5db199SXin Li        raise ValueError('incorrect channel_indexi: %d' % channel_index)
377*9c5db199SXin Li
378*9c5db199SXin Li    if channels == 1:
379*9c5db199SXin Li        return sox_utils.get_stat(
380*9c5db199SXin Li                input_audio, channels=channels, bits=bits, rate=rate)
381*9c5db199SXin Li
382*9c5db199SXin Li    p1 = cmd_utils.popen(
383*9c5db199SXin Li            sox_utils.extract_channel_cmd(
384*9c5db199SXin Li                    input_audio, '-', channel_index,
385*9c5db199SXin Li                    channels=channels, bits=bits, rate=rate),
386*9c5db199SXin Li            stdout=subprocess.PIPE)
387*9c5db199SXin Li    p2 = cmd_utils.popen(
388*9c5db199SXin Li            sox_utils.stat_cmd('-', channels=1, bits=bits, rate=rate),
389*9c5db199SXin Li            stdin=p1.stdout, stderr=subprocess.PIPE)
390*9c5db199SXin Li    stat_output = p2.stderr.read()
391*9c5db199SXin Li    cmd_utils.wait_and_check_returncode(p1, p2)
392*9c5db199SXin Li    return sox_utils.parse_stat_output(stat_output)
393*9c5db199SXin Li
394*9c5db199SXin Li
395*9c5db199SXin Lidef get_rms(input_audio, channels=1, bits=16, rate=48000):
396*9c5db199SXin Li    """Gets the RMS values of all channels of the input audio.
397*9c5db199SXin Li
398*9c5db199SXin Li    @param input_audio: The input audio file to be checked.
399*9c5db199SXin Li    @param channels: The number of channels in the input audio.
400*9c5db199SXin Li    @param bits: The number of bits of each audio sample.
401*9c5db199SXin Li    @param rate: The sampling rate.
402*9c5db199SXin Li    """
403*9c5db199SXin Li    stats = [get_channel_sox_stat(
404*9c5db199SXin Li            input_audio, i + 1, channels=channels, bits=bits,
405*9c5db199SXin Li            rate=rate) for i in range(channels)]
406*9c5db199SXin Li
407*9c5db199SXin Li    logging.info('sox stat: %s', [str(s) for s in stats])
408*9c5db199SXin Li    return [s.rms for s in stats]
409*9c5db199SXin Li
410*9c5db199SXin Li
411*9c5db199SXin Lidef reduce_noise_and_get_rms(
412*9c5db199SXin Li        input_audio, noise_file, channels=1, bits=16, rate=48000):
413*9c5db199SXin Li    """Reduces noise in the input audio by the given noise file and then gets
414*9c5db199SXin Li    the RMS values of all channels of the input audio.
415*9c5db199SXin Li
416*9c5db199SXin Li    @param input_audio: The input audio file to be analyzed.
417*9c5db199SXin Li    @param noise_file: The noise file used to reduce noise in the input audio.
418*9c5db199SXin Li    @param channels: The number of channels in the input audio.
419*9c5db199SXin Li    @param bits: The number of bits of each audio sample.
420*9c5db199SXin Li    @param rate: The sampling rate.
421*9c5db199SXin Li    """
422*9c5db199SXin Li    with tempfile.NamedTemporaryFile() as reduced_file:
423*9c5db199SXin Li        p1 = cmd_utils.popen(
424*9c5db199SXin Li                sox_utils.noise_profile_cmd(
425*9c5db199SXin Li                        noise_file, '-', channels=channels, bits=bits,
426*9c5db199SXin Li                        rate=rate),
427*9c5db199SXin Li                stdout=subprocess.PIPE)
428*9c5db199SXin Li        p2 = cmd_utils.popen(
429*9c5db199SXin Li                sox_utils.noise_reduce_cmd(
430*9c5db199SXin Li                        input_audio, reduced_file.name, '-',
431*9c5db199SXin Li                        channels=channels, bits=bits, rate=rate),
432*9c5db199SXin Li                stdin=p1.stdout)
433*9c5db199SXin Li        cmd_utils.wait_and_check_returncode(p1, p2)
434*9c5db199SXin Li        return get_rms(reduced_file.name, channels, bits, rate)
435*9c5db199SXin Li
436*9c5db199SXin Li
437*9c5db199SXin Lidef cras_rms_test_setup():
438*9c5db199SXin Li    """Setups for the cras_rms_tests.
439*9c5db199SXin Li
440*9c5db199SXin Li    To make sure the line_out-to-mic_in path is all green.
441*9c5db199SXin Li    """
442*9c5db199SXin Li    # TODO(owenlin): Now, the nodes are choosed by chrome.
443*9c5db199SXin Li    #                We should do it here.
444*9c5db199SXin Li    cras_utils.set_system_volume(_DEFAULT_PLAYBACK_VOLUME)
445*9c5db199SXin Li    cras_utils.set_selected_output_node_volume(_DEFAULT_PLAYBACK_VOLUME)
446*9c5db199SXin Li
447*9c5db199SXin Li    cras_utils.set_system_mute(False)
448*9c5db199SXin Li    cras_utils.set_capture_mute(False)
449*9c5db199SXin Li
450*9c5db199SXin Li
451*9c5db199SXin Lidef dump_rms_retrospective(result_dir):
452*9c5db199SXin Li    """Dumps retrospective for rms tests."""
453*9c5db199SXin Li    try:
454*9c5db199SXin Li        dump_audio_diagnostics(
455*9c5db199SXin Li                os.path.join(result_dir, "audio_diagnostics.txt"))
456*9c5db199SXin Li    except Exception:
457*9c5db199SXin Li        logging.exception('Error while generating retrospective report')
458*9c5db199SXin Li
459*9c5db199SXin Li
460*9c5db199SXin Lidef dump_audio_diagnostics(file_path=None):
461*9c5db199SXin Li    """Dumps audio diagnostics results to a file
462*9c5db199SXin Li
463*9c5db199SXin Li    Dumps the result of audio_diagnostics to a file. Returns a string
464*9c5db199SXin Li    containing the result if the file_path is not specified.
465*9c5db199SXin Li
466*9c5db199SXin Li    @returns: None if 'file_path' is specified, otherwise, a string containing
467*9c5db199SXin Li    the audio diagnostic results.
468*9c5db199SXin Li    """
469*9c5db199SXin Li    if file_path:
470*9c5db199SXin Li        with open(file_path, 'w') as f:
471*9c5db199SXin Li            return cmd_utils.execute([_AUDIO_DIAGNOSTICS_PATH], stdout=f)
472*9c5db199SXin Li
473*9c5db199SXin Li    return cmd_utils.execute([_AUDIO_DIAGNOSTICS_PATH], stdout=subprocess.PIPE)
474*9c5db199SXin Li
475*9c5db199SXin Li
476*9c5db199SXin Lidef get_max_cross_correlation(signal_a, signal_b):
477*9c5db199SXin Li    """Gets max cross-correlation and best time delay of two signals.
478*9c5db199SXin Li
479*9c5db199SXin Li    Computes cross-correlation function between two
480*9c5db199SXin Li    signals and gets the maximum value and time delay.
481*9c5db199SXin Li    The steps includes:
482*9c5db199SXin Li      1. Compute cross-correlation function of X and Y and get Cxy.
483*9c5db199SXin Li         The correlation function Cxy is an array where Cxy[k] is the
484*9c5db199SXin Li         cross product of X and Y when Y is delayed by k.
485*9c5db199SXin Li         Refer to manual of numpy.correlate for detail of correlation.
486*9c5db199SXin Li      2. Find the maximum value C_max and index C_index in Cxy.
487*9c5db199SXin Li      3. Compute L2 norm of X and Y to get norm(X) and norm(Y).
488*9c5db199SXin Li      4. Divide C_max by norm(X)*norm(Y) to get max cross-correlation.
489*9c5db199SXin Li
490*9c5db199SXin Li    Max cross-correlation indicates the similarity of X and Y. The value
491*9c5db199SXin Li    is 1 if X equals Y multiplied by a positive scalar.
492*9c5db199SXin Li    The value is -1 if X equals Y multiplied by a negative scaler.
493*9c5db199SXin Li    Any constant level shift will be regarded as distortion and will make
494*9c5db199SXin Li    max cross-correlation value deviated from 1.
495*9c5db199SXin Li    C_index is the best time delay of Y that make Y looks similar to X.
496*9c5db199SXin Li    Refer to http://en.wikipedia.org/wiki/Cross-correlation.
497*9c5db199SXin Li
498*9c5db199SXin Li    @param signal_a: A list of numbers which contains the first signal.
499*9c5db199SXin Li    @param signal_b: A list of numbers which contains the second signal.
500*9c5db199SXin Li
501*9c5db199SXin Li    @raises: ValueError if any number in signal_a or signal_b is not a float.
502*9c5db199SXin Li             ValueError if norm of any array is less than _MINIMUM_NORM.
503*9c5db199SXin Li
504*9c5db199SXin Li    @returns: A tuple (correlation index, best delay). If there are more than
505*9c5db199SXin Li              one best delay, just return the first one.
506*9c5db199SXin Li    """
507*9c5db199SXin Li    def check_list_contains_float(numbers):
508*9c5db199SXin Li        """Checks the elements in a list are all float.
509*9c5db199SXin Li
510*9c5db199SXin Li        @param numbers: A list of numbers.
511*9c5db199SXin Li
512*9c5db199SXin Li        @raises: ValueError if there is any element which is not a float
513*9c5db199SXin Li                 in the list.
514*9c5db199SXin Li        """
515*9c5db199SXin Li        if any(not isinstance(x, float) for x in numbers):
516*9c5db199SXin Li            raise ValueError('List contains number which is not a float')
517*9c5db199SXin Li
518*9c5db199SXin Li    check_list_contains_float(signal_a)
519*9c5db199SXin Li    check_list_contains_float(signal_b)
520*9c5db199SXin Li
521*9c5db199SXin Li    norm_a = numpy.linalg.norm(signal_a)
522*9c5db199SXin Li    norm_b = numpy.linalg.norm(signal_b)
523*9c5db199SXin Li    logging.debug('norm_a: %f', norm_a)
524*9c5db199SXin Li    logging.debug('norm_b: %f', norm_b)
525*9c5db199SXin Li    if norm_a <= _MINIMUM_NORM or norm_b <= _MINIMUM_NORM:
526*9c5db199SXin Li        raise ValueError('No meaningful data as norm is too small.')
527*9c5db199SXin Li
528*9c5db199SXin Li    correlation = numpy.correlate(signal_a, signal_b, 'full')
529*9c5db199SXin Li    max_correlation = max(correlation)
530*9c5db199SXin Li    best_delays = [i for i, j in enumerate(correlation) if j == max_correlation]
531*9c5db199SXin Li    if len(best_delays) > 1:
532*9c5db199SXin Li        logging.warning('There are more than one best delay: %r', best_delays)
533*9c5db199SXin Li    return max_correlation / (norm_a * norm_b), best_delays[0]
534*9c5db199SXin Li
535*9c5db199SXin Li
536*9c5db199SXin Lidef trim_data(data, threshold=0):
537*9c5db199SXin Li    """Trims a data by removing value that is too small in head and tail.
538*9c5db199SXin Li
539*9c5db199SXin Li    Removes elements in head and tail whose absolute value is smaller than
540*9c5db199SXin Li    or equal to threshold.
541*9c5db199SXin Li    E.g. trim_data([0.0, 0.1, 0.2, 0.3, 0.2, 0.1, 0.0], 0.2) =
542*9c5db199SXin Li    ([0.2, 0.3, 0.2], 2)
543*9c5db199SXin Li
544*9c5db199SXin Li    @param data: A list of numbers.
545*9c5db199SXin Li    @param threshold: The threshold to compare against.
546*9c5db199SXin Li
547*9c5db199SXin Li    @returns: A tuple (trimmed_data, end_trimmed_length), where
548*9c5db199SXin Li              end_trimmed_length is the length of original data being trimmed
549*9c5db199SXin Li              from the end.
550*9c5db199SXin Li              Returns ([], None) if there is no valid data.
551*9c5db199SXin Li    """
552*9c5db199SXin Li    indice_valid = [
553*9c5db199SXin Li            i for i, j in enumerate(data) if abs(j) > threshold]
554*9c5db199SXin Li    if not indice_valid:
555*9c5db199SXin Li        logging.warning(
556*9c5db199SXin Li                'There is no element with absolute value greater '
557*9c5db199SXin Li                'than threshold %f', threshold)
558*9c5db199SXin Li        return [], None
559*9c5db199SXin Li    logging.debug('Start and end of indice_valid: %d, %d',
560*9c5db199SXin Li                  indice_valid[0], indice_valid[-1])
561*9c5db199SXin Li    end_trimmed_length = len(data) - indice_valid[-1] - 1
562*9c5db199SXin Li    logging.debug('Trimmed length in the end: %d', end_trimmed_length)
563*9c5db199SXin Li    return (data[indice_valid[0] : indice_valid[-1] + 1], end_trimmed_length)
564*9c5db199SXin Li
565*9c5db199SXin Li
566*9c5db199SXin Lidef get_one_channel_correlation(test_data, golden_data):
567*9c5db199SXin Li    """Gets max cross-correlation of test_data and golden_data.
568*9c5db199SXin Li
569*9c5db199SXin Li    Trims test data and compute the max cross-correlation against golden_data.
570*9c5db199SXin Li    Signal can be trimmed because those zero values in the head and tail of
571*9c5db199SXin Li    a signal will not affect correlation computation.
572*9c5db199SXin Li
573*9c5db199SXin Li    @param test_data: A list containing the data to compare against golden data.
574*9c5db199SXin Li    @param golden_data: A list containing the golden data.
575*9c5db199SXin Li
576*9c5db199SXin Li    @returns: A tuple (max cross-correlation, best_delay) if data is valid.
577*9c5db199SXin Li              Otherwise returns (None, None). Refer to docstring of
578*9c5db199SXin Li              get_max_cross_correlation.
579*9c5db199SXin Li    """
580*9c5db199SXin Li    trimmed_test_data, end_trimmed_length = trim_data(test_data)
581*9c5db199SXin Li
582*9c5db199SXin Li    def to_float(samples):
583*9c5db199SXin Li        """Casts elements in the list to float.
584*9c5db199SXin Li
585*9c5db199SXin Li      @param samples: A list of numbers.
586*9c5db199SXin Li
587*9c5db199SXin Li      @returns: A list of original numbers casted to float.
588*9c5db199SXin Li      """
589*9c5db199SXin Li        samples_float = [float(x) for x in samples]
590*9c5db199SXin Li        return samples_float
591*9c5db199SXin Li
592*9c5db199SXin Li    max_cross_correlation, best_delay =  get_max_cross_correlation(
593*9c5db199SXin Li            to_float(golden_data),
594*9c5db199SXin Li            to_float(trimmed_test_data))
595*9c5db199SXin Li
596*9c5db199SXin Li    # The reason to add back the trimmed length in the end.
597*9c5db199SXin Li    # E.g.:
598*9c5db199SXin Li    # golden data:
599*9c5db199SXin Li    #
600*9c5db199SXin Li    # |-----------vvvv----------------|  vvvv is the signal of interest.
601*9c5db199SXin Li    #       a                 b
602*9c5db199SXin Li    #
603*9c5db199SXin Li    # test data:
604*9c5db199SXin Li    #
605*9c5db199SXin Li    # |---x----vvvv--------x----------------|  x is the place to trim.
606*9c5db199SXin Li    #   c   d         e            f
607*9c5db199SXin Li    #
608*9c5db199SXin Li    # trimmed test data:
609*9c5db199SXin Li    #
610*9c5db199SXin Li    # |----vvvv--------|
611*9c5db199SXin Li    #   d         e
612*9c5db199SXin Li    #
613*9c5db199SXin Li    # The first output of cross correlation computation :
614*9c5db199SXin Li    #
615*9c5db199SXin Li    #                  |-----------vvvv----------------|
616*9c5db199SXin Li    #                       a                 b
617*9c5db199SXin Li    #
618*9c5db199SXin Li    # |----vvvv--------|
619*9c5db199SXin Li    #   d         e
620*9c5db199SXin Li    #
621*9c5db199SXin Li    # The largest output of cross correlation computation happens at
622*9c5db199SXin Li    # delay a + e.
623*9c5db199SXin Li    #
624*9c5db199SXin Li    #                  |-----------vvvv----------------|
625*9c5db199SXin Li    #                       a                 b
626*9c5db199SXin Li    #
627*9c5db199SXin Li    #                         |----vvvv--------|
628*9c5db199SXin Li    #                           d         e
629*9c5db199SXin Li    #
630*9c5db199SXin Li    # Cross correlation starts computing by aligning the last sample
631*9c5db199SXin Li    # of the trimmed test data to the first sample of golden data.
632*9c5db199SXin Li    # The best delay calculated from trimmed test data and golden data
633*9c5db199SXin Li    # cross correlation is e + a. But the real best delay that should be
634*9c5db199SXin Li    # identical on two channel should be e + a + f.
635*9c5db199SXin Li    # So we need to add back the length being trimmed in the end.
636*9c5db199SXin Li
637*9c5db199SXin Li    if max_cross_correlation:
638*9c5db199SXin Li        return max_cross_correlation, best_delay + end_trimmed_length
639*9c5db199SXin Li    else:
640*9c5db199SXin Li        return None, None
641*9c5db199SXin Li
642*9c5db199SXin Li
643*9c5db199SXin Lidef compare_one_channel_correlation(test_data, golden_data, parameters):
644*9c5db199SXin Li    """Compares two one-channel data by correlation.
645*9c5db199SXin Li
646*9c5db199SXin Li    @param test_data: A list containing the data to compare against golden data.
647*9c5db199SXin Li    @param golden_data: A list containing the golden data.
648*9c5db199SXin Li    @param parameters: A dict containing parameters for method.
649*9c5db199SXin Li
650*9c5db199SXin Li    @returns: A dict containing:
651*9c5db199SXin Li              index: The index of similarity where 1 means they are different
652*9c5db199SXin Li                  only by a positive scale.
653*9c5db199SXin Li              best_delay: The best delay of test data in relative to golden
654*9c5db199SXin Li                  data.
655*9c5db199SXin Li              equal: A bool containing comparing result.
656*9c5db199SXin Li    """
657*9c5db199SXin Li    if 'correlation_threshold' in parameters:
658*9c5db199SXin Li        threshold = parameters['correlation_threshold']
659*9c5db199SXin Li    else:
660*9c5db199SXin Li        threshold = _CORRELATION_INDEX_THRESHOLD
661*9c5db199SXin Li
662*9c5db199SXin Li    result_dict = dict()
663*9c5db199SXin Li    max_cross_correlation, best_delay = get_one_channel_correlation(
664*9c5db199SXin Li            test_data, golden_data)
665*9c5db199SXin Li    result_dict['index'] = max_cross_correlation
666*9c5db199SXin Li    result_dict['best_delay'] = best_delay
667*9c5db199SXin Li    result_dict['equal'] = True if (
668*9c5db199SXin Li        max_cross_correlation and
669*9c5db199SXin Li        max_cross_correlation > threshold) else False
670*9c5db199SXin Li    logging.debug('result_dict: %r', result_dict)
671*9c5db199SXin Li    return result_dict
672*9c5db199SXin Li
673*9c5db199SXin Li
674*9c5db199SXin Lidef compare_data_correlation(golden_data_binary, golden_data_format,
675*9c5db199SXin Li                             test_data_binary, test_data_format,
676*9c5db199SXin Li                             channel_map, parameters=None):
677*9c5db199SXin Li    """Compares two raw data using correlation.
678*9c5db199SXin Li
679*9c5db199SXin Li    @param golden_data_binary: The binary containing golden data.
680*9c5db199SXin Li    @param golden_data_format: The data format of golden data.
681*9c5db199SXin Li    @param test_data_binary: The binary containing test data.
682*9c5db199SXin Li    @param test_data_format: The data format of test data.
683*9c5db199SXin Li    @param channel_map: A list containing channel mapping.
684*9c5db199SXin Li                        E.g. [1, 0, None, None, None, None, None, None] means
685*9c5db199SXin Li                        channel 0 of test data should map to channel 1 of
686*9c5db199SXin Li                        golden data. Channel 1 of test data should map to
687*9c5db199SXin Li                        channel 0 of golden data. Channel 2 to 7 of test data
688*9c5db199SXin Li                        should be skipped.
689*9c5db199SXin Li    @param parameters: A dict containing parameters for method, if needed.
690*9c5db199SXin Li
691*9c5db199SXin Li    @raises: NotImplementedError if file type is not raw.
692*9c5db199SXin Li             NotImplementedError if sampling rates of two data are not the same.
693*9c5db199SXin Li             error.TestFail if golden data and test data are not equal.
694*9c5db199SXin Li    """
695*9c5db199SXin Li    if parameters is None:
696*9c5db199SXin Li        parameters = dict()
697*9c5db199SXin Li
698*9c5db199SXin Li    if (golden_data_format['file_type'] != 'raw' or
699*9c5db199SXin Li        test_data_format['file_type'] != 'raw'):
700*9c5db199SXin Li        raise NotImplementedError('Only support raw data in compare_data.')
701*9c5db199SXin Li    if (golden_data_format['rate'] != test_data_format['rate']):
702*9c5db199SXin Li        raise NotImplementedError(
703*9c5db199SXin Li                'Only support comparing data with the same sampling rate')
704*9c5db199SXin Li    golden_data = audio_data.AudioRawData(
705*9c5db199SXin Li            binary=golden_data_binary,
706*9c5db199SXin Li            channel=golden_data_format['channel'],
707*9c5db199SXin Li            sample_format=golden_data_format['sample_format'])
708*9c5db199SXin Li    test_data = audio_data.AudioRawData(
709*9c5db199SXin Li            binary=test_data_binary,
710*9c5db199SXin Li            channel=test_data_format['channel'],
711*9c5db199SXin Li            sample_format=test_data_format['sample_format'])
712*9c5db199SXin Li    compare_results = []
713*9c5db199SXin Li    for test_channel, golden_channel in enumerate(channel_map):
714*9c5db199SXin Li        if golden_channel is None:
715*9c5db199SXin Li            logging.info('Skipped channel %d', test_channel)
716*9c5db199SXin Li            continue
717*9c5db199SXin Li        test_data_one_channel = test_data.channel_data[test_channel]
718*9c5db199SXin Li        golden_data_one_channel = golden_data.channel_data[golden_channel]
719*9c5db199SXin Li        result_dict = dict(test_channel=test_channel,
720*9c5db199SXin Li                           golden_channel=golden_channel)
721*9c5db199SXin Li        result_dict.update(
722*9c5db199SXin Li                compare_one_channel_correlation(
723*9c5db199SXin Li                        test_data_one_channel, golden_data_one_channel,
724*9c5db199SXin Li                        parameters))
725*9c5db199SXin Li        compare_results.append(result_dict)
726*9c5db199SXin Li    logging.info('compare_results: %r', compare_results)
727*9c5db199SXin Li    for result in compare_results:
728*9c5db199SXin Li        if not result['equal']:
729*9c5db199SXin Li            error_msg = ('Failed on test channel %d and golden channel %d with '
730*9c5db199SXin Li                         'index %f') % (
731*9c5db199SXin Li                                 result['test_channel'],
732*9c5db199SXin Li                                 result['golden_channel'],
733*9c5db199SXin Li                                 result['index'])
734*9c5db199SXin Li            logging.error(error_msg)
735*9c5db199SXin Li            raise error.TestFail(error_msg)
736*9c5db199SXin Li    # Also checks best delay are exactly the same.
737*9c5db199SXin Li    best_delays = set([result['best_delay'] for result in compare_results])
738*9c5db199SXin Li    if len(best_delays) > 1:
739*9c5db199SXin Li        error_msg = 'There are more than one best delay: %s' % best_delays
740*9c5db199SXin Li        logging.error(error_msg)
741*9c5db199SXin Li        raise error.TestFail(error_msg)
742*9c5db199SXin Li
743*9c5db199SXin Li
744*9c5db199SXin Lidef recorded_filesize_check(filesize,
745*9c5db199SXin Li                            duration,
746*9c5db199SXin Li                            channels=1,
747*9c5db199SXin Li                            rate=48000,
748*9c5db199SXin Li                            bits=16,
749*9c5db199SXin Li                            tolerant_ratio=0.1):
750*9c5db199SXin Li    """Checks the recorded file size is correct. The check will pass if the
751*9c5db199SXin Li    file size is larger than expected and smaller than our tolerant size. We
752*9c5db199SXin Li    can tolerate size larger than expected as the way cras_test_client stop
753*9c5db199SXin Li    recording base on duration is not always accurate but should record longer
754*9c5db199SXin Li    than expected audio.
755*9c5db199SXin Li
756*9c5db199SXin Li    @param filesize: Actually file size.
757*9c5db199SXin Li    @param duration: Expected seconds to record.
758*9c5db199SXin Li    @param channels: Number of channels to record.
759*9c5db199SXin Li    @param rate: Recording sample rate.
760*9c5db199SXin Li    @param bits: The sample bit width.
761*9c5db199SXin Li    @param tolerant_ratio: The larger than expected file size ratio that we
762*9c5db199SXin Li    regard as pass.
763*9c5db199SXin Li
764*9c5db199SXin Li    @raises: TestFail if the size is less or larger than expect.
765*9c5db199SXin Li    """
766*9c5db199SXin Li    expected = duration * channels * (bits // 8) * rate
767*9c5db199SXin Li    ratio = abs(float(filesize) / expected)
768*9c5db199SXin Li    if ratio < 1 or ratio > 1 + tolerant_ratio:
769*9c5db199SXin Li        raise error.TestFail(
770*9c5db199SXin Li                'File size not correct: %d expect: %d' % (filesize, expected))
771*9c5db199SXin Li
772*9c5db199SXin Li
773*9c5db199SXin Liclass _base_rms_test(test.test):
774*9c5db199SXin Li    """Base class for all rms_test """
775*9c5db199SXin Li
776*9c5db199SXin Li    def postprocess(self):
777*9c5db199SXin Li        super(_base_rms_test, self).postprocess()
778*9c5db199SXin Li
779*9c5db199SXin Li        # Sum up the number of failed constraints in each iteration
780*9c5db199SXin Li        if sum(len(x) for x in self.failed_constraints):
781*9c5db199SXin Li            dump_audio_diagnostics(test.resultsdir)
782*9c5db199SXin Li
783*9c5db199SXin Li
784*9c5db199SXin Liclass chrome_rms_test(_base_rms_test):
785*9c5db199SXin Li    """Base test class for audio RMS test with Chrome.
786*9c5db199SXin Li
787*9c5db199SXin Li    The chrome instance can be accessed by self.chrome.
788*9c5db199SXin Li    """
789*9c5db199SXin Li    def warmup(self):
790*9c5db199SXin Li        super(chrome_rms_test, self).warmup()
791*9c5db199SXin Li
792*9c5db199SXin Li        # Not all client of this file using telemetry.
793*9c5db199SXin Li        # Just do the import here for those who really need it.
794*9c5db199SXin Li        from autotest_lib.client.common_lib.cros import chrome
795*9c5db199SXin Li
796*9c5db199SXin Li        self.chrome = chrome.Chrome(init_network_controller=True)
797*9c5db199SXin Li
798*9c5db199SXin Li        # The audio configuration could be changed when we
799*9c5db199SXin Li        # restart chrome.
800*9c5db199SXin Li        try:
801*9c5db199SXin Li            cras_rms_test_setup()
802*9c5db199SXin Li        except Exception:
803*9c5db199SXin Li            self.chrome.browser.Close()
804*9c5db199SXin Li            raise
805*9c5db199SXin Li
806*9c5db199SXin Li
807*9c5db199SXin Li    def cleanup(self, *args):
808*9c5db199SXin Li        try:
809*9c5db199SXin Li            self.chrome.close()
810*9c5db199SXin Li        finally:
811*9c5db199SXin Li            super(chrome_rms_test, self).cleanup()
812*9c5db199SXin Li
813*9c5db199SXin Liclass cras_rms_test(_base_rms_test):
814*9c5db199SXin Li    """Base test class for CRAS audio RMS test."""
815*9c5db199SXin Li
816*9c5db199SXin Li    def warmup(self):
817*9c5db199SXin Li        super(cras_rms_test, self).warmup()
818*9c5db199SXin Li        # Stop ui to make sure there are not other streams.
819*9c5db199SXin Li        utils.stop_service('ui', ignore_status=True)
820*9c5db199SXin Li        cras_rms_test_setup()
821*9c5db199SXin Li
822*9c5db199SXin Li    def cleanup(self, *args):
823*9c5db199SXin Li        # Restart ui.
824*9c5db199SXin Li        utils.start_service('ui', ignore_status=True)
825*9c5db199SXin Li
826*9c5db199SXin Li
827*9c5db199SXin Liclass alsa_rms_test(_base_rms_test):
828*9c5db199SXin Li    """Base test class for ALSA audio RMS test.
829*9c5db199SXin Li
830*9c5db199SXin Li    Note the warmup will take 10 seconds and the device cannot be used before it
831*9c5db199SXin Li    returns.
832*9c5db199SXin Li    """
833*9c5db199SXin Li    def warmup(self):
834*9c5db199SXin Li        super(alsa_rms_test, self).warmup()
835*9c5db199SXin Li
836*9c5db199SXin Li        cras_rms_test_setup()
837*9c5db199SXin Li        # We need CRAS to initialize the volume and gain.
838*9c5db199SXin Li        cras_utils.playback(playback_file="/dev/zero", duration=1)
839*9c5db199SXin Li        # CRAS will release the device after 10 seconds.
840*9c5db199SXin Li        time.sleep(11)
841