01bae734b832295788abf5b0edf1431a06dfc3f0
Joining voice on the voice-chat preset was unreliable: audio arrived after several seconds of the route flipping back and forth, sometimes not at all, and VoiceOver went quiet while it happened. Device logs show why. A graph with voice processing enabled reports a successful start and is then torn down within a second, roughly three times in four; every configuration without voice processing — both microphone presets, and voice chat with processing off — comes up first time and runs indefinitely. With voice processing the input and output are one IO unit, and it only stays up while the input is part of the render chain. The input node carried a tap and no connection, which leaves it out of that chain. Route the input through a silent mixer so it is genuinely rendered. The rest of this is the amplifier rather than the cause, and each part of it turned one failed start into a storm: The stall watchdog rebuilt on every missed tick, without bound. That converted a graph that could not start into endless session reconfiguration, which is what the user heard and what hid the reason from the log. It now backs off after each failed attempt and stops after four, logging VC_WATCHDOG exhausted, so a transient freeze still recovers and a graph that will not start fails visibly. Nothing waited for a graph to start before judging it dead. Enabling voice processing rebuilds both halves of the IO, which posts a configuration change and reads as not running for several hundred milliseconds, so the configuration-change handler and the watchdog both tore down graphs that were about to run. A settling window holds them off for two seconds. A route change forced a full rebuild, and every rebuild moves the route, so one notification produced the next. Route changes now take the non-forcing path, which rebuilds a stopped graph and leaves a healthy one alone; the hardware test it uses reads the input node's format, not AVAudioSession, whose reported rate and channel count do not settle until after the graph has started. The input side is built once per session instead of being added when voice is joined, so joining and leaving voice set a stream id rather than replacing the graph, and a mono voice-chat apply no longer clears a stereo capsule configuration it never applied. Every rebuild now logs its cause, and VC_START/VC_START_CHECK record whether the graph survived its start. The first-attempt failure is not fixed and is recorded in PROGRESS.md as a release gate: capture still comes up on a watchdog rebuild rather than immediately. The changed logic sits on AVAudioSession and AVAudioEngine, which the net10.0 test project cannot reference, so the behaviour is covered by the existing source assertions; verification is on device.
VoiceCat
VoiceCat is a self-hosted, channel-based voice and text chat system built on .NET 10. It uses TLS 1.3 for protobuf control traffic and authenticated encrypted UDP for Opus media. There is no WebRTC, central directory, or plaintext mode.
The repository contains a managed server, CLI, shared client/audio core, and native Windows, macOS, and iOS user interfaces. A small C library supplies Opus/RNNoise, and a small Swift iOS extension captures ReplayKit application audio.
Build
./scripts/build-native.ps1
dotnet restore VoiceCat.slnx --locked-mode
dotnet build VoiceCat.slnx -c Release --no-restore
dotnet test VoiceCat.slnx -c Release --no-build
See CLAUDE.md for the developer map, docs/README.md for current contracts, and PROGRESS.md for the short release handoff.
Layout
proto/ protobuf wire schema
src/ managed protocol, crypto, server, client, audio, and CLI
tests/ managed behavior and integration tests
clients/windows/ WinForms client
clients/apple/ AppKit and UIKit clients
native/media/ narrow Opus/RNNoise C shim
native/rnnoise/ vendored RNNoise source and model
native/apple/broadcast/ ReplayKit broadcast extension
docs/ current contracts and operating documentation
Non-negotiable constraints
- Encryption is mandatory.
- No GPL or LGPL dependencies.
- Real-time audio callbacks never allocate, lock, block, or perform I/O.
- Accessibility is a release requirement on every client platform.
Languages
C++
43.9%
Swift
30.6%
C#
17.5%
Shell
3.6%
CMake
2.1%
Other
2.3%