chore: comment cleanup pass ahead of open-sourcing
Some checks failed
Build Linux Binaries / linux/amd64 (push) Has been cancelled
Build Linux Binaries / linux/arm64 (push) Has been cancelled

Removes leftover debug scaffolding (stray Console.WriteLine/NSLog traces,
dead nick_buf_ptr, a no-op --print-config flag now implemented for real),
fixes stale/misleading comments (channel passwords are no longer a "future
M5+" feature, a wrong cross-reference, a stale TlsContext::close() mention,
an incomplete BanRecord::subject_type doc, and a smoke test pointing at a
build/m1-dev preset that no longer exists), strips internal M1-M5 milestone
jargon from comments now that the roadmap is done, trims comments that just
restated the following line, and consolidates a few "why" explanations that
were duplicated 2-3 times in the same file.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-03 10:20:18 +01:00
parent bda37ec27b
commit bba605401d
50 changed files with 229 additions and 331 deletions

View File

@@ -6,7 +6,7 @@ file(GLOB_RECURSE VOICECAT_SOURCES CONFIGURE_DEPENDS
if(VOICECAT_BUILD_SHARED)
add_library(voicecat SHARED ${VOICECAT_SOURCES})
if(WIN32 AND MINGW)
# M4: the C# client only ships voicecat.dll itself — no MinGW runtime DLLs alongside
# The C# client only ships voicecat.dll itself — no MinGW runtime DLLs alongside
# it. x64-mingw-static only statically links vcpkg's OWN library deps (protobuf,
# sodium, mbedTLS, ...); the GCC/MinGW runtime stays dynamic by default
# (libgcc_s_seh-1.dll/libwinpthread-1.dll/libstdc++-6.dll — confirmed via `objdump -p`

View File

@@ -8,9 +8,8 @@
* Design: docs/architecture.md §4. Everything here is async + event-driven — calls return
* immediately and results/state changes arrive via the vc_callbacks.on_event callback.
*
* STATUS: real. Control plane, voice, multi-stream, device enumeration, VAD/PTT, and stereo
* playback all work via core/src/core/client.cpp. webrtc AEC/NS/AGC remains an inert passthrough
* (no Windows/MSVC port upstream — docs/voice.md §8/§11, PROGRESS.md).
* webrtc AEC/NS/AGC remains an inert passthrough (no Windows/MSVC port upstream — see
* docs/voice.md §8/§11).
*/
#ifndef VOICECAT_H
#define VOICECAT_H
@@ -81,7 +80,7 @@ typedef enum vc_connection_state {
VC_STATE_TLS_HANDSHAKE = 2,
VC_STATE_AUTHENTICATING = 3,
VC_STATE_CONNECTED = 4,
/* M4: between TLS_HANDSHAKE and AUTHENTICATING — the handshake succeeded and the core is
/* Between TLS_HANDSHAKE and AUTHENTICATING — the handshake succeeded and the core is
* waiting for vc_confirm_server_identity() (see VC_EVENT_SERVER_IDENTITY below). Appended
* at the end (not inserted) to keep existing enum values stable — additive-only ABI. */
VC_STATE_VERIFYING_IDENTITY = 5,
@@ -125,7 +124,7 @@ typedef enum vc_event_type {
VC_EVENT_TALK_STATE = 9, /* user_id, stream_id, u32a = talking(0/1) */
VC_EVENT_ERROR = 10, /* result, text */
VC_EVENT_DISCONNECTED = 11, /* result, text = reason */
/* M4 additions — appended, not inserted, to keep existing enum values stable. */
/* Appended, not inserted, to keep existing enum values stable. */
VC_EVENT_JOIN_RESULT = 12, /* result (VC_OK/VC_ERR_*), channel_id, text = error on
failure. Reply to vc_join_channel(). */
VC_EVENT_SERVER_IDENTITY = 13, /* u32a = vc_tofu_status, text = hex-encoded TLS leaf-cert
@@ -134,7 +133,7 @@ typedef enum vc_event_type {
attempt, right after the TLS handshake succeeds. The
connection is held open until vc_confirm_server_identity()
is called. */
/* M5 additions — appended, not inserted. */
/* Appended, not inserted. */
VC_EVENT_GENERIC_RESULT = 14, /* result, u32a = server error code, text = message. Reply
to vc_kick_user/vc_ban_user/vc_set_permission/
vc_move_user/vc_create_channel/vc_edit_channel/
@@ -147,7 +146,7 @@ typedef enum vc_event_type {
(vc_user) carries per-user voice_subscribed. */
} vc_event_type;
/* TOFU server-identity classification (M4) — see VC_EVENT_SERVER_IDENTITY and
/* TOFU server-identity classification — see VC_EVENT_SERVER_IDENTITY and
* vc_confirm_server_identity. Pins the TLS leaf certificate's own SHA-256 fingerprint
* (verifiable directly from the handshake), NOT the declared Ed25519
* server_identity_fingerprint from ServerHello — the TLS cert and the server's Ed25519
@@ -194,7 +193,7 @@ typedef struct vc_config {
const char* client_name; /* e.g. "VoiceCat-macOS" */
const char* client_version; /* e.g. "0.0.1" */
vc_log_level log_level;
/* M4, optional (added at the end — existing brace-initialized callers default this to
/* Optional (added at the end — existing brace-initialized callers default this to
* NULL, no source change needed). Path to the TOFU pin file (see VC_EVENT_SERVER_IDENTITY/
* vc_confirm_server_identity). NULL = a built-in relative default
* ("./voicecat_tofu_pins.txt") so existing tests need no real persistence. A real app
@@ -232,7 +231,7 @@ typedef struct vc_audio_config {
int dred; /* bool — Deep REDundancy (Opus 1.6), off by default */
} vc_audio_config;
/* M5: permission bitset (mirrors protocol Permissions). */
/* Permission bitset (mirrors protocol Permissions). */
typedef struct vc_permissions {
int can_create_temp_channel; /* bool */
int can_kick; /* bool */
@@ -242,7 +241,7 @@ typedef struct vc_permissions {
int is_admin; /* bool */
} vc_permissions;
/* M5: account entry (reply to vc_list_accounts / vc_get_account_list). */
/* Account entry (reply to vc_list_accounts / vc_get_account_list). */
typedef struct vc_account {
const char* username;
int is_admin; /* bool */
@@ -255,7 +254,7 @@ typedef struct vc_account_list {
size_t count;
} vc_account_list;
/* M5: channel creation/edition descriptor. */
/* Channel creation/edition descriptor. */
typedef struct vc_channel_info {
uint32_t id; /* 0 = new channel for create */
uint32_t parent_id; /* 0 = root */
@@ -280,7 +279,7 @@ typedef struct vc_device_list {
size_t count;
} vc_device_list;
/* ── Channel / user / stream snapshots (M4 — for the channel-tree/user-list UI) ───────────
/* ── Channel / user / stream snapshots (for the channel-tree/user-list UI) ────────────────
* Pull-based: re-call after VC_EVENT_CHANNEL_LIST / VC_EVENT_USER_JOINED / _LEFT / _UPDATED to
* refresh — there is no push variant; those events just mean "go look". Same ownership
* contract as vc_device/vc_device_list above: core-allocated, caller frees with the matching
@@ -363,10 +362,8 @@ VC_API vc_result vc_authenticate_user(vc_client* c, const char* username,
/* ── Channels ─────────────────────────────────────────────────────────────── */
/* Result arrives as VC_EVENT_JOIN_RESULT, not a return value beyond "request queued". `password`
* is forwarded to the server's JoinChannelRequest.password for channels with
* vc_channel.password_protected set; NOTE (M4): no in-tree channel currently has a server-side
* password to check against — channel creation/passwords are a future (M5+) feature, so this
* path is wired but not yet exercisable end-to-end. */
* is forwarded to the server's JoinChannelRequest.password and checked against the channel's
* stored password for channels with vc_channel.password_protected set. */
VC_API vc_result vc_join_channel(vc_client* c, uint32_t channel_id,
const char* password /* nullable */);
VC_API vc_result vc_leave_channel(vc_client* c);
@@ -468,7 +465,7 @@ VC_API vc_result vc_set_capture_channels(vc_client* c, uint32_t stream_id, uint3
* music / relay), soundboards, DAW integration. Works for any stream kind (MIC /
* SCREEN_AUDIO / AUX_DEVICE). Thread-safe; may be called from any thread.
*
* Replaces vc_test_inject_capture (deprecated alias, see below). */
* Replaces vc_test_inject_capture (deprecated alias, see above). */
VC_API vc_result vc_stream_feed_pcm(vc_client* c, uint32_t stream_id,
const int16_t* pcm, size_t samples_per_channel,
uint32_t channels);
@@ -538,7 +535,7 @@ VC_API vc_result vc_send_text(vc_client* c, vc_text_scope scope, uint32_t target
VC_API vc_result vc_list_devices(vc_client* c, vc_device_kind kind, vc_device_list* out);
VC_API void vc_free_device_list(vc_device_list* list);
/* ── Channel / user / stream enumeration (M4; mirrors vc_list_devices above) ─────────────── */
/* ── Channel / user / stream enumeration (mirrors vc_list_devices above) ──────────────────── */
VC_API vc_result vc_list_channels(vc_client* c, vc_channel_list* out);
VC_API void vc_free_channel_list(vc_channel_list* list);
@@ -551,7 +548,7 @@ VC_API vc_result vc_list_user_streams(vc_client* c, uint32_t user_id,
vc_stream_summary_list* out);
VC_API void vc_free_stream_summary_list(vc_stream_summary_list* list);
/* ── TOFU server-identity confirmation (M4) — see VC_EVENT_SERVER_IDENTITY/vc_tofu_status ── */
/* ── TOFU server-identity confirmation — see VC_EVENT_SERVER_IDENTITY/vc_tofu_status ──────── */
/* Accept or reject the pending server-identity check for the in-progress connect(). Must be
* called after a VC_EVENT_SERVER_IDENTITY event; the io_thread_ holds the connection open
* (ClientHello/auth deferred) until this is called, up to a generous internal timeout (after
@@ -570,7 +567,7 @@ VC_API vc_result vc_confirm_server_identity(vc_client* c, int accept /* bool */)
VC_API vc_result vc_get_server_identity_display(vc_client* c, char* out_buf, size_t buf_cap,
size_t* out_len);
/* ── M5: Moderation & admin ─────────────────────────────────────────────────
/* ── Moderation & admin ───────────────────────────────────────────────────────
* All calls are async; the result arrives as VC_EVENT_GENERIC_RESULT (or
* VC_EVENT_ACCOUNT_LIST for vc_list_accounts). They require VC_STATE_CONNECTED and,
* on the server side, the appropriate permission. */

View File

@@ -122,7 +122,7 @@ message User {
bool self_deafened = 6;
bool server_muted = 7;
repeated StreamInfo streams = 8;
bool server_deafened = 9; // M5: server-imposed deafen
bool server_deafened = 9; // server-imposed deafen
bool voice_subscribed = 10; // true when the user is on the voice plane (hears + can send)
}
@@ -194,7 +194,7 @@ message UserEvent {
Kind kind = 1;
User user = 2;
uint32 left_id = 3;
string reason = 4; // M5: kick/ban reason for LEFT events
string reason = 4; // kick/ban reason for LEFT events
}
message SubscribeRequest { repeated uint32 channel_ids = 1; bool presence = 2; }

View File

@@ -7,7 +7,7 @@
*
* REAL-TIME RULE: audio-callback threads never allocate, lock, or block (architecture.md §3).
* The JitterBuffer and per-stream maps are accessed only under a try_lock; a failed lock
* causes PLC for that period (acceptable for M2; lock-free ring buffer is the M3 upgrade).
* causes PLC for that period (acceptable today; a lock-free ring buffer would remove even that).
*/
#ifndef VOICECAT_AUDIO_AUDIO_ENGINE_H
#define VOICECAT_AUDIO_AUDIO_ENGINE_H
@@ -134,11 +134,11 @@ class AudioEngine {
// Callback type for encoded capture frames ready to be sent. `kind` identifies which
// local stream this PCM belongs to (a vc_stream_kind value; 0 = MIC for the real capture
// device, which is always the "primary" tap). `channels` is the channel count of the PCM
// buffer (1 = mono, 2 = stereo interleaved) — the mic capture device is mono in v1, but
// buffer (1 = mono, 2 = stereo interleaved) — the mic capture device is mono by default, but
// the WASAPI loopback path (SCREEN_AUDIO) captures in the channel's mode when stereo, so
// the encoder sees real interleaved L/R PCM rather than a mono upmix. M3: multiple
// concurrent local streams are possible (e.g. MIC + SCREEN_AUDIO), each fed via its own
// injection tap (see inject_capture) since there is only one real hardware capture device.
// the encoder sees real interleaved L/R PCM rather than a mono upmix. Multiple concurrent
// local streams are possible (e.g. MIC + SCREEN_AUDIO), each fed via its own injection tap
// (see inject_capture) since there is only one real hardware capture device.
using CaptureCallback = std::function<void(int kind, const int16_t* pcm, int samples,
int channels)>;
@@ -459,7 +459,7 @@ class AudioEngine {
// never called. See on_playback's decode loop.
int64_t plc_samples_since_real = 0;
// M3: listener-chosen, local-only noise reduction (docs/voice.md §10). Lazily
// Listener-chosen, local-only noise reduction (docs/voice.md §10). Lazily
// created only when enabled — bounded by how many remote streams this listener
// subscribes to, so no separate instance cap is needed.
bool noise_reduction_enabled = false;
@@ -485,7 +485,7 @@ class AudioEngine {
uint32_t user_id = 0;
uint32_t stream_id = 0;
// M3: talk-indicator edge detection (docs/voice.md §7) — updated by push_recv_frame
// Talk-indicator edge detection (docs/voice.md §7) — updated by push_recv_frame
// (already off the real-time audio thread), polled by poll_talk_transitions().
std::atomic<int64_t> last_voice_ms{0};
bool talking = false;

View File

@@ -40,10 +40,10 @@ std::vector<uint8_t> make_frame(const voicecat::v1::Envelope& env) {
} // namespace
// ── vc_client M1 implementation ───────────────────────────────────────────────
// ── vc_client implementation ───────────────────────────────────────────────────
vc_client::vc_client(const vc_config& cfg, vc_callbacks cb) : cfg_(cfg), cb_(cb) {
// M4 TOFU: NULL/empty tofu_store_path falls back to a relative default so existing tests
// TOFU: NULL/empty tofu_store_path falls back to a relative default so existing tests
// (which never set this field) keep working without real per-user persistence.
std::filesystem::path tofu_path = (cfg.tofu_store_path && cfg.tofu_store_path[0])
? std::filesystem::path(cfg.tofu_store_path)
@@ -228,7 +228,7 @@ void vc_client::run_io(std::string host, uint16_t port) {
}
}
// ── TOFU server-identity gate (M4) ──────────────────────────────────────
// ── 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
@@ -542,7 +542,7 @@ void vc_client::handle_envelope(const voicecat::v1::Envelope& env) {
void vc_client::handle_server_hello(const voicecat::v1::ServerHello& msg, uint64_t /*req_id*/) {
server_udp_port_ = static_cast<uint16_t>(msg.udp_port());
// M4: stash the declared Ed25519 fingerprint for vc_get_server_identity_display() —
// Stash the declared Ed25519 fingerprint for vc_get_server_identity_display() —
// display-only, not the TOFU-pinned value (that's the TLS cert fingerprint, gated before
// ClientHello was even sent — see the TOFU block above in run_io()).
{
@@ -687,7 +687,6 @@ void vc_client::handle_user_event(const voicecat::v1::UserEvent& ue) {
ev.user_id = user.id();
ev.channel_id = user.channel_id();
static const char* nick_buf_ptr = nullptr;
std::string nick = user.nickname();
switch (ue.kind()) {
@@ -714,7 +713,7 @@ void vc_client::handle_user_event(const voicecat::v1::UserEvent& ue) {
case voicecat::v1::UserEvent::UPDATED:
ev.type = VC_EVENT_USER_UPDATED;
emit(ev);
// M5: if this is an update to our own user, reflect server-mute/deafen locally.
// If this is an update to our own user, reflect server-mute/deafen locally.
if (user.id() == self_user_id_) {
server_muted_.store(user.server_muted(), std::memory_order_release);
server_deafened_.store(user.server_deafened(), std::memory_order_release);
@@ -731,7 +730,6 @@ void vc_client::handle_user_event(const voicecat::v1::UserEvent& ue) {
default:
break;
}
(void)nick_buf_ptr;
}
void vc_client::handle_text_message(const voicecat::v1::TextMessage& msg) {
@@ -799,9 +797,7 @@ 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: wired through to the wire message, but no
// in-tree channel has a server-side password to check yet (no channel-creation feature
// exists — M5+).
// See voicecat.h's vc_join_channel doc comment.
if (password) jc->set_password(password);
queue_envelope(req);
return VC_OK;
@@ -828,7 +824,7 @@ vc_result vc_client::send_text(vc_text_scope scope, uint32_t target_id, const ch
return VC_OK;
}
// ── M2: UDP binding ───────────────────────────────────────────────────────────
// ── UDP binding ────────────────────────────────────────────────────────────────
void vc_client::start_udp_binding() {
voicecat::v1::Envelope req;
@@ -977,8 +973,8 @@ int64_t client_now_ms() {
}
// Builds an OpusParams from a wire AudioConfig, applying the same field-by-field mapping on
// both the send (local-stream encoder) and receive (remote-stream decoder) paths — fixes the
// M2 gap where mode/dtx/complexity/application were silently dropped.
// both the send (local-stream encoder) and receive (remote-stream decoder) paths — fixes a
// 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
@@ -1038,8 +1034,8 @@ 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 (docs §M3 scope decision).
// M5: server-mute is also a hard gate on MIC transmission.
// playing while the user's mic is muted (scope decision — see docs/voice.md).
// 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) ||
server_muted_.load(std::memory_order_acquire))) return;
@@ -1308,7 +1304,7 @@ void vc_client::sync_remote_streams(const voicecat::v1::User& user) {
}
}
// ── M2: 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;
@@ -1884,7 +1880,7 @@ vc_result vc_client::list_devices(vc_device_kind kind, vc_device_list* out) {
return VC_OK;
}
// ── M4: channel/user/stream snapshot getters ─────────────────────────────────
// ── Channel/user/stream snapshot getters ──────────────────────────────────────
vc_result vc_client::list_channels(vc_channel_list* out) {
std::lock_guard<std::mutex> lk(session_model_mu_);
@@ -1962,7 +1958,7 @@ vc_result vc_client::list_user_streams(uint32_t user_id, vc_stream_summary_list*
return VC_OK;
}
// ── M4: TOFU server-identity gate ─────────────────────────────────────────────
// ── TOFU server-identity gate ───────────────────────────────────────────────────
vc_result vc_client::confirm_server_identity(bool accept) {
std::lock_guard<std::mutex> lk(tofu_mu_);
@@ -1987,7 +1983,7 @@ vc_result vc_client::get_server_identity_display(char* out_buf, size_t buf_cap,
return VC_OK;
}
// ── M5: Moderation & admin ───────────────────────────────────────────────────
// ── 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;

View File

@@ -68,16 +68,16 @@ struct vc_client {
vc_result list_devices(vc_device_kind kind, vc_device_list* out);
// M4: channel/user/stream snapshot getters (read session_model_; see voicecat.h).
// Channel/user/stream snapshot getters (read session_model_; see voicecat.h).
vc_result list_channels(vc_channel_list* out);
vc_result list_users(vc_user_list* out);
vc_result list_user_streams(uint32_t user_id, vc_stream_summary_list* out);
// M4: TOFU server-identity gate (see voicecat.h's VC_EVENT_SERVER_IDENTITY doc comment).
// TOFU server-identity gate (see voicecat.h's VC_EVENT_SERVER_IDENTITY doc comment).
vc_result confirm_server_identity(bool accept);
vc_result get_server_identity_display(char* out_buf, size_t buf_cap, size_t* out_len);
// M3: effective Opus config for a (user_id, stream_id) — our own pending/active local
// Effective Opus config for a (user_id, stream_id) — our own pending/active local
// streams, or any peer's broadcast StreamInfo.audio.
vc_result get_stream_audio_config(uint32_t user_id, uint32_t stream_id,
vc_audio_config* out);
@@ -97,7 +97,7 @@ struct vc_client {
// 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);
// M5: moderation & admin.
// Moderation & admin.
vc_result kick_user(uint32_t user_id, const char* reason);
vc_result ban_user(uint32_t user_id, const char* reason, uint64_t expires_unix_ms);
vc_result set_permission(uint32_t user_id, const vc_permissions* perms);
@@ -123,7 +123,7 @@ struct vc_client {
vc_config cfg_{};
vc_callbacks cb_{};
// ── M1: 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)
@@ -187,17 +187,17 @@ struct vc_client {
std::atomic<int64_t> last_udp_keepalive_ms_{0};
// Client-side session model. Mutated only on io_thread_ (handle_server_state/
// handle_user_event/handle_channel_event), but read from any thread via the M4
// handle_user_event/handle_channel_event), but read from any thread via the
// list_channels/list_users/list_user_streams getters — session_model_mu_ guards both.
voicecat::session::SessionModel session_model_;
mutable std::mutex session_model_mu_;
// M5: last ListAccountsResult snapshot, populated on io_thread_ when
// Last ListAccountsResult snapshot, populated on io_thread_ when
// VC_EVENT_ACCOUNT_LIST fires and read by vc_get_account_list on caller threads.
std::vector<voicecat::v1::AccountEntry> last_account_list_;
mutable std::mutex account_list_mu_;
// ── M4: 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 +205,7 @@ struct vc_client {
bool tofu_accept_{false};
std::string pending_identity_fp_hex_; // ServerHello's Ed25519 fp, display-only
// ── M2: UDP / media plane ────────────────────────────────────────────────────
// ── UDP / media plane ──────────────────────────────────────────────────────────
std::array<uint8_t, 16> udp_token_{};
uint16_t server_udp_port_{0};
std::string udp_host_;
@@ -220,9 +220,8 @@ struct vc_client {
voicecat::audio::AudioEngine audio_engine_;
// M3: one LocalStream per concurrently-active stream kind (MIC/SCREEN_AUDIO/AUX_DEVICE
// are each singletons for a given client — see docs/roadmap.md §M3). Replaces the M2
// single-stream fields (local_encoder_/local_stream_active_/etc).
// One LocalStream per concurrently-active stream kind (MIC/SCREEN_AUDIO/AUX_DEVICE
// are each singletons for a given client).
struct LocalStream {
voicecat::codec::OpusEncoder encoder;
std::atomic<bool> active{false};
@@ -291,7 +290,7 @@ struct vc_client {
mutable std::mutex remote_streams_mu_;
std::unordered_map<uint32_t, std::pair<uint32_t, uint32_t>> remote_streams_;
// M3: talk-indicator polling thread (separate from udp_thread_ / the miniaudio callback
// Talk-indicator polling thread (separate from udp_thread_ / the miniaudio callback
// thread — see architecture.md §3 real-time rule).
std::thread talk_timer_thread_;
std::atomic<bool> talk_timer_stop_{false};
@@ -307,10 +306,10 @@ struct vc_client {
std::atomic<bool> server_muted_{false};
std::atomic<bool> server_deafened_{false};
// M5: permissions from last AuthResult.
// Permissions from last AuthResult.
vc_permissions own_permissions_{};
// Follow-up to M3: send-side input gate (docs/voice.md §11). MIC-only — SCREEN_AUDIO/
// 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).
@@ -366,7 +365,7 @@ struct vc_client {
// Stop every active local stream (used on voice leave — emits STREAM_STOPPED for each).
void stop_all_local_streams();
// ── M2: UDP / media helpers ──────────────────────────────────────────────────
// ── 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_.
@@ -378,7 +377,7 @@ struct vc_client {
// (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` (M3: multiple concurrent local streams are possible).
// 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
// for a stereo channel as needed), seal it, and send it over UDP, advancing ls.timestamp.
@@ -394,7 +393,7 @@ struct vc_client {
// Joins udp_thread_, stops audio_engine_, clears media crypto/remote-stream state.
// Safe to call multiple times. Called both from run_io()'s cleanup and disconnect().
void teardown_voice();
// talk_timer_thread_ entry point (M3): polls audio_engine_ for remote talk-state edges
// talk_timer_thread_ entry point: polls audio_engine_ for remote talk-state edges
// and local capture activity, emitting VC_EVENT_TALK_STATE. Never the audio RT thread.
void run_talk_timer();
// Find a LocalStream by its client-assigned stream_id (held under local_streams_mu_ by

View File

@@ -95,14 +95,12 @@ ServerCert ServerCert::generate(const std::string& server_name) {
strlen(pers)),
"ctr_drbg_seed");
// Generate ECDSA-P256 key
throw_if(mbedtls_pk_setup(&key, mbedtls_pk_info_from_type(MBEDTLS_PK_ECKEY)),
"pk_setup");
throw_if(mbedtls_ecp_gen_key(MBEDTLS_ECP_DP_SECP256R1, mbedtls_pk_ec(key),
mbedtls_ctr_drbg_random, &ctr_drbg),
"ecp_gen_key");
// Build self-signed cert
mbedtls_x509write_crt_set_version(&cert, MBEDTLS_X509_CRT_VERSION_3);
mbedtls_x509write_crt_set_md_alg(&cert, MBEDTLS_MD_SHA256);
mbedtls_x509write_crt_set_subject_key(&cert, &key);
@@ -112,7 +110,6 @@ ServerCert ServerCert::generate(const std::string& server_name) {
throw_if(mbedtls_x509write_crt_set_subject_name(&cert, dn.c_str()), "set_subject");
throw_if(mbedtls_x509write_crt_set_issuer_name(&cert, dn.c_str()), "set_issuer");
// Serial = 0x01 (1 byte, value 1)
uint8_t serial_raw[] = {0x01};
throw_if(mbedtls_x509write_crt_set_serial_raw(&cert, serial_raw, sizeof(serial_raw)),
"set_serial");
@@ -124,13 +121,11 @@ ServerCert ServerCert::generate(const std::string& server_name) {
throw_if(mbedtls_x509write_crt_set_basic_constraints(&cert, 0, -1),
"set_basic_constraints");
// Write PEM cert
unsigned char cert_buf[4096] = {};
throw_if(mbedtls_x509write_crt_pem(&cert, cert_buf, sizeof(cert_buf),
mbedtls_ctr_drbg_random, &ctr_drbg),
"write_cert_pem");
// Write PEM key
unsigned char key_buf[4096] = {};
throw_if(mbedtls_pk_write_key_pem(&key, key_buf, sizeof(key_buf)), "write_key_pem");

View File

@@ -59,7 +59,7 @@ struct ServerCert {
// ── TLS 1.3 context ───────────────────────────────────────────────────────────
// Wraps mbedTLS for one TLS connection (server or client side).
// All public methods except close() must be called from a single thread at a time.
// All public methods must be called from a single thread at a time.
class TlsContext {
public:
enum class Role { Server, Client };
@@ -86,7 +86,7 @@ class TlsContext {
bool export_keying_material(const char* label, const uint8_t* ctx, size_t ctx_len,
uint8_t* out, size_t out_len);
// M4 TOFU: SHA-256 of the peer's leaf X.509 certificate (DER), valid only after a
// TOFU: SHA-256 of the peer's leaf X.509 certificate (DER), valid only after a
// successful Role::Client handshake(). This is the value vc_client pins — see
// voicecat.h's vc_tofu_status doc comment for why the cert fingerprint is pinned instead
// of the declared Ed25519 server_identity_fingerprint. Returns false if no peer cert is
@@ -119,7 +119,7 @@ class TlsContext {
mbedtls_net_context net_ctx_{};
};
// ── Media AEAD (M2) ───────────────────────────────────────────────────────────
// ── Media AEAD ─────────────────────────────────────────────────────────────────
// Per-frame voice encryption. Abstracted so the backend is swappable.
class MediaCrypto {
public:

View File

@@ -27,7 +27,7 @@ class TofuStore {
// Check the fingerprint for host:port. Stores on first connect.
// Thread-safe (single-writer lock).
// NOTE: kept for compatibility; the M4 gated-confirmation flow (vc_client) uses peek()
// NOTE: kept for compatibility; vc_client's gated-confirmation flow uses peek()
// + pin() instead, since check_and_pin's unconditional first-connect write is wrong for a
// flow where the application must approve the fingerprint before it's trusted/persisted.
TofuResult check_and_pin(const std::string& host, uint16_t port,

View File

@@ -3,9 +3,6 @@
*
* Design: docs/architecture.md (Net thread), docs/protocol.md §1 (framing).
* Implementation uses standalone Asio for sockets and timers.
*
* Design: docs/architecture.md (Net thread), docs/protocol.md §1 (framing).
* Implementation uses standalone Asio for sockets and timers.
*/
#ifndef VOICECAT_NET_TRANSPORT_H
#define VOICECAT_NET_TRANSPORT_H
@@ -180,7 +177,7 @@ class TcpAcceptor {
std::vector<std::shared_ptr<TcpServerConn>> conns_;
};
// ── UDP media channel (M2) ───────────────────────────────────────────────────
// ── UDP media channel ────────────────────────────────────────────────────────
// Thin async UDP socket. send_to() is thread-safe. Recv callbacks fire on the
// io_context's thread (same thread that runs the io_context::run() loop).
class UdpMediaChannel {

View File

@@ -6,10 +6,9 @@
* request_id ↔ response, and dispatches to handlers. Media frames do NOT come through here
* (they use the fixed binary header in voice.md §2).
*
* STATUS: real. Protobuf codegen is on (core/CMakeLists.txt) for VOICECAT_HAS_NET builds
* (`dev`/`release`/`server-release`); FrameCodec below is fully implemented and used by both the
* client (net/transport.h) and the server (conn_session.cpp). See protocol/envelope.h for the
* Envelope-level encode/decode that sits on top of this.
* FrameCodec below is used by both the client (net/transport.h) and the server
* (conn_session.cpp). See protocol/envelope.h for the Envelope-level encode/decode that sits
* on top of this.
*/
#ifndef VOICECAT_PROTOCOL_PROTOCOL_H
#define VOICECAT_PROTOCOL_PROTOCOL_H

View File

@@ -148,9 +148,7 @@ void SessionModel::apply_channel_event(const voicecat::v1::ChannelEvent& ev) {
} else if (ev.kind() == Kind::DELETED) {
// deleted_id, not channel().id() — the proto leaves `channel` unset for deletes
// (docs/protocol.md, core/proto/voicecat.proto's ChannelEvent). Pre-existing bug, dead
// code until something actually emits ChannelEvent (no channel CRUD exists yet — M5+),
// fixed here while touching this function for the M4 field-population fix.
// (docs/protocol.md, core/proto/voicecat.proto's ChannelEvent).
uint32_t cid = ev.deleted_id();
channels_.erase(std::remove_if(channels_.begin(), channels_.end(),
[cid](const Channel& x) { return x.id == cid; }),

View File

@@ -46,7 +46,7 @@ struct Stream {
int kind{0};
std::string label;
// Full effective AudioConfig (docs/protocol.md §5), as broadcast by the server in
// StreamInfo.audio — mirrors voicecat::v1::AudioConfig field-for-field so M3 per-channel
// StreamInfo.audio — mirrors voicecat::v1::AudioConfig field-for-field so per-channel
// tuning (mono/stereo, bitrate, FEC/DTX, application) is observable client-side, not just
// sample_rate/frame_ms.
uint32_t sample_rate{48000};

View File

@@ -1,11 +1,8 @@
/*
* voicecat.cpp — C ABI implementation.
*
* Lifecycle (create/destroy) and trivial accessors are always real. Everything else below
* just delegates to vc_client (core/src/core/client.cpp): under VOICECAT_HAS_NET
* (`dev`/`release`/`server-release` — see docs/building.md) that's the real M1M3 implementation;
* under the no-deps `skeleton` preset, client.cpp's `#else` branch returns VC_ERR_NOT_IMPLEMENTED
* for all of it, to keep that skeleton build green.
* Lifecycle (create/destroy) and trivial accessors are handled directly here; everything else
* delegates to vc_client (core/src/core/client.cpp).
*/
#include "voicecat.h"