diff --git a/pop/bin/vowel-hold.py b/pop/bin/vowel-hold.py index 07f079612e..da249f536f 100644 --- a/pop/bin/vowel-hold.py +++ b/pop/bin/vowel-hold.py @@ -21,7 +21,7 @@ import numpy as np, soundfile as sf, pyworld as pw ap = argparse.ArgumentParser() ap.add_argument("wav"); ap.add_argument("--at", type=float, required=True); ap.add_argument("--len", type=float, required=True) ap.add_argument("--out", required=True); ap.add_argument("--steady", type=float, default=0.2) -ap.add_argument("--curve", type=float, default=0.8); ap.add_argument("--vibrato", type=float, default=0.2) +ap.add_argument("--curve", type=float, default=0.8); ap.add_argument("--vibrato", type=float, default=0.15) ap.add_argument("--fade-in", type=float, default=0.03) a = ap.parse_args() FRAME_MS = 5.0 @@ -37,10 +37,16 @@ voiced = np.where(f0 > 0)[0] last = voiced[voiced >= len(f0) - n_steady - 2] if len(last) < 4: raise SystemExit(f"no voiced frames in the last {a.steady}s before {a.at}s") idx = last[-n_steady:] if len(last) >= n_steady else last -# ping-pong through the steady frames: a b c d c b a b c … -order = np.concatenate([idx, idx[-2:0:-1]]) +# ONE envelope for the whole hold: the log-mean spectrum of the steady frames (frame-to-frame +# jumps and WORLD's per-frame buzz are what screeched), the median aperiodicity with a floor of +# 0.35 above 4 kHz (a too-periodic top band whistles), and a gentle tilt above 5 kHz N = int(a.len * 1000 / FRAME_MS) -seq = np.array([order[k % len(order)] for k in range(N)]) +sp_mean = np.exp(np.mean(np.log(sp[idx] + 1e-12), axis=0)) +ap_med = np.median(apr[idx], axis=0) +freqs = np.linspace(0, fs / 2, sp_mean.shape[0]) +ap_med = np.where(freqs > 4000, np.maximum(ap_med, 0.35), ap_med) +tilt = np.where(freqs > 5000, 10 ** (-(freqs - 5000) / 4000 * 6 / 20), 1.0) # −6 dB per 4 kHz above 5 kHz +sp_hold = np.tile(sp_mean * tilt ** 2, (N, 1)); ap_hold = np.tile(ap_med, (N, 1)) f0_med = float(np.median(f0[idx])) tt = np.arange(N) * FRAME_MS / 1000 vib_env = np.clip(tt / 0.4, 0, 1) @@ -48,7 +54,7 @@ rng = np.random.default_rng(3) drift = np.cumsum(rng.normal(0, 0.004, N)); drift -= np.linspace(0, drift[-1], N) # a breath of wander, zero net cents = a.vibrato * vib_env * np.sin(2 * np.pi * 5.2 * tt) + drift f0_hold = f0_med * 2 ** (cents / 12) -y = pw.synthesize(np.ascontiguousarray(f0_hold), np.ascontiguousarray(sp[seq]), np.ascontiguousarray(apr[seq]), fs, frame_period=FRAME_MS) +y = pw.synthesize(np.ascontiguousarray(f0_hold), np.ascontiguousarray(sp_hold), np.ascontiguousarray(ap_hold), fs, frame_period=FRAME_MS) y = y[: int(a.len * fs)] # level: the steady vowel's RMS ref = seg[int(t[idx[0]] * fs): int(t[idx[-1]] * fs) + 1]