8.4 KiB
VoiceCat .NET rewrite
The first slice targets .NET 10: protobuf, control framing, voice headers, and media encryption, TLS 1.3, persisted TOFU pins, server credentials, and an initial managed control server. Media relay, client state, audio, and UI migration are next. The existing C++ implementation remains the conformance oracle.
Codec/DSP wrappers now cover Opus, DRED recovery, RNNoise, and energy VAD. Build the desktop native library before running their tests (CMake and a C compiler required):
./dotnet/build-native.ps1
The script downloads upstream Opus 1.5.2 with a pinned SHA-256, builds DRED-enabled
Opus and the existing vendored RNNoise model, and stages voicecat_media plus license
notices under dotnet/artifacts/native/. It builds independently of the C++ core and
vcpkg. On Windows, Visual Studio's C++ workload works with the default generator;
for this repository's MinGW toolchain use:
./dotnet/build-native.ps1 -Generator Ninja -CCompiler C:/tools/msys64/ucrt64/bin/cc.exe
Linux/macOS can run the same script with PowerShell, or use CMake directly:
cmake -S dotnet/native -B dotnet/artifacts/native-build -DCMAKE_BUILD_TYPE=Release
cmake --build dotnet/artifacts/native-build --target voicecat_media --parallel 2
cmake --install dotnet/artifacts/native-build --component DotnetMedia --prefix dotnet/artifacts/native
MSBuild copies the staged library into managed build/publish output for the selected
RID. Override VoiceCatNativeRid or VoiceCatNativeDirectory for explicit staging;
RuntimeIdentifier takes priority over the SDK's host RID. Cross-compilation is not
automatic. iOS static linking and audio-device shims belong to later client phases.
Native codec/DSP tests require this library; they do not silently skip.
From the repository root:
dotnet restore dotnet/VoiceCat.slnx --locked-mode
dotnet build dotnet/VoiceCat.slnx -c Release --no-restore
dotnet test dotnet/VoiceCat.slnx -c Release --no-build
Dependencies are pinned in project files and lock files. Generated protobuf is build
output; the schema remains core/proto/voicecat.proto. Production dependencies are
Google.Protobuf (BSD-3-Clause), BouncyCastle.Cryptography (MIT), and the build-only
Grpc.Tools (Apache-2.0). No GPL/LGPL dependencies are permitted.
C# conventions
Use file-scoped namespaces, standard .NET naming, immutable values where useful, and spans for binary data. Invalid arguments throw; invalid network packets use parsing results or protocol exceptions. Async APIs accept cancellation tokens.
Comments explain constraints that cannot be made clear in code. Avoid banners,
implementation history, and narration. Keep durable design explanations in docs/.
Regenerating C++ fixtures
The optional oracle target calls the existing C++ protobuf, header serializer, and libsodium media implementation. From the root, with the development dependencies:
cmake --preset dev -DVOICECAT_BUILD_DOTNET_ORACLE=ON
cmake --build --preset dev --target voicecat-dotnet-oracle
New-Item -ItemType Directory -Force dotnet/tests/VoiceCat.Tests/Fixtures
./build/dev/bin/voicecat-dotnet-oracle.exe dotnet/tests/VoiceCat.Tests/Fixtures/cpp-wire.json
git diff -- dotnet/tests/VoiceCat.Tests/Fixtures/cpp-wire.json
On Linux/macOS, omit .exe and create the directory with mkdir -p.
The oracle writes deterministic JSON directly, avoiding shell output encoding.
Fixtures contain a framed ClientHello and media packets at counters 0, 1, 65535,
and 65536. Keys contain bytes 0–31; payload bytes count upward from zero. The
20-byte header has type 1, marker flag, codec 0, SSRC 0xcafebabe, timestamp 960.
Both managed crypto backends must match these bytes.
The DSP oracle calls the existing C++ ApmProcessor with 200 deterministic noise
frames and records the final 960 samples. Regenerate its fixture with:
cmake --build --preset dev --target voicecat-dotnet-dsp-oracle
./build/dev/bin/voicecat-dotnet-dsp-oracle.exe dotnet/tests/VoiceCat.Tests/Fixtures/cpp-noise.json
The managed test allows a one-unit PCM difference for floating-point rounding.
TLS interoperability
The optional TLS oracle uses the existing mbedTLS context and libsodium media crypto. The test authenticates an encrypted challenge in both directions, proving exporter compatibility without sending raw keys. It also loads the C++ server's credential files.
cmake --build --preset dev --target voicecat-dotnet-tls-oracle
$env:VOICECAT_TLS_ORACLE = (Resolve-Path build/dev/bin/voicecat-dotnet-tls-oracle.exe).Path
dotnet test dotnet/VoiceCat.slnx -c Release --no-restore
On Linux/macOS, set VOICECAT_TLS_ORACLE to the absolute executable path without
.exe. Without that variable, only this native interoperability test is skipped;
managed TLS loopback, rejection, persistence, and wire tests still run. CI's C++
conformance job requires the native test. See docs/api-dotnet.md for ownership
and certificate acceptance requirements.
Managed server checkpoint
Run the TLS control server on loopback (optional arguments: data directory, TCP port):
dotnet run --project dotnet/src/VoiceCat.Server -c Release -- ./voicecat-data 7443
./build/dev/bin/vccli.exe --host 127.0.0.1 --port 7443 --nick Guest --text "hello"
It creates or imports server_identity.key, server.crt, server.key, and
voicecat.db. An empty channel table gets Lobby and Music Room; existing channels
are preserved. Guests are enabled by the CLI; hosting VoiceServer directly can
disable them. Existing accounts authenticate without resetting passwords. Account
creation is currently available through AccountStore; bootstrap/admin CLI and
wire administration are pending.
Tests cover real TLS sockets, authentication retries, snapshots, channel moves, text routing, sender attribution, ping, and disconnect events. Enable native checks:
cmake --build --preset dev --target voicecat-dotnet-password-oracle voicecat-dotnet-database-oracle vccli
$env:VOICECAT_DATABASE_ORACLE = (Resolve-Path build/dev/bin/voicecat-dotnet-database-oracle.exe).Path
$env:VOICECAT_VCCLI = (Resolve-Path build/dev/bin/vccli.exe).Path
dotnet test dotnet/VoiceCat.slnx -c Release --no-restore
The database oracle creates an account/channel using the shipped C++ database code;
managed code imports and authenticates it, then C++ authenticates a managed-created
account. CI also regenerates the libsodium password fixture. Native checks require
the optional VOICECAT_BUILD_DOTNET_ORACLE=ON configure flag and a real-deps build.
The server also advertises UDP on the TCP port number, supports voice subscription
and stream signaling, and reseals encoded audio for subscribers in the same channel.
UDP binding fixes the first endpoint for the session; reconnect after endpoint changes.
Protected joins, administration, moderation and production configuration remain
before Phase 4 completion. The server's media-aware reaper defaults to 45 seconds
of inactivity with a 15-second sweep. Parsed control envelopes, valid encrypted
voice and keepalives from bound endpoints refresh activity; invalid media does not.
VoiceServerOptions configures timeouts and capacity; zero idle timeout disables
reaping. The constructor overload accepts TimeProvider for deterministic expiry tests.
Enable deterministic native voice interoperability (no audio hardware required):
cmake --build --preset dev --target voicecat-dotnet-voice-oracle
$env:VOICECAT_VOICE_ORACLE = (Resolve-Path build/dev/bin/voicecat-dotnet-voice-oracle.exe).Path
dotnet test dotnet/VoiceCat.slnx -c Release --no-restore
Two existing C++ clients authenticate, join Lobby or Music Room, publish three
concurrent streams, feed PCM, and verify decoded energy and metadata in both directions.
The native clients use external capture/playback to avoid device dependencies in CI.
MediaFanoutTests separately verifies 50-subscriber routing/resealing without managed
allocations after warm-up and reports throughput; socket scheduling is excluded.
The transport load test delivers all 2,500 recipient packets from a paced 50 pps sender.
Native vccli --test-tone-ms 4000 runs finite external capture/playback, feeds a tone,
and fails without decoded remote audio. Tests start two CLI processes in mono/stereo
channels and also verify channel text. Normal --voice now explicitly subscribes before
announcing its microphone stream. No C ABI or wire changes were needed.