feat(M4): Windows WinForms client, TOFU identity pinning, VAD threshold + always-on mode
Core ABI extensions (voicecat.h):
- vc_list_channels / vc_list_users / vc_list_user_streams — pull-based snapshot getters
for the channel-tree and user-list UI; session_model_mu_ guards cross-thread reads
- VC_EVENT_JOIN_RESULT / vc_join_channel — channel join with optional password
- VC_EVENT_SERVER_IDENTITY + vc_confirm_server_identity — TOFU gate that blocks io_thread_
until the UI approves or rejects; pins TLS leaf-cert SHA-256 (not declared Ed25519)
- vc_get_server_identity_display — Ed25519 fingerprint for human-readable display only
- VC_INPUT_ALWAYS_ON = 2 in vc_input_mode — transmit unconditionally, no VAD gate
- vc_set_vad_threshold — live RMS threshold update (0.0–1.0); EnergyVadProcessor stores
it atomically so the audio RT path reads without a lock
C++ implementation:
- SessionModel::apply_snapshot / apply_channel_event fixed to populate parent_id,
password_protected, and max_users (were permanently zeroed)
- TlsContext::peer_cert_fingerprint — SHA-256 of peer leaf cert DER via mbedTLS
- TofuStore split into peek (read-only) + pin (write) so first-connect only persists
after user approval; tofu_store_path in vc_config for per-user pin file location
- TcpAcceptor uses dual-stack IPv6+IPv4 fallback (fixes localhost → ::1 on Windows)
- windows-client CMake preset: Release shared DLL, static MinGW runtime, no tools/tests
- New C++ tests: test_channel_user_list_abi, test_tofu_flow (14/14 green)
Windows client (clients/windows/ — .NET 10 WinForms):
- VoiceCat.Interop: LibraryImport P/Invoke surface, UnmanagedCallersOnly callbacks,
Channel<VoiceCatEvent> event delivery drained by 30ms WinForms Timer
- VoiceCat.App: ConnectDialog (saved servers, DPAPI password storage), ServerIdentity-
Dialog (TOFU first-connect / mismatch warning), MainForm (channel TreeView, user
ListBox, RichTextBox chat, voice controls, device pickers, VAD/PTT/always-on mode,
per-user gain/mute/NR tuning, VAD sensitivity TrackBar, level meter ProgressBar)
- PttKeyCaptureDialog — focus-scoped PTT key capture (documented limitation)
- PerUserTuningDialog — real-time gain/mute/NR applied to all of a user's streams
- Accessibility: explicit AccessibleName/Description on every control, & mnemonics,
Activity log ListBox as durable screen-reader record, AutomationNotification for
curated live announcements
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 00:35:16 +02:00
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
// ── 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_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_remote_stream(nint c, uint userId, uint streamId,
|
|
|
|
|
float gain, int muted, int noiseReduction);
|
|
|
|
|
|
|
|
|
|
[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);
|
|
|
|
|
|
|
|
|
|
// ── 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);
|
2026-06-17 16:31:29 +02:00
|
|
|
|
|
|
|
|
// ── 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);
|
feat(M4): Windows WinForms client, TOFU identity pinning, VAD threshold + always-on mode
Core ABI extensions (voicecat.h):
- vc_list_channels / vc_list_users / vc_list_user_streams — pull-based snapshot getters
for the channel-tree and user-list UI; session_model_mu_ guards cross-thread reads
- VC_EVENT_JOIN_RESULT / vc_join_channel — channel join with optional password
- VC_EVENT_SERVER_IDENTITY + vc_confirm_server_identity — TOFU gate that blocks io_thread_
until the UI approves or rejects; pins TLS leaf-cert SHA-256 (not declared Ed25519)
- vc_get_server_identity_display — Ed25519 fingerprint for human-readable display only
- VC_INPUT_ALWAYS_ON = 2 in vc_input_mode — transmit unconditionally, no VAD gate
- vc_set_vad_threshold — live RMS threshold update (0.0–1.0); EnergyVadProcessor stores
it atomically so the audio RT path reads without a lock
C++ implementation:
- SessionModel::apply_snapshot / apply_channel_event fixed to populate parent_id,
password_protected, and max_users (were permanently zeroed)
- TlsContext::peer_cert_fingerprint — SHA-256 of peer leaf cert DER via mbedTLS
- TofuStore split into peek (read-only) + pin (write) so first-connect only persists
after user approval; tofu_store_path in vc_config for per-user pin file location
- TcpAcceptor uses dual-stack IPv6+IPv4 fallback (fixes localhost → ::1 on Windows)
- windows-client CMake preset: Release shared DLL, static MinGW runtime, no tools/tests
- New C++ tests: test_channel_user_list_abi, test_tofu_flow (14/14 green)
Windows client (clients/windows/ — .NET 10 WinForms):
- VoiceCat.Interop: LibraryImport P/Invoke surface, UnmanagedCallersOnly callbacks,
Channel<VoiceCatEvent> event delivery drained by 30ms WinForms Timer
- VoiceCat.App: ConnectDialog (saved servers, DPAPI password storage), ServerIdentity-
Dialog (TOFU first-connect / mismatch warning), MainForm (channel TreeView, user
ListBox, RichTextBox chat, voice controls, device pickers, VAD/PTT/always-on mode,
per-user gain/mute/NR tuning, VAD sensitivity TrackBar, level meter ProgressBar)
- PttKeyCaptureDialog — focus-scoped PTT key capture (documented limitation)
- PerUserTuningDialog — real-time gain/mute/NR applied to all of a user's streams
- Accessibility: explicit AccessibleName/Description on every control, & mnemonics,
Activity log ListBox as durable screen-reader record, AutomationNotification for
curated live announcements
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 00:35:16 +02:00
|
|
|
}
|