Files
voice-cat/clients/windows/VoiceCat.Interop.Tests/ExternalPcmTests.cs

56 lines
2.2 KiB
C#
Raw Normal View History

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
using System.Runtime.InteropServices;
using VoiceCat.Interop;
namespace VoiceCat.Interop.Tests;
/// <summary>
/// API-surface smoke tests for StreamFeedPcm / SetPcmSink through the P/Invoke layer.
/// These tests need voicecat.dll but NOT a running server — they verify the C# wrapper
/// compiles, is callable, and the error paths work at the C-ABI boundary. Full E2E relay
/// verification is handled by tests/test_external_pcm.cpp (C++ ctest).
/// </summary>
public sealed class ExternalPcmTests
{
[Fact]
public void StreamFeedPcm_NoActiveStream_ReturnsInvalidArg()
{
using var client = new VoiceCatClient("ext-pcm-test", "0.1", VcLogLevel.Off);
var pcm = new short[960];
// Stream 0 doesn't exist — core must return InvalidArg, not crash.
var result = client.StreamFeedPcm(0, pcm, 960, channels: 1);
Assert.Equal(VcResult.InvalidArg, result);
}
[Fact]
public void StreamFeedPcm_InvalidChannels_ReturnsInvalidArg()
{
using var client = new VoiceCatClient("ext-pcm-test", "0.1", VcLogLevel.Off);
var pcm = new short[960 * 3];
// channels=3 is not supported — trampoline rejects before touching audio state.
var result = client.StreamFeedPcm(0, pcm, 960, channels: 3);
Assert.Equal(VcResult.InvalidArg, result);
}
[Fact]
public void SetPcmSink_Zero_ReturnsOk()
{
using var client = new VoiceCatClient("ext-pcm-test", "0.1", VcLogLevel.Off);
// IntPtr.Zero = disable — default state, must be a no-op that returns Ok.
var result = client.SetPcmSink(IntPtr.Zero, IntPtr.Zero);
Assert.Equal(VcResult.Ok, result);
}
[Fact]
public void SetPcmSink_EnableThenDisable_BothSucceed()
{
using var client = new VoiceCatClient("ext-pcm-test", "0.1", VcLogLevel.Off);
// Keep the delegate alive until after we unregister it (not just until the P/Invoke call).
NativeMethods.VcPcmSinkCallback sink = static (_, _, _, _, _, _, _) => { };
nint fp = Marshal.GetFunctionPointerForDelegate(sink);
Assert.Equal(VcResult.Ok, client.SetPcmSink(fp, IntPtr.Zero));
Assert.Equal(VcResult.Ok, client.SetPcmSink(IntPtr.Zero, IntPtr.Zero));
GC.KeepAlive(sink);
}
}