Add managed codec DSP and initial control server

This commit is contained in:
2026-09-15 22:51:33 +02:00
parent 2df79cdd4c
commit 4067bab7c2
52 changed files with 2503 additions and 20 deletions
+96
View File
@@ -115,3 +115,99 @@ On Unix new files use owner read/write permissions; Windows inherits directory A
The credential directory must have one provisioning owner. PEM strings and crypto
library internal copies are managed memory; owned-array clearing does not promise
erasure of every runtime/library copy.
## Codec and DSP
`VoiceCat.Codec` and `VoiceCat.Dsp` call the desktop `voicecat_media` native library
through source-generated `LibraryImport`. It links pinned Opus 1.5.2 and the existing
vendored RNNoise; it has no dependency on libvoicecat or its C ABI. Fixed C signatures
wrap Opus controls so P/Invoke never calls C varargs. SafeHandle owns every native
encoder, decoder, DRED parser/state, and denoiser, including failed initialization.
`OpusOptions` is an immutable record. Supported PCM rates are 8/12/16/24/48 kHz,
one or two interleaved channels, and integral 10/20/40/60 ms frames. These match the
current VoiceCat protocol's integer frame duration; fractional Opus frame durations
are not exposed. Low-delay application mode requires at most 20 ms. Channel capture
bandwidth is controlled separately by `MaximumBandwidthHz`; the production audio
clock will remain 48 kHz. Options are validated before native creation, and native
control failures throw `OpusException` with the libopus error code.
`OpusEncoder.Encode(ReadOnlySpan<short>, Span<byte>)` accepts exactly one frame
and returns encoded bytes. `OpusDecoder.Decode(packet, pcm, samplesPerChannel,
recoverPreviousFrame)` returns samples **per channel**, not total interleaved samples.
An empty packet requests PLC. Passing the next packet with `recoverPreviousFrame`
requests in-band FEC; absence of FEC permits libopus's PLC fallback. Decode that next
packet normally afterward. Capacity/overlap errors throw before native processing.
DRED is explicit. Unsupported native builds reject `DeepRedundancy = true` rather
than silently disabling it. With pinned Opus 1.5.2, DRED encoding requires PCM at
16/24/48 kHz; its activity analysis cannot emit DRED at 8/12 kHz. Such configurations
are rejected. DRED packets can still be decoded at all five rates. The encoder uses
a 30 ms minimum redundancy duration because this release needs two redundancy chunks;
the old 20 ms setting produces no DRED packets. Actual redundancy remains adaptive
to bitrate, loss estimate, and activity; it is not guaranteed in every packet.
`OpusDeepRedundancy.TryRecover(audioDecoder, nextPacket, pcm, samplesPerChannel,
offset)` parses the next packet and reconstructs a missing frame. Default offset is
one missing frame's samples per channel before the next packet's start, matching
libopus's offset convention. A packet without DRED returns false; then the owner
can try FEC/PLC. Only consume recovery output on success. Parse/native errors throw.
`RnnoiseProcessor.Process(Span<short>, sampleRate)` operates in place on complete
480-sample mono chunks at 48 kHz. Other rates pass through unchanged; partial chunks
at 48 kHz throw instead of leaving a tail silently untreated. Float scratch is
preallocated, and rounding/clipping matches the C++ processor. Use distinct instances
for stereo channels when the later pipeline supports stereo microphone denoising.
Noise reduction does not gate speech.
`EnergyVadProcessor.Process(ReadOnlySpan<short>)` compares normalized RMS against
`Threshold`, retains speech for `HangTime`, and starts closed. It uses monotonic
`TimeProvider` timestamps; tests inject a clock. Threshold changes are atomic; all
processing state otherwise has one owner. Codec/DSP processing methods allocate no
managed memory after initialization, verified across 1,000 combined cycles. They run
on a managed worker, never the native real-time device callback. Native device rings,
jitter, mixer, and audio scheduling remain later work.
## Initial managed server
`VoiceServer(directory, endpoint, allowGuests, name)` owns credentials, the SQLite
store, a TCP listener and its connection tasks. `EndPoint` reports the actual bound
port (zero requests an ephemeral port). Dispose asynchronously to stop the listener
and wait for all connections. The CLI currently binds loopback and accepts optional
data-directory/port positional arguments.
All control traffic uses TLS 1.3 with the existing v2 protobuf. A single async loop
owns each `TlsSession`; handlers exchange envelopes through bounded queues.
This checkpoint caps connections at 64, queued input at 32 envelopes, queued output
at 64 envelopes, and each control payload at 64 KiB (stricter than the shared framer's
16 MiB limit). Queue exhaustion disconnects slow consumers. Handshake timeout is
15 seconds; completed TLS connections have a 60-second receive-idle timeout.
Authentication starts users in unprotected Lobby (id 1), subject to its capacity.
Success returns permissions, then a cloned snapshot; peers receive joined/updated/left
events. Server-authoritative text replaces supplied sender ids/timestamps, limits
bodies to 4096 UTF-8 bytes, and acknowledges valid or rejected routing. Channel text
requires membership; private text echoes to sender and recipient. Protected channel
joins and all admin/moderation handlers are pending. No UDP port or media features
are advertised; voice subscription explicitly fails until the SFU is implemented.
`AccountStore(path)` retains the C++ schema version 2, accepts version 1 migration,
and rejects unknown revisions. Opening an existing channel table does not reseed it.
Account creation/authentication uses parameterized SQL; two password workers bound
per-store Argon2 work. Failed authentication leaves `last_login` unchanged. Dispose
after its operations finish. Account provisioning currently uses this API or the
existing native administration path; there is no automatic bootstrap account.
`PasswordHasher` uses strict UTF-8 without normalization and libsodium-compatible
Argon2id v19 PHC strings: 16-byte salt, 32-byte output, new-hash parameters
64 MiB memory, two iterations, parallelism one. Verification supports up to 128 MiB,
ten iterations, parallelism four and 1024 UTF-8 password bytes; malformed or excessive
hashes fail closed. Standard C++ interactive-cost accounts are preserved. These
bounds intentionally reject imported hashes above those costs. Native fixtures cover
ASCII, Unicode and embedded NUL; the database oracle verifies cross-implementation
authentication in both directions.
SQLite's MIT provider/bundle uses the pinned public-domain SourceGear SQLite build.
The license audit checks that exact package version and repository identity because
the native package lacks a NuGet license expression; other dependencies still require
an approved permissive expression.