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"""Echo path simulation module.
10"""
11
12import hashlib
13import os
14
15from . import signal_processing
16
17
18class EchoPathSimulator(object):
19  """Abstract class for the echo path simulators.
20
21  In general, an echo path simulator is a function of the render signal and
22  simulates the propagation of the latter into the microphone (e.g., due to
23  mechanical or electrical paths).
24  """
25
26  NAME = None
27  REGISTERED_CLASSES = {}
28
29  def __init__(self):
30    pass
31
32  def Simulate(self, output_path):
33    """Creates the echo signal and stores it in an audio file (abstract method).
34
35    Args:
36      output_path: Path in which any output can be saved.
37
38    Returns:
39      Path to the generated audio track file or None if no echo is present.
40    """
41    raise NotImplementedError()
42
43  @classmethod
44  def RegisterClass(cls, class_to_register):
45    """Registers an EchoPathSimulator implementation.
46
47    Decorator to automatically register the classes that extend
48    EchoPathSimulator.
49    Example usage:
50
51    @EchoPathSimulator.RegisterClass
52    class NoEchoPathSimulator(EchoPathSimulator):
53      pass
54    """
55    cls.REGISTERED_CLASSES[class_to_register.NAME] = class_to_register
56    return class_to_register
57
58
59@EchoPathSimulator.RegisterClass
60class NoEchoPathSimulator(EchoPathSimulator):
61  """Simulates absence of echo."""
62
63  NAME = 'noecho'
64
65  def __init__(self):
66    EchoPathSimulator.__init__(self)
67
68  def Simulate(self, output_path):
69    return None
70
71
72@EchoPathSimulator.RegisterClass
73class LinearEchoPathSimulator(EchoPathSimulator):
74  """Simulates linear echo path.
75
76  This class applies a given impulse response to the render input and then it
77  sums the signal to the capture input signal.
78  """
79
80  NAME = 'linear'
81
82  def __init__(self, render_input_filepath, impulse_response):
83    """
84    Args:
85      render_input_filepath: Render audio track file.
86      impulse_response: list or numpy vector of float values.
87    """
88    EchoPathSimulator.__init__(self)
89    self._render_input_filepath = render_input_filepath
90    self._impulse_response = impulse_response
91
92  def Simulate(self, output_path):
93    """Simulates linear echo path."""
94    # Form the file name with a hash of the impulse response.
95    impulse_response_hash = hashlib.sha256(
96        str(self._impulse_response).encode('utf-8', 'ignore')).hexdigest()
97    echo_filepath = os.path.join(output_path, 'linear_echo_{}.wav'.format(
98        impulse_response_hash))
99
100    # If the simulated echo audio track file does not exists, create it.
101    if not os.path.exists(echo_filepath):
102      render = signal_processing.SignalProcessingUtils.LoadWav(
103          self._render_input_filepath)
104      echo = signal_processing.SignalProcessingUtils.ApplyImpulseResponse(
105          render, self._impulse_response)
106      signal_processing.SignalProcessingUtils.SaveWav(echo_filepath, echo)
107
108    return echo_filepath
109
110
111@EchoPathSimulator.RegisterClass
112class RecordedEchoPathSimulator(EchoPathSimulator):
113  """Uses recorded echo.
114
115  This class uses the clean capture input file name to build the file name of
116  the corresponding recording containing echo (a predefined suffix is used).
117  Such a file is expected to be already existing.
118  """
119
120  NAME = 'recorded'
121
122  _FILE_NAME_SUFFIX = '_echo'
123
124  def __init__(self, render_input_filepath):
125    EchoPathSimulator.__init__(self)
126    self._render_input_filepath = render_input_filepath
127
128  def Simulate(self, output_path):
129    """Uses recorded echo path."""
130    path, file_name_ext = os.path.split(self._render_input_filepath)
131    file_name, file_ext = os.path.splitext(file_name_ext)
132    echo_filepath = os.path.join(path, '{}{}{}'.format(
133        file_name, self._FILE_NAME_SUFFIX, file_ext))
134    assert os.path.exists(echo_filepath), (
135           'cannot find the echo audio track file {}'.format(echo_filepath))
136    return echo_filepath
137