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"""Input mixer module.
9"""
10
11import logging
12import os
13
14from . import exceptions
15from . import signal_processing
16
17
18class ApmInputMixer(object):
19    """Class to mix a set of audio segments down to the APM input."""
20
21    _HARD_CLIPPING_LOG_MSG = 'hard clipping detected in the  mixed signal'
22
23    def __init__(self):
24        pass
25
26    @classmethod
27    def HardClippingLogMessage(cls):
28        """Returns the log message used when hard clipping is detected in the mix.
29
30    This method is mainly intended to be used by the unit tests.
31    """
32        return cls._HARD_CLIPPING_LOG_MSG
33
34    @classmethod
35    def Mix(cls, output_path, capture_input_filepath, echo_filepath):
36        """Mixes capture and echo.
37
38    Creates the overall capture input for APM by mixing the "echo-free" capture
39    signal with the echo signal (e.g., echo simulated via the
40    echo_path_simulation module).
41
42    The echo signal cannot be shorter than the capture signal and the generated
43    mix will have the same duration of the capture signal. The latter property
44    is enforced in order to let the input of APM and the reference signal
45    created by TestDataGenerator have the same length (required for the
46    evaluation step).
47
48    Hard-clipping may occur in the mix; a warning is raised when this happens.
49
50    If |echo_filepath| is None, nothing is done and |capture_input_filepath| is
51    returned.
52
53    Args:
54      speech: AudioSegment instance.
55      echo_path: AudioSegment instance or None.
56
57    Returns:
58      Path to the mix audio track file.
59    """
60        if echo_filepath is None:
61            return capture_input_filepath
62
63        # Build the mix output file name as a function of the echo file name.
64        # This ensures that if the internal parameters of the echo path simulator
65        # change, no erroneous cache hit occurs.
66        echo_file_name, _ = os.path.splitext(os.path.split(echo_filepath)[1])
67        capture_input_file_name, _ = os.path.splitext(
68            os.path.split(capture_input_filepath)[1])
69        mix_filepath = os.path.join(
70            output_path,
71            'mix_capture_{}_{}.wav'.format(capture_input_file_name,
72                                           echo_file_name))
73
74        # Create the mix if not done yet.
75        mix = None
76        if not os.path.exists(mix_filepath):
77            echo_free_capture = signal_processing.SignalProcessingUtils.LoadWav(
78                capture_input_filepath)
79            echo = signal_processing.SignalProcessingUtils.LoadWav(
80                echo_filepath)
81
82            if signal_processing.SignalProcessingUtils.CountSamples(echo) < (
83                    signal_processing.SignalProcessingUtils.CountSamples(
84                        echo_free_capture)):
85                raise exceptions.InputMixerException(
86                    'echo cannot be shorter than capture')
87
88            mix = echo_free_capture.overlay(echo)
89            signal_processing.SignalProcessingUtils.SaveWav(mix_filepath, mix)
90
91        # Check if hard clipping occurs.
92        if mix is None:
93            mix = signal_processing.SignalProcessingUtils.LoadWav(mix_filepath)
94        if signal_processing.SignalProcessingUtils.DetectHardClipping(mix):
95            logging.warning(cls._HARD_CLIPPING_LOG_MSG)
96
97        return mix_filepath
98