Files
voice-cat/clients/windows/VoiceCat.Interop/NativeMethods.cs
Talon 6fe7bf0158
Some checks failed
Build Linux Binaries / linux/amd64 (push) Has been cancelled
Build Linux Binaries / linux/arm64 (push) Has been cancelled
feat: fix voice join/leave, channel edit defaults, channel-update stream restart
Three bugs fixed across the full stack (proto/server/core/ABI/Win/macOS/iOS):

1. Join/Leave Voice now truly subscribes/unsubscribes from the voice plane.
   Previously the button only toggled the local mic — receiving was always on
   (gated by channel membership alone). Added a protocol-level voice subscription
   concept: new SubscribeVoiceRequest/UnsubscribeVoiceRequest/VoiceSubscriptionResult
   proto messages, User.voice_subscribed field, vc_join_voice/vc_leave_voice C ABI
   functions, VC_EVENT_VOICE_STATE event, server-side voice_subscribed flag checked
   by the SFU relay recipient filter, and core-client gating of remote-stream
   decoder setup. All three clients rewired to subscribe+mic on Join / unsubscribe
   on Leave. Text chat works regardless of voice subscription.

2. Channel edit dialog now shows the channel's actual current settings. The read
   struct vc_channel was missing sort_order and audio fields — only the write
   struct vc_channel_info had them. Extended vc_channel with both (additive, no
   ABI break), updated the session model and list_channels marshaling to populate
   them, and updated all three clients' edit callers to use actual channel info
   instead of hardcoded defaults.

3. Channel parameter updates now automatically restart everyone's streams.
   Previously editing a channel's audio config persisted and broadcast a
   ChannelEvent::UPDATED, but no layer restarted streams — encoders/decoders are
   frozen at announce time. handle_channel_event now detects audio-config changes
   on the user's current channel and stop->starts each active local stream. The
   server reads the updated config on re-announce; peers wire up fresh decoders
   at the new ssrc.

All 29 CTest tests pass; Windows DLL + C# client build clean. Apple clients not
yet compile-verified (Windows environment).
2026-06-24 14:29:39 +02:00

229 lines
12 KiB
C#

using System.Runtime.InteropServices;
// Raw P/Invoke surface over core/include/voicecat.h, via LibraryImport (source-generated —
// no runtime reflection marshaling stub; see docs/tech-stack.md §2). One entry per voicecat.h
// function. `vc_client*` is represented as a raw `nint` here — VoiceCatClientHandle (a
// SafeHandle) owns the create/destroy lifetime one level up; these declarations never see a
// SafeHandle directly, per .NET's own SafeHandle convention.
//
// "voicecat" resolves to voicecat.dll via the OS's standard DLL search order (same directory
// as the .exe first) — see clients/windows/README.md for how it gets there at build time.
namespace VoiceCat.Interop;
internal static partial class NativeMethods
{
private const string LibName = "voicecat";
// ── Lifecycle ────────────────────────────────────────────────────────────────────────
// NOTE: these two return `const char*` pointing at STATIC string literals the core never
// expects the caller to free. Declaring them as `string` with StringMarshalling.Utf8
// would be wrong: the built-in Utf8StringMarshaller's return-value convention assumes the
// native callee allocated the string FOR this call and that the marshaller should free it
// afterward — calling that on a static literal corrupts the heap (confirmed: it crashes
// with STATUS_HEAP_CORRUPTION / 0xC0000374). Return the raw pointer instead and convert
// with Marshal.PtrToStringUTF8 ourselves, without ever freeing it — see VoiceCatClient.cs.
[LibraryImport(LibName)]
internal static partial nint vc_version_string();
[LibraryImport(LibName)]
internal static partial nint vc_result_string(VcResult code);
[LibraryImport(LibName)]
internal static partial nint vc_client_create(in VcConfigNative cfg, VcCallbacksNative cb);
[LibraryImport(LibName)]
internal static partial void vc_client_destroy(nint c);
// ── Connection & auth (async; results via on_event) ────────────────────────────────────
[LibraryImport(LibName, StringMarshalling = StringMarshalling.Utf8)]
internal static partial VcResult vc_connect(nint c, string host, ushort port);
[LibraryImport(LibName)]
internal static partial VcResult vc_disconnect(nint c);
[LibraryImport(LibName, StringMarshalling = StringMarshalling.Utf8)]
internal static partial VcResult vc_authenticate_guest(nint c, string nickname);
[LibraryImport(LibName, StringMarshalling = StringMarshalling.Utf8)]
internal static partial VcResult vc_authenticate_user(nint c, string username, string password);
// ── Channels ─────────────────────────────────────────────────────────────────────────
[LibraryImport(LibName, StringMarshalling = StringMarshalling.Utf8)]
internal static partial VcResult vc_join_channel(nint c, uint channelId, string? password);
[LibraryImport(LibName)]
internal static partial VcResult vc_leave_channel(nint c);
[LibraryImport(LibName)]
internal static partial VcResult vc_join_voice(nint c);
[LibraryImport(LibName)]
internal static partial VcResult vc_leave_voice(nint c);
// ── Local media streams ─────────────────────────────────────────────────────────────────
[LibraryImport(LibName)]
internal static partial VcResult vc_stream_start(nint c, in VcStreamDescNative desc,
out uint outStreamId);
[LibraryImport(LibName)]
internal static partial VcResult vc_stream_stop(nint c, uint streamId);
[LibraryImport(LibName, StringMarshalling = StringMarshalling.Utf8)]
internal static partial VcResult vc_set_input_device(nint c, uint streamId, string? deviceId);
[LibraryImport(LibName)]
internal static partial VcResult vc_set_capture_channels(nint c, uint streamId, uint channels);
[LibraryImport(LibName)]
internal static partial VcResult vc_audio_restart(nint c);
[LibraryImport(LibName)]
internal static partial VcResult vc_set_input_mode(nint c, VcInputMode mode);
[LibraryImport(LibName)]
internal static partial VcResult vc_set_vad_threshold(nint c, float threshold);
[LibraryImport(LibName)]
internal static partial VcResult vc_set_push_to_talk(nint c, int active);
[LibraryImport(LibName)]
internal static partial VcResult vc_set_self_mute(nint c, int micMuted, int deafened);
[LibraryImport(LibName)]
internal static partial VcResult vc_set_output_volume(nint c, float gain);
[LibraryImport(LibName)]
internal static partial VcResult vc_set_input_gain(nint c, float gain);
[LibraryImport(LibName)]
internal static partial VcResult vc_set_input_noise_reduction(nint c, int enable);
[LibraryImport(LibName)]
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);
// TEST-ONLY in the core (see voicecat.h) — declared for ABI parity; the real app never
// calls this (no microphone-bypass path in production UI).
[LibraryImport(LibName)]
internal static unsafe partial VcResult vc_test_inject_capture(nint c, uint streamId,
short* pcm, nuint samples);
// ── External PCM feed / tap ──────────────────────────────────────────────────────────
[LibraryImport(LibName)]
internal static unsafe partial VcResult vc_stream_feed_pcm(nint c, uint streamId,
short* pcm, nuint samplesPerChannel, uint channels);
// Delegate type for the PCM sink callback — callers convert to a native function
// pointer via Marshal.GetFunctionPointerForDelegate (for instance members) or by casting
// a static lambda to delegate* unmanaged<> (for [UnmanagedCallersOnly] statics).
// Keep the delegate alive for the lifetime of the sink registration.
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate void VcPcmSinkCallback(IntPtr user, uint userId, uint streamId,
IntPtr pcm, nuint samplesPerChannel, uint channels, uint sampleRate);
// cb is a raw function pointer (IntPtr.Zero = disable). Use
// Marshal.GetFunctionPointerForDelegate(sinkDelegate) to convert from VcPcmSinkCallback.
[LibraryImport(LibName)]
internal static partial VcResult vc_set_pcm_sink(nint c, nint cb, IntPtr user);
// ── Text ─────────────────────────────────────────────────────────────────────────────
[LibraryImport(LibName, StringMarshalling = StringMarshalling.Utf8)]
internal static partial VcResult vc_send_text(nint c, VcTextScope scope, uint targetId,
string utf8);
// ── Device enumeration ───────────────────────────────────────────────────────────────
[LibraryImport(LibName)]
internal static partial VcResult vc_list_devices(nint c, VcDeviceKind kind,
out VcDeviceListNative outList);
[LibraryImport(LibName)]
internal static partial void vc_free_device_list(ref VcDeviceListNative list);
// ── M4: channel / user / stream snapshot getters ────────────────────────────────────────
[LibraryImport(LibName)]
internal static partial VcResult vc_list_channels(nint c, out VcChannelListNative outList);
[LibraryImport(LibName)]
internal static partial void vc_free_channel_list(ref VcChannelListNative list);
[LibraryImport(LibName)]
internal static partial VcResult vc_list_users(nint c, out VcUserListNative outList);
[LibraryImport(LibName)]
internal static partial void vc_free_user_list(ref VcUserListNative list);
[LibraryImport(LibName)]
internal static partial VcResult vc_list_user_streams(nint c, uint userId,
out VcStreamSummaryListNative outList);
[LibraryImport(LibName)]
internal static partial void vc_free_stream_summary_list(ref VcStreamSummaryListNative list);
// ── M4: TOFU server-identity gate ───────────────────────────────────────────────────────
[LibraryImport(LibName)]
internal static partial VcResult vc_confirm_server_identity(nint c, int accept);
[LibraryImport(LibName)]
internal static partial VcResult vc_get_server_identity_display(nint c, nint outBuf,
nuint bufCap, out nuint outLen);
// ── M5: Moderation & admin ─────────────────────────────────────────────────────────────
[LibraryImport(LibName, StringMarshalling = StringMarshalling.Utf8)]
internal static partial VcResult vc_kick_user(nint c, uint userId, string? reason);
[LibraryImport(LibName, StringMarshalling = StringMarshalling.Utf8)]
internal static partial VcResult vc_ban_user(nint c, uint userId, string? reason,
ulong expiresUnixMs);
[LibraryImport(LibName)]
internal static partial VcResult vc_set_permission(nint c, uint userId,
in VcPermissionsNative perms);
[LibraryImport(LibName)]
internal static partial VcResult vc_set_server_mute(nint c, uint userId, int muted,
int deafened);
[LibraryImport(LibName)]
internal static partial VcResult vc_move_user(nint c, uint userId, uint channelId);
[LibraryImport(LibName)]
internal static partial VcResult vc_create_channel(nint c, in VcChannelInfoNative info);
[LibraryImport(LibName)]
internal static partial VcResult vc_edit_channel(nint c, in VcChannelInfoNative info);
[LibraryImport(LibName)]
internal static partial VcResult vc_delete_channel(nint c, uint channelId);
[LibraryImport(LibName, StringMarshalling = StringMarshalling.Utf8)]
internal static partial VcResult vc_create_account(nint c, string username, string password);
[LibraryImport(LibName, StringMarshalling = StringMarshalling.Utf8)]
internal static partial VcResult vc_reset_password(nint c, string username,
string newPassword);
[LibraryImport(LibName, StringMarshalling = StringMarshalling.Utf8)]
internal static partial VcResult vc_delete_account(nint c, string username);
[LibraryImport(LibName)]
internal static partial VcResult vc_list_accounts(nint c);
[LibraryImport(LibName)]
internal static partial VcResult vc_get_account_list(nint c, out VcAccountListNative outList);
[LibraryImport(LibName)]
internal static partial void vc_free_account_list(ref VcAccountListNative list);
[LibraryImport(LibName)]
internal static partial VcResult vc_get_permissions(nint c, out VcPermissionsNative outPerms);
}