xref: /aosp_15_r20/external/autotest/client/cros/audio/check_quality.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
1*9c5db199SXin Li#!/usr/bin/env python3
2*9c5db199SXin Li# Lint as: python2, python3
3*9c5db199SXin Li# Copyright 2016 The Chromium OS Authors. All rights reserved.
4*9c5db199SXin Li# Use of this source code is governed by a BSD-style license that can be
5*9c5db199SXin Li# found in the LICENSE file.
6*9c5db199SXin Li
7*9c5db199SXin Li"""Command line tool to analyze wave file and detect artifacts."""
8*9c5db199SXin Li
9*9c5db199SXin Lifrom __future__ import absolute_import
10*9c5db199SXin Lifrom __future__ import division
11*9c5db199SXin Lifrom __future__ import print_function
12*9c5db199SXin Liimport argparse
13*9c5db199SXin Liimport collections
14*9c5db199SXin Liimport json
15*9c5db199SXin Liimport logging
16*9c5db199SXin Liimport numpy
17*9c5db199SXin Liimport pprint
18*9c5db199SXin Liimport subprocess
19*9c5db199SXin Liimport tempfile
20*9c5db199SXin Liimport wave
21*9c5db199SXin Lifrom six.moves import range
22*9c5db199SXin Li
23*9c5db199SXin Li# Normal autotest environment.
24*9c5db199SXin Litry:
25*9c5db199SXin Li    import common
26*9c5db199SXin Li    from autotest_lib.client.cros.audio import audio_analysis
27*9c5db199SXin Li    from autotest_lib.client.cros.audio import audio_data
28*9c5db199SXin Li    from autotest_lib.client.cros.audio import audio_quality_measurement
29*9c5db199SXin Li# Standalone execution without autotest environment.
30*9c5db199SXin Liexcept ImportError:
31*9c5db199SXin Li    import audio_analysis
32*9c5db199SXin Li    import audio_data
33*9c5db199SXin Li    import audio_quality_measurement
34*9c5db199SXin Li
35*9c5db199SXin Li
36*9c5db199SXin Li# Holder for quality parameters used in audio_quality_measurement module.
37*9c5db199SXin LiQualityParams = collections.namedtuple('QualityParams',
38*9c5db199SXin Li      ['block_size_secs',
39*9c5db199SXin Li       'frequency_error_threshold',
40*9c5db199SXin Li       'delay_amplitude_threshold',
41*9c5db199SXin Li       'noise_amplitude_threshold',
42*9c5db199SXin Li       'burst_amplitude_threshold'])
43*9c5db199SXin Li
44*9c5db199SXin Li
45*9c5db199SXin Lidef add_args(parser):
46*9c5db199SXin Li    """Adds command line arguments."""
47*9c5db199SXin Li    parser.add_argument('filename', metavar='FILE', type=str,
48*9c5db199SXin Li                        help='The wav or raw file to check.'
49*9c5db199SXin Li                             'The file format is determined by file extension.'
50*9c5db199SXin Li                             'For raw format, user must also pass -b, -r, -c'
51*9c5db199SXin Li                             'for bit width, rate, and number of channels.')
52*9c5db199SXin Li    parser.add_argument('--debug', action='store_true', default=False,
53*9c5db199SXin Li                        help='Show debug message.')
54*9c5db199SXin Li    parser.add_argument('--spectral-only', action='store_true', default=False,
55*9c5db199SXin Li                        help='Only do spectral analysis on each channel.')
56*9c5db199SXin Li    parser.add_argument('--freqs', metavar='FREQ', type=float,
57*9c5db199SXin Li                        nargs='*',
58*9c5db199SXin Li                        help='Expected frequencies in the channels. '
59*9c5db199SXin Li                             'Frequencies are separated by space. '
60*9c5db199SXin Li                             'E.g.: --freqs 1000 2000. '
61*9c5db199SXin Li                             'It means only the first two '
62*9c5db199SXin Li                             'channels (1000Hz, 2000Hz) are to be checked. '
63*9c5db199SXin Li                             'Unwanted channels can be specified by 0. '
64*9c5db199SXin Li                             'E.g.: --freqs 1000 0 2000 0 3000. '
65*9c5db199SXin Li                             'It means only channe 0,2,4 are to be examined.')
66*9c5db199SXin Li    parser.add_argument('--freq-threshold', metavar='FREQ_THRESHOLD', type=float,
67*9c5db199SXin Li                        default=5,
68*9c5db199SXin Li                        help='Frequency difference threshold in Hz. '
69*9c5db199SXin Li                             'Default is 5Hz')
70*9c5db199SXin Li    parser.add_argument('--ignore-high-freq', metavar='HIGH_FREQ_THRESHOLD',
71*9c5db199SXin Li                        type=float, default=5000,
72*9c5db199SXin Li                        help='Frequency threshold in Hz to be ignored for '
73*9c5db199SXin Li                             'high frequency. Default is 5KHz')
74*9c5db199SXin Li    parser.add_argument('--output-file', metavar='OUTPUT_FILE', type=str,
75*9c5db199SXin Li                        help='Output file to dump analysis result in JSON format')
76*9c5db199SXin Li    parser.add_argument('-b', '--bit-width', metavar='BIT_WIDTH', type=int,
77*9c5db199SXin Li                        default=32,
78*9c5db199SXin Li                        help='For raw file. Bit width of a sample. '
79*9c5db199SXin Li                             'Assume sample format is little-endian signed int. '
80*9c5db199SXin Li                             'Default is 32')
81*9c5db199SXin Li    parser.add_argument('-r', '--rate', metavar='RATE', type=int,
82*9c5db199SXin Li                        default=48000,
83*9c5db199SXin Li                        help='For raw file. Sampling rate. Default is 48000')
84*9c5db199SXin Li    parser.add_argument('-c', '--channel', metavar='CHANNEL', type=int,
85*9c5db199SXin Li                        default=8,
86*9c5db199SXin Li                        help='For raw file. Number of channels. '
87*9c5db199SXin Li                             'Default is 8.')
88*9c5db199SXin Li
89*9c5db199SXin Li    # Arguments for quality measurement customization.
90*9c5db199SXin Li    parser.add_argument(
91*9c5db199SXin Li             '--quality-block-size-secs',
92*9c5db199SXin Li             metavar='BLOCK_SIZE_SECS', type=float,
93*9c5db199SXin Li             default=audio_quality_measurement.DEFAULT_BLOCK_SIZE_SECS,
94*9c5db199SXin Li             help='Block size for quality measurement. '
95*9c5db199SXin Li                  'Refer to audio_quality_measurement module for detail.')
96*9c5db199SXin Li    parser.add_argument(
97*9c5db199SXin Li             '--quality-frequency-error-threshold',
98*9c5db199SXin Li             metavar='FREQ_ERR_THRESHOLD', type=float,
99*9c5db199SXin Li             default=audio_quality_measurement.DEFAULT_FREQUENCY_ERROR,
100*9c5db199SXin Li             help='Frequency error threshold for identifying sine wave'
101*9c5db199SXin Li                  'in quality measurement. '
102*9c5db199SXin Li                  'Refer to audio_quality_measurement module for detail.')
103*9c5db199SXin Li    parser.add_argument(
104*9c5db199SXin Li             '--quality-delay-amplitude-threshold',
105*9c5db199SXin Li             metavar='DELAY_AMPLITUDE_THRESHOLD', type=float,
106*9c5db199SXin Li             default=audio_quality_measurement.DEFAULT_DELAY_AMPLITUDE_THRESHOLD,
107*9c5db199SXin Li             help='Amplitude ratio threshold for identifying delay in sine wave'
108*9c5db199SXin Li                  'in quality measurement. '
109*9c5db199SXin Li                  'Refer to audio_quality_measurement module for detail.')
110*9c5db199SXin Li    parser.add_argument(
111*9c5db199SXin Li             '--quality-noise-amplitude-threshold',
112*9c5db199SXin Li             metavar='NOISE_AMPLITUDE_THRESHOLD', type=float,
113*9c5db199SXin Li             default=audio_quality_measurement.DEFAULT_NOISE_AMPLITUDE_THRESHOLD,
114*9c5db199SXin Li             help='Amplitude ratio threshold for identifying noise in sine wave'
115*9c5db199SXin Li                  'in quality measurement. '
116*9c5db199SXin Li                  'Refer to audio_quality_measurement module for detail.')
117*9c5db199SXin Li    parser.add_argument(
118*9c5db199SXin Li             '--quality-burst-amplitude-threshold',
119*9c5db199SXin Li             metavar='BURST_AMPLITUDE_THRESHOLD', type=float,
120*9c5db199SXin Li             default=audio_quality_measurement.DEFAULT_BURST_AMPLITUDE_THRESHOLD,
121*9c5db199SXin Li             help='Amplitude ratio threshold for identifying burst in sine wave'
122*9c5db199SXin Li                  'in quality measurement. '
123*9c5db199SXin Li                  'Refer to audio_quality_measurement module for detail.')
124*9c5db199SXin Li
125*9c5db199SXin Li
126*9c5db199SXin Lidef parse_args(parser):
127*9c5db199SXin Li    """Parses args."""
128*9c5db199SXin Li    args = parser.parse_args()
129*9c5db199SXin Li    return args
130*9c5db199SXin Li
131*9c5db199SXin Li
132*9c5db199SXin Liclass WaveFileException(Exception):
133*9c5db199SXin Li    """Error in WaveFile."""
134*9c5db199SXin Li    pass
135*9c5db199SXin Li
136*9c5db199SXin Li
137*9c5db199SXin Liclass WaveFormatExtensibleException(Exception):
138*9c5db199SXin Li    """Wave file is in WAVE_FORMAT_EXTENSIBLE format which is not supported."""
139*9c5db199SXin Li    pass
140*9c5db199SXin Li
141*9c5db199SXin Li
142*9c5db199SXin Liclass WaveFile(object):
143*9c5db199SXin Li    """Class which handles wave file reading.
144*9c5db199SXin Li
145*9c5db199SXin Li    Properties:
146*9c5db199SXin Li        raw_data: audio_data.AudioRawData object for data in wave file.
147*9c5db199SXin Li        rate: sampling rate.
148*9c5db199SXin Li
149*9c5db199SXin Li    """
150*9c5db199SXin Li    def __init__(self, filename):
151*9c5db199SXin Li        """Inits a wave file.
152*9c5db199SXin Li
153*9c5db199SXin Li        @param filename: file name of the wave file.
154*9c5db199SXin Li
155*9c5db199SXin Li        """
156*9c5db199SXin Li        self.raw_data = None
157*9c5db199SXin Li        self.rate = None
158*9c5db199SXin Li
159*9c5db199SXin Li        self._wave_reader = None
160*9c5db199SXin Li        self._n_channels = None
161*9c5db199SXin Li        self._sample_width_bits = None
162*9c5db199SXin Li        self._n_frames = None
163*9c5db199SXin Li        self._binary = None
164*9c5db199SXin Li
165*9c5db199SXin Li        try:
166*9c5db199SXin Li            self._read_wave_file(filename)
167*9c5db199SXin Li        except WaveFormatExtensibleException:
168*9c5db199SXin Li            logging.warning(
169*9c5db199SXin Li                    'WAVE_FORMAT_EXTENSIBLE is not supproted. '
170*9c5db199SXin Li                    'Try command "sox in.wav -t wavpcm out.wav" to convert '
171*9c5db199SXin Li                    'the file to WAVE_FORMAT_PCM format.')
172*9c5db199SXin Li            self._convert_and_read_wav_file(filename)
173*9c5db199SXin Li
174*9c5db199SXin Li
175*9c5db199SXin Li    def _convert_and_read_wav_file(self, filename):
176*9c5db199SXin Li        """Converts the wav file and read it.
177*9c5db199SXin Li
178*9c5db199SXin Li        Converts the file into WAVE_FORMAT_PCM format using sox command and
179*9c5db199SXin Li        reads its content.
180*9c5db199SXin Li
181*9c5db199SXin Li        @param filename: The wave file to be read.
182*9c5db199SXin Li
183*9c5db199SXin Li        @raises: RuntimeError: sox is not installed.
184*9c5db199SXin Li
185*9c5db199SXin Li        """
186*9c5db199SXin Li        # Checks if sox is installed.
187*9c5db199SXin Li        try:
188*9c5db199SXin Li            subprocess.check_output(['sox', '--version'])
189*9c5db199SXin Li        except:
190*9c5db199SXin Li            raise RuntimeError('sox command is not installed. '
191*9c5db199SXin Li                               'Try sudo apt-get install sox')
192*9c5db199SXin Li
193*9c5db199SXin Li        with tempfile.NamedTemporaryFile(suffix='.wav') as converted_file:
194*9c5db199SXin Li            command = ['sox', filename, '-t', 'wavpcm', converted_file.name]
195*9c5db199SXin Li            logging.debug('Convert the file using sox: %s', command)
196*9c5db199SXin Li            subprocess.check_call(command)
197*9c5db199SXin Li            self._read_wave_file(converted_file.name)
198*9c5db199SXin Li
199*9c5db199SXin Li
200*9c5db199SXin Li    def _read_wave_file(self, filename):
201*9c5db199SXin Li        """Reads wave file header and samples.
202*9c5db199SXin Li
203*9c5db199SXin Li        @param filename: The wave file to be read.
204*9c5db199SXin Li
205*9c5db199SXin Li        @raises WaveFormatExtensibleException: Wave file is in
206*9c5db199SXin Li                                               WAVE_FORMAT_EXTENSIBLE format.
207*9c5db199SXin Li        @raises WaveFileException: Wave file format is not supported.
208*9c5db199SXin Li
209*9c5db199SXin Li        """
210*9c5db199SXin Li        try:
211*9c5db199SXin Li            self._wave_reader = wave.open(filename, 'r')
212*9c5db199SXin Li            self._read_wave_header()
213*9c5db199SXin Li            self._read_wave_binary()
214*9c5db199SXin Li        except wave.Error as e:
215*9c5db199SXin Li            if 'unknown format: 65534' in str(e):
216*9c5db199SXin Li                raise WaveFormatExtensibleException()
217*9c5db199SXin Li            else:
218*9c5db199SXin Li                logging.exception('Unsupported wave format')
219*9c5db199SXin Li                raise WaveFileException()
220*9c5db199SXin Li        finally:
221*9c5db199SXin Li            if self._wave_reader:
222*9c5db199SXin Li                self._wave_reader.close()
223*9c5db199SXin Li
224*9c5db199SXin Li
225*9c5db199SXin Li    def _read_wave_header(self):
226*9c5db199SXin Li        """Reads wave file header.
227*9c5db199SXin Li
228*9c5db199SXin Li        @raises WaveFileException: wave file is compressed.
229*9c5db199SXin Li
230*9c5db199SXin Li        """
231*9c5db199SXin Li        # Header is a tuple of
232*9c5db199SXin Li        # (nchannels, sampwidth, framerate, nframes, comptype, compname).
233*9c5db199SXin Li        header = self._wave_reader.getparams()
234*9c5db199SXin Li        logging.debug('Wave header: %s', header)
235*9c5db199SXin Li
236*9c5db199SXin Li        self._n_channels = header[0]
237*9c5db199SXin Li        self._sample_width_bits = header[1] * 8
238*9c5db199SXin Li        self.rate = header[2]
239*9c5db199SXin Li        self._n_frames = header[3]
240*9c5db199SXin Li        comptype = header[4]
241*9c5db199SXin Li        compname = header[5]
242*9c5db199SXin Li
243*9c5db199SXin Li        if comptype != 'NONE' or compname != 'not compressed':
244*9c5db199SXin Li            raise WaveFileException('Can not support compressed wav file.')
245*9c5db199SXin Li
246*9c5db199SXin Li
247*9c5db199SXin Li    def _read_wave_binary(self):
248*9c5db199SXin Li        """Reads in samples in wave file."""
249*9c5db199SXin Li        self._binary = self._wave_reader.readframes(self._n_frames)
250*9c5db199SXin Li        format_str = 'S%d_LE' % self._sample_width_bits
251*9c5db199SXin Li        self.raw_data = audio_data.AudioRawData(
252*9c5db199SXin Li                binary=self._binary,
253*9c5db199SXin Li                channel=self._n_channels,
254*9c5db199SXin Li                sample_format=format_str)
255*9c5db199SXin Li
256*9c5db199SXin Li
257*9c5db199SXin Li    def get_number_frames(self):
258*9c5db199SXin Li        """Get the number of frames in the wave file."""
259*9c5db199SXin Li        return self._n_frames
260*9c5db199SXin Li
261*9c5db199SXin Li
262*9c5db199SXin Liclass QualityCheckerError(Exception):
263*9c5db199SXin Li    """Error in QualityChecker."""
264*9c5db199SXin Li    pass
265*9c5db199SXin Li
266*9c5db199SXin Li
267*9c5db199SXin Liclass CompareFailure(QualityCheckerError):
268*9c5db199SXin Li    """Exception when frequency comparison fails."""
269*9c5db199SXin Li    pass
270*9c5db199SXin Li
271*9c5db199SXin Li
272*9c5db199SXin Liclass QualityFailure(QualityCheckerError):
273*9c5db199SXin Li    """Exception when quality check fails."""
274*9c5db199SXin Li    pass
275*9c5db199SXin Li
276*9c5db199SXin Li
277*9c5db199SXin Liclass QualityChecker(object):
278*9c5db199SXin Li    """Quality checker controls the flow of checking quality of raw data."""
279*9c5db199SXin Li    def __init__(self, raw_data, rate):
280*9c5db199SXin Li        """Inits a quality checker.
281*9c5db199SXin Li
282*9c5db199SXin Li        @param raw_data: An audio_data.AudioRawData object.
283*9c5db199SXin Li        @param rate: Sampling rate.
284*9c5db199SXin Li
285*9c5db199SXin Li        """
286*9c5db199SXin Li        self._raw_data = raw_data
287*9c5db199SXin Li        self._rate = rate
288*9c5db199SXin Li        self._spectrals = []
289*9c5db199SXin Li        self._quality_result = []
290*9c5db199SXin Li
291*9c5db199SXin Li
292*9c5db199SXin Li    def do_spectral_analysis(self, ignore_high_freq, check_quality,
293*9c5db199SXin Li                             quality_params):
294*9c5db199SXin Li        """Gets the spectral_analysis result.
295*9c5db199SXin Li
296*9c5db199SXin Li        @param ignore_high_freq: Ignore high frequencies above this threshold.
297*9c5db199SXin Li        @param check_quality: Check quality of each channel.
298*9c5db199SXin Li        @param quality_params: A QualityParams object for quality measurement.
299*9c5db199SXin Li
300*9c5db199SXin Li        """
301*9c5db199SXin Li        self.has_data()
302*9c5db199SXin Li        for channel_idx in range(self._raw_data.channel):
303*9c5db199SXin Li            signal = self._raw_data.channel_data[channel_idx]
304*9c5db199SXin Li            max_abs = max(numpy.abs(signal))
305*9c5db199SXin Li            logging.debug('Channel %d max abs signal: %f', channel_idx, max_abs)
306*9c5db199SXin Li            if max_abs == 0:
307*9c5db199SXin Li                logging.info('No data on channel %d, skip this channel',
308*9c5db199SXin Li                              channel_idx)
309*9c5db199SXin Li                continue
310*9c5db199SXin Li
311*9c5db199SXin Li            saturate_value = audio_data.get_maximum_value_from_sample_format(
312*9c5db199SXin Li                    self._raw_data.sample_format)
313*9c5db199SXin Li            normalized_signal = audio_analysis.normalize_signal(
314*9c5db199SXin Li                    signal, saturate_value)
315*9c5db199SXin Li            logging.debug('saturate_value: %f', saturate_value)
316*9c5db199SXin Li            logging.debug('max signal after normalized: %f', max(normalized_signal))
317*9c5db199SXin Li            spectral = audio_analysis.spectral_analysis(
318*9c5db199SXin Li                    normalized_signal, self._rate)
319*9c5db199SXin Li
320*9c5db199SXin Li            logging.debug('Channel %d spectral:\n%s', channel_idx,
321*9c5db199SXin Li                          pprint.pformat(spectral))
322*9c5db199SXin Li
323*9c5db199SXin Li            # Ignore high frequencies above the threshold.
324*9c5db199SXin Li            spectral = [(f, c) for (f, c) in spectral if f < ignore_high_freq]
325*9c5db199SXin Li
326*9c5db199SXin Li            logging.info('Channel %d spectral after ignoring high frequencies '
327*9c5db199SXin Li                          'above %f:\n%s', channel_idx, ignore_high_freq,
328*9c5db199SXin Li                          pprint.pformat(spectral))
329*9c5db199SXin Li
330*9c5db199SXin Li            if check_quality:
331*9c5db199SXin Li                quality = audio_quality_measurement.quality_measurement(
332*9c5db199SXin Li                        signal=normalized_signal,
333*9c5db199SXin Li                        rate=self._rate,
334*9c5db199SXin Li                        dominant_frequency=spectral[0][0],
335*9c5db199SXin Li                        block_size_secs=quality_params.block_size_secs,
336*9c5db199SXin Li                        frequency_error_threshold=quality_params.frequency_error_threshold,
337*9c5db199SXin Li                        delay_amplitude_threshold=quality_params.delay_amplitude_threshold,
338*9c5db199SXin Li                        noise_amplitude_threshold=quality_params.noise_amplitude_threshold,
339*9c5db199SXin Li                        burst_amplitude_threshold=quality_params.burst_amplitude_threshold)
340*9c5db199SXin Li
341*9c5db199SXin Li                logging.debug('Channel %d quality:\n%s', channel_idx,
342*9c5db199SXin Li                              pprint.pformat(quality))
343*9c5db199SXin Li                self._quality_result.append(quality)
344*9c5db199SXin Li
345*9c5db199SXin Li            self._spectrals.append(spectral)
346*9c5db199SXin Li
347*9c5db199SXin Li
348*9c5db199SXin Li    def has_data(self):
349*9c5db199SXin Li        """Checks if data has been set.
350*9c5db199SXin Li
351*9c5db199SXin Li        @raises QualityCheckerError: if data or rate is not set yet.
352*9c5db199SXin Li
353*9c5db199SXin Li        """
354*9c5db199SXin Li        if not self._raw_data or not self._rate:
355*9c5db199SXin Li            raise QualityCheckerError('Data and rate is not set yet')
356*9c5db199SXin Li
357*9c5db199SXin Li
358*9c5db199SXin Li    def check_freqs(self, expected_freqs, freq_threshold):
359*9c5db199SXin Li        """Checks the dominant frequencies in the channels.
360*9c5db199SXin Li
361*9c5db199SXin Li        @param expected_freq: A list of frequencies. If frequency is 0, it
362*9c5db199SXin Li                              means this channel should be ignored.
363*9c5db199SXin Li        @param freq_threshold: The difference threshold to compare two
364*9c5db199SXin Li                               frequencies.
365*9c5db199SXin Li
366*9c5db199SXin Li        """
367*9c5db199SXin Li        logging.debug('expected_freqs: %s', expected_freqs)
368*9c5db199SXin Li        for idx, expected_freq in enumerate(expected_freqs):
369*9c5db199SXin Li            if expected_freq == 0:
370*9c5db199SXin Li                continue
371*9c5db199SXin Li            if not self._spectrals[idx]:
372*9c5db199SXin Li                raise CompareFailure(
373*9c5db199SXin Li                        'Failed at channel %d: no dominant frequency' % idx)
374*9c5db199SXin Li            dominant_freq = self._spectrals[idx][0][0]
375*9c5db199SXin Li            if abs(dominant_freq - expected_freq) > freq_threshold:
376*9c5db199SXin Li                raise CompareFailure(
377*9c5db199SXin Li                        'Failed at channel %d: %f is too far away from %f' % (
378*9c5db199SXin Li                                idx, dominant_freq, expected_freq))
379*9c5db199SXin Li
380*9c5db199SXin Li
381*9c5db199SXin Li    def check_quality(self):
382*9c5db199SXin Li        """Checks the quality measurement results on each channel.
383*9c5db199SXin Li
384*9c5db199SXin Li        @raises: QualityFailure when there is artifact.
385*9c5db199SXin Li
386*9c5db199SXin Li        """
387*9c5db199SXin Li        error_msgs = []
388*9c5db199SXin Li
389*9c5db199SXin Li        for idx, quality_res in enumerate(self._quality_result):
390*9c5db199SXin Li            artifacts = quality_res['artifacts']
391*9c5db199SXin Li            if artifacts['noise_before_playback']:
392*9c5db199SXin Li                error_msgs.append(
393*9c5db199SXin Li                        'Found noise before playback: %s' % (
394*9c5db199SXin Li                                artifacts['noise_before_playback']))
395*9c5db199SXin Li            if artifacts['noise_after_playback']:
396*9c5db199SXin Li                error_msgs.append(
397*9c5db199SXin Li                        'Found noise after playback: %s' % (
398*9c5db199SXin Li                                artifacts['noise_after_playback']))
399*9c5db199SXin Li            if artifacts['delay_during_playback']:
400*9c5db199SXin Li                error_msgs.append(
401*9c5db199SXin Li                        'Found delay during playback: %s' % (
402*9c5db199SXin Li                                artifacts['delay_during_playback']))
403*9c5db199SXin Li            if artifacts['burst_during_playback']:
404*9c5db199SXin Li                error_msgs.append(
405*9c5db199SXin Li                        'Found burst during playback: %s' % (
406*9c5db199SXin Li                                artifacts['burst_during_playback']))
407*9c5db199SXin Li        if error_msgs:
408*9c5db199SXin Li            raise QualityFailure('Found bad quality: %s', '\n'.join(error_msgs))
409*9c5db199SXin Li
410*9c5db199SXin Li
411*9c5db199SXin Li    def dump(self, output_file):
412*9c5db199SXin Li        """Dumps the result into a file in json format.
413*9c5db199SXin Li
414*9c5db199SXin Li        @param output_file: A file path to dump spectral and quality
415*9c5db199SXin Li                            measurement result of each channel.
416*9c5db199SXin Li
417*9c5db199SXin Li        """
418*9c5db199SXin Li        dump_dict = {
419*9c5db199SXin Li            'spectrals': self._spectrals,
420*9c5db199SXin Li            'quality_result': self._quality_result
421*9c5db199SXin Li        }
422*9c5db199SXin Li        with open(output_file, 'w') as f:
423*9c5db199SXin Li            json.dump(dump_dict, f)
424*9c5db199SXin Li
425*9c5db199SXin Li
426*9c5db199SXin Liclass CheckQualityError(Exception):
427*9c5db199SXin Li    """Error in check_quality main function."""
428*9c5db199SXin Li    pass
429*9c5db199SXin Li
430*9c5db199SXin Li
431*9c5db199SXin Lidef read_audio_file(args):
432*9c5db199SXin Li    """Reads audio file.
433*9c5db199SXin Li
434*9c5db199SXin Li    @param args: The namespace parsed from command line arguments.
435*9c5db199SXin Li
436*9c5db199SXin Li    @returns: A tuple (raw_data, rate) where raw_data is
437*9c5db199SXin Li              audio_data.AudioRawData, rate is sampling rate.
438*9c5db199SXin Li
439*9c5db199SXin Li    """
440*9c5db199SXin Li    if args.filename.endswith('.wav'):
441*9c5db199SXin Li        wavefile = WaveFile(args.filename)
442*9c5db199SXin Li        raw_data = wavefile.raw_data
443*9c5db199SXin Li        rate = wavefile.rate
444*9c5db199SXin Li    elif args.filename.endswith('.raw'):
445*9c5db199SXin Li        binary = None
446*9c5db199SXin Li        with open(args.filename, 'rb') as f:
447*9c5db199SXin Li            binary = f.read()
448*9c5db199SXin Li
449*9c5db199SXin Li        raw_data = audio_data.AudioRawData(
450*9c5db199SXin Li                binary=binary,
451*9c5db199SXin Li                channel=args.channel,
452*9c5db199SXin Li                sample_format='S%d_LE' % args.bit_width)
453*9c5db199SXin Li        rate = args.rate
454*9c5db199SXin Li    else:
455*9c5db199SXin Li        raise CheckQualityError(
456*9c5db199SXin Li                'File format for %s is not supported' % args.filename)
457*9c5db199SXin Li
458*9c5db199SXin Li    return raw_data, rate
459*9c5db199SXin Li
460*9c5db199SXin Li
461*9c5db199SXin Lidef get_quality_params(args):
462*9c5db199SXin Li    """Gets quality parameters in arguments.
463*9c5db199SXin Li
464*9c5db199SXin Li    @param args: The namespace parsed from command line arguments.
465*9c5db199SXin Li
466*9c5db199SXin Li    @returns: A QualityParams object.
467*9c5db199SXin Li
468*9c5db199SXin Li    """
469*9c5db199SXin Li    quality_params = QualityParams(
470*9c5db199SXin Li            block_size_secs=args.quality_block_size_secs,
471*9c5db199SXin Li            frequency_error_threshold=args.quality_frequency_error_threshold,
472*9c5db199SXin Li            delay_amplitude_threshold=args.quality_delay_amplitude_threshold,
473*9c5db199SXin Li            noise_amplitude_threshold=args.quality_noise_amplitude_threshold,
474*9c5db199SXin Li            burst_amplitude_threshold=args.quality_burst_amplitude_threshold)
475*9c5db199SXin Li
476*9c5db199SXin Li    return quality_params
477*9c5db199SXin Li
478*9c5db199SXin Li
479*9c5db199SXin Liif __name__ == "__main__":
480*9c5db199SXin Li    parser = argparse.ArgumentParser(
481*9c5db199SXin Li        description='Check signal quality of a wave file. Each channel should'
482*9c5db199SXin Li                    ' either be all zeros, or sine wave of a fixed frequency.')
483*9c5db199SXin Li    add_args(parser)
484*9c5db199SXin Li    args = parse_args(parser)
485*9c5db199SXin Li
486*9c5db199SXin Li    level = logging.DEBUG if args.debug else logging.INFO
487*9c5db199SXin Li    format = '%(asctime)-15s:%(levelname)s:%(pathname)s:%(lineno)d: %(message)s'
488*9c5db199SXin Li    logging.basicConfig(format=format, level=level)
489*9c5db199SXin Li
490*9c5db199SXin Li    raw_data, rate = read_audio_file(args)
491*9c5db199SXin Li
492*9c5db199SXin Li    checker = QualityChecker(raw_data, rate)
493*9c5db199SXin Li
494*9c5db199SXin Li    quality_params = get_quality_params(args)
495*9c5db199SXin Li
496*9c5db199SXin Li    checker.do_spectral_analysis(ignore_high_freq=args.ignore_high_freq,
497*9c5db199SXin Li                                 check_quality=(not args.spectral_only),
498*9c5db199SXin Li                                 quality_params=quality_params)
499*9c5db199SXin Li
500*9c5db199SXin Li    if args.output_file:
501*9c5db199SXin Li        checker.dump(args.output_file)
502*9c5db199SXin Li
503*9c5db199SXin Li    if args.freqs:
504*9c5db199SXin Li        checker.check_freqs(args.freqs, args.freq_threshold)
505*9c5db199SXin Li
506*9c5db199SXin Li    if not args.spectral_only:
507*9c5db199SXin Li        checker.check_quality()
508