inoryQwQ commited on
Commit
819e690
·
verified ·
1 Parent(s): 28c1505

Upload folder using huggingface_hub

Browse files
.gitattributes CHANGED
@@ -34,3 +34,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
  *.axmodel filter=lfs diff=lfs merge=lfs -text
 
 
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
  *.axmodel filter=lfs diff=lfs merge=lfs -text
37
+ python/sample_speech.pcm filter=lfs diff=lfs merge=lfs -text
cpp/src/rnnoise_ax.cpp ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // RNNoise AX SDK:用 AX Engine 替换原版 compute_rnn(网络推理),
2
+ // 其余信号处理(biquad/FFT/pitch/合成)沿用原版 C 实现。
3
+ #include "rnnoise_ax.hpp"
4
+
5
+ #include "model_runner.hpp"
6
+
7
+ #include <cstring>
8
+ #include <mutex>
9
+ #include <stdexcept>
10
+ #include <vector>
11
+
12
+ extern "C" {
13
+ #include "denoise.h"
14
+ #include "rnn.h"
15
+ }
16
+
17
+ namespace {
18
+
19
+ // 单实例模型 runner(compute_rnn 无状态上下文可用,采用全局注册方式;
20
+ // 同一时刻只允许一个 RNNoiseAX 实例执行推理)。
21
+ std::mutex g_runner_mu;
22
+ ModelRunner* g_ax_runner = nullptr;
23
+
24
+ extern "C" void rnnoise_ax_set_runner(ModelRunner* runner) {
25
+ std::lock_guard<std::mutex> lk(g_runner_mu);
26
+ g_ax_runner = runner;
27
+ }
28
+
29
+ extern "C" void compute_rnn(const RNNoise* model, RNNState* rnn,
30
+ float* gains, float* vad,
31
+ const float* input, int arch) {
32
+ (void)model;
33
+ (void)arch;
34
+ ModelRunner* runner = nullptr;
35
+ {
36
+ std::lock_guard<std::mutex> lk(g_runner_mu);
37
+ runner = g_ax_runner;
38
+ }
39
+ if (runner == nullptr) {
40
+ throw std::runtime_error("compute_rnn: AX runner 未初始化");
41
+ }
42
+
43
+ std::vector<std::vector<float>> feeds = {
44
+ std::vector<float>(input, input + NB_FEATURES),
45
+ std::vector<float>(rnn->conv1_state,
46
+ rnn->conv1_state + CONV1_STATE_SIZE),
47
+ std::vector<float>(rnn->conv2_state,
48
+ rnn->conv2_state + CONV2_STATE_SIZE),
49
+ std::vector<float>(rnn->gru1_state,
50
+ rnn->gru1_state + GRU1_OUT_SIZE),
51
+ std::vector<float>(rnn->gru2_state,
52
+ rnn->gru2_state + GRU2_OUT_SIZE),
53
+ std::vector<float>(rnn->gru3_state,
54
+ rnn->gru3_state + GRU3_OUT_SIZE),
55
+ };
56
+ std::vector<std::vector<float>> outs = runner->Run(feeds);
57
+
58
+ std::memcpy(gains, outs[0].data(), NB_BANDS * sizeof(float));
59
+ *vad = outs[1][0];
60
+ std::memcpy(rnn->conv1_state, outs[2].data(),
61
+ CONV1_STATE_SIZE * sizeof(float));
62
+ std::memcpy(rnn->conv2_state, outs[3].data(),
63
+ CONV2_STATE_SIZE * sizeof(float));
64
+ std::memcpy(rnn->gru1_state, outs[4].data(),
65
+ GRU1_OUT_SIZE * sizeof(float));
66
+ std::memcpy(rnn->gru2_state, outs[5].data(),
67
+ GRU2_OUT_SIZE * sizeof(float));
68
+ std::memcpy(rnn->gru3_state, outs[6].data(),
69
+ GRU3_OUT_SIZE * sizeof(float));
70
+ }
71
+
72
+ } // namespace
73
+
74
+ struct RNNoiseAX::Impl {
75
+ std::unique_ptr<ModelRunner> runner;
76
+ DenoiseState* state = nullptr;
77
+
78
+ explicit Impl(const std::string& model_path)
79
+ : runner(new ModelRunner(model_path)),
80
+ state(rnnoise_create(nullptr)) {
81
+ if (state == nullptr) {
82
+ throw std::runtime_error("rnnoise_create 失败");
83
+ }
84
+ rnnoise_ax_set_runner(runner.get());
85
+ }
86
+
87
+ ~Impl() {
88
+ rnnoise_ax_set_runner(nullptr);
89
+ if (state) {
90
+ rnnoise_destroy(state);
91
+ }
92
+ }
93
+ };
94
+
95
+ RNNoiseAX::RNNoiseAX(const std::string& model_path)
96
+ : impl_(new Impl(model_path)) {}
97
+
98
+ RNNoiseAX::~RNNoiseAX() = default;
99
+
100
+ void RNNoiseAX::Reset() {
101
+ if (!impl_) {
102
+ return;
103
+ }
104
+ rnnoise_ax_set_runner(nullptr);
105
+ rnnoise_destroy(impl_->state);
106
+ impl_->state = rnnoise_create(nullptr);
107
+ if (impl_->state == nullptr) {
108
+ throw std::runtime_error("rnnoise_create 失败");
109
+ }
110
+ rnnoise_ax_set_runner(impl_->runner.get());
111
+ }
112
+
113
+ float RNNoiseAX::ProcessFrame(float* out, const float* in) {
114
+ return rnnoise_process_frame(impl_->state, out, in);
115
+ }
models/model.axmodel ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e758f3120eff228d8e15d8cf035d336bda94f3920b3ab60e947f74908aa6d16e
3
+ size 3393952
models/model_meta.json ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_name": "rnnoise-ax650",
3
+ "framework": "pytorch->onnx",
4
+ "task": "noise_suppression",
5
+ "route": "general",
6
+ "opset": 13,
7
+ "inputs": [
8
+ {
9
+ "name": "features",
10
+ "shape": [
11
+ 1,
12
+ 65
13
+ ],
14
+ "dtype": "float32",
15
+ "layout": "NC"
16
+ },
17
+ {
18
+ "name": "conv1_mem",
19
+ "shape": [
20
+ 1,
21
+ 130
22
+ ],
23
+ "dtype": "float32",
24
+ "layout": "NC"
25
+ },
26
+ {
27
+ "name": "conv2_mem",
28
+ "shape": [
29
+ 1,
30
+ 256
31
+ ],
32
+ "dtype": "float32",
33
+ "layout": "NC"
34
+ },
35
+ {
36
+ "name": "gru1_s",
37
+ "shape": [
38
+ 1,
39
+ 384
40
+ ],
41
+ "dtype": "float32",
42
+ "layout": "NC"
43
+ },
44
+ {
45
+ "name": "gru2_s",
46
+ "shape": [
47
+ 1,
48
+ 384
49
+ ],
50
+ "dtype": "float32",
51
+ "layout": "NC"
52
+ },
53
+ {
54
+ "name": "gru3_s",
55
+ "shape": [
56
+ 1,
57
+ 384
58
+ ],
59
+ "dtype": "float32",
60
+ "layout": "NC"
61
+ }
62
+ ],
63
+ "outputs": [
64
+ {
65
+ "name": "gains",
66
+ "shape": [
67
+ 1,
68
+ 32
69
+ ],
70
+ "dtype": "float32",
71
+ "layout": "NC"
72
+ },
73
+ {
74
+ "name": "vad",
75
+ "shape": [
76
+ 1,
77
+ 1
78
+ ],
79
+ "dtype": "float32",
80
+ "layout": "NC"
81
+ },
82
+ {
83
+ "name": "conv1_mem_new",
84
+ "shape": [
85
+ 1,
86
+ 130
87
+ ],
88
+ "dtype": "float32",
89
+ "layout": "NC"
90
+ },
91
+ {
92
+ "name": "conv2_mem_new",
93
+ "shape": [
94
+ 1,
95
+ 256
96
+ ],
97
+ "dtype": "float32",
98
+ "layout": "NC"
99
+ },
100
+ {
101
+ "name": "gru1_s_new",
102
+ "shape": [
103
+ 1,
104
+ 384
105
+ ],
106
+ "dtype": "float32",
107
+ "layout": "NC"
108
+ },
109
+ {
110
+ "name": "gru2_s_new",
111
+ "shape": [
112
+ 1,
113
+ 384
114
+ ],
115
+ "dtype": "float32",
116
+ "layout": "NC"
117
+ },
118
+ {
119
+ "name": "gru3_s_new",
120
+ "shape": [
121
+ 1,
122
+ 384
123
+ ],
124
+ "dtype": "float32",
125
+ "layout": "NC"
126
+ }
127
+ ],
128
+ "stateful": true,
129
+ "frame_size": 480,
130
+ "sample_rate": 48000,
131
+ "preprocess": "48k PCM 帧(480) -> biquad HP -> FFT(960) -> 32 波段能量/DCT + pitch 特征 -> 65 维特征",
132
+ "input_domain": "float 等价 16-bit PCM(±32768 量级,不做归一化,与官方 demo 一致)",
133
+ "postprocess": "32 gains + vad -> gain 平滑/限幅 -> pitch filter -> 频谱合成 -> 480 样本",
134
+ "sdk_interface": {
135
+ "entry": "rnnoise_process_frame",
136
+ "args": "float32 帧(480) + 状态(内部维护)",
137
+ "returns": "float32 去噪帧 + vad"
138
+ },
139
+ "license": "isc"
140
+ }
python/demo.py ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """RNNoise AX650 降噪一键演示(复制即用)。
2
+
3
+ 板端:bash setup.sh && bash run.sh(自带 sample_speech.pcm 演示样本)。
4
+ 非 AX 主机:提示在板端运行后正常退出。
5
+ """
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ ROOT = Path(__file__).resolve().parents[1]
10
+
11
+ try:
12
+ import axengine # noqa: F401
13
+ AX_AVAILABLE = True
14
+ except Exception:
15
+ AX_AVAILABLE = False
16
+
17
+
18
+ def main() -> None:
19
+ if not AX_AVAILABLE:
20
+ print("当前主机没有 AX 芯片(pyaxengine 不可用),无法运行 NPU 推理。")
21
+ print("本交付包为 NPU 专用版,请在 AX650/AX630 板端执行:")
22
+ print(" bash setup.sh && bash run.sh")
23
+ return
24
+
25
+ import numpy as np
26
+ from rnnoise_ax650_sdk import RNNoiseDenoiser, dsp
27
+
28
+ model = ROOT / "models" / "model.axmodel"
29
+ sample = ROOT / "python" / "sample_speech.pcm"
30
+ denoiser = RNNoiseDenoiser(str(model))
31
+ pcm = np.fromfile(sample, dtype=np.float32)
32
+ out, vads = denoiser.process(pcm)
33
+ out_dir = ROOT / "output"
34
+ out_dir.mkdir(exist_ok=True)
35
+ out.astype(np.float32).tofile(out_dir / "out.pcm")
36
+ np.save(out_dir / "vad.npy", vads)
37
+ print(f"模型加载成功:{model}")
38
+ print(f"帧数: {vads.size}(每帧 10ms)")
39
+ print(f"语音存在比例: {float((vads > 0.5).mean()):.2f}")
40
+ print(f"输出已保存: {out_dir / 'out.pcm'}")
41
+
42
+
43
+ if __name__ == "__main__":
44
+ main()
python/requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ numpy
2
+ pyaxengine @ git+https://gh-proxy.com/https://github.com/AXERA-TECH/pyaxengine.git
python/rnnoise_ax650_sdk/README.md ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # rnnoise-ax650 Python SDK
2
+
3
+ 48kHz 单声道实时降噪(RNNoise,AX650 NPU3 编译)。
4
+
5
+ - 输入:480 采样/帧 float32(16-bit PCM 等价域 ±32768,不做归一化)
6
+ - 输出:去噪帧(480) + vad
7
+ - 模型:6 输入(features + 5 个状态)/ 7 输出(gains/vad + 5 个新状态),
8
+ 逐帧状态化推理,状态在 `RNNoiseDenoiser` 实例内维护
9
+ - 前后处理:numpy 移植原版 rnnoise C 管线(biquad/FFT/带能量 DCT/pitch/
10
+ 频谱合成),对照 `c_ref` 逐帧验证:特征 cosine≥0.995、去噪输出 cosine≥0.995
11
+
12
+ ```python
13
+ from rnnoise_ax650_sdk import RNNoiseDenoiser
14
+ import numpy as np
15
+
16
+ denoiser = RNNoiseDenoiser("model.axmodel")
17
+ frame = np.random.randn(480).astype(np.float32) * 3000 # 16-bit 域
18
+ out, vad = denoiser.process_frame(frame)
19
+ ```
20
+
21
+ 开发版含 onnxruntime CPU 回退;发布版(NPU 专用)仅依赖 numpy + pyaxengine。
python/rnnoise_ax650_sdk/__init__.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ from .inference import RNNoiseDenoiser
2
+ from . import dsp, preprocess, postprocess
3
+
4
+ __all__ = ["RNNoiseDenoiser", "dsp", "preprocess", "postprocess"]
python/rnnoise_ax650_sdk/dsp.py ADDED
@@ -0,0 +1,547 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """RNNoise 信号处理链的 numpy 移植(1:1 对应 origin/rnnoise/src 的 C 实现)。
2
+
3
+ 覆盖 denoise.c / pitch.c / kiss_fft / celt_lpc / rnnoise_tables 的浮点路径:
4
+ PCM 帧(480) -> biquad HP -> FFT(960) -> 32 波段能量/DCT + pitch -> 65 维特征
5
+ gains/vad -> gain 平滑/限幅 -> pitch filter -> 频谱合成 -> 480 样本
6
+
7
+ 约定:输入音频为 16-bit PCM 等价 float(±32768 量级,不做 /32768 归一化,
8
+ 与官方 rnnoise demo 一致)。所有运算尽量 float32,与 C float 语义对齐。
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import numpy as np
13
+
14
+ FRAME_SIZE = 480
15
+ WINDOW_SIZE = 2 * FRAME_SIZE
16
+ FREQ_SIZE = FRAME_SIZE + 1
17
+ NB_BANDS = 32
18
+ NB_FEATURES = 2 * NB_BANDS + 1
19
+
20
+ PITCH_MIN_PERIOD = 60
21
+ PITCH_MAX_PERIOD = 768
22
+ PITCH_FRAME_SIZE = 960
23
+ PITCH_BUF_SIZE = PITCH_MAX_PERIOD + PITCH_FRAME_SIZE
24
+
25
+ EBAND20MS = np.array(
26
+ [0, 2, 4, 6, 8, 10, 12, 15, 18, 21, 24, 28, 32, 36, 41, 47, 53, 60,
27
+ 68, 77, 87, 98, 110, 124, 140, 157, 176, 198, 223, 251, 282, 317, 356,
28
+ 400], dtype=np.int32)
29
+
30
+ SECOND_CHECK = np.array(
31
+ [0, 0, 3, 2, 3, 2, 5, 2, 3, 2, 3, 2, 5, 2, 3, 2], dtype=np.int32)
32
+
33
+
34
+ def f32(x):
35
+ return np.asarray(x, dtype=np.float32)
36
+
37
+
38
+ def make_half_window() -> np.ndarray:
39
+ """rnn_half_window(dump_rnnoise_tables.c 公式)。"""
40
+ i = (np.arange(FRAME_SIZE, dtype=np.float64) + 0.5)
41
+ s = np.sin(0.5 * np.pi * i / FRAME_SIZE)
42
+ w = np.sin(0.5 * np.pi * s * s)
43
+ return f32(w)
44
+
45
+
46
+ HALF_WINDOW = make_half_window()
47
+
48
+
49
+ def make_dct_table() -> np.ndarray:
50
+ """rnn_dct_table:行 i、列 j 为 cos((i+.5)*j*pi/32),j==0 乘 sqrt(.5)。"""
51
+ i = np.arange(NB_BANDS, dtype=np.float64)[:, None]
52
+ j = np.arange(NB_BANDS, dtype=np.float64)[None, :]
53
+ t = np.cos((i + 0.5) * j * np.pi / NB_BANDS)
54
+ t[:, 0] *= np.sqrt(0.5)
55
+ return f32(t)
56
+
57
+
58
+ DCT_TABLE = make_dct_table()
59
+
60
+
61
+ def fft(x: np.ndarray) -> np.ndarray:
62
+ """forward FFT:kiss_fft 无缩放正变换(960 点),返回复数组。"""
63
+ return np.fft.fft(f32(x).astype(np.float64))
64
+
65
+
66
+ def forward_transform(x: np.ndarray) -> np.ndarray:
67
+ """x[960] -> X[481](kiss_fft 该 fork 正变换带 1/N 缩放)。"""
68
+ y = fft(x) / WINDOW_SIZE
69
+ return y[:FREQ_SIZE]
70
+
71
+
72
+ def inverse_transform(y: np.ndarray) -> np.ndarray:
73
+ """X[481] -> x[960](Hermitian 镜像 + 正变换实现逆变换)。"""
74
+ y = np.asarray(y)
75
+ n = WINDOW_SIZE
76
+ x = np.zeros(n, dtype=np.complex128)
77
+ x[:FREQ_SIZE] = y
78
+ x[FREQ_SIZE:] = np.conj(y[1:(n - FREQ_SIZE) + 1][::-1])
79
+ out = np.fft.fft(x)
80
+ res = np.empty(n, dtype=np.float64)
81
+ res[0] = out[0].real
82
+ res[1:] = out[n:0:-1].real
83
+ return res
84
+
85
+
86
+ def apply_window(x: np.ndarray) -> np.ndarray:
87
+ x = x.copy()
88
+ x[:FRAME_SIZE] *= HALF_WINDOW
89
+ x[WINDOW_SIZE - 1:FRAME_SIZE - 1:-1] *= HALF_WINDOW
90
+ return x
91
+
92
+
93
+ def compute_band_energy(X: np.ndarray) -> np.ndarray:
94
+ """X[481] 复数 -> bandE[32]。"""
95
+ X = np.asarray(X)
96
+ power = (X.real ** 2 + X.imag ** 2).astype(np.float64)
97
+ # C 实现用 sum[0..NB_BANDS+1],sum[1] 和 sum[NB_BANDS] 修正后取 sum[1..NB_BANDS]
98
+ s = np.zeros(NB_BANDS + 2)
99
+ for i in range(NB_BANDS):
100
+ b0, b1 = int(EBAND20MS[i]), int(EBAND20MS[i + 1])
101
+ frac = (np.arange(b1 - b0, dtype=np.float64)) / (b1 - b0)
102
+ p = power[b0:b1]
103
+ s[i] += np.sum((1 - frac) * p)
104
+ s[i + 1] += np.sum(frac * p)
105
+ s[1] = (s[0] + s[1]) * 2.0 / 3.0
106
+ s[NB_BANDS] = (s[NB_BANDS] + s[NB_BANDS + 1]) * 2.0 / 3.0
107
+ return f32(s[1:NB_BANDS + 1])
108
+
109
+
110
+ def compute_band_corr(X: np.ndarray, P: np.ndarray) -> np.ndarray:
111
+ X = np.asarray(X)
112
+ P = np.asarray(P)
113
+ prod = (X.real * P.real + X.imag * P.imag).astype(np.float64)
114
+ s = np.zeros(NB_BANDS + 2)
115
+ for i in range(NB_BANDS):
116
+ b0, b1 = int(EBAND20MS[i]), int(EBAND20MS[i + 1])
117
+ frac = (np.arange(b1 - b0, dtype=np.float64)) / (b1 - b0)
118
+ p = prod[b0:b1]
119
+ s[i] += np.sum((1 - frac) * p)
120
+ s[i + 1] += np.sum(frac * p)
121
+ s[1] = (s[0] + s[1]) * 2.0 / 3.0
122
+ s[NB_BANDS] = (s[NB_BANDS] + s[NB_BANDS + 1]) * 2.0 / 3.0
123
+ return f32(s[1:NB_BANDS + 1])
124
+
125
+
126
+ def dct(inp: np.ndarray) -> np.ndarray:
127
+ """out[i] = sqrt(2/22) * sum_j in[j] * table[j, i]。"""
128
+ inp = f32(inp)
129
+ return f32(inp @ DCT_TABLE * np.sqrt(2.0 / 22.0))
130
+
131
+
132
+ def rnn_biquad(x: np.ndarray, mem: np.ndarray, b, a) -> tuple[np.ndarray, np.ndarray]:
133
+ x = f32(x)
134
+ y = np.empty_like(x)
135
+ mem = mem.copy()
136
+ for i in range(x.size):
137
+ xi = float(x[i])
138
+ yi = xi + mem[0]
139
+ mem[0] = mem[1] + (b[0] * xi - a[0] * yi)
140
+ mem[1] = b[1] * xi - a[1] * yi
141
+ y[i] = yi
142
+ return y, mem
143
+
144
+
145
+ def rnn_autocorr(x: np.ndarray, lag: int) -> np.ndarray:
146
+ """rnn_autocorr(overlap=0):ac[k] = sum_i x[i]*x[i+k]。"""
147
+ n = x.size
148
+ x = f32(x)
149
+ ac = np.zeros(lag + 1, dtype=np.float64)
150
+ for k in range(lag + 1):
151
+ ac[k] = np.dot(x[:n - k].astype(np.float64), x[k:].astype(np.float64))
152
+ return ac
153
+
154
+
155
+ def rnn_lpc(ac: np.ndarray, p: int) -> np.ndarray:
156
+ ac = np.asarray(ac, dtype=np.float64)
157
+ lpc = np.zeros(p, dtype=np.float64)
158
+ error = ac[0]
159
+ if ac[0] != 0:
160
+ for i in range(p):
161
+ rr = sum(lpc[j] * ac[i - j] for j in range(i)) + ac[i + 1]
162
+ r = -rr / error
163
+ lpc[i] = r
164
+ for j in range((i + 1) // 2):
165
+ tmp1 = lpc[j]
166
+ tmp2 = lpc[i - 1 - j]
167
+ lpc[j] = tmp1 + r * tmp2
168
+ lpc[i - 1 - j] = tmp2 + r * tmp1
169
+ error = error - r * r * error
170
+ if error < 0.001 * ac[0]:
171
+ break
172
+ return f32(lpc)
173
+
174
+
175
+ def celt_fir5(x: np.ndarray, num: np.ndarray) -> np.ndarray:
176
+ """5 抽头 FIR:y[i] = x[i] + sum_j num[j]*x[i-1-j]。"""
177
+ x = f32(x)
178
+ num = f32(num)
179
+ n = x.size
180
+ y = np.empty(n, dtype=np.float32)
181
+ mem = np.zeros(5, dtype=np.float64)
182
+ for i in range(n):
183
+ xi = float(x[i])
184
+ s = xi + float(num[0]) * mem[0] + float(num[1]) * mem[1] + \
185
+ float(num[2]) * mem[2] + float(num[3]) * mem[3] + float(num[4]) * mem[4]
186
+ mem[4] = mem[3]
187
+ mem[3] = mem[2]
188
+ mem[2] = mem[1]
189
+ mem[1] = mem[0]
190
+ mem[0] = xi
191
+ y[i] = s
192
+ return y
193
+
194
+
195
+ def rnn_pitch_downsample(x: np.ndarray) -> np.ndarray:
196
+ """1728 采样 -> 864 低通(2:1 抽取 + LPC 预加重)。"""
197
+ x = f32(x)
198
+ length = x.size
199
+ half = length // 2
200
+ x_lp = np.empty(half, dtype=np.float32)
201
+ x_lp[0] = 0.5 * (0.5 * float(x[1]) + float(x[0]))
202
+ for i in range(1, half):
203
+ x_lp[i] = 0.5 * (0.5 * (float(x[2 * i - 1]) + float(x[2 * i + 1])) + float(x[2 * i]))
204
+ ac = rnn_autocorr(x_lp, 4)
205
+ ac[0] *= 1.0001
206
+ for i in range(1, 5):
207
+ ac[i] -= ac[i] * (0.008 * i) * (0.008 * i)
208
+ lpc = rnn_lpc(ac, 4)
209
+ tmp = 1.0
210
+ for i in range(4):
211
+ tmp *= 0.9
212
+ lpc[i] = lpc[i] * tmp
213
+ lpc2 = np.zeros(5, dtype=np.float32)
214
+ c1 = 0.8
215
+ lpc2[0] = lpc[0] + 0.8
216
+ lpc2[1] = lpc[1] + c1 * lpc[0]
217
+ lpc2[2] = lpc[2] + c1 * lpc[1]
218
+ lpc2[3] = lpc[3] + c1 * lpc[2]
219
+ lpc2[4] = c1 * lpc[3]
220
+ return celt_fir5(x_lp, lpc2)
221
+
222
+
223
+ def _find_best_pitch(xcorr: np.ndarray, y: np.ndarray, length: int, max_pitch: int):
224
+ """find_best_pitch(浮点路径),返回 best_pitch[2]。"""
225
+ xcorr = np.asarray(xcorr, dtype=np.float64)
226
+ y = np.asarray(y, dtype=np.float64)
227
+ Syy = 1.0 + np.dot(y[:length], y[:length])
228
+ best_num = [-1.0, -1.0]
229
+ best_den = [0.0, 0.0]
230
+ best_pitch = [0, 1]
231
+ for i in range(max_pitch):
232
+ if xcorr[i] > 0:
233
+ xcorr16 = xcorr[i] * 1e-12
234
+ num = xcorr16 * xcorr16
235
+ if num * best_den[1] > best_num[1] * Syy:
236
+ if num * best_den[0] > best_num[0] * Syy:
237
+ best_num[1] = best_num[0]
238
+ best_den[1] = best_den[0]
239
+ best_pitch[1] = best_pitch[0]
240
+ best_num[0] = num
241
+ best_den[0] = Syy
242
+ best_pitch[0] = i
243
+ else:
244
+ best_num[1] = num
245
+ best_den[1] = Syy
246
+ best_pitch[1] = i
247
+ Syy += y[i + length] * y[i + length] - y[i] * y[i]
248
+ Syy = max(1.0, Syy)
249
+ return best_pitch
250
+
251
+
252
+ def rnn_pitch_search(x_lp: np.ndarray, y: np.ndarray, length: int, max_pitch: int) -> int:
253
+ """rnn_pitch_search 返回 pitch(粗搜 + 细搜 + 伪插值)。"""
254
+ x_lp = f32(x_lp)
255
+ y = f32(y)
256
+ lag = length + max_pitch
257
+ len4 = length >> 2
258
+ x_lp4 = x_lp[0:length:2][:len4]
259
+ y_lp4 = y[0:lag:2][:(lag >> 2)]
260
+ xcorr = np.array([np.dot(x_lp4.astype(np.float64),
261
+ y_lp4[i:i + len4].astype(np.float64))
262
+ for i in range(max_pitch >> 2)], dtype=np.float64)
263
+ best_pitch = _find_best_pitch(xcorr, y_lp4, len4, max_pitch >> 2)
264
+ half = max_pitch >> 1
265
+ xcorr2 = np.zeros(half, dtype=np.float64)
266
+ x_lp64 = x_lp.astype(np.float64)
267
+ y64 = y.astype(np.float64)
268
+ for i in range(half):
269
+ if abs(i - 2 * best_pitch[0]) > 2 and abs(i - 2 * best_pitch[1]) > 2:
270
+ continue
271
+ s = np.dot(x_lp64[:length >> 1], y64[i:i + (length >> 1)])
272
+ xcorr2[i] = max(-1.0, s)
273
+ best_pitch = _find_best_pitch(xcorr2, y64, length >> 1, half)
274
+ bp = best_pitch[0]
275
+ if 0 < bp < half - 1:
276
+ a, b, c = xcorr2[bp - 1], xcorr2[bp], xcorr2[bp + 1]
277
+ if (c - a) > 0.7 * (b - a):
278
+ offset = 1
279
+ elif (a - c) > 0.7 * (b - c):
280
+ offset = -1
281
+ else:
282
+ offset = 0
283
+ else:
284
+ offset = 0
285
+ return 2 * bp - offset
286
+
287
+
288
+ def _compute_pitch_gain(xy, xx, yy):
289
+ return xy / np.sqrt(1.0 + xx * yy)
290
+
291
+
292
+ def rnn_remove_doubling(x: np.ndarray, maxperiod: int, minperiod: int, n: int,
293
+ t0_in: int, prev_period: int, prev_gain: float):
294
+ """rnn_remove_doubling:返回 (pg, T0)。"""
295
+ x = f32(x)
296
+ minperiod0 = minperiod
297
+ maxperiod //= 2
298
+ minperiod //= 2
299
+ t0 = t0_in // 2
300
+ prev_period //= 2
301
+ n //= 2
302
+ x = x[maxperiod:]
303
+ if t0 >= maxperiod:
304
+ t0 = maxperiod - 1
305
+ T = T0 = t0
306
+ x64 = x.astype(np.float64)
307
+ xx = np.dot(x64[:n], x64[:n])
308
+ xy = np.dot(x64[:n], x64[T0:T0 + n])
309
+ yy_lookup = np.zeros(maxperiod + 1, dtype=np.float64)
310
+ yy_lookup[0] = xx
311
+ yy = xx
312
+ for i in range(1, maxperiod + 1):
313
+ yy = yy + float(x64[-i]) ** 2 - float(x64[n - i]) ** 2
314
+ yy_lookup[i] = max(0.0, yy)
315
+ yy = yy_lookup[T0]
316
+ best_xy, best_yy = xy, yy
317
+ g = g0 = _compute_pitch_gain(xy, xx, yy)
318
+ for k in range(2, 16):
319
+ T1 = (2 * T0 + k) // (2 * k)
320
+ if T1 < minperiod:
321
+ break
322
+ if k == 2:
323
+ T1b = T0 if T1 + T0 > maxperiod else T0 + T1
324
+ else:
325
+ T1b = (2 * int(SECOND_CHECK[k]) * T0 + k) // (2 * k)
326
+ xy2 = np.dot(x64[:n], x64[T1b:T1b + n])
327
+ xy = 0.5 * (xy + xy2)
328
+ yy = 0.5 * (yy_lookup[T1] + yy_lookup[T1b])
329
+ g1 = _compute_pitch_gain(xy, xx, yy)
330
+ if abs(T1 - prev_period) <= 1:
331
+ cont = prev_gain
332
+ elif abs(T1 - prev_period) <= 2 and 5 * k * k < T0:
333
+ cont = 0.5 * prev_gain
334
+ else:
335
+ cont = 0.0
336
+ thresh = max(0.3, 0.7 * g0 - cont)
337
+ if T1 < 3 * minperiod:
338
+ thresh = max(0.4, 0.85 * g0 - cont)
339
+ elif T1 < 2 * minperiod:
340
+ thresh = max(0.5, 0.9 * g0 - cont)
341
+ if g1 > thresh:
342
+ best_xy, best_yy = xy, yy
343
+ T = T1
344
+ g = g1
345
+ best_xy = max(0.0, best_xy)
346
+ pg = 1.0 if best_yy <= best_xy else best_xy / (best_yy + 1.0)
347
+ xcorr = np.zeros(3)
348
+ for k in range(3):
349
+ lag = T + k - 1
350
+ xcorr[k] = np.dot(x64[:n], x64[lag:lag + n])
351
+ if (xcorr[2] - xcorr[0]) > 0.7 * (xcorr[1] - xcorr[0]):
352
+ offset = 1
353
+ elif (xcorr[0] - xcorr[2]) > 0.7 * (xcorr[1] - xcorr[2]):
354
+ offset = -1
355
+ else:
356
+ offset = 0
357
+ if pg > g:
358
+ pg = g
359
+ T0 = 2 * T + offset
360
+ if T0 < minperiod0:
361
+ T0 = minperiod0
362
+ return float(pg), int(T0)
363
+
364
+
365
+ def interp_band_gain(bandE: np.ndarray) -> np.ndarray:
366
+ bandE = np.asarray(bandE, dtype=np.float64)
367
+ g = np.zeros(FREQ_SIZE)
368
+ for i in range(1, NB_BANDS):
369
+ b0, b1 = int(EBAND20MS[i]), int(EBAND20MS[i + 1])
370
+ frac = (np.arange(b1 - b0, dtype=np.float64)) / (b1 - b0)
371
+ g[b0:b1] = (1 - frac) * bandE[i - 1] + frac * bandE[i]
372
+ g[:int(EBAND20MS[1])] = bandE[0]
373
+ g[int(EBAND20MS[NB_BANDS]):int(EBAND20MS[NB_BANDS + 1])] = bandE[NB_BANDS - 1]
374
+ return f32(g)
375
+
376
+
377
+ def rnn_pitch_filter(X, P, Ex, Ep, Exp, g):
378
+ """rnn_pitch_filter:就地修改 X[481]。"""
379
+ X = np.asarray(X, dtype=np.complex128)
380
+ P = np.asarray(P, dtype=np.complex128)
381
+ Ex = np.asarray(Ex, dtype=np.float64)
382
+ Ep = np.asarray(Ep, dtype=np.float64)
383
+ Exp = np.asarray(Exp, dtype=np.float64)
384
+ g = np.asarray(g, dtype=np.float64)
385
+ r = np.zeros(NB_BANDS)
386
+ for i in range(NB_BANDS):
387
+ if Exp[i] > g[i]:
388
+ r[i] = 1.0
389
+ else:
390
+ r[i] = (Exp[i] ** 2) * (1 - g[i] ** 2) / (0.001 + g[i] ** 2 * (1 - Exp[i] ** 2))
391
+ r[i] = np.sqrt(min(1.0, max(0.0, r[i])))
392
+ r[i] *= np.sqrt(Ex[i] / (1e-8 + Ep[i]))
393
+ rf = interp_band_gain(r)
394
+ X += rf * P
395
+ newE = compute_band_energy(X)
396
+ norm = np.sqrt(Ex / (1e-8 + newE))
397
+ normf = interp_band_gain(norm)
398
+ X *= normf
399
+ return X
400
+
401
+
402
+ class RNNoiseState:
403
+ """对应 DenoiseState:维护全部帧间状态。"""
404
+
405
+ def __init__(self):
406
+ self.analysis_mem = np.zeros(FRAME_SIZE, dtype=np.float32)
407
+ self.synthesis_mem = np.zeros(FRAME_SIZE, dtype=np.float32)
408
+ self.pitch_buf = np.zeros(PITCH_BUF_SIZE, dtype=np.float32)
409
+ self.last_gain = 0.0
410
+ self.last_period = 0
411
+ self.mem_hp_x = np.zeros(2, dtype=np.float32)
412
+ self.lastg = np.zeros(NB_BANDS, dtype=np.float32)
413
+ self.delayed_X = np.zeros(FREQ_SIZE, dtype=np.complex128)
414
+ self.delayed_P = np.zeros(FREQ_SIZE, dtype=np.complex128)
415
+ self.delayed_Ex = np.zeros(NB_BANDS, dtype=np.float32)
416
+ self.delayed_Ep = np.zeros(NB_BANDS, dtype=np.float32)
417
+ self.delayed_Exp = np.zeros(NB_BANDS, dtype=np.float32)
418
+
419
+
420
+ def rnn_frame_analysis(st: RNNoiseState, inp: np.ndarray):
421
+ """返回 (X[481], Ex[32])。"""
422
+ x = np.concatenate([st.analysis_mem, f32(inp)])
423
+ st.analysis_mem = f32(inp).copy()
424
+ x = apply_window(x)
425
+ X = forward_transform(x)
426
+ Ex = compute_band_energy(X)
427
+ return X, Ex
428
+
429
+
430
+ def compute_frame_features(st: RNNoiseState, inp: np.ndarray):
431
+ """返回 (silence, X, P, Ex, Ep, Exp, features)。features 为空时清为 0。"""
432
+ X, Ex = rnn_frame_analysis(st, inp)
433
+ st.pitch_buf = np.concatenate([st.pitch_buf[FRAME_SIZE:], f32(inp)])
434
+ pre = st.pitch_buf
435
+ pitch_buf_lp = rnn_pitch_downsample(pre)
436
+ x_lp = pitch_buf_lp[(PITCH_MAX_PERIOD >> 1):]
437
+ y = pitch_buf_lp
438
+ pitch_index = rnn_pitch_search(
439
+ x_lp, y, PITCH_FRAME_SIZE, PITCH_MAX_PERIOD - 3 * PITCH_MIN_PERIOD)
440
+ pitch_index = PITCH_MAX_PERIOD - pitch_index
441
+ gain, pitch_index = rnn_remove_doubling(
442
+ st.pitch_buf, PITCH_MAX_PERIOD, PITCH_MIN_PERIOD, PITCH_FRAME_SIZE,
443
+ pitch_index, st.last_period, st.last_gain)
444
+ st.last_period = pitch_index
445
+ st.last_gain = gain
446
+ p = np.array([
447
+ st.pitch_buf[PITCH_BUF_SIZE - WINDOW_SIZE - pitch_index + i]
448
+ for i in range(WINDOW_SIZE)], dtype=np.float32)
449
+ p = apply_window(p)
450
+ P = forward_transform(p)
451
+ Ep = compute_band_energy(P)
452
+ Exp = compute_band_corr(X, P)
453
+ Exp = Exp / np.sqrt(0.001 + Ex * Ep)
454
+ features = np.zeros(NB_FEATURES, dtype=np.float32)
455
+ features[NB_BANDS:2 * NB_BANDS] = dct(Exp)
456
+ features[2 * NB_BANDS] = 0.01 * (pitch_index - 300)
457
+ logMax = -2.0
458
+ follow = -2.0
459
+ Ly = np.zeros(NB_BANDS)
460
+ E = 0.0
461
+ for i in range(NB_BANDS):
462
+ Ly[i] = np.log10(1e-2 + float(Ex[i]))
463
+ Ly[i] = max(logMax - 7, max(follow - 1.5, Ly[i]))
464
+ logMax = max(logMax, Ly[i])
465
+ follow = max(follow - 1.5, Ly[i])
466
+ E += float(Ex[i])
467
+ if E < 0.04:
468
+ features[:] = 0.0
469
+ return 1, X, P, Ex, Ep, Exp, features
470
+ features[:NB_BANDS] = dct(Ly)
471
+ features[0] -= 12.0
472
+ features[1] -= 4.0
473
+ return 0, X, P, Ex, Ep, Exp, features
474
+
475
+
476
+ def frame_synthesis(st: RNNoiseState, y) -> np.ndarray:
477
+ x = inverse_transform(y)
478
+ x = apply_window(x)
479
+ out = f32(x[:FRAME_SIZE] + st.synthesis_mem)
480
+ st.synthesis_mem = f32(x[FRAME_SIZE:])
481
+ return out
482
+
483
+
484
+ def analyze_frame(st: RNNoiseState, inp: np.ndarray) -> dict:
485
+ """C 端 rnnoise_process_frame 前半:biquad + 特征分析。
486
+
487
+ 返回 dict(silence, features, X, P, Ex, Ep, Exp)。会推进 st 的
488
+ analysis_mem / pitch_buf / last_period / last_gain 等 DSP 状态。
489
+ """
490
+ a_hp = np.array([-1.99599, 0.99600], dtype=np.float32)
491
+ b_hp = np.array([-2, 1], dtype=np.float32)
492
+ x, st.mem_hp_x = rnn_biquad(f32(inp), st.mem_hp_x, b_hp, a_hp)
493
+ silence, X, P, Ex, Ep, Exp, features = compute_frame_features(st, x)
494
+ return {"silence": silence, "features": features,
495
+ "X": X, "P": P, "Ex": Ex, "Ep": Ep, "Exp": Exp}
496
+
497
+
498
+ def synthesize_frame(st: RNNoiseState, ana: dict, gains, vad):
499
+ """C 端 rnnoise_process_frame 后半:pitch filter + 增益 + 频谱合成。
500
+
501
+ gains 为 None 表示静音帧(跳过模型相关部分,vad 记 0)。
502
+ 返回 (out_frame, vad_prob)。需在 analyze_frame 之后调用。
503
+ """
504
+ silence = ana["silence"]
505
+ X, P, Ex, Ep, Exp = ana["X"], ana["P"], ana["Ex"], ana["Ep"], ana["Exp"]
506
+ if not silence and gains is not None:
507
+ gains = f32(gains).reshape(-1)
508
+ st.delayed_X = rnn_pitch_filter(
509
+ st.delayed_X, st.delayed_P, st.delayed_Ex, st.delayed_Ep,
510
+ st.delayed_Exp, gains)
511
+ for i in range(NB_BANDS):
512
+ alpha = 0.6
513
+ gains[i] = max(gains[i], alpha * st.lastg[i])
514
+ st.lastg[i] = min(1.0, gains[i] * (st.delayed_Ex[i] + 1e-3) / (Ex[i] + 1e-3))
515
+ gf = interp_band_gain(gains)
516
+ st.delayed_X = st.delayed_X * gf
517
+ out = frame_synthesis(st, st.delayed_X)
518
+ st.delayed_X = X
519
+ st.delayed_P = P
520
+ st.delayed_Ex = Ex
521
+ st.delayed_Ep = Ep
522
+ st.delayed_Exp = Exp
523
+ vad_prob = 0.0 if (silence or gains is None) else float(np.asarray(vad).reshape(-1)[0])
524
+ return out, vad_prob
525
+
526
+
527
+ def process_frame(st: RNNoiseState, inp: np.ndarray, gains, vad):
528
+ """完整一帧(C 端 rnnoise_process_frame 等价),返回 (out, vad_prob)。"""
529
+ ana = analyze_frame(st, inp)
530
+ if ana["silence"]:
531
+ return synthesize_frame(st, ana, None, 0.0)
532
+ return synthesize_frame(st, ana, gains, vad)
533
+
534
+
535
+ def features_from_pcm(pcm: np.ndarray) -> np.ndarray:
536
+ """批量:48k f32 PCM(16-bit 域)-> (T, 65) 特征矩阵(含 silence 清零逻辑)。"""
537
+ st = RNNoiseState()
538
+ feats = []
539
+ pcm = f32(pcm)
540
+ for t in range(0, pcm.size - FRAME_SIZE + 1, FRAME_SIZE):
541
+ frame = pcm[t:t + FRAME_SIZE]
542
+ a_hp = np.array([-1.99599, 0.99600], dtype=np.float32)
543
+ b_hp = np.array([-2, 1], dtype=np.float32)
544
+ x, st.mem_hp_x = rnn_biquad(frame, st.mem_hp_x, b_hp, a_hp)
545
+ _, _, _, _, _, _, features = compute_frame_features(st, x)
546
+ feats.append(features.copy())
547
+ return np.stack(feats)
python/rnnoise_ax650_sdk/example.py ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """RNNoise 降噪示例:处理一段 48k 单声道音频,输出去噪结果。
2
+
3
+ 用法:
4
+ python example.py --model model.axmodel --input in.pcm [--output-dir out]
5
+
6
+ 输入格式:48kHz f32le PCM(16-bit 等价域,±32768,不做归一化);
7
+ 也可传 16-bit PCM .wav(wave 标准库自动解码为 ±32768 域)。
8
+ """
9
+ import argparse
10
+ import sys
11
+ import wave
12
+ from pathlib import Path
13
+
14
+ import numpy as np
15
+
16
+ sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
17
+
18
+ from rnnoise_ax650_sdk import RNNoiseDenoiser, dsp # noqa: E402
19
+
20
+
21
+ def load_pcm(path: Path) -> np.ndarray:
22
+ if path.suffix.lower() == ".wav":
23
+ with wave.open(str(path), "rb") as w:
24
+ assert w.getframerate() == 48000, "仅支持 48kHz WAV"
25
+ assert w.getsampwidth() == 2, "仅支持 16-bit PCM WAV"
26
+ raw = w.readframes(w.getnframes())
27
+ return np.frombuffer(raw, dtype="<i2").astype(np.float32)
28
+ return np.fromfile(path, dtype=np.float32)
29
+
30
+
31
+ def write_wav(path: Path, pcm: np.ndarray, sr: int = 48000) -> None:
32
+ pcm = np.clip(pcm, -32768.0, 32767.0).astype(np.int16)
33
+ with wave.open(str(path), "wb") as w:
34
+ w.setnchannels(1)
35
+ w.setsampwidth(2)
36
+ w.setframerate(sr)
37
+ w.writeframes(pcm.tobytes())
38
+
39
+
40
+ def main() -> None:
41
+ parser = argparse.ArgumentParser(description="RNNoise 48k 实时降噪示例")
42
+ parser.add_argument("--model", required=True, help="model.axmodel 路径")
43
+ parser.add_argument("--input", required=True, help="48k f32 PCM 或 16-bit WAV")
44
+ parser.add_argument("--output-dir", default="output")
45
+ args = parser.parse_args()
46
+
47
+ pcm = load_pcm(Path(args.input))
48
+ print(f"input: {pcm.size / 48000:.2f}s ({pcm.size // dsp.FRAME_SIZE} 帧)")
49
+ denoiser = RNNoiseDenoiser(args.model)
50
+ out, vads = denoiser.process(pcm)
51
+ out_dir = Path(args.output_dir)
52
+ out_dir.mkdir(parents=True, exist_ok=True)
53
+ out.astype(np.float32).tofile(out_dir / "out.pcm")
54
+ write_wav(out_dir / "out.wav", out)
55
+ np.save(out_dir / "vad.npy", vads)
56
+ print(f"backend: {denoiser.backend}")
57
+ print(f"frames: {vads.size} vad_mean: {float(vads.mean()):.4f}")
58
+ print(f"output RMS: {float(np.sqrt((out ** 2).mean())):.1f}")
59
+ print(f"saved to: {out_dir}")
60
+
61
+
62
+ if __name__ == "__main__":
63
+ main()
python/rnnoise_ax650_sdk/inference.py ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """RNNoise AX650 推理会话(NPU 专用发布版:无 onnxruntime/torch 回退)。"""
2
+ import numpy as np
3
+
4
+ from . import dsp
5
+
6
+ DEFAULT_PROVIDER = "AxEngineExecutionProvider"
7
+
8
+ INPUT_NAMES = ["features", "conv1_mem", "conv2_mem",
9
+ "gru1_s", "gru2_s", "gru3_s"]
10
+ INPUT_SHAPES = {"features": (1, 65), "conv1_mem": (1, 130),
11
+ "conv2_mem": (1, 256), "gru1_s": (1, 384),
12
+ "gru2_s": (1, 384), "gru3_s": (1, 384)}
13
+ OUTPUT_NAMES = ["gains", "vad", "conv1_mem_new", "conv2_mem_new",
14
+ "gru1_s_new", "gru2_s_new", "gru3_s_new"]
15
+ _STATE_INPUTS = ["conv1_mem", "conv2_mem", "gru1_s", "gru2_s", "gru3_s"]
16
+
17
+
18
+ class RNNoiseDenoiser:
19
+ """48k 单声道实时降噪器(AX 芯片端到端,无 CPU 回退)。"""
20
+
21
+ def __init__(self, model_path, providers=None):
22
+ try:
23
+ import axengine as axe
24
+ except ImportError as exc:
25
+ raise RuntimeError(
26
+ "SDK 为 NPU 专用发布版,仅支持在 AX 芯片上运行;请先安装 "
27
+ "requirements.txt 并在板端执行(无 onnxruntime/torch 回退)"
28
+ ) from exc
29
+ self.session = axe.InferenceSession(
30
+ model_path, providers=providers or [DEFAULT_PROVIDER])
31
+ self.backend = "axengine"
32
+ self.input_names = [i.name for i in self.session.get_inputs()]
33
+ self.output_names = [o.name for o in self.session.get_outputs()]
34
+ self.reset()
35
+
36
+ def reset(self):
37
+ self.st = dsp.RNNoiseState()
38
+ self._states = {k: np.zeros(INPUT_SHAPES[k], dtype=np.float32)
39
+ for k in _STATE_INPUTS}
40
+
41
+ def process_frame(self, frame):
42
+ frame = np.asarray(frame, dtype=np.float32).reshape(-1)
43
+ if frame.size != dsp.FRAME_SIZE:
44
+ raise ValueError(f"帧长必须为 {dsp.FRAME_SIZE},实际 {frame.size}")
45
+ ana = dsp.analyze_frame(self.st, frame)
46
+ if ana["silence"]:
47
+ out, _ = dsp.synthesize_frame(self.st, ana, None, 0.0)
48
+ return out, 0.0
49
+ feeds = {
50
+ "features": np.ascontiguousarray(
51
+ ana["features"][None, :].astype(np.float32)),
52
+ }
53
+ feeds.update({k: np.ascontiguousarray(v)
54
+ for k, v in self._states.items()})
55
+ outs = self.session.run(None, feeds)
56
+ out_idx = {n: i for i, n in enumerate(self.output_names)}
57
+ gains = outs[out_idx["gains"]]
58
+ vad = outs[out_idx["vad"]]
59
+ for k in _STATE_INPUTS:
60
+ self._states[k] = np.asarray(
61
+ outs[out_idx[k + "_new"]], dtype=np.float32)
62
+ out, vad = dsp.synthesize_frame(self.st, ana, gains, vad)
63
+ return out, float(vad)
64
+
65
+ def process(self, pcm):
66
+ pcm = np.asarray(pcm, dtype=np.float32)
67
+ if pcm.ndim == 1:
68
+ n = pcm.size // dsp.FRAME_SIZE
69
+ frames = pcm[:n * dsp.FRAME_SIZE].reshape(n, dsp.FRAME_SIZE)
70
+ else:
71
+ frames = pcm.reshape(-1, dsp.FRAME_SIZE)
72
+ outs, vads = [], []
73
+ for fr in frames:
74
+ o, v = self.process_frame(fr)
75
+ outs.append(o)
76
+ vads.append(v)
77
+ return np.concatenate(outs), np.asarray(vads, dtype=np.float32)
python/rnnoise_ax650_sdk/postprocess.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+
3
+ def postprocess(ana, gains, vad, state):
4
+ """模型输出(gains/vad) + 分析结果 -> (去噪帧(480,), vad)。
5
+ 对应原版 rnnoise_process_frame 的 pitch filter / 增益平滑 / 频谱合成部分。"""
6
+ from .dsp import synthesize_frame
7
+ return synthesize_frame(state, ana,
8
+ np.asarray(gains, dtype=np.float32),
9
+ np.asarray(vad, dtype=np.float32))
python/rnnoise_ax650_sdk/preprocess.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+
3
+ def preprocess(frame, state):
4
+ """48k PCM 帧(480, float32, 16-bit 域) -> (features(1,65), analysis)。
5
+ 对应原版 rnnoise_process_frame 的 biquad + 特征分析部分。
6
+ state 为 dsp.RNNoiseState 实例(帧间状态,由 SDK 内部维护)。"""
7
+ from .dsp import analyze_frame
8
+ ana = analyze_frame(state, np.asarray(frame, dtype=np.float32).reshape(-1))
9
+ return ana["features"][None, :].astype(np.float32), ana
python/rnnoise_ax650_sdk/requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ numpy
2
+ pyaxengine @ git+https://gh-proxy.com/https://github.com/AXERA-TECH/pyaxengine.git
python/sample_speech.pcm ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b7f2477120159ed81da6eda259c6ebe57f321aac41c04d4e9d8b21a055887416
3
+ size 192000
reports/compile_report.md ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ # Compile Report
2
+
3
+ - image: docker-registry.aitsw.axera-tech.com/pulsar2:20260810-temp-0d4427ff
4
+ - target: AX650
5
+ - input: features:1x65,conv1_mem:1x130,conv2_mem:1x256,gru1_s:1x384,gru2_s:1x384,gru3_s:1x384
6
+ - src_dtype: FP32
7
+ - size: 3314.4 KB
reports/export_report.md ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Export Report
2
+
3
+ - ONNX: export/model.onnx (opset 13, 静态 shape, 6 输入 / 7 输出)
4
+ - 输入域: 与官方 demo 一致,float 值等价 16-bit PCM(±32768 量级),不做 /32768 归一化
5
+ - 权重来源: 官方 rnnoise_data.c(float 数组 + 稀疏重建 + diag),
6
+ tanh/sigmoid 复刻 C 端有理逼近
7
+ - Torch(参考) ↔ ONNX 对分(198 帧真实语音特征序列):
8
+ - gains cosine: 1.000000
9
+ - vad cosine: 1.000000
10
+ - gains MAE: 1.42e-07
11
+ - C 库 compute_rnn ↔ Torch 参考对分(198 帧): gains cosine 0.999999, vad 0.9999995
12
+ - 校准数据: calib_data/<tensor>.tar.gz,来自真实语音(speech/speech-echo/
13
+ speech-reverb + 合成噪声混合 6dB),每输入 40 帧特征+状态轨迹(real 业务数据)
14
+ - 状态语义: 逐帧推理,conv1/conv2 mem 各保留 2 帧,GRU 状态 384x3
reports/runonboard_report.md ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Run On Board Report(RNNoise AX650)
2
+
3
+ - board: root@10.126.35.203(AX650C / MC50,aarch64)
4
+ - model: compile/model.axmodel(U16 链路,NPU3,3.31MB)
5
+ - engine: axengine 2.12.0s(板端本地推理,非远程代理);C++ 链接 /soc/lib/libax_engine.so
6
+ - 测试音频: sample_speech.pcm(48k f32,16-bit 域,1 秒=100 帧,语音+白噪声 6dB)
7
+
8
+ ## Python SDK(rnnoise_ax650_sdk,numpy DSP 移植)
9
+
10
+ - 每帧延迟: 60.8 ms(numpy DSP 为主,含每帧 axengine 调用;帧预算 10ms——实时路径请用 C++)
11
+ - 输出 vs c_ref(原版 C 全管线参考): cosine 0.9973,MAE 243.8
12
+ - vad vs c_ref: cosine 0.9998
13
+ - 内存: 推理前后 549→541 MB(无显著增长)
14
+
15
+ ## C++ SDK(原版 C 信号处理 + AX Engine compute_rnn 替换)
16
+
17
+ - 板上 cmake configure + make 通过(AX_RUNTIME_ROOT=/tmp/rnnoise_ax650/axrt,头文件 mc50)
18
+ - 每帧延迟: 2.85 ms(含 6 输入/7 输出拷贝 + NPU 0.3ms;实时预算 10ms ✅)
19
+ - 输出 vs c_ref: cosine 0.9820,MAE 819.6
20
+ - 内存: 539→541 MB
21
+
22
+ ## 结论
23
+
24
+ - Python / C++ SDK 板端端到端 NPU 推理通过(输出 cosine ≥ 0.98)
25
+ - 实时降噪(10ms/帧)用 C++ SDK;Python SDK 适合离线批处理/原型验证
26
+ - SIMULATE 阶段已确认模型层 gains cosine 0.9991 ≥ 0.99
reports/simulate_report.md ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SIMULATE Report
2
+
3
+ - board: 10.126.35.203
4
+ - frames: 198
5
+ - 方法:逐帧状态化远程推理(ONNX FP32 参考 vs AXMODEL),远程每帧 ~0.3ms NPU
6
+ - 量化:U16 链路(MatMul/Conv/Add/Mul/Div/Sub/Concat/Clip/Slice,S8 权重),MinMax,calibration_size=30
7
+ - 指标:
8
+
9
+ | 张量 | cosine | MAE |
10
+ |------|--------|-----|
11
+ | gains | 0.999122 | 0.017231 |
12
+ | vad | 0.999962 | 0.001985 |
13
+ | conv1_mem_new | 0.999127 | 0.001900 |
14
+ | conv2_mem_new | 0.997568 | 0.020142 |
15
+ | gru1_s_new | 0.995698 | 0.030715 |
16
+ | gru2_s_new | 0.994793 | 0.036976 |
17
+ | gru3_s_new | 0.992189 | 0.050399 |
18
+
19
+ - gains max_abs_diff: 0.275927
20
+ - vad max_abs_diff: 0.078959
21
+
22
+ 结论:gains cosine 0.9991 ≥ 0.99 ✅,GRU 状态 0.992-0.996 ✅,SIMULATE 通过。
run.sh ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ echo "=== 运行推理 ==="
5
+ if command -v python3 >/dev/null 2>&1; then PY=python3; else PY=python; fi
6
+ "$PY" python/demo.py
7
+ # ./cpp/build/model_example models/model.axmodel
setup.sh ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ echo "=== 安装依赖 ==="
5
+ PIP_INDEX_URL="${PIP_INDEX_URL:-https://mirrors.aliyun.com/pypi/simple/}"
6
+ if command -v python3 >/dev/null 2>&1; then PY=python3; else PY=python; fi
7
+ if "$PY" -m pip --version >/dev/null 2>&1; then
8
+ "$PY" -m pip install -i "$PIP_INDEX_URL" -r python/requirements.txt
9
+ elif command -v pip >/dev/null 2>&1; then
10
+ pip install -i "$PIP_INDEX_URL" -r python/requirements.txt
11
+ else
12
+ echo "⚠ 未找到 pip:本机仅用于查看/自测,NPU 推理请在 AX 板端执行(板端自带 pip)。"
13
+ fi
14
+
15
+ echo "C++ SDK: 请先安装 AX650 BSP SDK,然后:"
16
+ # export AX_RUNTIME_ROOT=/path/to/axruntime
17
+ # mkdir -p cpp/build && cd cpp/build
18
+ # cmake .. -DCMAKE_TOOLCHAIN_FILE=${AX_RUNTIME_ROOT}/toolchain.cmake
19
+ # make -j$(nproc)
20
+
21
+ echo "✅ 环境准备完成"