Retire legacy implementations and flatten managed layout
Build and test / test (macos-latest) (push) Canceled after 0s
Build and test / test (ubuntu-24.04) (push) Canceled after 0s
Build and test / test (windows-latest) (push) Canceled after 0s
Build and test / apple-client (push) Canceled after 0s

This commit is contained in:
2026-09-21 00:11:32 +02:00
parent dd811a0bb8
commit 08e6c5930a
422 changed files with 252 additions and 38242 deletions
+48
View File
@@ -0,0 +1,48 @@
namespace VoiceCat.Dsp;
public sealed class EnergyVadProcessor
{
private readonly TimeProvider timeProvider;
private long lastVoiceTimestamp;
private bool hasVoice;
private float threshold;
public float Threshold
{
get => Volatile.Read(ref threshold);
set
{
if (!float.IsFinite(value) || value is < 0 or > 1) throw new ArgumentOutOfRangeException(nameof(value));
Volatile.Write(ref threshold, value);
}
}
public TimeSpan HangTime { get; }
public EnergyVadProcessor(float threshold = 0.02f, TimeSpan? hangTime = null, TimeProvider? timeProvider = null)
{
Threshold = threshold;
HangTime = hangTime ?? TimeSpan.FromMilliseconds(300);
if (HangTime < TimeSpan.Zero) throw new ArgumentOutOfRangeException(nameof(hangTime));
this.timeProvider = timeProvider ?? TimeProvider.System;
}
public bool Process(ReadOnlySpan<short> pcm)
{
long now = timeProvider.GetTimestamp();
if (!pcm.IsEmpty)
{
double sum = 0;
foreach (short sample in pcm)
{
double normalized = sample / 32768.0;
sum += normalized * normalized;
}
if (Math.Sqrt(sum / pcm.Length) >= Threshold)
{
lastVoiceTimestamp = now;
hasVoice = true;
}
}
return hasVoice && timeProvider.GetElapsedTime(lastVoiceTimestamp, now) < HangTime;
}
}
+63
View File
@@ -0,0 +1,63 @@
using System.Runtime.InteropServices;
using System.Reflection;
using Microsoft.Win32.SafeHandles;
namespace VoiceCat.Dsp;
public sealed unsafe partial class RnnoiseProcessor : IDisposable
{
private const string NativeLibrary = "voicecat_media";
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];
static RnnoiseProcessor()
{
if (OperatingSystem.IsIOS()) System.Runtime.InteropServices.NativeLibrary.SetDllImportResolver(typeof(RnnoiseProcessor).Assembly, ResolveIosStaticLibrary);
}
private static nint ResolveIosStaticLibrary(string libraryName, Assembly assembly, DllImportSearchPath? searchPath) =>
libraryName == NativeLibrary ? System.Runtime.InteropServices.NativeLibrary.GetMainProgramHandle() : 0;
public RnnoiseProcessor()
{
handle = new(Create());
if (handle.IsInvalid) { handle.Dispose(); throw new OutOfMemoryException(); }
}
public void Process(Span<short> 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(NativeLibrary, EntryPoint = "vcm_rnnoise_create")]
private static partial nint Create();
[LibraryImport(NativeLibrary, EntryPoint = "vcm_rnnoise_destroy")]
private static partial void Destroy(nint state);
[LibraryImport(NativeLibrary, 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; }
}
}
+5
View File
@@ -0,0 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
</Project>
+8
View File
@@ -0,0 +1,8 @@
{
"version": 1,
"dependencies": {
"net10.0": {},
"net10.0/ios-arm64": {},
"net10.0/iossimulator-arm64": {}
}
}
+6
View File
@@ -0,0 +1,6 @@
{
"version": 1,
"dependencies": {
"net10.0": {}
}
}
@@ -0,0 +1,14 @@
{
"version": 1,
"dependencies": {
"net10.0": {
"Microsoft.NET.ILLink.Tasks": {
"type": "Direct",
"requested": "[10.0.7, )",
"resolved": "10.0.7",
"contentHash": "AA/yhzFHNtQZXLdqjzujPy25G8EWwGWsAnxOE2zYSBoT/8QHP6ketN3CToD3DFreO653ipUwnKHo22B8AlBMCw=="
}
},
"net10.0/win-x64": {}
}
}