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