scaffold: M0 skeleton + agent onboarding (build, architecture, progress)
Turn the design into a buildable, dependency-free M0 skeleton plus the
onboarding layer so a new agent can pick up instantly.
Build system:
- CMake + CMakePresets (dev = no deps; server-release = vcpkg) + vcpkg.json
- Skeleton builds with just a C++20 compiler; deps stay off until needed
- .gitattributes (LF), .gitignore, .clang-format
Core (libvoicecat):
- core/include/voicecat.h: full C ABI (the client/server contract), stubbed
- core/proto/voicecat.proto: control-plane wire format, matches docs/protocol.md
- src/{net,crypto,codec,protocol,session,audio,core}: subsystem stubs that
return VC_ERR_NOT_IMPLEMENTED, each pointing to its design doc
- server/ (voicecat-server) and tools/vccli/ link the core
- tests/: CTest smoke test asserting the C ABI contract (behavior, not just build)
- clients/{apple,windows}: M4 placeholders
Onboarding for agents:
- CLAUDE.md: hub — build/test commands, architecture at a glance, doc map, rules
- AGENTS.md: working method (behavior-driven; clean compile is the floor not the goal)
- PROGRESS.md: living tracker — M0 done, M1 task checklist, "where we left off"
Verified: cmake --preset dev && cmake --build --preset dev && ctest --preset dev → green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 21:09:09 +02:00
|
|
|
|
/*
|
|
|
|
|
|
* voicecat.h — the C ABI for libvoicecat.
|
|
|
|
|
|
*
|
|
|
|
|
|
* This is the single boundary every front-end calls: Swift (macOS/iOS) and C# (Windows)
|
|
|
|
|
|
* both bind to this header, and the server links the same core. It is C-linkage and
|
|
|
|
|
|
* handle-based so it is stable and trivially bindable from any language.
|
|
|
|
|
|
*
|
|
|
|
|
|
* 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.
|
|
|
|
|
|
*
|
2026-06-30 11:32:22 +01:00
|
|
|
|
* 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).
|
scaffold: M0 skeleton + agent onboarding (build, architecture, progress)
Turn the design into a buildable, dependency-free M0 skeleton plus the
onboarding layer so a new agent can pick up instantly.
Build system:
- CMake + CMakePresets (dev = no deps; server-release = vcpkg) + vcpkg.json
- Skeleton builds with just a C++20 compiler; deps stay off until needed
- .gitattributes (LF), .gitignore, .clang-format
Core (libvoicecat):
- core/include/voicecat.h: full C ABI (the client/server contract), stubbed
- core/proto/voicecat.proto: control-plane wire format, matches docs/protocol.md
- src/{net,crypto,codec,protocol,session,audio,core}: subsystem stubs that
return VC_ERR_NOT_IMPLEMENTED, each pointing to its design doc
- server/ (voicecat-server) and tools/vccli/ link the core
- tests/: CTest smoke test asserting the C ABI contract (behavior, not just build)
- clients/{apple,windows}: M4 placeholders
Onboarding for agents:
- CLAUDE.md: hub — build/test commands, architecture at a glance, doc map, rules
- AGENTS.md: working method (behavior-driven; clean compile is the floor not the goal)
- PROGRESS.md: living tracker — M0 done, M1 task checklist, "where we left off"
Verified: cmake --preset dev && cmake --build --preset dev && ctest --preset dev → green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 21:09:09 +02:00
|
|
|
|
*/
|
|
|
|
|
|
#ifndef VOICECAT_H
|
|
|
|
|
|
#define VOICECAT_H
|
|
|
|
|
|
|
|
|
|
|
|
#include <stddef.h>
|
|
|
|
|
|
#include <stdint.h>
|
|
|
|
|
|
|
|
|
|
|
|
#if defined(__cplusplus)
|
|
|
|
|
|
extern "C" {
|
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
|
|
/* ── Export macro ─────────────────────────────────────────────────────────── */
|
|
|
|
|
|
#if defined(VOICECAT_STATIC)
|
|
|
|
|
|
#define VC_API
|
|
|
|
|
|
#elif defined(_WIN32)
|
|
|
|
|
|
#if defined(VOICECAT_BUILDING)
|
|
|
|
|
|
#define VC_API __declspec(dllexport)
|
|
|
|
|
|
#else
|
|
|
|
|
|
#define VC_API __declspec(dllimport)
|
|
|
|
|
|
#endif
|
|
|
|
|
|
#else
|
|
|
|
|
|
#if defined(VOICECAT_BUILDING)
|
|
|
|
|
|
#define VC_API __attribute__((visibility("default")))
|
|
|
|
|
|
#else
|
|
|
|
|
|
#define VC_API
|
|
|
|
|
|
#endif
|
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
|
|
/* ── Version ──────────────────────────────────────────────────────────────── */
|
|
|
|
|
|
#define VOICECAT_VERSION_MAJOR 0
|
|
|
|
|
|
#define VOICECAT_VERSION_MINOR 0
|
2026-06-22 02:38:01 +02:00
|
|
|
|
#define VOICECAT_VERSION_PATCH 2 /* +vc_set_mixed_output_sink / vc_set_external_playback (iOS VPIO) */
|
scaffold: M0 skeleton + agent onboarding (build, architecture, progress)
Turn the design into a buildable, dependency-free M0 skeleton plus the
onboarding layer so a new agent can pick up instantly.
Build system:
- CMake + CMakePresets (dev = no deps; server-release = vcpkg) + vcpkg.json
- Skeleton builds with just a C++20 compiler; deps stay off until needed
- .gitattributes (LF), .gitignore, .clang-format
Core (libvoicecat):
- core/include/voicecat.h: full C ABI (the client/server contract), stubbed
- core/proto/voicecat.proto: control-plane wire format, matches docs/protocol.md
- src/{net,crypto,codec,protocol,session,audio,core}: subsystem stubs that
return VC_ERR_NOT_IMPLEMENTED, each pointing to its design doc
- server/ (voicecat-server) and tools/vccli/ link the core
- tests/: CTest smoke test asserting the C ABI contract (behavior, not just build)
- clients/{apple,windows}: M4 placeholders
Onboarding for agents:
- CLAUDE.md: hub — build/test commands, architecture at a glance, doc map, rules
- AGENTS.md: working method (behavior-driven; clean compile is the floor not the goal)
- PROGRESS.md: living tracker — M0 done, M1 task checklist, "where we left off"
Verified: cmake --preset dev && cmake --build --preset dev && ctest --preset dev → green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 21:09:09 +02:00
|
|
|
|
|
2026-06-21 17:45:28 +02:00
|
|
|
|
/* The control-protocol version this build speaks (docs/protocol.md §4).
|
|
|
|
|
|
* v2 widened the UDP voice frame seq field u16 → u64 (docs/voice.md §2); a v2 server
|
|
|
|
|
|
* and a v1 client cannot interoperate, so the Hello handshake rejects on mismatch. */
|
|
|
|
|
|
#define VOICECAT_PROTOCOL_VERSION 2
|
scaffold: M0 skeleton + agent onboarding (build, architecture, progress)
Turn the design into a buildable, dependency-free M0 skeleton plus the
onboarding layer so a new agent can pick up instantly.
Build system:
- CMake + CMakePresets (dev = no deps; server-release = vcpkg) + vcpkg.json
- Skeleton builds with just a C++20 compiler; deps stay off until needed
- .gitattributes (LF), .gitignore, .clang-format
Core (libvoicecat):
- core/include/voicecat.h: full C ABI (the client/server contract), stubbed
- core/proto/voicecat.proto: control-plane wire format, matches docs/protocol.md
- src/{net,crypto,codec,protocol,session,audio,core}: subsystem stubs that
return VC_ERR_NOT_IMPLEMENTED, each pointing to its design doc
- server/ (voicecat-server) and tools/vccli/ link the core
- tests/: CTest smoke test asserting the C ABI contract (behavior, not just build)
- clients/{apple,windows}: M4 placeholders
Onboarding for agents:
- CLAUDE.md: hub — build/test commands, architecture at a glance, doc map, rules
- AGENTS.md: working method (behavior-driven; clean compile is the floor not the goal)
- PROGRESS.md: living tracker — M0 done, M1 task checklist, "where we left off"
Verified: cmake --preset dev && cmake --build --preset dev && ctest --preset dev → green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 21:09:09 +02:00
|
|
|
|
|
|
|
|
|
|
/* ── Result codes ─────────────────────────────────────────────────────────── */
|
|
|
|
|
|
typedef enum vc_result {
|
|
|
|
|
|
VC_OK = 0,
|
2026-06-30 11:32:22 +01:00
|
|
|
|
VC_ERR_NOT_IMPLEMENTED = 1,
|
scaffold: M0 skeleton + agent onboarding (build, architecture, progress)
Turn the design into a buildable, dependency-free M0 skeleton plus the
onboarding layer so a new agent can pick up instantly.
Build system:
- CMake + CMakePresets (dev = no deps; server-release = vcpkg) + vcpkg.json
- Skeleton builds with just a C++20 compiler; deps stay off until needed
- .gitattributes (LF), .gitignore, .clang-format
Core (libvoicecat):
- core/include/voicecat.h: full C ABI (the client/server contract), stubbed
- core/proto/voicecat.proto: control-plane wire format, matches docs/protocol.md
- src/{net,crypto,codec,protocol,session,audio,core}: subsystem stubs that
return VC_ERR_NOT_IMPLEMENTED, each pointing to its design doc
- server/ (voicecat-server) and tools/vccli/ link the core
- tests/: CTest smoke test asserting the C ABI contract (behavior, not just build)
- clients/{apple,windows}: M4 placeholders
Onboarding for agents:
- CLAUDE.md: hub — build/test commands, architecture at a glance, doc map, rules
- AGENTS.md: working method (behavior-driven; clean compile is the floor not the goal)
- PROGRESS.md: living tracker — M0 done, M1 task checklist, "where we left off"
Verified: cmake --preset dev && cmake --build --preset dev && ctest --preset dev → green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 21:09:09 +02:00
|
|
|
|
VC_ERR_INVALID_ARG = 2,
|
|
|
|
|
|
VC_ERR_NOT_CONNECTED = 3,
|
|
|
|
|
|
VC_ERR_ALREADY = 4,
|
|
|
|
|
|
VC_ERR_AUTH_FAILED = 5,
|
|
|
|
|
|
VC_ERR_PERMISSION_DENIED = 6,
|
|
|
|
|
|
VC_ERR_TIMEOUT = 7,
|
|
|
|
|
|
VC_ERR_IO = 8,
|
|
|
|
|
|
VC_ERR_PROTOCOL = 9,
|
|
|
|
|
|
VC_ERR_CRYPTO = 10,
|
|
|
|
|
|
VC_ERR_AUDIO = 11,
|
|
|
|
|
|
VC_ERR_INTERNAL = 12,
|
|
|
|
|
|
} vc_result;
|
|
|
|
|
|
|
|
|
|
|
|
typedef enum vc_log_level {
|
|
|
|
|
|
VC_LOG_TRACE = 0,
|
|
|
|
|
|
VC_LOG_DEBUG = 1,
|
|
|
|
|
|
VC_LOG_INFO = 2,
|
|
|
|
|
|
VC_LOG_WARN = 3,
|
|
|
|
|
|
VC_LOG_ERROR = 4,
|
|
|
|
|
|
VC_LOG_OFF = 5,
|
|
|
|
|
|
} vc_log_level;
|
|
|
|
|
|
|
|
|
|
|
|
typedef enum vc_connection_state {
|
|
|
|
|
|
VC_STATE_DISCONNECTED = 0,
|
|
|
|
|
|
VC_STATE_CONNECTING = 1,
|
|
|
|
|
|
VC_STATE_TLS_HANDSHAKE = 2,
|
|
|
|
|
|
VC_STATE_AUTHENTICATING = 3,
|
|
|
|
|
|
VC_STATE_CONNECTED = 4,
|
feat(M4): Windows WinForms client, TOFU identity pinning, VAD threshold + always-on mode
Core ABI extensions (voicecat.h):
- vc_list_channels / vc_list_users / vc_list_user_streams — pull-based snapshot getters
for the channel-tree and user-list UI; session_model_mu_ guards cross-thread reads
- VC_EVENT_JOIN_RESULT / vc_join_channel — channel join with optional password
- VC_EVENT_SERVER_IDENTITY + vc_confirm_server_identity — TOFU gate that blocks io_thread_
until the UI approves or rejects; pins TLS leaf-cert SHA-256 (not declared Ed25519)
- vc_get_server_identity_display — Ed25519 fingerprint for human-readable display only
- VC_INPUT_ALWAYS_ON = 2 in vc_input_mode — transmit unconditionally, no VAD gate
- vc_set_vad_threshold — live RMS threshold update (0.0–1.0); EnergyVadProcessor stores
it atomically so the audio RT path reads without a lock
C++ implementation:
- SessionModel::apply_snapshot / apply_channel_event fixed to populate parent_id,
password_protected, and max_users (were permanently zeroed)
- TlsContext::peer_cert_fingerprint — SHA-256 of peer leaf cert DER via mbedTLS
- TofuStore split into peek (read-only) + pin (write) so first-connect only persists
after user approval; tofu_store_path in vc_config for per-user pin file location
- TcpAcceptor uses dual-stack IPv6+IPv4 fallback (fixes localhost → ::1 on Windows)
- windows-client CMake preset: Release shared DLL, static MinGW runtime, no tools/tests
- New C++ tests: test_channel_user_list_abi, test_tofu_flow (14/14 green)
Windows client (clients/windows/ — .NET 10 WinForms):
- VoiceCat.Interop: LibraryImport P/Invoke surface, UnmanagedCallersOnly callbacks,
Channel<VoiceCatEvent> event delivery drained by 30ms WinForms Timer
- VoiceCat.App: ConnectDialog (saved servers, DPAPI password storage), ServerIdentity-
Dialog (TOFU first-connect / mismatch warning), MainForm (channel TreeView, user
ListBox, RichTextBox chat, voice controls, device pickers, VAD/PTT/always-on mode,
per-user gain/mute/NR tuning, VAD sensitivity TrackBar, level meter ProgressBar)
- PttKeyCaptureDialog — focus-scoped PTT key capture (documented limitation)
- PerUserTuningDialog — real-time gain/mute/NR applied to all of a user's streams
- Accessibility: explicit AccessibleName/Description on every control, & mnemonics,
Activity log ListBox as durable screen-reader record, AutomationNotification for
curated live announcements
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 00:35:16 +02:00
|
|
|
|
/* M4: 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,
|
scaffold: M0 skeleton + agent onboarding (build, architecture, progress)
Turn the design into a buildable, dependency-free M0 skeleton plus the
onboarding layer so a new agent can pick up instantly.
Build system:
- CMake + CMakePresets (dev = no deps; server-release = vcpkg) + vcpkg.json
- Skeleton builds with just a C++20 compiler; deps stay off until needed
- .gitattributes (LF), .gitignore, .clang-format
Core (libvoicecat):
- core/include/voicecat.h: full C ABI (the client/server contract), stubbed
- core/proto/voicecat.proto: control-plane wire format, matches docs/protocol.md
- src/{net,crypto,codec,protocol,session,audio,core}: subsystem stubs that
return VC_ERR_NOT_IMPLEMENTED, each pointing to its design doc
- server/ (voicecat-server) and tools/vccli/ link the core
- tests/: CTest smoke test asserting the C ABI contract (behavior, not just build)
- clients/{apple,windows}: M4 placeholders
Onboarding for agents:
- CLAUDE.md: hub — build/test commands, architecture at a glance, doc map, rules
- AGENTS.md: working method (behavior-driven; clean compile is the floor not the goal)
- PROGRESS.md: living tracker — M0 done, M1 task checklist, "where we left off"
Verified: cmake --preset dev && cmake --build --preset dev && ctest --preset dev → green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 21:09:09 +02:00
|
|
|
|
} vc_connection_state;
|
|
|
|
|
|
|
|
|
|
|
|
typedef enum vc_text_scope {
|
|
|
|
|
|
VC_TEXT_CHANNEL = 0,
|
|
|
|
|
|
VC_TEXT_PRIVATE = 1,
|
|
|
|
|
|
VC_TEXT_SERVER = 2,
|
|
|
|
|
|
} vc_text_scope;
|
|
|
|
|
|
|
|
|
|
|
|
typedef enum vc_device_kind {
|
|
|
|
|
|
VC_DEVICE_INPUT = 0,
|
|
|
|
|
|
VC_DEVICE_OUTPUT = 1,
|
|
|
|
|
|
} vc_device_kind;
|
|
|
|
|
|
|
|
|
|
|
|
typedef enum vc_stream_kind {
|
|
|
|
|
|
VC_STREAM_MIC = 0,
|
|
|
|
|
|
VC_STREAM_SCREEN_AUDIO = 1, /* system/desktop audio (docs/voice.md §9) */
|
|
|
|
|
|
VC_STREAM_AUX_DEVICE = 2,
|
|
|
|
|
|
} vc_stream_kind;
|
|
|
|
|
|
|
|
|
|
|
|
/* Send-side input gate (docs/voice.md §11). */
|
|
|
|
|
|
typedef enum vc_input_mode {
|
|
|
|
|
|
VC_INPUT_VOICE_ACTIVATION = 0,
|
|
|
|
|
|
VC_INPUT_PUSH_TO_TALK = 1,
|
feat(M4): Windows WinForms client, TOFU identity pinning, VAD threshold + always-on mode
Core ABI extensions (voicecat.h):
- vc_list_channels / vc_list_users / vc_list_user_streams — pull-based snapshot getters
for the channel-tree and user-list UI; session_model_mu_ guards cross-thread reads
- VC_EVENT_JOIN_RESULT / vc_join_channel — channel join with optional password
- VC_EVENT_SERVER_IDENTITY + vc_confirm_server_identity — TOFU gate that blocks io_thread_
until the UI approves or rejects; pins TLS leaf-cert SHA-256 (not declared Ed25519)
- vc_get_server_identity_display — Ed25519 fingerprint for human-readable display only
- VC_INPUT_ALWAYS_ON = 2 in vc_input_mode — transmit unconditionally, no VAD gate
- vc_set_vad_threshold — live RMS threshold update (0.0–1.0); EnergyVadProcessor stores
it atomically so the audio RT path reads without a lock
C++ implementation:
- SessionModel::apply_snapshot / apply_channel_event fixed to populate parent_id,
password_protected, and max_users (were permanently zeroed)
- TlsContext::peer_cert_fingerprint — SHA-256 of peer leaf cert DER via mbedTLS
- TofuStore split into peek (read-only) + pin (write) so first-connect only persists
after user approval; tofu_store_path in vc_config for per-user pin file location
- TcpAcceptor uses dual-stack IPv6+IPv4 fallback (fixes localhost → ::1 on Windows)
- windows-client CMake preset: Release shared DLL, static MinGW runtime, no tools/tests
- New C++ tests: test_channel_user_list_abi, test_tofu_flow (14/14 green)
Windows client (clients/windows/ — .NET 10 WinForms):
- VoiceCat.Interop: LibraryImport P/Invoke surface, UnmanagedCallersOnly callbacks,
Channel<VoiceCatEvent> event delivery drained by 30ms WinForms Timer
- VoiceCat.App: ConnectDialog (saved servers, DPAPI password storage), ServerIdentity-
Dialog (TOFU first-connect / mismatch warning), MainForm (channel TreeView, user
ListBox, RichTextBox chat, voice controls, device pickers, VAD/PTT/always-on mode,
per-user gain/mute/NR tuning, VAD sensitivity TrackBar, level meter ProgressBar)
- PttKeyCaptureDialog — focus-scoped PTT key capture (documented limitation)
- PerUserTuningDialog — real-time gain/mute/NR applied to all of a user's streams
- Accessibility: explicit AccessibleName/Description on every control, & mnemonics,
Activity log ListBox as durable screen-reader record, AutomationNotification for
curated live announcements
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 00:35:16 +02:00
|
|
|
|
/* Transmit unconditionally — no VAD gate. Added at the end to keep existing values stable. */
|
|
|
|
|
|
VC_INPUT_ALWAYS_ON = 2,
|
scaffold: M0 skeleton + agent onboarding (build, architecture, progress)
Turn the design into a buildable, dependency-free M0 skeleton plus the
onboarding layer so a new agent can pick up instantly.
Build system:
- CMake + CMakePresets (dev = no deps; server-release = vcpkg) + vcpkg.json
- Skeleton builds with just a C++20 compiler; deps stay off until needed
- .gitattributes (LF), .gitignore, .clang-format
Core (libvoicecat):
- core/include/voicecat.h: full C ABI (the client/server contract), stubbed
- core/proto/voicecat.proto: control-plane wire format, matches docs/protocol.md
- src/{net,crypto,codec,protocol,session,audio,core}: subsystem stubs that
return VC_ERR_NOT_IMPLEMENTED, each pointing to its design doc
- server/ (voicecat-server) and tools/vccli/ link the core
- tests/: CTest smoke test asserting the C ABI contract (behavior, not just build)
- clients/{apple,windows}: M4 placeholders
Onboarding for agents:
- CLAUDE.md: hub — build/test commands, architecture at a glance, doc map, rules
- AGENTS.md: working method (behavior-driven; clean compile is the floor not the goal)
- PROGRESS.md: living tracker — M0 done, M1 task checklist, "where we left off"
Verified: cmake --preset dev && cmake --build --preset dev && ctest --preset dev → green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 21:09:09 +02:00
|
|
|
|
} vc_input_mode;
|
|
|
|
|
|
|
|
|
|
|
|
typedef enum vc_event_type {
|
|
|
|
|
|
VC_EVENT_CONNECTION_STATE = 0, /* connection_state set */
|
|
|
|
|
|
VC_EVENT_AUTH_RESULT = 1, /* result set; user_id = self on success */
|
|
|
|
|
|
VC_EVENT_CHANNEL_LIST = 2, /* channel tree snapshot/delta available */
|
|
|
|
|
|
VC_EVENT_USER_JOINED = 3, /* user_id, channel_id, text = nickname */
|
|
|
|
|
|
VC_EVENT_USER_LEFT = 4, /* user_id */
|
|
|
|
|
|
VC_EVENT_USER_UPDATED = 5, /* user_id */
|
|
|
|
|
|
VC_EVENT_TEXT_MESSAGE = 6, /* text_scope, user_id (sender), channel_id, text */
|
|
|
|
|
|
VC_EVENT_STREAM_STARTED = 7, /* user_id, stream_id */
|
|
|
|
|
|
VC_EVENT_STREAM_STOPPED = 8, /* user_id, stream_id */
|
|
|
|
|
|
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 */
|
M5: moderation, permissions, channel CRUD, in-app account management
- Server-side moderation & permissions (kick/ban/move/server-mute, channel CRUD).
- Database schema v2: channels, bans; BLAKE2b channel passwords, Argon2id accounts.
- C ABI additions and client-side handling (vc_kick_user, vc_ban_user, vc_set_permission, vc_set_server_mute, vc_move_user, vc_create/edit/delete_channel, vc_create/reset/delete/list_account).
- vccli flags for all M5 operations plus --username/--password auth.
- Four new tests covering permissions, kick/ban/move/mute, admin accounts, channel CRUD.
- Docs: protocol.md envelope updates, security.md channel-password hashing, PROGRESS.md.
2026-06-17 15:08:05 +02:00
|
|
|
|
/* M4 additions — appended, not inserted, to keep existing enum values stable. */
|
feat(M4): Windows WinForms client, TOFU identity pinning, VAD threshold + always-on mode
Core ABI extensions (voicecat.h):
- vc_list_channels / vc_list_users / vc_list_user_streams — pull-based snapshot getters
for the channel-tree and user-list UI; session_model_mu_ guards cross-thread reads
- VC_EVENT_JOIN_RESULT / vc_join_channel — channel join with optional password
- VC_EVENT_SERVER_IDENTITY + vc_confirm_server_identity — TOFU gate that blocks io_thread_
until the UI approves or rejects; pins TLS leaf-cert SHA-256 (not declared Ed25519)
- vc_get_server_identity_display — Ed25519 fingerprint for human-readable display only
- VC_INPUT_ALWAYS_ON = 2 in vc_input_mode — transmit unconditionally, no VAD gate
- vc_set_vad_threshold — live RMS threshold update (0.0–1.0); EnergyVadProcessor stores
it atomically so the audio RT path reads without a lock
C++ implementation:
- SessionModel::apply_snapshot / apply_channel_event fixed to populate parent_id,
password_protected, and max_users (were permanently zeroed)
- TlsContext::peer_cert_fingerprint — SHA-256 of peer leaf cert DER via mbedTLS
- TofuStore split into peek (read-only) + pin (write) so first-connect only persists
after user approval; tofu_store_path in vc_config for per-user pin file location
- TcpAcceptor uses dual-stack IPv6+IPv4 fallback (fixes localhost → ::1 on Windows)
- windows-client CMake preset: Release shared DLL, static MinGW runtime, no tools/tests
- New C++ tests: test_channel_user_list_abi, test_tofu_flow (14/14 green)
Windows client (clients/windows/ — .NET 10 WinForms):
- VoiceCat.Interop: LibraryImport P/Invoke surface, UnmanagedCallersOnly callbacks,
Channel<VoiceCatEvent> event delivery drained by 30ms WinForms Timer
- VoiceCat.App: ConnectDialog (saved servers, DPAPI password storage), ServerIdentity-
Dialog (TOFU first-connect / mismatch warning), MainForm (channel TreeView, user
ListBox, RichTextBox chat, voice controls, device pickers, VAD/PTT/always-on mode,
per-user gain/mute/NR tuning, VAD sensitivity TrackBar, level meter ProgressBar)
- PttKeyCaptureDialog — focus-scoped PTT key capture (documented limitation)
- PerUserTuningDialog — real-time gain/mute/NR applied to all of a user's streams
- Accessibility: explicit AccessibleName/Description on every control, & mnemonics,
Activity log ListBox as durable screen-reader record, AutomationNotification for
curated live announcements
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 00:35:16 +02:00
|
|
|
|
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
|
|
|
|
|
|
SHA-256 fingerprint (the value being pinned — see
|
|
|
|
|
|
vc_confirm_server_identity). Emitted once per connect
|
|
|
|
|
|
attempt, right after the TLS handshake succeeds. The
|
|
|
|
|
|
connection is held open until vc_confirm_server_identity()
|
|
|
|
|
|
is called. */
|
M5: moderation, permissions, channel CRUD, in-app account management
- Server-side moderation & permissions (kick/ban/move/server-mute, channel CRUD).
- Database schema v2: channels, bans; BLAKE2b channel passwords, Argon2id accounts.
- C ABI additions and client-side handling (vc_kick_user, vc_ban_user, vc_set_permission, vc_set_server_mute, vc_move_user, vc_create/edit/delete_channel, vc_create/reset/delete/list_account).
- vccli flags for all M5 operations plus --username/--password auth.
- Four new tests covering permissions, kick/ban/move/mute, admin accounts, channel CRUD.
- Docs: protocol.md envelope updates, security.md channel-password hashing, PROGRESS.md.
2026-06-17 15:08:05 +02:00
|
|
|
|
/* M5 additions — 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/
|
|
|
|
|
|
vc_delete_channel/vc_create_account/vc_reset_password/
|
|
|
|
|
|
vc_delete_account. */
|
|
|
|
|
|
VC_EVENT_ACCOUNT_LIST = 15, /* Reply to vc_list_accounts. */
|
feat: fix voice join/leave, channel edit defaults, channel-update stream restart
Three bugs fixed across the full stack (proto/server/core/ABI/Win/macOS/iOS):
1. Join/Leave Voice now truly subscribes/unsubscribes from the voice plane.
Previously the button only toggled the local mic — receiving was always on
(gated by channel membership alone). Added a protocol-level voice subscription
concept: new SubscribeVoiceRequest/UnsubscribeVoiceRequest/VoiceSubscriptionResult
proto messages, User.voice_subscribed field, vc_join_voice/vc_leave_voice C ABI
functions, VC_EVENT_VOICE_STATE event, server-side voice_subscribed flag checked
by the SFU relay recipient filter, and core-client gating of remote-stream
decoder setup. All three clients rewired to subscribe+mic on Join / unsubscribe
on Leave. Text chat works regardless of voice subscription.
2. Channel edit dialog now shows the channel's actual current settings. The read
struct vc_channel was missing sort_order and audio fields — only the write
struct vc_channel_info had them. Extended vc_channel with both (additive, no
ABI break), updated the session model and list_channels marshaling to populate
them, and updated all three clients' edit callers to use actual channel info
instead of hardcoded defaults.
3. Channel parameter updates now automatically restart everyone's streams.
Previously editing a channel's audio config persisted and broadcast a
ChannelEvent::UPDATED, but no layer restarted streams — encoders/decoders are
frozen at announce time. handle_channel_event now detects audio-config changes
on the user's current channel and stop->starts each active local stream. The
server reads the updated config on re-announce; peers wire up fresh decoders
at the new ssrc.
All 29 CTest tests pass; Windows DLL + C# client build clean. Apple clients not
yet compile-verified (Windows environment).
2026-06-24 14:29:39 +02:00
|
|
|
|
VC_EVENT_VOICE_STATE = 16, /* u32a = subscribed(0/1). Reply to vc_join_voice()/
|
|
|
|
|
|
vc_leave_voice(), and also emitted when the server
|
|
|
|
|
|
changes your voice-subscription state. The user list
|
|
|
|
|
|
(vc_user) carries per-user voice_subscribed. */
|
scaffold: M0 skeleton + agent onboarding (build, architecture, progress)
Turn the design into a buildable, dependency-free M0 skeleton plus the
onboarding layer so a new agent can pick up instantly.
Build system:
- CMake + CMakePresets (dev = no deps; server-release = vcpkg) + vcpkg.json
- Skeleton builds with just a C++20 compiler; deps stay off until needed
- .gitattributes (LF), .gitignore, .clang-format
Core (libvoicecat):
- core/include/voicecat.h: full C ABI (the client/server contract), stubbed
- core/proto/voicecat.proto: control-plane wire format, matches docs/protocol.md
- src/{net,crypto,codec,protocol,session,audio,core}: subsystem stubs that
return VC_ERR_NOT_IMPLEMENTED, each pointing to its design doc
- server/ (voicecat-server) and tools/vccli/ link the core
- tests/: CTest smoke test asserting the C ABI contract (behavior, not just build)
- clients/{apple,windows}: M4 placeholders
Onboarding for agents:
- CLAUDE.md: hub — build/test commands, architecture at a glance, doc map, rules
- AGENTS.md: working method (behavior-driven; clean compile is the floor not the goal)
- PROGRESS.md: living tracker — M0 done, M1 task checklist, "where we left off"
Verified: cmake --preset dev && cmake --build --preset dev && ctest --preset dev → green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 21:09:09 +02:00
|
|
|
|
} vc_event_type;
|
|
|
|
|
|
|
feat(M4): Windows WinForms client, TOFU identity pinning, VAD threshold + always-on mode
Core ABI extensions (voicecat.h):
- vc_list_channels / vc_list_users / vc_list_user_streams — pull-based snapshot getters
for the channel-tree and user-list UI; session_model_mu_ guards cross-thread reads
- VC_EVENT_JOIN_RESULT / vc_join_channel — channel join with optional password
- VC_EVENT_SERVER_IDENTITY + vc_confirm_server_identity — TOFU gate that blocks io_thread_
until the UI approves or rejects; pins TLS leaf-cert SHA-256 (not declared Ed25519)
- vc_get_server_identity_display — Ed25519 fingerprint for human-readable display only
- VC_INPUT_ALWAYS_ON = 2 in vc_input_mode — transmit unconditionally, no VAD gate
- vc_set_vad_threshold — live RMS threshold update (0.0–1.0); EnergyVadProcessor stores
it atomically so the audio RT path reads without a lock
C++ implementation:
- SessionModel::apply_snapshot / apply_channel_event fixed to populate parent_id,
password_protected, and max_users (were permanently zeroed)
- TlsContext::peer_cert_fingerprint — SHA-256 of peer leaf cert DER via mbedTLS
- TofuStore split into peek (read-only) + pin (write) so first-connect only persists
after user approval; tofu_store_path in vc_config for per-user pin file location
- TcpAcceptor uses dual-stack IPv6+IPv4 fallback (fixes localhost → ::1 on Windows)
- windows-client CMake preset: Release shared DLL, static MinGW runtime, no tools/tests
- New C++ tests: test_channel_user_list_abi, test_tofu_flow (14/14 green)
Windows client (clients/windows/ — .NET 10 WinForms):
- VoiceCat.Interop: LibraryImport P/Invoke surface, UnmanagedCallersOnly callbacks,
Channel<VoiceCatEvent> event delivery drained by 30ms WinForms Timer
- VoiceCat.App: ConnectDialog (saved servers, DPAPI password storage), ServerIdentity-
Dialog (TOFU first-connect / mismatch warning), MainForm (channel TreeView, user
ListBox, RichTextBox chat, voice controls, device pickers, VAD/PTT/always-on mode,
per-user gain/mute/NR tuning, VAD sensitivity TrackBar, level meter ProgressBar)
- PttKeyCaptureDialog — focus-scoped PTT key capture (documented limitation)
- PerUserTuningDialog — real-time gain/mute/NR applied to all of a user's streams
- Accessibility: explicit AccessibleName/Description on every control, & mnemonics,
Activity log ListBox as durable screen-reader record, AutomationNotification for
curated live announcements
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 00:35:16 +02:00
|
|
|
|
/* TOFU server-identity classification (M4) — 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
|
|
|
|
|
|
* identity key are generated independently with no cryptographic binding between them today
|
|
|
|
|
|
* (docs/security.md §1.1), so pinning the self-declared value would be circular. The Ed25519
|
|
|
|
|
|
* fingerprint is still available for human-readable display via
|
|
|
|
|
|
* vc_get_server_identity_display(), it just isn't the value this gate accepts/rejects on. */
|
|
|
|
|
|
typedef enum vc_tofu_status {
|
|
|
|
|
|
VC_TOFU_FIRST_CONNECT = 0, /* no pin on file yet for this host:port */
|
|
|
|
|
|
VC_TOFU_MATCHED = 1, /* matches the previously pinned fingerprint */
|
|
|
|
|
|
VC_TOFU_MISMATCH = 2, /* DIFFERENT from the pinned fingerprint — possible MITM or a
|
|
|
|
|
|
legitimate server key rotation; warn loudly */
|
|
|
|
|
|
} vc_tofu_status;
|
|
|
|
|
|
|
scaffold: M0 skeleton + agent onboarding (build, architecture, progress)
Turn the design into a buildable, dependency-free M0 skeleton plus the
onboarding layer so a new agent can pick up instantly.
Build system:
- CMake + CMakePresets (dev = no deps; server-release = vcpkg) + vcpkg.json
- Skeleton builds with just a C++20 compiler; deps stay off until needed
- .gitattributes (LF), .gitignore, .clang-format
Core (libvoicecat):
- core/include/voicecat.h: full C ABI (the client/server contract), stubbed
- core/proto/voicecat.proto: control-plane wire format, matches docs/protocol.md
- src/{net,crypto,codec,protocol,session,audio,core}: subsystem stubs that
return VC_ERR_NOT_IMPLEMENTED, each pointing to its design doc
- server/ (voicecat-server) and tools/vccli/ link the core
- tests/: CTest smoke test asserting the C ABI contract (behavior, not just build)
- clients/{apple,windows}: M4 placeholders
Onboarding for agents:
- CLAUDE.md: hub — build/test commands, architecture at a glance, doc map, rules
- AGENTS.md: working method (behavior-driven; clean compile is the floor not the goal)
- PROGRESS.md: living tracker — M0 done, M1 task checklist, "where we left off"
Verified: cmake --preset dev && cmake --build --preset dev && ctest --preset dev → green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 21:09:09 +02:00
|
|
|
|
/* ── Structs ──────────────────────────────────────────────────────────────── */
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
|
* An event delivered to vc_callbacks.on_event. Pointer fields are owned by the core and
|
|
|
|
|
|
* valid ONLY for the duration of the callback — copy what you need. Which fields are
|
|
|
|
|
|
* meaningful depends on `type` (see vc_event_type comments above).
|
|
|
|
|
|
*/
|
|
|
|
|
|
typedef struct vc_event {
|
|
|
|
|
|
vc_event_type type;
|
|
|
|
|
|
vc_connection_state connection_state;
|
|
|
|
|
|
int32_t result; /* vc_result */
|
|
|
|
|
|
uint32_t user_id;
|
|
|
|
|
|
uint32_t channel_id;
|
|
|
|
|
|
uint32_t stream_id;
|
|
|
|
|
|
vc_text_scope text_scope;
|
|
|
|
|
|
uint32_t u32a; /* generic small payload, meaning per event type */
|
|
|
|
|
|
const char* text;
|
|
|
|
|
|
uint64_t timestamp_unix_ms;
|
|
|
|
|
|
} vc_event;
|
|
|
|
|
|
|
|
|
|
|
|
typedef struct vc_callbacks {
|
|
|
|
|
|
/* State changes, messages, presence. Called on the core's event thread. */
|
|
|
|
|
|
void (*on_event)(void* user, const vc_event* ev);
|
|
|
|
|
|
/* Throttled level meter (RMS 0..1) for a local or remote stream; may be NULL. */
|
|
|
|
|
|
void (*on_level)(void* user, uint32_t stream_id, float rms);
|
|
|
|
|
|
void* user;
|
|
|
|
|
|
} vc_callbacks;
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
feat(M4): Windows WinForms client, TOFU identity pinning, VAD threshold + always-on mode
Core ABI extensions (voicecat.h):
- vc_list_channels / vc_list_users / vc_list_user_streams — pull-based snapshot getters
for the channel-tree and user-list UI; session_model_mu_ guards cross-thread reads
- VC_EVENT_JOIN_RESULT / vc_join_channel — channel join with optional password
- VC_EVENT_SERVER_IDENTITY + vc_confirm_server_identity — TOFU gate that blocks io_thread_
until the UI approves or rejects; pins TLS leaf-cert SHA-256 (not declared Ed25519)
- vc_get_server_identity_display — Ed25519 fingerprint for human-readable display only
- VC_INPUT_ALWAYS_ON = 2 in vc_input_mode — transmit unconditionally, no VAD gate
- vc_set_vad_threshold — live RMS threshold update (0.0–1.0); EnergyVadProcessor stores
it atomically so the audio RT path reads without a lock
C++ implementation:
- SessionModel::apply_snapshot / apply_channel_event fixed to populate parent_id,
password_protected, and max_users (were permanently zeroed)
- TlsContext::peer_cert_fingerprint — SHA-256 of peer leaf cert DER via mbedTLS
- TofuStore split into peek (read-only) + pin (write) so first-connect only persists
after user approval; tofu_store_path in vc_config for per-user pin file location
- TcpAcceptor uses dual-stack IPv6+IPv4 fallback (fixes localhost → ::1 on Windows)
- windows-client CMake preset: Release shared DLL, static MinGW runtime, no tools/tests
- New C++ tests: test_channel_user_list_abi, test_tofu_flow (14/14 green)
Windows client (clients/windows/ — .NET 10 WinForms):
- VoiceCat.Interop: LibraryImport P/Invoke surface, UnmanagedCallersOnly callbacks,
Channel<VoiceCatEvent> event delivery drained by 30ms WinForms Timer
- VoiceCat.App: ConnectDialog (saved servers, DPAPI password storage), ServerIdentity-
Dialog (TOFU first-connect / mismatch warning), MainForm (channel TreeView, user
ListBox, RichTextBox chat, voice controls, device pickers, VAD/PTT/always-on mode,
per-user gain/mute/NR tuning, VAD sensitivity TrackBar, level meter ProgressBar)
- PttKeyCaptureDialog — focus-scoped PTT key capture (documented limitation)
- PerUserTuningDialog — real-time gain/mute/NR applied to all of a user's streams
- Accessibility: explicit AccessibleName/Description on every control, & mnemonics,
Activity log ListBox as durable screen-reader record, AutomationNotification for
curated live announcements
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 00:35:16 +02:00
|
|
|
|
/* M4, 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
|
|
|
|
|
|
* (e.g. the Windows client) should pass an explicit per-user path, e.g.
|
|
|
|
|
|
* "%AppData%\VoiceCat\tofu_pins.txt". */
|
|
|
|
|
|
const char* tofu_store_path;
|
scaffold: M0 skeleton + agent onboarding (build, architecture, progress)
Turn the design into a buildable, dependency-free M0 skeleton plus the
onboarding layer so a new agent can pick up instantly.
Build system:
- CMake + CMakePresets (dev = no deps; server-release = vcpkg) + vcpkg.json
- Skeleton builds with just a C++20 compiler; deps stay off until needed
- .gitattributes (LF), .gitignore, .clang-format
Core (libvoicecat):
- core/include/voicecat.h: full C ABI (the client/server contract), stubbed
- core/proto/voicecat.proto: control-plane wire format, matches docs/protocol.md
- src/{net,crypto,codec,protocol,session,audio,core}: subsystem stubs that
return VC_ERR_NOT_IMPLEMENTED, each pointing to its design doc
- server/ (voicecat-server) and tools/vccli/ link the core
- tests/: CTest smoke test asserting the C ABI contract (behavior, not just build)
- clients/{apple,windows}: M4 placeholders
Onboarding for agents:
- CLAUDE.md: hub — build/test commands, architecture at a glance, doc map, rules
- AGENTS.md: working method (behavior-driven; clean compile is the floor not the goal)
- PROGRESS.md: living tracker — M0 done, M1 task checklist, "where we left off"
Verified: cmake --preset dev && cmake --build --preset dev && ctest --preset dev → green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 21:09:09 +02:00
|
|
|
|
} vc_config;
|
|
|
|
|
|
|
|
|
|
|
|
typedef struct vc_stream_desc {
|
|
|
|
|
|
vc_stream_kind kind;
|
|
|
|
|
|
const char* device_id; /* NULL = default device for this kind */
|
|
|
|
|
|
const char* label; /* human label, e.g. "Microphone" */
|
2026-06-22 00:19:38 +02:00
|
|
|
|
/* If 1, the caller will feed PCM via vc_stream_feed_pcm; the core will NOT start its own
|
|
|
|
|
|
* WASAPI loopback capture. Only meaningful for VC_STREAM_SCREEN_AUDIO on Windows. Callers
|
|
|
|
|
|
* that brace-initialize this struct (tests, macOS) get 0 = auto-start loopback — no ABI
|
|
|
|
|
|
* break. See clients/windows/VoiceCat.App/Audio/ProcessAudioMixer.cs for the Windows
|
|
|
|
|
|
* per-app capture implementation that sets this. */
|
|
|
|
|
|
int external_feed;
|
scaffold: M0 skeleton + agent onboarding (build, architecture, progress)
Turn the design into a buildable, dependency-free M0 skeleton plus the
onboarding layer so a new agent can pick up instantly.
Build system:
- CMake + CMakePresets (dev = no deps; server-release = vcpkg) + vcpkg.json
- Skeleton builds with just a C++20 compiler; deps stay off until needed
- .gitattributes (LF), .gitignore, .clang-format
Core (libvoicecat):
- core/include/voicecat.h: full C ABI (the client/server contract), stubbed
- core/proto/voicecat.proto: control-plane wire format, matches docs/protocol.md
- src/{net,crypto,codec,protocol,session,audio,core}: subsystem stubs that
return VC_ERR_NOT_IMPLEMENTED, each pointing to its design doc
- server/ (voicecat-server) and tools/vccli/ link the core
- tests/: CTest smoke test asserting the C ABI contract (behavior, not just build)
- clients/{apple,windows}: M4 placeholders
Onboarding for agents:
- CLAUDE.md: hub — build/test commands, architecture at a glance, doc map, rules
- AGENTS.md: working method (behavior-driven; clean compile is the floor not the goal)
- PROGRESS.md: living tracker — M0 done, M1 task checklist, "where we left off"
Verified: cmake --preset dev && cmake --build --preset dev && ctest --preset dev → green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 21:09:09 +02:00
|
|
|
|
} vc_stream_desc;
|
|
|
|
|
|
|
feat(M3): multi-stream & per-channel tuning
Implements docs/roadmap.md M3: multiple concurrent streams per user (MIC +
SCREEN_AUDIO + AUX_DEVICE), independent per-stream receiver gain/mute/noise-
reduction, talk indicators, and enforced per-channel Opus configurability
(mono/stereo, bitrate, frame size, FEC/DTX, application).
Bugs fixed along the way (found while implementing, not pre-existing scope):
- Server hard-coded stream_id=1 for every announce, so a second stream from
the same user silently overwrote the first in SessionRegistry::set_user_stream.
Now a per-session counter (ConnSession::next_stream_id_); handle_stream_stop
validates against announced_stream_ids_ before clearing.
- Client dropped mode/dtx/complexity/application from effective_audio even for
the single M2 stream -- only sample_rate/bitrate_bps/frame_ms/fec were ever
applied to OpusParams. Fixed on both the send (handle_stream_announce_result)
and receive (sync_remote_streams) paths via a shared
opus_params_from_audio_config() helper.
- OpusEncoder always used OPUS_APPLICATION_VOIP; added OpusParams::application
and wired it through.
- on_playback's per-stream decode passed the wrong frame_size to opus_decode
(total samples instead of samples-per-channel), which would have overflowed
the decode buffer for any stereo stream.
- teardown_voice() raced when called concurrently from run_io()'s own cleanup
and from disconnect() on a different thread -- both could see
udp_thread_/talk_timer_thread_ as joinable() at once and race to join() the
same std::thread (intermittent std::system_error under ctest). Fixed with a
teardown_mu_ guard instead of carrying the flake forward.
New:
- Per-channel AudioConfig: SessionRegistry now seeds Lobby (mono/24kbps/VOIP/
FEC+DTX) and a new "Music Room" channel (stereo/128kbps/AUDIO/no DTX);
handle_stream_announce enforces the channel's config, clamping (not
overriding) bitrate_bps to its ceiling.
- core/src/core/client.h/.cpp: local-stream state is now a
std::unordered_map<int, LocalStream> keyed by vc_stream_kind, with
request_id-correlated announce/result handling (request_id already
round-tripped on the wire; just wasn't read before). on_capture_frame is
kind-aware and upmixes mono capture to stereo when a stream's config calls
for it. set_self_mute's mic_muted now only gates the MIC kind. NS is wired
through set_remote_stream. New run_talk_timer() thread emits
VC_EVENT_TALK_STATE from both remote and local edge detection.
- core/src/audio/audio_engine.h/.cpp: kind-keyed injection taps
(inject_capture), stereo-to-mono downmix at the decode/mix boundary,
RemoteStream gains recv_ns (lazy ApmProcessor) + noise_reduction_enabled
and last_voice_ms/talking; new set_stream_noise_reduction() and
poll_talk_transitions().
- core/src/session/session.h/.cpp: Stream now carries the full AudioConfig,
not just sample_rate/frame_ms.
- New additive C ABI (core/include/voicecat.h): vc_audio_config +
vc_get_stream_audio_config (effective Opus config for any stream you own or
a peer's); vc_test_inject_capture (test-only synthetic PCM injection,
clearly marked, mirrors AudioEngine::inject_capture).
- tests/test_m3_multistream.cpp: the M3 exit criterion through the real ABI
(mirrors test_voice_client_abi.cpp's approach, not raw sockets) -- two
concurrent local streams, independent gain/mute/NS control, per-channel
config divergence via vc_get_stream_audio_config, talk indicators.
Explicitly out of scope for this pass (tracked in PROGRESS.md, not silently
dropped): VAD/PTT input gate + device enumeration; real WASAPI loopback
capture for SCREEN_AUDIO (synthetic injection only); true stereo playback
output (AudioEngine's mixer/output device stays mono -- Opus itself is fully
stereo-correct on the wire).
ctest --test-dir build/m1-dev: 11/11 green, verified across 3 consecutive
full-suite runs plus 8 standalone runs of the new test.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 14:12:37 +02:00
|
|
|
|
/* The effective Opus configuration in use for a stream — for a stream you own, this is
|
|
|
|
|
|
* StreamAnnounceResult.effective_audio (channel-enforced, docs/voice.md §3); for a remote
|
|
|
|
|
|
* stream, it's the peer's broadcast StreamInfo.audio. See vc_get_stream_audio_config. */
|
|
|
|
|
|
typedef struct vc_audio_config {
|
|
|
|
|
|
uint32_t codec; /* 0 = OPUS */
|
|
|
|
|
|
uint32_t mode; /* 0 = mono, 1 = stereo */
|
|
|
|
|
|
uint32_t sample_rate;
|
|
|
|
|
|
uint32_t bitrate_bps;
|
|
|
|
|
|
uint32_t frame_ms;
|
|
|
|
|
|
uint32_t application; /* 0 = VOIP, 1 = AUDIO, 2 = LOWDELAY */
|
|
|
|
|
|
int fec; /* bool */
|
|
|
|
|
|
uint32_t expected_packet_loss; /* % 0..100 */
|
|
|
|
|
|
int dtx; /* bool */
|
|
|
|
|
|
uint32_t complexity; /* 0..10 */
|
2026-06-20 13:40:47 +02:00
|
|
|
|
int dred; /* bool — Deep REDundancy (Opus 1.6), off by default */
|
feat(M3): multi-stream & per-channel tuning
Implements docs/roadmap.md M3: multiple concurrent streams per user (MIC +
SCREEN_AUDIO + AUX_DEVICE), independent per-stream receiver gain/mute/noise-
reduction, talk indicators, and enforced per-channel Opus configurability
(mono/stereo, bitrate, frame size, FEC/DTX, application).
Bugs fixed along the way (found while implementing, not pre-existing scope):
- Server hard-coded stream_id=1 for every announce, so a second stream from
the same user silently overwrote the first in SessionRegistry::set_user_stream.
Now a per-session counter (ConnSession::next_stream_id_); handle_stream_stop
validates against announced_stream_ids_ before clearing.
- Client dropped mode/dtx/complexity/application from effective_audio even for
the single M2 stream -- only sample_rate/bitrate_bps/frame_ms/fec were ever
applied to OpusParams. Fixed on both the send (handle_stream_announce_result)
and receive (sync_remote_streams) paths via a shared
opus_params_from_audio_config() helper.
- OpusEncoder always used OPUS_APPLICATION_VOIP; added OpusParams::application
and wired it through.
- on_playback's per-stream decode passed the wrong frame_size to opus_decode
(total samples instead of samples-per-channel), which would have overflowed
the decode buffer for any stereo stream.
- teardown_voice() raced when called concurrently from run_io()'s own cleanup
and from disconnect() on a different thread -- both could see
udp_thread_/talk_timer_thread_ as joinable() at once and race to join() the
same std::thread (intermittent std::system_error under ctest). Fixed with a
teardown_mu_ guard instead of carrying the flake forward.
New:
- Per-channel AudioConfig: SessionRegistry now seeds Lobby (mono/24kbps/VOIP/
FEC+DTX) and a new "Music Room" channel (stereo/128kbps/AUDIO/no DTX);
handle_stream_announce enforces the channel's config, clamping (not
overriding) bitrate_bps to its ceiling.
- core/src/core/client.h/.cpp: local-stream state is now a
std::unordered_map<int, LocalStream> keyed by vc_stream_kind, with
request_id-correlated announce/result handling (request_id already
round-tripped on the wire; just wasn't read before). on_capture_frame is
kind-aware and upmixes mono capture to stereo when a stream's config calls
for it. set_self_mute's mic_muted now only gates the MIC kind. NS is wired
through set_remote_stream. New run_talk_timer() thread emits
VC_EVENT_TALK_STATE from both remote and local edge detection.
- core/src/audio/audio_engine.h/.cpp: kind-keyed injection taps
(inject_capture), stereo-to-mono downmix at the decode/mix boundary,
RemoteStream gains recv_ns (lazy ApmProcessor) + noise_reduction_enabled
and last_voice_ms/talking; new set_stream_noise_reduction() and
poll_talk_transitions().
- core/src/session/session.h/.cpp: Stream now carries the full AudioConfig,
not just sample_rate/frame_ms.
- New additive C ABI (core/include/voicecat.h): vc_audio_config +
vc_get_stream_audio_config (effective Opus config for any stream you own or
a peer's); vc_test_inject_capture (test-only synthetic PCM injection,
clearly marked, mirrors AudioEngine::inject_capture).
- tests/test_m3_multistream.cpp: the M3 exit criterion through the real ABI
(mirrors test_voice_client_abi.cpp's approach, not raw sockets) -- two
concurrent local streams, independent gain/mute/NS control, per-channel
config divergence via vc_get_stream_audio_config, talk indicators.
Explicitly out of scope for this pass (tracked in PROGRESS.md, not silently
dropped): VAD/PTT input gate + device enumeration; real WASAPI loopback
capture for SCREEN_AUDIO (synthetic injection only); true stereo playback
output (AudioEngine's mixer/output device stays mono -- Opus itself is fully
stereo-correct on the wire).
ctest --test-dir build/m1-dev: 11/11 green, verified across 3 consecutive
full-suite runs plus 8 standalone runs of the new test.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 14:12:37 +02:00
|
|
|
|
} vc_audio_config;
|
|
|
|
|
|
|
M5: moderation, permissions, channel CRUD, in-app account management
- Server-side moderation & permissions (kick/ban/move/server-mute, channel CRUD).
- Database schema v2: channels, bans; BLAKE2b channel passwords, Argon2id accounts.
- C ABI additions and client-side handling (vc_kick_user, vc_ban_user, vc_set_permission, vc_set_server_mute, vc_move_user, vc_create/edit/delete_channel, vc_create/reset/delete/list_account).
- vccli flags for all M5 operations plus --username/--password auth.
- Four new tests covering permissions, kick/ban/move/mute, admin accounts, channel CRUD.
- Docs: protocol.md envelope updates, security.md channel-password hashing, PROGRESS.md.
2026-06-17 15:08:05 +02:00
|
|
|
|
/* M5: permission bitset (mirrors protocol Permissions). */
|
|
|
|
|
|
typedef struct vc_permissions {
|
|
|
|
|
|
int can_create_temp_channel; /* bool */
|
|
|
|
|
|
int can_kick; /* bool */
|
|
|
|
|
|
int can_ban; /* bool */
|
|
|
|
|
|
int can_move_users; /* bool */
|
|
|
|
|
|
int can_admin_accounts; /* bool */
|
|
|
|
|
|
int is_admin; /* bool */
|
|
|
|
|
|
} vc_permissions;
|
|
|
|
|
|
|
2026-06-17 16:31:29 +02:00
|
|
|
|
/* M5: account entry (reply to vc_list_accounts / vc_get_account_list). */
|
|
|
|
|
|
typedef struct vc_account {
|
|
|
|
|
|
const char* username;
|
|
|
|
|
|
int is_admin; /* bool */
|
|
|
|
|
|
uint64_t created_at_unix_ms;
|
|
|
|
|
|
uint64_t last_login_unix_ms;
|
|
|
|
|
|
} vc_account;
|
|
|
|
|
|
|
|
|
|
|
|
typedef struct vc_account_list {
|
|
|
|
|
|
vc_account* items;
|
|
|
|
|
|
size_t count;
|
|
|
|
|
|
} vc_account_list;
|
|
|
|
|
|
|
M5: moderation, permissions, channel CRUD, in-app account management
- Server-side moderation & permissions (kick/ban/move/server-mute, channel CRUD).
- Database schema v2: channels, bans; BLAKE2b channel passwords, Argon2id accounts.
- C ABI additions and client-side handling (vc_kick_user, vc_ban_user, vc_set_permission, vc_set_server_mute, vc_move_user, vc_create/edit/delete_channel, vc_create/reset/delete/list_account).
- vccli flags for all M5 operations plus --username/--password auth.
- Four new tests covering permissions, kick/ban/move/mute, admin accounts, channel CRUD.
- Docs: protocol.md envelope updates, security.md channel-password hashing, PROGRESS.md.
2026-06-17 15:08:05 +02:00
|
|
|
|
/* M5: channel creation/edition descriptor. */
|
|
|
|
|
|
typedef struct vc_channel_info {
|
|
|
|
|
|
uint32_t id; /* 0 = new channel for create */
|
|
|
|
|
|
uint32_t parent_id; /* 0 = root */
|
|
|
|
|
|
const char* name;
|
|
|
|
|
|
const char* topic;
|
|
|
|
|
|
int password_protected; /* bool */
|
|
|
|
|
|
const char* password; /* nullable; ignored if password_protected == 0 */
|
|
|
|
|
|
uint32_t max_users; /* 0 = unlimited */
|
|
|
|
|
|
uint32_t sort_order;
|
|
|
|
|
|
/* Audio config — 0/NULL fields use server defaults. */
|
|
|
|
|
|
vc_audio_config audio;
|
|
|
|
|
|
} vc_channel_info;
|
|
|
|
|
|
|
scaffold: M0 skeleton + agent onboarding (build, architecture, progress)
Turn the design into a buildable, dependency-free M0 skeleton plus the
onboarding layer so a new agent can pick up instantly.
Build system:
- CMake + CMakePresets (dev = no deps; server-release = vcpkg) + vcpkg.json
- Skeleton builds with just a C++20 compiler; deps stay off until needed
- .gitattributes (LF), .gitignore, .clang-format
Core (libvoicecat):
- core/include/voicecat.h: full C ABI (the client/server contract), stubbed
- core/proto/voicecat.proto: control-plane wire format, matches docs/protocol.md
- src/{net,crypto,codec,protocol,session,audio,core}: subsystem stubs that
return VC_ERR_NOT_IMPLEMENTED, each pointing to its design doc
- server/ (voicecat-server) and tools/vccli/ link the core
- tests/: CTest smoke test asserting the C ABI contract (behavior, not just build)
- clients/{apple,windows}: M4 placeholders
Onboarding for agents:
- CLAUDE.md: hub — build/test commands, architecture at a glance, doc map, rules
- AGENTS.md: working method (behavior-driven; clean compile is the floor not the goal)
- PROGRESS.md: living tracker — M0 done, M1 task checklist, "where we left off"
Verified: cmake --preset dev && cmake --build --preset dev && ctest --preset dev → green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 21:09:09 +02:00
|
|
|
|
typedef struct vc_device {
|
|
|
|
|
|
const char* id;
|
|
|
|
|
|
const char* name;
|
|
|
|
|
|
int is_default; /* bool */
|
|
|
|
|
|
} vc_device;
|
|
|
|
|
|
|
|
|
|
|
|
typedef struct vc_device_list {
|
|
|
|
|
|
vc_device* items;
|
|
|
|
|
|
size_t count;
|
|
|
|
|
|
} vc_device_list;
|
|
|
|
|
|
|
feat(M4): Windows WinForms client, TOFU identity pinning, VAD threshold + always-on mode
Core ABI extensions (voicecat.h):
- vc_list_channels / vc_list_users / vc_list_user_streams — pull-based snapshot getters
for the channel-tree and user-list UI; session_model_mu_ guards cross-thread reads
- VC_EVENT_JOIN_RESULT / vc_join_channel — channel join with optional password
- VC_EVENT_SERVER_IDENTITY + vc_confirm_server_identity — TOFU gate that blocks io_thread_
until the UI approves or rejects; pins TLS leaf-cert SHA-256 (not declared Ed25519)
- vc_get_server_identity_display — Ed25519 fingerprint for human-readable display only
- VC_INPUT_ALWAYS_ON = 2 in vc_input_mode — transmit unconditionally, no VAD gate
- vc_set_vad_threshold — live RMS threshold update (0.0–1.0); EnergyVadProcessor stores
it atomically so the audio RT path reads without a lock
C++ implementation:
- SessionModel::apply_snapshot / apply_channel_event fixed to populate parent_id,
password_protected, and max_users (were permanently zeroed)
- TlsContext::peer_cert_fingerprint — SHA-256 of peer leaf cert DER via mbedTLS
- TofuStore split into peek (read-only) + pin (write) so first-connect only persists
after user approval; tofu_store_path in vc_config for per-user pin file location
- TcpAcceptor uses dual-stack IPv6+IPv4 fallback (fixes localhost → ::1 on Windows)
- windows-client CMake preset: Release shared DLL, static MinGW runtime, no tools/tests
- New C++ tests: test_channel_user_list_abi, test_tofu_flow (14/14 green)
Windows client (clients/windows/ — .NET 10 WinForms):
- VoiceCat.Interop: LibraryImport P/Invoke surface, UnmanagedCallersOnly callbacks,
Channel<VoiceCatEvent> event delivery drained by 30ms WinForms Timer
- VoiceCat.App: ConnectDialog (saved servers, DPAPI password storage), ServerIdentity-
Dialog (TOFU first-connect / mismatch warning), MainForm (channel TreeView, user
ListBox, RichTextBox chat, voice controls, device pickers, VAD/PTT/always-on mode,
per-user gain/mute/NR tuning, VAD sensitivity TrackBar, level meter ProgressBar)
- PttKeyCaptureDialog — focus-scoped PTT key capture (documented limitation)
- PerUserTuningDialog — real-time gain/mute/NR applied to all of a user's streams
- Accessibility: explicit AccessibleName/Description on every control, & mnemonics,
Activity log ListBox as durable screen-reader record, AutomationNotification for
curated live announcements
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 00:35:16 +02:00
|
|
|
|
/* ── Channel / user / stream snapshots (M4 — 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
|
|
|
|
|
|
* vc_free_*, items' const char* fields are invalid after that call. */
|
|
|
|
|
|
typedef struct vc_channel {
|
|
|
|
|
|
uint32_t id;
|
|
|
|
|
|
uint32_t parent_id; /* 0 = root */
|
|
|
|
|
|
const char* name;
|
2026-06-17 16:31:29 +02:00
|
|
|
|
const char* topic;
|
feat(M4): Windows WinForms client, TOFU identity pinning, VAD threshold + always-on mode
Core ABI extensions (voicecat.h):
- vc_list_channels / vc_list_users / vc_list_user_streams — pull-based snapshot getters
for the channel-tree and user-list UI; session_model_mu_ guards cross-thread reads
- VC_EVENT_JOIN_RESULT / vc_join_channel — channel join with optional password
- VC_EVENT_SERVER_IDENTITY + vc_confirm_server_identity — TOFU gate that blocks io_thread_
until the UI approves or rejects; pins TLS leaf-cert SHA-256 (not declared Ed25519)
- vc_get_server_identity_display — Ed25519 fingerprint for human-readable display only
- VC_INPUT_ALWAYS_ON = 2 in vc_input_mode — transmit unconditionally, no VAD gate
- vc_set_vad_threshold — live RMS threshold update (0.0–1.0); EnergyVadProcessor stores
it atomically so the audio RT path reads without a lock
C++ implementation:
- SessionModel::apply_snapshot / apply_channel_event fixed to populate parent_id,
password_protected, and max_users (were permanently zeroed)
- TlsContext::peer_cert_fingerprint — SHA-256 of peer leaf cert DER via mbedTLS
- TofuStore split into peek (read-only) + pin (write) so first-connect only persists
after user approval; tofu_store_path in vc_config for per-user pin file location
- TcpAcceptor uses dual-stack IPv6+IPv4 fallback (fixes localhost → ::1 on Windows)
- windows-client CMake preset: Release shared DLL, static MinGW runtime, no tools/tests
- New C++ tests: test_channel_user_list_abi, test_tofu_flow (14/14 green)
Windows client (clients/windows/ — .NET 10 WinForms):
- VoiceCat.Interop: LibraryImport P/Invoke surface, UnmanagedCallersOnly callbacks,
Channel<VoiceCatEvent> event delivery drained by 30ms WinForms Timer
- VoiceCat.App: ConnectDialog (saved servers, DPAPI password storage), ServerIdentity-
Dialog (TOFU first-connect / mismatch warning), MainForm (channel TreeView, user
ListBox, RichTextBox chat, voice controls, device pickers, VAD/PTT/always-on mode,
per-user gain/mute/NR tuning, VAD sensitivity TrackBar, level meter ProgressBar)
- PttKeyCaptureDialog — focus-scoped PTT key capture (documented limitation)
- PerUserTuningDialog — real-time gain/mute/NR applied to all of a user's streams
- Accessibility: explicit AccessibleName/Description on every control, & mnemonics,
Activity log ListBox as durable screen-reader record, AutomationNotification for
curated live announcements
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 00:35:16 +02:00
|
|
|
|
int password_protected; /* bool */
|
|
|
|
|
|
uint32_t max_users; /* 0 = unlimited */
|
feat: fix voice join/leave, channel edit defaults, channel-update stream restart
Three bugs fixed across the full stack (proto/server/core/ABI/Win/macOS/iOS):
1. Join/Leave Voice now truly subscribes/unsubscribes from the voice plane.
Previously the button only toggled the local mic — receiving was always on
(gated by channel membership alone). Added a protocol-level voice subscription
concept: new SubscribeVoiceRequest/UnsubscribeVoiceRequest/VoiceSubscriptionResult
proto messages, User.voice_subscribed field, vc_join_voice/vc_leave_voice C ABI
functions, VC_EVENT_VOICE_STATE event, server-side voice_subscribed flag checked
by the SFU relay recipient filter, and core-client gating of remote-stream
decoder setup. All three clients rewired to subscribe+mic on Join / unsubscribe
on Leave. Text chat works regardless of voice subscription.
2. Channel edit dialog now shows the channel's actual current settings. The read
struct vc_channel was missing sort_order and audio fields — only the write
struct vc_channel_info had them. Extended vc_channel with both (additive, no
ABI break), updated the session model and list_channels marshaling to populate
them, and updated all three clients' edit callers to use actual channel info
instead of hardcoded defaults.
3. Channel parameter updates now automatically restart everyone's streams.
Previously editing a channel's audio config persisted and broadcast a
ChannelEvent::UPDATED, but no layer restarted streams — encoders/decoders are
frozen at announce time. handle_channel_event now detects audio-config changes
on the user's current channel and stop->starts each active local stream. The
server reads the updated config on re-announce; peers wire up fresh decoders
at the new ssrc.
All 29 CTest tests pass; Windows DLL + C# client build clean. Apple clients not
yet compile-verified (Windows environment).
2026-06-24 14:29:39 +02:00
|
|
|
|
uint32_t sort_order; /* channel sort order */
|
|
|
|
|
|
/* Authoritative channel Opus params (docs/voice.md §3). Populated from the Channel proto
|
|
|
|
|
|
* so the edit dialog can read back the current config without a separate round-trip. */
|
|
|
|
|
|
vc_audio_config audio;
|
feat(M4): Windows WinForms client, TOFU identity pinning, VAD threshold + always-on mode
Core ABI extensions (voicecat.h):
- vc_list_channels / vc_list_users / vc_list_user_streams — pull-based snapshot getters
for the channel-tree and user-list UI; session_model_mu_ guards cross-thread reads
- VC_EVENT_JOIN_RESULT / vc_join_channel — channel join with optional password
- VC_EVENT_SERVER_IDENTITY + vc_confirm_server_identity — TOFU gate that blocks io_thread_
until the UI approves or rejects; pins TLS leaf-cert SHA-256 (not declared Ed25519)
- vc_get_server_identity_display — Ed25519 fingerprint for human-readable display only
- VC_INPUT_ALWAYS_ON = 2 in vc_input_mode — transmit unconditionally, no VAD gate
- vc_set_vad_threshold — live RMS threshold update (0.0–1.0); EnergyVadProcessor stores
it atomically so the audio RT path reads without a lock
C++ implementation:
- SessionModel::apply_snapshot / apply_channel_event fixed to populate parent_id,
password_protected, and max_users (were permanently zeroed)
- TlsContext::peer_cert_fingerprint — SHA-256 of peer leaf cert DER via mbedTLS
- TofuStore split into peek (read-only) + pin (write) so first-connect only persists
after user approval; tofu_store_path in vc_config for per-user pin file location
- TcpAcceptor uses dual-stack IPv6+IPv4 fallback (fixes localhost → ::1 on Windows)
- windows-client CMake preset: Release shared DLL, static MinGW runtime, no tools/tests
- New C++ tests: test_channel_user_list_abi, test_tofu_flow (14/14 green)
Windows client (clients/windows/ — .NET 10 WinForms):
- VoiceCat.Interop: LibraryImport P/Invoke surface, UnmanagedCallersOnly callbacks,
Channel<VoiceCatEvent> event delivery drained by 30ms WinForms Timer
- VoiceCat.App: ConnectDialog (saved servers, DPAPI password storage), ServerIdentity-
Dialog (TOFU first-connect / mismatch warning), MainForm (channel TreeView, user
ListBox, RichTextBox chat, voice controls, device pickers, VAD/PTT/always-on mode,
per-user gain/mute/NR tuning, VAD sensitivity TrackBar, level meter ProgressBar)
- PttKeyCaptureDialog — focus-scoped PTT key capture (documented limitation)
- PerUserTuningDialog — real-time gain/mute/NR applied to all of a user's streams
- Accessibility: explicit AccessibleName/Description on every control, & mnemonics,
Activity log ListBox as durable screen-reader record, AutomationNotification for
curated live announcements
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 00:35:16 +02:00
|
|
|
|
} vc_channel;
|
|
|
|
|
|
|
|
|
|
|
|
typedef struct vc_channel_list {
|
|
|
|
|
|
vc_channel* items;
|
|
|
|
|
|
size_t count;
|
|
|
|
|
|
} vc_channel_list;
|
|
|
|
|
|
|
|
|
|
|
|
typedef struct vc_user {
|
|
|
|
|
|
uint32_t id;
|
|
|
|
|
|
const char* nickname;
|
|
|
|
|
|
int is_guest; /* bool */
|
|
|
|
|
|
uint32_t channel_id;
|
2026-06-17 16:31:29 +02:00
|
|
|
|
int self_mic_muted; /* bool */
|
|
|
|
|
|
int self_deafened; /* bool */
|
|
|
|
|
|
int server_muted; /* bool */
|
|
|
|
|
|
int server_deafened; /* bool */
|
feat: fix voice join/leave, channel edit defaults, channel-update stream restart
Three bugs fixed across the full stack (proto/server/core/ABI/Win/macOS/iOS):
1. Join/Leave Voice now truly subscribes/unsubscribes from the voice plane.
Previously the button only toggled the local mic — receiving was always on
(gated by channel membership alone). Added a protocol-level voice subscription
concept: new SubscribeVoiceRequest/UnsubscribeVoiceRequest/VoiceSubscriptionResult
proto messages, User.voice_subscribed field, vc_join_voice/vc_leave_voice C ABI
functions, VC_EVENT_VOICE_STATE event, server-side voice_subscribed flag checked
by the SFU relay recipient filter, and core-client gating of remote-stream
decoder setup. All three clients rewired to subscribe+mic on Join / unsubscribe
on Leave. Text chat works regardless of voice subscription.
2. Channel edit dialog now shows the channel's actual current settings. The read
struct vc_channel was missing sort_order and audio fields — only the write
struct vc_channel_info had them. Extended vc_channel with both (additive, no
ABI break), updated the session model and list_channels marshaling to populate
them, and updated all three clients' edit callers to use actual channel info
instead of hardcoded defaults.
3. Channel parameter updates now automatically restart everyone's streams.
Previously editing a channel's audio config persisted and broadcast a
ChannelEvent::UPDATED, but no layer restarted streams — encoders/decoders are
frozen at announce time. handle_channel_event now detects audio-config changes
on the user's current channel and stop->starts each active local stream. The
server reads the updated config on re-announce; peers wire up fresh decoders
at the new ssrc.
All 29 CTest tests pass; Windows DLL + C# client build clean. Apple clients not
yet compile-verified (Windows environment).
2026-06-24 14:29:39 +02:00
|
|
|
|
int voice_subscribed; /* bool — true when on the voice plane */
|
feat(M4): Windows WinForms client, TOFU identity pinning, VAD threshold + always-on mode
Core ABI extensions (voicecat.h):
- vc_list_channels / vc_list_users / vc_list_user_streams — pull-based snapshot getters
for the channel-tree and user-list UI; session_model_mu_ guards cross-thread reads
- VC_EVENT_JOIN_RESULT / vc_join_channel — channel join with optional password
- VC_EVENT_SERVER_IDENTITY + vc_confirm_server_identity — TOFU gate that blocks io_thread_
until the UI approves or rejects; pins TLS leaf-cert SHA-256 (not declared Ed25519)
- vc_get_server_identity_display — Ed25519 fingerprint for human-readable display only
- VC_INPUT_ALWAYS_ON = 2 in vc_input_mode — transmit unconditionally, no VAD gate
- vc_set_vad_threshold — live RMS threshold update (0.0–1.0); EnergyVadProcessor stores
it atomically so the audio RT path reads without a lock
C++ implementation:
- SessionModel::apply_snapshot / apply_channel_event fixed to populate parent_id,
password_protected, and max_users (were permanently zeroed)
- TlsContext::peer_cert_fingerprint — SHA-256 of peer leaf cert DER via mbedTLS
- TofuStore split into peek (read-only) + pin (write) so first-connect only persists
after user approval; tofu_store_path in vc_config for per-user pin file location
- TcpAcceptor uses dual-stack IPv6+IPv4 fallback (fixes localhost → ::1 on Windows)
- windows-client CMake preset: Release shared DLL, static MinGW runtime, no tools/tests
- New C++ tests: test_channel_user_list_abi, test_tofu_flow (14/14 green)
Windows client (clients/windows/ — .NET 10 WinForms):
- VoiceCat.Interop: LibraryImport P/Invoke surface, UnmanagedCallersOnly callbacks,
Channel<VoiceCatEvent> event delivery drained by 30ms WinForms Timer
- VoiceCat.App: ConnectDialog (saved servers, DPAPI password storage), ServerIdentity-
Dialog (TOFU first-connect / mismatch warning), MainForm (channel TreeView, user
ListBox, RichTextBox chat, voice controls, device pickers, VAD/PTT/always-on mode,
per-user gain/mute/NR tuning, VAD sensitivity TrackBar, level meter ProgressBar)
- PttKeyCaptureDialog — focus-scoped PTT key capture (documented limitation)
- PerUserTuningDialog — real-time gain/mute/NR applied to all of a user's streams
- Accessibility: explicit AccessibleName/Description on every control, & mnemonics,
Activity log ListBox as durable screen-reader record, AutomationNotification for
curated live announcements
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 00:35:16 +02:00
|
|
|
|
} vc_user;
|
|
|
|
|
|
|
|
|
|
|
|
typedef struct vc_user_list {
|
|
|
|
|
|
vc_user* items;
|
|
|
|
|
|
size_t count;
|
|
|
|
|
|
} vc_user_list;
|
|
|
|
|
|
|
|
|
|
|
|
/* Per-user stream summary — lighter than vc_audio_config; for the full effective Opus config
|
|
|
|
|
|
* of a specific (user_id, stream_id), use the existing vc_get_stream_audio_config. */
|
|
|
|
|
|
typedef struct vc_stream_summary {
|
|
|
|
|
|
uint32_t stream_id;
|
|
|
|
|
|
vc_stream_kind kind;
|
|
|
|
|
|
const char* label;
|
|
|
|
|
|
} vc_stream_summary;
|
|
|
|
|
|
|
|
|
|
|
|
typedef struct vc_stream_summary_list {
|
|
|
|
|
|
vc_stream_summary* items;
|
|
|
|
|
|
size_t count;
|
|
|
|
|
|
} vc_stream_summary_list;
|
|
|
|
|
|
|
2026-06-18 02:06:44 +02:00
|
|
|
|
/* Receive-side state the local listener has chosen for a specific remote stream — the
|
|
|
|
|
|
* counterpart to vc_set_remote_stream, so a UI can reopen its per-mix controls at the
|
|
|
|
|
|
* listener's actual current settings. All LOCAL (no protocol traffic) — docs/voice.md §10.
|
|
|
|
|
|
* If (user_id, stream_id) is known but the listener has never called vc_set_remote_stream on
|
|
|
|
|
|
* it, the defaults are gain=1.0, muted=0, noise_reduction=0 (matching a fresh RemoteStream). */
|
|
|
|
|
|
typedef struct vc_remote_stream_state {
|
|
|
|
|
|
float gain; /* 0.0–… ; default 1.0 */
|
|
|
|
|
|
int muted; /* bool */
|
|
|
|
|
|
int noise_reduction; /* bool */
|
|
|
|
|
|
} vc_remote_stream_state;
|
|
|
|
|
|
|
scaffold: M0 skeleton + agent onboarding (build, architecture, progress)
Turn the design into a buildable, dependency-free M0 skeleton plus the
onboarding layer so a new agent can pick up instantly.
Build system:
- CMake + CMakePresets (dev = no deps; server-release = vcpkg) + vcpkg.json
- Skeleton builds with just a C++20 compiler; deps stay off until needed
- .gitattributes (LF), .gitignore, .clang-format
Core (libvoicecat):
- core/include/voicecat.h: full C ABI (the client/server contract), stubbed
- core/proto/voicecat.proto: control-plane wire format, matches docs/protocol.md
- src/{net,crypto,codec,protocol,session,audio,core}: subsystem stubs that
return VC_ERR_NOT_IMPLEMENTED, each pointing to its design doc
- server/ (voicecat-server) and tools/vccli/ link the core
- tests/: CTest smoke test asserting the C ABI contract (behavior, not just build)
- clients/{apple,windows}: M4 placeholders
Onboarding for agents:
- CLAUDE.md: hub — build/test commands, architecture at a glance, doc map, rules
- AGENTS.md: working method (behavior-driven; clean compile is the floor not the goal)
- PROGRESS.md: living tracker — M0 done, M1 task checklist, "where we left off"
Verified: cmake --preset dev && cmake --build --preset dev && ctest --preset dev → green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 21:09:09 +02:00
|
|
|
|
/* Opaque client handle. */
|
|
|
|
|
|
typedef struct vc_client vc_client;
|
|
|
|
|
|
|
|
|
|
|
|
/* ── Lifecycle ────────────────────────────────────────────────────────────── */
|
|
|
|
|
|
VC_API const char* vc_version_string(void);
|
|
|
|
|
|
VC_API const char* vc_result_string(vc_result code);
|
|
|
|
|
|
|
|
|
|
|
|
VC_API vc_client* vc_client_create(const vc_config* cfg, vc_callbacks cb);
|
|
|
|
|
|
VC_API void vc_client_destroy(vc_client* c);
|
|
|
|
|
|
|
|
|
|
|
|
/* ── Connection & auth (async; results via on_event) ──────────────────────── */
|
|
|
|
|
|
VC_API vc_result vc_connect(vc_client* c, const char* host, uint16_t port);
|
|
|
|
|
|
VC_API vc_result vc_disconnect(vc_client* c);
|
|
|
|
|
|
VC_API vc_result vc_authenticate_guest(vc_client* c, const char* nickname);
|
|
|
|
|
|
VC_API vc_result vc_authenticate_user(vc_client* c, const char* username,
|
|
|
|
|
|
const char* password);
|
|
|
|
|
|
|
|
|
|
|
|
/* ── Channels ─────────────────────────────────────────────────────────────── */
|
feat(M4): Windows WinForms client, TOFU identity pinning, VAD threshold + always-on mode
Core ABI extensions (voicecat.h):
- vc_list_channels / vc_list_users / vc_list_user_streams — pull-based snapshot getters
for the channel-tree and user-list UI; session_model_mu_ guards cross-thread reads
- VC_EVENT_JOIN_RESULT / vc_join_channel — channel join with optional password
- VC_EVENT_SERVER_IDENTITY + vc_confirm_server_identity — TOFU gate that blocks io_thread_
until the UI approves or rejects; pins TLS leaf-cert SHA-256 (not declared Ed25519)
- vc_get_server_identity_display — Ed25519 fingerprint for human-readable display only
- VC_INPUT_ALWAYS_ON = 2 in vc_input_mode — transmit unconditionally, no VAD gate
- vc_set_vad_threshold — live RMS threshold update (0.0–1.0); EnergyVadProcessor stores
it atomically so the audio RT path reads without a lock
C++ implementation:
- SessionModel::apply_snapshot / apply_channel_event fixed to populate parent_id,
password_protected, and max_users (were permanently zeroed)
- TlsContext::peer_cert_fingerprint — SHA-256 of peer leaf cert DER via mbedTLS
- TofuStore split into peek (read-only) + pin (write) so first-connect only persists
after user approval; tofu_store_path in vc_config for per-user pin file location
- TcpAcceptor uses dual-stack IPv6+IPv4 fallback (fixes localhost → ::1 on Windows)
- windows-client CMake preset: Release shared DLL, static MinGW runtime, no tools/tests
- New C++ tests: test_channel_user_list_abi, test_tofu_flow (14/14 green)
Windows client (clients/windows/ — .NET 10 WinForms):
- VoiceCat.Interop: LibraryImport P/Invoke surface, UnmanagedCallersOnly callbacks,
Channel<VoiceCatEvent> event delivery drained by 30ms WinForms Timer
- VoiceCat.App: ConnectDialog (saved servers, DPAPI password storage), ServerIdentity-
Dialog (TOFU first-connect / mismatch warning), MainForm (channel TreeView, user
ListBox, RichTextBox chat, voice controls, device pickers, VAD/PTT/always-on mode,
per-user gain/mute/NR tuning, VAD sensitivity TrackBar, level meter ProgressBar)
- PttKeyCaptureDialog — focus-scoped PTT key capture (documented limitation)
- PerUserTuningDialog — real-time gain/mute/NR applied to all of a user's streams
- Accessibility: explicit AccessibleName/Description on every control, & mnemonics,
Activity log ListBox as durable screen-reader record, AutomationNotification for
curated live announcements
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 00:35:16 +02:00
|
|
|
|
/* 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. */
|
scaffold: M0 skeleton + agent onboarding (build, architecture, progress)
Turn the design into a buildable, dependency-free M0 skeleton plus the
onboarding layer so a new agent can pick up instantly.
Build system:
- CMake + CMakePresets (dev = no deps; server-release = vcpkg) + vcpkg.json
- Skeleton builds with just a C++20 compiler; deps stay off until needed
- .gitattributes (LF), .gitignore, .clang-format
Core (libvoicecat):
- core/include/voicecat.h: full C ABI (the client/server contract), stubbed
- core/proto/voicecat.proto: control-plane wire format, matches docs/protocol.md
- src/{net,crypto,codec,protocol,session,audio,core}: subsystem stubs that
return VC_ERR_NOT_IMPLEMENTED, each pointing to its design doc
- server/ (voicecat-server) and tools/vccli/ link the core
- tests/: CTest smoke test asserting the C ABI contract (behavior, not just build)
- clients/{apple,windows}: M4 placeholders
Onboarding for agents:
- CLAUDE.md: hub — build/test commands, architecture at a glance, doc map, rules
- AGENTS.md: working method (behavior-driven; clean compile is the floor not the goal)
- PROGRESS.md: living tracker — M0 done, M1 task checklist, "where we left off"
Verified: cmake --preset dev && cmake --build --preset dev && ctest --preset dev → green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 21:09:09 +02:00
|
|
|
|
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);
|
|
|
|
|
|
|
feat: fix voice join/leave, channel edit defaults, channel-update stream restart
Three bugs fixed across the full stack (proto/server/core/ABI/Win/macOS/iOS):
1. Join/Leave Voice now truly subscribes/unsubscribes from the voice plane.
Previously the button only toggled the local mic — receiving was always on
(gated by channel membership alone). Added a protocol-level voice subscription
concept: new SubscribeVoiceRequest/UnsubscribeVoiceRequest/VoiceSubscriptionResult
proto messages, User.voice_subscribed field, vc_join_voice/vc_leave_voice C ABI
functions, VC_EVENT_VOICE_STATE event, server-side voice_subscribed flag checked
by the SFU relay recipient filter, and core-client gating of remote-stream
decoder setup. All three clients rewired to subscribe+mic on Join / unsubscribe
on Leave. Text chat works regardless of voice subscription.
2. Channel edit dialog now shows the channel's actual current settings. The read
struct vc_channel was missing sort_order and audio fields — only the write
struct vc_channel_info had them. Extended vc_channel with both (additive, no
ABI break), updated the session model and list_channels marshaling to populate
them, and updated all three clients' edit callers to use actual channel info
instead of hardcoded defaults.
3. Channel parameter updates now automatically restart everyone's streams.
Previously editing a channel's audio config persisted and broadcast a
ChannelEvent::UPDATED, but no layer restarted streams — encoders/decoders are
frozen at announce time. handle_channel_event now detects audio-config changes
on the user's current channel and stop->starts each active local stream. The
server reads the updated config on re-announce; peers wire up fresh decoders
at the new ssrc.
All 29 CTest tests pass; Windows DLL + C# client build clean. Apple clients not
yet compile-verified (Windows environment).
2026-06-24 14:29:39 +02:00
|
|
|
|
/* ── Voice-plane subscription ─────────────────────────────────────────────── */
|
|
|
|
|
|
/* Joining voice subscribes to the voice plane: the server starts relaying voice frames
|
|
|
|
|
|
* to you, and the core wires up remote-stream decoders so you hear other users. Leaving
|
|
|
|
|
|
* voice unsubscribes: the server stops relaying voice to you, the core tears down all
|
|
|
|
|
|
* remote decoders (playback stops), and any active local mic/screen/aux streams are
|
|
|
|
|
|
* stopped. Text chat is unaffected either way. Result arrives as VC_EVENT_VOICE_STATE
|
|
|
|
|
|
* (u32a = 1 for subscribed, 0 for unsubscribed). */
|
|
|
|
|
|
VC_API vc_result vc_join_voice(vc_client* c);
|
|
|
|
|
|
VC_API vc_result vc_leave_voice(vc_client* c);
|
|
|
|
|
|
|
scaffold: M0 skeleton + agent onboarding (build, architecture, progress)
Turn the design into a buildable, dependency-free M0 skeleton plus the
onboarding layer so a new agent can pick up instantly.
Build system:
- CMake + CMakePresets (dev = no deps; server-release = vcpkg) + vcpkg.json
- Skeleton builds with just a C++20 compiler; deps stay off until needed
- .gitattributes (LF), .gitignore, .clang-format
Core (libvoicecat):
- core/include/voicecat.h: full C ABI (the client/server contract), stubbed
- core/proto/voicecat.proto: control-plane wire format, matches docs/protocol.md
- src/{net,crypto,codec,protocol,session,audio,core}: subsystem stubs that
return VC_ERR_NOT_IMPLEMENTED, each pointing to its design doc
- server/ (voicecat-server) and tools/vccli/ link the core
- tests/: CTest smoke test asserting the C ABI contract (behavior, not just build)
- clients/{apple,windows}: M4 placeholders
Onboarding for agents:
- CLAUDE.md: hub — build/test commands, architecture at a glance, doc map, rules
- AGENTS.md: working method (behavior-driven; clean compile is the floor not the goal)
- PROGRESS.md: living tracker — M0 done, M1 task checklist, "where we left off"
Verified: cmake --preset dev && cmake --build --preset dev && ctest --preset dev → green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 21:09:09 +02:00
|
|
|
|
/* ── Local media streams (mic / screen audio / aux) ───────────────────────── */
|
|
|
|
|
|
VC_API vc_result vc_stream_start(vc_client* c, const vc_stream_desc* desc,
|
|
|
|
|
|
uint32_t* out_stream_id);
|
|
|
|
|
|
VC_API vc_result vc_stream_stop(vc_client* c, uint32_t stream_id);
|
|
|
|
|
|
VC_API vc_result vc_set_input_device(vc_client* c, uint32_t stream_id,
|
|
|
|
|
|
const char* device_id);
|
|
|
|
|
|
|
|
|
|
|
|
/* Send-side: input gate mode + PTT key state, and self mute/deafen. */
|
|
|
|
|
|
VC_API vc_result vc_set_input_mode(vc_client* c, vc_input_mode mode);
|
feat(M4): Windows WinForms client, TOFU identity pinning, VAD threshold + always-on mode
Core ABI extensions (voicecat.h):
- vc_list_channels / vc_list_users / vc_list_user_streams — pull-based snapshot getters
for the channel-tree and user-list UI; session_model_mu_ guards cross-thread reads
- VC_EVENT_JOIN_RESULT / vc_join_channel — channel join with optional password
- VC_EVENT_SERVER_IDENTITY + vc_confirm_server_identity — TOFU gate that blocks io_thread_
until the UI approves or rejects; pins TLS leaf-cert SHA-256 (not declared Ed25519)
- vc_get_server_identity_display — Ed25519 fingerprint for human-readable display only
- VC_INPUT_ALWAYS_ON = 2 in vc_input_mode — transmit unconditionally, no VAD gate
- vc_set_vad_threshold — live RMS threshold update (0.0–1.0); EnergyVadProcessor stores
it atomically so the audio RT path reads without a lock
C++ implementation:
- SessionModel::apply_snapshot / apply_channel_event fixed to populate parent_id,
password_protected, and max_users (were permanently zeroed)
- TlsContext::peer_cert_fingerprint — SHA-256 of peer leaf cert DER via mbedTLS
- TofuStore split into peek (read-only) + pin (write) so first-connect only persists
after user approval; tofu_store_path in vc_config for per-user pin file location
- TcpAcceptor uses dual-stack IPv6+IPv4 fallback (fixes localhost → ::1 on Windows)
- windows-client CMake preset: Release shared DLL, static MinGW runtime, no tools/tests
- New C++ tests: test_channel_user_list_abi, test_tofu_flow (14/14 green)
Windows client (clients/windows/ — .NET 10 WinForms):
- VoiceCat.Interop: LibraryImport P/Invoke surface, UnmanagedCallersOnly callbacks,
Channel<VoiceCatEvent> event delivery drained by 30ms WinForms Timer
- VoiceCat.App: ConnectDialog (saved servers, DPAPI password storage), ServerIdentity-
Dialog (TOFU first-connect / mismatch warning), MainForm (channel TreeView, user
ListBox, RichTextBox chat, voice controls, device pickers, VAD/PTT/always-on mode,
per-user gain/mute/NR tuning, VAD sensitivity TrackBar, level meter ProgressBar)
- PttKeyCaptureDialog — focus-scoped PTT key capture (documented limitation)
- PerUserTuningDialog — real-time gain/mute/NR applied to all of a user's streams
- Accessibility: explicit AccessibleName/Description on every control, & mnemonics,
Activity log ListBox as durable screen-reader record, AutomationNotification for
curated live announcements
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 00:35:16 +02:00
|
|
|
|
/* VAD threshold: normalized RMS 0.0–1.0; default ~0.025. Takes effect immediately —
|
|
|
|
|
|
* recreates the VAD gate if a MIC stream is already active. No-op when mode != VOICE_ACTIVATION
|
|
|
|
|
|
* (value is remembered and applied if the mode switches back). */
|
|
|
|
|
|
VC_API vc_result vc_set_vad_threshold(vc_client* c, float threshold);
|
scaffold: M0 skeleton + agent onboarding (build, architecture, progress)
Turn the design into a buildable, dependency-free M0 skeleton plus the
onboarding layer so a new agent can pick up instantly.
Build system:
- CMake + CMakePresets (dev = no deps; server-release = vcpkg) + vcpkg.json
- Skeleton builds with just a C++20 compiler; deps stay off until needed
- .gitattributes (LF), .gitignore, .clang-format
Core (libvoicecat):
- core/include/voicecat.h: full C ABI (the client/server contract), stubbed
- core/proto/voicecat.proto: control-plane wire format, matches docs/protocol.md
- src/{net,crypto,codec,protocol,session,audio,core}: subsystem stubs that
return VC_ERR_NOT_IMPLEMENTED, each pointing to its design doc
- server/ (voicecat-server) and tools/vccli/ link the core
- tests/: CTest smoke test asserting the C ABI contract (behavior, not just build)
- clients/{apple,windows}: M4 placeholders
Onboarding for agents:
- CLAUDE.md: hub — build/test commands, architecture at a glance, doc map, rules
- AGENTS.md: working method (behavior-driven; clean compile is the floor not the goal)
- PROGRESS.md: living tracker — M0 done, M1 task checklist, "where we left off"
Verified: cmake --preset dev && cmake --build --preset dev && ctest --preset dev → green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 21:09:09 +02:00
|
|
|
|
VC_API vc_result vc_set_push_to_talk(vc_client* c, int active /* bool */);
|
|
|
|
|
|
VC_API vc_result vc_set_self_mute(vc_client* c, int mic_muted, int deafened);
|
|
|
|
|
|
|
feat(windows): UI overhaul -- toolbar, unified log, PM windows, channel counts, output volume
- Voice actions (Join Voice, Share Screen Audio) moved to a ToolStrip toolbar and
a new Voice menu in the menu bar; removed from the bottom voice panel
- Activity log and chat log collapsed into a single RichTextBox (rtbLog); activity
events appear in gray, chat messages in default color
- Private messaging reworked: each conversation opens in its own modeless
PrivateMessageForm instead of sharing the main chat log via a scope dropdown;
cboScope removed; main compose bar always sends to the current channel
- New "Messages -> New Private Message..." menu item (Ctrl+P) opens a UserPickerDialog
listing all connected server users (not just the current channel) so you can PM
anyone on the server
- Channel tree now shows live user counts, e.g. "General (3)" -- counts sourced from
the existing _users dictionary which already tracks all server users with channel IDs
- Global output volume slider (TrackBar, 0-100, default 80) added to the right panel;
wired to new vc_set_output_volume C ABI function that applies a master gain multiplier
in the audio engine playback callback after mixing all streams
- vc_set_output_volume added end-to-end: voicecat.h, audio_engine.h/.cpp,
client.h/.cpp, voicecat.cpp, NativeMethods.cs, VoiceCatClient.cs
- Documented Windows PowerShell ctest requirement in AGENTS.md and CLAUDE.md:
MinGW binaries exit 0xc0000139 in Git Bash; always run ctest/.exe via PowerShell
22/22 ctest green (PowerShell); dotnet build 0 warnings.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-20 14:24:54 +02:00
|
|
|
|
/* Global playback volume applied after mixing all remote streams. gain 0.0 = silent,
|
|
|
|
|
|
* 1.0 = unity (default), >1.0 amplifies. Always LOCAL — no protocol traffic. */
|
|
|
|
|
|
VC_API vc_result vc_set_output_volume(vc_client* c, float gain);
|
|
|
|
|
|
|
feat(clients): persist input settings, add mic input gain, fix iOS chat + VoiceOver
Input mode (VAD/PTT/Always-On), VAD threshold, and the new mic gain were
applied to the core + UI but never saved, so every relaunch reset to VAD
defaults. Each client now persists them and re-applies on connect:
- iOS: UserDefaults (SessionState.loadAndApplyVoiceSettings + setter writes)
- macOS: UserDefaults via MainWindowController didSet + loadPersistedAudioSettings
(settings window also restores the VAD slider from the stored threshold)
- Windows: new Models/VoiceSettings.cs (JSON at %AppData%\VoiceCat\voice.json,
mirrors FeedbackSettings) loaded/applied in MainForm
Add global send-side mic gain API vc_set_input_gain (applied to MIC PCM in
on_capture_frame before the VAD gate, clamped to int16) + Swift/C# bindings,
and a 0-300% (default 100%) mic-volume slider on all three clients.
Fix iOS chat: ChatView called sendText(scope:.channel) with no targetId (0),
so channel messages went nowhere; now passes session.currentChannelId.
Fix iOS per-user tuning for VoiceOver: the tuning sheet was long-press
.contextMenu only (invisible to VoiceOver); UserRow now also exposes the same
buttons via .accessibilityActions (no visual change).
Verified: core builds clean; ctest 24/27 (3 pre-existing teardown crashes,
reproduced with changes stashed); VoiceCatMac + VoiceCatiOS (arm64 sim) build
SUCCEEDED; VoiceCat.Interop dotnet build succeeded. Windows App not built
(WinForms can't build on macOS) — follows existing patterns.
2026-06-23 03:35:26 +02:00
|
|
|
|
/* Send-side microphone input gain. Applied to captured MIC PCM before the VAD/PTT gate and
|
|
|
|
|
|
* Opus encode (so boosting a quiet mic also helps it cross the VAD threshold). gain 0.0 = silent,
|
|
|
|
|
|
* 1.0 = unity (default), >1.0 amplifies; the boosted signal is clamped to int16. MIC stream only;
|
|
|
|
|
|
* always LOCAL — no protocol traffic. */
|
|
|
|
|
|
VC_API vc_result vc_set_input_gain(vc_client* c, float gain);
|
|
|
|
|
|
|
feat(audio): real noise suppression via vendored RNNoise (send + receive)
The two-sided NR plumbing (RemoteStream::recv_ns + the per-listener
vc_set_remote_stream noise_reduction toggle) was wired but inert:
ApmProcessor::create() returned a no-op passthrough, because the
originally-planned webrtc-audio-processing has no working Windows/macOS
build. Drop in RNNoise as the real backend behind the same ApmProcessor
interface, lighting up both NR paths.
- Vendor RNNoise (BSD-3 + CC0) at third_party/rnnoise/ — the vcpkg port
is !windows !arm, so it can't cover our primary targets. Shrunk int8
model (78MB -> 11.7MB via upstream scripts/shrink_model.sh), built as a
standalone C static lib with no RTCD (portable scalar path on x86,
auto-NEON on arm64) under -DDISABLE_DEBUG_FLOAT. Model is baked in
(rnnoise_create(NULL)); no runtime file.
- New RnnoiseProcessor (core/src/audio/apm_processor.cpp) selected by
ApmProcessor::create() when VOICECAT_HAS_NS. Mono/48kHz/480-sample;
our clock is fixed 48kHz and Opus frame sizes are multiples of 480, so
no resampling. RT-safe: allocates at construction, lock-free in the
capture/playback callbacks.
- Receive-side: lit up via the factory; gated to mono streams (a stereo
stream is a screen-audio share, not voice).
- Send-side (new): vc_set_input_noise_reduction(client, enable) ABI +
vc_client::mic_ns_, run before input gain/VAD in on_capture_frame. A
stereo mic is downmixed to mono ONLY when NR is on — with NR off a
stereo mic keeps full stereo (never collapse mic quality unasked).
- Enable C as a project language for the vendored lib.
- New noise_suppression test: white noise through ApmProcessor::create()
drops ~99.9% RMS. ctest --preset dev green, 28/28. windows-client DLL
builds clean with vc_set_input_noise_reduction exported, system-only deps.
- Docs synced: voice.md §10, tech-stack.md §1/§5, third_party/README.md,
vcpkg.json note, PROGRESS.md, CLAUDE.md.
Client on/off UI toggles (Windows/macOS/iOS) are the remaining follow-up.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 13:30:54 +02:00
|
|
|
|
/* Send-side microphone noise suppression (RNNoise). Denoises captured MIC PCM before the input
|
|
|
|
|
|
* gain and the VAD/PTT gate, so everyone hears the cleaned signal (one pass for all listeners).
|
|
|
|
|
|
* enable != 0 turns it on. MIC stream only, mono only; always LOCAL — no protocol traffic.
|
|
|
|
|
|
* Independent of the per-listener receive-side NR in vc_set_remote_stream (docs/voice.md §10). */
|
|
|
|
|
|
VC_API vc_result vc_set_input_noise_reduction(vc_client* c, int enable);
|
|
|
|
|
|
|
scaffold: M0 skeleton + agent onboarding (build, architecture, progress)
Turn the design into a buildable, dependency-free M0 skeleton plus the
onboarding layer so a new agent can pick up instantly.
Build system:
- CMake + CMakePresets (dev = no deps; server-release = vcpkg) + vcpkg.json
- Skeleton builds with just a C++20 compiler; deps stay off until needed
- .gitattributes (LF), .gitignore, .clang-format
Core (libvoicecat):
- core/include/voicecat.h: full C ABI (the client/server contract), stubbed
- core/proto/voicecat.proto: control-plane wire format, matches docs/protocol.md
- src/{net,crypto,codec,protocol,session,audio,core}: subsystem stubs that
return VC_ERR_NOT_IMPLEMENTED, each pointing to its design doc
- server/ (voicecat-server) and tools/vccli/ link the core
- tests/: CTest smoke test asserting the C ABI contract (behavior, not just build)
- clients/{apple,windows}: M4 placeholders
Onboarding for agents:
- CLAUDE.md: hub — build/test commands, architecture at a glance, doc map, rules
- AGENTS.md: working method (behavior-driven; clean compile is the floor not the goal)
- PROGRESS.md: living tracker — M0 done, M1 task checklist, "where we left off"
Verified: cmake --preset dev && cmake --build --preset dev && ctest --preset dev → green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 21:09:09 +02:00
|
|
|
|
/* Receive-side, per remote stream, all LOCAL (no protocol traffic) — docs/voice.md §10:
|
|
|
|
|
|
* gain (0..) , mute, and listener-chosen noise reduction on a specific user's stream. */
|
|
|
|
|
|
VC_API vc_result vc_set_remote_stream(vc_client* c, uint32_t user_id, uint32_t stream_id,
|
2026-06-18 02:06:44 +02:00
|
|
|
|
float gain, int muted, int noise_reduction);
|
|
|
|
|
|
|
|
|
|
|
|
/* Reads back the receive-side state last set on (user_id, stream_id) via
|
|
|
|
|
|
* vc_set_remote_stream (or the defaults if never set). VC_ERR_INVALID_ARG if the user/stream
|
|
|
|
|
|
* isn't known. */
|
|
|
|
|
|
VC_API vc_result vc_get_remote_stream(vc_client* c, uint32_t user_id, uint32_t stream_id,
|
|
|
|
|
|
vc_remote_stream_state* out);
|
scaffold: M0 skeleton + agent onboarding (build, architecture, progress)
Turn the design into a buildable, dependency-free M0 skeleton plus the
onboarding layer so a new agent can pick up instantly.
Build system:
- CMake + CMakePresets (dev = no deps; server-release = vcpkg) + vcpkg.json
- Skeleton builds with just a C++20 compiler; deps stay off until needed
- .gitattributes (LF), .gitignore, .clang-format
Core (libvoicecat):
- core/include/voicecat.h: full C ABI (the client/server contract), stubbed
- core/proto/voicecat.proto: control-plane wire format, matches docs/protocol.md
- src/{net,crypto,codec,protocol,session,audio,core}: subsystem stubs that
return VC_ERR_NOT_IMPLEMENTED, each pointing to its design doc
- server/ (voicecat-server) and tools/vccli/ link the core
- tests/: CTest smoke test asserting the C ABI contract (behavior, not just build)
- clients/{apple,windows}: M4 placeholders
Onboarding for agents:
- CLAUDE.md: hub — build/test commands, architecture at a glance, doc map, rules
- AGENTS.md: working method (behavior-driven; clean compile is the floor not the goal)
- PROGRESS.md: living tracker — M0 done, M1 task checklist, "where we left off"
Verified: cmake --preset dev && cmake --build --preset dev && ctest --preset dev → green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 21:09:09 +02:00
|
|
|
|
|
feat(M3): multi-stream & per-channel tuning
Implements docs/roadmap.md M3: multiple concurrent streams per user (MIC +
SCREEN_AUDIO + AUX_DEVICE), independent per-stream receiver gain/mute/noise-
reduction, talk indicators, and enforced per-channel Opus configurability
(mono/stereo, bitrate, frame size, FEC/DTX, application).
Bugs fixed along the way (found while implementing, not pre-existing scope):
- Server hard-coded stream_id=1 for every announce, so a second stream from
the same user silently overwrote the first in SessionRegistry::set_user_stream.
Now a per-session counter (ConnSession::next_stream_id_); handle_stream_stop
validates against announced_stream_ids_ before clearing.
- Client dropped mode/dtx/complexity/application from effective_audio even for
the single M2 stream -- only sample_rate/bitrate_bps/frame_ms/fec were ever
applied to OpusParams. Fixed on both the send (handle_stream_announce_result)
and receive (sync_remote_streams) paths via a shared
opus_params_from_audio_config() helper.
- OpusEncoder always used OPUS_APPLICATION_VOIP; added OpusParams::application
and wired it through.
- on_playback's per-stream decode passed the wrong frame_size to opus_decode
(total samples instead of samples-per-channel), which would have overflowed
the decode buffer for any stereo stream.
- teardown_voice() raced when called concurrently from run_io()'s own cleanup
and from disconnect() on a different thread -- both could see
udp_thread_/talk_timer_thread_ as joinable() at once and race to join() the
same std::thread (intermittent std::system_error under ctest). Fixed with a
teardown_mu_ guard instead of carrying the flake forward.
New:
- Per-channel AudioConfig: SessionRegistry now seeds Lobby (mono/24kbps/VOIP/
FEC+DTX) and a new "Music Room" channel (stereo/128kbps/AUDIO/no DTX);
handle_stream_announce enforces the channel's config, clamping (not
overriding) bitrate_bps to its ceiling.
- core/src/core/client.h/.cpp: local-stream state is now a
std::unordered_map<int, LocalStream> keyed by vc_stream_kind, with
request_id-correlated announce/result handling (request_id already
round-tripped on the wire; just wasn't read before). on_capture_frame is
kind-aware and upmixes mono capture to stereo when a stream's config calls
for it. set_self_mute's mic_muted now only gates the MIC kind. NS is wired
through set_remote_stream. New run_talk_timer() thread emits
VC_EVENT_TALK_STATE from both remote and local edge detection.
- core/src/audio/audio_engine.h/.cpp: kind-keyed injection taps
(inject_capture), stereo-to-mono downmix at the decode/mix boundary,
RemoteStream gains recv_ns (lazy ApmProcessor) + noise_reduction_enabled
and last_voice_ms/talking; new set_stream_noise_reduction() and
poll_talk_transitions().
- core/src/session/session.h/.cpp: Stream now carries the full AudioConfig,
not just sample_rate/frame_ms.
- New additive C ABI (core/include/voicecat.h): vc_audio_config +
vc_get_stream_audio_config (effective Opus config for any stream you own or
a peer's); vc_test_inject_capture (test-only synthetic PCM injection,
clearly marked, mirrors AudioEngine::inject_capture).
- tests/test_m3_multistream.cpp: the M3 exit criterion through the real ABI
(mirrors test_voice_client_abi.cpp's approach, not raw sockets) -- two
concurrent local streams, independent gain/mute/NS control, per-channel
config divergence via vc_get_stream_audio_config, talk indicators.
Explicitly out of scope for this pass (tracked in PROGRESS.md, not silently
dropped): VAD/PTT input gate + device enumeration; real WASAPI loopback
capture for SCREEN_AUDIO (synthetic injection only); true stereo playback
output (AudioEngine's mixer/output device stays mono -- Opus itself is fully
stereo-correct on the wire).
ctest --test-dir build/m1-dev: 11/11 green, verified across 3 consecutive
full-suite runs plus 8 standalone runs of the new test.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 14:12:37 +02:00
|
|
|
|
/* Effective Opus config in use for (user_id, stream_id) — your own stream or a peer's.
|
|
|
|
|
|
* VC_ERR_INVALID_ARG if the user/stream isn't known. */
|
|
|
|
|
|
VC_API vc_result vc_get_stream_audio_config(vc_client* c, uint32_t user_id, uint32_t stream_id,
|
|
|
|
|
|
vc_audio_config* out);
|
|
|
|
|
|
|
|
|
|
|
|
/* TEST-ONLY — not for production use. Bypasses the real capture device, injecting raw PCM
|
|
|
|
|
|
* directly into the named local stream's encode pipeline (see AudioEngine::inject_capture).
|
|
|
|
|
|
* Exists so automated tests can drive the real vc_client/ABI path end-to-end without a
|
|
|
|
|
|
* microphone. `stream_id` is the id returned by vc_stream_start. */
|
|
|
|
|
|
VC_API vc_result vc_test_inject_capture(vc_client* c, uint32_t stream_id, const int16_t* pcm,
|
|
|
|
|
|
size_t samples);
|
|
|
|
|
|
|
feat(ios): audio overhaul, Join/Leave Voice, channel-id sync fix, stereo mic capture
Three iOS client problems fixed plus a new core stereo-mic capture ABI:
1. Channel-id sync bug (mic button permanently dimmed): SessionState never
synced currentChannelId from the self user's channelId on connect, so the
mic button (gated on currentChannelId == 0) stayed dimmed. Added
syncSelfChannel() (mirrors macOS MainWindowController.swift:461,491,522);
called from init/.channelList/.userJoined/.userLeft/.userUpdated/.joinResult.
Added applyServerMuteState() + serverMuted/serverDeafened to VoiceState.
2. Join/Leave Voice button: replaced icon-only mic toggle with explicit
text button (parity with macOS). Mute/deafen disable when not in voice.
3. IOSAudioRouter.swift (new): full AVAudioSession routing layer — input
port selection, built-in mic orientation/polar patterns, Bluetooth
HFP/A2DP/Off modes, Standard/Raw mic processing, stereo capture, AirPlay,
UserDefaults persistence. AudioSessionManager delegates to it.
4. Core stereo-mic capture (append-only ABI): vc_set_capture_channels()
lets the core open the mic device in stereo (2-ch interleaved). LocalStream
gains capture_channels; ensure_audio_running reads it; audio_engine.cpp
capture_accum_ + on_capture updated to channel-aware accumulation. Test
test_stereo_mic_capture (headless, L!=R stereo round-trip). Swift wrapper
VoiceCatClient.setCaptureChannels.
5. Settings UI rework: AVAudioSession-derived input/output tree replaces
miniaudio device picker.
6. iOS deployment target raised to 18.0 (Package.swift + project.pbxproj).
swift-tools-version 6.0 with swiftLanguageModes .v5.
Docs: tech-stack.md, architecture.md, voice.md, roadmap.md, building.md
updated; stale 'vc_audio_suspend/resume deferred' claims corrected.
Verified: ctest --preset dev 21/21 green; swift test 6/6 green;
xcodebuild -target VoiceCatiOS -sdk iphonesimulator BUILD SUCCEEDED.
2026-06-19 13:17:52 +02:00
|
|
|
|
/* Set the capture channel count for a local MIC stream (1 = mono, 2 = stereo interleaved).
|
2026-06-19 17:39:23 +02:00
|
|
|
|
* Must be called after vc_stream_start. Stores the value; it takes effect on the next engine
|
|
|
|
|
|
* (re)start. Does NOT restart the engine itself — the caller must follow up with
|
|
|
|
|
|
* vc_audio_restart() after AVAudioSession routing has settled (iOS) or after any platform
|
|
|
|
|
|
* audio-session reconfiguration. On iOS the Swift AVAudioSession routing layer enables stereo
|
|
|
|
|
|
* built-in mic capture by switching the built-in mic's data source to the .stereo polar
|
|
|
|
|
|
* pattern (setPreferredDataSource + setPreferredPolarPattern(.stereo) + setPreferredInput +
|
|
|
|
|
|
* setInputDataSource), calls this to record the desired channel count, and then calls
|
|
|
|
|
|
* vc_audio_restart() so the core reopens capture and playback against the new route.
|
|
|
|
|
|
* VC_ERR_INVALID_ARG if stream_id is unknown or channels is not 1 or 2. */
|
feat(ios): audio overhaul, Join/Leave Voice, channel-id sync fix, stereo mic capture
Three iOS client problems fixed plus a new core stereo-mic capture ABI:
1. Channel-id sync bug (mic button permanently dimmed): SessionState never
synced currentChannelId from the self user's channelId on connect, so the
mic button (gated on currentChannelId == 0) stayed dimmed. Added
syncSelfChannel() (mirrors macOS MainWindowController.swift:461,491,522);
called from init/.channelList/.userJoined/.userLeft/.userUpdated/.joinResult.
Added applyServerMuteState() + serverMuted/serverDeafened to VoiceState.
2. Join/Leave Voice button: replaced icon-only mic toggle with explicit
text button (parity with macOS). Mute/deafen disable when not in voice.
3. IOSAudioRouter.swift (new): full AVAudioSession routing layer — input
port selection, built-in mic orientation/polar patterns, Bluetooth
HFP/A2DP/Off modes, Standard/Raw mic processing, stereo capture, AirPlay,
UserDefaults persistence. AudioSessionManager delegates to it.
4. Core stereo-mic capture (append-only ABI): vc_set_capture_channels()
lets the core open the mic device in stereo (2-ch interleaved). LocalStream
gains capture_channels; ensure_audio_running reads it; audio_engine.cpp
capture_accum_ + on_capture updated to channel-aware accumulation. Test
test_stereo_mic_capture (headless, L!=R stereo round-trip). Swift wrapper
VoiceCatClient.setCaptureChannels.
5. Settings UI rework: AVAudioSession-derived input/output tree replaces
miniaudio device picker.
6. iOS deployment target raised to 18.0 (Package.swift + project.pbxproj).
swift-tools-version 6.0 with swiftLanguageModes .v5.
Docs: tech-stack.md, architecture.md, voice.md, roadmap.md, building.md
updated; stale 'vc_audio_suspend/resume deferred' claims corrected.
Verified: ctest --preset dev 21/21 green; swift test 6/6 green;
xcodebuild -target VoiceCatiOS -sdk iphonesimulator BUILD SUCCEEDED.
2026-06-19 13:17:52 +02:00
|
|
|
|
VC_API vc_result vc_set_capture_channels(vc_client* c, uint32_t stream_id, uint32_t channels);
|
|
|
|
|
|
|
feat: external PCM feed/tap API (vc_stream_feed_pcm + vc_set_pcm_sink)
Promotes vc_test_inject_capture (mono-only, TEST-ONLY) to a public,
stereo-capable production API and adds a symmetric PCM tap on the
receive side. Enables ReplayKit (iOS), ScreenCaptureKit (macOS), bots,
soundboards, and custom clients — all without a hardware audio device.
Core C++:
- voicecat.h: new vc_stream_feed_pcm, vc_pcm_sink_cb typedef,
vc_set_pcm_sink; vc_test_inject_capture kept as deprecated alias
- audio_engine: stereo-aware inject_capture (channels param + ring
reset on channel-count change); atomic pcm_sink_ fired per decoded
frame in on_playback; RemoteStream carries user_id/stream_id for
RT-safe sink metadata; init_recv_stream takes user_id+stream_id
- client.cpp: stream_feed_pcm / set_pcm_sink implementations;
sync_remote_streams passes user_id/stream_id to init_recv_stream
- voicecat.cpp: trampolines + channels=1/2 validation
Tests: test_external_pcm (headless, 3 sub-tests: mono round-trip,
stereo feed L≠R, sink metadata+disable). ctest 23/23.
Swift: feedPcm / setPcmSink in VoiceCatClient.swift + 4 XCTest
smoke tests (ExternalPcmTests.swift).
C#: StreamFeedPcm / SetPcmSink in VoiceCatClient.cs + NativeMethods.cs
(vc_stream_feed_pcm unsafe P/Invoke, VcPcmSinkCallback delegate,
vc_set_pcm_sink via nint) + 4 xUnit smoke tests (ExternalPcmTests.cs).
Docs: architecture.md §4 new subsection, voice.md §9 updated
(macOS/iOS now reference vc_stream_feed_pcm), protocol.md §8 explicit
no-protocol-change note, roadmap.md M5 entry.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-20 17:52:09 +02:00
|
|
|
|
/* ── External PCM feed/tap ─────────────────────────────────────────────────── */
|
|
|
|
|
|
|
|
|
|
|
|
/* External PCM feed — production-grade API for driving a local stream's encode pipeline
|
|
|
|
|
|
* with caller-supplied PCM instead of (or in addition to) a hardware capture device. The
|
|
|
|
|
|
* stream must already be started (vc_stream_start). The core frames, encodes (Opus), seals
|
|
|
|
|
|
* (AEAD), and sends (UDP) the provided samples exactly as it would mic/loopback audio.
|
|
|
|
|
|
*
|
2026-06-22 16:45:02 +02:00
|
|
|
|
* pcm MUST be 48 kHz int16 — the core does NOT resample. (The whole audio engine runs at
|
|
|
|
|
|
* 48 kHz; see docs/voice.md §3. A bot is responsible for resampling its source to 48 kHz.)
|
|
|
|
|
|
*
|
|
|
|
|
|
* samples_per_channel : samples per channel for THIS call. Any count is accepted — the core
|
|
|
|
|
|
* buffers and re-chunks to the channel's Opus frame size (the channel's frame_ms decides
|
|
|
|
|
|
* this: 480 @ 10 ms, 960 @ 20 ms, 1920 @ 40 ms, …). You need not match the frame size, and
|
|
|
|
|
|
* a channel with a non-20 ms window is handled transparently.
|
feat: external PCM feed/tap API (vc_stream_feed_pcm + vc_set_pcm_sink)
Promotes vc_test_inject_capture (mono-only, TEST-ONLY) to a public,
stereo-capable production API and adds a symmetric PCM tap on the
receive side. Enables ReplayKit (iOS), ScreenCaptureKit (macOS), bots,
soundboards, and custom clients — all without a hardware audio device.
Core C++:
- voicecat.h: new vc_stream_feed_pcm, vc_pcm_sink_cb typedef,
vc_set_pcm_sink; vc_test_inject_capture kept as deprecated alias
- audio_engine: stereo-aware inject_capture (channels param + ring
reset on channel-count change); atomic pcm_sink_ fired per decoded
frame in on_playback; RemoteStream carries user_id/stream_id for
RT-safe sink metadata; init_recv_stream takes user_id+stream_id
- client.cpp: stream_feed_pcm / set_pcm_sink implementations;
sync_remote_streams passes user_id/stream_id to init_recv_stream
- voicecat.cpp: trampolines + channels=1/2 validation
Tests: test_external_pcm (headless, 3 sub-tests: mono round-trip,
stereo feed L≠R, sink metadata+disable). ctest 23/23.
Swift: feedPcm / setPcmSink in VoiceCatClient.swift + 4 XCTest
smoke tests (ExternalPcmTests.swift).
C#: StreamFeedPcm / SetPcmSink in VoiceCatClient.cs + NativeMethods.cs
(vc_stream_feed_pcm unsafe P/Invoke, VcPcmSinkCallback delegate,
vc_set_pcm_sink via nint) + 4 xUnit smoke tests (ExternalPcmTests.cs).
Docs: architecture.md §4 new subsection, voice.md §9 updated
(macOS/iOS now reference vc_stream_feed_pcm), protocol.md §8 explicit
no-protocol-change note, roadmap.md M5 entry.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-20 17:52:09 +02:00
|
|
|
|
* channels : 1 (mono) or 2 (stereo interleaved L/R). VC_ERR_INVALID_ARG otherwise.
|
|
|
|
|
|
*
|
|
|
|
|
|
* Use cases: ReplayKit Broadcast Extension (iOS), ScreenCaptureKit (macOS), bots (TTS /
|
|
|
|
|
|
* 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). */
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
|
|
/* External PCM tap — receive decoded remote audio as int16 PCM per stream, before it is
|
|
|
|
|
|
* summed into the hardware mix. The callback fires on the audio playback thread once per
|
|
|
|
|
|
* decoded Opus frame (typically every 20 ms) for each active remote stream:
|
|
|
|
|
|
*
|
|
|
|
|
|
* cb(user, user_id, stream_id, pcm, samples_per_channel, channels, sample_rate)
|
|
|
|
|
|
*
|
|
|
|
|
|
* user_id / stream_id : identify the sender (same values as VC_EVENT_STREAM_STARTED).
|
|
|
|
|
|
* pcm : decoded int16 PCM, interleaved when channels == 2.
|
|
|
|
|
|
* samples_per_channel : samples per channel for this frame (typically 960 @ 48 kHz).
|
|
|
|
|
|
* channels : 1 or 2, matching the sender's stream configuration.
|
|
|
|
|
|
* sample_rate : always 48000 in the current implementation.
|
|
|
|
|
|
*
|
|
|
|
|
|
* Pass cb = NULL to disable (default: disabled; hardware playback only).
|
|
|
|
|
|
* The callback MUST NOT block, lock, or allocate — copy what you need and return.
|
|
|
|
|
|
* PCM is still delivered to the hardware playback device regardless (dual output). */
|
|
|
|
|
|
typedef void (*vc_pcm_sink_cb)(void* user, uint32_t user_id, uint32_t stream_id,
|
|
|
|
|
|
const int16_t* pcm, size_t samples_per_channel,
|
|
|
|
|
|
uint32_t channels, uint32_t sample_rate);
|
|
|
|
|
|
VC_API vc_result vc_set_pcm_sink(vc_client* c, vc_pcm_sink_cb cb, void* user);
|
|
|
|
|
|
|
2026-06-22 02:38:01 +02:00
|
|
|
|
/* ── External playback (iOS VPIO / echo cancellation) ───────────────────────────
|
|
|
|
|
|
* On iOS, real echo cancellation + noise suppression + AGC are provided ONLY by Apple's
|
|
|
|
|
|
* Voice-Processing I/O audio unit (VPIO), which the Swift AVAudioEngine layer owns. For VPIO
|
|
|
|
|
|
* to cancel echo, the remote-audio playback must go through the SAME VPIO unit as the mic
|
|
|
|
|
|
* capture (VPIO subtracts the played-back signal from the mic). So in that topology the core
|
|
|
|
|
|
* must NOT open/drive its own hardware playback device — its output would bypass VPIO, giving
|
|
|
|
|
|
* it no reference signal and producing echo. Instead, enable external playback: the core keeps
|
|
|
|
|
|
* decoding + mixing every remote stream on a steady ~20 ms cadence and delivers the FINAL
|
|
|
|
|
|
* MIXED PCM (post output-volume, all streams summed) to this sink, which the Swift layer
|
|
|
|
|
|
* renders through the VPIO output.
|
|
|
|
|
|
*
|
|
|
|
|
|
* cb(user, pcm, samples_per_channel, channels, sample_rate)
|
|
|
|
|
|
*
|
|
|
|
|
|
* pcm : final mixed int16 PCM, interleaved when channels == 2.
|
|
|
|
|
|
* samples_per_channel : samples per channel for this block (960 @ 20 ms / 48 kHz).
|
|
|
|
|
|
* channels : the engine's playback channel count (2 = stereo).
|
|
|
|
|
|
* sample_rate : always 48000.
|
|
|
|
|
|
*
|
|
|
|
|
|
* The callback fires on the core's mixer-timer thread (NOT a hardware audio thread). It fires
|
|
|
|
|
|
* steadily even with no remote streams (a silent block), so the renderer has a continuous
|
|
|
|
|
|
* clock. The callback MUST NOT block, lock, or allocate — copy into a lock-free ring and
|
|
|
|
|
|
* return. Independent of vc_set_pcm_sink (the per-stream tap), which still works. Pass cb=NULL
|
|
|
|
|
|
* to disable (default: disabled). */
|
|
|
|
|
|
typedef void (*vc_mixed_output_cb)(void* user, const int16_t* pcm,
|
|
|
|
|
|
size_t samples_per_channel, uint32_t channels,
|
|
|
|
|
|
uint32_t sample_rate);
|
|
|
|
|
|
VC_API vc_result vc_set_mixed_output_sink(vc_client* c, vc_mixed_output_cb cb, void* user);
|
|
|
|
|
|
|
|
|
|
|
|
/* Enable/disable external-playback mode (default: disabled = normal hardware playback). When
|
|
|
|
|
|
* enabled, the core does NOT open a hardware playback device; decode+mix runs on an internal
|
|
|
|
|
|
* ~20 ms timer and the result is delivered via vc_set_mixed_output_sink. To also bypass the
|
|
|
|
|
|
* hardware mic (feeding VPIO-processed mic PCM instead), start the MIC stream with
|
|
|
|
|
|
* vc_stream_desc.external_feed=1 and push frames via vc_stream_feed_pcm — the core then skips
|
|
|
|
|
|
* the hardware capture device too. Apply BEFORE the engine starts, or follow with
|
|
|
|
|
|
* vc_audio_restart() to apply to a running engine. `enable` is a bool (0/1). */
|
|
|
|
|
|
VC_API vc_result vc_set_external_playback(vc_client* c, int enable);
|
|
|
|
|
|
|
scaffold: M0 skeleton + agent onboarding (build, architecture, progress)
Turn the design into a buildable, dependency-free M0 skeleton plus the
onboarding layer so a new agent can pick up instantly.
Build system:
- CMake + CMakePresets (dev = no deps; server-release = vcpkg) + vcpkg.json
- Skeleton builds with just a C++20 compiler; deps stay off until needed
- .gitattributes (LF), .gitignore, .clang-format
Core (libvoicecat):
- core/include/voicecat.h: full C ABI (the client/server contract), stubbed
- core/proto/voicecat.proto: control-plane wire format, matches docs/protocol.md
- src/{net,crypto,codec,protocol,session,audio,core}: subsystem stubs that
return VC_ERR_NOT_IMPLEMENTED, each pointing to its design doc
- server/ (voicecat-server) and tools/vccli/ link the core
- tests/: CTest smoke test asserting the C ABI contract (behavior, not just build)
- clients/{apple,windows}: M4 placeholders
Onboarding for agents:
- CLAUDE.md: hub — build/test commands, architecture at a glance, doc map, rules
- AGENTS.md: working method (behavior-driven; clean compile is the floor not the goal)
- PROGRESS.md: living tracker — M0 done, M1 task checklist, "where we left off"
Verified: cmake --preset dev && cmake --build --preset dev && ctest --preset dev → green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 21:09:09 +02:00
|
|
|
|
/* ── Text ─────────────────────────────────────────────────────────────────── */
|
|
|
|
|
|
VC_API vc_result vc_send_text(vc_client* c, vc_text_scope scope, uint32_t target_id,
|
|
|
|
|
|
const char* utf8);
|
|
|
|
|
|
|
|
|
|
|
|
/* ── Device enumeration (for UI pickers) ──────────────────────────────────── */
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
feat(M4): Windows WinForms client, TOFU identity pinning, VAD threshold + always-on mode
Core ABI extensions (voicecat.h):
- vc_list_channels / vc_list_users / vc_list_user_streams — pull-based snapshot getters
for the channel-tree and user-list UI; session_model_mu_ guards cross-thread reads
- VC_EVENT_JOIN_RESULT / vc_join_channel — channel join with optional password
- VC_EVENT_SERVER_IDENTITY + vc_confirm_server_identity — TOFU gate that blocks io_thread_
until the UI approves or rejects; pins TLS leaf-cert SHA-256 (not declared Ed25519)
- vc_get_server_identity_display — Ed25519 fingerprint for human-readable display only
- VC_INPUT_ALWAYS_ON = 2 in vc_input_mode — transmit unconditionally, no VAD gate
- vc_set_vad_threshold — live RMS threshold update (0.0–1.0); EnergyVadProcessor stores
it atomically so the audio RT path reads without a lock
C++ implementation:
- SessionModel::apply_snapshot / apply_channel_event fixed to populate parent_id,
password_protected, and max_users (were permanently zeroed)
- TlsContext::peer_cert_fingerprint — SHA-256 of peer leaf cert DER via mbedTLS
- TofuStore split into peek (read-only) + pin (write) so first-connect only persists
after user approval; tofu_store_path in vc_config for per-user pin file location
- TcpAcceptor uses dual-stack IPv6+IPv4 fallback (fixes localhost → ::1 on Windows)
- windows-client CMake preset: Release shared DLL, static MinGW runtime, no tools/tests
- New C++ tests: test_channel_user_list_abi, test_tofu_flow (14/14 green)
Windows client (clients/windows/ — .NET 10 WinForms):
- VoiceCat.Interop: LibraryImport P/Invoke surface, UnmanagedCallersOnly callbacks,
Channel<VoiceCatEvent> event delivery drained by 30ms WinForms Timer
- VoiceCat.App: ConnectDialog (saved servers, DPAPI password storage), ServerIdentity-
Dialog (TOFU first-connect / mismatch warning), MainForm (channel TreeView, user
ListBox, RichTextBox chat, voice controls, device pickers, VAD/PTT/always-on mode,
per-user gain/mute/NR tuning, VAD sensitivity TrackBar, level meter ProgressBar)
- PttKeyCaptureDialog — focus-scoped PTT key capture (documented limitation)
- PerUserTuningDialog — real-time gain/mute/NR applied to all of a user's streams
- Accessibility: explicit AccessibleName/Description on every control, & mnemonics,
Activity log ListBox as durable screen-reader record, AutomationNotification for
curated live announcements
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 00:35:16 +02:00
|
|
|
|
/* ── Channel / user / stream enumeration (M4; 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);
|
|
|
|
|
|
|
|
|
|
|
|
VC_API vc_result vc_list_users(vc_client* c, vc_user_list* out);
|
|
|
|
|
|
VC_API void vc_free_user_list(vc_user_list* list);
|
|
|
|
|
|
|
|
|
|
|
|
/* Streams currently owned by user_id (their mic/screen-audio/aux), per the last snapshot/
|
|
|
|
|
|
* event. VC_ERR_INVALID_ARG if user_id is unknown. */
|
|
|
|
|
|
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 ── */
|
|
|
|
|
|
/* 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
|
|
|
|
|
|
* which it's treated as a reject). accept=0 aborts the connection (emits
|
|
|
|
|
|
* VC_EVENT_DISCONNECTED, result=VC_ERR_CRYPTO) and does NOT update the pin file. accept=1 on
|
|
|
|
|
|
* FIRST_CONNECT/MISMATCH updates the pin file to the new fingerprint and proceeds; accept=1 on
|
|
|
|
|
|
* MATCHED is a no-op confirmation (always safe) and proceeds. VC_ERR_INVALID_ARG if no
|
|
|
|
|
|
* identity confirmation is currently pending. */
|
|
|
|
|
|
VC_API vc_result vc_confirm_server_identity(vc_client* c, int accept /* bool */);
|
|
|
|
|
|
|
|
|
|
|
|
/* The Ed25519 identity fingerprint from ServerHello, hex-formatted for display (e.g. "this
|
|
|
|
|
|
* server also identifies as <hex>"). Purely informational — NOT the value
|
|
|
|
|
|
* vc_confirm_server_identity gates on (see vc_tofu_status's doc comment). Empty string if not
|
|
|
|
|
|
* yet available. Pass out_buf=NULL to query the required buffer size via *out_len first;
|
|
|
|
|
|
* otherwise out_buf must be >= *out_len + 1 bytes (NUL-terminated UTF-8/ASCII hex). */
|
|
|
|
|
|
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, permissions, channel CRUD, in-app account management
- Server-side moderation & permissions (kick/ban/move/server-mute, channel CRUD).
- Database schema v2: channels, bans; BLAKE2b channel passwords, Argon2id accounts.
- C ABI additions and client-side handling (vc_kick_user, vc_ban_user, vc_set_permission, vc_set_server_mute, vc_move_user, vc_create/edit/delete_channel, vc_create/reset/delete/list_account).
- vccli flags for all M5 operations plus --username/--password auth.
- Four new tests covering permissions, kick/ban/move/mute, admin accounts, channel CRUD.
- Docs: protocol.md envelope updates, security.md channel-password hashing, PROGRESS.md.
2026-06-17 15:08:05 +02:00
|
|
|
|
/* ── M5: 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. */
|
|
|
|
|
|
|
|
|
|
|
|
VC_API vc_result vc_kick_user(vc_client* c, uint32_t user_id, const char* reason);
|
|
|
|
|
|
VC_API vc_result vc_ban_user(vc_client* c, uint32_t user_id, const char* reason,
|
|
|
|
|
|
uint64_t expires_unix_ms);
|
|
|
|
|
|
VC_API vc_result vc_set_permission(vc_client* c, uint32_t user_id,
|
|
|
|
|
|
const vc_permissions* perms);
|
|
|
|
|
|
VC_API vc_result vc_set_server_mute(vc_client* c, uint32_t user_id, int muted, int deafened);
|
|
|
|
|
|
VC_API vc_result vc_move_user(vc_client* c, uint32_t user_id, uint32_t channel_id);
|
|
|
|
|
|
|
|
|
|
|
|
VC_API vc_result vc_create_channel(vc_client* c, const vc_channel_info* info);
|
|
|
|
|
|
VC_API vc_result vc_edit_channel(vc_client* c, const vc_channel_info* info);
|
|
|
|
|
|
VC_API vc_result vc_delete_channel(vc_client* c, uint32_t channel_id);
|
|
|
|
|
|
|
|
|
|
|
|
VC_API vc_result vc_create_account(vc_client* c, const char* username, const char* password);
|
|
|
|
|
|
VC_API vc_result vc_reset_password(vc_client* c, const char* username,
|
|
|
|
|
|
const char* new_password);
|
|
|
|
|
|
VC_API vc_result vc_delete_account(vc_client* c, const char* username);
|
|
|
|
|
|
VC_API vc_result vc_list_accounts(vc_client* c);
|
|
|
|
|
|
|
2026-06-17 16:31:29 +02:00
|
|
|
|
/* Pull the last received account list (populated when VC_EVENT_ACCOUNT_LIST fires).
|
|
|
|
|
|
* Caller must free the list with vc_free_account_list. */
|
|
|
|
|
|
VC_API vc_result vc_get_account_list(vc_client* c, vc_account_list* out);
|
|
|
|
|
|
VC_API void vc_free_account_list(vc_account_list* list);
|
|
|
|
|
|
|
M5: moderation, permissions, channel CRUD, in-app account management
- Server-side moderation & permissions (kick/ban/move/server-mute, channel CRUD).
- Database schema v2: channels, bans; BLAKE2b channel passwords, Argon2id accounts.
- C ABI additions and client-side handling (vc_kick_user, vc_ban_user, vc_set_permission, vc_set_server_mute, vc_move_user, vc_create/edit/delete_channel, vc_create/reset/delete/list_account).
- vccli flags for all M5 operations plus --username/--password auth.
- Four new tests covering permissions, kick/ban/move/mute, admin accounts, channel CRUD.
- Docs: protocol.md envelope updates, security.md channel-password hashing, PROGRESS.md.
2026-06-17 15:08:05 +02:00
|
|
|
|
/* Pull the caller's own permissions (from the last AuthResult). */
|
|
|
|
|
|
VC_API vc_result vc_get_permissions(vc_client* c, vc_permissions* out);
|
|
|
|
|
|
|
2026-06-19 02:10:25 +02:00
|
|
|
|
/* AVAudioSession interruption hooks (iOS M6). Pause/resume miniaudio device I/O for
|
|
|
|
|
|
* AVAudioSession interruptions (phone call, Siri, etc.) and backgrounding. Call
|
|
|
|
|
|
* vc_audio_suspend() when an interruption begins; call vc_audio_resume() after the
|
|
|
|
|
|
* session is re-activated. No-op if the audio engine is not running. */
|
|
|
|
|
|
VC_API vc_result vc_audio_suspend(vc_client* c);
|
|
|
|
|
|
VC_API vc_result vc_audio_resume(vc_client* c);
|
|
|
|
|
|
|
2026-06-19 16:58:21 +02:00
|
|
|
|
/* Full audio engine restart (iOS M6). Unlike vc_audio_suspend/resume which only stop/start
|
|
|
|
|
|
* the existing miniaudio devices (leaving them bound to the route that was active when they
|
|
|
|
|
|
* were opened), vc_audio_restart() uninitializes and re-initializes the capture and playback
|
|
|
|
|
|
* devices so they pick up a new AVAudioSession route. Call this from the Swift layer AFTER
|
|
|
|
|
|
* reconfiguring AVAudioSession (setCategory, setPreferredInput, setPreferredPolarPattern, etc.)
|
2026-06-20 03:03:34 +02:00
|
|
|
|
* so the core's devices reopen against the new route.
|
2026-06-19 16:58:21 +02:00
|
|
|
|
* Safe to call when the engine is not running (it will just start it). */
|
|
|
|
|
|
VC_API vc_result vc_audio_restart(vc_client* c);
|
|
|
|
|
|
|
scaffold: M0 skeleton + agent onboarding (build, architecture, progress)
Turn the design into a buildable, dependency-free M0 skeleton plus the
onboarding layer so a new agent can pick up instantly.
Build system:
- CMake + CMakePresets (dev = no deps; server-release = vcpkg) + vcpkg.json
- Skeleton builds with just a C++20 compiler; deps stay off until needed
- .gitattributes (LF), .gitignore, .clang-format
Core (libvoicecat):
- core/include/voicecat.h: full C ABI (the client/server contract), stubbed
- core/proto/voicecat.proto: control-plane wire format, matches docs/protocol.md
- src/{net,crypto,codec,protocol,session,audio,core}: subsystem stubs that
return VC_ERR_NOT_IMPLEMENTED, each pointing to its design doc
- server/ (voicecat-server) and tools/vccli/ link the core
- tests/: CTest smoke test asserting the C ABI contract (behavior, not just build)
- clients/{apple,windows}: M4 placeholders
Onboarding for agents:
- CLAUDE.md: hub — build/test commands, architecture at a glance, doc map, rules
- AGENTS.md: working method (behavior-driven; clean compile is the floor not the goal)
- PROGRESS.md: living tracker — M0 done, M1 task checklist, "where we left off"
Verified: cmake --preset dev && cmake --build --preset dev && ctest --preset dev → green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 21:09:09 +02:00
|
|
|
|
#if defined(__cplusplus)
|
|
|
|
|
|
} /* extern "C" */
|
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
|
|
#endif /* VOICECAT_H */
|