Upload folder using huggingface_hub
Browse files- .gitattributes +1 -0
- cpp/src/rnnoise_ax.cpp +115 -0
- models/model.axmodel +3 -0
- models/model_meta.json +140 -0
- python/demo.py +44 -0
- python/requirements.txt +2 -0
- python/rnnoise_ax650_sdk/README.md +21 -0
- python/rnnoise_ax650_sdk/__init__.py +4 -0
- python/rnnoise_ax650_sdk/dsp.py +547 -0
- python/rnnoise_ax650_sdk/example.py +63 -0
- python/rnnoise_ax650_sdk/inference.py +77 -0
- python/rnnoise_ax650_sdk/postprocess.py +9 -0
- python/rnnoise_ax650_sdk/preprocess.py +9 -0
- python/rnnoise_ax650_sdk/requirements.txt +2 -0
- python/sample_speech.pcm +3 -0
- reports/compile_report.md +7 -0
- reports/export_report.md +14 -0
- reports/runonboard_report.md +26 -0
- reports/simulate_report.md +22 -0
- run.sh +7 -0
- setup.sh +21 -0
.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 "✅ 环境准备完成"
|