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