| # Copyright (c) 2017 The WebRTC project authors. All Rights Reserved. |
| # |
| # Use of this source code is governed by a BSD-style license |
| # that can be found in the LICENSE file in the root of the source |
| # tree. An additional intellectual property rights grant can be found |
| # in the file PATENTS. All contributing project authors may |
| # be found in the AUTHORS file in the root of the source tree. |
| """APM module simulator. |
| """ |
| |
| import logging |
| import os |
| |
| from . import annotations |
| from . import data_access |
| from . import echo_path_simulation |
| from . import echo_path_simulation_factory |
| from . import eval_scores |
| from . import exceptions |
| from . import input_mixer |
| from . import input_signal_creator |
| from . import signal_processing |
| from . import test_data_generation |
| |
| |
| class ApmModuleSimulator(object): |
| """Audio processing module (APM) simulator class. |
| """ |
| |
| _TEST_DATA_GENERATOR_CLASSES = ( |
| test_data_generation.TestDataGenerator.REGISTERED_CLASSES) |
| _EVAL_SCORE_WORKER_CLASSES = eval_scores.EvaluationScore.REGISTERED_CLASSES |
| |
| _PREFIX_APM_CONFIG = 'apmcfg-' |
| _PREFIX_CAPTURE = 'capture-' |
| _PREFIX_RENDER = 'render-' |
| _PREFIX_ECHO_SIMULATOR = 'echosim-' |
| _PREFIX_TEST_DATA_GEN = 'datagen-' |
| _PREFIX_TEST_DATA_GEN_PARAMS = 'datagen_params-' |
| _PREFIX_SCORE = 'score-' |
| |
| def __init__(self, |
| test_data_generator_factory, |
| evaluation_score_factory, |
| ap_wrapper, |
| evaluator, |
| external_vads=None): |
| if external_vads is None: |
| external_vads = {} |
| self._test_data_generator_factory = test_data_generator_factory |
| self._evaluation_score_factory = evaluation_score_factory |
| self._audioproc_wrapper = ap_wrapper |
| self._evaluator = evaluator |
| self._annotator = annotations.AudioAnnotationsExtractor( |
| annotations.AudioAnnotationsExtractor.VadType.ENERGY_THRESHOLD |
| | annotations.AudioAnnotationsExtractor.VadType.WEBRTC_COMMON_AUDIO |
| | annotations.AudioAnnotationsExtractor.VadType.WEBRTC_APM, |
| external_vads) |
| |
| # Init. |
| self._test_data_generator_factory.SetOutputDirectoryPrefix( |
| self._PREFIX_TEST_DATA_GEN_PARAMS) |
| self._evaluation_score_factory.SetScoreFilenamePrefix( |
| self._PREFIX_SCORE) |
| |
| # Properties for each run. |
| self._base_output_path = None |
| self._output_cache_path = None |
| self._test_data_generators = None |
| self._evaluation_score_workers = None |
| self._config_filepaths = None |
| self._capture_input_filepaths = None |
| self._render_input_filepaths = None |
| self._echo_path_simulator_class = None |
| |
| @classmethod |
| def GetPrefixApmConfig(cls): |
| return cls._PREFIX_APM_CONFIG |
| |
| @classmethod |
| def GetPrefixCapture(cls): |
| return cls._PREFIX_CAPTURE |
| |
| @classmethod |
| def GetPrefixRender(cls): |
| return cls._PREFIX_RENDER |
| |
| @classmethod |
| def GetPrefixEchoSimulator(cls): |
| return cls._PREFIX_ECHO_SIMULATOR |
| |
| @classmethod |
| def GetPrefixTestDataGenerator(cls): |
| return cls._PREFIX_TEST_DATA_GEN |
| |
| @classmethod |
| def GetPrefixTestDataGeneratorParameters(cls): |
| return cls._PREFIX_TEST_DATA_GEN_PARAMS |
| |
| @classmethod |
| def GetPrefixScore(cls): |
| return cls._PREFIX_SCORE |
| |
| def Run(self, |
| config_filepaths, |
| capture_input_filepaths, |
| test_data_generator_names, |
| eval_score_names, |
| output_dir, |
| render_input_filepaths=None, |
| echo_path_simulator_name=( |
| echo_path_simulation.NoEchoPathSimulator.NAME)): |
| """Runs the APM simulation. |
| |
| Initializes paths and required instances, then runs all the simulations. |
| The render input can be optionally added. If added, the number of capture |
| input audio tracks and the number of render input audio tracks have to be |
| equal. The two lists are used to form pairs of capture and render input. |
| |
| Args: |
| config_filepaths: set of APM configuration files to test. |
| capture_input_filepaths: set of capture input audio track files to test. |
| test_data_generator_names: set of test data 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. |
| render_input_filepaths: set of render input audio track files to test. |
| echo_path_simulator_name: name of the echo path simulator to use when |
| render input is provided. |
| """ |
| assert render_input_filepaths is None or ( |
| len(capture_input_filepaths) == len(render_input_filepaths)), ( |
| 'render input set size not matching input set size') |
| assert render_input_filepaths is None or echo_path_simulator_name in ( |
| echo_path_simulation.EchoPathSimulator.REGISTERED_CLASSES), ( |
| 'invalid echo path simulator') |
| self._base_output_path = os.path.abspath(output_dir) |
| |
| # Output path used to cache the data shared across simulations. |
| self._output_cache_path = os.path.join(self._base_output_path, |
| '_cache') |
| |
| # Instance test data generators. |
| self._test_data_generators = [ |
| self._test_data_generator_factory.GetInstance( |
| test_data_generators_class=( |
| self._TEST_DATA_GENERATOR_CLASSES[name])) |
| for name in (test_data_generator_names) |
| ] |
| |
| # Instance evaluation score workers. |
| self._evaluation_score_workers = [ |
| self._evaluation_score_factory.GetInstance( |
| evaluation_score_class=self._EVAL_SCORE_WORKER_CLASSES[name]) |
| for (name) in eval_score_names |
| ] |
| |
| # Set APM configuration file paths. |
| self._config_filepaths = self._CreatePathsCollection(config_filepaths) |
| |
| # Set probing signal file paths. |
| if render_input_filepaths is None: |
| # Capture input only. |
| self._capture_input_filepaths = self._CreatePathsCollection( |
| capture_input_filepaths) |
| self._render_input_filepaths = None |
| else: |
| # Set both capture and render input signals. |
| self._SetTestInputSignalFilePaths(capture_input_filepaths, |
| render_input_filepaths) |
| |
| # Set the echo path simulator class. |
| self._echo_path_simulator_class = ( |
| echo_path_simulation.EchoPathSimulator. |
| REGISTERED_CLASSES[echo_path_simulator_name]) |
| |
| self._SimulateAll() |
| |
| def _SimulateAll(self): |
| """Runs all the simulations. |
| |
| Iterates over the combinations of APM configurations, probing signals, and |
| test data generators. This method is mainly responsible for the creation of |
| the cache and output directories required in order to call _Simulate(). |
| """ |
| without_render_input = self._render_input_filepaths is None |
| |
| # Try different APM config files. |
| for config_name in self._config_filepaths: |
| config_filepath = self._config_filepaths[config_name] |
| |
| # Try different capture-render pairs. |
| for capture_input_name in self._capture_input_filepaths: |
| # Output path for the capture signal annotations. |
| capture_annotations_cache_path = os.path.join( |
| self._output_cache_path, |
| self._PREFIX_CAPTURE + capture_input_name) |
| data_access.MakeDirectory(capture_annotations_cache_path) |
| |
| # Capture. |
| capture_input_filepath = self._capture_input_filepaths[ |
| capture_input_name] |
| if not os.path.exists(capture_input_filepath): |
| # If the input signal file does not exist, try to create using the |
| # available input signal creators. |
| self._CreateInputSignal(capture_input_filepath) |
| assert os.path.exists(capture_input_filepath) |
| self._ExtractCaptureAnnotations( |
| capture_input_filepath, capture_annotations_cache_path) |
| |
| # Render and simulated echo path (optional). |
| render_input_filepath = None if without_render_input else ( |
| self._render_input_filepaths[capture_input_name]) |
| render_input_name = '(none)' if without_render_input else ( |
| self._ExtractFileName(render_input_filepath)) |
| echo_path_simulator = (echo_path_simulation_factory. |
| EchoPathSimulatorFactory.GetInstance( |
| self._echo_path_simulator_class, |
| render_input_filepath)) |
| |
| # Try different test data generators. |
| for test_data_generators in self._test_data_generators: |
| logging.info( |
| 'APM config preset: <%s>, capture: <%s>, render: <%s>,' |
| 'test data generator: <%s>, echo simulator: <%s>', |
| config_name, capture_input_name, render_input_name, |
| test_data_generators.NAME, echo_path_simulator.NAME) |
| |
| # Output path for the generated test data. |
| test_data_cache_path = os.path.join( |
| capture_annotations_cache_path, |
| self._PREFIX_TEST_DATA_GEN + test_data_generators.NAME) |
| data_access.MakeDirectory(test_data_cache_path) |
| logging.debug('test data cache path: <%s>', |
| test_data_cache_path) |
| |
| # Output path for the echo simulator and APM input mixer output. |
| echo_test_data_cache_path = os.path.join( |
| test_data_cache_path, |
| 'echosim-{}'.format(echo_path_simulator.NAME)) |
| data_access.MakeDirectory(echo_test_data_cache_path) |
| logging.debug('echo test data cache path: <%s>', |
| echo_test_data_cache_path) |
| |
| # Full output path. |
| output_path = os.path.join( |
| self._base_output_path, |
| self._PREFIX_APM_CONFIG + config_name, |
| self._PREFIX_CAPTURE + capture_input_name, |
| self._PREFIX_RENDER + render_input_name, |
| self._PREFIX_ECHO_SIMULATOR + echo_path_simulator.NAME, |
| self._PREFIX_TEST_DATA_GEN + test_data_generators.NAME) |
| data_access.MakeDirectory(output_path) |
| logging.debug('output path: <%s>', output_path) |
| |
| self._Simulate(test_data_generators, |
| capture_input_filepath, |
| render_input_filepath, test_data_cache_path, |
| echo_test_data_cache_path, output_path, |
| config_filepath, echo_path_simulator) |
| |
| @staticmethod |
| def _CreateInputSignal(input_signal_filepath): |
| """Creates a missing input signal file. |
| |
| The file name is parsed to extract input signal creator and params. If a |
| creator is matched and the parameters are valid, a new signal is generated |
| and written in `input_signal_filepath`. |
| |
| Args: |
| input_signal_filepath: Path to the input signal audio file to write. |
| |
| Raises: |
| InputSignalCreatorException |
| """ |
| filename = os.path.splitext( |
| os.path.split(input_signal_filepath)[-1])[0] |
| filename_parts = filename.split('-') |
| |
| if len(filename_parts) < 2: |
| raise exceptions.InputSignalCreatorException( |
| 'Cannot parse input signal file name') |
| |
| signal, metadata = input_signal_creator.InputSignalCreator.Create( |
| filename_parts[0], filename_parts[1].split('_')) |
| |
| signal_processing.SignalProcessingUtils.SaveWav( |
| input_signal_filepath, signal) |
| data_access.Metadata.SaveFileMetadata(input_signal_filepath, metadata) |
| |
| def _ExtractCaptureAnnotations(self, |
| input_filepath, |
| output_path, |
| annotation_name=""): |
| self._annotator.Extract(input_filepath) |
| self._annotator.Save(output_path, annotation_name) |
| |
| def _Simulate(self, test_data_generators, clean_capture_input_filepath, |
| render_input_filepath, test_data_cache_path, |
| echo_test_data_cache_path, output_path, config_filepath, |
| echo_path_simulator): |
| """Runs a single set of simulation. |
| |
| Simulates a given combination of APM configuration, probing signal, and |
| test data generator. It iterates over the test data generator |
| internal configurations. |
| |
| Args: |
| test_data_generators: TestDataGenerator instance. |
| clean_capture_input_filepath: capture input audio track file to be |
| processed by a test data generator and |
| not affected by echo. |
| render_input_filepath: render input audio track file to test. |
| test_data_cache_path: path for the generated test audio track files. |
| echo_test_data_cache_path: path for the echo simulator. |
| output_path: base output path for the test data generator. |
| config_filepath: APM configuration file to test. |
| echo_path_simulator: EchoPathSimulator instance. |
| """ |
| # Generate pairs of noisy input and reference signal files. |
| test_data_generators.Generate( |
| input_signal_filepath=clean_capture_input_filepath, |
| test_data_cache_path=test_data_cache_path, |
| base_output_path=output_path) |
| |
| # Extract metadata linked to the clean input file (if any). |
| apm_input_metadata = None |
| try: |
| apm_input_metadata = data_access.Metadata.LoadFileMetadata( |
| clean_capture_input_filepath) |
| except IOError as e: |
| apm_input_metadata = {} |
| apm_input_metadata['test_data_gen_name'] = test_data_generators.NAME |
| apm_input_metadata['test_data_gen_config'] = None |
| |
| # For each test data pair, simulate a call and evaluate. |
| for config_name in test_data_generators.config_names: |
| logging.info(' - test data generator config: <%s>', config_name) |
| apm_input_metadata['test_data_gen_config'] = config_name |
| |
| # Paths to the test data generator output. |
| # Note that the reference signal does not depend on the render input |
| # which is optional. |
| noisy_capture_input_filepath = ( |
| test_data_generators.noisy_signal_filepaths[config_name]) |
| reference_signal_filepath = ( |
| test_data_generators.reference_signal_filepaths[config_name]) |
| |
| # Output path for the evaluation (e.g., APM output file). |
| evaluation_output_path = test_data_generators.apm_output_paths[ |
| config_name] |
| |
| # Paths to the APM input signals. |
| echo_path_filepath = echo_path_simulator.Simulate( |
| echo_test_data_cache_path) |
| apm_input_filepath = input_mixer.ApmInputMixer.Mix( |
| echo_test_data_cache_path, noisy_capture_input_filepath, |
| echo_path_filepath) |
| |
| # Extract annotations for the APM input mix. |
| apm_input_basepath, apm_input_filename = os.path.split( |
| apm_input_filepath) |
| self._ExtractCaptureAnnotations( |
| apm_input_filepath, apm_input_basepath, |
| os.path.splitext(apm_input_filename)[0] + '-') |
| |
| # Simulate a call using APM. |
| self._audioproc_wrapper.Run( |
| config_filepath=config_filepath, |
| capture_input_filepath=apm_input_filepath, |
| render_input_filepath=render_input_filepath, |
| output_path=evaluation_output_path) |
| |
| try: |
| # Evaluate. |
| self._evaluator.Run( |
| evaluation_score_workers=self._evaluation_score_workers, |
| apm_input_metadata=apm_input_metadata, |
| apm_output_filepath=self._audioproc_wrapper. |
| output_filepath, |
| reference_input_filepath=reference_signal_filepath, |
| render_input_filepath=render_input_filepath, |
| output_path=evaluation_output_path, |
| ) |
| |
| # Save simulation metadata. |
| data_access.Metadata.SaveAudioTestDataPaths( |
| output_path=evaluation_output_path, |
| clean_capture_input_filepath=clean_capture_input_filepath, |
| echo_free_capture_filepath=noisy_capture_input_filepath, |
| echo_filepath=echo_path_filepath, |
| render_filepath=render_input_filepath, |
| capture_filepath=apm_input_filepath, |
| apm_output_filepath=self._audioproc_wrapper. |
| output_filepath, |
| apm_reference_filepath=reference_signal_filepath, |
| apm_config_filepath=config_filepath, |
| ) |
| except exceptions.EvaluationScoreException as e: |
| logging.warning('the evaluation failed: %s', e.message) |
| continue |
| |
| def _SetTestInputSignalFilePaths(self, capture_input_filepaths, |
| render_input_filepaths): |
| """Sets input and render input file paths collections. |
| |
| Pairs the input and render input files by storing the file paths into two |
| collections. The key is the file name of the input file. |
| |
| Args: |
| capture_input_filepaths: list of file paths. |
| render_input_filepaths: list of file paths. |
| """ |
| self._capture_input_filepaths = {} |
| self._render_input_filepaths = {} |
| assert len(capture_input_filepaths) == len(render_input_filepaths) |
| for capture_input_filepath, render_input_filepath in zip( |
| capture_input_filepaths, render_input_filepaths): |
| name = self._ExtractFileName(capture_input_filepath) |
| self._capture_input_filepaths[name] = os.path.abspath( |
| capture_input_filepath) |
| self._render_input_filepaths[name] = os.path.abspath( |
| render_input_filepath) |
| |
| @classmethod |
| 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: |
| name = cls._ExtractFileName(filepath) |
| filepaths_collection[name] = os.path.abspath(filepath) |
| return filepaths_collection |
| |
| @classmethod |
| def _ExtractFileName(cls, filepath): |
| return os.path.splitext(os.path.split(filepath)[-1])[0] |