inoryQwQ commited on
Commit
8cc1eef
·
verified ·
1 Parent(s): 45f893b

HiFT vocoder: python/hift_vocoder.py

Browse files
Files changed (1) hide show
  1. 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()