feat(audio): per-stream mix controls in Windows client + vc_get_remote_stream getter

PerUserTuningDialog previously broadcast one gain/mute/NR set to *all* of a
user's streams, even though the core mixer (AudioEngine::RemoteStream) and
the C ABI (vc_set_remote_stream) were already per-stream. The UI had no
per-mix controls anywhere.

Reworks the dialog to enumerate ListUserStreams on open and render one row
per stream (kind + label + Gain + Mute + NR), each wiring only to its own
stream_id. Adds a read-back ABI counterpart, vc_get_remote_stream, so the
dialog opens at the listener's actual current per-stream settings (defaults
1.0/unmuted/NR-off) rather than always 100%. Additive ABI change only; no
existing symbols touched.

Tests: test_m3_multistream extended with getter round-trip assertions; new
C# smoke test exercises the full P/Invoke marshaling path with two clients.
Docs: voice.md §10 notes the getter. NR checkbox keeps its honest
'passthrough' label (NS DSP still unbuilt per §8).
This commit is contained in:
2026-06-18 02:06:44 +02:00
parent 1dfe3c95ed
commit d397731db9
15 changed files with 402 additions and 82 deletions

View File

@@ -81,6 +81,11 @@ internal static class Marshaling
return result;
}
public static RemoteStreamState ToManaged(in VcRemoteStreamStateNative native) => new(
native.Gain,
native.Muted != 0,
native.NoiseReduction != 0);
public static AudioConfigInfo ToManaged(in VcAudioConfigNative native) => new(
native.Codec,
native.Mode != 0,

View File

@@ -51,6 +51,11 @@ public sealed record StreamSummary(
VcStreamKind Kind,
string Label);
public sealed record RemoteStreamState(
float Gain,
bool Muted,
bool NoiseReduction);
public sealed record DeviceInfo(
string Id,
string Name,

View File

@@ -81,6 +81,10 @@ internal static partial class NativeMethods
internal static partial VcResult vc_set_remote_stream(nint c, uint userId, uint streamId,
float gain, int muted, int noiseReduction);
[LibraryImport(LibName)]
internal static partial VcResult vc_get_remote_stream(nint c, uint userId, uint streamId,
out VcRemoteStreamStateNative outState);
[LibraryImport(LibName)]
internal static partial VcResult vc_get_stream_audio_config(nint c, uint userId,
uint streamId, out VcAudioConfigNative outCfg);

View File

@@ -156,6 +156,14 @@ internal struct VcStreamSummaryListNative
public nuint Count;
}
[StructLayout(LayoutKind.Sequential)]
internal struct VcRemoteStreamStateNative
{
public float Gain;
public int Muted;
public int NoiseReduction;
}
[StructLayout(LayoutKind.Sequential)]
internal struct VcAccountNative
{

View File

@@ -208,6 +208,13 @@ public sealed class VoiceCatClient : IDisposable
NativeMethods.vc_set_remote_stream(_handle.DangerousGetHandle(), userId, streamId, gain,
muted ? 1 : 0, noiseReduction ? 1 : 0);
public (VcResult Result, RemoteStreamState? State) GetRemoteStream(uint userId, uint streamId)
{
var r = NativeMethods.vc_get_remote_stream(_handle.DangerousGetHandle(), userId,
streamId, out var native);
return (r, r == VcResult.Ok ? Marshaling.ToManaged(in native) : null);
}
public (VcResult Result, AudioConfigInfo? Config) GetStreamAudioConfig(uint userId, uint streamId)
{
var r = NativeMethods.vc_get_stream_audio_config(_handle.DangerousGetHandle(), userId,