Unity使用DPDFNet实现实时语音增强(降噪)

原工程地址

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

音频文件处理效果图

主要代码

csharp 复制代码
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);
        }
    }
}
csharp 复制代码
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

相关推荐
平行云PVT2 小时前
国产信创实时云渲染解决方案:如何实现数字孪生与三维应用云化部署?
unity·webrtc·unreal engine·实时云渲染·国产信创·摩尔线程·砺算科技
夏炎黄3 小时前
Terrain地形剪裁
unity
小贺儿开发5 小时前
DeepSeek + Codex + MCP:Unity 游戏开发实测
科技·游戏·unity·ai·程序员·编程·deepseek
新的瑞拉公主5 小时前
Cocos plist 粒子一键导入 Unity:Editor 工具实现与参数映射
unity·游戏开发·cocos2d-x·粒子系统·editor工具
淡海水6 小时前
03-02-架构篇-Boehm GC架构解析
unity·架构·c#·gc·boehm
xcLeigh1 天前
Unity基础:Instantiate实例化——在运行时动态创建游戏物体
游戏·unity·游戏引擎·教程·instantiate
流浪打工人2 天前
Unity3d TimeLine中audio track的使用
unity
淡海水2 天前
02-07-原理篇-GC暂停时间的本质
开发语言·unity·c#·gc·暂停时间
WarPigs2 天前
UI路径配置SO>字典json工作流
ui·unity