From 2da1975b2eaace4dcf1044a968bc645935c2ab74 Mon Sep 17 00:00:00 2001 From: takenori-y Date: Tue, 12 Jul 2022 14:28:08 +0900 Subject: [PATCH 01/23] add mglsadf --- diffsptk/core/__init__.py | 4 ++ diffsptk/core/imglsadf.py | 110 ++++++++++++++++++++++++++++++ diffsptk/core/mglsadf.py | 138 ++++++++++++++++++++++++++++++++++++++ docs/core/imglsadf.rst | 11 +++ docs/core/mglsadf.rst | 11 +++ tests/test_imglsadf.py | 79 ++++++++++++++++++++++ tests/test_mglsadf.py | 65 ++++++++++++++++++ 7 files changed, 418 insertions(+) create mode 100644 diffsptk/core/imglsadf.py create mode 100644 diffsptk/core/mglsadf.py create mode 100644 docs/core/imglsadf.rst create mode 100644 docs/core/mglsadf.rst create mode 100644 tests/test_imglsadf.py create mode 100644 tests/test_mglsadf.py diff --git a/diffsptk/core/__init__.py b/diffsptk/core/__init__.py index 2119a5b..83b323b 100644 --- a/diffsptk/core/__init__.py +++ b/diffsptk/core/__init__.py @@ -22,6 +22,8 @@ from .idct import InverseDiscreteCosineTransform from .idct import InverseDiscreteCosineTransform as IDCT from .ignorm import GeneralizedCepstrumInverseGainNormalization +from .imglsadf import PseudoInverseMGLSADigitalFilter +from .imglsadf import PseudoInverseMGLSADigitalFilter as IMLSA from .interpolate import Interpolation from .ipqmf import InversePseudoQuadratureMirrorFilterBanks from .ipqmf import InversePseudoQuadratureMirrorFilterBanks as IPQMF @@ -42,6 +44,8 @@ from .mgc2sp import MelGeneralizedCepstrumToSpectrum from .mgcep import MelGeneralizedCepstralAnalysis from .mgcep import MelGeneralizedCepstralAnalysis as MelCepstralAnalysis +from .mglsadf import PseudoMGLSADigitalFilter +from .mglsadf import PseudoMGLSADigitalFilter as MLSA from .mlpg import MaximumLikelihoodParameterGeneration from .mlpg import MaximumLikelihoodParameterGeneration as MLPG from .mpir2c import MinimumPhaseImpulseResponseToCepstrum diff --git a/diffsptk/core/imglsadf.py b/diffsptk/core/imglsadf.py new file mode 100644 index 0000000..c4708eb --- /dev/null +++ b/diffsptk/core/imglsadf.py @@ -0,0 +1,110 @@ +# ------------------------------------------------------------------------ # +# Copyright 2022 SPTK Working Group # +# # +# Licensed under the Apache License, Version 2.0 (the "License"); # +# you may not use this file except in compliance with the License. # +# You may obtain a copy of the License at # +# # +# http://www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software # +# distributed under the License is distributed on an "AS IS" BASIS, # +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # +# See the License for the specific language governing permissions and # +# limitations under the License. # +# ------------------------------------------------------------------------ # + +import torch.nn as nn + +from .mglsadf import PseudoMGLSADigitalFilter + + +class PseudoInverseMGLSADigitalFilter(nn.Module): + """See `this page `_ + for details. The exponential filter is approximated by the Taylor expansion. + + Parameters + ---------- + filter_order : int >= 0 [scalar] + Order of filter coefficients, :math:`M`. + + cep_order : int >= filter_order [scalar] + Order of linear cepstrum. + + alpha : float [-1 < alpha < 1] + Frequency warping factor, :math:`\\alpha`. + + gamma : float [-1 <= gamma <= 1] + Gamma, :math:`\\gamma`. + + c : int >= 1 [scalar] + Number of stages. + + taylor_order : int >= 0 [scalar] + Order of Taylor series expansion, :math:`L`. + + frame_period : int >= 1 [scalar] + Frame period, :math:`P`. + + ignore_gain : bool [scalar] + If True, perform filtering without gain. + + """ + + def __init__( + self, + filter_order, + cep_order=200, + alpha=0, + gamma=0, + c=None, + taylor_order=50, + frame_period=1, + ignore_gain=False, + ): + super(PseudoInverseMGLSADigitalFilter, self).__init__() + + self.mglsadf = PseudoMGLSADigitalFilter( + filter_order, + cep_order=cep_order, + alpha=alpha, + gamma=gamma, + c=c, + taylor_order=taylor_order, + frame_period=frame_period, + ignore_gain=ignore_gain, + inverse=True, + ) + + def forward(self, y, mc): + """Apply an inverse MGLSA digital filter. + + Parameters + ---------- + y : Tensor [shape=(..., T)] + Audio signal. + + mc : Tensor [shape=(..., T/P, M+1)] + Mel-generalized cepstrum, not MLSA digital filter coefficients. + + Returns + ------- + x : Tensor [shape=(..., T)] + Residual signal. + + Examples + -------- + >>> M = 4 + >>> y = diffsptk.step(3) + >>> mc = torch.randn(2, M + 1) + >>> mc + tensor([[ 0.8457, 1.5812, 0.1379, 1.6558, 1.4591], + [-1.3714, -0.9669, -1.2025, -1.3683, -0.2352]]) + >>> imglsadf = diffsptk.PseudoInverseMGLSADigitalFilter(M, frame_period=2) + >>> x = imglsadf(y.view(1, -1), mc.view(1, 2, M + 1)) + >>> x + tensor([[ 0.4293, 1.0592, 7.9349, 14.9794]]) + + """ + x = self.mglsadf(y, mc) + return x diff --git a/diffsptk/core/mglsadf.py b/diffsptk/core/mglsadf.py new file mode 100644 index 0000000..0865f8a --- /dev/null +++ b/diffsptk/core/mglsadf.py @@ -0,0 +1,138 @@ +# ------------------------------------------------------------------------ # +# Copyright 2022 SPTK Working Group # +# # +# Licensed under the Apache License, Version 2.0 (the "License"); # +# you may not use this file except in compliance with the License. # +# You may obtain a copy of the License at # +# # +# http://www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software # +# distributed under the License is distributed on an "AS IS" BASIS, # +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # +# See the License for the specific language governing permissions and # +# limitations under the License. # +# ------------------------------------------------------------------------ # + +import torch.nn as nn + +from ..misc.utils import check_size +from ..misc.utils import get_gamma +from .linear_intpl import LinearInterpolation +from .mgc2mgc import MelGeneralizedCepstrumToMelGeneralizedCepstrum + + +class PseudoMGLSADigitalFilter(nn.Module): + """See `this page `_ + for details. The exponential filter is approximated by the Taylor expansion. + + Parameters + ---------- + filter_order : int >= 0 [scalar] + Order of filter coefficients, :math:`M`. + + cep_order : int >= filter_order [scalar] + Order of linear cepstrum. + + alpha : float [-1 < alpha < 1] + Frequency warping factor, :math:`\\alpha`. + + gamma : float [-1 <= gamma <= 1] + Gamma, :math:`\\gamma`. + + c : int >= 1 [scalar] + Number of stages. + + taylor_order : int >= 0 [scalar] + Order of Taylor series expansion, :math:`L`. + + frame_period : int >= 1 [scalar] + Frame period, :math:`P`. + + ignore_gain : bool [scalar] + If True, perform filtering without gain. + + inverse : bool [scalar] + If True, perform inverse filtering. + + """ + + def __init__( + self, + filter_order, + cep_order=200, + alpha=0, + gamma=0, + c=None, + taylor_order=30, + frame_period=1, + ignore_gain=False, + inverse=False, + ): + super(PseudoMGLSADigitalFilter, self).__init__() + + self.filter_order = filter_order + self.taylor_order = taylor_order + self.frame_period = frame_period + self.ignore_gain = ignore_gain + self.inverse = inverse + + assert 0 <= self.taylor_order + + self.pad = nn.ConstantPad1d((cep_order, 0), 0) + self.mgc2c = MelGeneralizedCepstrumToMelGeneralizedCepstrum( + filter_order, + cep_order, + in_alpha=alpha, + in_gamma=get_gamma(gamma, c), + ) + self.linear_intpl = LinearInterpolation(frame_period) + + def forward(self, x, mc): + """Apply an MGLSA digital filter. + + Parameters + ---------- + x : Tensor [shape=(..., T)] + Excitation signal. + + mc : Tensor [shape=(..., T/P, M+1)] + Mel-generalized cepstrum, not MLSA digital filter coefficients. + + Returns + ------- + y : Tensor [shape=(..., T)] + Output signal. + + Examples + -------- + >>> M = 4 + >>> x = diffsptk.step(3) + >>> mc = torch.randn(2, M + 1) + >>> mc + tensor([[-0.9134, -0.5774, -0.4567, 0.7423, -0.5782], + [ 0.6904, 0.5175, 0.8765, 0.1677, 2.4624]]) + >>> mglsadf = diffsptk.PseudoMGLSADigitalFilter(M, frame_period=2) + >>> y = mglsadf(x.view(1, -1), mc.view(1, 2, M + 1)) + >>> y + tensor([[0.4011, 0.8760, 3.5677, 4.8725]]) + + """ + check_size(mc.size(-1), self.filter_order + 1, "dimension of mel-cepstrum") + check_size(x.size(-1), mc.size(-2) * self.frame_period, "sequence length") + + c = self.mgc2c(mc) + if self.ignore_gain: + c[..., 0] = 0 + c = self.linear_intpl(c.flip(-1)) + + y = x.clone() + for a in range(1, self.taylor_order + 1): + x = self.pad(x) + x = x.unfold(-1, c.size(-1), 1) + x = (x * c).sum(-1) / a + if self.inverse and a % 2 == 1: + y -= x + else: + y += x + return y diff --git a/docs/core/imglsadf.rst b/docs/core/imglsadf.rst new file mode 100644 index 0000000..5c748e3 --- /dev/null +++ b/docs/core/imglsadf.rst @@ -0,0 +1,11 @@ +.. _imglsadf: + +imglsadf +-------- + +.. autoclass:: diffsptk.IMLSA + +.. autoclass:: diffsptk.PseudoInverseMGLSADigitalFilter + :members: + +.. seealso:: :ref:`mgcep` :ref:`mglsadf` diff --git a/docs/core/mglsadf.rst b/docs/core/mglsadf.rst new file mode 100644 index 0000000..35edcc4 --- /dev/null +++ b/docs/core/mglsadf.rst @@ -0,0 +1,11 @@ +.. _mglsadf: + +mglsadf +------- + +.. autoclass:: diffsptk.MLSA + +.. autoclass:: diffsptk.PseudoMGLSADigitalFilter + :members: + +.. seealso:: :ref:`mgcep` :ref:`imglsadf` diff --git a/tests/test_imglsadf.py b/tests/test_imglsadf.py new file mode 100644 index 0000000..683fbe8 --- /dev/null +++ b/tests/test_imglsadf.py @@ -0,0 +1,79 @@ +# ------------------------------------------------------------------------ # +# Copyright 2022 SPTK Working Group # +# # +# Licensed under the Apache License, Version 2.0 (the "License"); # +# you may not use this file except in compliance with the License. # +# You may obtain a copy of the License at # +# # +# http://www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software # +# distributed under the License is distributed on an "AS IS" BASIS, # +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # +# See the License for the specific language governing permissions and # +# limitations under the License. # +# ------------------------------------------------------------------------ # + +import pytest +import torch + +import diffsptk +import tests.utils as U + + +@pytest.mark.parametrize("device", ["cpu", "cuda"]) +@pytest.mark.parametrize("ignore_gain", [False, True]) +def test_compatibility(device, ignore_gain, c=0, alpha=0.42, M=24, P=80): + if device == "cuda" and not torch.cuda.is_available(): + return + + # Prepare data for C++. + tmp1 = "mglsadf.tmp1" + tmp2 = "mglsadf.tmp2" + U.call(f"x2x +sd tools/SPTK/asset/data.short > {tmp1}", get=False) + cmd = ( + f"x2x +sd tools/SPTK/asset/data.short | " + f"frame -p {P} -l 400 | " + f"window -w 1 -n 1 -l 400 -L 512 | " + f"mgcep -c {c} -a {alpha} -m {M} -l 512 > {tmp2}" + ) + U.call(f"{cmd}", get=False) + + # Prepare data for C++. + y = torch.from_numpy(U.call(f"cat {tmp1}")).to(device) + mc = torch.from_numpy(U.call(f"cat {tmp2}").reshape(-1, M + 1)).to(device) + U.call(f"rm {tmp1} {tmp2}", get=False) + + # Get residual signal. + imglsadf = diffsptk.IMLSA( + M, + cep_order=100, + taylor_order=50, + frame_period=P, + alpha=alpha, + c=c, + ignore_gain=ignore_gain, + ).to(device) + x = imglsadf(y, mc) + + # Get reconstructed signal. + mglsadf = diffsptk.MLSA( + M, + cep_order=100, + taylor_order=30, + frame_period=P, + alpha=alpha, + c=c, + ignore_gain=ignore_gain, + ).to(device) + y_hat = mglsadf(x, mc) + + # Compute error between two signals. + error = torch.max(torch.abs(y - y_hat)) + assert torch.lt(error, 1) + + +@pytest.mark.parametrize("device", ["cpu", "cuda"]) +def test_differentiable(device, B=4, T=20, M=4): + imglsadf = diffsptk.IMLSA(M, taylor_order=5) + U.check_differentiable(device, imglsadf, [(B, T), (B, T, M + 1)]) diff --git a/tests/test_mglsadf.py b/tests/test_mglsadf.py new file mode 100644 index 0000000..3be6531 --- /dev/null +++ b/tests/test_mglsadf.py @@ -0,0 +1,65 @@ +# ------------------------------------------------------------------------ # +# Copyright 2022 SPTK Working Group # +# # +# Licensed under the Apache License, Version 2.0 (the "License"); # +# you may not use this file except in compliance with the License. # +# You may obtain a copy of the License at # +# # +# http://www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software # +# distributed under the License is distributed on an "AS IS" BASIS, # +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # +# See the License for the specific language governing permissions and # +# limitations under the License. # +# ------------------------------------------------------------------------ # + +import os + +import numpy as np +import pytest + +import diffsptk +import tests.utils as U + + +@pytest.mark.parametrize("device", ["cpu", "cuda"]) +@pytest.mark.parametrize("ignore_gain", [False, True]) +@pytest.mark.parametrize("c", [0, 10]) +def test_compatibility(device, ignore_gain, c, alpha=0.42, M=24, P=80): + mglsadf = diffsptk.MLSA( + M, + cep_order=100, + taylor_order=20, + frame_period=P, + alpha=alpha, + c=c, + ignore_gain=ignore_gain, + ) + + tmp1 = "mglsadf.tmp1" + tmp2 = "mglsadf.tmp2" + opt = "-k" if ignore_gain else "" + cmd = ( + f"x2x +sd tools/SPTK/asset/data.short | " + f"frame -p {P} -l 400 | " + f"window -w 1 -n 1 -l 400 -L 512 | " + f"mgcep -c {c} -a {alpha} -m {M} -l 512 > {tmp2}" + ) + T = os.path.getsize("tools/SPTK/asset/data.short") // 2 + U.check_compatibility( + device, + mglsadf, + [f"nrand -l {T} > {tmp1}", cmd], + [f"cat {tmp1}", f"cat {tmp2}"], + f"mglsadf {tmp2} < {tmp1} -i 1 -m {M} -p {P} -c {c} -a {alpha} {opt}", + [f"rm {tmp1} {tmp2}"], + dx=[None, M + 1], + eq=lambda a, b: np.max(np.abs(a - b)) < 1000, + ) + + +@pytest.mark.parametrize("device", ["cpu", "cuda"]) +def test_differentiable(device, B=4, T=20, M=4): + mglsadf = diffsptk.MLSA(M, taylor_order=5) + U.check_differentiable(device, mglsadf, [(B, T), (B, T, M + 1)]) From ee0e47684f805a1548920074735b744f170333a4 Mon Sep 17 00:00:00 2001 From: takenori-y Date: Tue, 12 Jul 2022 14:32:20 +0900 Subject: [PATCH 02/23] add example wav --- README.md | 197 ++++++++++++------------------------------------ assets/data.wav | Bin 0 -> 38444 bytes setup.py | 6 +- 3 files changed, 53 insertions(+), 150 deletions(-) create mode 100644 assets/data.wav diff --git a/README.md b/README.md index 1edeb99..4397328 100644 --- a/README.md +++ b/README.md @@ -38,50 +38,83 @@ pip install -e diffsptk Examples -------- -### Mel-cepstral analysis +### Mel-cepstral analysis and synthesis ```python import diffsptk +import soundfile as sf import torch -# Generate waveform. -x = torch.randn(100) +# Set analysis condition. +fl = 400 +fp = 80 +n_fft = 512 +M = 24 +alpha = 0.42 + +# Read waveform. +x, sr = sf.read("assets/data.wav") +x = torch.FloatTensor(x) # Compute STFT of x. -stft = diffsptk.STFT(frame_length=12, frame_period=10, fft_length=16) +stft = diffsptk.STFT(frame_length=fl, frame_period=fp, fft_length=n_fft) X = stft(x) -# Estimate 4-th order mel-cepstrum of x. -mcep = diffsptk.MelCepstralAnalysis(cep_order=4, fft_length=16, alpha=0.1, n_iter=1) +# Estimate mel-cepstrum of x. +mcep = diffsptk.MelCepstralAnalysis(cep_order=M, fft_length=n_fft, alpha=alpha) mc = mcep(X) + +# Generate excitation. +e = torch.randn(x.size(0)) + +# Apply MLSA filter to the excitation. +mlsa = diffsptk.MLSA(filter_order=M, alpha=alpha, frame_period=fp) +y = mlsa(e, mc) + +# Write waveform. +sf.write("unvoice.wav", y.cpu().numpy(), sr) ``` ### Mel-spectrogram extraction ```python import diffsptk +import soundfile as sf import torch -# Generate waveform. -x = torch.randn(100) +# Set analysis condition. +fl = 400 +fp = 80 +n_fft = 512 +n_channel = 80 + +# Read waveform. +x, sr = sf.read("assets/data.wav") +x = torch.FloatTensor(x) # Compute STFT of x. -stft = diffsptk.STFT(frame_length=12, frame_period=10, fft_length=32) +stft = diffsptk.STFT(frame_length=fl, frame_period=fp, fft_length=n_fft) X = stft(x) -# Apply 4 mel-filter banks to the STFT. -fbank = diffsptk.MelFilterBankAnalysis(n_channel=4, fft_length=32, sample_rate=8000, floor=1e-1) +# Apply mel-filter banks to the STFT. +fbank = diffsptk.MelFilterBankAnalysis( + n_channel=n_channel, + fft_length=n_fft, + sample_rate=sr, +) Y = fbank(X) ``` ### Subband decomposition ```python import diffsptk +import soundfile as sf import torch K = 4 # Number of subbands. M = 40 # Order of filter. -# Generate waveform. -x = torch.randn(100) +# Read waveform. +x, sr = sf.read("assets/data.wav") +x = torch.FloatTensor(x) # Decompose x. pqmf = diffsptk.PQMF(K, M) @@ -91,144 +124,14 @@ y = decimate(pqmf(x), dim=-1) # Reconstruct x. interpolate = diffsptk.Interpolation(K) ipqmf = diffsptk.IPQMF(K, M) -x_hat = ipqmf(interpolate(K * y, dim=-1)) +x_hat = ipqmf(interpolate(K * y, dim=-1)).reshape(-1) # Compute error between two signals. error = torch.abs(x_hat - x).sum() -``` - -Status ------- -~~module~~ will not be implemented in this repository. -- [x] acorr -- [ ] ~~acr2csm~~ -- [ ] ~~aeq~~ (*torch.allclose*) -- [ ] ~~amgcep~~ -- [ ] ~~average~~ (*torch.mean*) -- [x] b2mc -- [ ] ~~bcp~~ (*torch.split*) -- [ ] ~~bcut~~ -- [x] c2acr -- [x] c2mpir -- [x] c2ndps -- [x] cdist -- [ ] ~~clip~~ (*torch.clip*) -- [ ] ~~csm2acr~~ -- [x] dct -- [x] decimate -- [x] delay -- [x] delta -- [x] dequantize -- [x] df2 -- [x] dfs -- [ ] ~~dmp~~ -- [ ] ~~dtw~~ -- [ ] ~~dtw_merge~~ -- [ ] ~~entropy~~ (*torch.special.entr*) -- [ ] ~~excite~~ -- [ ] ~~extract~~ -- [x] fbank -- [ ] ~~fd~~ -- [ ] ~~fdrw~~ -- [ ] ~~fft~~ (*torch.fft.fft*) -- [ ] ~~fft2~~ (*torch.fft.fft2*) -- [x] fftcep -- [ ] ~~fftr~~ (*torch.fft.rfft*) -- [ ] ~~fftr2~~ (*torch.fft.rfft2*) -- [x] frame -- [x] freqt -- [ ] ~~glogsp~~ -- [ ] ~~gmm~~ -- [ ] ~~gmmp~~ -- [x] gnorm -- [ ] ~~gpolezero~~ -- [ ] ~~grlogsp~~ -- [x] grpdelay -- [ ] ~~gseries~~ -- [ ] ~~gspecgram~~ -- [ ] ~~gwave~~ -- [ ] ~~histogram~~ (*torch.histogram*) -- [ ] ~~huffman~~ -- [ ] ~~huffman_decode~~ -- [ ] ~~huffman_encode~~ -- [x] idct -- [ ] ~~ifft~~ (*torch.fft.ifft*) -- [ ] ~~ifft2~~ (*torch.fft.ifft2*) -- [x] ignorm -- [ ] imglsadf (*will be appeared*) -- [x] impulse -- [x] imsvq -- [x] interpolate -- [x] ipqmf -- [x] iulaw -- [x] lar2par -- [ ] ~~lbg~~ -- [x] levdur -- [x] linear_intpl -- [x] lpc -- [ ] ~~lpc2c~~ -- [ ] ~~lpc2lsp~~ -- [x] lpc2par -- [x] lpccheck -- [ ] ~~lsp2lpc~~ -- [ ] ~~lspcheck~~ -- [ ] ~~lspdf~~ -- [ ] ~~ltcdf~~ -- [x] mc2b -- [x] mcpf -- [ ] ~~median~~ (*torch.median*) -- [ ] ~~merge~~ (*torch.cat*) -- [x] mfcc -- [x] mgc2mgc -- [x] mgc2sp -- [x] mgcep -- [ ] mglsadf (*will be appeared*) -- [ ] ~~mglsp2sp~~ -- [ ] ~~minmax~~ -- [x] mlpg (*support only unit variance*) -- [ ] ~~mlsacheck~~ -- [x] mpir2c -- [ ] ~~mseq~~ -- [ ] ~~msvq~~ -- [ ] ~~nan~~ (*torch.isnan*) -- [x] ndps2c -- [x] norm0 -- [ ] ~~nrand~~ (*torch.randn*) -- [x] par2lar -- [x] par2lpc -- [x] pca -- [ ] ~~pcas~~ -- [x] phase -- [x] pitch -- [ ] ~~pitch_mark~~ -- [ ] ~~poledf~~ -- [x] pqmf -- [x] quantize -- [x] ramp -- [ ] ~~reverse~~ (*torch.flip*) -- [ ] ~~rlevdur~~ -- [x] rmse -- [ ] ~~root_pol~~ -- [x] sin -- [x] smcep -- [x] snr -- [x] sopr -- [x] spec -- [x] step -- [ ] ~~swab~~ -- [ ] ~~symmetrize~~ -- [ ] ~~train~~ -- [ ] ~~transpose~~ (*torch.transpose*) -- [x] ulaw -- [ ] ~~vc~~ -- [ ] ~~vopr~~ -- [ ] ~~vstat~~ (*torch.var_mean*) -- [ ] ~~vsum~~ (*torch.sum*) -- [x] window -- [ ] ~~x2x~~ -- [x] zcross -- [x] zerodf +# Write reconstructed waveform. +sf.write("reconst.wav", x_hat.cpu().numpy(), sr) +``` License diff --git a/assets/data.wav b/assets/data.wav new file mode 100644 index 0000000000000000000000000000000000000000..bf6ec2ccc07ae8c528790d2ab58a10a7999c097f GIT binary patch literal 38444 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 l5H&mjs;@V*MF) Date: Tue, 12 Jul 2022 14:37:59 +0900 Subject: [PATCH 03/23] update version --- README.md | 2 +- diffsptk/version.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4397328..5c9dd55 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ diffsptk *diffsptk* is a differentiable version of [SPTK](https://github.com/sp-nitech/SPTK) based on the PyTorch framework. [![Latest Manual](https://img.shields.io/badge/docs-latest-blue.svg)](https://sp-nitech.github.io/diffsptk/latest/) -[![Stable Manual](https://img.shields.io/badge/docs-stable-blue.svg)](https://sp-nitech.github.io/diffsptk/0.4.0/) +[![Stable Manual](https://img.shields.io/badge/docs-stable-blue.svg)](https://sp-nitech.github.io/diffsptk/1.0.0/) [![Python Version](https://img.shields.io/pypi/pyversions/diffsptk.svg)](https://pypi.python.org/pypi/diffsptk) [![PyTorch Version](https://img.shields.io/badge/pytorch-1.10.0%20%7C%201.12.0-orange.svg)](https://pypi.python.org/pypi/diffsptk) [![PyPI Version](https://img.shields.io/pypi/v/diffsptk.svg)](https://pypi.python.org/pypi/diffsptk) diff --git a/diffsptk/version.py b/diffsptk/version.py index 6a9beea..5becc17 100644 --- a/diffsptk/version.py +++ b/diffsptk/version.py @@ -1 +1 @@ -__version__ = "0.4.0" +__version__ = "1.0.0" From 51d128a0ebe28d58427976e78dc8f20e47bb0098 Mon Sep 17 00:00:00 2001 From: takenori-y Date: Tue, 19 Jul 2022 16:44:29 +0900 Subject: [PATCH 04/23] update test --- tests/test_mglsadf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_mglsadf.py b/tests/test_mglsadf.py index 3be6531..f01e2a9 100644 --- a/tests/test_mglsadf.py +++ b/tests/test_mglsadf.py @@ -44,7 +44,7 @@ def test_compatibility(device, ignore_gain, c, alpha=0.42, M=24, P=80): f"x2x +sd tools/SPTK/asset/data.short | " f"frame -p {P} -l 400 | " f"window -w 1 -n 1 -l 400 -L 512 | " - f"mgcep -c {c} -a {alpha} -m {M} -l 512 > {tmp2}" + f"mgcep -c {c} -a {alpha} -m {M} -l 512 -E -60 > {tmp2}" ) T = os.path.getsize("tools/SPTK/asset/data.short") // 2 U.check_compatibility( @@ -55,7 +55,7 @@ def test_compatibility(device, ignore_gain, c, alpha=0.42, M=24, P=80): f"mglsadf {tmp2} < {tmp1} -i 1 -m {M} -p {P} -c {c} -a {alpha} {opt}", [f"rm {tmp1} {tmp2}"], dx=[None, M + 1], - eq=lambda a, b: np.max(np.abs(a - b)) < 1000, + eq=lambda a, b: np.corrcoef(a, b)[0, 1] > 0.99, ) From 1c77c36a6f6e960dbd32e24b0d2d3d0296c0e5ea Mon Sep 17 00:00:00 2001 From: takenori-y Date: Tue, 19 Jul 2022 21:45:09 +0900 Subject: [PATCH 05/23] update c2mpir --- diffsptk/core/c2mpir.py | 30 ++++++++++++++---------------- tests/test_c2mpir.py | 4 ++-- 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/diffsptk/core/c2mpir.py b/diffsptk/core/c2mpir.py index 41c783d..8b42f97 100644 --- a/diffsptk/core/c2mpir.py +++ b/diffsptk/core/c2mpir.py @@ -22,7 +22,7 @@ class CepstrumToMinimumPhaseImpulseResponse(nn.Module): """See `this page `_ - for details. This module may be slow due to recursive computation. + for details. Parameters ---------- @@ -32,18 +32,21 @@ class CepstrumToMinimumPhaseImpulseResponse(nn.Module): impulse_response_length : int >= 1 [scalar] Length of impulse response, :math:`N`. + fft_length : int >= 2 [scalar] + Number of FFT bins, :math:`L`. + """ - def __init__(self, cep_order, impulse_response_length): + def __init__(self, cep_order, impulse_response_length, fft_length): super(CepstrumToMinimumPhaseImpulseResponse, self).__init__() self.cep_order = cep_order self.impulse_response_length = impulse_response_length + self.fft_length = fft_length assert 0 <= self.cep_order assert 1 <= self.impulse_response_length - - self.register_buffer("ramp", torch.arange(1, self.cep_order + 1)) + assert self.impulse_response_length * 2 <= self.fft_length def forward(self, c): """Convert cepstrum to impulse response. @@ -69,16 +72,11 @@ def forward(self, c): """ check_size(c.size(-1), self.cep_order + 1, "dimension of cepstrum") - c0 = c[..., 0] - c1 = (c[..., 1:] * self.ramp).flip(-1) - - h = torch.empty( - (*(c.shape[:-1]), self.impulse_response_length), device=c.device - ) - h[..., 0] = torch.exp(c0) - for n in range(1, self.impulse_response_length): - s = n - self.cep_order - h[..., n] = (h[..., max(0, s) : n].clone() * c1[..., max(0, -s) :]).sum( - -1 - ) / n + C = torch.fft.fft(c, n=self.fft_length) + # Compute complex exponential. + r = torch.exp(C.real) + real = r * torch.cos(C.imag) + imag = r * torch.sin(C.imag) + S = torch.complex(real, imag) + h = torch.fft.ifft(S)[..., : self.impulse_response_length].real return h diff --git a/tests/test_c2mpir.py b/tests/test_c2mpir.py index 3b7d373..b731630 100644 --- a/tests/test_c2mpir.py +++ b/tests/test_c2mpir.py @@ -21,8 +21,8 @@ @pytest.mark.parametrize("device", ["cpu", "cuda"]) -def test_compatibility(device, M=19, N=30, B=2): - c2mpir = diffsptk.CepstrumToMinimumPhaseImpulseResponse(M, N) +def test_compatibility(device, M=19, N=30, L=512, B=2): + c2mpir = diffsptk.CepstrumToMinimumPhaseImpulseResponse(M, N, L) U.check_compatibility( device, From e97b772f0ff23592fcfbe0b62f48fb4c420ed47a Mon Sep 17 00:00:00 2001 From: takenori-y Date: Thu, 21 Jul 2022 21:11:19 +0900 Subject: [PATCH 06/23] update mpir2c --- diffsptk/core/c2mpir.py | 10 +++++----- diffsptk/core/mpir2c.py | 38 +++++++++++++++++++++++--------------- tests/test_mpir2c.py | 6 +++--- 3 files changed, 31 insertions(+), 23 deletions(-) diff --git a/diffsptk/core/c2mpir.py b/diffsptk/core/c2mpir.py index 8b42f97..ca4eb26 100644 --- a/diffsptk/core/c2mpir.py +++ b/diffsptk/core/c2mpir.py @@ -32,7 +32,7 @@ class CepstrumToMinimumPhaseImpulseResponse(nn.Module): impulse_response_length : int >= 1 [scalar] Length of impulse response, :math:`N`. - fft_length : int >= 2 [scalar] + fft_length : int >> :math:`M` [scalar] Number of FFT bins, :math:`L`. """ @@ -46,10 +46,10 @@ def __init__(self, cep_order, impulse_response_length, fft_length): assert 0 <= self.cep_order assert 1 <= self.impulse_response_length - assert self.impulse_response_length * 2 <= self.fft_length + assert self.impulse_response_length <= self.fft_length // 2 def forward(self, c): - """Convert cepstrum to impulse response. + """Convert cepstrum to minimum phase impulse response. Parameters ---------- @@ -59,12 +59,12 @@ def forward(self, c): Returns ------- h : Tensor [shape=(..., N)] - Truncated impulse response. + Truncated minimum phase impulse response. Examples -------- >>> c = diffsptk.ramp(3) - >>> c2mpir = diffsptk.CepstrumToMinimumPhaseImpulseResponse(3, 5) + >>> c2mpir = diffsptk.CepstrumToMinimumPhaseImpulseResponse(3, 5, 64) >>> h = c2mpir(c) >>> h tensor([1.0000, 1.0000, 2.5000, 5.1667, 6.0417]) diff --git a/diffsptk/core/mpir2c.py b/diffsptk/core/mpir2c.py index 2042f44..c9ba155 100644 --- a/diffsptk/core/mpir2c.py +++ b/diffsptk/core/mpir2c.py @@ -14,14 +14,15 @@ # limitations under the License. # # ------------------------------------------------------------------------ # +import torch import torch.nn as nn -from .mgc2mgc import MelGeneralizedCepstrumToMelGeneralizedCepstrum +from ..misc.utils import check_size class MinimumPhaseImpulseResponseToCepstrum(nn.Module): """See `this page `_ - for details. This module may be slow due to recursive computation. + for details. Parameters ---------- @@ -31,27 +32,29 @@ class MinimumPhaseImpulseResponseToCepstrum(nn.Module): impulse_response_length : int >= 1 [scalar] Length of impulse response, :math:`N`. + fft_length : int >> :math:`M` [scalar] + Number of FFT bins, :math:`L`. + """ - def __init__(self, cep_order, impulse_response_length): + def __init__(self, cep_order, impulse_response_length, fft_length): super(MinimumPhaseImpulseResponseToCepstrum, self).__init__() - self.ir2c = MelGeneralizedCepstrumToMelGeneralizedCepstrum( - impulse_response_length - 1, - cep_order, - in_gamma=1, - out_gamma=0, - in_mul=True, - out_mul=False, - ) + self.cep_order = cep_order + self.impulse_response_length = impulse_response_length + self.fft_length = fft_length + + assert 0 <= self.cep_order + assert 1 <= self.impulse_response_length + assert self.cep_order + 1 < self.fft_length // 2 def forward(self, h): - """Convert impulse response to cepstrum. + """Convert minimum phase impulse response to cepstrum. Parameters ---------- h : Tensor [shape=(..., N)] - Truncated impulse response. + Truncated minimum phase impulse response. Returns ------- @@ -61,11 +64,16 @@ def forward(self, h): Examples -------- >>> h = diffsptk.ramp(4, 0, -1) - >>> mpir2c = diffsptk.MinimumPhaseImpulseResponseToCepstrum(3, 5) + >>> mpir2c = diffsptk.MinimumPhaseImpulseResponseToCepstrum(3, 5, 64) >>> c = mpir2c(h) >>> c tensor([1.3863, 0.7500, 0.2188, 0.0156]) """ - c = self.ir2c(h) + check_size(h.size(-1), self.impulse_response_length, "impulse response length") + + H = torch.fft.fft(h, n=self.fft_length) + C = torch.log(H.abs()) + c = torch.fft.ifft(C)[..., : self.cep_order + 1].real + c[..., 1:] *= 2 return c diff --git a/tests/test_mpir2c.py b/tests/test_mpir2c.py index 360916b..bd93b46 100644 --- a/tests/test_mpir2c.py +++ b/tests/test_mpir2c.py @@ -22,14 +22,14 @@ @pytest.mark.parametrize("device", ["cpu", "cuda"]) -def test_compatibility(device, M=19, N=30, B=2): - mpir2c = diffsptk.MinimumPhaseImpulseResponseToCepstrum(M, N) +def test_compatibility(device, M=19, N=30, L=256, B=2): + mpir2c = diffsptk.MinimumPhaseImpulseResponseToCepstrum(M, N, L) U.check_compatibility( device, mpir2c, [], - f"nrand -l {B*N} | sopr -ABS", + f"nrand -l {B*L} | fftcep -l {L} -m {M} | c2mpir -m {M} -l {N}", f"mpir2c -M {M} -l {N}", [], dx=N, From 53a86fe88232cf13adbef2c534a76b5cb69bac38 Mon Sep 17 00:00:00 2001 From: takenori-y Date: Sun, 24 Jul 2022 22:09:11 +0900 Subject: [PATCH 07/23] update mgc2mgc --- diffsptk/core/c2mpir.py | 12 +++------ diffsptk/core/mgc2mgc.py | 53 ++++++++++++++++++++++++---------------- diffsptk/core/mpir2c.py | 8 +++--- diffsptk/misc/utils.py | 8 ++++++ tests/test_mgc2mgc.py | 23 +++++++++++------ 5 files changed, 63 insertions(+), 41 deletions(-) diff --git a/diffsptk/core/c2mpir.py b/diffsptk/core/c2mpir.py index ca4eb26..9175b84 100644 --- a/diffsptk/core/c2mpir.py +++ b/diffsptk/core/c2mpir.py @@ -17,6 +17,7 @@ import torch import torch.nn as nn +from ..misc.utils import cexp from ..misc.utils import check_size @@ -37,7 +38,7 @@ class CepstrumToMinimumPhaseImpulseResponse(nn.Module): """ - def __init__(self, cep_order, impulse_response_length, fft_length): + def __init__(self, cep_order, impulse_response_length, fft_length=512): super(CepstrumToMinimumPhaseImpulseResponse, self).__init__() self.cep_order = cep_order @@ -64,7 +65,7 @@ def forward(self, c): Examples -------- >>> c = diffsptk.ramp(3) - >>> c2mpir = diffsptk.CepstrumToMinimumPhaseImpulseResponse(3, 5, 64) + >>> c2mpir = diffsptk.CepstrumToMinimumPhaseImpulseResponse(3, 5) >>> h = c2mpir(c) >>> h tensor([1.0000, 1.0000, 2.5000, 5.1667, 6.0417]) @@ -73,10 +74,5 @@ def forward(self, c): check_size(c.size(-1), self.cep_order + 1, "dimension of cepstrum") C = torch.fft.fft(c, n=self.fft_length) - # Compute complex exponential. - r = torch.exp(C.real) - real = r * torch.cos(C.imag) - imag = r * torch.sin(C.imag) - S = torch.complex(real, imag) - h = torch.fft.ifft(S)[..., : self.impulse_response_length].real + h = torch.fft.ifft(cexp(C))[..., : self.impulse_response_length].real return h diff --git a/diffsptk/core/mgc2mgc.py b/diffsptk/core/mgc2mgc.py index dd13e51..b4fa921 100644 --- a/diffsptk/core/mgc2mgc.py +++ b/diffsptk/core/mgc2mgc.py @@ -18,6 +18,8 @@ import torch import torch.nn as nn +from ..misc.utils import cexp +from ..misc.utils import clog from ..misc.utils import default_dtype from .freqt import FrequencyTransform from .gnorm import GeneralizedCepstrumGainNormalization as GainNormalization @@ -27,37 +29,43 @@ class GeneralizedCepstrumToGeneralizedCepstrum(nn.Module): - def __init__(self, in_order, out_order, in_gamma, out_gamma): + def __init__(self, in_order, out_order, in_gamma, out_gamma, fft_length): super(GeneralizedCepstrumToGeneralizedCepstrum, self).__init__() self.in_order = in_order self.out_order = out_order self.in_gamma = in_gamma self.out_gamma = out_gamma + self.fft_length = fft_length assert 0 <= self.in_order assert 0 <= self.out_order assert abs(self.in_gamma) <= 1 assert abs(self.out_gamma) <= 1 - - ramp = np.arange(self.in_order + 1, dtype=default_dtype()) - self.register_buffer("k", torch.from_numpy(ramp)) + assert max(self.in_order, self.out_order) + 1 < fft_length def forward(self, c1): - c2 = torch.zeros((*(c1.shape[:-1]), self.out_order + 1), device=c1.device) - - copy_length = min(1, min(self.in_order, self.out_order)) + 1 - c2[..., :copy_length] = c1[..., :copy_length] - for m in range(2, self.out_order + 1): - n = max(1, m - self.in_order) - cc = c1[..., 1:m] * c2[..., n:m].flip(-1) - s2 = (self.k[1:m] * cc).sum(-1) / m - s1 = cc.sum(-1) - s2 - ss = self.out_gamma * s2 - self.in_gamma * s1 - if m <= self.in_order: - c2[..., m] = ss + c1[..., m] - else: - c2[..., m] = ss + c01 = torch.cat((c1[..., :1] * 0, c1[..., 1:]), dim=-1) + C1 = torch.fft.fft(c01, n=self.fft_length) + + if self.in_gamma == 0: + sC1 = cexp(C1) + else: + C1 *= self.in_gamma + C1.real += 1 + r = C1.abs() ** (1 / self.in_gamma) + theta = C1.angle() / self.in_gamma + sC1 = torch.polar(r, theta) + + if self.out_gamma == 0: + C2 = clog(sC1) + else: + r = sC1.abs() ** self.out_gamma + theta = sC1.angle() * self.out_gamma + C2 = (r * torch.cos(theta) - 1) / self.out_gamma + + c02 = torch.fft.ifft(C2)[..., : self.out_order + 1].real + c2 = torch.cat((c1[..., :1], 2 * c02[..., 1:]), dim=-1) return c2 @@ -143,6 +151,9 @@ class MelGeneralizedCepstrumToMelGeneralizedCepstrum(nn.Module): out_mul : bool [scalar] If True, assume gamma-multiplied output. + fft_length : int [scalar] + Number of FFT bins, :math:`L`. + """ def __init__( @@ -157,6 +168,7 @@ def __init__( out_norm=False, in_mul=False, out_mul=False, + fft_length=512, ): super(MelGeneralizedCepstrumToMelGeneralizedCepstrum, self).__init__() @@ -185,7 +197,7 @@ def __init__( if True: modules.append( GeneralizedCepstrumToGeneralizedCepstrum( - in_order, out_order, in_gamma, out_gamma + in_order, out_order, in_gamma, out_gamma, fft_length ) ) if not out_norm: @@ -204,7 +216,7 @@ def __init__( if in_gamma != out_gamma: modules.append( GeneralizedCepstrumToGeneralizedCepstrum( - out_order, out_order, in_gamma, out_gamma + out_order, out_order, in_gamma, out_gamma, fft_length ) ) if not out_norm and in_gamma != out_gamma: @@ -233,7 +245,6 @@ def forward(self, c1): Examples -------- >>> c1 = diffsptk.ramp(3) - tensor([0., 1., 2., 3.]) >>> mgc2mgc = diffsptk.MelGeneralizedCepstrumToMelGeneralizedCepstrum(3, 4, 0.1) >>> c2 = mgc2mgc(c1) >>> c2 diff --git a/diffsptk/core/mpir2c.py b/diffsptk/core/mpir2c.py index c9ba155..dad2ca2 100644 --- a/diffsptk/core/mpir2c.py +++ b/diffsptk/core/mpir2c.py @@ -18,6 +18,7 @@ import torch.nn as nn from ..misc.utils import check_size +from ..misc.utils import clog class MinimumPhaseImpulseResponseToCepstrum(nn.Module): @@ -37,7 +38,7 @@ class MinimumPhaseImpulseResponseToCepstrum(nn.Module): """ - def __init__(self, cep_order, impulse_response_length, fft_length): + def __init__(self, cep_order, impulse_response_length, fft_length=512): super(MinimumPhaseImpulseResponseToCepstrum, self).__init__() self.cep_order = cep_order @@ -64,7 +65,7 @@ def forward(self, h): Examples -------- >>> h = diffsptk.ramp(4, 0, -1) - >>> mpir2c = diffsptk.MinimumPhaseImpulseResponseToCepstrum(3, 5, 64) + >>> mpir2c = diffsptk.MinimumPhaseImpulseResponseToCepstrum(3, 5) >>> c = mpir2c(h) >>> c tensor([1.3863, 0.7500, 0.2188, 0.0156]) @@ -73,7 +74,6 @@ def forward(self, h): check_size(h.size(-1), self.impulse_response_length, "impulse response length") H = torch.fft.fft(h, n=self.fft_length) - C = torch.log(H.abs()) - c = torch.fft.ifft(C)[..., : self.cep_order + 1].real + c = torch.fft.ifft(clog(H))[..., : self.cep_order + 1].real c[..., 1:] *= 2 return c diff --git a/diffsptk/misc/utils.py b/diffsptk/misc/utils.py index 0ea49db..9c420c5 100644 --- a/diffsptk/misc/utils.py +++ b/diffsptk/misc/utils.py @@ -65,5 +65,13 @@ def hankel(x): return X +def cexp(x): + return torch.polar(torch.exp(x.real), x.imag) + + +def clog(x): + return torch.log(x.abs()) + + def check_size(x, y, cause): assert x == y, f"Unexpected {cause} (input {x} vs target {y})" diff --git a/tests/test_mgc2mgc.py b/tests/test_mgc2mgc.py index 82a0d3a..9ae50cb 100644 --- a/tests/test_mgc2mgc.py +++ b/tests/test_mgc2mgc.py @@ -30,7 +30,7 @@ "M, A, G", [[4, 0, 0.1], [4, 0, 0.2], [2, 0.1, 0.1], [6, 0.1, 0.2]] ) def test_compatibility( - device, in_norm, out_norm, in_mul, out_mul, M, A, G, m=4, a=0, g=0.1, B=2 + device, in_norm, out_norm, in_mul, out_mul, M, A, G, m=4, a=0, g=0.1, L=256, B=2 ): mgc2mgc = diffsptk.MelGeneralizedCepstrumToMelGeneralizedCepstrum( in_order=m, @@ -43,24 +43,31 @@ def test_compatibility( out_norm=out_norm, in_mul=in_mul, out_mul=out_mul, + fft_length=L, ) - opt = f"-m {m} -M {M} -a {a} -A {A} -g {g} -G {G} " + opt1 = f"-m {m} -M {m} -a 0 -A {a} -g {0} -G {g} " if in_norm: - opt += "-n " + opt1 += "-N " + if in_mul: + opt1 += "-U " + + opt2 = f"-m {m} -M {M} -a {a} -A {A} -g {g} -G {G} " + if in_norm: + opt2 += "-n " if out_norm: - opt += "-N " + opt2 += "-N " if in_mul: - opt += "-u " + opt2 += "-u " if out_mul: - opt += "-U " + opt2 += "-U " U.check_compatibility( device, mgc2mgc, [], - f"nrand -l {B*(m+1)} | sopr -ABS", - f"mgc2mgc {opt}", + f"nrand -l {B*L} | fftcep -l {L} -m {m} | mgc2mgc {opt1}", + f"mgc2mgc {opt2}", [], dx=m + 1, dy=M + 1, From df08dc3288daed35e59e551a3faed93262336ff6 Mon Sep 17 00:00:00 2001 From: takenori-y Date: Tue, 23 Aug 2022 07:47:32 +0900 Subject: [PATCH 08/23] remove inverse --- diffsptk/core/imglsadf.py | 3 +-- diffsptk/core/mglsadf.py | 10 +--------- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/diffsptk/core/imglsadf.py b/diffsptk/core/imglsadf.py index c4708eb..229fd87 100644 --- a/diffsptk/core/imglsadf.py +++ b/diffsptk/core/imglsadf.py @@ -73,7 +73,6 @@ def __init__( taylor_order=taylor_order, frame_period=frame_period, ignore_gain=ignore_gain, - inverse=True, ) def forward(self, y, mc): @@ -106,5 +105,5 @@ def forward(self, y, mc): tensor([[ 0.4293, 1.0592, 7.9349, 14.9794]]) """ - x = self.mglsadf(y, mc) + x = self.mglsadf(y, -mc) return x diff --git a/diffsptk/core/mglsadf.py b/diffsptk/core/mglsadf.py index 0865f8a..cd072a1 100644 --- a/diffsptk/core/mglsadf.py +++ b/diffsptk/core/mglsadf.py @@ -52,9 +52,6 @@ class PseudoMGLSADigitalFilter(nn.Module): ignore_gain : bool [scalar] If True, perform filtering without gain. - inverse : bool [scalar] - If True, perform inverse filtering. - """ def __init__( @@ -67,7 +64,6 @@ def __init__( taylor_order=30, frame_period=1, ignore_gain=False, - inverse=False, ): super(PseudoMGLSADigitalFilter, self).__init__() @@ -75,7 +71,6 @@ def __init__( self.taylor_order = taylor_order self.frame_period = frame_period self.ignore_gain = ignore_gain - self.inverse = inverse assert 0 <= self.taylor_order @@ -131,8 +126,5 @@ def forward(self, x, mc): x = self.pad(x) x = x.unfold(-1, c.size(-1), 1) x = (x * c).sum(-1) / a - if self.inverse and a % 2 == 1: - y -= x - else: - y += x + y += x return y From 7a0eb88b6c32970f8e7edad5f9bebe82d2995d9e Mon Sep 17 00:00:00 2001 From: takenori-y Date: Mon, 29 Aug 2022 12:06:02 +0900 Subject: [PATCH 09/23] support zero-phase filter --- diffsptk/core/mglsadf.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/diffsptk/core/mglsadf.py b/diffsptk/core/mglsadf.py index cd072a1..3ecbf8d 100644 --- a/diffsptk/core/mglsadf.py +++ b/diffsptk/core/mglsadf.py @@ -14,6 +14,7 @@ # limitations under the License. # # ------------------------------------------------------------------------ # +import torch import torch.nn as nn from ..misc.utils import check_size @@ -52,6 +53,9 @@ class PseudoMGLSADigitalFilter(nn.Module): ignore_gain : bool [scalar] If True, perform filtering without gain. + phase : ['minimum', 'zero'] + Filter type. + """ def __init__( @@ -64,6 +68,7 @@ def __init__( taylor_order=30, frame_period=1, ignore_gain=False, + phase="minimum", ): super(PseudoMGLSADigitalFilter, self).__init__() @@ -71,10 +76,17 @@ def __init__( self.taylor_order = taylor_order self.frame_period = frame_period self.ignore_gain = ignore_gain + self.phase = phase assert 0 <= self.taylor_order - self.pad = nn.ConstantPad1d((cep_order, 0), 0) + if self.phase == "minimum": + self.pad = nn.ConstantPad1d((cep_order, 0), 0) + elif self.phase == "zero": + self.pad = nn.ConstantPad1d((cep_order, cep_order), 0) + else: + raise ValueError(f"phase {phase} is not supported") + self.mgc2c = MelGeneralizedCepstrumToMelGeneralizedCepstrum( filter_order, cep_order, @@ -119,7 +131,13 @@ def forward(self, x, mc): c = self.mgc2c(mc) if self.ignore_gain: c[..., 0] = 0 - c = self.linear_intpl(c.flip(-1)) + + if self.phase == "minimum": + c = c.flip(-1) + elif self.phase == "zero": + c1 = 0.5 * c[..., 1:] + c = torch.cat((c1.flip(-1), c[..., :1], c1), dim=-1) + c = self.linear_intpl(c) y = x.clone() for a in range(1, self.taylor_order + 1): From c4257c5c4a893fe7f59c6cf512c999625f6ec5d1 Mon Sep 17 00:00:00 2001 From: takenori-y Date: Tue, 30 Aug 2022 09:19:40 +0900 Subject: [PATCH 10/23] support maximum phase --- diffsptk/core/mglsadf.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/diffsptk/core/mglsadf.py b/diffsptk/core/mglsadf.py index 3ecbf8d..5281188 100644 --- a/diffsptk/core/mglsadf.py +++ b/diffsptk/core/mglsadf.py @@ -53,7 +53,7 @@ class PseudoMGLSADigitalFilter(nn.Module): ignore_gain : bool [scalar] If True, perform filtering without gain. - phase : ['minimum', 'zero'] + phase : ['minimum', 'maximum', 'zero'] Filter type. """ @@ -82,6 +82,8 @@ def __init__( if self.phase == "minimum": self.pad = nn.ConstantPad1d((cep_order, 0), 0) + elif self.phase == "maximum": + self.pad = nn.ConstantPad1d((0, cep_order), 0) elif self.phase == "zero": self.pad = nn.ConstantPad1d((cep_order, cep_order), 0) else: @@ -134,6 +136,8 @@ def forward(self, x, mc): if self.phase == "minimum": c = c.flip(-1) + elif self.phase == "maximum": + pass elif self.phase == "zero": c1 = 0.5 * c[..., 1:] c = torch.cat((c1.flip(-1), c[..., :1], c1), dim=-1) From ac3e7301d8bad40156c21154e743f064964fc3bc Mon Sep 17 00:00:00 2001 From: takenori-y Date: Tue, 30 Aug 2022 10:38:18 +0900 Subject: [PATCH 11/23] update test --- tests/test_mglsadf.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_mglsadf.py b/tests/test_mglsadf.py index f01e2a9..3b36dd7 100644 --- a/tests/test_mglsadf.py +++ b/tests/test_mglsadf.py @@ -60,6 +60,7 @@ def test_compatibility(device, ignore_gain, c, alpha=0.42, M=24, P=80): @pytest.mark.parametrize("device", ["cpu", "cuda"]) -def test_differentiable(device, B=4, T=20, M=4): - mglsadf = diffsptk.MLSA(M, taylor_order=5) +@pytest.mark.parametrize("phase", ["minimum", "maximum", "zero"]) +def test_differentiable(device, phase, B=4, T=20, M=4): + mglsadf = diffsptk.MLSA(M, taylor_order=5, phase=phase) U.check_differentiable(device, mglsadf, [(B, T), (B, T, M + 1)]) From cef855e72d3b53ba1481d487315503240d2b525c Mon Sep 17 00:00:00 2001 From: takenori-y Date: Mon, 5 Sep 2022 13:33:30 +0900 Subject: [PATCH 12/23] support mixed phase --- diffsptk/core/mglsadf.py | 28 +++++++++++++++++++++++----- tests/test_mglsadf.py | 7 +++++-- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/diffsptk/core/mglsadf.py b/diffsptk/core/mglsadf.py index 5281188..0858395 100644 --- a/diffsptk/core/mglsadf.py +++ b/diffsptk/core/mglsadf.py @@ -53,7 +53,7 @@ class PseudoMGLSADigitalFilter(nn.Module): ignore_gain : bool [scalar] If True, perform filtering without gain. - phase : ['minimum', 'maximum', 'zero'] + phase : ['minimum', 'maximum', 'zero', 'mixed'] Filter type. """ @@ -84,7 +84,7 @@ def __init__( self.pad = nn.ConstantPad1d((cep_order, 0), 0) elif self.phase == "maximum": self.pad = nn.ConstantPad1d((0, cep_order), 0) - elif self.phase == "zero": + elif self.phase == "zero" or self.phase == "mixed": self.pad = nn.ConstantPad1d((cep_order, cep_order), 0) else: raise ValueError(f"phase {phase} is not supported") @@ -97,7 +97,7 @@ def __init__( ) self.linear_intpl = LinearInterpolation(frame_period) - def forward(self, x, mc): + def forward(self, x, mc, nc=None): """Apply an MGLSA digital filter. Parameters @@ -108,6 +108,9 @@ def forward(self, x, mc): mc : Tensor [shape=(..., T/P, M+1)] Mel-generalized cepstrum, not MLSA digital filter coefficients. + nc : Tensor [shape=(..., T/P, M+1)] + Mxiumum phase part of mel-generalized cepstrum (used only mixed-phase mode). + Returns ------- y : Tensor [shape=(..., T)] @@ -139,8 +142,23 @@ def forward(self, x, mc): elif self.phase == "maximum": pass elif self.phase == "zero": - c1 = 0.5 * c[..., 1:] - c = torch.cat((c1.flip(-1), c[..., :1], c1), dim=-1) + c0, c1 = torch.split(c, [1, c.size(-1) - 1], dim=-1) + c1 = c1 * 0.5 + c = torch.cat((c1.flip(-1), c0, c1), dim=-1) + elif self.phase == "mixed": + check_size(nc.size(-1), self.filter_order + 1, "dimension of mel-cepstrum") + check_size(x.size(-1), nc.size(-2) * self.frame_period, "sequence length") + + d = self.mgc2c(nc) + if self.ignore_gain: + d[..., 0] = 0 + + c0, c1 = torch.split(c, [1, c.size(-1) - 1], dim=-1) + d0, d1 = torch.split(d, [1, d.size(-1) - 1], dim=-1) + c = torch.cat((c1.flip(-1), c0 + d0, d1), dim=-1) + else: + raise NotImplementedError + c = self.linear_intpl(c) y = x.clone() diff --git a/tests/test_mglsadf.py b/tests/test_mglsadf.py index 3b36dd7..dc6ee6b 100644 --- a/tests/test_mglsadf.py +++ b/tests/test_mglsadf.py @@ -60,7 +60,10 @@ def test_compatibility(device, ignore_gain, c, alpha=0.42, M=24, P=80): @pytest.mark.parametrize("device", ["cpu", "cuda"]) -@pytest.mark.parametrize("phase", ["minimum", "maximum", "zero"]) +@pytest.mark.parametrize("phase", ["minimum", "maximum", "zero", "mixed"]) def test_differentiable(device, phase, B=4, T=20, M=4): mglsadf = diffsptk.MLSA(M, taylor_order=5, phase=phase) - U.check_differentiable(device, mglsadf, [(B, T), (B, T, M + 1)]) + if phase == "mixed": + U.check_differentiable(device, mglsadf, [(B, T), (B, T, M + 1), (B, T, M + 1)]) + else: + U.check_differentiable(device, mglsadf, [(B, T), (B, T, M + 1)]) From 58bf85cd0620d5fb1a85347a1af43e6047d6728c Mon Sep 17 00:00:00 2001 From: takenori-y Date: Mon, 5 Sep 2022 15:21:47 +0900 Subject: [PATCH 13/23] update mglsadf --- diffsptk/core/mglsadf.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/diffsptk/core/mglsadf.py b/diffsptk/core/mglsadf.py index 0858395..11b0343 100644 --- a/diffsptk/core/mglsadf.py +++ b/diffsptk/core/mglsadf.py @@ -148,14 +148,9 @@ def forward(self, x, mc, nc=None): elif self.phase == "mixed": check_size(nc.size(-1), self.filter_order + 1, "dimension of mel-cepstrum") check_size(x.size(-1), nc.size(-2) * self.frame_period, "sequence length") - d = self.mgc2c(nc) - if self.ignore_gain: - d[..., 0] = 0 - - c0, c1 = torch.split(c, [1, c.size(-1) - 1], dim=-1) - d0, d1 = torch.split(d, [1, d.size(-1) - 1], dim=-1) - c = torch.cat((c1.flip(-1), c0 + d0, d1), dim=-1) + _, d1 = torch.split(d, [1, d.size(-1) - 1], dim=-1) + c = torch.cat((c.flip(-1), d1), dim=-1) else: raise NotImplementedError From e947e41c69a29ced1074c4fda5430dff6e96018e Mon Sep 17 00:00:00 2001 From: takenori-y Date: Tue, 22 Nov 2022 12:22:32 +0900 Subject: [PATCH 14/23] update mglsadf --- diffsptk/core/c2mpir.py | 14 ++-- diffsptk/core/imglsadf.py | 16 +--- diffsptk/core/mgc2mgc.py | 20 ++--- diffsptk/core/mgc2sp.py | 5 ++ diffsptk/core/mglsadf.py | 172 +++++++++++++++++++++++++++++--------- diffsptk/core/mpir2c.py | 14 ++-- setup.py | 1 + tests/test_imglsadf.py | 35 +++----- tests/test_mgc2mgc.py | 2 +- tests/test_mglsadf.py | 25 +++--- tests/utils.py | 6 ++ 11 files changed, 200 insertions(+), 110 deletions(-) diff --git a/diffsptk/core/c2mpir.py b/diffsptk/core/c2mpir.py index 9175b84..874a2fd 100644 --- a/diffsptk/core/c2mpir.py +++ b/diffsptk/core/c2mpir.py @@ -23,7 +23,7 @@ class CepstrumToMinimumPhaseImpulseResponse(nn.Module): """See `this page `_ - for details. + for details. The conversion uses FFT instead of recursive formula. Parameters ---------- @@ -33,21 +33,21 @@ class CepstrumToMinimumPhaseImpulseResponse(nn.Module): impulse_response_length : int >= 1 [scalar] Length of impulse response, :math:`N`. - fft_length : int >> :math:`M` [scalar] - Number of FFT bins, :math:`L`. + n_fft : int >> :math:`N` [scalar] + Number of FFT bins. Accurate conversion requires the large value. """ - def __init__(self, cep_order, impulse_response_length, fft_length=512): + def __init__(self, cep_order, impulse_response_length, n_fft=512): super(CepstrumToMinimumPhaseImpulseResponse, self).__init__() self.cep_order = cep_order self.impulse_response_length = impulse_response_length - self.fft_length = fft_length + self.n_fft = n_fft assert 0 <= self.cep_order assert 1 <= self.impulse_response_length - assert self.impulse_response_length <= self.fft_length // 2 + assert max(self.cep_order + 1, self.impulse_response_length) < self.n_fft def forward(self, c): """Convert cepstrum to minimum phase impulse response. @@ -73,6 +73,6 @@ def forward(self, c): """ check_size(c.size(-1), self.cep_order + 1, "dimension of cepstrum") - C = torch.fft.fft(c, n=self.fft_length) + C = torch.fft.fft(c, n=self.n_fft) h = torch.fft.ifft(cexp(C))[..., : self.impulse_response_length].real return h diff --git a/diffsptk/core/imglsadf.py b/diffsptk/core/imglsadf.py index 229fd87..1f80d03 100644 --- a/diffsptk/core/imglsadf.py +++ b/diffsptk/core/imglsadf.py @@ -54,25 +54,13 @@ class PseudoInverseMGLSADigitalFilter(nn.Module): def __init__( self, filter_order, - cep_order=200, - alpha=0, - gamma=0, - c=None, - taylor_order=50, - frame_period=1, - ignore_gain=False, + **kwargs, ): super(PseudoInverseMGLSADigitalFilter, self).__init__() self.mglsadf = PseudoMGLSADigitalFilter( filter_order, - cep_order=cep_order, - alpha=alpha, - gamma=gamma, - c=c, - taylor_order=taylor_order, - frame_period=frame_period, - ignore_gain=ignore_gain, + **kwargs, ) def forward(self, y, mc): diff --git a/diffsptk/core/mgc2mgc.py b/diffsptk/core/mgc2mgc.py index b4fa921..9214176 100644 --- a/diffsptk/core/mgc2mgc.py +++ b/diffsptk/core/mgc2mgc.py @@ -29,24 +29,24 @@ class GeneralizedCepstrumToGeneralizedCepstrum(nn.Module): - def __init__(self, in_order, out_order, in_gamma, out_gamma, fft_length): + def __init__(self, in_order, out_order, in_gamma, out_gamma, n_fft): super(GeneralizedCepstrumToGeneralizedCepstrum, self).__init__() self.in_order = in_order self.out_order = out_order self.in_gamma = in_gamma self.out_gamma = out_gamma - self.fft_length = fft_length + self.n_fft = n_fft assert 0 <= self.in_order assert 0 <= self.out_order assert abs(self.in_gamma) <= 1 assert abs(self.out_gamma) <= 1 - assert max(self.in_order, self.out_order) + 1 < fft_length + assert max(self.in_order, self.out_order) + 1 < self.n_fft def forward(self, c1): c01 = torch.cat((c1[..., :1] * 0, c1[..., 1:]), dim=-1) - C1 = torch.fft.fft(c01, n=self.fft_length) + C1 = torch.fft.fft(c01, n=self.n_fft) if self.in_gamma == 0: sC1 = cexp(C1) @@ -117,7 +117,7 @@ def forward(self, c): class MelGeneralizedCepstrumToMelGeneralizedCepstrum(nn.Module): """See `this page `_ - for details. This module may be slow due to recursive computation. + for details. The conversion uses FFT instead of recursive formula. Parameters ---------- @@ -151,8 +151,8 @@ class MelGeneralizedCepstrumToMelGeneralizedCepstrum(nn.Module): out_mul : bool [scalar] If True, assume gamma-multiplied output. - fft_length : int [scalar] - Number of FFT bins, :math:`L`. + n_fft : int >> :math:`M_1, M_2` [scalar] + Number of FFT bins. Accurate conversion requires the large value. """ @@ -168,7 +168,7 @@ def __init__( out_norm=False, in_mul=False, out_mul=False, - fft_length=512, + n_fft=512, ): super(MelGeneralizedCepstrumToMelGeneralizedCepstrum, self).__init__() @@ -197,7 +197,7 @@ def __init__( if True: modules.append( GeneralizedCepstrumToGeneralizedCepstrum( - in_order, out_order, in_gamma, out_gamma, fft_length + in_order, out_order, in_gamma, out_gamma, n_fft ) ) if not out_norm: @@ -216,7 +216,7 @@ def __init__( if in_gamma != out_gamma: modules.append( GeneralizedCepstrumToGeneralizedCepstrum( - out_order, out_order, in_gamma, out_gamma, fft_length + out_order, out_order, in_gamma, out_gamma, n_fft ) ) if not out_norm and in_gamma != out_gamma: diff --git a/diffsptk/core/mgc2sp.py b/diffsptk/core/mgc2sp.py index 72e90d4..0e89a12 100644 --- a/diffsptk/core/mgc2sp.py +++ b/diffsptk/core/mgc2sp.py @@ -50,6 +50,9 @@ class MelGeneralizedCepstrumToSpectrum(nn.Module): 'cycle', 'radian', 'degree'] Output format. + n_fft : int >> :math:`L` [scalar] + Number of FFT bins. Accurate conversion requires the large value. + """ def __init__( @@ -61,6 +64,7 @@ def __init__( norm=False, mul=False, out_format="power", + n_fft=512, ): super(MelGeneralizedCepstrumToSpectrum, self).__init__() @@ -94,6 +98,7 @@ def __init__( in_gamma=gamma, in_norm=norm, in_mul=mul, + n_fft=n_fft, ) def forward(self, mc): diff --git a/diffsptk/core/mglsadf.py b/diffsptk/core/mglsadf.py index 11b0343..b7a735a 100644 --- a/diffsptk/core/mglsadf.py +++ b/diffsptk/core/mglsadf.py @@ -25,16 +25,13 @@ class PseudoMGLSADigitalFilter(nn.Module): """See `this page `_ - for details. The exponential filter is approximated by the Taylor expansion. + for details. Parameters ---------- filter_order : int >= 0 [scalar] Order of filter coefficients, :math:`M`. - cep_order : int >= filter_order [scalar] - Order of linear cepstrum. - alpha : float [-1 < alpha < 1] Frequency warping factor, :math:`\\alpha`. @@ -44,60 +41,73 @@ class PseudoMGLSADigitalFilter(nn.Module): c : int >= 1 [scalar] Number of stages. - taylor_order : int >= 0 [scalar] - Order of Taylor series expansion, :math:`L`. - frame_period : int >= 1 [scalar] Frame period, :math:`P`. ignore_gain : bool [scalar] If True, perform filtering without gain. - phase : ['minimum', 'maximum', 'zero', 'mixed'] + phase : ['minimum', 'maximum', 'zero'] Filter type. + cascade : bool [scalar] + If True, use multi-stage FIR filters. + + cep_order : int >= 0 [scalar] + Order of linear cepstrum (valid only if cascade is True). + + taylor_order : int >= 0 [scalar] + Order of Taylor series expansion (valid only if cascade is True). + + impulse_response_length : int >= filter_order [scalar] + Length of impulse response (valid only if cascade is False). + + n_fft : int >= 0 [scalar] + Number of FFT bins for conversion (valid only if cascade is False). + """ def __init__( self, filter_order, - cep_order=200, alpha=0, gamma=0, c=None, - taylor_order=30, frame_period=1, ignore_gain=False, phase="minimum", + cascade=True, + **kwargs, ): super(PseudoMGLSADigitalFilter, self).__init__() self.filter_order = filter_order - self.taylor_order = taylor_order self.frame_period = frame_period - self.ignore_gain = ignore_gain - self.phase = phase - assert 0 <= self.taylor_order + gamma = get_gamma(gamma, c) - if self.phase == "minimum": - self.pad = nn.ConstantPad1d((cep_order, 0), 0) - elif self.phase == "maximum": - self.pad = nn.ConstantPad1d((0, cep_order), 0) - elif self.phase == "zero" or self.phase == "mixed": - self.pad = nn.ConstantPad1d((cep_order, cep_order), 0) + if cascade: + self.mglsadf = MultiStageFIRFilter( + filter_order, + alpha=alpha, + gamma=gamma, + frame_period=frame_period, + ignore_gain=ignore_gain, + phase=phase, + **kwargs, + ) else: - raise ValueError(f"phase {phase} is not supported") - - self.mgc2c = MelGeneralizedCepstrumToMelGeneralizedCepstrum( - filter_order, - cep_order, - in_alpha=alpha, - in_gamma=get_gamma(gamma, c), - ) - self.linear_intpl = LinearInterpolation(frame_period) + self.mglsadf = SingleStageFIRFilter( + filter_order, + alpha=alpha, + gamma=gamma, + frame_period=frame_period, + ignore_gain=ignore_gain, + phase=phase, + **kwargs, + ) - def forward(self, x, mc, nc=None): + def forward(self, x, mc): """Apply an MGLSA digital filter. Parameters @@ -108,9 +118,6 @@ def forward(self, x, mc, nc=None): mc : Tensor [shape=(..., T/P, M+1)] Mel-generalized cepstrum, not MLSA digital filter coefficients. - nc : Tensor [shape=(..., T/P, M+1)] - Mxiumum phase part of mel-generalized cepstrum (used only mixed-phase mode). - Returns ------- y : Tensor [shape=(..., T)] @@ -133,6 +140,48 @@ def forward(self, x, mc, nc=None): check_size(mc.size(-1), self.filter_order + 1, "dimension of mel-cepstrum") check_size(x.size(-1), mc.size(-2) * self.frame_period, "sequence length") + y = self.mglsadf(x, mc) + return y + + +class MultiStageFIRFilter(nn.Module): + def __init__( + self, + filter_order, + alpha=0, + gamma=0, + frame_period=1, + ignore_gain=False, + phase="minimum", + taylor_order=20, + cep_order=199, + ): + super(MultiStageFIRFilter, self).__init__() + + self.taylor_order = taylor_order + self.ignore_gain = ignore_gain + self.phase = phase + + assert 0 <= self.taylor_order + + if self.phase == "minimum": + self.pad = nn.ConstantPad1d((cep_order, 0), 0) + elif self.phase == "maximum": + self.pad = nn.ConstantPad1d((0, cep_order), 0) + elif self.phase == "zero": + self.pad = nn.ConstantPad1d((cep_order, cep_order), 0) + else: + raise ValueError(f"phase {phase} is not supported") + + self.mgc2c = MelGeneralizedCepstrumToMelGeneralizedCepstrum( + filter_order, + cep_order, + in_alpha=alpha, + in_gamma=gamma, + ) + self.linear_intpl = LinearInterpolation(frame_period) + + def forward(self, x, mc): c = self.mgc2c(mc) if self.ignore_gain: c[..., 0] = 0 @@ -145,12 +194,6 @@ def forward(self, x, mc, nc=None): c0, c1 = torch.split(c, [1, c.size(-1) - 1], dim=-1) c1 = c1 * 0.5 c = torch.cat((c1.flip(-1), c0, c1), dim=-1) - elif self.phase == "mixed": - check_size(nc.size(-1), self.filter_order + 1, "dimension of mel-cepstrum") - check_size(x.size(-1), nc.size(-2) * self.frame_period, "sequence length") - d = self.mgc2c(nc) - _, d1 = torch.split(d, [1, d.size(-1) - 1], dim=-1) - c = torch.cat((c.flip(-1), d1), dim=-1) else: raise NotImplementedError @@ -163,3 +206,54 @@ def forward(self, x, mc, nc=None): x = (x * c).sum(-1) / a y += x return y + + +class SingleStageFIRFilter(nn.Module): + def __init__( + self, + filter_order, + alpha=0, + gamma=0, + frame_period=1, + ignore_gain=False, + phase="minimum", + impulse_response_length=2000, + n_fft=4096, + ): + super(SingleStageFIRFilter, self).__init__() + + self.ignore_gain = ignore_gain + self.phase = phase + + taps = impulse_response_length - 1 + if self.phase == "minimum": + self.pad = nn.ConstantPad1d((taps, 0), 0) + elif self.phase == "maximum": + self.pad = nn.ConstantPad1d((0, taps), 0) + else: + raise ValueError(f"phase {phase} is not supported") + + self.mgc2ir = MelGeneralizedCepstrumToMelGeneralizedCepstrum( + filter_order, + impulse_response_length - 1, + in_alpha=alpha, + in_gamma=gamma, + out_gamma=1, + out_mul=True, + n_fft=n_fft, + ) + self.linear_intpl = LinearInterpolation(frame_period) + + def forward(self, x, mc): + h = self.mgc2ir(mc) + if self.phase == "minimum": + h = h.flip(-1) + + h = self.linear_intpl(h) + if self.ignore_gain: + h = h / h[..., -1:] + + x = self.pad(x) + x = x.unfold(-1, h.size(-1), 1) + y = (x * h).sum(-1) + return y diff --git a/diffsptk/core/mpir2c.py b/diffsptk/core/mpir2c.py index dad2ca2..46aaa04 100644 --- a/diffsptk/core/mpir2c.py +++ b/diffsptk/core/mpir2c.py @@ -23,7 +23,7 @@ class MinimumPhaseImpulseResponseToCepstrum(nn.Module): """See `this page `_ - for details. + for details. The conversion uses FFT instead of recursive formula. Parameters ---------- @@ -33,21 +33,21 @@ class MinimumPhaseImpulseResponseToCepstrum(nn.Module): impulse_response_length : int >= 1 [scalar] Length of impulse response, :math:`N`. - fft_length : int >> :math:`M` [scalar] - Number of FFT bins, :math:`L`. + n_fft : int >> :math:`N` [scalar] + Number of FFT bins. Accurate conversion requires the large value. """ - def __init__(self, cep_order, impulse_response_length, fft_length=512): + def __init__(self, cep_order, impulse_response_length, n_fft=512): super(MinimumPhaseImpulseResponseToCepstrum, self).__init__() self.cep_order = cep_order self.impulse_response_length = impulse_response_length - self.fft_length = fft_length + self.n_fft = n_fft assert 0 <= self.cep_order assert 1 <= self.impulse_response_length - assert self.cep_order + 1 < self.fft_length // 2 + assert max(self.cep_order + 1, self.impulse_response_length) < self.n_fft def forward(self, h): """Convert minimum phase impulse response to cepstrum. @@ -73,7 +73,7 @@ def forward(self, h): """ check_size(h.size(-1), self.impulse_response_length, "impulse response length") - H = torch.fft.fft(h, n=self.fft_length) + H = torch.fft.fft(h, n=self.n_fft) c = torch.fft.ifft(clog(H))[..., : self.cep_order + 1].real c[..., 1:] *= 2 return c diff --git a/setup.py b/setup.py index a2bebce..4e38f4f 100644 --- a/setup.py +++ b/setup.py @@ -36,6 +36,7 @@ "pydata-sphinx-theme", "pytest", "pytest-cov", + "soundfile", "sphinx", "twine", ], diff --git a/tests/test_imglsadf.py b/tests/test_imglsadf.py index 683fbe8..0029c1c 100644 --- a/tests/test_imglsadf.py +++ b/tests/test_imglsadf.py @@ -23,7 +23,8 @@ @pytest.mark.parametrize("device", ["cpu", "cuda"]) @pytest.mark.parametrize("ignore_gain", [False, True]) -def test_compatibility(device, ignore_gain, c=0, alpha=0.42, M=24, P=80): +@pytest.mark.parametrize("cascade", [False, True]) +def test_compatibility(device, ignore_gain, cascade, c=0, alpha=0.42, M=24, P=80): if device == "cuda" and not torch.cuda.is_available(): return @@ -44,36 +45,26 @@ def test_compatibility(device, ignore_gain, c=0, alpha=0.42, M=24, P=80): mc = torch.from_numpy(U.call(f"cat {tmp2}").reshape(-1, M + 1)).to(device) U.call(f"rm {tmp1} {tmp2}", get=False) + if cascade: + params = {"cep_order": 100, "taylor_order": 40} + else: + params = {"impulse_response_length": 500, "n_fft": 1024} + # Get residual signal. imglsadf = diffsptk.IMLSA( M, - cep_order=100, - taylor_order=50, frame_period=P, alpha=alpha, c=c, ignore_gain=ignore_gain, + cascade=cascade, + **params, ).to(device) x = imglsadf(y, mc) # Get reconstructed signal. - mglsadf = diffsptk.MLSA( - M, - cep_order=100, - taylor_order=30, - frame_period=P, - alpha=alpha, - c=c, - ignore_gain=ignore_gain, - ).to(device) - y_hat = mglsadf(x, mc) - - # Compute error between two signals. - error = torch.max(torch.abs(y - y_hat)) - assert torch.lt(error, 1) + y_hat = imglsadf(x, -mc) - -@pytest.mark.parametrize("device", ["cpu", "cuda"]) -def test_differentiable(device, B=4, T=20, M=4): - imglsadf = diffsptk.IMLSA(M, taylor_order=5) - U.check_differentiable(device, imglsadf, [(B, T), (B, T, M + 1)]) + # Compute correlation between two signals. + r = torch.corrcoef(torch.stack([y, y_hat]))[0, 1] + assert torch.gt(r, 0.99) diff --git a/tests/test_mgc2mgc.py b/tests/test_mgc2mgc.py index 9ae50cb..8c3b5b1 100644 --- a/tests/test_mgc2mgc.py +++ b/tests/test_mgc2mgc.py @@ -43,7 +43,7 @@ def test_compatibility( out_norm=out_norm, in_mul=in_mul, out_mul=out_mul, - fft_length=L, + n_fft=L, ) opt1 = f"-m {m} -M {m} -a 0 -A {a} -g {0} -G {g} " diff --git a/tests/test_mglsadf.py b/tests/test_mglsadf.py index dc6ee6b..833a06f 100644 --- a/tests/test_mglsadf.py +++ b/tests/test_mglsadf.py @@ -25,16 +25,23 @@ @pytest.mark.parametrize("device", ["cpu", "cuda"]) @pytest.mark.parametrize("ignore_gain", [False, True]) +@pytest.mark.parametrize("cascade", [False, True]) @pytest.mark.parametrize("c", [0, 10]) -def test_compatibility(device, ignore_gain, c, alpha=0.42, M=24, P=80): +def test_compatibility(device, ignore_gain, cascade, c, alpha=0.42, M=24, P=80): + if cascade: + params = {"cep_order": 100, "taylor_order": 20} + else: + params = {"impulse_response_length": 200, "n_fft": 512} + mglsadf = diffsptk.MLSA( M, - cep_order=100, - taylor_order=20, frame_period=P, alpha=alpha, c=c, ignore_gain=ignore_gain, + cascade=cascade, + phase="minimum", + **params, ) tmp1 = "mglsadf.tmp1" @@ -60,10 +67,8 @@ def test_compatibility(device, ignore_gain, c, alpha=0.42, M=24, P=80): @pytest.mark.parametrize("device", ["cpu", "cuda"]) -@pytest.mark.parametrize("phase", ["minimum", "maximum", "zero", "mixed"]) -def test_differentiable(device, phase, B=4, T=20, M=4): - mglsadf = diffsptk.MLSA(M, taylor_order=5, phase=phase) - if phase == "mixed": - U.check_differentiable(device, mglsadf, [(B, T), (B, T, M + 1), (B, T, M + 1)]) - else: - U.check_differentiable(device, mglsadf, [(B, T), (B, T, M + 1)]) +@pytest.mark.parametrize("phase", ["minimum", "maximum"]) +@pytest.mark.parametrize("cascade", [False, True]) +def test_differentiable(device, cascade, phase, B=4, T=20, M=4): + mglsadf = diffsptk.MLSA(M, cascade=cascade, phase=phase) + U.check_differentiable(device, mglsadf, [(B, T), (B, T, M + 1)]) diff --git a/tests/utils.py b/tests/utils.py index 272f9ee..b3e71fc 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -20,6 +20,7 @@ import warnings import numpy as np +import soundfile as sf import torch @@ -68,6 +69,7 @@ def check_compatibility( dy=None, eq=None, opt={}, + sr=None, verbose=False, ): if device == "cuda" and not torch.cuda.is_available(): @@ -105,6 +107,10 @@ def check_compatibility( module = compose(*[m.to(device) if hasattr(m, "to") else m for m in modules]) y_hat = module(*x, **opt).cpu().numpy() + if sr is not None: + sf.write("output.wav", y_hat / 32768, sr) + sf.write("target.wav", y / 32768, sr) + if verbose: print(f"Output: {y_hat}") print(f"Target: {y}") From 7889414bc5d4d2c72a1923272c6c2e5b030a0c4b Mon Sep 17 00:00:00 2001 From: takenori-y Date: Tue, 22 Nov 2022 12:27:00 +0900 Subject: [PATCH 15/23] modify version --- README.md | 2 +- diffsptk/version.py | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 5c9dd55..c565a59 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ diffsptk *diffsptk* is a differentiable version of [SPTK](https://github.com/sp-nitech/SPTK) based on the PyTorch framework. [![Latest Manual](https://img.shields.io/badge/docs-latest-blue.svg)](https://sp-nitech.github.io/diffsptk/latest/) -[![Stable Manual](https://img.shields.io/badge/docs-stable-blue.svg)](https://sp-nitech.github.io/diffsptk/1.0.0/) +[![Stable Manual](https://img.shields.io/badge/docs-stable-blue.svg)](https://sp-nitech.github.io/diffsptk/0.5.0/) [![Python Version](https://img.shields.io/pypi/pyversions/diffsptk.svg)](https://pypi.python.org/pypi/diffsptk) [![PyTorch Version](https://img.shields.io/badge/pytorch-1.10.0%20%7C%201.12.0-orange.svg)](https://pypi.python.org/pypi/diffsptk) [![PyPI Version](https://img.shields.io/pypi/v/diffsptk.svg)](https://pypi.python.org/pypi/diffsptk) diff --git a/diffsptk/version.py b/diffsptk/version.py index 5becc17..3d18726 100644 --- a/diffsptk/version.py +++ b/diffsptk/version.py @@ -1 +1 @@ -__version__ = "1.0.0" +__version__ = "0.5.0" diff --git a/setup.py b/setup.py index 4e38f4f..31c5d4d 100644 --- a/setup.py +++ b/setup.py @@ -23,6 +23,7 @@ ], python_requires=">= 3.8", install_requires=[ + "soundfile", "torch >= 1.10.0", "torchcrepe >= 0.0.16", "numpy", @@ -36,7 +37,6 @@ "pydata-sphinx-theme", "pytest", "pytest-cov", - "soundfile", "sphinx", "twine", ], From bbaee6aa71142294784969c5a3e2d24c36a795fe Mon Sep 17 00:00:00 2001 From: takenori-y Date: Tue, 22 Nov 2022 13:07:41 +0900 Subject: [PATCH 16/23] update docs --- README.md | 43 ++++++++++++++++----------------------- diffsptk/__init__.py | 2 ++ diffsptk/core/imglsadf.py | 31 ++++++++++++++++++---------- diffsptk/core/mglsadf.py | 14 ++++++------- diffsptk/misc/utils.py | 14 +++++++++++++ 5 files changed, 61 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index 6ab3fce..134f5aa 100644 --- a/README.md +++ b/README.md @@ -41,8 +41,6 @@ Examples ### Mel-cepstral analysis and synthesis ```python import diffsptk -import soundfile as sf -import torch # Set analysis condition. fl = 400 @@ -52,10 +50,9 @@ M = 24 alpha = 0.42 # Read waveform. -x, sr = sf.read("assets/data.wav") -x = torch.FloatTensor(x) +x, sr = diffsptk.read("assets/data.wav") -# Compute STFT of x. +# Compute STFT amplitude of x. stft = diffsptk.STFT(frame_length=fl, frame_period=fp, fft_length=n_fft) X = stft(x) @@ -63,22 +60,21 @@ X = stft(x) mcep = diffsptk.MelCepstralAnalysis(cep_order=M, fft_length=n_fft, alpha=alpha) mc = mcep(X) -# Generate excitation. -e = torch.randn(x.size(0)) +# Reconstruct x. +mlsa = diffsptk.MLSA(filter_order=M, alpha=alpha, frame_period=fp, taylor_order=30) +x_hat = mlsa(mlsa(x, -mc), mc) -# Apply MLSA filter to the excitation. -mlsa = diffsptk.MLSA(filter_order=M, alpha=alpha, frame_period=fp) -y = mlsa(e, mc) +# Write reconstructed waveform. +diffsptk.write("reconst.wav", x_hat, sr) -# Write waveform. -sf.write("unvoice.wav", y.cpu().numpy(), sr) +# Compute error. +error = (x_hat - x).abs().sum() +print(error) ``` ### Mel-spectrogram extraction ```python import diffsptk -import soundfile as sf -import torch # Set analysis condition. fl = 400 @@ -87,10 +83,9 @@ n_fft = 512 n_channel = 80 # Read waveform. -x, sr = sf.read("assets/data.wav") -x = torch.FloatTensor(x) +x, sr = diffsptk.read("assets/data.wav") -# Compute STFT of x. +# Compute STFT amplitude of x. stft = diffsptk.STFT(frame_length=fl, frame_period=fp, fft_length=n_fft) X = stft(x) @@ -106,15 +101,12 @@ Y = fbank(X) ### Subband decomposition ```python import diffsptk -import soundfile as sf -import torch K = 4 # Number of subbands. M = 40 # Order of filter. # Read waveform. -x, sr = sf.read("assets/data.wav") -x = torch.FloatTensor(x) +x, sr = diffsptk.read("assets/data.wav") # Decompose x. pqmf = diffsptk.PQMF(K, M) @@ -126,11 +118,12 @@ interpolate = diffsptk.Interpolation(K) ipqmf = diffsptk.IPQMF(K, M) x_hat = ipqmf(interpolate(K * y, dim=-1)).reshape(-1) -# Compute error between two signals. -error = torch.abs(x_hat - x).sum() - # Write reconstructed waveform. -sf.write("reconst.wav", x_hat.cpu().numpy(), sr) +diffsptk.write("reconst.wav", x_hat, sr) + +# Compute error. +error = (x_hat - x).abs().sum() +print(error) ``` diff --git a/diffsptk/__init__.py b/diffsptk/__init__.py index 3766638..ea8cd36 100644 --- a/diffsptk/__init__.py +++ b/diffsptk/__init__.py @@ -1,4 +1,6 @@ from . import core from .core import * from .misc.signals import * +from .misc.utils import read +from .misc.utils import write from .version import __version__ diff --git a/diffsptk/core/imglsadf.py b/diffsptk/core/imglsadf.py index 1f80d03..2f3e540 100644 --- a/diffsptk/core/imglsadf.py +++ b/diffsptk/core/imglsadf.py @@ -21,16 +21,13 @@ class PseudoInverseMGLSADigitalFilter(nn.Module): """See `this page `_ - for details. The exponential filter is approximated by the Taylor expansion. + for details. Parameters ---------- filter_order : int >= 0 [scalar] Order of filter coefficients, :math:`M`. - cep_order : int >= filter_order [scalar] - Order of linear cepstrum. - alpha : float [-1 < alpha < 1] Frequency warping factor, :math:`\\alpha`. @@ -40,15 +37,30 @@ class PseudoInverseMGLSADigitalFilter(nn.Module): c : int >= 1 [scalar] Number of stages. - taylor_order : int >= 0 [scalar] - Order of Taylor series expansion, :math:`L`. - frame_period : int >= 1 [scalar] Frame period, :math:`P`. ignore_gain : bool [scalar] If True, perform filtering without gain. + phase : ['minimum', 'maximum', 'zero'] + Filter type. + + cascade : bool [scalar] + If True, use multi-stage FIR filter. + + cep_order : int >= 0 [scalar] + Order of linear cepstrum (valid only if **cascade** is True). + + taylor_order : int >= 0 [scalar] + Order of Taylor series expansion (valid only if **cascade** is True). + + impulse_response_length : int >= 1 [scalar] + Length of impulse response (valid only if **cascade** is False). + + n_fft : int >= 1 [scalar] + Number of FFT bins for conversion (valid only if **cascade** is False). + """ def __init__( @@ -58,10 +70,7 @@ def __init__( ): super(PseudoInverseMGLSADigitalFilter, self).__init__() - self.mglsadf = PseudoMGLSADigitalFilter( - filter_order, - **kwargs, - ) + self.mglsadf = PseudoMGLSADigitalFilter(filter_order**kwargs) def forward(self, y, mc): """Apply an inverse MGLSA digital filter. diff --git a/diffsptk/core/mglsadf.py b/diffsptk/core/mglsadf.py index b7a735a..9d144b0 100644 --- a/diffsptk/core/mglsadf.py +++ b/diffsptk/core/mglsadf.py @@ -51,19 +51,19 @@ class PseudoMGLSADigitalFilter(nn.Module): Filter type. cascade : bool [scalar] - If True, use multi-stage FIR filters. + If True, use multi-stage FIR filter. cep_order : int >= 0 [scalar] - Order of linear cepstrum (valid only if cascade is True). + Order of linear cepstrum (valid only if **cascade** is True). taylor_order : int >= 0 [scalar] - Order of Taylor series expansion (valid only if cascade is True). + Order of Taylor series expansion (valid only if **cascade** is True). - impulse_response_length : int >= filter_order [scalar] - Length of impulse response (valid only if cascade is False). + impulse_response_length : int >= 1 [scalar] + Length of impulse response (valid only if **cascade** is False). - n_fft : int >= 0 [scalar] - Number of FFT bins for conversion (valid only if cascade is False). + n_fft : int >= 1 [scalar] + Number of FFT bins for conversion (valid only if **cascade** is False). """ diff --git a/diffsptk/misc/utils.py b/diffsptk/misc/utils.py index a97b1e1..a22e924 100644 --- a/diffsptk/misc/utils.py +++ b/diffsptk/misc/utils.py @@ -17,6 +17,7 @@ import warnings import numpy as np +import soundfile as sf import torch @@ -85,3 +86,16 @@ def clog(x): def check_size(x, y, cause): assert x == y, f"Unexpected {cause} (input {x} vs target {y})" + + +def read(filename, double=False): + x, sr = sf.read(filename) + if double: + x = torch.DoubleTensor(x) + else: + x = torch.FloatTensor(x) + return x, sr + + +def write(filename, x, sr): + sf.write(filename, x.cpu().numpy(), sr) From 3613244f08a67173d7eef17a254f5e8bfa9546f0 Mon Sep 17 00:00:00 2001 From: takenori-y Date: Tue, 22 Nov 2022 13:20:15 +0900 Subject: [PATCH 17/23] minor fix --- README.md | 2 +- diffsptk/core/imglsadf.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 134f5aa..c526690 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ stft = diffsptk.STFT(frame_length=fl, frame_period=fp, fft_length=n_fft) X = stft(x) # Estimate mel-cepstrum of x. -mcep = diffsptk.MelCepstralAnalysis(cep_order=M, fft_length=n_fft, alpha=alpha) +mcep = diffsptk.MelCepstralAnalysis(cep_order=M, fft_length=n_fft, alpha=alpha, n_iter=1) mc = mcep(X) # Reconstruct x. diff --git a/diffsptk/core/imglsadf.py b/diffsptk/core/imglsadf.py index 2f3e540..ba922a8 100644 --- a/diffsptk/core/imglsadf.py +++ b/diffsptk/core/imglsadf.py @@ -70,7 +70,7 @@ def __init__( ): super(PseudoInverseMGLSADigitalFilter, self).__init__() - self.mglsadf = PseudoMGLSADigitalFilter(filter_order**kwargs) + self.mglsadf = PseudoMGLSADigitalFilter(filter_order, **kwargs) def forward(self, y, mc): """Apply an inverse MGLSA digital filter. From d87d21ce53120ff5d3347422fb8ac95c6e070b56 Mon Sep 17 00:00:00 2001 From: takenori-y Date: Tue, 22 Nov 2022 13:43:41 +0900 Subject: [PATCH 18/23] add reference --- diffsptk/core/mglsadf.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/diffsptk/core/mglsadf.py b/diffsptk/core/mglsadf.py index 9d144b0..05f25f4 100644 --- a/diffsptk/core/mglsadf.py +++ b/diffsptk/core/mglsadf.py @@ -65,6 +65,11 @@ class PseudoMGLSADigitalFilter(nn.Module): n_fft : int >= 1 [scalar] Number of FFT bins for conversion (valid only if **cascade** is False). + References + ---------- + .. [1] T. Yoshimura et al., "Embedding a differentiable mel-cepstral synthesis + filter to a neural speech synthesis system," *arXiv:2211.11222*, 2022. + """ def __init__( From 10b2cad969bf55391344c67787f4be349ba37da3 Mon Sep 17 00:00:00 2001 From: takenori-y Date: Tue, 22 Nov 2022 13:44:03 +0900 Subject: [PATCH 19/23] improve coverage --- tests/test_mglsadf.py | 4 +++- tests/test_utils.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 tests/test_utils.py diff --git a/tests/test_mglsadf.py b/tests/test_mglsadf.py index 833a06f..a645da8 100644 --- a/tests/test_mglsadf.py +++ b/tests/test_mglsadf.py @@ -67,8 +67,10 @@ def test_compatibility(device, ignore_gain, cascade, c, alpha=0.42, M=24, P=80): @pytest.mark.parametrize("device", ["cpu", "cuda"]) -@pytest.mark.parametrize("phase", ["minimum", "maximum"]) +@pytest.mark.parametrize("phase", ["minimum", "maximum", "zero"]) @pytest.mark.parametrize("cascade", [False, True]) def test_differentiable(device, cascade, phase, B=4, T=20, M=4): + if not cascade and phase == "zero": + return mglsadf = diffsptk.MLSA(M, cascade=cascade, phase=phase) U.check_differentiable(device, mglsadf, [(B, T), (B, T, M + 1)]) diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 0000000..0491eb2 --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,31 @@ +# ------------------------------------------------------------------------ # +# Copyright 2022 SPTK Working Group # +# # +# Licensed under the Apache License, Version 2.0 (the "License"); # +# you may not use this file except in compliance with the License. # +# You may obtain a copy of the License at # +# # +# http://www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software # +# distributed under the License is distributed on an "AS IS" BASIS, # +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # +# See the License for the specific language governing permissions and # +# limitations under the License. # +# ------------------------------------------------------------------------ # + +import filecmp +import os + +import diffsptk +import pytest + + +@pytest.mark.parametrize("double", [False, True]) +def test_read_and_write(double): + in_wav = "assets/data.wav" + out_wav = "data.wav" + x, sr = diffsptk.read(in_wav, double=double) + diffsptk.write(out_wav, x, sr) + assert filecmp.cmp(in_wav, out_wav, shallow=False) + os.remove(out_wav) From cd1f69ffa103a6a355f25b173fd2d79600deb97f Mon Sep 17 00:00:00 2001 From: takenori-y Date: Tue, 22 Nov 2022 14:20:06 +0900 Subject: [PATCH 20/23] add doc [skip ci] --- diffsptk/core/mcpf.py | 8 ++++---- diffsptk/misc/utils.py | 43 ++++++++++++++++++++++++++++++++++++++++++ docs/misc/utils.rst | 6 ++++++ 3 files changed, 53 insertions(+), 4 deletions(-) create mode 100644 docs/misc/utils.rst diff --git a/diffsptk/core/mcpf.py b/diffsptk/core/mcpf.py index c6a75ef..12a7e63 100644 --- a/diffsptk/core/mcpf.py +++ b/diffsptk/core/mcpf.py @@ -40,15 +40,15 @@ class MelCepstrumPostfiltering(nn.Module): beta : float [scalar] Intensity parameter, :math:`\\beta`. - impulse_response_length : int >= 1 [scalar] - Length of impulse response. - onset : int >= 0 [scalar] Onset index. + impulse_response_length : int >= 1 [scalar] + Length of impulse response. + """ - def __init__(self, cep_order, alpha, beta, onset=2, impulse_response_length=1024): + def __init__(self, cep_order, alpha=0, beta=0, onset=2, impulse_response_length=1024): super(MelCepstrumPostfiltering, self).__init__() assert 0 <= onset diff --git a/diffsptk/misc/utils.py b/diffsptk/misc/utils.py index a22e924..8acd111 100644 --- a/diffsptk/misc/utils.py +++ b/diffsptk/misc/utils.py @@ -89,6 +89,30 @@ def check_size(x, y, cause): def read(filename, double=False): + """Read waveform from file. + + Parameters + ---------- + filename : str [scalar] + Path of wav file. + + double : bool [scalar] + If True, return double-type tensor. + + Returns + ------- + x : Tensor + Waveform. + + Examples + -------- + >>> x, sr = diffsptk.read("assets/data.wav") + >>> x + tensor([ 0.0002, 0.0004, 0.0006, ..., 0.0006, -0.0006, -0.0007]) + >>> sr + 16000 + + """ x, sr = sf.read(filename) if double: x = torch.DoubleTensor(x) @@ -98,4 +122,23 @@ def read(filename, double=False): def write(filename, x, sr): + """Write waveform to file. + + Parameters + ---------- + filename : str [scalar] + Path of wav file. + + x : Tensor + Waveform. + + sr : int [scalar] + Sample rate in Hz. + + Examples + -------- + >>> x, sr = diffsptk.read("assets/data.wav") + >>> diffsptk.write("out.wav", x, sr) + + """ sf.write(filename, x.cpu().numpy(), sr) diff --git a/docs/misc/utils.rst b/docs/misc/utils.rst new file mode 100644 index 0000000..7e1ddc0 --- /dev/null +++ b/docs/misc/utils.rst @@ -0,0 +1,6 @@ +utils +----- + +.. autofunction:: diffsptk.read + +.. autofunction:: diffsptk.write From ca0c6068dd5607b36a6f42bf7ac737cd707a2acf Mon Sep 17 00:00:00 2001 From: takenori-y Date: Tue, 22 Nov 2022 16:15:03 +0900 Subject: [PATCH 21/23] minor fix --- diffsptk/core/mcpf.py | 4 +++- diffsptk/core/mglsadf.py | 9 ++++++++- tests/test_utils.py | 3 ++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/diffsptk/core/mcpf.py b/diffsptk/core/mcpf.py index 12a7e63..09f7f33 100644 --- a/diffsptk/core/mcpf.py +++ b/diffsptk/core/mcpf.py @@ -48,7 +48,9 @@ class MelCepstrumPostfiltering(nn.Module): """ - def __init__(self, cep_order, alpha=0, beta=0, onset=2, impulse_response_length=1024): + def __init__( + self, cep_order, alpha=0, beta=0, onset=2, impulse_response_length=1024 + ): super(MelCepstrumPostfiltering, self).__init__() assert 0 <= onset diff --git a/diffsptk/core/mglsadf.py b/diffsptk/core/mglsadf.py index 05f25f4..9620dc3 100644 --- a/diffsptk/core/mglsadf.py +++ b/diffsptk/core/mglsadf.py @@ -253,10 +253,17 @@ def forward(self, x, mc): h = self.mgc2ir(mc) if self.phase == "minimum": h = h.flip(-1) + elif self.phase == "maximum": + pass + else: + raise NotImplementedError h = self.linear_intpl(h) if self.ignore_gain: - h = h / h[..., -1:] + if self.phase == "minimum": + h = h / h[..., -1:] + elif self.phase == "maximum": + h = h / h[..., :1] x = self.pad(x) x = x.unfold(-1, h.size(-1), 1) diff --git a/tests/test_utils.py b/tests/test_utils.py index 0491eb2..3a71c9d 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -17,9 +17,10 @@ import filecmp import os -import diffsptk import pytest +import diffsptk + @pytest.mark.parametrize("double", [False, True]) def test_read_and_write(double): From b1f40acb8ef94aa5e81a9a12e1f619151bc2a2f8 Mon Sep 17 00:00:00 2001 From: takenori-y Date: Tue, 22 Nov 2022 16:35:44 +0900 Subject: [PATCH 22/23] update test --- tests/test_imglsadf.py | 1 + tests/test_mglsadf.py | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test_imglsadf.py b/tests/test_imglsadf.py index 0029c1c..6f5a920 100644 --- a/tests/test_imglsadf.py +++ b/tests/test_imglsadf.py @@ -58,6 +58,7 @@ def test_compatibility(device, ignore_gain, cascade, c=0, alpha=0.42, M=24, P=80 c=c, ignore_gain=ignore_gain, cascade=cascade, + phase="minimum", **params, ).to(device) x = imglsadf(y, mc) diff --git a/tests/test_mglsadf.py b/tests/test_mglsadf.py index a645da8..4a67719 100644 --- a/tests/test_mglsadf.py +++ b/tests/test_mglsadf.py @@ -67,9 +67,10 @@ def test_compatibility(device, ignore_gain, cascade, c, alpha=0.42, M=24, P=80): @pytest.mark.parametrize("device", ["cpu", "cuda"]) -@pytest.mark.parametrize("phase", ["minimum", "maximum", "zero"]) +@pytest.mark.parametrize("ignore_gain", [False, True]) @pytest.mark.parametrize("cascade", [False, True]) -def test_differentiable(device, cascade, phase, B=4, T=20, M=4): +@pytest.mark.parametrize("phase", ["minimum", "maximum", "zero"]) +def test_differentiable(device, ignore_gain, cascade, phase, B=4, T=20, M=4): if not cascade and phase == "zero": return mglsadf = diffsptk.MLSA(M, cascade=cascade, phase=phase) From f6538f4924542dfcf573d5577b9374fbda90930e Mon Sep 17 00:00:00 2001 From: takenori-y Date: Tue, 22 Nov 2022 16:51:11 +0900 Subject: [PATCH 23/23] fix test --- tests/test_mglsadf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_mglsadf.py b/tests/test_mglsadf.py index 4a67719..b0a765f 100644 --- a/tests/test_mglsadf.py +++ b/tests/test_mglsadf.py @@ -73,5 +73,5 @@ def test_compatibility(device, ignore_gain, cascade, c, alpha=0.42, M=24, P=80): def test_differentiable(device, ignore_gain, cascade, phase, B=4, T=20, M=4): if not cascade and phase == "zero": return - mglsadf = diffsptk.MLSA(M, cascade=cascade, phase=phase) + mglsadf = diffsptk.MLSA(M, ignore_gain=ignore_gain, cascade=cascade, phase=phase) U.check_differentiable(device, mglsadf, [(B, T), (B, T, M + 1)])