using System.Runtime.InteropServices; using Microsoft.Win32.SafeHandles; namespace VoiceCat.Dsp; public sealed unsafe partial class RnnoiseProcessor : IDisposable { public const int SampleRate = 48000; public const int FrameSamples = 480; private readonly RnnoiseHandle handle; private readonly float[] input = new float[FrameSamples]; private readonly float[] output = new float[FrameSamples]; public RnnoiseProcessor() { handle = new(Create()); if (handle.IsInvalid) { handle.Dispose(); throw new OutOfMemoryException(); } } public void Process(Span pcm, int sampleRate = SampleRate) { ObjectDisposedException.ThrowIf(handle.IsClosed, this); if (sampleRate != SampleRate) return; if (pcm.Length % FrameSamples != 0) throw new ArgumentException("RNNoise requires complete 480-sample mono chunks.", nameof(pcm)); fixed (float* source = input) fixed (float* destination = output) { for (int offset = 0; offset < pcm.Length; offset += FrameSamples) { for (int i = 0; i < FrameSamples; i++) input[i] = pcm[offset + i]; ProcessFrame(handle, destination, source); for (int i = 0; i < FrameSamples; i++) pcm[offset + i] = (short)Math.Clamp(MathF.Round(output[i], MidpointRounding.AwayFromZero), short.MinValue, short.MaxValue); } } } public void Dispose() => handle.Dispose(); [LibraryImport("voicecat_media", EntryPoint = "vcm_rnnoise_create")] private static partial nint Create(); [LibraryImport("voicecat_media", EntryPoint = "vcm_rnnoise_destroy")] private static partial void Destroy(nint state); [LibraryImport("voicecat_media", EntryPoint = "vcm_rnnoise_process")] private static partial float ProcessFrame(RnnoiseHandle state, float* output, float* input); private sealed class RnnoiseHandle : SafeHandleZeroOrMinusOneIsInvalid { public RnnoiseHandle() : base(true) { } internal RnnoiseHandle(nint value) : this() => SetHandle(value); protected override bool ReleaseHandle() { Destroy(handle); return true; } } }