Skip to content

Commit e602161

Browse files
authored
Use the torchaudio resampler instead of the custom implementation (speechbrain#2410)
* Use the torchaudio resampler for better performance * Add intersphinx mapping for torchaudio * Resampler fixes and better docstring * Make the linter happy * Make the linter happy again * Keep compat by migrating the resampler to whatever input device
1 parent b8a3ee3 commit e602161

2 files changed

Lines changed: 24 additions & 249 deletions

File tree

docs/conf.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
"python": ("https://docs.python.org/", None),
6262
"numpy": ("http://docs.scipy.org/doc/numpy/", None),
6363
"torch": ("https://pytorch.org/docs/master/", None),
64+
"torchaudio": ("https://pytorch.org/audio/stable/", None),
6465
}
6566

6667
# AUTODOC:

speechbrain/augment/time_domain.py

Lines changed: 23 additions & 249 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@
1111
"""
1212

1313
# Importing libraries
14-
import math
1514
import random
1615
import torch
1716
import torch.nn.functional as F
17+
import torchaudio
1818
from speechbrain.dataio.legacy import ExtendedCSVDataset
1919
from speechbrain.dataio.dataloader import make_dataloader
2020
from speechbrain.processing.signal_processing import (
@@ -508,21 +508,22 @@ def forward(self, waveform):
508508

509509

510510
class Resample(torch.nn.Module):
511-
"""This class resamples an audio signal using sinc-based interpolation.
512-
513-
It is a modification of the `resample` function from torchaudio
514-
(https://pytorch.org/audio/stable/tutorials/audio_resampling_tutorial.html)
511+
"""This class resamples audio using the
512+
:class:`torchaudio resampler <torchaudio.transforms.Resample>` based on
513+
sinc interpolation.
515514
516515
Arguments
517516
---------
518517
orig_freq : int
519518
the sampling frequency of the input signal.
520519
new_freq : int
521520
the new sampling frequency after this operation is performed.
522-
lowpass_filter_width : int
523-
Controls the sharpness of the filter, larger numbers result in a
524-
sharper filter, but they are less efficient. Values from 4 to 10 are
525-
allowed.
521+
*args
522+
additional arguments forwarded to the
523+
:class:`torchaudio.transforms.Resample` constructor
524+
**kwargs
525+
additional keyword arguments forwarded to the
526+
:class:`torchaudio.transforms.Resample` constructor
526527
527528
Example
528529
-------
@@ -537,49 +538,28 @@ class Resample(torch.nn.Module):
537538
torch.Size([1, 26087])
538539
"""
539540

540-
def __init__(self, orig_freq=16000, new_freq=16000, lowpass_filter_width=6):
541+
def __init__(self, orig_freq=16000, new_freq=16000, *args, **kwargs):
541542
super().__init__()
543+
542544
self.orig_freq = orig_freq
543545
self.new_freq = new_freq
544-
self.lowpass_filter_width = lowpass_filter_width
545-
546-
# Compute rate for striding
547-
self._compute_strides()
548-
assert self.orig_freq % self.conv_stride == 0
549-
assert self.new_freq % self.conv_transpose_stride == 0
550546

551-
def _compute_strides(self):
552-
"""Compute the phases in polyphase filter.
553-
554-
(almost directly from torchaudio.compliance.kaldi)
555-
"""
556-
557-
# Compute new unit based on ratio of in/out frequencies
558-
base_freq = math.gcd(self.orig_freq, self.new_freq)
559-
input_samples_in_unit = self.orig_freq // base_freq
560-
self.output_samples = self.new_freq // base_freq
561-
562-
# Store the appropriate stride based on the new units
563-
self.conv_stride = input_samples_in_unit
564-
self.conv_transpose_stride = self.output_samples
547+
self.resampler = torchaudio.transforms.Resample(
548+
orig_freq=orig_freq, new_freq=new_freq, *args, **kwargs,
549+
)
565550

566551
def forward(self, waveforms):
567552
"""
568553
Arguments
569554
---------
570555
waveforms : tensor
571556
Shape should be `[batch, time]` or `[batch, time, channels]`.
572-
lengths : tensor
573-
Shape should be a single dimension, `[batch]`.
574557
575558
Returns
576559
-------
577560
Tensor of shape `[batch, time]` or `[batch, time, channels]`.
578561
"""
579562

580-
if not hasattr(self, "first_indices"):
581-
self._indices_and_weights(waveforms)
582-
583563
# Don't do anything if the frequencies are the same
584564
if self.orig_freq == self.new_freq:
585565
return waveforms
@@ -593,8 +573,15 @@ def forward(self, waveforms):
593573
else:
594574
raise ValueError("Input must be 2 or 3 dimensions")
595575

576+
# If necessary, migrate the resampler to the current device, for
577+
# backwards compat with scripts that do not call `resampler.to()`
578+
# themselves.
579+
# Please do not reuse the sample resampler for tensors that live on
580+
# different devices, though.
581+
self.resampler.to(waveforms.device) # in-place
582+
596583
# Do resampling
597-
resampled_waveform = self._perform_resample(waveforms)
584+
resampled_waveform = self.resampler(waveforms)
598585

599586
if unsqueezed:
600587
resampled_waveform = resampled_waveform.squeeze(1)
@@ -603,219 +590,6 @@ def forward(self, waveforms):
603590

604591
return resampled_waveform
605592

606-
def _perform_resample(self, waveforms):
607-
"""Resamples the waveform at the new frequency.
608-
609-
This matches Kaldi's OfflineFeatureTpl ResampleWaveform which uses a
610-
LinearResample (resample a signal at linearly spaced intervals to
611-
up/downsample a signal). LinearResample (LR) means that the output
612-
signal is at linearly spaced intervals (i.e the output signal has a
613-
frequency of `new_freq`). It uses sinc/bandlimited interpolation to
614-
upsample/downsample the signal.
615-
616-
(almost directly from torchaudio.compliance.kaldi)
617-
618-
https://ccrma.stanford.edu/~jos/resample/
619-
Theory_Ideal_Bandlimited_Interpolation.html
620-
621-
https://github.com/kaldi-asr/kaldi/blob/master/src/feat/resample.h#L56
622-
623-
Arguments
624-
---------
625-
waveforms : tensor
626-
The batch of audio signals to resample.
627-
628-
Returns
629-
-------
630-
The waveforms at the new frequency.
631-
"""
632-
633-
# Compute output size and initialize
634-
batch_size, num_channels, wave_len = waveforms.size()
635-
window_size = self.weights.size(1)
636-
tot_output_samp = self._output_samples(wave_len)
637-
resampled_waveform = torch.zeros(
638-
(batch_size, num_channels, tot_output_samp), device=waveforms.device
639-
)
640-
self.weights = self.weights.to(waveforms.device)
641-
642-
# Check weights are on correct device
643-
if waveforms.device != self.weights.device:
644-
self.weights = self.weights.to(waveforms.device)
645-
646-
# eye size: (num_channels, num_channels, 1)
647-
eye = torch.eye(num_channels, device=waveforms.device).unsqueeze(2)
648-
649-
# Iterate over the phases in the polyphase filter
650-
for i in range(self.first_indices.size(0)):
651-
wave_to_conv = waveforms
652-
first_index = int(self.first_indices[i].item())
653-
if first_index >= 0:
654-
# trim the signal as the filter will not be applied
655-
# before the first_index
656-
wave_to_conv = wave_to_conv[..., first_index:]
657-
658-
# pad the right of the signal to allow partial convolutions
659-
# meaning compute values for partial windows (e.g. end of the
660-
# window is outside the signal length)
661-
max_index = (tot_output_samp - 1) // self.output_samples
662-
end_index = max_index * self.conv_stride + window_size
663-
current_wave_len = wave_len - first_index
664-
right_padding = max(0, end_index + 1 - current_wave_len)
665-
left_padding = max(0, -first_index)
666-
wave_to_conv = torch.nn.functional.pad(
667-
wave_to_conv, (left_padding, right_padding)
668-
)
669-
conv_wave = torch.nn.functional.conv1d(
670-
input=wave_to_conv,
671-
weight=self.weights[i].repeat(num_channels, 1, 1),
672-
stride=self.conv_stride,
673-
groups=num_channels,
674-
)
675-
676-
# we want conv_wave[:, i] to be at
677-
# output[:, i + n*conv_transpose_stride]
678-
dilated_conv_wave = torch.nn.functional.conv_transpose1d(
679-
conv_wave, eye, stride=self.conv_transpose_stride
680-
)
681-
682-
# pad dilated_conv_wave so it reaches the output length if needed.
683-
left_padding = i
684-
previous_padding = left_padding + dilated_conv_wave.size(-1)
685-
right_padding = max(0, tot_output_samp - previous_padding)
686-
dilated_conv_wave = torch.nn.functional.pad(
687-
dilated_conv_wave, (left_padding, right_padding)
688-
)
689-
dilated_conv_wave = dilated_conv_wave[..., :tot_output_samp]
690-
691-
resampled_waveform += dilated_conv_wave
692-
693-
return resampled_waveform
694-
695-
def _output_samples(self, input_num_samp):
696-
"""Based on LinearResample::GetNumOutputSamples.
697-
698-
LinearResample (LR) means that the output signal is at
699-
linearly spaced intervals (i.e the output signal has a
700-
frequency of ``new_freq``). It uses sinc/bandlimited
701-
interpolation to upsample/downsample the signal.
702-
703-
(almost directly from torchaudio.compliance.kaldi)
704-
705-
Arguments
706-
---------
707-
input_num_samp : int
708-
The number of samples in each example in the batch.
709-
710-
Returns
711-
-------
712-
Number of samples in the output waveform.
713-
"""
714-
715-
# For exact computation, we measure time in "ticks" of 1.0 / tick_freq,
716-
# where tick_freq is the least common multiple of samp_in and
717-
# samp_out.
718-
samp_in = int(self.orig_freq)
719-
samp_out = int(self.new_freq)
720-
721-
tick_freq = abs(samp_in * samp_out) // math.gcd(samp_in, samp_out)
722-
ticks_per_input_period = tick_freq // samp_in
723-
724-
# work out the number of ticks in the time interval
725-
# [ 0, input_num_samp/samp_in ).
726-
interval_length = input_num_samp * ticks_per_input_period
727-
if interval_length <= 0:
728-
return 0
729-
ticks_per_output_period = tick_freq // samp_out
730-
731-
# Get the last output-sample in the closed interval,
732-
# i.e. replacing [ ) with [ ]. Note: integer division rounds down.
733-
# See http://en.wikipedia.org/wiki/Interval_(mathematics) for an
734-
# explanation of the notation.
735-
last_output_samp = interval_length // ticks_per_output_period
736-
737-
# We need the last output-sample in the open interval, so if it
738-
# takes us to the end of the interval exactly, subtract one.
739-
if last_output_samp * ticks_per_output_period == interval_length:
740-
last_output_samp -= 1
741-
742-
# First output-sample index is zero, so the number of output samples
743-
# is the last output-sample plus one.
744-
num_output_samp = last_output_samp + 1
745-
746-
return num_output_samp
747-
748-
def _indices_and_weights(self, waveforms):
749-
"""Based on LinearResample::SetIndexesAndWeights
750-
751-
Retrieves the weights for resampling as well as the indices in which
752-
they are valid. LinearResample (LR) means that the output signal is at
753-
linearly spaced intervals (i.e the output signal has a frequency
754-
of ``new_freq``). It uses sinc/bandlimited interpolation to
755-
upsample/downsample the signal.
756-
757-
Returns
758-
-------
759-
- the place where each filter should start being applied
760-
- the filters to be applied to the signal for resampling
761-
"""
762-
763-
# Lowpass filter frequency depends on smaller of two frequencies
764-
min_freq = min(self.orig_freq, self.new_freq)
765-
lowpass_cutoff = 0.99 * 0.5 * min_freq
766-
767-
assert lowpass_cutoff * 2 <= min_freq
768-
window_width = self.lowpass_filter_width / (2.0 * lowpass_cutoff)
769-
770-
assert lowpass_cutoff < min(self.orig_freq, self.new_freq) / 2
771-
output_t = torch.arange(
772-
start=0.0, end=self.output_samples, device=waveforms.device
773-
)
774-
output_t /= self.new_freq
775-
min_t = output_t - window_width
776-
max_t = output_t + window_width
777-
778-
min_input_index = torch.ceil(min_t * self.orig_freq)
779-
max_input_index = torch.floor(max_t * self.orig_freq)
780-
num_indices = max_input_index - min_input_index + 1
781-
782-
max_weight_width = num_indices.max()
783-
j = torch.arange(max_weight_width, device=waveforms.device)
784-
input_index = min_input_index.unsqueeze(1) + j.unsqueeze(0)
785-
delta_t = (input_index / self.orig_freq) - output_t.unsqueeze(1)
786-
787-
weights = torch.zeros_like(delta_t)
788-
inside_window_indices = delta_t.abs().lt(window_width)
789-
790-
# raised-cosine (Hanning) window with width `window_width`
791-
weights[inside_window_indices] = 0.5 * (
792-
1
793-
+ torch.cos(
794-
2
795-
* math.pi
796-
* lowpass_cutoff
797-
/ self.lowpass_filter_width
798-
* delta_t[inside_window_indices]
799-
)
800-
)
801-
802-
t_eq_zero_indices = delta_t.eq(0.0)
803-
t_not_eq_zero_indices = ~t_eq_zero_indices
804-
805-
# sinc filter function
806-
weights[t_not_eq_zero_indices] *= torch.sin(
807-
2 * math.pi * lowpass_cutoff * delta_t[t_not_eq_zero_indices]
808-
) / (math.pi * delta_t[t_not_eq_zero_indices])
809-
810-
# limit of the function at t = 0
811-
weights[t_eq_zero_indices] *= 2 * lowpass_cutoff
812-
813-
# size (output_samples, max_weight_width)
814-
weights /= self.orig_freq
815-
816-
self.first_indices = min_input_index
817-
self.weights = weights
818-
819593

820594
class DropFreq(torch.nn.Module):
821595
"""This class drops a random frequency from the signal.

0 commit comments

Comments
 (0)