xref: /aosp_15_r20/external/autotest/client/cros/audio/visqol_utils.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
1*9c5db199SXin Li# Copyright 2021 The Chromium OS Authors. All rights reserved.
2*9c5db199SXin Li# Use of this source code is governed by a BSD-style license that can be
3*9c5db199SXin Li# found in the LICENSE file.
4*9c5db199SXin Li
5*9c5db199SXin Liimport os
6*9c5db199SXin Liimport re
7*9c5db199SXin Liimport subprocess
8*9c5db199SXin Li
9*9c5db199SXin Lifrom autotest_lib.client.common_lib import error
10*9c5db199SXin Lifrom autotest_lib.client.cros.bluetooth.bluetooth_audio_test_data import (
11*9c5db199SXin Li        VISQOL_PATH, VISQOL_SIMILARITY_MODEL)
12*9c5db199SXin Li
13*9c5db199SXin Li
14*9c5db199SXin Lidef parse_visqol_output(stdout, stderr, log_dir):
15*9c5db199SXin Li    """
16*9c5db199SXin Li    Parses stdout and stderr string from VISQOL output and parse into
17*9c5db199SXin Li    a float score.
18*9c5db199SXin Li
19*9c5db199SXin Li    On error, stderr will contain the error message, otherwise will be None.
20*9c5db199SXin Li    On success, stdout will be a string, first line will be
21*9c5db199SXin Li    VISQOL version, followed by indication of speech mode. Followed by
22*9c5db199SXin Li    paths to reference and degraded file, and a float MOS-LQO score, which
23*9c5db199SXin Li    is what we're interested in. Followed by more detailed charts about
24*9c5db199SXin Li    specific scoring by segments of the files. Stdout is None on error.
25*9c5db199SXin Li
26*9c5db199SXin Li    @param stdout: The stdout bytes from commandline output of VISQOL.
27*9c5db199SXin Li    @param stderr: The stderr bytes from commandline output of VISQOL.
28*9c5db199SXin Li    @param log_dir: Directory path for storing VISQOL log.
29*9c5db199SXin Li
30*9c5db199SXin Li    @returns: A tuple of a float score and string representation of the
31*9c5db199SXin Li            srderr or None if there was no error.
32*9c5db199SXin Li    """
33*9c5db199SXin Li    stdout = '' if stdout is None else stdout.decode('utf-8')
34*9c5db199SXin Li    stderr = '' if stderr is None else stderr.decode('utf-8')
35*9c5db199SXin Li
36*9c5db199SXin Li    # Log verbose VISQOL output:
37*9c5db199SXin Li    log_file = os.path.join(log_dir, 'VISQOL_LOG.txt')
38*9c5db199SXin Li    with open(log_file, 'a+') as f:
39*9c5db199SXin Li        f.write('String Error:\n{}\n'.format(stderr))
40*9c5db199SXin Li        f.write('String Out:\n{}\n'.format(stdout))
41*9c5db199SXin Li
42*9c5db199SXin Li    # pattern matches first float or int after 'MOS-LQO:' in stdout,
43*9c5db199SXin Li    # e.g. it would match the line 'MOS-LQO       2.3' in the stdout
44*9c5db199SXin Li    score_pattern = re.compile(r'.*MOS-LQO:\s*(\d+.?\d*)')
45*9c5db199SXin Li    score_search = re.search(score_pattern, stdout)
46*9c5db199SXin Li
47*9c5db199SXin Li    # re.search returns None if no pattern match found, otherwise the score
48*9c5db199SXin Li    # would be in the match object's group 1 matches just the float score
49*9c5db199SXin Li    score = float(score_search.group(1)) if score_search else -1.0
50*9c5db199SXin Li    return stderr, score
51*9c5db199SXin Li
52*9c5db199SXin Li
53*9c5db199SXin Lidef get_visqol_score(ref_file,
54*9c5db199SXin Li                     deg_file,
55*9c5db199SXin Li                     log_dir,
56*9c5db199SXin Li                     speech_mode=True,
57*9c5db199SXin Li                     verbose=True):
58*9c5db199SXin Li    """
59*9c5db199SXin Li    Runs VISQOL using the subprocess library on the provided reference file
60*9c5db199SXin Li    and degraded file and returns the VISQOL score.
61*9c5db199SXin Li
62*9c5db199SXin Li    Notes that the difference between the duration of reference and degraded
63*9c5db199SXin Li    audio must be smaller than 1.0 second.
64*9c5db199SXin Li
65*9c5db199SXin Li    @param ref_file: File path to the reference wav file.
66*9c5db199SXin Li    @param deg_file: File path to the degraded wav file.
67*9c5db199SXin Li    @param log_dir: Directory path for storing VISQOL log.
68*9c5db199SXin Li    @param speech_mode: [Optional] Defaults to True, accepts 16k sample
69*9c5db199SXin Li            rate files and ignores frequencies > 8kHz for scoring.
70*9c5db199SXin Li    @param verbose: [Optional] Defaults to True, outputs more details.
71*9c5db199SXin Li
72*9c5db199SXin Li    @returns: A float score for the tested file.
73*9c5db199SXin Li    """
74*9c5db199SXin Li    visqol_cmd = [VISQOL_PATH]
75*9c5db199SXin Li    visqol_cmd += ['--reference_file', ref_file]
76*9c5db199SXin Li    visqol_cmd += ['--degraded_file', deg_file]
77*9c5db199SXin Li    visqol_cmd += ['--similarity_to_quality_model', VISQOL_SIMILARITY_MODEL]
78*9c5db199SXin Li
79*9c5db199SXin Li    if speech_mode:
80*9c5db199SXin Li        visqol_cmd.append('--use_speech_mode')
81*9c5db199SXin Li    if verbose:
82*9c5db199SXin Li        visqol_cmd.append('--verbose')
83*9c5db199SXin Li
84*9c5db199SXin Li    visqol_process = subprocess.Popen(visqol_cmd,
85*9c5db199SXin Li                                      stdout=subprocess.PIPE,
86*9c5db199SXin Li                                      stderr=subprocess.PIPE)
87*9c5db199SXin Li    stdout, stderr = visqol_process.communicate()
88*9c5db199SXin Li
89*9c5db199SXin Li    err, score = parse_visqol_output(stdout, stderr, log_dir)
90*9c5db199SXin Li
91*9c5db199SXin Li    if err:
92*9c5db199SXin Li        raise error.TestError(err)
93*9c5db199SXin Li    elif score < 0.0:
94*9c5db199SXin Li        raise error.TestError('Failed to parse score, got {}'.format(score))
95*9c5db199SXin Li
96*9c5db199SXin Li    return score
97