From a5f282902ab475135c1608b384fe7a19910e1b96 Mon Sep 17 00:00:00 2001 From: takenori-y Date: Tue, 21 Jan 2025 17:09:23 +0900 Subject: [PATCH 1/4] fix --- README.md | 4 ++-- codecov.yml | 7 +++++++ tests/test_syn.py | 4 ++-- 3 files changed, 11 insertions(+), 4 deletions(-) create mode 100644 codecov.yml diff --git a/README.md b/README.md index 3807737..1a7bea3 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,10 @@ This is an unofficial Python reimplemantation of the [legacy-STRAIGHT](https://github.com/HidekiKawahara/legacy_STRAIGHT), which was originally written in MATLAB. -[![Stable Manual](https://img.shields.io/badge/docs-latest-blue.svg)](https://takenori-y.github.io/pylstraight/0.1.0/) +[![Stable Manual](https://img.shields.io/badge/docs-stable-blue.svg)](https://takenori-y.github.io/pylstraight/0.1.0/) [![Downloads](https://static.pepy.tech/badge/pylstriaght)](https://pepy.tech/project/pylstriaght) [![Python Version](https://img.shields.io/pypi/pyversions/pylstraight.svg)](https://pypi.python.org/pypi/pylstraight) -[![PyPI Version](https://img.shields.io/pypi/v/diffsptk.svg)](https://pypi.python.org/pypi/diffsptk) +[![PyPI Version](https://img.shields.io/pypi/v/pylstraight.svg)](https://pypi.python.org/pypi/pylstraight) [![Codecov](https://codecov.io/gh/takenori-y/pylstraight/branch/master/graph/badge.svg)](https://app.codecov.io/gh/takenori-y/pylstraight) [![License](https://img.shields.io/github/license/takenori-y/pylstraight.svg)](https://github.com/takenori-y/pylstraight/blob/master/LICENSE) [![GitHub Actions](https://github.com/takenori-y/pylstraight/workflows/package/badge.svg)](https://github.com/takenori-y/pylstraight/actions) diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..48c1cba --- /dev/null +++ b/codecov.yml @@ -0,0 +1,7 @@ +coverage: + status: + project: + default: + target: auto + threshold: 1% + patch: off diff --git a/tests/test_syn.py b/tests/test_syn.py index f23788d..3d2ab52 100644 --- a/tests/test_syn.py +++ b/tests/test_syn.py @@ -28,7 +28,7 @@ def test_synthesis() -> None: ap = pyls.fromfile("tests/reference/data.ap", fs) ap = pyls.sp_to_sp(ap, "db", "linear") sp = pyls.fromfile("tests/reference/data.sp", fs) - hyp_syn = pyls.synthesize(f0, ap, sp, fs)[:len(ref_syn)] + hyp_syn = pyls.synthesize(f0, ap, sp, fs)[: len(ref_syn)] assert 0.95 < np.corrcoef(ref_syn, hyp_syn)[0, 1] pyls.write("tests/output/data.syn.wav", hyp_syn, fs) @@ -40,7 +40,7 @@ def test_sample(sample_data: tuple[np.ndarray, int]) -> None: f0 = pyls.extract_f0(x, fs) ap = pyls.extract_ap(x, fs, f0) sp = pyls.extract_sp(x, fs, f0) - hyp_syn = pyls.synthesize(f0, ap, sp, fs)[:len(ref_syn)] + hyp_syn = pyls.synthesize(f0, ap, sp, fs)[: len(ref_syn)] assert 0.95 < np.corrcoef(ref_syn, hyp_syn)[0, 1] pyls.write("tests/output/data.pyls.wav", hyp_syn, fs) From d9792fbeddf5b1ebe91d31b05babf467b6eac7fc Mon Sep 17 00:00:00 2001 From: takenori-y Date: Tue, 21 Jan 2025 17:17:28 +0900 Subject: [PATCH 2/4] changed to arcsin --- pylstraight/core/f0.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pylstraight/core/f0.py b/pylstraight/core/f0.py index 1bf28b6..4afda15 100644 --- a/pylstraight/core/f0.py +++ b/pylstraight/core/f0.py @@ -487,7 +487,7 @@ def zwvlt2ifq(pm: np.ndarray, fs: float) -> np.ndarray: npm = pm / (np.abs(pm) + 1e-10) pif = np.abs(np.diff(npm, axis=0)) pif = np.pad(pif, ((1, 0), (0, 0)), mode="edge") - return fs / np.pi * np.asin(pif / 2) + return fs / np.pi * np.arcsin(pif / 2) def zifq2gpm2( From 0e952db19a266d22b24a6120f27fb6a540cf49be Mon Sep 17 00:00:00 2001 From: takenori-y Date: Tue, 21 Jan 2025 17:42:48 +0900 Subject: [PATCH 3/4] improve coverage --- pylstraight/core/syn.py | 3 +-- pyproject.toml | 3 +++ tests/test_syn.py | 8 +++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pylstraight/core/syn.py b/pylstraight/core/syn.py index 175d613..296d3e0 100644 --- a/pylstraight/core/syn.py +++ b/pylstraight/core/syn.py @@ -195,14 +195,13 @@ def straightSynthTB07ca( inx = np.arange(nii) inxq = np.arange(niiNew) * ((nii - 1) / (niiNew - 1)) n2sgram = interp1(inx, n2sgram, inxq) - ap = interp1(inx, ap, inxq) fftl = fftLengthForLowestF0 nii = niiNew if ap.shape[1] != nii: inx = np.arange(ap.shape[1]) inxq = np.arange(nii) * ((ap.shape[1] - 1) / (nii - 1)) - ap = interp1(inx, ap, inxq, method="*linear") + ap = interp1(inx, ap, inxq) aprms = 10 ** (ap / 20) aprm = np.clip(aprms * 1.6 - 0.015, 0.001, 1) diff --git a/pyproject.toml b/pyproject.toml index 1c53e57..9e064f3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -94,8 +94,11 @@ exclude = ["__init__.py"] [tool.coverage.report] exclude_lines = [ "pragma: no cover", + "pytest.fail", + "except", "msg", "raise", + "if TYPE_CHECKING:", ] [tool.pytest.ini_options] diff --git a/tests/test_syn.py b/tests/test_syn.py index 3d2ab52..c5bd99f 100644 --- a/tests/test_syn.py +++ b/tests/test_syn.py @@ -29,7 +29,7 @@ def test_synthesis() -> None: ap = pyls.sp_to_sp(ap, "db", "linear") sp = pyls.fromfile("tests/reference/data.sp", fs) hyp_syn = pyls.synthesize(f0, ap, sp, fs)[: len(ref_syn)] - assert 0.95 < np.corrcoef(ref_syn, hyp_syn)[0, 1] + assert 0.94 < np.corrcoef(ref_syn, hyp_syn)[0, 1] pyls.write("tests/output/data.syn.wav", hyp_syn, fs) @@ -41,7 +41,7 @@ def test_sample(sample_data: tuple[np.ndarray, int]) -> None: ap = pyls.extract_ap(x, fs, f0) sp = pyls.extract_sp(x, fs, f0) hyp_syn = pyls.synthesize(f0, ap, sp, fs)[: len(ref_syn)] - assert 0.95 < np.corrcoef(ref_syn, hyp_syn)[0, 1] + assert 0.94 < np.corrcoef(ref_syn, hyp_syn)[0, 1] pyls.write("tests/output/data.pyls.wav", hyp_syn, fs) @@ -91,8 +91,6 @@ def test_short_fftl_input() -> None: fs = 8000 f0 = np.ones(200) * 40 ap = np.ones((200, 400)) - sp = np.ones((200, 513)) + sp = np.ones((200, 200)) syn = pyls.synthesize(f0, ap, sp, fs) assert len(syn) == 8000 - syn = pyls.synthesize(f0, ap, sp[:, :400], fs) - assert len(syn) == 8000 From 261cb062797bd14638fe091658125ad90bbf4d4a Mon Sep 17 00:00:00 2001 From: takenori-y Date: Tue, 21 Jan 2025 23:37:32 +0900 Subject: [PATCH 4/4] improve coverage --- pylstraight/api.py | 56 +++++++++++++++++++++--------------------- pylstraight/core/f0.py | 1 + tests/test_ap.py | 38 ++++++++++++++++++++++++++++ tests/test_f0.py | 47 +++++++++++++++++++++++++++++++++++ tests/test_sp.py | 38 ++++++++++++++++++++++++++++ 5 files changed, 152 insertions(+), 28 deletions(-) diff --git a/pylstraight/api.py b/pylstraight/api.py index e0c9bfc..cb2a128 100644 --- a/pylstraight/api.py +++ b/pylstraight/api.py @@ -43,10 +43,10 @@ def f0_to_f0( f0 : np.ndarray [shape=(nframe,)] The input F0. - in_format : ['pitch', 'f0', 'logf0'] + in_format : ['inverse', 'linear', 'log'] The format of the input F0. - out_format : ['pitch', 'f0', 'logf0'] + out_format : ['inverse', 'linear', 'log'] The format of the output F0. fs : int @@ -63,9 +63,9 @@ def f0_to_f0( >>> import pylstraight as pyls >>> import numpy as np >>> f0 = np.array([100, 200, 0, 400], dtype=float) - >>> pyls.f0_to_f0(f0, "f0", "logf0") + >>> pyls.f0_to_f0(f0, "linear", "log") array([ 4.60517019e+00, 5.29831737e+00, -1.00000000e+10, 5.99146455e+00]) - >>> pyls.f0_to_f0(f0, "f0", "pitch", fs=16000) + >>> pyls.f0_to_f0(f0, "linear", "inverse", fs=16000) array([160., 80., 0., 40.]) """ @@ -76,28 +76,28 @@ def f0_to_f0( if in_format == out_format: return f0 - if (in_format == "pitch" or out_format == "pitch") and fs <= 0: + if (in_format == "inverse" or out_format == "inverse") and fs <= 0: msg = "Sampling frequency is required." raise ValueError(msg) - voiced = f0 != (-1e10 if in_format == "logf0" else 0) + voiced = f0 != (-1e10 if in_format == "log" else 0) voiced_func = { - "pitch": { - "f0": lambda x: fs / x, - "logf0": lambda x: np.log(fs / x), + "inverse": { + "linear": lambda x: fs / x, + "log": lambda x: np.log(fs / x), }, - "f0": { - "pitch": lambda x: fs / x, - "logf0": np.log, + "linear": { + "inverse": lambda x: fs / x, + "log": np.log, }, - "logf0": { - "pitch": lambda x: fs / np.exp(x), - "f0": np.exp, + "log": { + "inverse": lambda x: fs / np.exp(x), + "linear": np.exp, }, } unvoiced = ~voiced - unvoiced_value = -1e10 if out_format == "logf0" else 0 + unvoiced_value = -1e10 if out_format == "log" else 0 new_f0 = np.empty_like(f0) try: @@ -299,7 +299,7 @@ def extract_f0( *, frame_shift: float = 5.0, f0_range: tuple[float, float] = (40.0, 400.0), - f0_format: str = "f0", + f0_format: str = "linear", f0_param: F0Param | None = None, return_aux: bool = False, ) -> np.ndarray: @@ -319,7 +319,7 @@ def extract_f0( f0_range : tuple[float, float] The lower and upper bounds of F0 search in Hz. - f0_format : ['pitch', 'f0', 'logf0'] + f0_format : ['inverse', 'linear', 'log'] The output format. f0_param : F0Param or None @@ -382,7 +382,7 @@ def extract_f0( f0, vuv, auxouts = MulticueF0v14(x, fs, f0_param) f0 *= vuv f0[f0_ceil < f0] = f0_ceil - f0 = f0_to_f0(f0, "f0", f0_format, fs=fs) + f0 = f0_to_f0(f0, "linear", f0_format, fs=fs) if return_aux: return f0, auxouts @@ -398,7 +398,7 @@ def extract_ap( *, frame_shift: float = 5.0, ap_floor: float = 0.001, - f0_format: str = "f0", + f0_format: str = "linear", ap_format: str = "a", ap_param: ApParam | None = None, ) -> np.ndarray: @@ -424,7 +424,7 @@ def extract_ap( ap_floor : float The minimum value of aperiodicity. - f0_format : ['pitch', 'f0', 'logf0'] + f0_format : ['inverse', 'linear', 'log'] The input format of the F0. ap_format : ['a', 'p', 'a/p', 'p/a'] @@ -485,7 +485,7 @@ def extract_ap( ap = exstraightAPind( x, fs, - f0_to_f0(f0, f0_format, "f0", fs=fs), + f0_to_f0(f0, f0_format, "linear", fs=fs), None if aux is None else aux.refined_cn, ap_param, ) @@ -500,7 +500,7 @@ def extract_sp( f0: np.ndarray, *, frame_shift: float = 5.0, - f0_format: str = "f0", + f0_format: str = "linear", sp_format: str = "linear", sp_param: SpParam | None = None, ) -> np.ndarray: @@ -520,7 +520,7 @@ def extract_sp( frame_shift : float The frame shift in msec. - f0_format : ['pitch', 'f0', 'logf0'] + f0_format : ['inverse', 'linear', 'log'] The input format of the F0. sp_format : ['db', 'log', 'linear', 'power'] @@ -577,7 +577,7 @@ def extract_sp( ) raise ValueError(msg) - sp = exstraightspec(x, f0_to_f0(f0, f0_format, "f0", fs=fs), fs, sp_param) + sp = exstraightspec(x, f0_to_f0(f0, f0_format, "linear", fs=fs), fs, sp_param) if scaler != 1: sp *= scaler return sp_to_sp(sp, "linear", sp_format) @@ -590,7 +590,7 @@ def synthesize( fs: int, *, frame_shift: float = 5.0, - f0_format: str = "f0", + f0_format: str = "linear", ap_format: str = "a", sp_format: str = "linear", syn_param: SynParam | None = None, @@ -614,7 +614,7 @@ def synthesize( frame_shift : float The frame shift in msec. - f0_format : ['pitch', 'f0', 'logf0'] + f0_format : ['inverse', 'linear', 'log'] The format of F0. ap_format : ['a', 'p', 'a/p', 'p/a'] @@ -670,7 +670,7 @@ def synthesize( syn_param.spectral_update_interval = frame_shift return exstraightsynth( - f0_to_f0(f0, f0_format, "f0", fs=fs), + f0_to_f0(f0, f0_format, "linear", fs=fs), sp_to_sp(sp, sp_format, "linear"), sp_to_sp(ap_to_ap(ap, ap_format, "a"), "linear", "db"), fs, diff --git a/pylstraight/core/f0.py b/pylstraight/core/f0.py index 4afda15..4afa7cf 100644 --- a/pylstraight/core/f0.py +++ b/pylstraight/core/f0.py @@ -798,6 +798,7 @@ def zremoveACinduction( h60 = np.sum(np.abs(f - 60) < 5) / np.sum(0 < f) if h50 < 0.2 and h60 < 0.2: return x, ind, 0 + ind = 1 fq = 50 if h60 < h50 else 60 tx = (np.arange(len(x)) + 1) / fs fqv = mrange(-0.3, 0.025, 0.3) + fq diff --git a/tests/test_ap.py b/tests/test_ap.py index 143a7e5..6a18607 100644 --- a/tests/test_ap.py +++ b/tests/test_ap.py @@ -78,3 +78,41 @@ def test_long_f0_input() -> None: f0 = np.zeros(201) ap = pyls.extract_ap(x, fs, f0) assert len(ap) == 201 + + +def test_conversion() -> None: + """Test conversion between different aperiodicity formats.""" + + def check_reversibility(x: np.ndarray, in_format: str, out_format: str) -> bool: + """Check if the conversion is identity. + + Parameters + ---------- + x : np.ndarray + The input. + + in_format : str + The input format. + + out_format : str + The output format. + + Returns + ------- + out : bool + True if the conversion is identity. + + """ + y = pyls.ap_to_ap(x, in_format, out_format) + z = pyls.ap_to_ap(y, out_format, in_format) + return np.allclose(x, z) + + a = np.array([0.001, 0.5, 0.999]) + assert check_reversibility(a, "a", "p") + assert check_reversibility(a, "a", "a/p") + assert check_reversibility(a, "a", "p/a") + p = pyls.ap_to_ap(a, "a", "p") + assert check_reversibility(p, "p", "a/p") + assert check_reversibility(p, "p", "p/a") + a_p = pyls.ap_to_ap(p, "p", "a/p") + assert check_reversibility(a_p, "a/p", "p/a") diff --git a/tests/test_f0.py b/tests/test_f0.py index cb4ddcf..7fc3035 100644 --- a/tests/test_f0.py +++ b/tests/test_f0.py @@ -93,6 +93,18 @@ def test_very_long_frame_shift() -> None: assert len(f0) == 1 +def test_ac_induction_removal(sample_data: tuple[np.ndarray, int]) -> None: + """Test removal of AC induction.""" + x, fs = sample_data + t = np.arange(len(x)) / fs + ac_frequency = 50 + ac_amplitude = 1e-3 + ac_noise = ac_amplitude * np.sin(2 * np.pi * ac_frequency * t) + f0 = pyls.extract_f0(x, fs) + f02 = pyls.extract_f0(x + ac_noise, fs) + assert len(f0[0 < f0]) == len(f02[0 < f02]) + + def test_if_number_of_harmonic(sample_data: tuple[np.ndarray, int]) -> None: """Test if_number_of_harmonic parameter.""" x, fs = sample_data @@ -111,3 +123,38 @@ def test_if_number_of_harmonic(sample_data: tuple[np.ndarray, int]) -> None: assert abs(len(f01[0 < f01]) - len(f03[0 < f03])) <= 1 assert np.max(np.abs(f01[voiced] - f02[voiced])) < 5 assert np.max(np.abs(f01[voiced] - f03[voiced])) < 5 + + +def test_conversion() -> None: + """Test conversion between different f0 formats.""" + fs = 8000 + + def check_reversibility(x: np.ndarray, in_format: str, out_format: str) -> bool: + """Check if the conversion is identity. + + Parameters + ---------- + x : np.ndarray + The input. + + in_format : str + The input format. + + out_format : str + The output format. + + Returns + ------- + out : bool + True if the conversion is identity. + + """ + y = pyls.f0_to_f0(x, in_format, out_format, fs) + z = pyls.f0_to_f0(y, out_format, in_format, fs) + return np.allclose(x, z) + + f0 = np.array([100, 0, 200]).astype(np.float64) + assert check_reversibility(f0, "linear", "log") + assert check_reversibility(f0, "linear", "inverse") + log_f0 = pyls.f0_to_f0(f0, "linear", "log") + assert check_reversibility(log_f0, "log", "inverse") diff --git a/tests/test_sp.py b/tests/test_sp.py index 51abc7d..a1923cc 100644 --- a/tests/test_sp.py +++ b/tests/test_sp.py @@ -75,3 +75,41 @@ def test_long_f0_input() -> None: f0 = np.zeros(201) sp = pyls.extract_sp(x, fs, f0) assert len(sp) == 200 + + +def test_conversion() -> None: + """Test conversion between different aperiodicity formats.""" + + def check_reversibility(x: np.ndarray, in_format: str, out_format: str) -> bool: + """Check if the conversion is identity. + + Parameters + ---------- + x : np.ndarray + The input. + + in_format : str + The input format. + + out_format : str + The output format. + + Returns + ------- + out : bool + True if the conversion is identity. + + """ + y = pyls.sp_to_sp(x, in_format, out_format) + z = pyls.sp_to_sp(y, out_format, in_format) + return np.allclose(x, z) + + sp = np.array([-60, 0, 60]).astype(np.float64) + assert check_reversibility(sp, "db", "log") + assert check_reversibility(sp, "db", "linear") + assert check_reversibility(sp, "db", "power") + sp = pyls.sp_to_sp(sp, "db", "log") + assert check_reversibility(sp, "log", "linear") + assert check_reversibility(sp, "log", "power") + sp = pyls.sp_to_sp(sp, "log", "linear") + assert check_reversibility(sp, "linear", "power")