Repository: pytorch/audio

Issue:
Add Fade
#260 Working on add fade into transformations


Discussion:

Resolving patch:
diff --git a/torchaudio/transforms.py b/torchaudio/transforms.py
--- a/torchaudio/transforms.py
+++ b/torchaudio/transforms.py
@@ -22,6 +22,7 @@
     'Resample',
     'ComplexNorm',
     'TimeStretch',
+    'Fade',
     'FrequencyMasking',
     'TimeMasking',
 ]
@@ -640,6 +641,79 @@ def forward(self, complex_specgrams, overriding_rate=None):
         return F.phase_vocoder(complex_specgrams, rate, self.phase_advance)
 
 
+class Fade(torch.nn.Module):
+    r"""Add a fade in and/or fade out to an waveform.
+
+    Args:
+        fade_in_len (int, optional): Length of fade-in (time frames). (Default: ``0``)
+        fade_out_len (int, optional): Length of fade-out (time frames). (Default: ``0``)
+        fade_shape (str, optional): Shape of fade. Must be one of: "quarter_sine",
+            "half_sine", "linear", "logarithmic", "exponential". (Default: ``"linear"``)
+    """
+    def __init__(self, fade_in_len=0, fade_out_len=0, fade_shape="linear"):
+        super(Fade, self).__init__()
+        self.fade_in_len = fade_in_len
+        self.fade_out_len = fade_out_len
+        self.fade_shape = fade_shape
+
+    def forward(self, waveform):
+        # type: (Tensor) -> Tensor
+        r"""
+        Args:
+            waveform (torch.Tensor): Tensor of audio of dimension (..., time).
+
+        Returns:
+            torch.Tensor: Tensor of audio of dimension (..., time).
+        """
+        waveform_length = waveform.size()[-1]
+
+        return self._fade_in(waveform_length) * self._fade_out(waveform_length) * waveform
+
+    def _fade_in(self, waveform_length):
+        # type: (int) -> Tensor
+        fade = torch.linspace(0, 1, self.fade_in_len)
+        ones = torch.ones(waveform_length - self.fade_in_len)
+
+        if self.fade_shape == "linear":
+            fade = fade
+
+        if self.fade_shape == "exponential":
+            fade = torch.pow(2, (fade - 1)) * fade
+
+        if self.fade_shape == "logarithmic":
+            fade = torch.log10(.1 + fade) + 1
+
+        if self.fade_shape == "quarter_sine":
+            fade = torch.sin(fade * math.pi / 2)
+
+        if self.fade_shape == "half_sine":
+            fade = torch.sin(fade * math.pi - math.pi / 2) / 2 + 0.5
+
+        return torch.cat((fade, ones)).clamp_(0, 1)
+
+    def _fade_out(self, waveform_length):
+        # type: (int) -> Tensor
+        fade = torch.linspace(0, 1, self.fade_out_len)
+        ones = torch.ones(waveform_length - self.fade_out_len)
+
+        if self.fade_shape == "linear":
+            fade = - fade + 1
+
+        if self.fade_shape == "exponential":
+            fade = torch.pow(2, - fade) * (1 - fade)
+
+        if self.fade_shape == "logarithmic":
+            fade = torch.log10(1.1 - fade) + 1
+
+        if self.fade_shape == "quarter_sine":
+            fade = torch.sin(fade * math.pi / 2 + math.pi / 2)
+
+        if self.fade_shape == "half_sine":
+            fade = torch.sin(fade * math.pi + math.pi / 2) / 2 + 0.5
+
+        return torch.cat((ones, fade)).clamp_(0, 1)
+
+
 class _AxisMasking(torch.nn.Module):
     r"""Apply masking to a spectrogram.
 


Test patch:
diff --git a/test/test_sox_effects.py b/test/test_sox_effects.py
--- a/test/test_sox_effects.py
+++ b/test/test_sox_effects.py
@@ -228,6 +228,22 @@ def test_invalid_effect_options(self):
         with self.assertRaises(RuntimeError):
             E.sox_build_flow_effects()
 
+    def test_fade(self):
+        x_orig, _ = torchaudio.load(self.test_filepath)
+        fade_in_len = 44100
+        fade_out_len = 44100
+
+        for fade_shape_sox, fade_shape_torchaudio in (("q", "quarter_sine"), ("h", "half_sine"), ("t", "linear")):
+            E = torchaudio.sox_effects.SoxEffectsChain()
+            E.set_input_file(self.test_filepath)
+            E.append_effect_to_chain("fade", [fade_shape_sox, 1, "0", 1])
+            x, sr = E.sox_build_flow_effects()
+
+            fade = torchaudio.transforms.Fade(fade_in_len, fade_out_len, fade_shape_torchaudio)
+
+            # check if effect worked
+            self.assertTrue(x.allclose(fade(x_orig), rtol=1e-4, atol=1e-4))
+
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/test/test_transforms.py b/test/test_transforms.py
--- a/test/test_transforms.py
+++ b/test/test_transforms.py
@@ -519,6 +519,27 @@ def test_scriptmodule_TimeStretch(self):
         tensor = torch.rand((10, 2, n_freq, 10, 2))
         _test_script_module(transforms.TimeStretch, tensor, n_freq=n_freq, hop_length=hop_length, fixed_rate=fixed_rate)
 
+    def test_batch_Fade(self):
+        waveform, sample_rate = torchaudio.load(self.test_filepath)
+        fade_in_len = 3000
+        fade_out_len = 3000
+
+        # Single then transform then batch
+        expected = transforms.Fade(fade_in_len, fade_out_len)(waveform).repeat(3, 1, 1)
+
+        # Batch then transform
+        computed = transforms.Fade(fade_in_len, fade_out_len)(waveform.repeat(3, 1, 1))
+
+        self.assertTrue(computed.shape == expected.shape, (computed.shape, expected.shape))
+        self.assertTrue(torch.allclose(computed, expected))
+
+    def test_scriptmodule_Fade(self):
+        waveform, sample_rate = torchaudio.load(self.test_filepath)
+        fade_in_len = 3000
+        fade_out_len = 3000
+
+        _test_script_module(transforms.Fade, waveform, fade_in_len, fade_out_len)
+
     def test_scriptmodule_FrequencyMasking(self):
         tensor = torch.rand((10, 2, 50, 10, 2))
         _test_script_module(transforms.FrequencyMasking, tensor, freq_mask_param=60, iid_masks=False)