Retire legacy implementations and flatten managed layout
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
using VoiceCat.Audio;
|
||||
using Voicecat.V1;
|
||||
|
||||
namespace VoiceCat.Windows;
|
||||
|
||||
public sealed partial class VoiceCatClient
|
||||
{
|
||||
private sealed record PcmRegistration(nint Callback, nint User);
|
||||
private PcmRegistration? pcm;
|
||||
public (VcResult Result, uint StreamId) StartStream(VcStreamKind kind, string label) => Start(kind, label, false);
|
||||
public (VcResult Result, uint StreamId) StartStreamExternalFeed(VcStreamKind kind, string label) => Start(kind, label, true);
|
||||
private (VcResult, uint) Start(VcStreamKind kind, string label, bool external)
|
||||
{
|
||||
StreamInfo? stream = null;
|
||||
try
|
||||
{
|
||||
stream = core.StartStreamAsync((StreamKind)kind, label).GetAwaiter().GetResult();
|
||||
local[stream.StreamId] = new(stream, external);
|
||||
if (!external && backend is not null) captures[stream.StreamId] = Capture(stream);
|
||||
return (VcResult.Ok, stream.StreamId);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
if (stream is not null) { local.TryRemove(stream.StreamId, out _); core.StopStream(stream.StreamId); }
|
||||
Queue(new(VcEventType.Error, Result: VcResult.Audio, Text: exception.Message)); return (VcResult.Audio, 0);
|
||||
}
|
||||
}
|
||||
private IAudioCapture Capture(StreamInfo stream) => backend!.OpenCapture(devices.GetValueOrDefault(stream.StreamId), stream.Kind == StreamKind.StreamScreenAudio,
|
||||
(samples, channels) => StreamFeedPcm(stream.StreamId, samples, samples.Length / channels, (uint)channels));
|
||||
public VcResult StopStream(uint id)
|
||||
{
|
||||
if (captures.Remove(id, out var capture)) capture.Dispose();
|
||||
if (!local.TryRemove(id, out LocalStream? stream)) return VcResult.InvalidArg;
|
||||
try { core.StopStream(stream.Info.StreamId); return VcResult.Ok; } catch { return VcResult.NotConnected; }
|
||||
}
|
||||
public VcResult SetInputDevice(uint id, string? device)
|
||||
{
|
||||
devices[id] = device;
|
||||
if (!captures.Remove(id, out var previous)) return VcResult.Ok;
|
||||
previous.Dispose();
|
||||
try { var info = local[id].Info.Clone(); info.StreamId = id; captures[id] = Capture(info); return VcResult.Ok; }
|
||||
catch { return VcResult.Audio; }
|
||||
}
|
||||
public VcResult SetCaptureChannels(uint id, uint channels)
|
||||
{ try { LocalStream stream = local[id]; core.Audio.SetCaptureChannels(stream.Info.StreamId, checked((int)channels)); stream.CaptureChannels = (int)channels; return VcResult.Ok; } catch { return VcResult.InvalidArg; } }
|
||||
public VcResult AudioRestart()
|
||||
{
|
||||
foreach (uint id in captures.Keys.ToArray()) { VcResult result = SetInputDevice(id, devices.GetValueOrDefault(id)); if (result != VcResult.Ok) return result; }
|
||||
return VcResult.Ok;
|
||||
}
|
||||
public VcResult SetInputMode(VcInputMode mode) { if (!Enum.IsDefined(mode)) return VcResult.InvalidArg; core.Audio.InputMode = (AudioInputMode)mode; return VcResult.Ok; }
|
||||
public VcResult SetVadThreshold(float threshold) { if (!float.IsFinite(threshold) || threshold is < 0 or > 1) return VcResult.InvalidArg; core.Audio.VadThreshold = threshold; return VcResult.Ok; }
|
||||
public VcResult SetPushToTalk(bool active) { core.Audio.PushToTalk = active; return VcResult.Ok; }
|
||||
public VcResult SetSelfMute(bool muted, bool deafened) { core.Audio.MicMuted = muted; core.Audio.Deafened = deafened; return VcResult.Ok; }
|
||||
public VcResult SetOutputVolume(float gain) { if (!float.IsFinite(gain) || gain is < 0 or > 4) return VcResult.InvalidArg; core.Audio.OutputGain = gain; return VcResult.Ok; }
|
||||
public VcResult SetInputGain(float gain) { if (!float.IsFinite(gain) || gain is < 0 or > 4) return VcResult.InvalidArg; core.Audio.InputGain = gain; return VcResult.Ok; }
|
||||
public VcResult SetInputNoiseReduction(bool enabled) { core.Audio.InputNoiseReduction = enabled; return VcResult.Ok; }
|
||||
public VcResult SetAudioBufferMilliseconds(int milliseconds)
|
||||
{
|
||||
try
|
||||
{
|
||||
core.Audio.DeviceBufferMilliseconds = milliseconds;
|
||||
if (Volatile.Read(ref playback) is { } output) output.BufferMilliseconds = milliseconds;
|
||||
return VcResult.Ok;
|
||||
}
|
||||
catch (ArgumentOutOfRangeException) { return VcResult.InvalidArg; }
|
||||
}
|
||||
public VcResult SetRemoteStream(uint userId, uint streamId, float gain, bool muted, bool nr)
|
||||
{ try { core.Audio.SetRemotePlayback(userId, streamId, gain, muted, nr); return VcResult.Ok; } catch { return VcResult.InvalidArg; } }
|
||||
public (VcResult Result, RemoteStreamState? State) GetRemoteStream(uint userId, uint streamId)
|
||||
{
|
||||
var state = core.Audio.GetRemotePlayback(userId, streamId);
|
||||
return state is { } value ? (VcResult.Ok, new(value.Gain, value.Muted, value.NoiseReduction)) : (VcResult.InvalidArg, null);
|
||||
}
|
||||
public (VcResult Result, AudioConfigInfo? Config) GetStreamAudioConfig(uint userId, uint streamId)
|
||||
{
|
||||
var info = core.Users.FirstOrDefault(u => u.Id == userId)?.Streams.FirstOrDefault(s => s.StreamId == streamId);
|
||||
if (core.Authentication?.Self.Id == userId && local.TryGetValue(streamId, out LocalStream? own)) info = own.Info;
|
||||
return info is null ? (VcResult.InvalidArg, null) : (VcResult.Ok, Audio(info.Audio));
|
||||
}
|
||||
public VcResult StreamFeedPcm(uint id, ReadOnlySpan<short> samples, int samplesPerChannel, uint channels)
|
||||
{
|
||||
if (samplesPerChannel < 0 || channels is not (1 or 2) || samples.Length != (long)samplesPerChannel * channels) return VcResult.InvalidArg;
|
||||
if (!local.TryGetValue(id, out LocalStream? stream)) return VcResult.InvalidArg;
|
||||
core.Audio.FeedPcm(Volatile.Read(ref stream.Info).StreamId, samples, (int)channels); return VcResult.Ok; // A full real-time ring drops, never waits.
|
||||
}
|
||||
public VcResult SetPcmSink(nint callback, nint user) { Volatile.Write(ref pcm, callback == 0 ? null : new(callback, user)); return VcResult.Ok; }
|
||||
private unsafe void ForwardPcm(uint userId, uint streamId, ReadOnlySpan<short> samples, int channels)
|
||||
{
|
||||
var target = Volatile.Read(ref pcm); if (target is null) return;
|
||||
fixed (short* input = samples)
|
||||
((delegate* unmanaged[Cdecl]<nint, uint, uint, short*, nuint, uint, uint, void>)target.Callback)(target.User, userId, streamId, input, (nuint)(samples.Length / channels), (uint)channels, 48000);
|
||||
}
|
||||
|
||||
// Server-authoritative moves/edits stop old SSRCs. Reannounce with the new channel
|
||||
// configuration while keeping UI/external-feed ids stable; captures continue feeding
|
||||
// the alias and switch to the new audio owner only when negotiation completes.
|
||||
private void ReconcileStreams()
|
||||
{
|
||||
var active = core.LocalStreams;
|
||||
foreach (LocalStream stream in local.Values)
|
||||
if (!active.Any(s => s.StreamId == stream.Info.StreamId) && Interlocked.CompareExchange(ref stream.Restarting, 1, 0) == 0) _ = RestartAsync(stream);
|
||||
}
|
||||
private async Task RestartAsync(LocalStream stream)
|
||||
{
|
||||
try
|
||||
{
|
||||
StreamInfo previous = stream.Info;
|
||||
StreamInfo next = await core.StartStreamAsync(previous.Kind, previous.Label, stream.CaptureChannels).ConfigureAwait(false);
|
||||
if (!local.ContainsKey(stream.Alias)) core.StopStream(next.StreamId);
|
||||
else Volatile.Write(ref stream.Info, next);
|
||||
}
|
||||
catch (Exception exception) { Queue(new(VcEventType.Error, Result: VcResult.Audio, Text: exception.Message)); }
|
||||
finally { Volatile.Write(ref stream.Restarting, 0); }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user