1# Copyright (c) 2017 The WebRTC project authors. All Rights Reserved. 2# 3# Use of this source code is governed by a BSD-style license 4# that can be found in the LICENSE file in the root of the source 5# tree. An additional intellectual property rights grant can be found 6# in the file PATENTS. All contributing project authors may 7# be found in the AUTHORS file in the root of the source tree. 8 9"""APM module simulator. 10""" 11 12import logging 13import os 14 15from . import annotations 16from . import data_access 17from . import echo_path_simulation 18from . import echo_path_simulation_factory 19from . import eval_scores 20from . import exceptions 21from . import input_mixer 22from . import input_signal_creator 23from . import signal_processing 24from . import test_data_generation 25 26 27class ApmModuleSimulator(object): 28 """Audio processing module (APM) simulator class. 29 """ 30 31 _TEST_DATA_GENERATOR_CLASSES = ( 32 test_data_generation.TestDataGenerator.REGISTERED_CLASSES) 33 _EVAL_SCORE_WORKER_CLASSES = eval_scores.EvaluationScore.REGISTERED_CLASSES 34 35 _PREFIX_APM_CONFIG = 'apmcfg-' 36 _PREFIX_CAPTURE = 'capture-' 37 _PREFIX_RENDER = 'render-' 38 _PREFIX_ECHO_SIMULATOR = 'echosim-' 39 _PREFIX_TEST_DATA_GEN = 'datagen-' 40 _PREFIX_TEST_DATA_GEN_PARAMS = 'datagen_params-' 41 _PREFIX_SCORE = 'score-' 42 43 def __init__(self, test_data_generator_factory, evaluation_score_factory, 44 ap_wrapper, evaluator, external_vads=None): 45 if external_vads is None: 46 external_vads = {} 47 self._test_data_generator_factory = test_data_generator_factory 48 self._evaluation_score_factory = evaluation_score_factory 49 self._audioproc_wrapper = ap_wrapper 50 self._evaluator = evaluator 51 self._annotator = annotations.AudioAnnotationsExtractor( 52 annotations.AudioAnnotationsExtractor.VadType.ENERGY_THRESHOLD | 53 annotations.AudioAnnotationsExtractor.VadType.WEBRTC_COMMON_AUDIO | 54 annotations.AudioAnnotationsExtractor.VadType.WEBRTC_APM, 55 external_vads 56 ) 57 58 # Init. 59 self._test_data_generator_factory.SetOutputDirectoryPrefix( 60 self._PREFIX_TEST_DATA_GEN_PARAMS) 61 self._evaluation_score_factory.SetScoreFilenamePrefix( 62 self._PREFIX_SCORE) 63 64 # Properties for each run. 65 self._base_output_path = None 66 self._output_cache_path = None 67 self._test_data_generators = None 68 self._evaluation_score_workers = None 69 self._config_filepaths = None 70 self._capture_input_filepaths = None 71 self._render_input_filepaths = None 72 self._echo_path_simulator_class = None 73 74 @classmethod 75 def GetPrefixApmConfig(cls): 76 return cls._PREFIX_APM_CONFIG 77 78 @classmethod 79 def GetPrefixCapture(cls): 80 return cls._PREFIX_CAPTURE 81 82 @classmethod 83 def GetPrefixRender(cls): 84 return cls._PREFIX_RENDER 85 86 @classmethod 87 def GetPrefixEchoSimulator(cls): 88 return cls._PREFIX_ECHO_SIMULATOR 89 90 @classmethod 91 def GetPrefixTestDataGenerator(cls): 92 return cls._PREFIX_TEST_DATA_GEN 93 94 @classmethod 95 def GetPrefixTestDataGeneratorParameters(cls): 96 return cls._PREFIX_TEST_DATA_GEN_PARAMS 97 98 @classmethod 99 def GetPrefixScore(cls): 100 return cls._PREFIX_SCORE 101 102 def Run(self, config_filepaths, capture_input_filepaths, 103 test_data_generator_names, eval_score_names, output_dir, 104 render_input_filepaths=None, echo_path_simulator_name=( 105 echo_path_simulation.NoEchoPathSimulator.NAME)): 106 """Runs the APM simulation. 107 108 Initializes paths and required instances, then runs all the simulations. 109 The render input can be optionally added. If added, the number of capture 110 input audio tracks and the number of render input audio tracks have to be 111 equal. The two lists are used to form pairs of capture and render input. 112 113 Args: 114 config_filepaths: set of APM configuration files to test. 115 capture_input_filepaths: set of capture input audio track files to test. 116 test_data_generator_names: set of test data generator names to test. 117 eval_score_names: set of evaluation score names to test. 118 output_dir: base path to the output directory for wav files and outcomes. 119 render_input_filepaths: set of render input audio track files to test. 120 echo_path_simulator_name: name of the echo path simulator to use when 121 render input is provided. 122 """ 123 assert render_input_filepaths is None or ( 124 len(capture_input_filepaths) == len(render_input_filepaths)), ( 125 'render input set size not matching input set size') 126 assert render_input_filepaths is None or echo_path_simulator_name in ( 127 echo_path_simulation.EchoPathSimulator.REGISTERED_CLASSES), ( 128 'invalid echo path simulator') 129 self._base_output_path = os.path.abspath(output_dir) 130 131 # Output path used to cache the data shared across simulations. 132 self._output_cache_path = os.path.join(self._base_output_path, '_cache') 133 134 # Instance test data generators. 135 self._test_data_generators = [self._test_data_generator_factory.GetInstance( 136 test_data_generators_class=( 137 self._TEST_DATA_GENERATOR_CLASSES[name])) for name in ( 138 test_data_generator_names)] 139 140 # Instance evaluation score workers. 141 self._evaluation_score_workers = [ 142 self._evaluation_score_factory.GetInstance( 143 evaluation_score_class=self._EVAL_SCORE_WORKER_CLASSES[name]) for ( 144 name) in eval_score_names] 145 146 # Set APM configuration file paths. 147 self._config_filepaths = self._CreatePathsCollection(config_filepaths) 148 149 # Set probing signal file paths. 150 if render_input_filepaths is None: 151 # Capture input only. 152 self._capture_input_filepaths = self._CreatePathsCollection( 153 capture_input_filepaths) 154 self._render_input_filepaths = None 155 else: 156 # Set both capture and render input signals. 157 self._SetTestInputSignalFilePaths( 158 capture_input_filepaths, render_input_filepaths) 159 160 # Set the echo path simulator class. 161 self._echo_path_simulator_class = ( 162 echo_path_simulation.EchoPathSimulator.REGISTERED_CLASSES[ 163 echo_path_simulator_name]) 164 165 self._SimulateAll() 166 167 def _SimulateAll(self): 168 """Runs all the simulations. 169 170 Iterates over the combinations of APM configurations, probing signals, and 171 test data generators. This method is mainly responsible for the creation of 172 the cache and output directories required in order to call _Simulate(). 173 """ 174 without_render_input = self._render_input_filepaths is None 175 176 # Try different APM config files. 177 for config_name in self._config_filepaths: 178 config_filepath = self._config_filepaths[config_name] 179 180 # Try different capture-render pairs. 181 for capture_input_name in self._capture_input_filepaths: 182 # Output path for the capture signal annotations. 183 capture_annotations_cache_path = os.path.join( 184 self._output_cache_path, 185 self._PREFIX_CAPTURE + capture_input_name) 186 data_access.MakeDirectory(capture_annotations_cache_path) 187 188 # Capture. 189 capture_input_filepath = self._capture_input_filepaths[ 190 capture_input_name] 191 if not os.path.exists(capture_input_filepath): 192 # If the input signal file does not exist, try to create using the 193 # available input signal creators. 194 self._CreateInputSignal(capture_input_filepath) 195 assert os.path.exists(capture_input_filepath) 196 self._ExtractCaptureAnnotations( 197 capture_input_filepath, capture_annotations_cache_path) 198 199 # Render and simulated echo path (optional). 200 render_input_filepath = None if without_render_input else ( 201 self._render_input_filepaths[capture_input_name]) 202 render_input_name = '(none)' if without_render_input else ( 203 self._ExtractFileName(render_input_filepath)) 204 echo_path_simulator = ( 205 echo_path_simulation_factory.EchoPathSimulatorFactory.GetInstance( 206 self._echo_path_simulator_class, render_input_filepath)) 207 208 # Try different test data generators. 209 for test_data_generators in self._test_data_generators: 210 logging.info('APM config preset: <%s>, capture: <%s>, render: <%s>,' 211 'test data generator: <%s>, echo simulator: <%s>', 212 config_name, capture_input_name, render_input_name, 213 test_data_generators.NAME, echo_path_simulator.NAME) 214 215 # Output path for the generated test data. 216 test_data_cache_path = os.path.join( 217 capture_annotations_cache_path, 218 self._PREFIX_TEST_DATA_GEN + test_data_generators.NAME) 219 data_access.MakeDirectory(test_data_cache_path) 220 logging.debug('test data cache path: <%s>', test_data_cache_path) 221 222 # Output path for the echo simulator and APM input mixer output. 223 echo_test_data_cache_path = os.path.join( 224 test_data_cache_path, 'echosim-{}'.format( 225 echo_path_simulator.NAME)) 226 data_access.MakeDirectory(echo_test_data_cache_path) 227 logging.debug('echo test data cache path: <%s>', 228 echo_test_data_cache_path) 229 230 # Full output path. 231 output_path = os.path.join( 232 self._base_output_path, 233 self._PREFIX_APM_CONFIG + config_name, 234 self._PREFIX_CAPTURE + capture_input_name, 235 self._PREFIX_RENDER + render_input_name, 236 self._PREFIX_ECHO_SIMULATOR + echo_path_simulator.NAME, 237 self._PREFIX_TEST_DATA_GEN + test_data_generators.NAME) 238 data_access.MakeDirectory(output_path) 239 logging.debug('output path: <%s>', output_path) 240 241 self._Simulate(test_data_generators, capture_input_filepath, 242 render_input_filepath, test_data_cache_path, 243 echo_test_data_cache_path, output_path, 244 config_filepath, echo_path_simulator) 245 246 @staticmethod 247 def _CreateInputSignal(input_signal_filepath): 248 """Creates a missing input signal file. 249 250 The file name is parsed to extract input signal creator and params. If a 251 creator is matched and the parameters are valid, a new signal is generated 252 and written in |input_signal_filepath|. 253 254 Args: 255 input_signal_filepath: Path to the input signal audio file to write. 256 257 Raises: 258 InputSignalCreatorException 259 """ 260 filename = os.path.splitext(os.path.split(input_signal_filepath)[-1])[0] 261 filename_parts = filename.split('-') 262 263 if len(filename_parts) < 2: 264 raise exceptions.InputSignalCreatorException( 265 'Cannot parse input signal file name') 266 267 signal, metadata = input_signal_creator.InputSignalCreator.Create( 268 filename_parts[0], filename_parts[1].split('_')) 269 270 signal_processing.SignalProcessingUtils.SaveWav( 271 input_signal_filepath, signal) 272 data_access.Metadata.SaveFileMetadata(input_signal_filepath, metadata) 273 274 def _ExtractCaptureAnnotations(self, input_filepath, output_path, 275 annotation_name=""): 276 self._annotator.Extract(input_filepath) 277 self._annotator.Save(output_path, annotation_name) 278 279 def _Simulate(self, test_data_generators, clean_capture_input_filepath, 280 render_input_filepath, test_data_cache_path, 281 echo_test_data_cache_path, output_path, config_filepath, 282 echo_path_simulator): 283 """Runs a single set of simulation. 284 285 Simulates a given combination of APM configuration, probing signal, and 286 test data generator. It iterates over the test data generator 287 internal configurations. 288 289 Args: 290 test_data_generators: TestDataGenerator instance. 291 clean_capture_input_filepath: capture input audio track file to be 292 processed by a test data generator and 293 not affected by echo. 294 render_input_filepath: render input audio track file to test. 295 test_data_cache_path: path for the generated test audio track files. 296 echo_test_data_cache_path: path for the echo simulator. 297 output_path: base output path for the test data generator. 298 config_filepath: APM configuration file to test. 299 echo_path_simulator: EchoPathSimulator instance. 300 """ 301 # Generate pairs of noisy input and reference signal files. 302 test_data_generators.Generate( 303 input_signal_filepath=clean_capture_input_filepath, 304 test_data_cache_path=test_data_cache_path, 305 base_output_path=output_path) 306 307 # Extract metadata linked to the clean input file (if any). 308 apm_input_metadata = None 309 try: 310 apm_input_metadata = data_access.Metadata.LoadFileMetadata( 311 clean_capture_input_filepath) 312 except IOError as e: 313 apm_input_metadata = {} 314 apm_input_metadata['test_data_gen_name'] = test_data_generators.NAME 315 apm_input_metadata['test_data_gen_config'] = None 316 317 # For each test data pair, simulate a call and evaluate. 318 for config_name in test_data_generators.config_names: 319 logging.info(' - test data generator config: <%s>', config_name) 320 apm_input_metadata['test_data_gen_config'] = config_name 321 322 # Paths to the test data generator output. 323 # Note that the reference signal does not depend on the render input 324 # which is optional. 325 noisy_capture_input_filepath = ( 326 test_data_generators.noisy_signal_filepaths[config_name]) 327 reference_signal_filepath = ( 328 test_data_generators.reference_signal_filepaths[config_name]) 329 330 # Output path for the evaluation (e.g., APM output file). 331 evaluation_output_path = test_data_generators.apm_output_paths[ 332 config_name] 333 334 # Paths to the APM input signals. 335 echo_path_filepath = echo_path_simulator.Simulate( 336 echo_test_data_cache_path) 337 apm_input_filepath = input_mixer.ApmInputMixer.Mix( 338 echo_test_data_cache_path, noisy_capture_input_filepath, 339 echo_path_filepath) 340 341 # Extract annotations for the APM input mix. 342 apm_input_basepath, apm_input_filename = os.path.split( 343 apm_input_filepath) 344 self._ExtractCaptureAnnotations( 345 apm_input_filepath, apm_input_basepath, 346 os.path.splitext(apm_input_filename)[0] + '-') 347 348 # Simulate a call using APM. 349 self._audioproc_wrapper.Run( 350 config_filepath=config_filepath, 351 capture_input_filepath=apm_input_filepath, 352 render_input_filepath=render_input_filepath, 353 output_path=evaluation_output_path) 354 355 try: 356 # Evaluate. 357 self._evaluator.Run( 358 evaluation_score_workers=self._evaluation_score_workers, 359 apm_input_metadata=apm_input_metadata, 360 apm_output_filepath=self._audioproc_wrapper.output_filepath, 361 reference_input_filepath=reference_signal_filepath, 362 render_input_filepath=render_input_filepath, 363 output_path=evaluation_output_path, 364 ) 365 366 # Save simulation metadata. 367 data_access.Metadata.SaveAudioTestDataPaths( 368 output_path=evaluation_output_path, 369 clean_capture_input_filepath=clean_capture_input_filepath, 370 echo_free_capture_filepath=noisy_capture_input_filepath, 371 echo_filepath=echo_path_filepath, 372 render_filepath=render_input_filepath, 373 capture_filepath=apm_input_filepath, 374 apm_output_filepath=self._audioproc_wrapper.output_filepath, 375 apm_reference_filepath=reference_signal_filepath, 376 apm_config_filepath=config_filepath, 377 ) 378 except exceptions.EvaluationScoreException as e: 379 logging.warning('the evaluation failed: %s', e.message) 380 continue 381 382 def _SetTestInputSignalFilePaths(self, capture_input_filepaths, 383 render_input_filepaths): 384 """Sets input and render input file paths collections. 385 386 Pairs the input and render input files by storing the file paths into two 387 collections. The key is the file name of the input file. 388 389 Args: 390 capture_input_filepaths: list of file paths. 391 render_input_filepaths: list of file paths. 392 """ 393 self._capture_input_filepaths = {} 394 self._render_input_filepaths = {} 395 assert len(capture_input_filepaths) == len(render_input_filepaths) 396 for capture_input_filepath, render_input_filepath in zip( 397 capture_input_filepaths, render_input_filepaths): 398 name = self._ExtractFileName(capture_input_filepath) 399 self._capture_input_filepaths[name] = os.path.abspath( 400 capture_input_filepath) 401 self._render_input_filepaths[name] = os.path.abspath( 402 render_input_filepath) 403 404 @classmethod 405 def _CreatePathsCollection(cls, filepaths): 406 """Creates a collection of file paths. 407 408 Given a list of file paths, makes a collection with one item for each file 409 path. The value is absolute path, the key is the file name without 410 extenstion. 411 412 Args: 413 filepaths: list of file paths. 414 415 Returns: 416 A dict. 417 """ 418 filepaths_collection = {} 419 for filepath in filepaths: 420 name = cls._ExtractFileName(filepath) 421 filepaths_collection[name] = os.path.abspath(filepath) 422 return filepaths_collection 423 424 @classmethod 425 def _ExtractFileName(cls, filepath): 426 return os.path.splitext(os.path.split(filepath)[-1])[0] 427