From 826b3bfb86e5bceacae1b7084a7dc5bf84097086 Mon Sep 17 00:00:00 2001 From: Talon Date: Fri, 3 Jul 2026 15:29:09 +0100 Subject: [PATCH] Cleanup client and worker pool comments --- core/src/core/client.cpp | 121 +++++++++++----------------------- core/src/core/client.h | 62 +++++++---------- core/src/core/worker_pool.cpp | 2 +- 3 files changed, 65 insertions(+), 120 deletions(-) diff --git a/core/src/core/client.cpp b/core/src/core/client.cpp index 8cad54f..eac3548 100644 --- a/core/src/core/client.cpp +++ b/core/src/core/client.cpp @@ -40,7 +40,7 @@ std::vector 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(sock), std::memory_order_release); - // ── TLS handshake ──────────────────────────────────────────────────── + // TLS handshake set_state(VC_STATE_TLS_HANDSHAKE); tls_ = std::make_unique( @@ -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 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 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::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::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(::recv(static_cast(fd), reinterpret_cast(buf.data()), static_cast(buf.size()), 0)); if (n < static_cast(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(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(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(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(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 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 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; diff --git a/core/src/core/client.h b/core/src/core/client.h index d37aab7..7ea8f8a 100644 --- a/core/src/core/client.h +++ b/core/src/core/client.h @@ -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 state_net_{VC_STATE_DISCONNECTED}; // Blocking I/O thread (one per vc_client lifetime) @@ -197,7 +195,7 @@ struct vc_client { std::vector last_account_list_; mutable std::mutex account_list_mu_; - // ── TOFU server-identity gate ───────────────────────────────────────────────── + // TOFU server-identity gate std::unique_ptr 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 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 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 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 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> 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 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 current_input_mode_{VC_INPUT_VOICE_ACTIVATION}; std::atomic ptt_active_{false}; std::atomic vad_threshold_{0.025f}; // remembered across mode switches std::atomic input_gain_{1.0f}; // send-side MIC gain (vc_set_input_gain) std::atomic 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 external_playback_{false}; std::unique_ptr 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); }; diff --git a/core/src/core/worker_pool.cpp b/core/src/core/worker_pool.cpp index 06130de..1333e66 100644 --- a/core/src/core/worker_pool.cpp +++ b/core/src/core/worker_pool.cpp @@ -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.