| # Copyright 2021 The Chromium OS Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| import os |
| import re |
| import subprocess |
| |
| from autotest_lib.client.common_lib import error |
| from autotest_lib.client.cros.bluetooth.bluetooth_audio_test_data import ( |
| VISQOL_PATH, VISQOL_SIMILARITY_MODEL) |
| |
| |
| def parse_visqol_output(stdout, stderr, log_dir): |
| """ |
| Parses stdout and stderr string from VISQOL output and parse into |
| a float score. |
| |
| On error, stderr will contain the error message, otherwise will be None. |
| On success, stdout will be a string, first line will be |
| VISQOL version, followed by indication of speech mode. Followed by |
| paths to reference and degraded file, and a float MOS-LQO score, which |
| is what we're interested in. Followed by more detailed charts about |
| specific scoring by segments of the files. Stdout is None on error. |
| |
| @param stdout: The stdout bytes from commandline output of VISQOL. |
| @param stderr: The stderr bytes from commandline output of VISQOL. |
| @param log_dir: Directory path for storing VISQOL log. |
| |
| @returns: A tuple of a float score and string representation of the |
| srderr or None if there was no error. |
| """ |
| stdout = '' if stdout is None else stdout.decode('utf-8') |
| stderr = '' if stderr is None else stderr.decode('utf-8') |
| |
| # Log verbose VISQOL output: |
| log_file = os.path.join(log_dir, 'VISQOL_LOG.txt') |
| with open(log_file, 'a+') as f: |
| f.write('String Error:\n{}\n'.format(stderr)) |
| f.write('String Out:\n{}\n'.format(stdout)) |
| |
| # pattern matches first float or int after 'MOS-LQO:' in stdout, |
| # e.g. it would match the line 'MOS-LQO 2.3' in the stdout |
| score_pattern = re.compile(r'.*MOS-LQO:\s*(\d+.?\d*)') |
| score_search = re.search(score_pattern, stdout) |
| |
| # re.search returns None if no pattern match found, otherwise the score |
| # would be in the match object's group 1 matches just the float score |
| score = float(score_search.group(1)) if score_search else -1.0 |
| return stderr, score |
| |
| |
| def get_visqol_score(ref_file, |
| deg_file, |
| log_dir, |
| speech_mode=True, |
| verbose=True): |
| """ |
| Runs VISQOL using the subprocess library on the provided reference file |
| and degraded file and returns the VISQOL score. |
| |
| Notes that the difference between the duration of reference and degraded |
| audio must be smaller than 1.0 second. |
| |
| @param ref_file: File path to the reference wav file. |
| @param deg_file: File path to the degraded wav file. |
| @param log_dir: Directory path for storing VISQOL log. |
| @param speech_mode: [Optional] Defaults to True, accepts 16k sample |
| rate files and ignores frequencies > 8kHz for scoring. |
| @param verbose: [Optional] Defaults to True, outputs more details. |
| |
| @returns: A float score for the tested file. |
| """ |
| visqol_cmd = [VISQOL_PATH] |
| visqol_cmd += ['--reference_file', ref_file] |
| visqol_cmd += ['--degraded_file', deg_file] |
| visqol_cmd += ['--similarity_to_quality_model', VISQOL_SIMILARITY_MODEL] |
| |
| if speech_mode: |
| visqol_cmd.append('--use_speech_mode') |
| if verbose: |
| visqol_cmd.append('--verbose') |
| |
| visqol_process = subprocess.Popen(visqol_cmd, |
| stdout=subprocess.PIPE, |
| stderr=subprocess.PIPE) |
| stdout, stderr = visqol_process.communicate() |
| |
| err, score = parse_visqol_output(stdout, stderr, log_dir) |
| |
| if err: |
| raise error.TestError(err) |
| elif score < 0.0: |
| raise error.TestError('Failed to parse score, got {}'.format(score)) |
| |
| return score |