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