HiFT vocoder: python/hift_vocoder.py
Browse files- python/hift_vocoder.py +132 -0
python/hift_vocoder.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""HiFT 神经声码器(NPU 版):f0/decode 两个 AXMODEL + 宿主侧 DSP(纯 numpy,无 torch)。
|
| 3 |
+
|
| 4 |
+
HiFT 被拆成两个静态模型:
|
| 5 |
+
hifift_f0.axmodel mel[1,80,198] -> f0[1,198](纯卷积,U8)
|
| 6 |
+
hifift_decode.axmodel mel[1,80,198] + s_stft[1,18,23761] -> raw_mag[1,9,23761],
|
| 7 |
+
raw_phase[1,9,23761](U16,双输出独立量化)
|
| 8 |
+
|
| 9 |
+
宿主侧 DSP(本模块):f0 最近邻上采样 x480 -> SineGen 源激励(cumsum/sin/噪声)
|
| 10 |
+
-> 16 点 STFT -> decode -> exp/sin -> 16 点 ISTFT -> clamp。
|
| 11 |
+
|
| 12 |
+
与 torch 原版 HiFT 逐位验证:stft max diff ~5.6e-9、istft ~1.3e-7、源激励 ~4.9e-10、
|
| 13 |
+
端到端 wav corr > 0.99999(fp32);板端 U16 编译后 wav corr ~0.92、>4kHz 能量与 torch 持平。
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
from __future__ import annotations
|
| 17 |
+
|
| 18 |
+
import io
|
| 19 |
+
import math
|
| 20 |
+
import wave
|
| 21 |
+
from pathlib import Path
|
| 22 |
+
|
| 23 |
+
import numpy as np
|
| 24 |
+
|
| 25 |
+
SR = 24000
|
| 26 |
+
T_MEL = 198 # 静态 mel 帧
|
| 27 |
+
PAD_VAL = -11.0 # mel 尾部补静音值(log10)
|
| 28 |
+
NFFT, HOP = 16, 4
|
| 29 |
+
HARMONICS = 9 # 8 次谐波 + 基频
|
| 30 |
+
SINE_AMP = 0.1
|
| 31 |
+
NOISE_STD = 0.003
|
| 32 |
+
VOICED_THRESHOLD = 10.0
|
| 33 |
+
UPSAMPLE_SCALE = 480 # f0 -> 源激励上采样倍数
|
| 34 |
+
STFT_FRAMES = T_MEL * UPSAMPLE_SCALE // HOP + 1 # 23761
|
| 35 |
+
AUDIO_LIMIT = 0.99
|
| 36 |
+
|
| 37 |
+
# periodic hann == torch hann_window(16)
|
| 38 |
+
HANN16 = (0.5 * (1.0 - np.cos(2.0 * np.pi * np.arange(NFFT) / NFFT))).astype(np.float64)
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def _reflect_pad1d(x: np.ndarray, p: int) -> np.ndarray:
|
| 42 |
+
return np.pad(x, (p, p), mode="reflect")
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def f0_upsample(f0: np.ndarray) -> np.ndarray:
|
| 46 |
+
"""(1,T) Hz -> (1,T*480) 最近邻上采样。"""
|
| 47 |
+
return np.repeat(f0, UPSAMPLE_SCALE, axis=-1)
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def sine_source(f0_up: np.ndarray, phase: np.ndarray, noise: np.ndarray,
|
| 51 |
+
linear_w: np.ndarray, linear_b: np.ndarray) -> np.ndarray:
|
| 52 |
+
"""SineGen + SourceModuleHnNSF:f0_up(1,T*480) -> 源激励 s(1,1,T*480)。"""
|
| 53 |
+
F_mat = np.stack([f0_up * (h + 1) / SR for h in range(HARMONICS)], axis=1)
|
| 54 |
+
theta = 2.0 * math.pi * (np.cumsum(F_mat, axis=-1) % 1.0)
|
| 55 |
+
sine = SINE_AMP * np.sin(theta + phase)
|
| 56 |
+
uv = (f0_up > VOICED_THRESHOLD).astype(np.float64)
|
| 57 |
+
noise_amp = uv * NOISE_STD + (1.0 - uv) * SINE_AMP / 3.0
|
| 58 |
+
sine_wavs = sine * uv + noise_amp * noise
|
| 59 |
+
return np.tanh(np.einsum("oh,bht->bot", linear_w, sine_wavs) + linear_b.reshape(1, 1, -1))
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def source_stft(s: np.ndarray) -> np.ndarray:
|
| 63 |
+
"""源激励 (1,1,L) -> s_stft (1,18,F) real/imag 拼接。"""
|
| 64 |
+
x = s[0, 0]
|
| 65 |
+
xp = _reflect_pad1d(x, NFFT // 2)
|
| 66 |
+
frames = np.lib.stride_tricks.sliding_window_view(xp, NFFT)[::HOP] * HANN16
|
| 67 |
+
spec = np.fft.rfft(frames, n=NFFT, axis=1)
|
| 68 |
+
return np.concatenate([spec.real.T, spec.imag.T], axis=0)[None].astype(np.float32)
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def _istft(mag: np.ndarray, ph: np.ndarray) -> np.ndarray:
|
| 72 |
+
"""mag/ph (F,9) -> wav (L,):16 点 ISTFT + 去 center pad。"""
|
| 73 |
+
F = mag.shape[0]
|
| 74 |
+
frames = np.fft.irfft(mag * np.exp(1j * ph), n=NFFT, axis=1) * HANN16
|
| 75 |
+
n = (F - 1) * HOP + NFFT
|
| 76 |
+
idx = np.arange(F)[:, None] * HOP + np.arange(NFFT)[None, :]
|
| 77 |
+
out = np.bincount(idx.ravel(), weights=frames.ravel(), minlength=n)
|
| 78 |
+
wsum = np.bincount(idx.ravel(), weights=np.tile(HANN16 * HANN16, F), minlength=n)
|
| 79 |
+
y = np.divide(out, wsum, out=np.zeros_like(out), where=wsum > 1e-8)
|
| 80 |
+
return y[NFFT // 2:n - NFFT // 2]
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
class HiftVocoder:
|
| 84 |
+
"""NPU HiFT 声码器:f0 + decode 两个 axmodel(默认本地 AxEngineExecutionProvider)。"""
|
| 85 |
+
|
| 86 |
+
def __init__(self, f0_model: str | Path, decode_model: str | Path,
|
| 87 |
+
linear_w: np.ndarray, linear_b: np.ndarray, providers=None):
|
| 88 |
+
import axengine as axe
|
| 89 |
+
self.f0 = axe.InferenceSession(str(f0_model), providers=providers or ["AxEngineExecutionProvider"])
|
| 90 |
+
self.dec = axe.InferenceSession(str(decode_model), providers=providers or ["AxEngineExecutionProvider"])
|
| 91 |
+
self.linear_w = np.asarray(linear_w, dtype=np.float64).reshape(1, HARMONICS)
|
| 92 |
+
self.linear_b = np.asarray(linear_b, dtype=np.float64).reshape(-1)
|
| 93 |
+
self.out_names = [o.name for o in self.dec.get_outputs()]
|
| 94 |
+
|
| 95 |
+
def _run(self, sess, feeds):
|
| 96 |
+
return sess.run(None, {k: np.ascontiguousarray(v) for k, v in feeds.items()})
|
| 97 |
+
|
| 98 |
+
def synth(self, mel: np.ndarray, valid_frames: int) -> np.ndarray:
|
| 99 |
+
"""mel(1,80,T_valid) -> wav (T_valid*480,),尾部按 valid_frames 截断。"""
|
| 100 |
+
mel = np.asarray(mel, dtype=np.float32)
|
| 101 |
+
T = mel.shape[2]
|
| 102 |
+
mel_pad = np.full((1, 80, T_MEL), PAD_VAL, dtype=np.float32)
|
| 103 |
+
mel_pad[:, :, :T] = mel
|
| 104 |
+
rng = np.random.default_rng()
|
| 105 |
+
phase = rng.uniform(-np.pi, np.pi, size=(1, HARMONICS, 1))
|
| 106 |
+
phase[:, 0, :] = 0.0
|
| 107 |
+
noise = rng.standard_normal((1, HARMONICS, T_MEL * UPSAMPLE_SCALE))
|
| 108 |
+
|
| 109 |
+
f0 = self._run(self.f0, {"mel": mel_pad})[0].astype(np.float32)
|
| 110 |
+
s = sine_source(f0_upsample(f0), phase, noise, self.linear_w, self.linear_b)
|
| 111 |
+
s_stft = source_stft(s)
|
| 112 |
+
outs = self._run(self.dec, {"mel": mel_pad, "s_stft": s_stft})
|
| 113 |
+
if len(outs) == 2 and len(self.out_names) == 2:
|
| 114 |
+
mag_raw, ph_raw = outs[0][0], outs[1][0]
|
| 115 |
+
else:
|
| 116 |
+
raw = outs[0][0]
|
| 117 |
+
mag_raw, ph_raw = raw[:9], raw[9:]
|
| 118 |
+
mag = np.exp(np.asarray(mag_raw, dtype=np.float64))
|
| 119 |
+
ph = np.sin(np.asarray(ph_raw, dtype=np.float64))
|
| 120 |
+
wav = np.clip(_istft(mag.T, ph.T), -AUDIO_LIMIT, AUDIO_LIMIT)
|
| 121 |
+
return wav[: int(valid_frames) * UPSAMPLE_SCALE].astype(np.float32)
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
def wav_bytes(x: np.ndarray, sr: int = SR) -> bytes:
|
| 125 |
+
pcm = (np.clip(x, -1, 1) * 32767).astype(np.int16)
|
| 126 |
+
buf = io.BytesIO()
|
| 127 |
+
with wave.open(buf, "wb") as w:
|
| 128 |
+
w.setnchannels(1)
|
| 129 |
+
w.setsampwidth(2)
|
| 130 |
+
w.setframerate(sr)
|
| 131 |
+
w.writeframes(pcm.tobytes())
|
| 132 |
+
return buf.getvalue()
|