原工程地址
https://github.com/ceva-ip/DPDFNet
Model Profile
8 kHz models
| Model |
Params M |
MACs G |
TFLite Size MB |
ONNX Size MB |
| dpdfnet2_8khz |
2.51 |
1.29 |
10.5 |
9.7 |
| dpdfnet8_8khz |
3.56 |
3.99 |
16.5 |
13.8 |
16 kHz models
| Model |
Params M |
MACs G |
TFLite Size MB |
ONNX Size MB |
| baseline |
2.31 |
0.36 |
8.5 |
8.3 |
| dpdfnet2 |
2.49 |
1.35 |
10.7 |
9.7 |
| dpdfnet4 |
2.84 |
2.36 |
12.9 |
11.1 |
| dpdfnet8 |
3.54 |
4.37 |
17.2 |
13.9 |
48 kHz models
| Model |
Params M |
MACs G |
TFLite Size MB |
ONNX Size MB |
| dpdfnet2_48khz_hr |
2.58 |
2.42 |
11.6 |
10.0 |
| dpdfnet8_48khz_hr |
3.63 |
7.17 |
18.7 |
14.2 |
音频文件处理效果图
主要代码
using System;
using System.IO;
using UnityEngine;
namespace DPDFNetUnity
{
/// <summary>
/// 16 kHz 音频文件离线降噪示例(不依赖任何额外包,仅需 onnxruntime 包)。
/// 严格对齐 DPDFNet 源码 onnx_model/infer_dpdfnet_onnx.py 的 enhance_file_onnx:
/// center=True 反射填充 STFT -> 流式 ONNX(state) -> center=True ISTFT(win^2 归一化)
/// -> postprocess_spec(裁 win_len*2 / 补 win_len*2) -> fit_length。
/// 与「实时」路径(real_time_demo.py 的因果流式 STFT)不同:文件离线用 librosa 的
/// center=True 算法,逐帧对齐与边界处理都不一样,故结果以源码为准。
/// 适用于桌面 / Editor 下以本地文件路径处理;移动端请将模型与音频放在可读写路径
/// (如 Application.persistentDataPath),因 StreamingAssets 在 Android 内打包为 APK 资源。
/// </summary>
public class DpdfNetFileExample : MonoBehaviour
{
private void Start()
{
ProcessWav(Application.streamingAssetsPath + "/dpdfnet/dpdfnet4.onnx",
Application.dataPath + "/mix.wav",
Application.dataPath + "/dpdfnet.wav");
}
/// <summary>
/// 处理一个 16 kHz WAV 文件(单/多声道均可,自动转单声道)。
/// 输出为同采样率的 16-bit PCM 单声道 WAV。
/// </summary>
/// <param name="modelPath">ONNX 模型文件路径(如 StreamingAssets/dpdfnet/dpdfnet2.onnx)</param>
/// <param name="inputWav">输入 WAV 路径(建议 16 kHz)</param>
/// <param name="outputWav">输出 WAV 路径</param>
public void ProcessWav(string modelPath, string inputWav, string outputWav)
{
// 1) 读取 WAV -> 单声道 float 数组(归一化到 [-1,1])
if (!WavIo.TryRead(inputWav, out float[] samples, out int sampleRate, out _))
throw new InvalidOperationException($"[DPDFNet] 无法读取 WAV: {inputWav}");
if (sampleRate != 16000)
Debug.LogWarning($"[DPDFNet] 输入采样率为 {sampleRate} Hz,DPDFNet 模型为 16 kHz 训练,建议先重采样到 16000 再处理(此处按原率处理,结果可能异常)。");
// 2) 加载模型(using 释放,替代原先经 DpdfNetProcessor.Dispose 释放)
using var session = new OnnxRuntimeSession(modelPath);
var cfg = DpdfNetModelConfig.FromSession(session);
// 3) 离线增强(严格对齐 infer_dpdfnet_onnx.enhance_file_onnx)
float[] enhanced = EnhanceOffline(samples, session, cfg);
// 4) 写回 WAV(16 kHz 单声道 16-bit PCM)
WavIo.Write(outputWav, enhanced, 16000, 1);
Debug.Log($"[DPDFNet] 已处理 {samples.Length} 样本 -> {outputWav}");
}
/// <summary>
/// 离线增强,逐行对应 infer_dpdfnet_onnx.enhance_file_onnx(attn_limit 缺省关闭):
/// 末端补 win_len -> center=True 反射填充 STFT -> 流式 ONNX(state) ->
/// center=True ISTFT(win^2 归一化 OLA, 两端裁 n_fft/2) ->
/// postprocess_spec(裁 win_len*2, 补 win_len*2) -> fit_length。
/// </summary>
static float[] EnhanceOffline(float[] x, IOnnxSession session, DpdfNetModelConfig cfg)
{
int nFft = cfg.n_fft; // 320
int hop = cfg.hop_length; // 160
int winLen = cfg.window_length; // 320
float[] win = VorbisWindow.Compute(winLen);
int half = nFft / 2;
// 1) 末端补 win_len 个 0:np.pad(waveform, (0, win_len), mode='constant')
int L = x.Length;
float[] xpad = new float[L + winLen];
Array.Copy(x, 0, xpad, 0, L);
// 2) center=True 反射填充 n_fft//2(librosa pad_mode='reflect')
float[] xp = ReflectPad(xpad, half);
// 3) center=True 分帧 STFT(librosa.stft,逐帧 rfft)
int numFrames = 1 + (xp.Length - nFft) / hop;
var specRI = new float[numFrames][];
for (int t = 0; t < numFrames; t++)
{
var frame = new float[nFft];
int off = t * hop;
for (int i = 0; i < nFft; i++) frame[i] = xp[off + i] * win[i];
specRI[t] = StftMath.Rfft(frame);
}
// 4) 流式 ONNX(state 逐帧回传,对应 run_onnx_streaming)
float[] state = cfg.BuildInitialState();
var specE = new float[numFrames][];
for (int t = 0; t < numFrames; t++)
{
session.Run(specRI[t], state, out var se, out var st);
specE[t] = se;
state = st;
}
// 5) center=True ISTFT(librosa.istft):win^2 归一化 OLA,再两端裁 n_fft//2
int expected = nFft + hop * (numFrames - 1);
float[] y = new float[expected];
float[] winSum = new float[expected];
for (int t = 0; t < numFrames; t++)
{
float[] td = StftMath.Irfft(specE[t], nFft);
int off = t * hop;
for (int i = 0; i < nFft; i++)
{
y[off + i] += td[i] * win[i];
winSum[off + i] += win[i] * win[i];
}
}
for (int i = 0; i < expected; i++)
y[i] = winSum[i] > 1e-12f ? y[i] / winSum[i] : 0f;
int waveELen = expected - 2 * half;
float[] waveE = new float[waveELen];
Array.Copy(y, half, waveE, 0, waveELen);
// 6) postprocess_spec:裁掉前 win_len*2,末尾补 win_len*2 个 0(长度不变)
int drop = winLen * 2;
float[] post = new float[waveELen];
if (waveELen > drop)
Array.Copy(waveE, drop, post, 0, waveELen - drop);
// 末尾 drop 个 0 由零初始化
// 7) fit_length:对齐到原始长度
float[] outArr = new float[L];
int copyLen = Math.Min(L, post.Length);
Array.Copy(post, 0, outArr, 0, copyLen);
return outArr;
}
/// <summary>np.pad(x, pad, mode='reflect'):不含边缘重复的对称反射填充。</summary>
static float[] ReflectPad(float[] x, int pad)
{
if (pad <= 0) return (float[])x.Clone();
int L = x.Length;
float[] r = new float[L + 2 * pad];
for (int i = 0; i < L; i++) r[pad + i] = x[i];
for (int i = 0; i < pad; i++) r[i] = x[pad - i]; // x[pad]..x[1]
for (int i = 0; i < pad; i++) r[pad + L + i] = x[L - 2 - i]; // x[L-2]..x[L-1-pad]
return r;
}
}
/// <summary>
/// 极简 WAV 读写(仅依赖 System.IO / UnityEngine.Debug),覆盖常见 PCM 格式。
/// 读:自动转为单声道 float[-1,1];写:16-bit PCM(单/多声道)。
/// 仅作示例用途;生产环境可替换为 NAudio / Unity AudioClip 等成熟实现。
/// </summary>
public class WavIo
{
public static bool TryRead(string path, out float[] mono, out int sampleRate, out int channels)
{
mono = null; sampleRate = 0; channels = 0;
try
{
using var fs = new FileStream(path, FileMode.Open, FileAccess.Read);
using var br = new BinaryReader(fs);
if (new string(br.ReadChars(4)) != "RIFF") return false;
br.ReadInt32(); // chunk size
if (new string(br.ReadChars(4)) != "WAVE") return false;
int fmtSampleRate = 0, fmtChannels = 0, fmtBits = 0, audioFormat = 0;
byte[] data = null;
while (fs.Position < fs.Length)
{
string id = new string(br.ReadChars(4));
int size = br.ReadInt32();
if (id == "fmt ")
{
audioFormat = br.ReadInt16();
fmtChannels = br.ReadInt16();
fmtSampleRate = br.ReadInt32();
br.ReadInt32(); // byteRate
br.ReadInt16(); // blockAlign
fmtBits = br.ReadInt16();
int skip = size - 16;
if (skip > 0) br.ReadBytes(skip);
}
else if (id == "data")
{
data = br.ReadBytes(size);
}
else
{
br.ReadBytes(size);
}
}
if (data == null || fmtSampleRate == 0) return false;
sampleRate = fmtSampleRate;
channels = fmtChannels;
mono = Decode(data, audioFormat, fmtChannels, fmtBits);
return true;
}
catch (Exception e)
{
Debug.LogError($"[DPDFNet] WAV 读取失败: {e.Message}");
return false;
}
}
private static float[] Decode(byte[] data, int audioFormat, int channels, int bits)
{
int bytesPerSample = bits / 8;
int totalSamples = data.Length / (bytesPerSample * channels);
float[] outBuf = new float[totalSamples];
int offset = 0;
for (int i = 0; i < totalSamples; i++)
{
float sum = 0f;
for (int c = 0; c < channels; c++)
{
int pos = offset + c * bytesPerSample;
float v = 0f;
if (audioFormat == 3) // IEEE float
{
if (bits == 32) v = BitConverter.ToSingle(data, pos);
else if (bits == 64) v = (float)BitConverter.ToDouble(data, pos);
}
else if (bits == 16)
{
v = BitConverter.ToInt16(data, pos) / 32768f;
}
else if (bits == 24)
{
int s = (data[pos + 2] << 16) | (data[pos + 1] << 8) | data[pos];
if ((s & 0x800000) != 0) s |= unchecked((int)0xFF000000);
v = s / 8388608f;
}
else if (bits == 32)
{
v = BitConverter.ToInt32(data, pos) / 2147483648f;
}
else if (bits == 8)
{
v = (data[pos] - 128) / 128f;
}
sum += v;
}
outBuf[i] = sum / channels;
offset += bytesPerSample * channels;
}
return outBuf;
}
public static void Write(string path, float[] samples, int sampleRate, int channels)
{
using var fs = new FileStream(path, FileMode.Create, FileAccess.Write);
using var bw = new BinaryWriter(fs);
int dataLen = samples.Length * 2 * channels; // 16-bit
bw.Write("RIFF".ToCharArray());
bw.Write(36 + dataLen);
bw.Write("WAVE".ToCharArray());
bw.Write("fmt ".ToCharArray());
bw.Write(16); // PCM fmt chunk 大小
bw.Write((short)1); // PCM
bw.Write((short)channels);
bw.Write(sampleRate);
bw.Write(sampleRate * channels * 2); // byteRate
bw.Write((short)(channels * 2)); // blockAlign
bw.Write((short)16); // bitsPerSample
bw.Write("data".ToCharArray());
bw.Write(dataLen);
if (channels == 1)
{
foreach (float s in samples)
bw.Write(Clamp16(s));
}
else
{
// 示例简化:samples 视为单声道,写入第 0 声道,其余声道置 0
for (int i = 0; i < samples.Length; i++)
{
bw.Write(Clamp16(samples[i]));
for (int c = 1; c < channels; c++) bw.Write((short)0);
}
}
}
private static short Clamp16(float s)
{
float v = Math.Max(-1f, Math.Min(1f, s));
return (short)(v * 32767f);
}
}
}
using System;
using System.Collections;
using System.Collections.Generic;
using System.Threading;
using UnityEngine;
namespace DPDFNetUnity
{
/// <summary>
/// 实时麦克风降噪 Demo(无 OnAudioFilterRead / 无重采样架构)。
///
/// 参考原工程 DPDFNet/real_time_demo.py 的本质:采集率 == 模型率 == 播放率(均为 16 kHz),
/// 因此全程不需要重采样,也不存在此前 OnAudioFilterRead(data 处于输出率) 导致的采样率错位与队列漂移。
///
/// 管线:
/// 麦克风(16000) --Microphone.GetData 逐帧--> DPDFNetProcessor.Process(hop=160)
/// --> 增强样本入播放环形队列 --> PCMReaderCallback 喂给 playClip --> AudioSource 播放。
///
/// 说明:
/// - DPDFNetProcessor 对齐 Python stream.py 的 StreamEnhancer:win_len 帧缓冲,
/// 首窗未满(~20ms)无输出,之后每次调用返回 1 个 hop 的增强样本(可变长,可能为空)。
/// - 用 Microphone.GetData 轮询读取(带环绕安全处理 + 跨帧 leftover 续传,不丢样本)。
/// - 播放通过 AudioClip.Create 的 PCMReaderCallback 提供数据,AudioSource 直接播放该 clip。
/// PCMReaderCallback 是 clip 的数据供给回调,并非 OnAudioFilterRead。
/// - 采集时钟与播放时钟均源自同一音频硬件、同为 16000,速率天然匹配,环形队列仅作平滑缓冲。
/// 建议戴耳机避免啸叫。
/// </summary>
[RequireComponent(typeof(AudioSource))]
public class DpdfNetMicrophoneDemo : MonoBehaviour
{
[Header("模型")]
[Tooltip("StreamingAssets/dpdfnet 下的模型名(不含扩展名)")] public string modelName = "dpdfnet2";
[Tooltip("请求麦克风采样率,必须与模型一致(16000)")] public int captureSampleRate = 16000;
[Header("输出")]
[Range(0f, 1f)] public float playbackMix = 0f; // 0=纯增强, 1=纯原始
public bool monitorBypass = false; // true=直出原始麦克风(诊断用)
public bool enableAgc = true;
[Header("调试")]
public float lastInferenceMs;
[Tooltip("每 N 秒打印一次诊断(RMS/帧耗时)")] public float diagInterval = 0.5f;
private DpdfNetProcessor processor;
private AudioClip micClip;
private AudioClip playClip;
private string micDevice;
private AudioSource src;
private int micRate;
// 播放环形队列(主线程写,音频线程读)。用 lock 保护。
private readonly Queue<float> playbackQueue = new Queue<float>();
private readonly object queueLock = new object();
private const int MaxQueueSamples = 16000 * 4; // 4s 上限,溢出丢弃最旧
// 麦克风读取游标与跨帧续传
private int micPrevPos;
private readonly Queue<float> pending = new Queue<float>();
private float[] mixBuf;
private bool running;
// 诊断累计
private float diagTimer;
private float inRmsSum, outRmsSum;
private int rmsFrames;
private bool loggedModelError;
private float agcGain = 1f;
// ------ 后台推理(Loom)相关 ------
// DPDFNet 是 stateful 流式模型:state 必须逐帧串行传递,且 Process 内部维护
// win_len 输入帧缓冲(StreamEnhancer 语义)。用 inferLock 把「推理 + 原始样本
// 对齐出队」包成原子,既保证帧顺序不被并发打乱,也保证 rawDelay 队列一致性。
private readonly object inferLock = new object();
// 原始样本对齐队列:输出流是输入流的延迟镜像(输出样本 m ↔ 输入样本 m),
// Process 每吐出 N 个增强样本,就从队头取 N 个原始样本与之配对(供 playbackMix)。
private readonly Queue<float> rawDelay = new Queue<float>();
private int inflight; // 在途推理任务数(背压计数)
private const int MaxInflight = 2; // 背压上限:超过则丢弃本帧
private int droppedFrames; // 诊断:因背压丢弃的帧数
void Start() => Initialize();
void OnDisable() => Cleanup();
void OnDestroy() => Cleanup();
[ContextMenu("Reinitialize")]
public void Initialize()
{
Cleanup();
// 零依赖:先加载 ONNX,再从 session 推导配置与初始 state(不再读 JSON)。
string onnxPath = System.IO.Path.Combine(Application.streamingAssetsPath, "dpdfnet", modelName + ".onnx");
if (!System.IO.File.Exists(onnxPath))
{
Debug.LogError($"[DPDFNet] 未找到 ONNX 模型: {onnxPath}");
return;
}
IOnnxSession session;
try
{
session = new OnnxRuntimeSession(onnxPath);
}
catch (Exception e)
{
Debug.LogError($"[DPDFNet] 加载 ONNX 失败(是否已安装 Microsoft.ML.OnnxRuntime?):{e.Message}");
return;
}
var cfg = DpdfNetModelConfig.FromSession(session);
if (cfg.sample_rate != captureSampleRate)
Debug.LogWarning($"[DPDFNet] 模型率 {cfg.sample_rate} 与 captureSampleRate {captureSampleRate} 不一致,建议统一为 16000。");
processor = new DpdfNetProcessor(cfg, session);
mixBuf = new float[processor.HopLength];
if (Microphone.devices.Length == 0)
{
Debug.LogError("[DPDFNet] 未检测到麦克风设备。");
return;
}
micDevice = Microphone.devices[0];
// 直接以模型率(16000)录制,原生即 16k,无需重采样。
micClip = Microphone.Start(micDevice, true, 1, captureSampleRate);
micRate = micClip.frequency;
// 播放 clip:16k、单声道、循环、由 PCMReaderCallback 持续供给增强后的样本。
playClip = AudioClip.Create("DPDFNetOut", captureSampleRate * 2, 1, captureSampleRate, true, OnAudioRead, null);
src = GetComponent<AudioSource>();
src.clip = playClip;
src.loop = true;
src.playOnAwake = false;
src.Play();
micPrevPos = 0;
pending.Clear();
lock (inferLock) rawDelay.Clear();
lock (queueLock) playbackQueue.Clear();
running = true;
StartCoroutine(CaptureLoop());
Debug.Log($"[DPDFNet] 采集率={micRate}, 模型率={cfg.sample_rate}, 播放率={captureSampleRate}(全程 16k,无重采样)");
}
private IEnumerator CaptureLoop()
{
int total = micClip.samples;
int hop = processor.HopLength;
float[] hopBuf = new float[hop];
while (running && micClip != null)
{
int pos = Microphone.GetPosition(micDevice);
int avail = (pos - micPrevPos + total) % total;
if (avail > 0)
{
// 环绕安全读取 avail 个样本到 region(最多 2 次 GetData)。
float[] region = new float[avail];
int first = Math.Min(avail, total - micPrevPos);
if (first == avail)
{
micClip.GetData(region, micPrevPos);
}
else
{
float[] head = new float[first];
micClip.GetData(head, micPrevPos);
Array.Copy(head, 0, region, 0, first);
float[] tail = new float[avail - first];
micClip.GetData(tail, 0);
Array.Copy(tail, 0, region, first, avail - first);
}
micPrevPos = (micPrevPos + avail) % total;
// 入 pending,按 hop 精确切帧(跨帧 leftover 续传,不丢样本)。
for (int i = 0; i < avail; i++) pending.Enqueue(region[i]);
while (pending.Count >= hop)
{
for (int i = 0; i < hop; i++) hopBuf[i] = pending.Dequeue();
DispatchHop(hopBuf); // 主线程只负责读+提交,重活在 Loom 后台线程
}
}
yield return null;
}
}
/// <summary>
/// 主线程调度:算出输入 RMS,把 hop 提交到后台线程推理(Loom,不阻塞主线程)。
/// 背压:在途任务超过上限则丢弃本帧(降质但不无限延迟)。
/// </summary>
private void DispatchHop(float[] hop)
{
double inSq = 0;
for (int i = 0; i < hop.Length; i++) inSq += hop[i] * hop[i];
float inRms = (float)Math.Sqrt(inSq / hop.Length + 1e-12f);
if (monitorBypass)
{
CommitOutput(hop, hop, inRms, 0f); // 跳过模型,原样透传(主线程同步)
return;
}
// 背压:在途推理任务过多则丢弃本帧,避免 ThreadPool 堆积与无限延迟。
if (Interlocked.Increment(ref inflight) > MaxInflight)
{
Interlocked.Decrement(ref inflight);
droppedFrames++;
return;
}
// 拷贝 hop(hopBuf 会被协程下一轮复用),后台线程持有独立副本。
float[] task = new float[hop.Length];
Array.Copy(hop, task, hop.Length);
Loom.RunAsync(
() => DoInfer(task, inRms),
(ex) => { Interlocked.Decrement(ref inflight); Debug.LogError($"[DPDFNet] 推理异常:{ex.Message}"); });
}
/// <summary>
/// 后台线程:调用 ONNX 推理。用 inferLock 把「推理 + 原始样本对齐出队」包成原子------
/// 保证流式 state 的帧顺序(stateful 模型必须逐帧串行),并保证 rawDelay 队列一致。
/// DpdfNetProcessor.Process 为 StreamEnhancer 语义:返回本次已提交的增强样本
/// (hop 的整数倍,首窗未满时为空),输出样本 m 与输入样本 m 一一对应(延迟镜像),
/// 故增强结果与 rawDelay 队头的原始样本严格对齐。推理耗时测好后再回主线程落地。
/// </summary>
private void DoInfer(float[] hop, float inRms)
{
var sw = System.Diagnostics.Stopwatch.StartNew();
try
{
float[] enh;
float[] rawAligned;
lock (inferLock)
{
// 原始样本先进对齐队列,再推理:输出是输入的延迟镜像(m↔m),
// Process 吐出的 N 个样本对应队头最旧的 N 个原始样本。
for (int i = 0; i < hop.Length; i++) rawDelay.Enqueue(hop[i]);
enh = processor.Process(hop); // 新数组实例(StreamEnhancer),无需拷走
rawAligned = new float[enh.Length];
for (int i = 0; i < enh.Length; i++)
rawAligned[i] = rawDelay.Count > 0 ? rawDelay.Dequeue() : enh[i];
}
double ms = sw.ElapsedTicks * 1000.0 / System.Diagnostics.Stopwatch.Frequency;
Loom.QueueOnMainThread(() => CommitOutput(enh, rawAligned, inRms, (float)ms));
}
finally
{
Interlocked.Decrement(ref inflight);
}
}
/// <summary>
/// 主线程落地:混合(playbackMix) + 块级 AGC + 入播放队列 + 诊断累计。
/// 由 Loom 泵送(或 monitorBypass 时由 DispatchHop 同步)调用,始终在主线程。
/// enh 为 StreamEnhancer 语义的可变长输出(hop 的整数倍;首窗未满 win_len 预热期为空),
/// rawHop 为与 enh 逐样本对齐的原始样本(DoInfer 中经 rawDelay 出队配对)。
/// </summary>
private void CommitOutput(float[] enh, float[] rawHop, float inRms, float inferMs)
{
lastInferenceMs = inferMs;
if (enh == null || enh.Length == 0) return; // 预热期无输出,跳过
float outRms;
lock (queueLock)
{
if (mixBuf == null || mixBuf.Length < enh.Length) mixBuf = new float[enh.Length];
double mSq = 0;
for (int i = 0; i < enh.Length; i++)
{
float raw = (rawHop != null && i < rawHop.Length) ? rawHop[i] : enh[i];
float v = (1f - playbackMix) * enh[i] + playbackMix * raw;
mixBuf[i] = v;
mSq += v * v;
}
float rms = (float)Math.Sqrt(mSq / enh.Length + 1e-12f);
if (enableAgc)
{
const float targetRms = 0.12f, floor = 1e-3f, minGain = 0.25f, maxGain = 8f;
float desired = Mathf.Clamp(targetRms / Mathf.Max(rms, floor), minGain, maxGain);
agcGain = Mathf.Lerp(agcGain, desired, 0.05f);
for (int i = 0; i < enh.Length; i++)
mixBuf[i] = Mathf.Clamp(mixBuf[i] * agcGain, -1f, 1f);
outRms = (float)Math.Sqrt((mSq * agcGain * agcGain) / enh.Length + 1e-12f);
}
else
{
for (int i = 0; i < enh.Length; i++)
mixBuf[i] = Mathf.Clamp(mixBuf[i], -1f, 1f);
outRms = rms;
}
for (int i = 0; i < enh.Length; i++) playbackQueue.Enqueue(mixBuf[i]);
while (playbackQueue.Count > MaxQueueSamples) playbackQueue.Dequeue(); // 溢出丢弃最旧
}
AccumDiag(inRms, outRms);
}
// 由 Unity 音频线程调用,把增强样本供给 playClip。
private void OnAudioRead(float[] data)
{
lock (queueLock)
{
for (int i = 0; i < data.Length; i++)
data[i] = playbackQueue.Count > 0 ? playbackQueue.Dequeue() : 0f;
}
}
private void AccumDiag(float inRms, float outRms)
{
inRmsSum += inRms;
outRmsSum += outRms;
rmsFrames++;
}
void Update()
{
diagTimer += Time.deltaTime;
if (diagTimer >= diagInterval && rmsFrames > 0)
{
float avgIn = inRmsSum / rmsFrames;
float avgOut = outRmsSum / rmsFrames;
int q;
lock (queueLock) q = playbackQueue.Count;
Debug.Log($"[DPDFNet] inRMS={avgIn:F4} outRMS={avgOut:F4} infer={lastInferenceMs:F2}ms bypass={monitorBypass} qPlay={q} drop={droppedFrames}");
inRmsSum = outRmsSum = 0f;
rmsFrames = 0;
diagTimer = 0f;
}
}
private void Cleanup()
{
running = false;
StopAllCoroutines();
if (micDevice != null && micClip != null && Microphone.IsRecording(micDevice))
Microphone.End(micDevice);
if (src != null && src.isPlaying) src.Stop();
processor?.Dispose();
processor = null;
micDevice = null;
micClip = null;
playClip = null;
pending.Clear();
lock (inferLock) rawDelay.Clear();
lock (queueLock) playbackQueue.Clear();
}
}
}
最后是工程地址
https://github.com/xue-fei/dpdfnet-unity.git