Skip to content

Commit 53ab0eb

Browse files
authored
Merge pull request #162 from sp-nitech/harmonic [skip ci]
Support excitation used in Wavehax
2 parents f44019e + 894d8d0 commit 53ab0eb

5 files changed

Lines changed: 188 additions & 73 deletions

File tree

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ test: tool
6969
python -m pytest $$module $(OPT)
7070

7171
test-example: tool
72-
[ -n "$(MODULE)" ] && module=modules/$(MODULE).py || module=modules; \
72+
[ -n "$(MODULE)" ] && module=modules/$(MODULE).py || module=; \
7373
. .venv/bin/activate && export NUMBA_CUDA_LOW_OCCUPANCY_WARNINGS=0 && \
7474
python -m pytest --doctest-modules --no-cov --ignore=$(PROJECT)/third_party $(PROJECT)/$$module
7575

diffsptk/functional.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -635,7 +635,7 @@ def excite(
635635
The frame period in samples, :math:`P`.
636636
637637
voiced_region : ['pulse', 'sinusoidal', 'sawtooth', 'inverted-sawtooth', \
638-
'triangle', 'square']
638+
'triangle', 'square', 'harmonic-pulse']
639639
The type of voiced region.
640640
641641
unvoiced_region : ['zeros', 'gauss', 'm-sequence', 'uniform']

diffsptk/modules/excite.py

Lines changed: 146 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,114 @@
1919
import torch
2020
import torch.nn.functional as F
2121

22-
from ..signals import mseq
22+
from ..signals import mseq_like
2323
from ..typing import Precomputed
2424
from ..utils.private import TAU, UNVOICED_SYMBOL, filter_values
2525
from .base import BaseFunctionalModule
2626
from .linear_intpl import LinearInterpolation
2727

2828

29+
def generate_pulse(
30+
pitch: torch.Tensor,
31+
phase: torch.Tensor,
32+
bipolar: bool,
33+
) -> torch.Tensor:
34+
def get_pulse_pos(p):
35+
return torch.ge(torch.diff(torch.ceil(p)), 1)
36+
37+
e = torch.zeros_like(pitch)
38+
pulse_pos = get_pulse_pos(phase)
39+
e[pulse_pos] = torch.sqrt(pitch[pulse_pos])
40+
41+
if bipolar:
42+
pulse_pos_double = get_pulse_pos(0.5 * phase)
43+
e[pulse_pos & ~pulse_pos_double] *= -1
44+
45+
return e
46+
47+
48+
def generate_harmonic_pulse(
49+
pitch: torch.Tensor,
50+
phase: torch.Tensor,
51+
bipolar: bool,
52+
) -> torch.Tensor:
53+
# The number of harmonics is floor(0.5 * fs / f0) = floor(0.5 * 1 / T)
54+
n_harmonics = torch.floor(0.5 * pitch)
55+
56+
# The summation of sinusoids can be computed efficiently using the closed-form
57+
# expression of the Dirichlet kernel.
58+
theta = TAU * phase[..., :-1]
59+
half_theta = 0.5 * theta
60+
if bipolar:
61+
numer = torch.cos(half_theta) - torch.cos((n_harmonics + 0.5) * theta)
62+
else:
63+
numer = -torch.sin(half_theta) + torch.sin((n_harmonics + 0.5) * theta)
64+
denom = 2 * torch.sin(half_theta)
65+
66+
# Handle singularities at phase is zero (i.e., harmonics).
67+
eps = 1e-6
68+
is_on_peak = denom.abs() < eps
69+
safe_denom = torch.where(is_on_peak, torch.ones_like(denom), denom)
70+
e = numer / safe_denom
71+
if bipolar:
72+
e[is_on_peak] = 0
73+
else:
74+
e[is_on_peak] = n_harmonics[is_on_peak]
75+
76+
# Normalize as the same energy as the pulse excitation.
77+
norm_factor = torch.sqrt(2 / n_harmonics.clip(min=1))
78+
return norm_factor * e
79+
80+
81+
def generate_sinusoidal(phase: torch.Tensor, bipolar: bool) -> torch.Tensor:
82+
if bipolar:
83+
e = torch.sin(TAU * phase)
84+
else:
85+
e = 0.5 * (1 - torch.cos(TAU * phase))
86+
return e
87+
88+
89+
def generate_sawtooth(phase: torch.Tensor, bipolar: bool) -> torch.Tensor:
90+
e = torch.fmod(phase, 1)
91+
if bipolar:
92+
e = 2 * e - 1
93+
return e
94+
95+
96+
def generate_inverted_sawtooth(phase: torch.Tensor, bipolar: bool) -> torch.Tensor:
97+
e = 1 - torch.fmod(phase, 1)
98+
if bipolar:
99+
e = 2 * e - 1
100+
return e
101+
102+
103+
def generate_triangle(phase: torch.Tensor, bipolar: bool) -> torch.Tensor:
104+
if bipolar:
105+
e = 2 * torch.abs(2 * torch.fmod(phase + 0.75, 1) - 1) - 1
106+
else:
107+
e = torch.abs(2 * torch.fmod(phase + 0.5, 1) - 1)
108+
return e
109+
110+
111+
def generate_square(phase: torch.Tensor, bipolar: bool) -> torch.Tensor:
112+
e = torch.le(torch.fmod(phase, 1), 0.5).to(phase.dtype)
113+
if bipolar:
114+
e = 2 * e - 1
115+
return e
116+
117+
118+
def generate_gauss(source: torch.Tensor) -> torch.Tensor:
119+
return torch.randn_like(source)
120+
121+
122+
def generate_mseq(source: torch.Tensor) -> torch.Tensor:
123+
return mseq_like(source)
124+
125+
126+
def generate_uniform(source: torch.Tensor) -> torch.Tensor:
127+
return math.sqrt(12) * torch.rand_like(source)
128+
129+
29130
class ExcitationGeneration(BaseFunctionalModule):
30131
"""See `this page <https://sp-nitech.github.io/sptk/latest/main/excite.html>`_
31132
for details.
@@ -36,7 +137,7 @@ class ExcitationGeneration(BaseFunctionalModule):
36137
The frame period in samples, :math:`P`.
37138
38139
voiced_region : ['pulse', 'sinusoidal', 'sawtooth', 'inverted-sawtooth', \
39-
'triangle', 'square']
140+
'triangle', 'square', 'harmonic-pulse']
40141
The type of voiced region.
41142
42143
unvoiced_region : ['zeros', 'gauss', 'm-sequence', 'uniform']
@@ -141,13 +242,7 @@ def _forward(
141242
p = p.transpose(-2, -1)
142243
p *= mask
143244

144-
# Compute phase.
145-
voiced_pos = torch.gt(p, 0)
146-
q = torch.zeros_like(p)
147-
q[voiced_pos] = torch.reciprocal(p[voiced_pos])
148-
s = torch.cumsum(q.double(), dim=-1)
149-
bias, _ = torch.cummax(s * ~mask, dim=-1)
150-
phase = (s - bias).to(p.dtype)
245+
# Compute initial phase.
151246
if not isinstance(init_phase, str):
152247
shift = init_phase / TAU
153248
elif init_phase == "zeros":
@@ -156,74 +251,56 @@ def _forward(
156251
shift = torch.rand_like(p[..., :1])
157252
else:
158253
raise ValueError(f"init_phase {init_phase} is not supported.")
159-
if isinstance(shift, torch.Tensor) or shift != 0.0:
160-
phase += shift
161254

162-
# Generate excitation signal using phase.
255+
# Compute phase.
256+
voiced_pos = torch.gt(p, 0)
257+
q = torch.zeros_like(p)
258+
q[voiced_pos] = torch.reciprocal(p[voiced_pos])
259+
s = torch.cumsum(q.double(), dim=-1)
260+
bias, _ = torch.cummax(s * ~mask, dim=-1)
261+
phase = (s - bias).to(p.dtype)
262+
163263
if polarity == "auto":
164-
unipolar = voiced_region == "pulse"
264+
bipolar = voiced_region != "pulse"
165265
elif polarity in ("unipolar", "bipolar"):
166-
unipolar = polarity == "unipolar"
266+
bipolar = polarity == "bipolar"
167267
else:
168268
raise ValueError(f"polarity {polarity} is not supported.")
169-
e = torch.zeros_like(p)
170-
if voiced_region == "pulse":
171-
172-
def get_pulse_pos(p):
173-
r = torch.ceil(p)
174-
return torch.ge(torch.diff(r), 1)
175-
176-
if isinstance(shift, float):
177-
padded_phase = F.pad(phase, (1, 0), value=shift)
178-
else:
179-
padded_phase = torch.cat([shift, phase], dim=-1)
180-
181-
if unipolar:
182-
pulse_pos = get_pulse_pos(padded_phase)
183-
e[pulse_pos] = torch.sqrt(p[pulse_pos])
184-
else:
185-
pulse_pos1 = get_pulse_pos(padded_phase)
186-
pulse_pos2 = get_pulse_pos(0.5 * padded_phase)
187-
e[pulse_pos1] = torch.sqrt(p[pulse_pos1])
188-
e[pulse_pos1 & ~pulse_pos2] *= -1
189-
elif voiced_region == "sinusoidal":
190-
if unipolar:
191-
e[mask] = 0.5 * (1 - torch.cos(TAU * phase[mask]))
192-
else:
193-
e[mask] = torch.sin(TAU * phase[mask])
194-
elif voiced_region == "sawtooth":
195-
if unipolar:
196-
e[mask] = torch.fmod(phase[mask], 1)
197-
else:
198-
e[mask] = 2 * torch.fmod(phase[mask], 1) - 1
199-
elif voiced_region == "inverted-sawtooth":
200-
if unipolar:
201-
e[mask] = 1 - torch.fmod(phase[mask], 1)
202-
else:
203-
e[mask] = 1 - 2 * torch.fmod(phase[mask], 1)
204-
elif voiced_region == "triangle":
205-
if unipolar:
206-
e[mask] = torch.abs(2 * torch.fmod(phase[mask] + 0.5, 1) - 1)
207-
else:
208-
e[mask] = 2 * torch.abs(2 * torch.fmod(phase[mask] + 0.75, 1) - 1) - 1
209-
elif voiced_region == "square":
210-
if unipolar:
211-
e[mask] = torch.le(torch.fmod(phase[mask], 1), 0.5).to(e.dtype)
212-
else:
213-
e[mask] = 2 * torch.le(torch.fmod(phase[mask], 1), 0.5).to(e.dtype) - 1
269+
270+
if "pulse" in voiced_region:
271+
generators = {
272+
"pulse": generate_pulse,
273+
"harmonic-pulse": generate_harmonic_pulse,
274+
}
275+
if voiced_region not in generators:
276+
raise ValueError(f"voiced_region {voiced_region} is not supported.")
277+
phase = F.pad(phase, (1, 0))
278+
phase += shift
279+
e = generators[voiced_region](p, phase, bipolar)
214280
else:
215-
raise ValueError(f"voiced_region {voiced_region} is not supported.")
281+
generators = {
282+
"sinusoidal": generate_sinusoidal,
283+
"sawtooth": generate_sawtooth,
284+
"inverted-sawtooth": generate_inverted_sawtooth,
285+
"triangle": generate_triangle,
286+
"square": generate_square,
287+
}
288+
if voiced_region not in generators:
289+
raise ValueError(f"voiced_region {voiced_region} is not supported.")
290+
phase += shift
291+
e = torch.zeros_like(p)
292+
e[mask] = generators[voiced_region](phase[mask], bipolar)
216293

217294
if unvoiced_region == "zeros":
218295
pass
219-
elif unvoiced_region == "gauss":
220-
e[~mask] = torch.randn(torch.sum(~mask), device=e.device, dtype=e.dtype)
221-
elif unvoiced_region == "m-sequence":
222-
e[~mask] = mseq(torch.sum(~mask) - 1, device=e.device, dtype=e.dtype)
223-
elif unvoiced_region == "uniform":
224-
e[~mask] = math.sqrt(12) * (
225-
torch.rand(torch.sum(~mask), device=e.device, dtype=e.dtype) - 0.5
226-
)
227296
else:
228-
raise ValueError(f"unvoiced_region {unvoiced_region} is not supported.")
297+
generators = {
298+
"gauss": generate_gauss,
299+
"m-sequence": generate_mseq,
300+
"uniform": generate_uniform,
301+
}
302+
if unvoiced_region not in generators:
303+
raise ValueError(f"unvoiced_region {unvoiced_region} is not supported.")
304+
e[~mask] = generators[unvoiced_region](e[~mask])
305+
229306
return e

diffsptk/signals.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,36 @@ def mseq(*order: int, **kwargs) -> torch.Tensor:
301301
return out.reshape(order)
302302

303303

304+
def mseq_like(tensor: torch.Tensor, **kwargs) -> torch.Tensor:
305+
"""Generate M-sequence with the same shape as the input tensor.
306+
307+
Parameters
308+
----------
309+
tensor : Tensor
310+
The input tensor. The shape of the output is the same as this tensor.
311+
312+
**kwargs : additional keyword arguments
313+
See `torch.ones <https://pytorch.org/docs/stable/generated/torch.ones.html>`_.
314+
315+
Returns
316+
-------
317+
out : Tensor
318+
The random value sequence.
319+
320+
Examples
321+
--------
322+
>>> import diffsptk
323+
>>> x = torch.empty(4)
324+
>>> y = diffsptk.mseq_like(x)
325+
>>> y
326+
tensor([-1., 1., -1., 1.])
327+
328+
"""
329+
shape = list(tensor.shape)
330+
shape[-1] -= 1
331+
return mseq(*shape, device=tensor.device, dtype=tensor.dtype, **kwargs)
332+
333+
304334
def nrand(
305335
*order: int, mean: float = 0, stdv: float = 1, var: float | None = None, **kwargs
306336
) -> torch.Tensor:

tests/test_excite.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,15 @@ def compute_error(infile):
9292

9393
@pytest.mark.parametrize(
9494
"voiced_region",
95-
["pulse", "sinusoidal", "sawtooth", "inverted-sawtooth", "triangle", "square"],
95+
[
96+
"pulse",
97+
"sinusoidal",
98+
"sawtooth",
99+
"inverted-sawtooth",
100+
"triangle",
101+
"square",
102+
"harmonic-pulse",
103+
],
96104
)
97105
@pytest.mark.parametrize("polarity", ["unipolar", "bipolar"])
98106
@pytest.mark.parametrize("init_phase", ["zeros", "random", np.round(np.pi / 2, 2)])
@@ -108,7 +116,7 @@ def test_waveform(voiced_region, polarity, init_phase, P=80, verbose=False):
108116
U.call(f"x2x +sd tools/SPTK/asset/data.short | pitch -s 16 -p {P} -o 0 -a 2")
109117
)
110118
e = excite(pitch)
111-
if voiced_region == "pulse":
119+
if "pulse" in voiced_region:
112120
e = e / e.abs().max()
113121
if verbose:
114122
sf.write(f"excite_{voiced_region}_{polarity}_{init_phase}.wav", e, 16000)

0 commit comments

Comments
 (0)