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

相关推荐
peter67682 天前
unity2d游戏开发经验总结
unity
郝学胜-神的一滴2 天前
游戏设计:原型与开发 19:U3D工位布置与性格说明书
人工智能·算法·unity·unreal engine·游戏策划·游戏设计
Aman19842 天前
Claude Code做Unity项目移植(二): 自动执行只读操作,告别频繁的
unity·游戏开发·vibe coding·claude code
暴走约伯2 天前
【VFX工具链源码拆解 02】告别机械劳动(上):一键统改百个子粒子颜色与批量重命名
3d·unity·c#·材质
郝学胜-神的一滴2 天前
游戏设计:原型与开发 18:Unity入门的第一课
人工智能·程序人生·游戏·ui·unity·游戏引擎
淡海水3 天前
09-01-内存-Unity资源生命周期全景
unity·c#·游戏引擎·.netcore·gc
LF男男3 天前
Flower Defense—UIFacade
unity
呆呆敲代码的小Y3 天前
【实战精选面试题】Unity精选面试题详细解答,按模块整理汇总
人工智能·游戏·unity·游戏引擎
小贺儿开发3 天前
《流光绘忆》Unity 沉浸式光影互动
科技·unity·ai·程序员·工具·互动·光影
Aman19843 天前
Claude Code做Unity项目移植(一):从零散工具到团队式
unity·游戏开发·vibe coding·claude code·项目迁移