Skip to content

Commit 894d8d0

Browse files
committed
fix harmonic-pulse
1 parent 17d6200 commit 894d8d0

2 files changed

Lines changed: 33 additions & 34 deletions

File tree

diffsptk/modules/excite.py

Lines changed: 31 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -29,22 +29,17 @@
2929
def generate_pulse(
3030
pitch: torch.Tensor,
3131
phase: torch.Tensor,
32-
shift: float | torch.Tensor,
3332
bipolar: bool,
3433
) -> torch.Tensor:
3534
def get_pulse_pos(p):
3635
return torch.ge(torch.diff(torch.ceil(p)), 1)
3736

3837
e = torch.zeros_like(pitch)
39-
40-
padded_phase = F.pad(phase, (1, 0))
41-
padded_phase += shift
42-
43-
pulse_pos = get_pulse_pos(padded_phase)
38+
pulse_pos = get_pulse_pos(phase)
4439
e[pulse_pos] = torch.sqrt(pitch[pulse_pos])
4540

4641
if bipolar:
47-
pulse_pos_double = get_pulse_pos(0.5 * padded_phase)
42+
pulse_pos_double = get_pulse_pos(0.5 * phase)
4843
e[pulse_pos & ~pulse_pos_double] *= -1
4944

5045
return e
@@ -53,30 +48,33 @@ def get_pulse_pos(p):
5348
def generate_harmonic_pulse(
5449
pitch: torch.Tensor,
5550
phase: torch.Tensor,
56-
shift: float | torch.Tensor,
5751
bipolar: bool,
58-
power_factor: float = 0.1,
5952
) -> torch.Tensor:
60-
if not bipolar:
61-
raise ValueError("Harmonic pulse is only defined for bipolar polarity.")
62-
63-
# number of harmonics = floor(0.5 * fs / f0) = floor(0.5 * 1 / T)
53+
# The number of harmonics is floor(0.5 * fs / f0) = floor(0.5 * 1 / T)
6454
n_harmonics = torch.floor(0.5 * pitch)
6555

6656
# The summation of sinusoids can be computed efficiently using the closed-form
6757
# expression of the Dirichlet kernel.
68-
theta = TAU * (phase + shift)
69-
numer = torch.cos(0.5 * theta) - torch.cos((n_harmonics + 0.5) * theta)
70-
denom = 2 * torch.sin(0.5 * theta)
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)
7165

72-
# Handle singularities at theta = 0, where the limit is the number of harmonics.
66+
# Handle singularities at phase is zero (i.e., harmonics).
7367
eps = 1e-6
74-
is_off_peak = denom.abs() > eps
75-
safe_denom = torch.where(is_off_peak, denom, torch.ones_like(denom))
68+
is_on_peak = denom.abs() < eps
69+
safe_denom = torch.where(is_on_peak, torch.ones_like(denom), denom)
7670
e = numer / safe_denom
77-
e = torch.where(is_off_peak, e, n_harmonics)
71+
if bipolar:
72+
e[is_on_peak] = 0
73+
else:
74+
e[is_on_peak] = n_harmonics[is_on_peak]
7875

79-
norm_factor = power_factor * torch.sqrt(2 / n_harmonics.clip(min=1))
76+
# Normalize as the same energy as the pulse excitation.
77+
norm_factor = torch.sqrt(2 / n_harmonics.clip(min=1))
8078
return norm_factor * e
8179

8280

@@ -244,13 +242,7 @@ def _forward(
244242
p = p.transpose(-2, -1)
245243
p *= mask
246244

247-
# Compute phase.
248-
voiced_pos = torch.gt(p, 0)
249-
q = torch.zeros_like(p)
250-
q[voiced_pos] = torch.reciprocal(p[voiced_pos])
251-
s = torch.cumsum(q.double(), dim=-1)
252-
bias, _ = torch.cummax(s * ~mask, dim=-1)
253-
phase = (s - bias).to(p.dtype)
245+
# Compute initial phase.
254246
if not isinstance(init_phase, str):
255247
shift = init_phase / TAU
256248
elif init_phase == "zeros":
@@ -260,6 +252,14 @@ def _forward(
260252
else:
261253
raise ValueError(f"init_phase {init_phase} is not supported.")
262254

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+
263263
if polarity == "auto":
264264
bipolar = voiced_region != "pulse"
265265
elif polarity in ("unipolar", "bipolar"):
@@ -274,7 +274,9 @@ def _forward(
274274
}
275275
if voiced_region not in generators:
276276
raise ValueError(f"voiced_region {voiced_region} is not supported.")
277-
e = generators[voiced_region](p, phase, shift, bipolar)
277+
phase = F.pad(phase, (1, 0))
278+
phase += shift
279+
e = generators[voiced_region](p, phase, bipolar)
278280
else:
279281
generators = {
280282
"sinusoidal": generate_sinusoidal,

tests/test_excite.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -104,10 +104,7 @@ def compute_error(infile):
104104
)
105105
@pytest.mark.parametrize("polarity", ["unipolar", "bipolar"])
106106
@pytest.mark.parametrize("init_phase", ["zeros", "random", np.round(np.pi / 2, 2)])
107-
def test_waveform(voiced_region, polarity, init_phase, P=80, verbose=True):
108-
if voiced_region == "harmonic-pulse" and polarity == "unipolar":
109-
pytest.skip("Harmonic pulse cannot be unipolar.")
110-
107+
def test_waveform(voiced_region, polarity, init_phase, P=80, verbose=False):
111108
excite = diffsptk.ExcitationGeneration(
112109
P,
113110
voiced_region=voiced_region,
@@ -119,7 +116,7 @@ def test_waveform(voiced_region, polarity, init_phase, P=80, verbose=True):
119116
U.call(f"x2x +sd tools/SPTK/asset/data.short | pitch -s 16 -p {P} -o 0 -a 2")
120117
)
121118
e = excite(pitch)
122-
if voiced_region == "pulse":
119+
if "pulse" in voiced_region:
123120
e = e / e.abs().max()
124121
if verbose:
125122
sf.write(f"excite_{voiced_region}_{polarity}_{init_phase}.wav", e, 16000)

0 commit comments

Comments
 (0)