diff --git a/webrtc/modules/audio_processing/test/py_quality_assessment/README.md b/webrtc/modules/audio_processing/test/py_quality_assessment/README.md index 1d15c29c62..d6e26c90be 100644 --- a/webrtc/modules/audio_processing/test/py_quality_assessment/README.md +++ b/webrtc/modules/audio_processing/test/py_quality_assessment/README.md @@ -12,8 +12,15 @@ reference one used for evaluation. - OS: Linux - Python 2.7 - Python libraries: numpy, scipy, pydub (0.17.0+) - - `$ sudo apt install python-numpy python-scipy` - - `$ sudo pip install pydub` + - It is recommended that a dedicated Python environment is used + - install `virtualenv` + - `$ sudo apt-get install python-virtualenv` + - setup a new Python environment (e.g., `my_env`) + - `$ cd ~ && virtualenv my_env` + - activate the new Python environment + - `$ source ~/my_env/bin/activate` + - add dependcies via `pip` + - `(my_env)$ pip install numpy pydub scipy` - PolqaOem64 (see http://www.polqa.info/) - Tested with POLQA Library v1.180 / P863 v2.400 - Aachen Impulse Response (AIR) Database diff --git a/webrtc/modules/audio_processing/test/py_quality_assessment/apm_quality_assessment.py b/webrtc/modules/audio_processing/test/py_quality_assessment/apm_quality_assessment.py index 7ba0028a8c..fc9c4d2899 100755 --- a/webrtc/modules/audio_processing/test/py_quality_assessment/apm_quality_assessment.py +++ b/webrtc/modules/audio_processing/test/py_quality_assessment/apm_quality_assessment.py @@ -32,7 +32,10 @@ _EVAL_SCORE_WORKER_NAMES = _EVAL_SCORE_WORKER_CLASSES.keys() _DEFAULT_CONFIG_FILE = 'apm_configs/default.json' -def _instance_arguments_parser(): + +def _InstanceArgumentsParser(): + """Arguments parser factory. + """ parser = argparse.ArgumentParser(description=( 'Perform APM module quality assessment on one or more input files using ' 'one or more audioproc_f configuration files and one or more noise ' @@ -76,13 +79,13 @@ def main(): # TODO(alessiob): level = logging.INFO once debugged. logging.basicConfig(level=logging.DEBUG) - parser = _instance_arguments_parser() + parser = _InstanceArgumentsParser() args = parser.parse_args() simulator = simulation.ApmModuleSimulator( aechen_ir_database_path=args.air_db_path, polqa_tool_path=args.polqa_path) - simulator.run( + simulator.Run( config_filepaths=args.config_files, input_filepaths=args.input_files, noise_generator_names=args.noise_generators, diff --git a/webrtc/modules/audio_processing/test/py_quality_assessment/apm_quality_assessment_export.py b/webrtc/modules/audio_processing/test/py_quality_assessment/apm_quality_assessment_export.py index 350a5da379..66a5a50543 100755 --- a/webrtc/modules/audio_processing/test/py_quality_assessment/apm_quality_assessment_export.py +++ b/webrtc/modules/audio_processing/test/py_quality_assessment/apm_quality_assessment_export.py @@ -29,7 +29,10 @@ RE_INPUT_NAME = re.compile(r'input-(.+)') RE_NOISE_NAME = re.compile(r'noise-(.+)') RE_SCORE_NAME = re.compile(r'score-(.+)\.txt') + def _InstanceArgumentsParser(): + """Arguments parser factory. + """ parser = argparse.ArgumentParser(description=( 'Exports pre-computed APM module quality assessment results into HTML ' 'tables.')) @@ -61,8 +64,15 @@ def _InstanceArgumentsParser(): def _GetScoreDescriptors(score_filepath): - """ - Extract a score descriptors from the score file path. + """Extracts a score descriptor from the given score file path. + + Args: + score_filepath: path to the score file. + + Returns: + A tuple of strings (APM configuration name, input audio track name, + noise generator name, noise generator parameters name, evaluation score + name). """ config_name, input_name, noise_name, noise_params, score_name = ( score_filepath.split(os.sep)[-5:]) @@ -74,10 +84,21 @@ def _GetScoreDescriptors(score_filepath): def _ExcludeScore(config_name, input_name, noise_name, score_name, args): - """ + """Decides whether excluding a score. + Given a score descriptor, encoded in config_name, input_name, noise_name, and score_name, use the corresponding regular expressions to determine if the score should be excluded. + + Args: + config_name: APM configuration name. + input_name: input audio track name. + noise_name: noise generator name. + score_name: evaluation score name. + args: parsed arguments. + + Returns: + A boolean. """ value_regexpr_pairs = [ (config_name, args.config_names), @@ -96,9 +117,14 @@ def _ExcludeScore(config_name, input_name, noise_name, score_name, args): return False -def _GetOutputFilename(filename_suffix): - """ - Build the filename for the exported file. +def _BuildOutputFilename(filename_suffix): + """Builds the filename for the exported file. + + Args: + filename_suffix: suffix for the output file name. + + Returns: + A string. """ if filename_suffix is None: return 'results.html' @@ -133,23 +159,23 @@ def main(): # Get metadata. score_path, _ = os.path.split(score_filepath) audio_in_filepath, audio_ref_filepath = ( - data_access.Metadata.load_audio_in_ref_paths(score_path)) + data_access.Metadata.LoadAudioInRefPaths(score_path)) audio_out_filepath = os.path.abspath(os.path.join( score_path, audioproc_wrapper.AudioProcWrapper.OUTPUT_FILENAME)) # Add the score to the nested dictionary. scores[score_name][config_name][input_name][noise_name][noise_params] = { - 'score': data_access.ScoreFile.load(score_filepath), + 'score': data_access.ScoreFile.Load(score_filepath), 'audio_in_filepath': audio_in_filepath, 'audio_out_filepath': audio_out_filepath, 'audio_ref_filepath': audio_ref_filepath, } # Export. - output_filepath = os.path.join(args.output_dir, _GetOutputFilename( + output_filepath = os.path.join(args.output_dir, _BuildOutputFilename( args.filename_suffix)) exporter = export.HtmlExport(output_filepath) - exporter.export(scores) + exporter.Export(scores) logging.info('output file successfully written in %s', output_filepath) sys.exit(0) diff --git a/webrtc/modules/audio_processing/test/py_quality_assessment/apm_quality_assessment_gencfgs.py b/webrtc/modules/audio_processing/test/py_quality_assessment/apm_quality_assessment_gencfgs.py index 97a1eeb5f6..0be696d9ba 100755 --- a/webrtc/modules/audio_processing/test/py_quality_assessment/apm_quality_assessment_gencfgs.py +++ b/webrtc/modules/audio_processing/test/py_quality_assessment/apm_quality_assessment_gencfgs.py @@ -18,8 +18,10 @@ import quality_assessment.data_access as data_access OUTPUT_PATH = os.path.abspath('apm_configs') -def _generate_default_overridden(config_override): - """ + +def _GenerateDefaultOverridden(config_override): + """Generates one or more APM overriden configurations. + For each item in config_override, it overrides the default configuration and writes a new APM configuration file. @@ -39,8 +41,11 @@ def _generate_default_overridden(config_override): settings.use_ns = rtc::Optional(true); settings.use_ts = rtc::Optional(true); settings.use_vad = rtc::Optional(true); - """ + Args: + config_override: dict of APM configuration file names as keys; the values + are dict instances encoding the audioproc_f flags. + """ for config_filename in config_override: config = config_override[config_filename] config['-all_default'] = None @@ -49,14 +54,12 @@ def _generate_default_overridden(config_override): config_filename)) logging.debug('config file <%s> | %s', config_filepath, config) - data_access.AudioProcConfigFile.save(config_filepath, config) + data_access.AudioProcConfigFile.Save(config_filepath, config) logging.info('config file created: <%s>', config_filepath) -def generate_all_default_but_one(): - """ - Generate configurations in which all the default flags are used but one (one - flag at a time is excluded). +def _GenerateAllDefaultButOne(): + """Disables the flags enabled by default one-by-one. """ CONFIG_SETS = { 'no_AEC': {'-aec': 0,}, @@ -67,14 +70,11 @@ def generate_all_default_but_one(): 'no_transient_suppressor': {'-ts': 0,}, 'no_vad': {'-vad': 0,}, } - - return _generate_default_overridden(CONFIG_SETS) + _GenerateDefaultOverridden(CONFIG_SETS) -def generate_all_default_plus_one(): - """ - Generate configuratoins in which all the default flags are used and each - unused flag is added one at a time. +def _GenerateAllDefaultPlusOne(): + """Enables the flags disabled by default one-by-one. """ CONFIG_SETS = { 'with_AECM': {'-aec': 0, '-aecm': 1,}, # AEC and AECM are exclusive. @@ -87,14 +87,13 @@ def generate_all_default_plus_one(): 'with_LC': {'-lc': 1,}, 'with_refined_adaptive_filter': {'-refined_adaptive_filter': 1,}, } - - return _generate_default_overridden(CONFIG_SETS) + _GenerateDefaultOverridden(CONFIG_SETS) def main(): logging.basicConfig(level=logging.INFO) - generate_all_default_plus_one() - generate_all_default_but_one() + _GenerateAllDefaultPlusOne() + _GenerateAllDefaultButOne() if __name__ == '__main__': diff --git a/webrtc/modules/audio_processing/test/py_quality_assessment/apm_quality_assessment_unittest.py b/webrtc/modules/audio_processing/test/py_quality_assessment/apm_quality_assessment_unittest.py index 0f64a6af97..3cc8ddf9a8 100644 --- a/webrtc/modules/audio_processing/test/py_quality_assessment/apm_quality_assessment_unittest.py +++ b/webrtc/modules/audio_processing/test/py_quality_assessment/apm_quality_assessment_unittest.py @@ -6,11 +6,16 @@ # in the file PATENTS. All contributing project authors may # be found in the AUTHORS file in the root of the source tree. +"""Unit tests for the apm_quality_assessment module. +""" + import unittest import apm_quality_assessment class TestSimulationScript(unittest.TestCase): + """Unit tests for the apm_quality_assessment module. + """ def test_main(self): # Exit with error code if no arguments are passed. diff --git a/webrtc/modules/audio_processing/test/py_quality_assessment/quality_assessment/audioproc_wrapper.py b/webrtc/modules/audio_processing/test/py_quality_assessment/quality_assessment/audioproc_wrapper.py index f5454d9206..3c618b4605 100644 --- a/webrtc/modules/audio_processing/test/py_quality_assessment/quality_assessment/audioproc_wrapper.py +++ b/webrtc/modules/audio_processing/test/py_quality_assessment/quality_assessment/audioproc_wrapper.py @@ -36,7 +36,7 @@ class AudioProcWrapper(object): def output_filepath(self): return self._output_signal_filepath - def run(self, config_filepath, input_filepath, output_path): + def Run(self, config_filepath, input_filepath, output_path): """Run audioproc_f. Args: @@ -57,7 +57,7 @@ class AudioProcWrapper(object): return # Load configuration. - self._config = data_access.AudioProcConfigFile.load(config_filepath) + self._config = data_access.AudioProcConfigFile.Load(config_filepath) # Set remaining parametrs. self._config['-i'] = self._input_signal_filepath diff --git a/webrtc/modules/audio_processing/test/py_quality_assessment/quality_assessment/data_access.py b/webrtc/modules/audio_processing/test/py_quality_assessment/quality_assessment/data_access.py index aeee74746c..50ef5d11f0 100644 --- a/webrtc/modules/audio_processing/test/py_quality_assessment/quality_assessment/data_access.py +++ b/webrtc/modules/audio_processing/test/py_quality_assessment/quality_assessment/data_access.py @@ -13,8 +13,8 @@ import json import os -def make_directory(path): - """Recursively make a directory without rising exceptions if already existing. +def MakeDirectory(path): + """Makes a directory recursively without rising exceptions if existing. Args: path: path to the directory to be created. @@ -34,13 +34,14 @@ class Metadata(object): _AUDIO_IN_REF_FILENAME = 'audio_in_ref.txt' @classmethod - def load_audio_in_ref_paths(cls, metadata_path): - """Metadata loader for input and reference audio track paths. + def LoadAudioInRefPaths(cls, metadata_path): + """Loads the input and the reference audio track paths. Args: metadata_path: path to the directory containing the metadata file. - Returns: pair of metadata file paths for the input and output audio tracks. + Returns: + Pair of metadata file paths for the input and output audio tracks. """ metadata_filepath = os.path.join(metadata_path, cls._AUDIO_IN_REF_FILENAME) with open(metadata_filepath) as f: @@ -49,9 +50,14 @@ class Metadata(object): return audio_in_filepath, audio_ref_filepath @classmethod - def save_audio_in_ref_paths(cls, output_path, audio_in_filepath, + def SaveAudioInRefPaths(cls, output_path, audio_in_filepath, audio_ref_filepath): - """Metadata saver for input and reference audio track paths. + """Saves the input and the reference audio track paths. + + Args: + output_path: path to the directory containing the metadata file. + audio_in_filepath: path to the input audio track file. + audio_ref_filepath: path to the reference audio track file. """ output_filepath = os.path.join(output_path, cls._AUDIO_IN_REF_FILENAME) with open(output_filepath, 'w') as f: @@ -68,12 +74,26 @@ class AudioProcConfigFile(object): pass @classmethod - def load(cls, filepath): + def Load(cls, filepath): + """Loads a configuration file for audioproc_f. + + Args: + filepath: path to the configuration file. + + Returns: + A dict containing the configuration. + """ with open(filepath) as f: return json.load(f) @classmethod - def save(cls, filepath, config): + def Save(cls, filepath, config): + """Saves a configuration file for audioproc_f. + + Args: + filepath: path to the configuration file. + config: a dict containing the configuration. + """ with open(filepath, 'w') as f: json.dump(config, f) @@ -86,11 +106,25 @@ class ScoreFile(object): pass @classmethod - def load(cls, filepath): + def Load(cls, filepath): + """Loads a score from file. + + Args: + filepath: path to the score file. + + Returns: + A float encoding the score. + """ with open(filepath) as f: return float(f.readline().strip()) @classmethod - def save(cls, filepath, score): + def Save(cls, filepath, score): + """Saves a score into a file. + + Args: + filepath: path to the score file. + score: float encoding the score. + """ with open(filepath, 'w') as f: f.write('{0:f}\n'.format(score)) diff --git a/webrtc/modules/audio_processing/test/py_quality_assessment/quality_assessment/eval_scores.py b/webrtc/modules/audio_processing/test/py_quality_assessment/quality_assessment/eval_scores.py index 1488b4a126..9fb9c968dd 100644 --- a/webrtc/modules/audio_processing/test/py_quality_assessment/quality_assessment/eval_scores.py +++ b/webrtc/modules/audio_processing/test/py_quality_assessment/quality_assessment/eval_scores.py @@ -33,10 +33,15 @@ class EvaluationScore(object): self._score = None @classmethod - def register_class(cls, class_to_register): - """Register an EvaluationScore implementation. + def RegisterClass(cls, class_to_register): + """Registers an EvaluationScore implementation. Decorator to automatically register the classes that extend EvaluationScore. + Example usage: + + @EvaluationScore.RegisterClass + class AudioLevelScore(EvaluationScore): + pass """ cls.REGISTERED_CLASSES[class_to_register.NAME] = class_to_register return class_to_register @@ -49,54 +54,66 @@ class EvaluationScore(object): def score(self): return self._score - def set_reference_signal_filepath(self, filepath): - """ Set the path to the audio track used as reference signal. + def SetReferenceSignalFilepath(self, filepath): + """ Sets the path to the audio track used as reference signal. + + Args: + filepath: path to the reference audio track. """ self._reference_signal_filepath = filepath - def set_tested_signal_filepath(self, filepath): - """ Set the path to the audio track used as test signal. + def SetTestedSignalFilepath(self, filepath): + """ Sets the path to the audio track used as test signal. + + Args: + filepath: path to the test audio track. """ self._tested_signal_filepath = filepath - def _load_reference_signal(self): - assert self._reference_signal_filepath is not None - self._reference_signal = signal_processing.SignalProcessingUtils.load_wav( - self._reference_signal_filepath) - - def _load_tested_signal(self): - assert self._tested_signal_filepath is not None - self._tested_signal = signal_processing.SignalProcessingUtils.load_wav( - self._tested_signal_filepath) - - def run(self, output_path): + def Run(self, output_path): """Extracts the score for the set input-reference pair. + + Args: + output_path: path to the directory where the output is written. """ self._output_filepath = os.path.join(output_path, 'score-{}.txt'.format( self.NAME)) try: # If the score has already been computed, load. - self._load_score() + self._LoadScore() logging.debug('score found and loaded') except IOError: # Compute the score. logging.debug('score not found, compute') - self._run(output_path) + self._Run(output_path) - def _run(self, output_path): + def _Run(self, output_path): # Abstract method. raise NotImplementedError() - def _load_score(self): - return data_access.ScoreFile.load(self._output_filepath) + def _LoadReferenceSignal(self): + assert self._reference_signal_filepath is not None + self._reference_signal = signal_processing.SignalProcessingUtils.LoadWav( + self._reference_signal_filepath) - def _save_score(self): - return data_access.ScoreFile.save(self._output_filepath, self._score) + def _LoadTestedSignal(self): + assert self._tested_signal_filepath is not None + self._tested_signal = signal_processing.SignalProcessingUtils.LoadWav( + self._tested_signal_filepath) -@EvaluationScore.register_class + def _LoadScore(self): + return data_access.ScoreFile.Load(self._output_filepath) + + def _SaveScore(self): + return data_access.ScoreFile.Save(self._output_filepath, self._score) + + +@EvaluationScore.RegisterClass class AudioLevelScore(EvaluationScore): - """Compute the difference between the average audio level of the tested and + """Audio level score. + + Defined as the difference between the average audio level of the tested and the reference signals. Unit: dB @@ -109,16 +126,18 @@ class AudioLevelScore(EvaluationScore): def __init__(self): EvaluationScore.__init__(self) - def _run(self, output_path): - self._load_reference_signal() - self._load_tested_signal() + def _Run(self, output_path): + self._LoadReferenceSignal() + self._LoadTestedSignal() self._score = self._tested_signal.dBFS - self._reference_signal.dBFS - self._save_score() + self._SaveScore() -@EvaluationScore.register_class +@EvaluationScore.RegisterClass class PolqaScore(EvaluationScore): - """Compute the POLQA score. + """POLQA score. + + See http://www.polqa.info/. Unit: MOS Ideal: 4.5 @@ -141,7 +160,7 @@ class PolqaScore(EvaluationScore): logging.error('cannot find POLQA tool binary file') raise exceptions.FileNotFoundError() - def _run(self, output_path): + def _Run(self, output_path): polqa_out_filepath = os.path.join(output_path, 'polqa.out') if os.path.exists(polqa_out_filepath): os.unlink(polqa_out_filepath) @@ -157,15 +176,21 @@ class PolqaScore(EvaluationScore): subprocess.call(args, cwd=self._polqa_tool_path) # Parse POLQA tool output and extract the score. - polqa_output = self._parse_output_file(polqa_out_filepath) + polqa_output = self._ParseOutputFile(polqa_out_filepath) self._score = float(polqa_output['PolqaScore']) - self._save_score() + self._SaveScore() @classmethod - def _parse_output_file(cls, polqa_out_filepath): + def _ParseOutputFile(cls, polqa_out_filepath): """ - Parse the POLQA tool output formatted as a table ('-t' option). + Parses the POLQA tool output formatted as a table ('-t' option). + + Args: + polqa_out_filepath: path to the POLQA tool output file. + + Returns: + A dict. """ data = [] with open(polqa_out_filepath) as f: diff --git a/webrtc/modules/audio_processing/test/py_quality_assessment/quality_assessment/eval_scores_factory.py b/webrtc/modules/audio_processing/test/py_quality_assessment/quality_assessment/eval_scores_factory.py index b33d3f96b2..00e05377b6 100644 --- a/webrtc/modules/audio_processing/test/py_quality_assessment/quality_assessment/eval_scores_factory.py +++ b/webrtc/modules/audio_processing/test/py_quality_assessment/quality_assessment/eval_scores_factory.py @@ -17,8 +17,7 @@ from . import eval_scores class EvaluationScoreWorkerFactory(object): """Factory class used to instantiate evaluation score workers. - It can be used by instanciating a factory, passing parameters to the - constructor. These parameters are used to instantiate evaluation score + The ctor gets the parametrs that are used to instatiate the evaluation score workers. """ @@ -27,6 +26,9 @@ class EvaluationScoreWorkerFactory(object): def GetInstance(self, evaluation_score_class): """Creates an EvaluationScore instance given a class object. + + Args: + evaluation_score_class: EvaluationScore class object (not an instance). """ logging.debug( 'factory producing a %s evaluation score', evaluation_score_class) diff --git a/webrtc/modules/audio_processing/test/py_quality_assessment/quality_assessment/eval_scores_unittest.py b/webrtc/modules/audio_processing/test/py_quality_assessment/quality_assessment/eval_scores_unittest.py index 1abe786a50..9b29555c1a 100644 --- a/webrtc/modules/audio_processing/test/py_quality_assessment/quality_assessment/eval_scores_unittest.py +++ b/webrtc/modules/audio_processing/test/py_quality_assessment/quality_assessment/eval_scores_unittest.py @@ -6,7 +6,7 @@ # in the file PATENTS. All contributing project authors may # be found in the AUTHORS file in the root of the source tree. -"""Unit tests for the evaluation scores. +"""Unit tests for the eval_scores module. """ import unittest @@ -15,6 +15,8 @@ from . import eval_scores class TestEvalScores(unittest.TestCase): + """Unit tests for the eval_scores module. + """ def test_registered_classes(self): # Check that there is at least one registered evaluation score worker. diff --git a/webrtc/modules/audio_processing/test/py_quality_assessment/quality_assessment/evaluation.py b/webrtc/modules/audio_processing/test/py_quality_assessment/quality_assessment/evaluation.py index 016690a2c4..e18f193bb0 100644 --- a/webrtc/modules/audio_processing/test/py_quality_assessment/quality_assessment/evaluation.py +++ b/webrtc/modules/audio_processing/test/py_quality_assessment/quality_assessment/evaluation.py @@ -13,28 +13,39 @@ import logging class ApmModuleEvaluator(object): + """APM evaluator class. + """ def __init__(self): pass @classmethod - def run(cls, evaluation_score_workers, apm_output_filepath, + def Run(cls, evaluation_score_workers, apm_output_filepath, reference_input_filepath, output_path): """Runs the evaluation. Iterates over the given evaluation score workers. + + Args: + evaluation_score_workers: list of EvaluationScore instances. + apm_output_filepath: path to the audio track file with the APM output. + reference_input_filepath: path to the reference audio track file. + output_path: output path. + + Returns: + A dict of evaluation score name and score pairs. """ # Init. scores = {} for evaluation_score_worker in evaluation_score_workers: logging.info(' computing <%s> score', evaluation_score_worker.NAME) - evaluation_score_worker.set_reference_signal_filepath( + evaluation_score_worker.SetReferenceSignalFilepath( reference_input_filepath) - evaluation_score_worker.set_tested_signal_filepath( + evaluation_score_worker.SetTestedSignalFilepath( apm_output_filepath) - evaluation_score_worker.run(output_path) + evaluation_score_worker.Run(output_path) scores[evaluation_score_worker.NAME] = evaluation_score_worker.score return scores diff --git a/webrtc/modules/audio_processing/test/py_quality_assessment/quality_assessment/exceptions.py b/webrtc/modules/audio_processing/test/py_quality_assessment/quality_assessment/exceptions.py index b67a64e0c4..0e9116cb6b 100644 --- a/webrtc/modules/audio_processing/test/py_quality_assessment/quality_assessment/exceptions.py +++ b/webrtc/modules/audio_processing/test/py_quality_assessment/quality_assessment/exceptions.py @@ -11,8 +11,12 @@ class FileNotFoundError(Exception): + """File not found exeception. + """ pass class SignalProcessingException(Exception): + """Signal processing exeception. + """ pass diff --git a/webrtc/modules/audio_processing/test/py_quality_assessment/quality_assessment/export.py b/webrtc/modules/audio_processing/test/py_quality_assessment/quality_assessment/export.py index a0cb41148d..c3cb9fd1f1 100644 --- a/webrtc/modules/audio_processing/test/py_quality_assessment/quality_assessment/export.py +++ b/webrtc/modules/audio_processing/test/py_quality_assessment/quality_assessment/export.py @@ -12,6 +12,8 @@ import re class HtmlExport(object): + """HTML exporter class for APM quality scores. + """ # Path to CSS and JS files. _PATH = os.path.dirname(os.path.realpath(__file__)) @@ -31,9 +33,8 @@ class HtmlExport(object): self._noise_params = None self._output_filepath = output_filepath - def export(self, scores): - """ - Export the scores into an HTML file. + def Export(self, scores): + """Exports the scores into an HTML file. Args: scores: nested dictionary containing the scores. @@ -41,29 +42,33 @@ class HtmlExport(object): # Generate one table for each evaluation score. tables = [] for score_name in sorted(scores.keys()): - tables.append(self._build_score_table(score_name, scores[score_name])) + tables.append(self._BuildScoreTable(score_name, scores[score_name])) # Create the html file. html = ( '' + - self._build_header() + + self._BuildHeader() + '' + '

Results from {}

'.format(self._output_filepath) + self._NEW_LINE.join(tables) + '' + '') - self._save(self._output_filepath, html) + self._Save(self._output_filepath, html) - def _build_header(self): - """ - HTML file header with page title and either embedded or linked CSS and JS + def _BuildHeader(self): + """Builds the section of the HTML file. + + The header contains the page title and either embedded or linked CSS and JS files. + + Returns: + A string with ... HTML. """ html = ['', 'Results'] # Function to append the lines of a text file to html. - def _embed_file(filepath): + def EmbedFile(filepath): with open(filepath) as f: for l in f: html.append(l.strip()) @@ -72,7 +77,7 @@ class HtmlExport(object): if self._INLINE_CSS: # Embed. html.append('') else: # Link. @@ -83,7 +88,7 @@ class HtmlExport(object): if self._INLINE_JS: # Embed. html.append('') else: # Link. @@ -94,54 +99,88 @@ class HtmlExport(object): return self._NEW_LINE.join(html) - def _build_score_table(self, score_name, scores): - """ - Generate a table for a specific evaluation score (e.g., POLQA). + def _BuildScoreTable(self, score_name, scores): + """Builds a table for a specific evaluation score (e.g., POLQA). + + Args: + score_name: name of the score. + scores: nested dictionary of scores. + + Returns: + A string with ...
HTML. """ config_names = sorted(scores.keys()) input_names = sorted(scores[config_names[0]].keys()) - rows = [self._table_row( + rows = [self._BuildTableRow( score_name, config_name, scores[config_name], input_names) for ( config_name) in config_names] html = ( '' + '{}'.format( - self._table_header(score_name, input_names)) + + self._BuildTableHeader(score_name, input_names)) + '' + '' + ''.join(rows) + '' + '' + - '
' + self._legend()) + '' + self._BuildLegend()) return html - def _table_header(self, score_name, input_names): - """ - Generate a table header with the name of the evaluation score in the first - column and then one column for each probing signal. + def _BuildTableHeader(self, score_name, input_names): + """Builds the cells of a table header. + + A table header starts with a cell containing the name of the evaluation + score, and then it includes one column for each probing signal. + + Args: + score_name: name of the score. + input_names: list of probing signal names. + + Returns: + A string with a list of ... HTML elements. """ html = ( - '{}'.format(self._format_name(score_name)) + + '{}'.format(self._FormatName(score_name)) + '' + ''.join( - [self._format_name(name) for name in input_names]) + '') + [self._FormatName(name) for name in input_names]) + '') return html - def _table_row(self, score_name, config_name, scores, input_names): + def _BuildTableRow(self, score_name, config_name, scores, input_names): + """Builds the cells of a table row. + + A table row starts with the name of the APM configuration file, and then it + includes one column for each probing singal. + + Args: + score_name: name of the score. + config_name: name of the APM configuration. + scores: nested dictionary of scores. + input_names: list of probing signal names. + + Returns: + A string with a list of ... HTML elements. """ - Generate a table body row with the name of the APM configuration file in the - first column and then one column for each probing singal. - """ - cells = [self._table_cell( + cells = [self._BuildTableCell( scores[input_name], score_name, config_name, input_name) for ( input_name) in input_names] - html = ('{}'.format(self._format_name(config_name)) + + html = ('{}'.format(self._FormatName(config_name)) + '' + ''.join(cells) + '') return html - def _table_cell(self, scores, score_name, config_name, input_name): - """ - Generate a table cell content with all the scores for the current evaluation - score, APM configuration, and probing signal. + def _BuildTableCell(self, scores, score_name, config_name, input_name): + """Builds the inner content of a table cell. + + A table cell includes all the scores computed for a specific evaluation + score (e.g., POLQA), APM configuration (e.g., default), and probing signal. + + Args: + scores: dictionary of score data. + score_name: name of the score. + config_name: name of the APM configuration. + input_name: name of the probing signal. + + Returns: + A string with the HTML of a table body cell. """ # Init noise generator names and noise parameters cache (if not done). if self._noise_names is None: @@ -195,9 +234,13 @@ class HtmlExport(object): return html - def _legend(self): - """ - Generate the legend for each noise generator name and parameters pair. + def _BuildLegend(self): + """Builds the legend. + + The legend details noise generator name and parameter pairs. + + Returns: + A string with a
...
HTML element. """ items = [] for name_index, noise_name in enumerate(self._noise_names): @@ -213,10 +256,24 @@ class HtmlExport(object): return html @classmethod - def _save(cls, output_filepath, html): + def _Save(cls, output_filepath, html): + """Writes the HTML file. + + Args: + output_filepath: output file path. + html: string with the HTML content. + """ with open(output_filepath, 'w') as f: f.write(html) @classmethod - def _format_name(cls, name): + def _FormatName(cls, name): + """Formats a name. + + Args: + name: a string. + + Returns: + A copy of name in which underscores and dashes are replaced with a space. + """ return re.sub(r'[_\-]', ' ', name) diff --git a/webrtc/modules/audio_processing/test/py_quality_assessment/quality_assessment/noise_generation.py b/webrtc/modules/audio_processing/test/py_quality_assessment/quality_assessment/noise_generation.py index 9bfce34db2..090d3506ab 100644 --- a/webrtc/modules/audio_processing/test/py_quality_assessment/quality_assessment/noise_generation.py +++ b/webrtc/modules/audio_processing/test/py_quality_assessment/quality_assessment/noise_generation.py @@ -47,23 +47,34 @@ class NoiseGenerator(object): signal. A noise generator generates one or more input-reference pairs. + + TODO(alessiob): Rename from NoiseGenerator to InputReferencePairGenerator. """ NAME = None REGISTERED_CLASSES = {} def __init__(self): - # Input + # Init dictionaries with one entry for each noise generator configuration + # (e.g., different SNRs). + # Noisy audio track files (stored separately in a cache folder). self._noisy_signal_filepaths = None - self._output_paths = None + # Path to be used for the APM simulation output files. + self._apm_output_paths = None + # Reference audio track files (stored separately in a cache folder). self._reference_signal_filepaths = None - self.clear() + self.Clear() @classmethod - def register_class(cls, class_to_register): - """Register an NoiseGenerator implementation. + def RegisterClass(cls, class_to_register): + """Registers an NoiseGenerator implementation. Decorator to automatically register the classes that extend NoiseGenerator. + Example usage: + + @NoiseGenerator.RegisterClass + class IdentityGenerator(NoiseGenerator): + pass """ cls.REGISTERED_CLASSES[class_to_register.NAME] = class_to_register return class_to_register @@ -77,37 +88,44 @@ class NoiseGenerator(object): return self._noisy_signal_filepaths @property - def output_paths(self): - return self._output_paths + def apm_output_paths(self): + return self._apm_output_paths @property def reference_signal_filepaths(self): return self._reference_signal_filepaths - def generate( + def Generate( self, input_signal_filepath, input_noise_cache_path, base_output_path): - """Generate a set of noisy input and reference audiotrack file pairs. + """Generates a set of noisy input and reference audiotrack file pairs. - This method initializes an empty set of pairs and calls the _generate() + This method initializes an empty set of pairs and calls the _Generate() method implemented in a concrete class. + + Args: + input_signal_filepath: path to the clean input audio track file. + input_noise_cache_path: path to the cache of noisy audio track files. + base_output_path: base path where output is written. """ - self.clear() - return self._generate( + self.Clear() + self._Generate( input_signal_filepath, input_noise_cache_path, base_output_path) - def clear(self): + def Clear(self): + """Clears the generated output path dictionaries. + """ self._noisy_signal_filepaths = {} - self._output_paths = {} + self._apm_output_paths = {} self._reference_signal_filepaths = {} - def _generate( + def _Generate( self, input_signal_filepath, input_noise_cache_path, base_output_path): - """This is an abstract method to be implemented in each concrete class. + """Abstract method to be implemented in each concrete class. """ raise NotImplementedError() - def _add_noise_snr_pairs(self, base_output_path, noisy_mix_filepaths, - snr_value_pairs): + def _AddNoiseSnrPairs(self, base_output_path, noisy_mix_filepaths, + snr_value_pairs): """Adds noisy-reference signal pairs. Args: @@ -120,8 +138,8 @@ class NoiseGenerator(object): for snr_noisy, snr_refence in snr_value_pairs: config_name = '{0}_{1:d}_{2:d}_SNR'.format( noise_track_name, snr_noisy, snr_refence) - output_path = self._make_dir(base_output_path, config_name) - self._add_noise_reference_files_pair( + output_path = self._MakeDir(base_output_path, config_name) + self._AddNoiseReferenceFilesPair( config_name=config_name, noisy_signal_filepath=noisy_mix_filepaths[ noise_track_name][snr_noisy], @@ -129,30 +147,38 @@ class NoiseGenerator(object): noise_track_name][snr_refence], output_path=output_path) - def _add_noise_reference_files_pair(self, config_name, noisy_signal_filepath, - reference_signal_filepath, output_path): + def _AddNoiseReferenceFilesPair(self, config_name, noisy_signal_filepath, + reference_signal_filepath, output_path): + """Adds one noisy-reference signal pair. + + Args: + config_name: name of the APM configuration. + noisy_signal_filepath: path to noisy audio track file. + reference_signal_filepath: path to reference audio track file. + output_path: APM output path. + """ assert config_name not in self._noisy_signal_filepaths self._noisy_signal_filepaths[config_name] = os.path.abspath( noisy_signal_filepath) - self._output_paths[config_name] = os.path.abspath(output_path) + self._apm_output_paths[config_name] = os.path.abspath(output_path) self._reference_signal_filepaths[config_name] = os.path.abspath( reference_signal_filepath) # Save noisy and reference file paths. - data_access.Metadata.save_audio_in_ref_paths( + data_access.Metadata.SaveAudioInRefPaths( output_path=output_path, audio_in_filepath=self._noisy_signal_filepaths[config_name], audio_ref_filepath=self._reference_signal_filepaths[config_name]) @classmethod - def _make_dir(cls, base_output_path, noise_generator_config_name): + def _MakeDir(cls, base_output_path, noise_generator_config_name): output_path = os.path.join(base_output_path, noise_generator_config_name) - data_access.make_directory(output_path) + data_access.MakeDirectory(output_path) return output_path # Identity generator. -@NoiseGenerator.register_class +@NoiseGenerator.RegisterClass class IdentityGenerator(NoiseGenerator): """Generator that adds no noise. @@ -164,18 +190,18 @@ class IdentityGenerator(NoiseGenerator): def __init__(self): NoiseGenerator.__init__(self) - def _generate( + def _Generate( self, input_signal_filepath, input_noise_cache_path, base_output_path): CONFIG_NAME = 'default' - output_path = self._make_dir(base_output_path, CONFIG_NAME) - self._add_noise_reference_files_pair( + output_path = self._MakeDir(base_output_path, CONFIG_NAME) + self._AddNoiseReferenceFilesPair( config_name=CONFIG_NAME, noisy_signal_filepath=input_signal_filepath, reference_signal_filepath=input_signal_filepath, output_path=output_path) -@NoiseGenerator.register_class +@NoiseGenerator.RegisterClass class WhiteNoiseGenerator(NoiseGenerator): """Additive white noise generator. """ @@ -197,18 +223,18 @@ class WhiteNoiseGenerator(NoiseGenerator): def __init__(self): NoiseGenerator.__init__(self) - def _generate( + def _Generate( self, input_signal_filepath, input_noise_cache_path, base_output_path): # Load the input signal. - input_signal = signal_processing.SignalProcessingUtils.load_wav( + input_signal = signal_processing.SignalProcessingUtils.LoadWav( input_signal_filepath) - input_signal = signal_processing.SignalProcessingUtils.normalize( + input_signal = signal_processing.SignalProcessingUtils.Normalize( input_signal) # Create the noise track. - noise_signal = signal_processing.SignalProcessingUtils.generate_white_noise( + noise_signal = signal_processing.SignalProcessingUtils.GenerateWhiteNoise( input_signal) - noise_signal = signal_processing.SignalProcessingUtils.normalize( + noise_signal = signal_processing.SignalProcessingUtils.Normalize( noise_signal) # Create the noisy mixes (once for each unique SNR value). @@ -222,11 +248,11 @@ class WhiteNoiseGenerator(NoiseGenerator): # Create and save if not done. if not os.path.exists(noisy_signal_filepath): # Create noisy signal. - noisy_signal = signal_processing.SignalProcessingUtils.mix_signals( + noisy_signal = signal_processing.SignalProcessingUtils.MixSignals( input_signal, noise_signal, snr) # Save. - signal_processing.SignalProcessingUtils.save_wav( + signal_processing.SignalProcessingUtils.SaveWav( noisy_signal_filepath, noisy_signal) # Add file to the collection of mixes. @@ -235,8 +261,8 @@ class WhiteNoiseGenerator(NoiseGenerator): # Add all the noisy-reference signal pairs. for snr_noisy, snr_refence in self._SNR_VALUE_PAIRS: config_name = '{0:d}_{1:d}_SNR'.format(snr_noisy, snr_refence) - output_path = self._make_dir(base_output_path, config_name) - self._add_noise_reference_files_pair( + output_path = self._MakeDir(base_output_path, config_name) + self._AddNoiseReferenceFilesPair( config_name=config_name, noisy_signal_filepath=noisy_mix_filepaths[snr_noisy], reference_signal_filepath=noisy_mix_filepaths[snr_refence], @@ -244,7 +270,7 @@ class WhiteNoiseGenerator(NoiseGenerator): # TODO(alessiob): remove comment when class implemented. -# @NoiseGenerator.register_class +# @NoiseGenerator.RegisterClass class NarrowBandNoiseGenerator(NoiseGenerator): """Additive narrow-band noise generator. """ @@ -254,13 +280,13 @@ class NarrowBandNoiseGenerator(NoiseGenerator): def __init__(self): NoiseGenerator.__init__(self) - def _generate( + def _Generate( self, input_signal_filepath, input_noise_cache_path, base_output_path): # TODO(alessiob): implement. pass -@NoiseGenerator.register_class +@NoiseGenerator.RegisterClass class EnvironmentalNoiseGenerator(NoiseGenerator): """Additive environmental noise generator. """ @@ -290,27 +316,22 @@ class EnvironmentalNoiseGenerator(NoiseGenerator): def __init__(self): NoiseGenerator.__init__(self) - def _generate( + def _Generate( self, input_signal_filepath, input_noise_cache_path, base_output_path): - """Generate environmental noise. + """Generates environmental noise. - For each noise track and pair of SNR values, the following 2 audio tracks + For each noise track and pair of SNR values, the following two audio tracks are created: the noisy signal and the reference signal. The former is obtained by mixing the (clean) input signal to the corresponding noise track enforcing the target SNR. - - Args: - input_signal_filepath: (clean) input signal file path. - input_noise_cache_path: path for the cached noise track files. - base_output_path: base output path. """ # Init. snr_values = set([snr for pair in self._SNR_VALUE_PAIRS for snr in pair]) # Load the input signal. - input_signal = signal_processing.SignalProcessingUtils.load_wav( + input_signal = signal_processing.SignalProcessingUtils.LoadWav( input_signal_filepath) - input_signal = signal_processing.SignalProcessingUtils.normalize( + input_signal = signal_processing.SignalProcessingUtils.Normalize( input_signal) noisy_mix_filepaths = {} @@ -323,9 +344,9 @@ class EnvironmentalNoiseGenerator(NoiseGenerator): logging.error('cannot find the <%s> noise track', noise_track_filename) raise exceptions.FileNotFoundError() - noise_signal = signal_processing.SignalProcessingUtils.load_wav( + noise_signal = signal_processing.SignalProcessingUtils.LoadWav( noise_track_filepath) - noise_signal = signal_processing.SignalProcessingUtils.normalize( + noise_signal = signal_processing.SignalProcessingUtils.Normalize( noise_signal) # Create the noisy mixes (once for each unique SNR value). @@ -338,24 +359,26 @@ class EnvironmentalNoiseGenerator(NoiseGenerator): # Create and save if not done. if not os.path.exists(noisy_signal_filepath): # Create noisy signal. - noisy_signal = signal_processing.SignalProcessingUtils.mix_signals( + noisy_signal = signal_processing.SignalProcessingUtils.MixSignals( input_signal, noise_signal, snr) # Save. - signal_processing.SignalProcessingUtils.save_wav( + signal_processing.SignalProcessingUtils.SaveWav( noisy_signal_filepath, noisy_signal) # Add file to the collection of mixes. noisy_mix_filepaths[noise_track_name][snr] = noisy_signal_filepath # Add all the noise-SNR pairs. - self._add_noise_snr_pairs( + self._AddNoiseSnrPairs( base_output_path, noisy_mix_filepaths, self._SNR_VALUE_PAIRS) -@NoiseGenerator.register_class +@NoiseGenerator.RegisterClass class EchoNoiseGenerator(NoiseGenerator): """Echo noise generator. + + TODO(alessiob): Rename from echo to reverberation. """ NAME = 'echo' @@ -381,7 +404,7 @@ class EchoNoiseGenerator(NoiseGenerator): NoiseGenerator.__init__(self) self._aechen_ir_database_path = aechen_ir_database_path - def _generate( + def _Generate( self, input_signal_filepath, input_noise_cache_path, base_output_path): """Generates echo noise. @@ -390,17 +413,12 @@ class EchoNoiseGenerator(NoiseGenerator): created: the noisy signal and the reference signal. The former is obtained by mixing the (clean) input signal to the corresponding noise track enforcing the target SNR. - - Args: - input_signal_filepath: (clean) input signal file path. - input_noise_cache_path: path for the cached noise track files. - base_output_path: base output path. """ # Init. snr_values = set([snr for pair in self._SNR_VALUE_PAIRS for snr in pair]) # Load the input signal. - input_signal = signal_processing.SignalProcessingUtils.load_wav( + input_signal = signal_processing.SignalProcessingUtils.LoadWav( input_signal_filepath) noisy_mix_filepaths = {} @@ -412,14 +430,14 @@ class EchoNoiseGenerator(NoiseGenerator): noise_signal = None try: # Load noise track. - noise_signal = signal_processing.SignalProcessingUtils.load_wav( + noise_signal = signal_processing.SignalProcessingUtils.LoadWav( noise_track_filepath) except IOError: # File not found. # Generate noise track by applying the impulse response. impulse_response_filepath = os.path.join( self._aechen_ir_database_path, self._IMPULSE_RESPONSES[impulse_response_name]) - noise_signal = self._generate_noise_track( + noise_signal = self._GenerateNoiseTrack( noise_track_filepath, input_signal, impulse_response_filepath) assert noise_signal is not None @@ -434,21 +452,21 @@ class EchoNoiseGenerator(NoiseGenerator): # Create and save if not done. if not os.path.exists(noisy_signal_filepath): # Create noisy signal. - noisy_signal = signal_processing.SignalProcessingUtils.mix_signals( + noisy_signal = signal_processing.SignalProcessingUtils.MixSignals( input_signal, noise_signal, snr, bln_pad_shortest=True) # Save. - signal_processing.SignalProcessingUtils.save_wav( + signal_processing.SignalProcessingUtils.SaveWav( noisy_signal_filepath, noisy_signal) # Add file to the collection of mixes. noisy_mix_filepaths[impulse_response_name][snr] = noisy_signal_filepath # Add all the noise-SNR pairs. - self._add_noise_snr_pairs(base_output_path, noisy_mix_filepaths, - self._SNR_VALUE_PAIRS) + self._AddNoiseSnrPairs(base_output_path, noisy_mix_filepaths, + self._SNR_VALUE_PAIRS) - def _generate_noise_track(self, noise_track_filepath, input_signal, + def _GenerateNoiseTrack(self, noise_track_filepath, input_signal, impulse_response_filepath): """Generates noise track. @@ -459,6 +477,9 @@ class EchoNoiseGenerator(NoiseGenerator): noise_track_filepath: output file path for the noise track. input_signal: (clean) input signal samples. impulse_response_filepath: impulse response file path. + + Returns: + AudioSegment instance. """ # Load impulse response. data = scipy.io.loadmat(impulse_response_filepath) @@ -470,11 +491,11 @@ class EchoNoiseGenerator(NoiseGenerator): # Apply impulse response. processed_signal = ( - signal_processing.SignalProcessingUtils.apply_impulse_response( + signal_processing.SignalProcessingUtils.ApplyImpulseResponse( input_signal, impulse_response)) # Save. - signal_processing.SignalProcessingUtils.save_wav( + signal_processing.SignalProcessingUtils.SaveWav( noise_track_filepath, processed_signal) return processed_signal diff --git a/webrtc/modules/audio_processing/test/py_quality_assessment/quality_assessment/noise_generation_factory.py b/webrtc/modules/audio_processing/test/py_quality_assessment/quality_assessment/noise_generation_factory.py index acb9f07675..62b4c86e07 100644 --- a/webrtc/modules/audio_processing/test/py_quality_assessment/quality_assessment/noise_generation_factory.py +++ b/webrtc/modules/audio_processing/test/py_quality_assessment/quality_assessment/noise_generation_factory.py @@ -17,9 +17,9 @@ from . import noise_generation class NoiseGeneratorFactory(object): """Factory class used to instantiate noise generator workers. - It can be used by instanciating a factory, passing parameters to the - constructor. These parameters are used to instantiate noise generator - workers. + It can be used by instanciating a factory, passing parameters to the + constructor. These parameters are used to instantiate noise generator + workers. """ def __init__(self, aechen_ir_database_path): @@ -27,6 +27,9 @@ class NoiseGeneratorFactory(object): def GetInstance(self, noise_generator_class): """Creates an NoiseGenerator instance given a class object. + + Args: + noise_generator_class: NoiseGenerator class object (not an instance). """ logging.debug( 'factory producing a %s noise generator', noise_generator_class) diff --git a/webrtc/modules/audio_processing/test/py_quality_assessment/quality_assessment/noise_generation_unittest.py b/webrtc/modules/audio_processing/test/py_quality_assessment/quality_assessment/noise_generation_unittest.py index 2b750913ac..c5dfed2796 100644 --- a/webrtc/modules/audio_processing/test/py_quality_assessment/quality_assessment/noise_generation_unittest.py +++ b/webrtc/modules/audio_processing/test/py_quality_assessment/quality_assessment/noise_generation_unittest.py @@ -20,6 +20,8 @@ from . import signal_processing class TestNoiseGen(unittest.TestCase): + """Unit tests for the noise_generation module. + """ def setUp(self): """Create temporary folders.""" @@ -54,7 +56,7 @@ class TestNoiseGen(unittest.TestCase): self.assertTrue(os.path.exists(input_signal_filepath)) # Load input signal. - input_signal = signal_processing.SignalProcessingUtils.load_wav( + input_signal = signal_processing.SignalProcessingUtils.LoadWav( input_signal_filepath) # Try each registered noise generator. @@ -72,7 +74,7 @@ class TestNoiseGen(unittest.TestCase): registered_classes[noise_generator_name]) # Generate the noisy input - reference pairs. - noise_generator.generate( + noise_generator.Generate( input_signal_filepath=input_signal_filepath, input_noise_cache_path=self._input_noise_cache_path, base_output_path=self._base_output_path) @@ -92,7 +94,7 @@ class TestNoiseGen(unittest.TestCase): self.assertEqual(number_of_pairs, len(noise_generator.noisy_signal_filepaths)) self.assertEqual(number_of_pairs, - len(noise_generator.output_paths)) + len(noise_generator.apm_output_paths)) self.assertEqual(number_of_pairs, len(noise_generator.reference_signal_filepaths)) @@ -108,30 +110,30 @@ class TestNoiseGen(unittest.TestCase): input_signal: AudioSegment instance. """ input_signal_length = ( - signal_processing.SignalProcessingUtils.count_samples(input_signal)) + signal_processing.SignalProcessingUtils.CountSamples(input_signal)) # Iterate over the noisy signal - reference pairs. for noise_config_name in noise_generator.config_names: # Load the noisy input file. noisy_signal_filepath = noise_generator.noisy_signal_filepaths[ noise_config_name] - noisy_signal = signal_processing.SignalProcessingUtils.load_wav( + noisy_signal = signal_processing.SignalProcessingUtils.LoadWav( noisy_signal_filepath) # Check noisy input signal length. noisy_signal_length = ( - signal_processing.SignalProcessingUtils.count_samples(noisy_signal)) + signal_processing.SignalProcessingUtils.CountSamples(noisy_signal)) self.assertGreaterEqual(noisy_signal_length, input_signal_length) # Load the reference file. reference_signal_filepath = ( noise_generator.reference_signal_filepaths[noise_config_name]) - reference_signal = signal_processing.SignalProcessingUtils.load_wav( + reference_signal = signal_processing.SignalProcessingUtils.LoadWav( reference_signal_filepath) # Check noisy input signal length. reference_signal_length = ( - signal_processing.SignalProcessingUtils.count_samples( + signal_processing.SignalProcessingUtils.CountSamples( reference_signal)) self.assertGreaterEqual(reference_signal_length, input_signal_length) @@ -143,5 +145,5 @@ class TestNoiseGen(unittest.TestCase): """ # Iterate over the noisy signal - reference pairs. for noise_config_name in noise_generator.config_names: - output_path = noise_generator.output_paths[noise_config_name] + output_path = noise_generator.apm_output_paths[noise_config_name] self.assertTrue(os.path.exists(output_path)) diff --git a/webrtc/modules/audio_processing/test/py_quality_assessment/quality_assessment/signal_processing.py b/webrtc/modules/audio_processing/test/py_quality_assessment/quality_assessment/signal_processing.py index f611eca0dc..1b58833eb8 100644 --- a/webrtc/modules/audio_processing/test/py_quality_assessment/quality_assessment/signal_processing.py +++ b/webrtc/modules/audio_processing/test/py_quality_assessment/quality_assessment/signal_processing.py @@ -6,6 +6,9 @@ # in the file PATENTS. All contributing project authors may # be found in the AUTHORS file in the root of the source tree. +"""Signal processing utility module. +""" + import array import logging import os @@ -34,15 +37,21 @@ from . import exceptions class SignalProcessingUtils(object): + """Collection of signal processing utilities. + """ def __init__(self): pass @classmethod - def load_wav(cls, filepath, channels=1): - """Load wav file. + def LoadWav(cls, filepath, channels=1): + """Loads wav file. - Return: + Args: + filepath: path to the wav audio track file to load. + channels: number of channels (downmixing to mono by default). + + Returns: AudioSegment instance. """ if not os.path.exists(filepath): @@ -52,21 +61,24 @@ class SignalProcessingUtils(object): filepath, format='wav', channels=channels) @classmethod - def save_wav(cls, output_filepath, signal): - """Save wav file. + def SaveWav(cls, output_filepath, signal): + """Saves wav file. Args: - output_filepath: string, output file path. + output_filepath: path to the wav audio track file to save. signal: AudioSegment instance. """ return signal.export(output_filepath, format='wav') @classmethod - def count_samples(cls, signal): + def CountSamples(cls, signal): """Number of samples per channel. Args: signal: AudioSegment instance. + + Returns: + An integer. """ number_of_samples = len(signal.get_array_of_samples()) assert signal.channels > 0 @@ -74,10 +86,10 @@ class SignalProcessingUtils(object): return number_of_samples / signal.channels @classmethod - def generate_white_noise(cls, signal): - """Generate white noise. + def GenerateWhiteNoise(cls, signal): + """Generates white noise. - Generate white noise with the same duration and in the same format as a + White noise is generated with the same duration and in the same format as a given signal. Args: @@ -94,8 +106,15 @@ class SignalProcessingUtils(object): volume=0.0) @classmethod - def apply_impulse_response(cls, signal, impulse_response): - """Apply an impulse response to a signal. + def ApplyImpulseResponse(cls, signal, impulse_response): + """Applies an impulse response to a signal. + + Args: + signal: AudioSegment instance. + impulse_response: list or numpy vector of float values. + + Returns: + AudioSegment instance. """ # Get samples. assert signal.channels == 1, ( @@ -133,11 +152,27 @@ class SignalProcessingUtils(object): return convolved_signal @classmethod - def normalize(cls, signal): + def Normalize(cls, signal): + """Normalizes a signal. + + Args: + signal: AudioSegment instance. + + Returns: + An AudioSegment instance. + """ return signal.apply_gain(-signal.max_dBFS) @classmethod - def copy(cls, signal): + def Copy(cls, signal): + """Makes a copy os a signal. + + Args: + signal: AudioSegment instance. + + Returns: + An AudioSegment instance. + """ return pydub.AudioSegment( data=signal.get_array_of_samples(), metadata={ @@ -148,11 +183,10 @@ class SignalProcessingUtils(object): }) @classmethod - def mix_signals(cls, signal, noise, target_snr=0.0, - bln_pad_shortest=False): - """Mix two signals with a target SNR. + def MixSignals(cls, signal, noise, target_snr=0.0, bln_pad_shortest=False): + """Mixes two signals with a target SNR. - Mix two signals up to a desired SNR by scaling noise (noise). + Mix two signals with a desired SNR by scaling noise (noise). If the target SNR is +/- infinite, a copy of signal/noise is returned. Args: @@ -161,16 +195,19 @@ class SignalProcessingUtils(object): target_snr: float, numpy.Inf or -numpy.Inf (dB). bln_pad_shortest: if True, it pads the shortest signal with silence at the end. + + Returns: + An AudioSegment instance. """ # Handle infinite target SNR. if target_snr == -np.Inf: # Return a copy of noise. logging.warning('SNR = -Inf, returning noise') - return cls.copy(noise) + return cls.Copy(noise) elif target_snr == np.Inf: # Return a copy of signal. logging.warning('SNR = +Inf, returning signal') - return cls.copy(signal) + return cls.Copy(signal) # Check signal and noise power. signal_power = float(signal.dBFS) @@ -208,4 +245,4 @@ class SignalProcessingUtils(object): # Mix signals using the target SNR. gain_db = signal_power - noise_power - target_snr - return cls.normalize(signal.overlay(noise.apply_gain(gain_db))) + return cls.Normalize(signal.overlay(noise.apply_gain(gain_db))) diff --git a/webrtc/modules/audio_processing/test/py_quality_assessment/quality_assessment/signal_processing_unittest.py b/webrtc/modules/audio_processing/test/py_quality_assessment/quality_assessment/signal_processing_unittest.py index 7cb088c0d4..3edd5387d5 100644 --- a/webrtc/modules/audio_processing/test/py_quality_assessment/quality_assessment/signal_processing_unittest.py +++ b/webrtc/modules/audio_processing/test/py_quality_assessment/quality_assessment/signal_processing_unittest.py @@ -19,15 +19,17 @@ from . import signal_processing class TestSignalProcessing(unittest.TestCase): + """Unit tests for the signal_processing module. + """ def testMixSignals(self): # Generate a template signal with which white noise can be generated. silence = pydub.AudioSegment.silent(duration=1000, frame_rate=48000) # Generate two distinct AudioSegment instances with 1 second of white noise. - signal = signal_processing.SignalProcessingUtils.generate_white_noise( + signal = signal_processing.SignalProcessingUtils.GenerateWhiteNoise( silence) - noise = signal_processing.SignalProcessingUtils.generate_white_noise( + noise = signal_processing.SignalProcessingUtils.GenerateWhiteNoise( silence) # Extract samples. @@ -35,7 +37,7 @@ class TestSignalProcessing(unittest.TestCase): noise_samples = noise.get_array_of_samples() # Test target SNR -Inf (noise expected). - mix_neg_inf = signal_processing.SignalProcessingUtils.mix_signals( + mix_neg_inf = signal_processing.SignalProcessingUtils.MixSignals( signal, noise, -np.Inf) self.assertTrue(len(noise), len(mix_neg_inf)) # Check duration. mix_neg_inf_samples = mix_neg_inf.get_array_of_samples() @@ -43,7 +45,7 @@ class TestSignalProcessing(unittest.TestCase): all([x == y for x, y in zip(noise_samples, mix_neg_inf_samples)])) # Test target SNR 0.0 (different data expected). - mix_0 = signal_processing.SignalProcessingUtils.mix_signals( + mix_0 = signal_processing.SignalProcessingUtils.MixSignals( signal, noise, 0.0) self.assertTrue(len(signal), len(mix_0)) # Check duration. self.assertTrue(len(noise), len(mix_0)) @@ -54,7 +56,7 @@ class TestSignalProcessing(unittest.TestCase): any([x != y for x, y in zip(noise_samples, mix_0_samples)])) # Test target SNR +Inf (signal expected). - mix_pos_inf = signal_processing.SignalProcessingUtils.mix_signals( + mix_pos_inf = signal_processing.SignalProcessingUtils.MixSignals( signal, noise, np.Inf) self.assertTrue(len(signal), len(mix_pos_inf)) # Check duration. mix_pos_inf_samples = mix_pos_inf.get_array_of_samples() @@ -63,13 +65,13 @@ class TestSignalProcessing(unittest.TestCase): def testMixSignalsMinInfPower(self): silence = pydub.AudioSegment.silent(duration=1000, frame_rate=48000) - signal = signal_processing.SignalProcessingUtils.generate_white_noise( + signal = signal_processing.SignalProcessingUtils.GenerateWhiteNoise( silence) with self.assertRaises(exceptions.SignalProcessingException): - _ = signal_processing.SignalProcessingUtils.mix_signals( + _ = signal_processing.SignalProcessingUtils.MixSignals( signal, silence, 0.0) with self.assertRaises(exceptions.SignalProcessingException): - _ = signal_processing.SignalProcessingUtils.mix_signals( + _ = signal_processing.SignalProcessingUtils.MixSignals( silence, signal, 0.0) diff --git a/webrtc/modules/audio_processing/test/py_quality_assessment/quality_assessment/simulation.py b/webrtc/modules/audio_processing/test/py_quality_assessment/quality_assessment/simulation.py index e44ae225c3..ce73e65709 100644 --- a/webrtc/modules/audio_processing/test/py_quality_assessment/quality_assessment/simulation.py +++ b/webrtc/modules/audio_processing/test/py_quality_assessment/quality_assessment/simulation.py @@ -48,10 +48,18 @@ class ApmModuleSimulator(object): self._config_filepaths = None self._input_filepaths = None - def run(self, config_filepaths, input_filepaths, noise_generator_names, + def Run(self, config_filepaths, input_filepaths, noise_generator_names, eval_score_names, output_dir): - """ + """Runs the APM simulation. + Initializes paths and required instances, then runs all the simulations. + + Args: + config_filepaths: set of APM configuration files to test. + input_filepaths: set of input audio track files to test. + noise_generator_names: set of noise generator names to test. + eval_score_names: set of evaluation score names to test. + output_dir: base path to the output directory for wav files and outcomes. """ self._base_output_path = os.path.abspath(output_dir) @@ -67,15 +75,16 @@ class ApmModuleSimulator(object): name) in eval_score_names] # Set APM configuration file paths. - self._config_filepaths = self._get_paths_collection(config_filepaths) + self._config_filepaths = self._CreatePathsCollection(config_filepaths) # Set probing signal file paths. - self._input_filepaths = self._get_paths_collection(input_filepaths) + self._input_filepaths = self._CreatePathsCollection(input_filepaths) - self._simulate_all() + self._SimulateAll() + + def _SimulateAll(self): + """Runs all the simulations. - def _simulate_all(self): - """ Iterates over the combinations of APM configurations, probing signals, and noise generators. """ @@ -98,7 +107,7 @@ class ApmModuleSimulator(object): self._base_output_path, '_cache', 'input_{}-noise_{}'.format(input_name, noise_generator.NAME)) - data_access.make_directory(input_noise_cache_path) + data_access.MakeDirectory(input_noise_cache_path) logging.debug('input-noise cache path: <%s>', input_noise_cache_path) # Full output path. @@ -107,21 +116,29 @@ class ApmModuleSimulator(object): 'cfg-{}'.format(config_name), 'input-{}'.format(input_name), 'noise-{}'.format(noise_generator.NAME)) - data_access.make_directory(output_path) + data_access.MakeDirectory(output_path) logging.debug('output path: <%s>', output_path) - self._simulate(noise_generator, input_filepath, + self._Simulate(noise_generator, input_filepath, input_noise_cache_path, output_path, config_filepath) - def _simulate(self, noise_generator, input_filepath, input_noise_cache_path, + def _Simulate(self, noise_generator, input_filepath, input_noise_cache_path, output_path, config_filepath): - """ - Simulates a given combination of APM configurations, probing signals, and - noise generators. It iterates over the noise generator internal + """Runs a single set of simulation. + + Simulates a given combination of APM configuration, probing signal, and + noise generator. It iterates over the noise generator internal configurations. + + Args: + noise_generator: NoiseGenerator instance. + input_filepath: input audio track file to test. + input_noise_cache_path: path for the noisy audio track files. + output_path: base output path for the noise generator. + config_filepath: APM configuration file to test. """ # Generate pairs of noisy input and reference signal files. - noise_generator.generate( + noise_generator.Generate( input_signal_filepath=input_filepath, input_noise_cache_path=input_noise_cache_path, base_output_path=output_path) @@ -133,11 +150,11 @@ class ApmModuleSimulator(object): # APM input and output signal paths. noisy_signal_filepath = noise_generator.noisy_signal_filepaths[ noise_generator_config_name] - evaluation_output_path = noise_generator.output_paths[ + evaluation_output_path = noise_generator.apm_output_paths[ noise_generator_config_name] # Simulate a call using the audio processing module. - self._audioproc_wrapper.run( + self._audioproc_wrapper.Run( config_filepath=config_filepath, input_filepath=noisy_signal_filepath, output_path=evaluation_output_path) @@ -147,18 +164,25 @@ class ApmModuleSimulator(object): noise_generator_config_name] # Evaluate. - self._evaluator.run( + self._evaluator.Run( evaluation_score_workers=self._evaluation_score_workers, apm_output_filepath=self._audioproc_wrapper.output_filepath, reference_input_filepath=reference_signal_filepath, output_path=evaluation_output_path) @classmethod - def _get_paths_collection(cls, filepaths): - """ - Given a list of file paths, makes a collection with one pair for each item - in the list where the key is the file name without extension and the value - is the path. + def _CreatePathsCollection(cls, filepaths): + """Creates a collection of file paths. + + Given a list of file paths, makes a collection with one item for each file + path. The value is absolute path, the key is the file name without + extenstion. + + Args: + filepaths: list of file paths. + + Returns: + A dict. """ filepaths_collection = {} for filepath in filepaths: