"""RNNoise 信号处理链的 numpy 移植(1:1 对应 origin/rnnoise/src 的 C 实现)。 覆盖 denoise.c / pitch.c / kiss_fft / celt_lpc / rnnoise_tables 的浮点路径: PCM 帧(480) -> biquad HP -> FFT(960) -> 32 波段能量/DCT + pitch -> 65 维特征 gains/vad -> gain 平滑/限幅 -> pitch filter -> 频谱合成 -> 480 样本 约定:输入音频为 16-bit PCM 等价 float(±32768 量级,不做 /32768 归一化, 与官方 rnnoise demo 一致)。所有运算尽量 float32,与 C float 语义对齐。 """ from __future__ import annotations import numpy as np FRAME_SIZE = 480 WINDOW_SIZE = 2 * FRAME_SIZE FREQ_SIZE = FRAME_SIZE + 1 NB_BANDS = 32 NB_FEATURES = 2 * NB_BANDS + 1 PITCH_MIN_PERIOD = 60 PITCH_MAX_PERIOD = 768 PITCH_FRAME_SIZE = 960 PITCH_BUF_SIZE = PITCH_MAX_PERIOD + PITCH_FRAME_SIZE EBAND20MS = np.array( [0, 2, 4, 6, 8, 10, 12, 15, 18, 21, 24, 28, 32, 36, 41, 47, 53, 60, 68, 77, 87, 98, 110, 124, 140, 157, 176, 198, 223, 251, 282, 317, 356, 400], dtype=np.int32) SECOND_CHECK = np.array( [0, 0, 3, 2, 3, 2, 5, 2, 3, 2, 3, 2, 5, 2, 3, 2], dtype=np.int32) def f32(x): return np.asarray(x, dtype=np.float32) def make_half_window() -> np.ndarray: """rnn_half_window(dump_rnnoise_tables.c 公式)。""" i = (np.arange(FRAME_SIZE, dtype=np.float64) + 0.5) s = np.sin(0.5 * np.pi * i / FRAME_SIZE) w = np.sin(0.5 * np.pi * s * s) return f32(w) HALF_WINDOW = make_half_window() def make_dct_table() -> np.ndarray: """rnn_dct_table:行 i、列 j 为 cos((i+.5)*j*pi/32),j==0 乘 sqrt(.5)。""" i = np.arange(NB_BANDS, dtype=np.float64)[:, None] j = np.arange(NB_BANDS, dtype=np.float64)[None, :] t = np.cos((i + 0.5) * j * np.pi / NB_BANDS) t[:, 0] *= np.sqrt(0.5) return f32(t) DCT_TABLE = make_dct_table() def fft(x: np.ndarray) -> np.ndarray: """forward FFT:kiss_fft 无缩放正变换(960 点),返回复数组。""" return np.fft.fft(f32(x).astype(np.float64)) def forward_transform(x: np.ndarray) -> np.ndarray: """x[960] -> X[481](kiss_fft 该 fork 正变换带 1/N 缩放)。""" y = fft(x) / WINDOW_SIZE return y[:FREQ_SIZE] def inverse_transform(y: np.ndarray) -> np.ndarray: """X[481] -> x[960](Hermitian 镜像 + 正变换实现逆变换)。""" y = np.asarray(y) n = WINDOW_SIZE x = np.zeros(n, dtype=np.complex128) x[:FREQ_SIZE] = y x[FREQ_SIZE:] = np.conj(y[1:(n - FREQ_SIZE) + 1][::-1]) out = np.fft.fft(x) res = np.empty(n, dtype=np.float64) res[0] = out[0].real res[1:] = out[n:0:-1].real return res def apply_window(x: np.ndarray) -> np.ndarray: x = x.copy() x[:FRAME_SIZE] *= HALF_WINDOW x[WINDOW_SIZE - 1:FRAME_SIZE - 1:-1] *= HALF_WINDOW return x def compute_band_energy(X: np.ndarray) -> np.ndarray: """X[481] 复数 -> bandE[32]。""" X = np.asarray(X) power = (X.real ** 2 + X.imag ** 2).astype(np.float64) # C 实现用 sum[0..NB_BANDS+1],sum[1] 和 sum[NB_BANDS] 修正后取 sum[1..NB_BANDS] s = np.zeros(NB_BANDS + 2) for i in range(NB_BANDS): b0, b1 = int(EBAND20MS[i]), int(EBAND20MS[i + 1]) frac = (np.arange(b1 - b0, dtype=np.float64)) / (b1 - b0) p = power[b0:b1] s[i] += np.sum((1 - frac) * p) s[i + 1] += np.sum(frac * p) s[1] = (s[0] + s[1]) * 2.0 / 3.0 s[NB_BANDS] = (s[NB_BANDS] + s[NB_BANDS + 1]) * 2.0 / 3.0 return f32(s[1:NB_BANDS + 1]) def compute_band_corr(X: np.ndarray, P: np.ndarray) -> np.ndarray: X = np.asarray(X) P = np.asarray(P) prod = (X.real * P.real + X.imag * P.imag).astype(np.float64) s = np.zeros(NB_BANDS + 2) for i in range(NB_BANDS): b0, b1 = int(EBAND20MS[i]), int(EBAND20MS[i + 1]) frac = (np.arange(b1 - b0, dtype=np.float64)) / (b1 - b0) p = prod[b0:b1] s[i] += np.sum((1 - frac) * p) s[i + 1] += np.sum(frac * p) s[1] = (s[0] + s[1]) * 2.0 / 3.0 s[NB_BANDS] = (s[NB_BANDS] + s[NB_BANDS + 1]) * 2.0 / 3.0 return f32(s[1:NB_BANDS + 1]) def dct(inp: np.ndarray) -> np.ndarray: """out[i] = sqrt(2/22) * sum_j in[j] * table[j, i]。""" inp = f32(inp) return f32(inp @ DCT_TABLE * np.sqrt(2.0 / 22.0)) def rnn_biquad(x: np.ndarray, mem: np.ndarray, b, a) -> tuple[np.ndarray, np.ndarray]: x = f32(x) y = np.empty_like(x) mem = mem.copy() for i in range(x.size): xi = float(x[i]) yi = xi + mem[0] mem[0] = mem[1] + (b[0] * xi - a[0] * yi) mem[1] = b[1] * xi - a[1] * yi y[i] = yi return y, mem def rnn_autocorr(x: np.ndarray, lag: int) -> np.ndarray: """rnn_autocorr(overlap=0):ac[k] = sum_i x[i]*x[i+k]。""" n = x.size x = f32(x) ac = np.zeros(lag + 1, dtype=np.float64) for k in range(lag + 1): ac[k] = np.dot(x[:n - k].astype(np.float64), x[k:].astype(np.float64)) return ac def rnn_lpc(ac: np.ndarray, p: int) -> np.ndarray: ac = np.asarray(ac, dtype=np.float64) lpc = np.zeros(p, dtype=np.float64) error = ac[0] if ac[0] != 0: for i in range(p): rr = sum(lpc[j] * ac[i - j] for j in range(i)) + ac[i + 1] r = -rr / error lpc[i] = r for j in range((i + 1) // 2): tmp1 = lpc[j] tmp2 = lpc[i - 1 - j] lpc[j] = tmp1 + r * tmp2 lpc[i - 1 - j] = tmp2 + r * tmp1 error = error - r * r * error if error < 0.001 * ac[0]: break return f32(lpc) def celt_fir5(x: np.ndarray, num: np.ndarray) -> np.ndarray: """5 抽头 FIR:y[i] = x[i] + sum_j num[j]*x[i-1-j]。""" x = f32(x) num = f32(num) n = x.size y = np.empty(n, dtype=np.float32) mem = np.zeros(5, dtype=np.float64) for i in range(n): xi = float(x[i]) s = xi + float(num[0]) * mem[0] + float(num[1]) * mem[1] + \ float(num[2]) * mem[2] + float(num[3]) * mem[3] + float(num[4]) * mem[4] mem[4] = mem[3] mem[3] = mem[2] mem[2] = mem[1] mem[1] = mem[0] mem[0] = xi y[i] = s return y def rnn_pitch_downsample(x: np.ndarray) -> np.ndarray: """1728 采样 -> 864 低通(2:1 抽取 + LPC 预加重)。""" x = f32(x) length = x.size half = length // 2 x_lp = np.empty(half, dtype=np.float32) x_lp[0] = 0.5 * (0.5 * float(x[1]) + float(x[0])) for i in range(1, half): x_lp[i] = 0.5 * (0.5 * (float(x[2 * i - 1]) + float(x[2 * i + 1])) + float(x[2 * i])) ac = rnn_autocorr(x_lp, 4) ac[0] *= 1.0001 for i in range(1, 5): ac[i] -= ac[i] * (0.008 * i) * (0.008 * i) lpc = rnn_lpc(ac, 4) tmp = 1.0 for i in range(4): tmp *= 0.9 lpc[i] = lpc[i] * tmp lpc2 = np.zeros(5, dtype=np.float32) c1 = 0.8 lpc2[0] = lpc[0] + 0.8 lpc2[1] = lpc[1] + c1 * lpc[0] lpc2[2] = lpc[2] + c1 * lpc[1] lpc2[3] = lpc[3] + c1 * lpc[2] lpc2[4] = c1 * lpc[3] return celt_fir5(x_lp, lpc2) def _find_best_pitch(xcorr: np.ndarray, y: np.ndarray, length: int, max_pitch: int): """find_best_pitch(浮点路径),返回 best_pitch[2]。""" xcorr = np.asarray(xcorr, dtype=np.float64) y = np.asarray(y, dtype=np.float64) Syy = 1.0 + np.dot(y[:length], y[:length]) best_num = [-1.0, -1.0] best_den = [0.0, 0.0] best_pitch = [0, 1] for i in range(max_pitch): if xcorr[i] > 0: xcorr16 = xcorr[i] * 1e-12 num = xcorr16 * xcorr16 if num * best_den[1] > best_num[1] * Syy: if num * best_den[0] > best_num[0] * Syy: best_num[1] = best_num[0] best_den[1] = best_den[0] best_pitch[1] = best_pitch[0] best_num[0] = num best_den[0] = Syy best_pitch[0] = i else: best_num[1] = num best_den[1] = Syy best_pitch[1] = i Syy += y[i + length] * y[i + length] - y[i] * y[i] Syy = max(1.0, Syy) return best_pitch def rnn_pitch_search(x_lp: np.ndarray, y: np.ndarray, length: int, max_pitch: int) -> int: """rnn_pitch_search 返回 pitch(粗搜 + 细搜 + 伪插值)。""" x_lp = f32(x_lp) y = f32(y) lag = length + max_pitch len4 = length >> 2 x_lp4 = x_lp[0:length:2][:len4] y_lp4 = y[0:lag:2][:(lag >> 2)] xcorr = np.array([np.dot(x_lp4.astype(np.float64), y_lp4[i:i + len4].astype(np.float64)) for i in range(max_pitch >> 2)], dtype=np.float64) best_pitch = _find_best_pitch(xcorr, y_lp4, len4, max_pitch >> 2) half = max_pitch >> 1 xcorr2 = np.zeros(half, dtype=np.float64) x_lp64 = x_lp.astype(np.float64) y64 = y.astype(np.float64) for i in range(half): if abs(i - 2 * best_pitch[0]) > 2 and abs(i - 2 * best_pitch[1]) > 2: continue s = np.dot(x_lp64[:length >> 1], y64[i:i + (length >> 1)]) xcorr2[i] = max(-1.0, s) best_pitch = _find_best_pitch(xcorr2, y64, length >> 1, half) bp = best_pitch[0] if 0 < bp < half - 1: a, b, c = xcorr2[bp - 1], xcorr2[bp], xcorr2[bp + 1] if (c - a) > 0.7 * (b - a): offset = 1 elif (a - c) > 0.7 * (b - c): offset = -1 else: offset = 0 else: offset = 0 return 2 * bp - offset def _compute_pitch_gain(xy, xx, yy): return xy / np.sqrt(1.0 + xx * yy) def rnn_remove_doubling(x: np.ndarray, maxperiod: int, minperiod: int, n: int, t0_in: int, prev_period: int, prev_gain: float): """rnn_remove_doubling:返回 (pg, T0)。""" x = f32(x) minperiod0 = minperiod maxperiod //= 2 minperiod //= 2 t0 = t0_in // 2 prev_period //= 2 n //= 2 x = x[maxperiod:] if t0 >= maxperiod: t0 = maxperiod - 1 T = T0 = t0 x64 = x.astype(np.float64) xx = np.dot(x64[:n], x64[:n]) xy = np.dot(x64[:n], x64[T0:T0 + n]) yy_lookup = np.zeros(maxperiod + 1, dtype=np.float64) yy_lookup[0] = xx yy = xx for i in range(1, maxperiod + 1): yy = yy + float(x64[-i]) ** 2 - float(x64[n - i]) ** 2 yy_lookup[i] = max(0.0, yy) yy = yy_lookup[T0] best_xy, best_yy = xy, yy g = g0 = _compute_pitch_gain(xy, xx, yy) for k in range(2, 16): T1 = (2 * T0 + k) // (2 * k) if T1 < minperiod: break if k == 2: T1b = T0 if T1 + T0 > maxperiod else T0 + T1 else: T1b = (2 * int(SECOND_CHECK[k]) * T0 + k) // (2 * k) xy2 = np.dot(x64[:n], x64[T1b:T1b + n]) xy = 0.5 * (xy + xy2) yy = 0.5 * (yy_lookup[T1] + yy_lookup[T1b]) g1 = _compute_pitch_gain(xy, xx, yy) if abs(T1 - prev_period) <= 1: cont = prev_gain elif abs(T1 - prev_period) <= 2 and 5 * k * k < T0: cont = 0.5 * prev_gain else: cont = 0.0 thresh = max(0.3, 0.7 * g0 - cont) if T1 < 3 * minperiod: thresh = max(0.4, 0.85 * g0 - cont) elif T1 < 2 * minperiod: thresh = max(0.5, 0.9 * g0 - cont) if g1 > thresh: best_xy, best_yy = xy, yy T = T1 g = g1 best_xy = max(0.0, best_xy) pg = 1.0 if best_yy <= best_xy else best_xy / (best_yy + 1.0) xcorr = np.zeros(3) for k in range(3): lag = T + k - 1 xcorr[k] = np.dot(x64[:n], x64[lag:lag + n]) if (xcorr[2] - xcorr[0]) > 0.7 * (xcorr[1] - xcorr[0]): offset = 1 elif (xcorr[0] - xcorr[2]) > 0.7 * (xcorr[1] - xcorr[2]): offset = -1 else: offset = 0 if pg > g: pg = g T0 = 2 * T + offset if T0 < minperiod0: T0 = minperiod0 return float(pg), int(T0) def interp_band_gain(bandE: np.ndarray) -> np.ndarray: bandE = np.asarray(bandE, dtype=np.float64) g = np.zeros(FREQ_SIZE) for i in range(1, NB_BANDS): b0, b1 = int(EBAND20MS[i]), int(EBAND20MS[i + 1]) frac = (np.arange(b1 - b0, dtype=np.float64)) / (b1 - b0) g[b0:b1] = (1 - frac) * bandE[i - 1] + frac * bandE[i] g[:int(EBAND20MS[1])] = bandE[0] g[int(EBAND20MS[NB_BANDS]):int(EBAND20MS[NB_BANDS + 1])] = bandE[NB_BANDS - 1] return f32(g) def rnn_pitch_filter(X, P, Ex, Ep, Exp, g): """rnn_pitch_filter:就地修改 X[481]。""" X = np.asarray(X, dtype=np.complex128) P = np.asarray(P, dtype=np.complex128) Ex = np.asarray(Ex, dtype=np.float64) Ep = np.asarray(Ep, dtype=np.float64) Exp = np.asarray(Exp, dtype=np.float64) g = np.asarray(g, dtype=np.float64) r = np.zeros(NB_BANDS) for i in range(NB_BANDS): if Exp[i] > g[i]: r[i] = 1.0 else: r[i] = (Exp[i] ** 2) * (1 - g[i] ** 2) / (0.001 + g[i] ** 2 * (1 - Exp[i] ** 2)) r[i] = np.sqrt(min(1.0, max(0.0, r[i]))) r[i] *= np.sqrt(Ex[i] / (1e-8 + Ep[i])) rf = interp_band_gain(r) X += rf * P newE = compute_band_energy(X) norm = np.sqrt(Ex / (1e-8 + newE)) normf = interp_band_gain(norm) X *= normf return X class RNNoiseState: """对应 DenoiseState:维护全部帧间状态。""" def __init__(self): self.analysis_mem = np.zeros(FRAME_SIZE, dtype=np.float32) self.synthesis_mem = np.zeros(FRAME_SIZE, dtype=np.float32) self.pitch_buf = np.zeros(PITCH_BUF_SIZE, dtype=np.float32) self.last_gain = 0.0 self.last_period = 0 self.mem_hp_x = np.zeros(2, dtype=np.float32) self.lastg = np.zeros(NB_BANDS, dtype=np.float32) self.delayed_X = np.zeros(FREQ_SIZE, dtype=np.complex128) self.delayed_P = np.zeros(FREQ_SIZE, dtype=np.complex128) self.delayed_Ex = np.zeros(NB_BANDS, dtype=np.float32) self.delayed_Ep = np.zeros(NB_BANDS, dtype=np.float32) self.delayed_Exp = np.zeros(NB_BANDS, dtype=np.float32) def rnn_frame_analysis(st: RNNoiseState, inp: np.ndarray): """返回 (X[481], Ex[32])。""" x = np.concatenate([st.analysis_mem, f32(inp)]) st.analysis_mem = f32(inp).copy() x = apply_window(x) X = forward_transform(x) Ex = compute_band_energy(X) return X, Ex def compute_frame_features(st: RNNoiseState, inp: np.ndarray): """返回 (silence, X, P, Ex, Ep, Exp, features)。features 为空时清为 0。""" X, Ex = rnn_frame_analysis(st, inp) st.pitch_buf = np.concatenate([st.pitch_buf[FRAME_SIZE:], f32(inp)]) pre = st.pitch_buf pitch_buf_lp = rnn_pitch_downsample(pre) x_lp = pitch_buf_lp[(PITCH_MAX_PERIOD >> 1):] y = pitch_buf_lp pitch_index = rnn_pitch_search( x_lp, y, PITCH_FRAME_SIZE, PITCH_MAX_PERIOD - 3 * PITCH_MIN_PERIOD) pitch_index = PITCH_MAX_PERIOD - pitch_index gain, pitch_index = rnn_remove_doubling( st.pitch_buf, PITCH_MAX_PERIOD, PITCH_MIN_PERIOD, PITCH_FRAME_SIZE, pitch_index, st.last_period, st.last_gain) st.last_period = pitch_index st.last_gain = gain p = np.array([ st.pitch_buf[PITCH_BUF_SIZE - WINDOW_SIZE - pitch_index + i] for i in range(WINDOW_SIZE)], dtype=np.float32) p = apply_window(p) P = forward_transform(p) Ep = compute_band_energy(P) Exp = compute_band_corr(X, P) Exp = Exp / np.sqrt(0.001 + Ex * Ep) features = np.zeros(NB_FEATURES, dtype=np.float32) features[NB_BANDS:2 * NB_BANDS] = dct(Exp) features[2 * NB_BANDS] = 0.01 * (pitch_index - 300) logMax = -2.0 follow = -2.0 Ly = np.zeros(NB_BANDS) E = 0.0 for i in range(NB_BANDS): Ly[i] = np.log10(1e-2 + float(Ex[i])) Ly[i] = max(logMax - 7, max(follow - 1.5, Ly[i])) logMax = max(logMax, Ly[i]) follow = max(follow - 1.5, Ly[i]) E += float(Ex[i]) if E < 0.04: features[:] = 0.0 return 1, X, P, Ex, Ep, Exp, features features[:NB_BANDS] = dct(Ly) features[0] -= 12.0 features[1] -= 4.0 return 0, X, P, Ex, Ep, Exp, features def frame_synthesis(st: RNNoiseState, y) -> np.ndarray: x = inverse_transform(y) x = apply_window(x) out = f32(x[:FRAME_SIZE] + st.synthesis_mem) st.synthesis_mem = f32(x[FRAME_SIZE:]) return out def analyze_frame(st: RNNoiseState, inp: np.ndarray) -> dict: """C 端 rnnoise_process_frame 前半:biquad + 特征分析。 返回 dict(silence, features, X, P, Ex, Ep, Exp)。会推进 st 的 analysis_mem / pitch_buf / last_period / last_gain 等 DSP 状态。 """ a_hp = np.array([-1.99599, 0.99600], dtype=np.float32) b_hp = np.array([-2, 1], dtype=np.float32) x, st.mem_hp_x = rnn_biquad(f32(inp), st.mem_hp_x, b_hp, a_hp) silence, X, P, Ex, Ep, Exp, features = compute_frame_features(st, x) return {"silence": silence, "features": features, "X": X, "P": P, "Ex": Ex, "Ep": Ep, "Exp": Exp} def synthesize_frame(st: RNNoiseState, ana: dict, gains, vad): """C 端 rnnoise_process_frame 后半:pitch filter + 增益 + 频谱合成。 gains 为 None 表示静音帧(跳过模型相关部分,vad 记 0)。 返回 (out_frame, vad_prob)。需在 analyze_frame 之后调用。 """ silence = ana["silence"] X, P, Ex, Ep, Exp = ana["X"], ana["P"], ana["Ex"], ana["Ep"], ana["Exp"] if not silence and gains is not None: gains = f32(gains).reshape(-1) st.delayed_X = rnn_pitch_filter( st.delayed_X, st.delayed_P, st.delayed_Ex, st.delayed_Ep, st.delayed_Exp, gains) for i in range(NB_BANDS): alpha = 0.6 gains[i] = max(gains[i], alpha * st.lastg[i]) st.lastg[i] = min(1.0, gains[i] * (st.delayed_Ex[i] + 1e-3) / (Ex[i] + 1e-3)) gf = interp_band_gain(gains) st.delayed_X = st.delayed_X * gf out = frame_synthesis(st, st.delayed_X) st.delayed_X = X st.delayed_P = P st.delayed_Ex = Ex st.delayed_Ep = Ep st.delayed_Exp = Exp vad_prob = 0.0 if (silence or gains is None) else float(np.asarray(vad).reshape(-1)[0]) return out, vad_prob def process_frame(st: RNNoiseState, inp: np.ndarray, gains, vad): """完整一帧(C 端 rnnoise_process_frame 等价),返回 (out, vad_prob)。""" ana = analyze_frame(st, inp) if ana["silence"]: return synthesize_frame(st, ana, None, 0.0) return synthesize_frame(st, ana, gains, vad) def features_from_pcm(pcm: np.ndarray) -> np.ndarray: """批量:48k f32 PCM(16-bit 域)-> (T, 65) 特征矩阵(含 silence 清零逻辑)。""" st = RNNoiseState() feats = [] pcm = f32(pcm) for t in range(0, pcm.size - FRAME_SIZE + 1, FRAME_SIZE): frame = pcm[t:t + FRAME_SIZE] a_hp = np.array([-1.99599, 0.99600], dtype=np.float32) b_hp = np.array([-2, 1], dtype=np.float32) x, st.mem_hp_x = rnn_biquad(frame, st.mem_hp_x, b_hp, a_hp) _, _, _, _, _, _, features = compute_frame_features(st, x) feats.append(features.copy()) return np.stack(feats)