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