Cleanup client and worker pool comments
This commit is contained in:
@@ -40,7 +40,7 @@ std::vector<uint8_t> make_frame(const voicecat::v1::Envelope& env) {
|
||||
|
||||
} // namespace
|
||||
|
||||
// ── vc_client implementation ───────────────────────────────────────────────────
|
||||
|
||||
|
||||
vc_client::vc_client(const vc_config& cfg, vc_callbacks cb) : cfg_(cfg), cb_(cb) {
|
||||
// TOFU: NULL/empty tofu_store_path falls back to a relative default so existing tests
|
||||
@@ -82,7 +82,7 @@ void vc_client::emit_disconnected(vc_result r, const char* reason) {
|
||||
set_state(VC_STATE_DISCONNECTED);
|
||||
}
|
||||
|
||||
// ── Connection ────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
vc_result vc_client::connect(const char* host, uint16_t port) {
|
||||
auto cur = state_net_.load(std::memory_order_acquire);
|
||||
@@ -102,15 +102,6 @@ vc_result vc_client::disconnect() {
|
||||
auto cur = state_net_.load(std::memory_order_acquire);
|
||||
if (cur == VC_STATE_DISCONNECTED && !io_thread_.joinable()) return VC_ERR_NOT_CONNECTED;
|
||||
|
||||
// Graceful disconnect (Tier 5): queue Disconnect{code=0}, set a flag, and let the io
|
||||
// thread drain the queue + send it + exit naturally. The io thread's cleanup handles
|
||||
// teardown_voice() and socket close — the main thread just joins. This avoids the
|
||||
// double-close race that a send_cv_ wait + main-thread socket close would create
|
||||
// (the server closes the connection on receipt of Disconnect, so the io thread exits
|
||||
// while the main thread is still waiting, and both try to close the same socket).
|
||||
// Only when fully authenticated: the server gates Disconnect handling on Authenticated,
|
||||
// and in earlier states (TLS handshake, TOFU gate, auth pending) the io thread may be
|
||||
// blocked outside the read loop (e.g. tofu_cv_) where the flag would never be checked.
|
||||
if (cur == VC_STATE_CONNECTED && io_thread_.joinable()) {
|
||||
voicecat::v1::Envelope env;
|
||||
env.set_request_id(next_req_id_++);
|
||||
@@ -129,7 +120,7 @@ vc_result vc_client::disconnect() {
|
||||
return VC_OK;
|
||||
}
|
||||
|
||||
// Non-graceful path: force-close (original logic). Used when the io thread is already
|
||||
// Non-graceful path: force-close Used when the io thread is already
|
||||
// gone, the connection is in an early state, or as a fallback.
|
||||
io_stop_.store(true, std::memory_order_release);
|
||||
|
||||
@@ -162,13 +153,13 @@ vc_result vc_client::disconnect() {
|
||||
return VC_OK;
|
||||
}
|
||||
|
||||
// ── io_thread_ entry point ────────────────────────────────────────────────────
|
||||
// io_thread_ entry point
|
||||
|
||||
void vc_client::run_io(std::string host, uint16_t port) {
|
||||
udp_host_ = host;
|
||||
set_state(VC_STATE_CONNECTING);
|
||||
|
||||
// ── TCP connect ──────────────────────────────────────────────────────────
|
||||
// TCP connect
|
||||
#ifdef _WIN32
|
||||
WSADATA wsa{};
|
||||
WSAStartup(MAKEWORD(2, 2), &wsa);
|
||||
@@ -211,7 +202,7 @@ void vc_client::run_io(std::string host, uint16_t port) {
|
||||
res = nullptr;
|
||||
io_fd_.store(static_cast<int>(sock), std::memory_order_release);
|
||||
|
||||
// ── TLS handshake ────────────────────────────────────────────────────
|
||||
// TLS handshake
|
||||
set_state(VC_STATE_TLS_HANDSHAKE);
|
||||
|
||||
tls_ = std::make_unique<voicecat::crypto::TlsContext>(
|
||||
@@ -228,12 +219,8 @@ void vc_client::run_io(std::string host, uint16_t port) {
|
||||
}
|
||||
}
|
||||
|
||||
// ── TOFU server-identity gate ────────────────────────────────────────────
|
||||
// Pins the TLS leaf cert's own fingerprint (real, verifiable right here from the
|
||||
// handshake) — NOT the declared Ed25519 server_identity_fingerprint from ServerHello,
|
||||
// which hasn't even arrived yet at this point (it's sent *inside* this now-established
|
||||
// tunnel) and isn't cryptographically bound to this cert anyway (docs/security.md
|
||||
// §1.1). See voicecat.h's vc_tofu_status doc comment.
|
||||
// TOFU server-identity gate
|
||||
|
||||
set_state(VC_STATE_VERIFYING_IDENTITY);
|
||||
{
|
||||
std::array<uint8_t, 32> peer_fp{};
|
||||
@@ -271,7 +258,7 @@ void vc_client::run_io(std::string host, uint16_t port) {
|
||||
return !tofu_decision_pending_ || io_stop_.load(std::memory_order_acquire);
|
||||
});
|
||||
// Timeout or an external stop (disconnect() during the wait) both leave
|
||||
// tofu_decision_pending_ true here — treated as a reject, per voicecat.h.
|
||||
// tofu_decision_pending_ true here — treated as a reject
|
||||
accepted = tofu_decision_pending_ ? false : tofu_accept_;
|
||||
tofu_decision_pending_ = false;
|
||||
}
|
||||
@@ -315,7 +302,7 @@ void vc_client::run_io(std::string host, uint16_t port) {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Read loop ────────────────────────────────────────────────────────
|
||||
// Read loop
|
||||
{
|
||||
voicecat::protocol::FrameCodec codec;
|
||||
std::vector<uint8_t> buf(16384);
|
||||
@@ -340,8 +327,7 @@ void vc_client::run_io(std::string host, uint16_t port) {
|
||||
}
|
||||
|
||||
// Keepalive: send a Ping every ~15s so the server's reaper doesn't drop us
|
||||
// (docs/protocol.md §7). The 50ms read timeout means this loop spins ~20×/s,
|
||||
// plenty of resolution for a 15s interval.
|
||||
|
||||
{
|
||||
auto now_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now().time_since_epoch()).count();
|
||||
@@ -464,7 +450,7 @@ void vc_client::send_ping() {
|
||||
queue_envelope(env);
|
||||
}
|
||||
|
||||
// ── Protocol dispatch ─────────────────────────────────────────────────────────
|
||||
// Protocol dispatch
|
||||
|
||||
void vc_client::handle_envelope(const voicecat::v1::Envelope& env) {
|
||||
switch (env.body_case()) {
|
||||
@@ -502,7 +488,7 @@ void vc_client::handle_envelope(const voicecat::v1::Envelope& env) {
|
||||
handle_voice_subscription_result(env.voice_subscription_result());
|
||||
break;
|
||||
case voicecat::v1::Envelope::kPong: {
|
||||
// Correlate the echoed nonce to measure RTT (docs/protocol.md §7).
|
||||
// Correlate the echoed nonce to measure RTT
|
||||
uint64_t nonce = env.pong().nonce();
|
||||
auto now_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now().time_since_epoch()).count();
|
||||
@@ -626,7 +612,7 @@ void vc_client::handle_channel_event(const voicecat::v1::ChannelEvent& ce) {
|
||||
|
||||
// Detect audio-config changes on our current channel BEFORE apply_channel_event
|
||||
// overwrites the stored config. Encoders/decoders are frozen at StreamAnnounceResult
|
||||
// time (docs/voice.md §3), so a channel audio change requires restarting active
|
||||
// time , so a channel audio change requires restarting active
|
||||
// local streams to pick up the new params.
|
||||
if (ce.kind() == voicecat::v1::ChannelEvent::UPDATED) {
|
||||
updated_channel_id = ce.channel().id();
|
||||
@@ -657,17 +643,12 @@ void vc_client::handle_channel_event(const voicecat::v1::ChannelEvent& ce) {
|
||||
restart_active_streams_for_channel(updated_channel_id);
|
||||
}
|
||||
|
||||
// Same "go look" signal vc_list_channels' callers already poll on after the initial
|
||||
// snapshot — see voicecat.h's VC_EVENT_CHANNEL_LIST doc comment.
|
||||
vc_event ev{};
|
||||
ev.type = VC_EVENT_CHANNEL_LIST;
|
||||
emit(ev);
|
||||
}
|
||||
|
||||
void vc_client::handle_join_channel_result(const voicecat::v1::JoinChannelResult& msg) {
|
||||
// This response only acks the request + carries our private audio config. The
|
||||
// authoritative channel change reaches us as a UserEvent::UPDATED broadcast (the server
|
||||
// no longer excludes the mover), which keeps session_model_ in sync via apply_user_event.
|
||||
vc_event ev{};
|
||||
ev.type = VC_EVENT_JOIN_RESULT;
|
||||
ev.result = msg.ok() ? VC_OK : VC_ERR_PROTOCOL;
|
||||
@@ -748,7 +729,7 @@ void vc_client::handle_disconnect(const voicecat::v1::Disconnect& msg) {
|
||||
emit_disconnected(VC_ERR_IO, msg.reason().c_str());
|
||||
}
|
||||
|
||||
// ── Auth / channel / text commands ───────────────────────────────────────────
|
||||
// Auth / channel / text commands
|
||||
|
||||
vc_result vc_client::authenticate_guest(const char* nickname) {
|
||||
auto cur = state_net_.load(std::memory_order_acquire);
|
||||
@@ -797,7 +778,6 @@ vc_result vc_client::join_channel(uint32_t channel_id, const char* password) {
|
||||
req.set_request_id(next_req_id_++);
|
||||
auto* jc = req.mutable_join_channel();
|
||||
jc->set_channel_id(channel_id);
|
||||
// See voicecat.h's vc_join_channel doc comment.
|
||||
if (password) jc->set_password(password);
|
||||
queue_envelope(req);
|
||||
return VC_OK;
|
||||
@@ -824,7 +804,7 @@ vc_result vc_client::send_text(vc_text_scope scope, uint32_t target_id, const ch
|
||||
return VC_OK;
|
||||
}
|
||||
|
||||
// ── UDP binding ────────────────────────────────────────────────────────────────
|
||||
// UDP binding
|
||||
|
||||
void vc_client::start_udp_binding() {
|
||||
voicecat::v1::Envelope req;
|
||||
@@ -909,7 +889,7 @@ void vc_client::run_udp_recv() {
|
||||
int n = static_cast<int>(::recv(static_cast<sock_t>(fd), reinterpret_cast<char*>(buf.data()),
|
||||
static_cast<int>(buf.size()), 0));
|
||||
if (n < static_cast<int>(voicecat::net::kVoiceHeaderSize)) {
|
||||
// Timeout or short packet — send a KEEPALIVE if the interval has elapsed.
|
||||
// Timeout or short packet, send a KEEPALIVE if the interval has elapsed.
|
||||
send_udp_keepalive();
|
||||
continue;
|
||||
}
|
||||
@@ -917,8 +897,6 @@ void vc_client::run_udp_recv() {
|
||||
voicecat::net::VoiceFrame hdr{};
|
||||
if (!voicecat::net::parse_header(buf.data(), static_cast<size_t>(n), hdr)) continue;
|
||||
if (hdr.type == voicecat::net::kFrameKeepalive) {
|
||||
// Echoed keepalive from the server — media path is alive. (RTT measurement
|
||||
// could be added here later by correlating a nonce; not needed for NAT/timeout.)
|
||||
continue;
|
||||
}
|
||||
if (hdr.type != voicecat::net::kFrameVoice) continue;
|
||||
@@ -953,7 +931,7 @@ void vc_client::send_udp_keepalive() {
|
||||
last_udp_keepalive_ms_.store(now_ms, std::memory_order_release);
|
||||
|
||||
// Plaintext KEEPALIVE: 14-byte header, type=2, no payload, no AEAD. The server
|
||||
// identifies us by the verified UDP endpoint (set during the UdpBinding handshake).
|
||||
// identifies by the verified UDP endpoint (set during the UdpBinding handshake).
|
||||
uint8_t pkt[voicecat::net::kVoiceHeaderSize] = {0};
|
||||
pkt[0] = voicecat::net::kFrameKeepalive;
|
||||
|
||||
@@ -977,7 +955,7 @@ int64_t client_now_ms() {
|
||||
// gap where mode/dtx/complexity/application were silently dropped.
|
||||
voicecat::codec::OpusParams opus_params_from_audio_config(const voicecat::v1::AudioConfig& a) {
|
||||
voicecat::codec::OpusParams p;
|
||||
// Opus always runs at 48 kHz internally (docs/voice.md §3): the whole AudioEngine clock is
|
||||
// Opus always runs at 48 kHz internally: the whole AudioEngine clock is
|
||||
// 48 kHz and external PCM is fed at 48 kHz, so the codec must match regardless of what a
|
||||
// channel advertises. Honoring a non-48k effective sample_rate as the codec rate would
|
||||
// create an encoder expecting e.g. 16k PCM while being fed 48k frames — wrong pitch/duration.
|
||||
@@ -1034,7 +1012,7 @@ void vc_client::on_capture_frame(int kind, const int16_t* pcm, int samples, int
|
||||
auto it = local_streams_.find(kind);
|
||||
if (it == local_streams_.end() || !it->second.active.load(std::memory_order_acquire)) return;
|
||||
// "Mic muted" only gates the MIC stream — a concurrently-running SCREEN_AUDIO share keeps
|
||||
// playing while the user's mic is muted (scope decision — see docs/voice.md).
|
||||
// playing while the user's mic is muted
|
||||
// Server-mute is also a hard gate on MIC transmission.
|
||||
if (kind == static_cast<int>(VC_STREAM_MIC) &&
|
||||
(self_mic_muted_.load(std::memory_order_acquire) ||
|
||||
@@ -1073,9 +1051,7 @@ void vc_client::on_capture_frame(int kind, const int16_t* pcm, int samples, int
|
||||
}
|
||||
}
|
||||
|
||||
// Send-side input gate (docs/voice.md §11) — MIC only. SCREEN_AUDIO/AUX_DEVICE always
|
||||
// bypass this: gating a screen-share on the user's own voice activity would silently drop
|
||||
// shared music/video audio whenever the user isn't talking, which defeats the feature.
|
||||
|
||||
if (kind == static_cast<int>(VC_STREAM_MIC)) {
|
||||
auto mode = current_input_mode_.load(std::memory_order_acquire);
|
||||
if (mode == VC_INPUT_PUSH_TO_TALK) {
|
||||
@@ -1101,7 +1077,7 @@ void vc_client::on_capture_frame(int kind, const int16_t* pcm, int samples, int
|
||||
// (480 @10ms) or larger (1920 @40ms). Reframe to that size before encoding (docs/voice.md §3).
|
||||
const int target = ls.frame_samples;
|
||||
if (samples == target) {
|
||||
// Fast path — the common 20 ms channel: encode the engine frame directly, no buffering.
|
||||
// Fast path, the common 20 ms channel: encode the engine frame directly, no buffering.
|
||||
encode_and_send_frame(ls, pcm, samples, channels, fd);
|
||||
return;
|
||||
}
|
||||
@@ -1226,18 +1202,11 @@ void vc_client::ensure_audio_running() {
|
||||
p.external_capture = it->second.external_feed;
|
||||
}
|
||||
}
|
||||
// iOS unified mode (external_playback): capture is ALWAYS external — the Swift AVAudioEngine
|
||||
// owns the only mic path and feeds via vc_stream_feed_pcm. Force external_capture so start()
|
||||
// never opens a hardware mic device, even when ensure_audio_running runs before a MIC stream
|
||||
// exists (a remote stream arriving first — e.g. the post-auth ServerStateSnapshot — starts
|
||||
// the engine for playback). Without this the core opens a miniaudio capture device that races
|
||||
// the AVAudioEngine tap → dual mic capture → duplicated, crackly audio on the remote end.
|
||||
// No-op on desktop, where external_playback_ is never set.
|
||||
|
||||
if (external_playback_.load(std::memory_order_acquire)) {
|
||||
p.external_capture = true;
|
||||
}
|
||||
// iOS VPIO: skip the hardware playback device and drive the mixer on a timer, delivering the
|
||||
// final mix to the mixed-output sink for the Swift VPIO renderer (vc_set_external_playback).
|
||||
|
||||
audio_engine_.set_external_playback(external_playback_.load(std::memory_order_acquire));
|
||||
audio_engine_.start(p, [this](int kind, const int16_t* pcm, int samples, int channels) {
|
||||
on_capture_frame(kind, pcm, samples, channels);
|
||||
@@ -1264,7 +1233,7 @@ void vc_client::sync_remote_streams(const voicecat::v1::User& user) {
|
||||
remote_streams_[ssrc] = {user.id(), si.stream_id()};
|
||||
|
||||
voicecat::codec::OpusParams p = opus_params_from_audio_config(si.audio());
|
||||
// Only a MIC stream is voice; receive-side NR denoises voice only (docs/voice.md §10).
|
||||
// Only a MIC stream is voice; receive-side NR denoises voice only
|
||||
bool is_voice = si.kind() == voicecat::v1::STREAM_MIC;
|
||||
audio_engine_.init_recv_stream(ssrc, p, user.id(), si.stream_id(), is_voice);
|
||||
bool muted = self_deafened_.load(std::memory_order_acquire) ||
|
||||
@@ -1304,7 +1273,7 @@ void vc_client::sync_remote_streams(const voicecat::v1::User& user) {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Stream / device control ────────────────────────────────────────────────────
|
||||
// Stream / device control
|
||||
|
||||
vc_result vc_client::stream_start(const vc_stream_desc& desc, uint32_t* out_stream_id) {
|
||||
if (state_net_.load(std::memory_order_acquire) != VC_STATE_CONNECTED) return VC_ERR_NOT_CONNECTED;
|
||||
@@ -1338,9 +1307,7 @@ vc_result vc_client::stream_start(const vc_stream_desc& desc, uint32_t* out_stre
|
||||
auto* audio = ann->mutable_requested_audio();
|
||||
audio->set_sample_rate(48000);
|
||||
// bitrate_bps intentionally left unset (0 = "no preference"): the channel's AudioConfig
|
||||
// is authoritative when the channel has one (docs/voice.md §3) — a client-side default
|
||||
// here would otherwise always clamp the channel's bitrate down to this value. frame_ms/
|
||||
// fec are likewise channel-controlled when a channel config exists; these are only used
|
||||
// is authoritative when the channel has one
|
||||
// as a fallback when it doesn't.
|
||||
audio->set_frame_ms(20);
|
||||
audio->set_fec(true);
|
||||
@@ -1384,11 +1351,7 @@ void vc_client::handle_stream_announce_result(uint64_t req_id,
|
||||
}
|
||||
|
||||
// Pre-size the reframe buffers used by on_capture_frame when the channel's frame_ms
|
||||
// differs from the engine's 20 ms (frame_samples != 960). Worst case the accumulator
|
||||
// holds one sub-frame remainder (< frame_samples) plus one engine block (960 samples),
|
||||
// interleaved over up to 2 channels — so (frame_samples + 960) * 2. upmix_scratch sizes
|
||||
// one frame_samples chunk as stereo. Allocated here (control thread), never on the RT
|
||||
// audio thread (architecture.md §3).
|
||||
// differs from the engine's 20 ms
|
||||
const size_t fs = ls.frame_samples;
|
||||
ls.encode_accum.assign((fs + 960) * 2, 0);
|
||||
ls.upmix_scratch.assign(fs * 2, 0);
|
||||
@@ -1478,7 +1441,7 @@ vc_client::LocalStream* vc_client::find_local_stream_by_id(uint32_t stream_id) {
|
||||
}
|
||||
|
||||
void vc_client::restart_active_streams_for_channel(uint32_t channel_id) {
|
||||
// Collect active stream info under the lock, then stop→start outside the lock (stream_stop
|
||||
// Collect active stream info under the lock, then stopstart outside the lock (stream_stop
|
||||
// and stream_start both acquire local_streams_mu_). capture_device_id and capture_channels
|
||||
// survive the restart: stream_stop doesn't clear them, and stream_start reuses the existing
|
||||
// LocalStream entry (auto& ls = local_streams_[kind]) without resetting them.
|
||||
@@ -1509,7 +1472,7 @@ void vc_client::restart_active_streams_for_channel(uint32_t channel_id) {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Voice-plane subscription ──────────────────────────────────────────────────
|
||||
// Voice-plane subscription
|
||||
|
||||
vc_result vc_client::join_voice() {
|
||||
if (state_net_.load(std::memory_order_acquire) != VC_STATE_CONNECTED) return VC_ERR_NOT_CONNECTED;
|
||||
@@ -1623,11 +1586,7 @@ vc_result vc_client::set_input_device(uint32_t stream_id, const char* device_id)
|
||||
if (&entry == ls) { kind = k; break; }
|
||||
}
|
||||
}
|
||||
// Only the real capture device (MIC) is affected by device selection — SCREEN_AUDIO uses
|
||||
// loopback capture (no input device to pick) and AUX_DEVICE isn't backed by a real device
|
||||
// path yet. Restart the engine unconditionally when it's already running so the new device
|
||||
// id takes effect; AudioEngine doesn't expose a getter for "is this the same device" so we
|
||||
// don't try to skip the restart when it happens to be a no-op change.
|
||||
|
||||
if (kind == static_cast<int>(VC_STREAM_MIC) && audio_engine_.running()) {
|
||||
audio_engine_.stop();
|
||||
ensure_audio_running();
|
||||
@@ -1735,8 +1694,8 @@ vc_result vc_client::get_remote_stream(uint32_t user_id, uint32_t stream_id,
|
||||
}
|
||||
if (!found) return VC_ERR_INVALID_ARG;
|
||||
// The (user, stream) is known. The RemoteStream entry may not exist yet if the listener
|
||||
// has neither set any control nor received audio for it — in that case report the
|
||||
// defaults (docs/voice.md §10) so the UI opens at 100/unmuted/NR-off.
|
||||
// has neither set any control nor received audio for it, in that case report the
|
||||
// defaults so the UI opens at 100/unmuted/NR-off.
|
||||
float gain = 1.0f; bool mute = false; bool nr = false;
|
||||
if (audio_engine_.get_stream_state(ssrc, gain, mute, nr)) {
|
||||
out->gain = gain;
|
||||
@@ -1785,7 +1744,7 @@ vc_result vc_client::get_stream_audio_config(uint32_t user_id, uint32_t stream_i
|
||||
out->codec = 0;
|
||||
out->mode = p.stereo ? 1u : 0u;
|
||||
// Report the channel's configured sample_rate (carried as max_bandwidth_hz), not the
|
||||
// fixed 48 kHz codec clock — matches what the remote-stream path below reports.
|
||||
// fixed 48 kHz codec clock, matches what the remote-stream path below reports.
|
||||
out->sample_rate = p.max_bandwidth_hz ? p.max_bandwidth_hz : p.sample_rate;
|
||||
out->bitrate_bps = p.bitrate_bps;
|
||||
out->frame_ms = p.frame_ms;
|
||||
@@ -1850,8 +1809,8 @@ vc_result vc_client::set_mixed_output_sink(vc_mixed_output_cb cb, void* user) {
|
||||
|
||||
vc_result vc_client::set_external_playback(bool enable) {
|
||||
external_playback_.store(enable, std::memory_order_release);
|
||||
// Stored on the engine too; takes effect on the next start()/vc_audio_restart() (matching
|
||||
// the vc_set_capture_channels "apply on next restart" contract).
|
||||
// Stored on the engine too. takes effect on the next start()/vc_audio_restart() (matching
|
||||
// the vc_set_capture_channels "apply on next restart"
|
||||
audio_engine_.set_external_playback(enable);
|
||||
return VC_OK;
|
||||
}
|
||||
@@ -1861,7 +1820,7 @@ vc_result vc_client::test_inject_capture(uint32_t stream_id, const int16_t* pcm,
|
||||
}
|
||||
|
||||
vc_result vc_client::list_devices(vc_device_kind kind, vc_device_list* out) {
|
||||
// Works in any connection state — device pickers need to populate pre-connect.
|
||||
// Works in any connection state, device pickers need to populate pre-connect.
|
||||
auto devices = voicecat::audio::AudioEngine::enumerate_devices(kind == VC_DEVICE_INPUT);
|
||||
|
||||
auto* items = new vc_device[devices.size()];
|
||||
@@ -1880,7 +1839,7 @@ vc_result vc_client::list_devices(vc_device_kind kind, vc_device_list* out) {
|
||||
return VC_OK;
|
||||
}
|
||||
|
||||
// ── Channel/user/stream snapshot getters ──────────────────────────────────────
|
||||
|
||||
|
||||
vc_result vc_client::list_channels(vc_channel_list* out) {
|
||||
std::lock_guard<std::mutex> lk(session_model_mu_);
|
||||
@@ -1958,7 +1917,7 @@ vc_result vc_client::list_user_streams(uint32_t user_id, vc_stream_summary_list*
|
||||
return VC_OK;
|
||||
}
|
||||
|
||||
// ── TOFU server-identity gate ───────────────────────────────────────────────────
|
||||
|
||||
|
||||
vc_result vc_client::confirm_server_identity(bool accept) {
|
||||
std::lock_guard<std::mutex> lk(tofu_mu_);
|
||||
@@ -1983,7 +1942,7 @@ vc_result vc_client::get_server_identity_display(char* out_buf, size_t buf_cap,
|
||||
return VC_OK;
|
||||
}
|
||||
|
||||
// ── Moderation & admin ─────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
vc_result vc_client::kick_user(uint32_t user_id, const char* reason) {
|
||||
if (state_net_.load(std::memory_order_acquire) != VC_STATE_CONNECTED) return VC_ERR_NOT_CONNECTED;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* client.h — the implementation type behind the opaque `vc_client*` handle.
|
||||
* client.h: the implementation type behind the opaque `vc_client*` handle.
|
||||
*/
|
||||
#ifndef VOICECAT_CORE_CLIENT_H
|
||||
#define VOICECAT_CORE_CLIENT_H
|
||||
@@ -73,7 +73,7 @@ struct vc_client {
|
||||
vc_result list_users(vc_user_list* out);
|
||||
vc_result list_user_streams(uint32_t user_id, vc_stream_summary_list* out);
|
||||
|
||||
// TOFU server-identity gate (see voicecat.h's VC_EVENT_SERVER_IDENTITY doc comment).
|
||||
// TOFU server-identity gate
|
||||
vc_result confirm_server_identity(bool accept);
|
||||
vc_result get_server_identity_display(char* out_buf, size_t buf_cap, size_t* out_len);
|
||||
|
||||
@@ -82,8 +82,7 @@ struct vc_client {
|
||||
vc_result get_stream_audio_config(uint32_t user_id, uint32_t stream_id,
|
||||
vc_audio_config* out);
|
||||
|
||||
// External PCM feed (see voicecat.h: vc_stream_feed_pcm). Production API for driving a
|
||||
// local stream's encode pipeline without a hardware capture device. channels = 1 or 2.
|
||||
|
||||
vc_result stream_feed_pcm(uint32_t stream_id, const int16_t* pcm,
|
||||
size_t samples_per_channel, uint32_t channels);
|
||||
|
||||
@@ -94,7 +93,6 @@ struct vc_client {
|
||||
vc_result set_mixed_output_sink(vc_mixed_output_cb cb, void* user);
|
||||
vc_result set_external_playback(bool enable);
|
||||
|
||||
// TEST-ONLY (see voicecat.h) — deprecated alias for stream_feed_pcm(..., channels=1).
|
||||
vc_result test_inject_capture(uint32_t stream_id, const int16_t* pcm, size_t samples);
|
||||
|
||||
// Moderation & admin.
|
||||
@@ -123,7 +121,7 @@ struct vc_client {
|
||||
vc_config cfg_{};
|
||||
vc_callbacks cb_{};
|
||||
|
||||
// ── TCP/TLS control channel ───────────────────────────────────────────────────
|
||||
// TCP/TLS control channel
|
||||
std::atomic<vc_connection_state> state_net_{VC_STATE_DISCONNECTED};
|
||||
|
||||
// Blocking I/O thread (one per vc_client lifetime)
|
||||
@@ -197,7 +195,7 @@ struct vc_client {
|
||||
std::vector<voicecat::v1::AccountEntry> last_account_list_;
|
||||
mutable std::mutex account_list_mu_;
|
||||
|
||||
// ── TOFU server-identity gate ─────────────────────────────────────────────────
|
||||
// TOFU server-identity gate
|
||||
std::unique_ptr<voicecat::crypto::TofuStore> tofu_store_; // owns the pin file
|
||||
std::mutex tofu_mu_;
|
||||
std::condition_variable tofu_cv_;
|
||||
@@ -205,7 +203,7 @@ struct vc_client {
|
||||
bool tofu_accept_{false};
|
||||
std::string pending_identity_fp_hex_; // ServerHello's Ed25519 fp, display-only
|
||||
|
||||
// ── UDP / media plane ──────────────────────────────────────────────────────────
|
||||
// UDP / media plane
|
||||
std::array<uint8_t, 16> udp_token_{};
|
||||
uint16_t server_udp_port_{0};
|
||||
std::string udp_host_;
|
||||
@@ -235,8 +233,7 @@ struct vc_client {
|
||||
// vc_get_stream_audio_config() has something to read back for our own streams.
|
||||
voicecat::codec::OpusParams effective_params;
|
||||
|
||||
// Talk-indicator edge detection (docs/voice.md §7) — updated in on_capture_frame, after
|
||||
// the VAD/PTT gate so a gated-closed frame doesn't show as "talking".
|
||||
|
||||
std::atomic<int64_t> last_capture_ms{0};
|
||||
bool talking = false;
|
||||
|
||||
@@ -263,9 +260,7 @@ struct vc_client {
|
||||
// time and checked in handle_stream_announce_result / stream_stop.
|
||||
bool external_feed = false;
|
||||
|
||||
// Stream label (from vc_stream_desc.label at stream_start time). Retained so the
|
||||
// channel-update stream restart (restart_active_streams_for_channel) can re-announce
|
||||
// with the same label without the caller's involvement.
|
||||
|
||||
std::string label;
|
||||
|
||||
// Reframe buffer: the AudioEngine clock is fixed at 48 kHz / 20 ms, so capture/feed
|
||||
@@ -273,9 +268,8 @@ struct vc_client {
|
||||
// can be 2.5…60 ms, so the encoder needs frame_samples per call (480 @10ms, 1920 @40ms,
|
||||
// …). on_capture_frame accumulates the engine's 960-sample frames here and emits
|
||||
// frame_samples-sized chunks. The 20 ms case (frame_samples == 960) bypasses this
|
||||
// entirely (fast path). Pre-sized at announce; never resized on the audio thread
|
||||
// (architecture.md §3 — no RT-thread allocation). encode_accum holds interleaved int16
|
||||
// at accum_channels; upmix_scratch is the pre-sized mono→stereo upmix target.
|
||||
// entirely. encode_accum holds interleaved int16
|
||||
// at accum_channels; upmix_scratch is the pre-sized monostereo upmix target.
|
||||
std::vector<int16_t> encode_accum;
|
||||
size_t accum_count = 0; // flat samples currently buffered
|
||||
int accum_channels = 0; // channel count of buffered data; resets on change
|
||||
@@ -286,12 +280,12 @@ struct vc_client {
|
||||
std::unordered_map<uint64_t, int> pending_announce_kind_; // request_id -> kind
|
||||
uint32_t next_local_stream_id_{1};
|
||||
|
||||
// ssrc → (user_id, stream_id) for remote streams already wired into audio_engine_.
|
||||
// ssrc (user_id, stream_id) for remote streams already wired into audio_engine_.
|
||||
mutable std::mutex remote_streams_mu_;
|
||||
std::unordered_map<uint32_t, std::pair<uint32_t, uint32_t>> remote_streams_;
|
||||
|
||||
// Talk-indicator polling thread (separate from udp_thread_ / the miniaudio callback
|
||||
// thread — see architecture.md §3 real-time rule).
|
||||
// thread.
|
||||
std::thread talk_timer_thread_;
|
||||
std::atomic<bool> talk_timer_stop_{false};
|
||||
static constexpr int64_t kTalkPollMs = 100;
|
||||
@@ -309,19 +303,15 @@ struct vc_client {
|
||||
// Permissions from last AuthResult.
|
||||
vc_permissions own_permissions_{};
|
||||
|
||||
// Send-side input gate (docs/voice.md §11). MIC-only — SCREEN_AUDIO/
|
||||
// AUX_DEVICE are never gated (see PROGRESS.md for the rationale). Pure local state, no
|
||||
// protocol traffic. mic_vad_ is constructed once the MIC stream's StreamAnnounceResult
|
||||
// lands (handle_stream_announce_result, on io_thread_ — not the RT capture callback).
|
||||
// Send-side input gate MIC-only — SCREEN_AUDIO/
|
||||
// AUX_DEVICE are never gated
|
||||
std::atomic<vc_input_mode> current_input_mode_{VC_INPUT_VOICE_ACTIVATION};
|
||||
std::atomic<bool> ptt_active_{false};
|
||||
std::atomic<float> vad_threshold_{0.025f}; // remembered across mode switches
|
||||
std::atomic<float> input_gain_{1.0f}; // send-side MIC gain (vc_set_input_gain)
|
||||
std::atomic<bool> input_noise_reduction_{false}; // send-side MIC NS (vc_set_input_noise_reduction)
|
||||
|
||||
// External-playback mode (iOS VPIO): when true, ensure_audio_running() configures the
|
||||
// AudioEngine to skip its hardware playback device and drive the mixer on a timer instead,
|
||||
// delivering the final mix to the mixed-output sink. Set via vc_set_external_playback.
|
||||
|
||||
std::atomic<bool> external_playback_{false};
|
||||
std::unique_ptr<voicecat::audio::ApmProcessor> mic_vad_;
|
||||
// Send-side mic noise suppressor (RNNoise). Constructed once with the MIC stream alongside
|
||||
@@ -333,10 +323,10 @@ struct vc_client {
|
||||
// makes it idempotent (avoids a double-join race on udp_thread_/talk_timer_thread_).
|
||||
std::mutex teardown_mu_;
|
||||
|
||||
// ── io_thread_ entry point ──────────────────────────────────────────────────
|
||||
// io_thread_ entry point
|
||||
void run_io(std::string host, uint16_t port);
|
||||
|
||||
// ── Protocol dispatch (called on io_thread_) ────────────────────────────────
|
||||
// Protocol dispatch (called on io_thread_)
|
||||
void handle_envelope(const voicecat::v1::Envelope& env);
|
||||
void handle_server_hello(const voicecat::v1::ServerHello& msg, uint64_t req_id);
|
||||
void handle_auth_result(const voicecat::v1::AuthResult& msg);
|
||||
@@ -344,8 +334,8 @@ struct vc_client {
|
||||
void handle_user_event(const voicecat::v1::UserEvent& ue);
|
||||
void handle_channel_event(const voicecat::v1::ChannelEvent& ce);
|
||||
// Called from handle_channel_event when the current channel's audio config changed:
|
||||
// stop→start every active local stream so the new Opus params take effect (encoders are
|
||||
// frozen at StreamAnnounceResult time — docs/voice.md §3). capture_device_id and
|
||||
// stops tart every active local stream so the new Opus params take effect (encoders are
|
||||
// frozen at StreamAnnounceResult time . capture_device_id and
|
||||
// capture_channels survive the restart (stream_stop doesn't clear them; stream_start
|
||||
// reuses the existing LocalStream entry). The server reads the updated channel config
|
||||
// on re-announce and returns new effective_audio; peers' sync_remote_streams wire up
|
||||
@@ -365,8 +355,8 @@ struct vc_client {
|
||||
// Stop every active local stream (used on voice leave — emits STREAM_STOPPED for each).
|
||||
void stop_all_local_streams();
|
||||
|
||||
// ── UDP / media helpers ────────────────────────────────────────────────────────
|
||||
// Kicks off TCP UdpBinding request; called once after a successful AuthResult.
|
||||
// UDP / media helpers
|
||||
// Kicks off TCP UdpBinding request, called once after a successful AuthResult.
|
||||
void start_udp_binding();
|
||||
// Opens the UDP socket, sends the plaintext bootstrap packet, starts udp_thread_.
|
||||
void finish_udp_binding();
|
||||
@@ -374,12 +364,11 @@ struct vc_client {
|
||||
void run_udp_recv();
|
||||
// Send a plaintext KEEPALIVE frame to the server media endpoint. Called from run_udp_recv
|
||||
// every kUdpKeepaliveIntervalMs to hold NAT bindings + bump the server's last_seen
|
||||
// (docs/voice.md §6). Plaintext — no AEAD — to avoid racing the audio thread's seal().
|
||||
void send_udp_keepalive();
|
||||
// capture_cb passed to audio_engine_.start(): encode + seal + send one frame for the
|
||||
// given local stream `kind` (multiple concurrent local streams are possible).
|
||||
void on_capture_frame(int kind, const int16_t* pcm, int samples, int channels);
|
||||
// Encode one frame of exactly ls.frame_samples samples-per-channel (upmixing mono→stereo
|
||||
// Encode one frame of exactly ls.frame_samples samples-per-channel (upmixing mono to stereo
|
||||
// for a stereo channel as needed), seal it, and send it over UDP, advancing ls.timestamp.
|
||||
// Called by on_capture_frame for each frame_samples-sized chunk. Assumes local_streams_mu_
|
||||
// is held and the send gate/crypto checks have already passed.
|
||||
@@ -400,22 +389,19 @@ struct vc_client {
|
||||
// the caller, or taken internally). Returns nullptr if not found/not active.
|
||||
LocalStream* find_local_stream_by_id(uint32_t stream_id);
|
||||
|
||||
// ── Helpers (io_thread_ and caller threads) ─────────────────────────────────
|
||||
// Queue an encoded envelope to be sent on io_thread_.
|
||||
|
||||
void queue_envelope(const voicecat::v1::Envelope& env);
|
||||
|
||||
// Keepalive: send a Ping envelope with a fresh nonce and record the sent time for
|
||||
// RTT measurement when the Pong arrives. Called from the read loop when kPingIntervalMs
|
||||
// has elapsed. (docs/protocol.md §7)
|
||||
// has elapsed.
|
||||
void send_ping();
|
||||
|
||||
// Drain send_queue_ by doing blocking TLS writes (called on io_thread_).
|
||||
void drain_sends();
|
||||
|
||||
// Transition state + emit VC_EVENT_CONNECTION_STATE.
|
||||
void set_state(vc_connection_state s);
|
||||
|
||||
// Convenience event emitters.
|
||||
void emit_error(vc_result r, const char* text);
|
||||
void emit_disconnected(vc_result r, const char* reason);
|
||||
};
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
#include "core/worker_pool.h"
|
||||
// WorkerPool is header-only via asio::thread_pool; nothing to define here.
|
||||
// WorkerPool is header-only via asio::thread_pool. nothing to define here.
|
||||
|
||||
Reference in New Issue
Block a user