Files
voice-cat/clients/windows/VoiceCat.Interop/NativeMethods.cs
Talon 615d2a8e5f feat: external PCM feed/tap API (vc_stream_feed_pcm + vc_set_pcm_sink)
Promotes vc_test_inject_capture (mono-only, TEST-ONLY) to a public,
stereo-capable production API and adds a symmetric PCM tap on the
receive side. Enables ReplayKit (iOS), ScreenCaptureKit (macOS), bots,
soundboards, and custom clients — all without a hardware audio device.

Core C++:
- voicecat.h: new vc_stream_feed_pcm, vc_pcm_sink_cb typedef,
  vc_set_pcm_sink; vc_test_inject_capture kept as deprecated alias
- audio_engine: stereo-aware inject_capture (channels param + ring
  reset on channel-count change); atomic pcm_sink_ fired per decoded
  frame in on_playback; RemoteStream carries user_id/stream_id for
  RT-safe sink metadata; init_recv_stream takes user_id+stream_id
- client.cpp: stream_feed_pcm / set_pcm_sink implementations;
  sync_remote_streams passes user_id/stream_id to init_recv_stream
- voicecat.cpp: trampolines + channels=1/2 validation

Tests: test_external_pcm (headless, 3 sub-tests: mono round-trip,
stereo feed L≠R, sink metadata+disable). ctest 23/23.

Swift: feedPcm / setPcmSink in VoiceCatClient.swift + 4 XCTest
smoke tests (ExternalPcmTests.swift).

C#: StreamFeedPcm / SetPcmSink in VoiceCatClient.cs + NativeMethods.cs
(vc_stream_feed_pcm unsafe P/Invoke, VcPcmSinkCallback delegate,
vc_set_pcm_sink via nint) + 4 xUnit smoke tests (ExternalPcmTests.cs).

Docs: architecture.md §4 new subsection, voice.md §9 updated
(macOS/iOS now reference vc_stream_feed_pcm), protocol.md §8 explicit
no-protocol-change note, roadmap.md M5 entry.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-20 17:52:09 +02:00

211 lines
11 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);
// ── 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_output_volume(nint c, float gain);
[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);
}