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>
This commit is contained in:
15
.clang-format
Normal file
15
.clang-format
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# VoiceCat C++ style. Keep formatting boring and consistent.
|
||||||
|
BasedOnStyle: Google
|
||||||
|
Language: Cpp
|
||||||
|
Standard: c++20
|
||||||
|
ColumnLimit: 100
|
||||||
|
IndentWidth: 4
|
||||||
|
TabWidth: 4
|
||||||
|
UseTab: Never
|
||||||
|
AccessModifierOffset: -2
|
||||||
|
PointerAlignment: Left
|
||||||
|
DerivePointerAlignment: false
|
||||||
|
AllowShortFunctionsOnASingleLine: Inline
|
||||||
|
AllowShortIfStatementsOnASingleLine: false
|
||||||
|
SortIncludes: CaseInsensitive
|
||||||
|
IncludeBlocks: Regroup
|
||||||
26
.gitattributes
vendored
Normal file
26
.gitattributes
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# Normalize line endings: store text as LF in the repo, check out native.
|
||||||
|
* text=auto
|
||||||
|
|
||||||
|
# Force LF for source and docs regardless of platform (silences CRLF warnings).
|
||||||
|
*.md text eol=lf
|
||||||
|
*.h text eol=lf
|
||||||
|
*.hpp text eol=lf
|
||||||
|
*.c text eol=lf
|
||||||
|
*.cpp text eol=lf
|
||||||
|
*.cmake text eol=lf
|
||||||
|
*.json text eol=lf
|
||||||
|
*.proto text eol=lf
|
||||||
|
*.toml text eol=lf
|
||||||
|
*.sh text eol=lf
|
||||||
|
*.swift text eol=lf
|
||||||
|
*.cs text eol=lf
|
||||||
|
CMakeLists.txt text eol=lf
|
||||||
|
|
||||||
|
# Windows scripts stay CRLF.
|
||||||
|
*.bat text eol=crlf
|
||||||
|
*.ps1 text eol=crlf
|
||||||
|
|
||||||
|
# Binary assets.
|
||||||
|
*.png binary
|
||||||
|
*.ico binary
|
||||||
|
*.icns binary
|
||||||
40
.gitignore
vendored
Normal file
40
.gitignore
vendored
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# Build output
|
||||||
|
/build/
|
||||||
|
/out/
|
||||||
|
*.o
|
||||||
|
*.obj
|
||||||
|
*.a
|
||||||
|
*.lib
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
*.dll
|
||||||
|
*.exe
|
||||||
|
*.pdb
|
||||||
|
|
||||||
|
# vcpkg
|
||||||
|
/vcpkg_installed/
|
||||||
|
/vcpkg/
|
||||||
|
|
||||||
|
# Generated protobuf
|
||||||
|
*.pb.cc
|
||||||
|
*.pb.h
|
||||||
|
*_pb2.py
|
||||||
|
|
||||||
|
# Server runtime data (self-host data dir, keys, db)
|
||||||
|
/voicecat-data/
|
||||||
|
*.sqlite
|
||||||
|
*.sqlite-*
|
||||||
|
|
||||||
|
# IDE / OS
|
||||||
|
.vs/
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.user
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Apple / Windows client build artifacts (added in M4)
|
||||||
|
clients/apple/**/build/
|
||||||
|
clients/apple/**/*.xcodeproj/xcuserdata/
|
||||||
|
clients/windows/**/bin/
|
||||||
|
clients/windows/**/obj/
|
||||||
94
AGENTS.md
Normal file
94
AGENTS.md
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
# AGENTS.md — working method
|
||||||
|
|
||||||
|
This file is the **working method** for a developer or AI agent picking up VoiceCat. Companion
|
||||||
|
files:
|
||||||
|
|
||||||
|
- [`CLAUDE.md`](CLAUDE.md) — the hub: build/test commands, architecture at a glance, doc map.
|
||||||
|
- [`PROGRESS.md`](PROGRESS.md) — living tracker: what's done, what's next. **Update it as you work.**
|
||||||
|
- [`docs/`](docs/) — the **source of truth** for all design.
|
||||||
|
|
||||||
|
Read those, then use the method below.
|
||||||
|
|
||||||
|
## What this repo is right now
|
||||||
|
|
||||||
|
A complete **design** ([`docs/`](docs/)) plus an **M0 skeleton**: it compiles and links, but
|
||||||
|
`libvoicecat`'s subsystems are stubs that return `VC_ERR_NOT_IMPLEMENTED`. Your job is to turn
|
||||||
|
the design into working software, one milestone at a time.
|
||||||
|
|
||||||
|
## The working method (important)
|
||||||
|
|
||||||
|
**A clean compile is the floor, not the goal.** Do not treat "make the compiler errors go
|
||||||
|
away" as done. Each milestone in [`docs/roadmap.md`](docs/roadmap.md) has an **exit
|
||||||
|
criterion stated as observable behavior** — that is what "done" means. Examples:
|
||||||
|
|
||||||
|
- M1 done = *two `vccli` instances actually chat through a real server over TLS*, not "it builds".
|
||||||
|
- M2 done = *you can talk between two clients in a channel and hear loss concealment work*.
|
||||||
|
|
||||||
|
So the loop is:
|
||||||
|
|
||||||
|
1. Pick the current milestone in `docs/roadmap.md`. Read the relevant design doc section.
|
||||||
|
2. Write the smallest test (CTest, or a `vccli` interaction) that encodes the exit behavior.
|
||||||
|
3. Implement the subsystem until that test passes — not just until it compiles.
|
||||||
|
4. Keep the build green and the existing tests passing on every commit.
|
||||||
|
|
||||||
|
Every commit must compile and pass `ctest`. Behavior tests are how you know you're actually
|
||||||
|
making progress.
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
Skeleton (no third-party deps — works immediately):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cmake --preset dev
|
||||||
|
cmake --build --preset dev
|
||||||
|
ctest --preset dev
|
||||||
|
```
|
||||||
|
|
||||||
|
When a subsystem needs real libraries, turn on vcpkg deps:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export VCPKG_ROOT=/path/to/vcpkg # bootstrap vcpkg first; cross-platform
|
||||||
|
cmake --preset server-release # installs deps pinned in vcpkg.json
|
||||||
|
cmake --build --preset server-release
|
||||||
|
```
|
||||||
|
|
||||||
|
`vcpkg.json` currently has a placeholder `builtin-baseline` — set it to a real vcpkg commit
|
||||||
|
SHA the first time you enable `VOICECAT_USE_VCPKG_DEPS`.
|
||||||
|
|
||||||
|
## Where each subsystem lives (and its doc)
|
||||||
|
|
||||||
|
| Path | Subsystem | Design |
|
||||||
|
|------|-----------|--------|
|
||||||
|
| `core/include/voicecat.h` | The C ABI every client/server calls | docs/architecture.md §4 |
|
||||||
|
| `core/proto/voicecat.proto` | Control-plane wire format | docs/protocol.md |
|
||||||
|
| `core/src/net/` | Asio TCP/UDP transport, framing | docs/architecture.md, docs/protocol.md §1 |
|
||||||
|
| `core/src/crypto/` | TLS 1.3 (mbedTLS), media AEAD (libsodium), anti-replay | docs/security.md |
|
||||||
|
| `core/src/codec/` | Opus encode/decode, FEC/DTX | docs/voice.md §3–4 |
|
||||||
|
| `core/src/protocol/` | Envelope (de)serialize, state machine, routing | docs/protocol.md |
|
||||||
|
| `core/src/session/` | Channels, users, streams, permissions, text | docs/protocol.md §5 |
|
||||||
|
| `core/src/audio/` | Capture/playback (miniaudio), APM DSP, jitter buffer, mixer | docs/voice.md §8–11 |
|
||||||
|
| `server/` | Connection mgr, session registry, SFU relay, SQLite | docs/architecture.md §5 |
|
||||||
|
| `tools/vccli/` | Headless client to drive/verify the protocol | — |
|
||||||
|
|
||||||
|
## Suggested first steps (M1 spine)
|
||||||
|
|
||||||
|
1. **Wire protobuf + framing** (`net/` + `protocol/`): build, then generate C++ from
|
||||||
|
`voicecat.proto`, implement the `[u32 length][Envelope]` framing over a plain TCP socket
|
||||||
|
(TLS can come right after). Test: round-trip an `Envelope` through the framer.
|
||||||
|
2. **TLS 1.3 via mbedTLS** (`crypto/`): wrap the TCP channel. Test: `vccli` completes a TLS
|
||||||
|
handshake against `voicecat-server` and exchanges a `ClientHello`/`ServerHello`.
|
||||||
|
3. **Auth + state** (`session/`): guest + admin-provisioned accounts (Argon2id/SQLite),
|
||||||
|
channel tree snapshot/deltas, ephemeral text relay. Test: two `vccli` chat.
|
||||||
|
|
||||||
|
Then proceed to M2 (UDP media) per the roadmap.
|
||||||
|
|
||||||
|
## House rules
|
||||||
|
|
||||||
|
- **No GPL/LGPL dependencies, ever** (closed-source redistribution is a goal). See
|
||||||
|
docs/tech-stack.md §5. CI should fail on a copyleft transitive dep.
|
||||||
|
- **Encryption is mandatory** — never add a plaintext transport path. docs/security.md.
|
||||||
|
- **Real-time audio threads** never allocate, lock, or block. docs/architecture.md §3.
|
||||||
|
- Keep `docs/` and code in sync. If you change a wire format or the C ABI, update the doc in
|
||||||
|
the same commit.
|
||||||
|
- The C ABI is the contract for the Swift/C# clients — treat changes to `voicecat.h` and
|
||||||
|
`voicecat.proto` as deliberate, versioned events (docs/protocol.md §8).
|
||||||
141
CLAUDE.md
Normal file
141
CLAUDE.md
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
# CLAUDE.md — agent hub for VoiceCat
|
||||||
|
|
||||||
|
Auto-loaded each session. This is the **map**: build commands, architecture at a glance, and
|
||||||
|
where everything is. For the *working method* read [`AGENTS.md`](AGENTS.md); for *what's done
|
||||||
|
and what's next* read [`PROGRESS.md`](PROGRESS.md); for *design* read [`docs/`](docs/).
|
||||||
|
|
||||||
|
> **One-line status:** M0 skeleton is complete and verified (builds + links + smoke test
|
||||||
|
> passes). Next up is **M1** (TCP/TLS control plane, auth, channels, ephemeral text). See
|
||||||
|
> [`PROGRESS.md`](PROGRESS.md).
|
||||||
|
|
||||||
|
VoiceCat = self-hosted native voice & text chat (TeamSpeak/Mumble-style). Plain TCP (control)
|
||||||
|
+ UDP (media), no WebRTC, encrypted by default. A shared C++ core (`libvoicecat`) drives
|
||||||
|
native clients (Swift on macOS/iOS, C# on Windows) and the server.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Build & test commands
|
||||||
|
|
||||||
|
The **M0 skeleton builds with no third-party dependencies** — just CMake + Ninja + a C++20
|
||||||
|
compiler. Deps (vcpkg) are off until a subsystem needs them.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Configure + build the skeleton (default; no vcpkg needed)
|
||||||
|
cmake --preset dev
|
||||||
|
cmake --build --preset dev
|
||||||
|
|
||||||
|
# Run the tests (behavior smoke test today; grows per milestone)
|
||||||
|
ctest --preset dev # or: ctest --test-dir build/dev --output-on-failure
|
||||||
|
|
||||||
|
# Run the binaries (Windows adds .exe; Linux/macOS no extension)
|
||||||
|
./build/dev/bin/vccli # headless test client
|
||||||
|
./build/dev/bin/voicecat-server --help
|
||||||
|
./build/dev/bin/voicecat-server --name "My Server"
|
||||||
|
|
||||||
|
# Build a single target / be verbose
|
||||||
|
cmake --build --preset dev --target vccli
|
||||||
|
cmake --build --preset dev --verbose
|
||||||
|
|
||||||
|
# Clean
|
||||||
|
rm -rf build/dev # nuke; or:
|
||||||
|
cmake --build --preset dev --target clean
|
||||||
|
```
|
||||||
|
|
||||||
|
When you start a subsystem that needs real libraries (mbedTLS, libsodium, opus, protobuf, …),
|
||||||
|
turn vcpkg deps on:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# one-time: git clone https://github.com/microsoft/vcpkg && ./vcpkg/bootstrap-vcpkg.sh (.bat on Windows)
|
||||||
|
export VCPKG_ROOT=/path/to/vcpkg # works on Linux / macOS / Windows
|
||||||
|
cmake --preset server-release # auto-installs deps pinned in vcpkg.json
|
||||||
|
cmake --build --preset server-release
|
||||||
|
```
|
||||||
|
|
||||||
|
Other useful toggles (pass with `-D` at configure time):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cmake --preset dev -DVOICECAT_BUILD_SHARED=ON # build libvoicecat as a .dll/.so/.dylib (for the C# client)
|
||||||
|
cmake --preset dev -DVOICECAT_BUILD_SERVER=OFF # core + tools only
|
||||||
|
cmake --preset dev -DVOICECAT_BUILD_TESTS=OFF
|
||||||
|
```
|
||||||
|
|
||||||
|
Formatting: `clang-format` config is `.clang-format` (Google base, 100 cols, 4-space).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git ls-files '*.cpp' '*.h' | xargs clang-format -i
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture at a glance
|
||||||
|
|
||||||
|
Full detail: [`docs/architecture.md`](docs/architecture.md). The short version:
|
||||||
|
|
||||||
|
```
|
||||||
|
Swift (macOS/iOS) ─┐ ┌─ C# (Windows)
|
||||||
|
├──▶ libvoicecat (C ABI: voicecat.h) ◀──┤
|
||||||
|
voicecat-server ───┘ net · crypto · codec · protocol · └─ all UIs are thin
|
||||||
|
(links core) session · audio the core owns audio
|
||||||
|
```
|
||||||
|
|
||||||
|
- **One core, many faces.** Protocol, Opus, crypto, networking, jitter buffer, and mixing
|
||||||
|
live once in C++. Clients call the **C ABI** (`core/include/voicecat.h`); the server links
|
||||||
|
the same core, so framing/crypto never drift between ends.
|
||||||
|
- **Two transports.** TCP + **TLS 1.3** (control, protobuf `Envelope`) and UDP + **exported-key
|
||||||
|
ChaCha20-Poly1305 AEAD** (media, fixed binary voice frame). Encryption is mandatory.
|
||||||
|
- **Threading.** Real-time audio threads never allocate/lock/block; they exchange data with
|
||||||
|
the net thread via lock-free ring buffers; a worker pool absorbs blocking work.
|
||||||
|
|
||||||
|
### Subsystem map (code ↔ design doc)
|
||||||
|
|
||||||
|
| Path | Subsystem | Design |
|
||||||
|
|------|-----------|--------|
|
||||||
|
| `core/include/voicecat.h` | The C ABI (client/server contract) | architecture.md §4 |
|
||||||
|
| `core/proto/voicecat.proto` | Control-plane wire format (source of truth) | protocol.md |
|
||||||
|
| `core/src/net/` | Asio TCP/UDP transport, `[u32 len][payload]` framing | protocol.md §1, voice.md §2 |
|
||||||
|
| `core/src/crypto/` | TLS 1.3 (mbedTLS), media AEAD (libsodium), anti-replay | security.md |
|
||||||
|
| `core/src/codec/` | Opus encode/decode, FEC/DTX | voice.md §3–4 |
|
||||||
|
| `core/src/protocol/` | Envelope (de)serialize, request/response, dispatch | protocol.md |
|
||||||
|
| `core/src/session/` | Channels, users, streams, permissions, ephemeral text | protocol.md §5 |
|
||||||
|
| `core/src/audio/` | miniaudio I/O, APM DSP, jitter buffer, mixer | voice.md §8–11 |
|
||||||
|
| `core/src/core/` | `vc_client` — the handle behind the C ABI | architecture.md §4 |
|
||||||
|
| `server/` | Connection mgr, session registry, SFU relay, SQLite | architecture.md §5 |
|
||||||
|
| `tools/vccli/` | Headless client that drives/verifies the protocol | — |
|
||||||
|
| `clients/apple/`, `clients/windows/` | Native GUIs (M4) | architecture.md §4 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Documentation index (source of truth)
|
||||||
|
|
||||||
|
Read [`docs/`](docs/) before changing behavior. Order:
|
||||||
|
|
||||||
|
1. [docs/README.md](docs/README.md) — overview, locked decisions, glossary
|
||||||
|
2. [docs/architecture.md](docs/architecture.md) — core, C ABI, threading, server
|
||||||
|
3. [docs/protocol.md](docs/protocol.md) — control plane, Envelope, message catalog
|
||||||
|
4. [docs/voice.md](docs/voice.md) — UDP media, Opus, multi-stream, two-sided NR, VAD/PTT
|
||||||
|
5. [docs/security.md](docs/security.md) — mandatory encryption, TLS+AEAD, accounts, threat model
|
||||||
|
6. [docs/tech-stack.md](docs/tech-stack.md) — libraries, permissive-license rule, tooling
|
||||||
|
7. [docs/deployment.md](docs/deployment.md) — zero-config self-host (Docker / binary / source)
|
||||||
|
8. [docs/roadmap.md](docs/roadmap.md) — milestones + resolved decisions
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Keeping track of progress
|
||||||
|
|
||||||
|
**[`PROGRESS.md`](PROGRESS.md) is the living status file.** When you finish a task, check it
|
||||||
|
off there and note the next step, so the next agent can pick up instantly. Treat it as part of
|
||||||
|
the work, not an afterthought — update it in the same commit as the code.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## House rules (hard constraints)
|
||||||
|
|
||||||
|
- **A clean compile is the floor, not the goal.** "Done" = the milestone's observable exit
|
||||||
|
criterion in [`docs/roadmap.md`](docs/roadmap.md) passes (e.g. M1 = two `vccli` actually chat
|
||||||
|
over TLS). Encode it as a test. See [`AGENTS.md`](AGENTS.md).
|
||||||
|
- **No GPL/LGPL dependencies, ever** (closed-source redistribution is a goal). docs/tech-stack.md §5.
|
||||||
|
- **Encryption is mandatory** — never add a plaintext transport path. docs/security.md.
|
||||||
|
- **Real-time audio threads** never allocate, lock, or block. docs/architecture.md §3.
|
||||||
|
- **Keep docs + code in sync.** Changing a wire format (`voicecat.proto`) or the C ABI
|
||||||
|
(`voicecat.h`) is a deliberate, versioned act — update the doc in the same commit (protocol.md §8).
|
||||||
|
- Every commit must build (`cmake --build --preset dev`) and pass `ctest --preset dev`.
|
||||||
50
CMakeLists.txt
Normal file
50
CMakeLists.txt
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.25)
|
||||||
|
|
||||||
|
project(voicecat
|
||||||
|
VERSION 0.0.1
|
||||||
|
DESCRIPTION "Self-hosted native voice & text chat (see docs/)"
|
||||||
|
LANGUAGES CXX)
|
||||||
|
|
||||||
|
# ── Options ───────────────────────────────────────────────────────────────────
|
||||||
|
# The M0 skeleton compiles with NO third-party dependencies: every subsystem is a
|
||||||
|
# stub that returns VC_ERR_NOT_IMPLEMENTED. As each subsystem is built out, flip
|
||||||
|
# VOICECAT_USE_VCPKG_DEPS=ON so CMake pulls the real libraries (mbedTLS, libsodium,
|
||||||
|
# opus, protobuf, ...) via the vcpkg toolchain (see vcpkg.json / docs/tech-stack.md).
|
||||||
|
option(VOICECAT_USE_VCPKG_DEPS "Link real third-party deps via vcpkg" OFF)
|
||||||
|
option(VOICECAT_BUILD_SERVER "Build voicecat-server" ON)
|
||||||
|
option(VOICECAT_BUILD_TOOLS "Build the vccli headless test client" ON)
|
||||||
|
option(VOICECAT_BUILD_TESTS "Build tests" ON)
|
||||||
|
option(VOICECAT_BUILD_SHARED "Build libvoicecat as a shared library" OFF)
|
||||||
|
|
||||||
|
# ── Global settings ───────────────────────────────────────────────────────────
|
||||||
|
set(CMAKE_CXX_STANDARD 20)
|
||||||
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
|
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||||
|
|
||||||
|
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
|
||||||
|
set(CMAKE_BUILD_TYPE Debug CACHE STRING "" FORCE)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
|
||||||
|
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
|
||||||
|
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
|
||||||
|
|
||||||
|
# ── Targets ───────────────────────────────────────────────────────────────────
|
||||||
|
add_subdirectory(core)
|
||||||
|
|
||||||
|
if(VOICECAT_BUILD_SERVER)
|
||||||
|
add_subdirectory(server)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(VOICECAT_BUILD_TOOLS)
|
||||||
|
add_subdirectory(tools/vccli)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(VOICECAT_BUILD_TESTS)
|
||||||
|
enable_testing()
|
||||||
|
add_subdirectory(tests)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
message(STATUS "VoiceCat ${PROJECT_VERSION} configured "
|
||||||
|
"(vcpkg deps: ${VOICECAT_USE_VCPKG_DEPS}, "
|
||||||
|
"server: ${VOICECAT_BUILD_SERVER}, tools: ${VOICECAT_BUILD_TOOLS})")
|
||||||
42
CMakePresets.json
Normal file
42
CMakePresets.json
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"version": 6,
|
||||||
|
"cmakeMinimumRequired": { "major": 3, "minor": 25, "patch": 0 },
|
||||||
|
"configurePresets": [
|
||||||
|
{
|
||||||
|
"name": "dev",
|
||||||
|
"displayName": "Dev (skeleton, no third-party deps)",
|
||||||
|
"description": "Builds the stub skeleton with just a compiler. Works out of the box; no vcpkg required.",
|
||||||
|
"generator": "Ninja",
|
||||||
|
"binaryDir": "${sourceDir}/build/dev",
|
||||||
|
"cacheVariables": {
|
||||||
|
"CMAKE_BUILD_TYPE": "Debug",
|
||||||
|
"VOICECAT_USE_VCPKG_DEPS": "OFF"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "vcpkg-base",
|
||||||
|
"hidden": true,
|
||||||
|
"description": "Shared base for presets that link real deps via vcpkg. Requires VCPKG_ROOT in the environment.",
|
||||||
|
"generator": "Ninja",
|
||||||
|
"toolchainFile": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake",
|
||||||
|
"cacheVariables": { "VOICECAT_USE_VCPKG_DEPS": "ON" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "server-release",
|
||||||
|
"inherits": "vcpkg-base",
|
||||||
|
"displayName": "Server (release, real deps)",
|
||||||
|
"binaryDir": "${sourceDir}/build/server-release",
|
||||||
|
"cacheVariables": {
|
||||||
|
"CMAKE_BUILD_TYPE": "Release",
|
||||||
|
"VOICECAT_BUILD_TOOLS": "ON"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"buildPresets": [
|
||||||
|
{ "name": "dev", "configurePreset": "dev" },
|
||||||
|
{ "name": "server-release", "configurePreset": "server-release" }
|
||||||
|
],
|
||||||
|
"testPresets": [
|
||||||
|
{ "name": "dev", "configurePreset": "dev", "output": { "outputOnFailure": true } }
|
||||||
|
]
|
||||||
|
}
|
||||||
88
PROGRESS.md
Normal file
88
PROGRESS.md
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
# PROGRESS — VoiceCat
|
||||||
|
|
||||||
|
Living status. **Update this file in the same commit as your work** so the next agent picks
|
||||||
|
up instantly. Newest status at the top.
|
||||||
|
|
||||||
|
- **Date convention:** ISO (YYYY-MM-DD).
|
||||||
|
- Statuses: `[ ]` not started · `[~]` in progress · `[x]` done.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ▶ Where we left off / next action
|
||||||
|
|
||||||
|
- **Done:** design docs (`docs/`) + **M0 skeleton** — repo builds, links, and passes the
|
||||||
|
smoke test with no third-party deps.
|
||||||
|
- **Next:** start **M1 — control plane**. First concrete task: implement protobuf + the
|
||||||
|
`[u32 length][Envelope]` frame codec in `core/src/protocol/` and round-trip an `Envelope`
|
||||||
|
in a test (see M1 checklist below and [`AGENTS.md`](AGENTS.md) "Suggested first steps").
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Milestones (see [docs/roadmap.md](docs/roadmap.md) for full detail)
|
||||||
|
|
||||||
|
- [x] **M0 — Scaffolding** ✓ complete
|
||||||
|
- [~] **M1 — Control plane** (TCP/TLS, auth, channels, ephemeral text) ← current
|
||||||
|
- [ ] **M2 — Voice, single stream** (UDP, Opus, jitter buffer, APM send-side, VAD/PTT)
|
||||||
|
- [ ] **M3 — Multi-stream & per-channel tuning** (screen audio, listener-side per-user NR)
|
||||||
|
- [ ] **M4 — Native clients** (Windows C#, macOS/iOS Swift)
|
||||||
|
- [ ] **M5 — Moderation, polish, beyond** (perms, bans, DRED; then file transfer, E2EE, …)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## M0 — Scaffolding ✓ (completed)
|
||||||
|
|
||||||
|
- [x] Repo layout (`core/ server/ tools/ clients/ tests/`), CMake + presets, vcpkg manifest.
|
||||||
|
- [x] C ABI header `core/include/voicecat.h` (full surface, stubbed).
|
||||||
|
- [x] Protocol source-of-truth `core/proto/voicecat.proto` (matches docs/protocol.md).
|
||||||
|
- [x] Core stubs for all six subsystems (net/crypto/codec/protocol/session/audio) + `vc_client`.
|
||||||
|
- [x] `voicecat-server` (arg parsing, config, stub run) and `vccli` (drives the C ABI).
|
||||||
|
- [x] CTest **smoke test** asserting the C ABI contract (not just "it compiles").
|
||||||
|
- [x] `.gitattributes` (LF), `.gitignore`, `.clang-format`, onboarding docs.
|
||||||
|
- **Verified:** `cmake --preset dev && cmake --build --preset dev && ctest --preset dev` → green.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## M1 — Control plane (current)
|
||||||
|
|
||||||
|
**Exit criterion (definition of done):** two `vccli` instances connect to a real
|
||||||
|
`voicecat-server` over **TLS 1.3**, authenticate (guest + admin-provisioned account), browse
|
||||||
|
the channel tree, and exchange channel + private text messages. Encode this as an integration
|
||||||
|
test driving two clients.
|
||||||
|
|
||||||
|
Tasks (rough order — see [docs/protocol.md](docs/protocol.md), [docs/security.md](docs/security.md)):
|
||||||
|
|
||||||
|
- [ ] Turn on vcpkg deps; set a real `builtin-baseline` in `vcpkg.json`; wire `find_package`
|
||||||
|
for protobuf in `core/CMakeLists.txt` and `protobuf_generate` for `voicecat.proto`.
|
||||||
|
- [ ] `protocol/`: implement the `[u32 length][Envelope]` `FrameCodec` (+ oversized-frame
|
||||||
|
guard). **Test:** round-trip an `Envelope` through feed/emit.
|
||||||
|
- [ ] `net/`: plain TCP connect/accept via Asio; then wrap with **TLS 1.3 (mbedTLS)** in
|
||||||
|
`crypto/`. **Test:** `vccli` ↔ `voicecat-server` complete a TLS handshake.
|
||||||
|
- [ ] Handshake: `ClientHello`/`ServerHello` with version + feature negotiation.
|
||||||
|
- [ ] Server identity: generate/persist Ed25519 key + self-signed cert on first run; expose
|
||||||
|
fingerprint; client TOFU pin. (docs/security.md §1)
|
||||||
|
- [ ] Auth: `AuthRequest` → `AuthResult`; guest path + Argon2id password verify (libsodium);
|
||||||
|
SQLite accounts; `voicecat-admin` account add/reset/del/list. (docs/security.md §4)
|
||||||
|
- [ ] Session model: channel tree snapshot (`ServerStateSnapshot`) + `ChannelEvent`/`UserEvent`
|
||||||
|
deltas; join/leave; create/edit/delete (permission-gated).
|
||||||
|
- [ ] Text: ephemeral relay of channel + private messages with acks (no history). (protocol.md §5)
|
||||||
|
- [ ] Wire the C ABI: `vc_connect/authenticate_*/join_channel/send_text` drive the above and
|
||||||
|
emit `vc_event`s; `vccli` exercises them.
|
||||||
|
- [ ] **Integration test:** two `vccli` chat through the server over TLS. ← M1 exit.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Decisions log
|
||||||
|
|
||||||
|
All architecture/scope decisions are settled and recorded in
|
||||||
|
[docs/roadmap.md §2 "Resolved decisions"](docs/roadmap.md) and reflected across `docs/`.
|
||||||
|
If you make a *new* decision, record it there and link it here.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## How to update this file
|
||||||
|
|
||||||
|
1. Check off tasks as you complete them; flip a milestone to `[x]` only when its **exit
|
||||||
|
criterion test** passes.
|
||||||
|
2. Keep the **"Where we left off / next action"** block at the top accurate — it's the first
|
||||||
|
thing the next agent reads.
|
||||||
|
3. When you start a milestone, copy its task list from `docs/roadmap.md` into a section here.
|
||||||
58
README.md
Normal file
58
README.md
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
# VoiceCat
|
||||||
|
|
||||||
|
Self-hosted, native voice & text chat in the spirit of classic TeamSpeak / Mumble —
|
||||||
|
channel-based voice, channel + private text, one server you run yourself. Plain **TCP**
|
||||||
|
(control) and **UDP** (media), no WebRTC. Encrypted by default. A shared **C++ core**
|
||||||
|
(`libvoicecat`) drives native clients (Swift on macOS/iOS, C# on Windows) and the server.
|
||||||
|
|
||||||
|
> **Status: pre-implementation.** The design is complete in [`docs/`](docs/). The code is an
|
||||||
|
> M0 **skeleton** — it compiles and links, but every subsystem is a stub. See
|
||||||
|
> [`AGENTS.md`](AGENTS.md) to start building, and [`docs/roadmap.md`](docs/roadmap.md) for the
|
||||||
|
> milestones.
|
||||||
|
|
||||||
|
## Read the design first
|
||||||
|
|
||||||
|
The [`docs/`](docs/) folder is the source of truth. Start at [`docs/README.md`](docs/README.md),
|
||||||
|
then `architecture` → `protocol` → `voice` → `security` → `tech-stack` → `deployment` →
|
||||||
|
`roadmap`.
|
||||||
|
|
||||||
|
## Build the skeleton (no dependencies needed yet)
|
||||||
|
|
||||||
|
The M0 skeleton builds with just a C++20 compiler + CMake + Ninja — **no vcpkg, no
|
||||||
|
third-party libraries**, because every subsystem is currently a stub.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cmake --preset dev
|
||||||
|
cmake --build --preset dev
|
||||||
|
ctest --preset dev # runs the smoke test (links the core, calls the C ABI)
|
||||||
|
```
|
||||||
|
|
||||||
|
Artifacts land in `build/dev/bin/` (`voicecat-server`, `vccli`).
|
||||||
|
|
||||||
|
When you start implementing a subsystem that needs real libraries, build with vcpkg deps:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# one-time: git clone https://github.com/microsoft/vcpkg && ./vcpkg/bootstrap-vcpkg.sh
|
||||||
|
export VCPKG_ROOT=/path/to/vcpkg # set VCPKG_ROOT (works on Linux/macOS/Windows)
|
||||||
|
cmake --preset server-release # auto-installs deps from vcpkg.json
|
||||||
|
cmake --build --preset server-release
|
||||||
|
```
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
docs/ design spec (read this)
|
||||||
|
core/ libvoicecat — the shared C++ core
|
||||||
|
include/ voicecat.h (the C ABI all clients call)
|
||||||
|
proto/ voicecat.proto (control-plane wire format, source of truth)
|
||||||
|
src/ net/ crypto/ codec/ protocol/ session/ audio/ (stubs today)
|
||||||
|
server/ voicecat-server (headless; links the core)
|
||||||
|
tools/vccli/ headless test client — drives the protocol from M1 on
|
||||||
|
clients/ apple/ (Swift, M4) windows/ (C#, M4) — placeholders for now
|
||||||
|
tests/ CTest targets
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Permissive-only dependencies (no GPL/LGPL) so the project can be redistributed freely,
|
||||||
|
including closed-source. Project license: TBD (see [`docs/tech-stack.md`](docs/tech-stack.md) §5).
|
||||||
19
clients/apple/README.md
Normal file
19
clients/apple/README.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# Apple client (macOS + iOS) — placeholder
|
||||||
|
|
||||||
|
Built in **M4** (see [`docs/roadmap.md`](../../docs/roadmap.md)). Swift + SwiftUI, consuming
|
||||||
|
`libvoicecat` through the C ABI ([`core/include/voicecat.h`](../../core/include/voicecat.h)).
|
||||||
|
|
||||||
|
Planned shape (see [`docs/architecture.md`](../../docs/architecture.md) §4 and
|
||||||
|
[`docs/tech-stack.md`](../../docs/tech-stack.md) §2):
|
||||||
|
|
||||||
|
- A Swift Package wrapping the core as an **XCFramework** (macOS + iOS device + simulator).
|
||||||
|
- A module map exposing `voicecat.h` to Swift (Swift can also use C++ interop directly, but
|
||||||
|
the C ABI is the stable contract).
|
||||||
|
- SwiftUI app target for macOS and iOS.
|
||||||
|
- **iOS audio:** app owns `AVAudioSession` (`.playAndRecord` / `.voiceChat`), mic permission,
|
||||||
|
interruption/route handling, calling `vc_audio_*` hooks on the core.
|
||||||
|
- **iOS screen/system audio (`SCREEN_AUDIO`):** a **ReplayKit Broadcast Upload Extension**
|
||||||
|
capturing `RPSampleBufferType.audioApp`, linking a minimal core slice, sharing session
|
||||||
|
state via an **App Group** ([`docs/voice.md`](../../docs/voice.md) §9).
|
||||||
|
|
||||||
|
Nothing here yet — the core must reach M2 (working voice) before the GUI is worth building.
|
||||||
18
clients/windows/README.md
Normal file
18
clients/windows/README.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Windows client — placeholder
|
||||||
|
|
||||||
|
Built in **M4** (see [`docs/roadmap.md`](../../docs/roadmap.md)). C# / .NET 8+, consuming
|
||||||
|
`libvoicecat` through the C ABI ([`core/include/voicecat.h`](../../core/include/voicecat.h)).
|
||||||
|
|
||||||
|
Planned shape (see [`docs/architecture.md`](../../docs/architecture.md) §4 and
|
||||||
|
[`docs/tech-stack.md`](../../docs/tech-stack.md) §2):
|
||||||
|
|
||||||
|
- A .NET solution with a P/Invoke interop layer over the C ABI using **`LibraryImport`**
|
||||||
|
(source-generated, .NET 7+). Build `libvoicecat` as a **shared library**
|
||||||
|
(`-DVOICECAT_BUILD_SHARED=ON`) so the DLL sits beside the app.
|
||||||
|
- The `on_event` callback marshaled as a function pointer (`[UnmanagedCallersOnly]`) to avoid
|
||||||
|
delegate-lifetime issues; keep the interface "chunky" to minimize managed↔native crossings.
|
||||||
|
- UI in **WinUI 3** (most native) or **Avalonia** (if a single C# desktop UI is wanted later).
|
||||||
|
- Audio (capture/playback, WASAPI loopback for `SCREEN_AUDIO`) is handled inside the core; the
|
||||||
|
C# layer only drives device selection, meters, and the VAD/PTT + per-user NR controls.
|
||||||
|
|
||||||
|
Nothing here yet — the core must reach M2 (working voice) before the GUI is worth building.
|
||||||
43
core/CMakeLists.txt
Normal file
43
core/CMakeLists.txt
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# libvoicecat — the shared C++ core (docs/architecture.md).
|
||||||
|
# Sources are globbed so adding a stub under src/<subsystem>/ needs no CMake edit.
|
||||||
|
file(GLOB_RECURSE VOICECAT_SOURCES CONFIGURE_DEPENDS
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp")
|
||||||
|
|
||||||
|
if(VOICECAT_BUILD_SHARED)
|
||||||
|
add_library(voicecat SHARED ${VOICECAT_SOURCES})
|
||||||
|
else()
|
||||||
|
add_library(voicecat STATIC ${VOICECAT_SOURCES})
|
||||||
|
# Static consumers must see VC_API as empty (no dllimport).
|
||||||
|
target_compile_definitions(voicecat PUBLIC VOICECAT_STATIC)
|
||||||
|
endif()
|
||||||
|
add_library(voicecat::voicecat ALIAS voicecat)
|
||||||
|
|
||||||
|
target_include_directories(voicecat
|
||||||
|
PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include
|
||||||
|
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src)
|
||||||
|
|
||||||
|
target_compile_definitions(voicecat PRIVATE VOICECAT_BUILDING)
|
||||||
|
target_compile_features(voicecat PUBLIC cxx_std_20)
|
||||||
|
|
||||||
|
set_target_properties(voicecat PROPERTIES
|
||||||
|
C_VISIBILITY_PRESET hidden
|
||||||
|
CXX_VISIBILITY_PRESET hidden
|
||||||
|
VISIBILITY_INLINES_HIDDEN ON)
|
||||||
|
|
||||||
|
if(VOICECAT_USE_VCPKG_DEPS)
|
||||||
|
# Wire real dependencies here as each subsystem is implemented. Example (uncomment
|
||||||
|
# the ones a subsystem needs; see docs/tech-stack.md and core/proto for protobuf):
|
||||||
|
# find_package(unofficial-sodium CONFIG REQUIRED) # crypto/ (libsodium)
|
||||||
|
# find_package(MbedTLS CONFIG REQUIRED) # crypto/ (TLS 1.3)
|
||||||
|
# find_package(Opus CONFIG REQUIRED) # codec/
|
||||||
|
# find_package(protobuf CONFIG REQUIRED) # protocol/
|
||||||
|
# find_package(asio CONFIG REQUIRED) # net/
|
||||||
|
# find_package(unofficial-sqlite3 CONFIG REQUIRED) # server persistence
|
||||||
|
# find_package(spdlog CONFIG REQUIRED)
|
||||||
|
# target_link_libraries(voicecat PRIVATE Opus::opus protobuf::libprotobuf ...)
|
||||||
|
#
|
||||||
|
# protobuf codegen (when protocol/ is implemented):
|
||||||
|
# find_package(Protobuf CONFIG REQUIRED)
|
||||||
|
# protobuf_generate(TARGET voicecat PROTOS proto/voicecat.proto LANGUAGE cpp)
|
||||||
|
message(STATUS "voicecat: deps ON — add find_package()/link calls here as subsystems land")
|
||||||
|
endif()
|
||||||
223
core/include/voicecat.h
Normal file
223
core/include/voicecat.h
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* STATUS: M0 skeleton. Implementations live in core/src and currently return
|
||||||
|
* VC_ERR_NOT_IMPLEMENTED. The shapes below are the contract to build against.
|
||||||
|
*/
|
||||||
|
#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
|
||||||
|
#define VOICECAT_VERSION_PATCH 1
|
||||||
|
|
||||||
|
/* The control-protocol version this build speaks (docs/protocol.md §4). */
|
||||||
|
#define VOICECAT_PROTOCOL_VERSION 1
|
||||||
|
|
||||||
|
/* ── Result codes ─────────────────────────────────────────────────────────── */
|
||||||
|
typedef enum vc_result {
|
||||||
|
VC_OK = 0,
|
||||||
|
VC_ERR_NOT_IMPLEMENTED = 1, /* skeleton stub */
|
||||||
|
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,
|
||||||
|
} 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,
|
||||||
|
} 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 */
|
||||||
|
} vc_event_type;
|
||||||
|
|
||||||
|
/* ── 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;
|
||||||
|
} 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" */
|
||||||
|
} vc_stream_desc;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
/* 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 ─────────────────────────────────────────────────────────────── */
|
||||||
|
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);
|
||||||
|
|
||||||
|
/* ── 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);
|
||||||
|
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);
|
||||||
|
|
||||||
|
/* 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,
|
||||||
|
float gain, int muted, int noise_reduction);
|
||||||
|
|
||||||
|
/* ── 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);
|
||||||
|
|
||||||
|
#if defined(__cplusplus)
|
||||||
|
} /* extern "C" */
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* VOICECAT_H */
|
||||||
238
core/proto/voicecat.proto
Normal file
238
core/proto/voicecat.proto
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
// VoiceCat control-plane wire format. SOURCE OF TRUTH for the protocol.
|
||||||
|
// Spec: docs/protocol.md. Each TCP/TLS frame is [u32 length][encoded Envelope].
|
||||||
|
// Media frames (voice) are NOT here — they use the fixed binary header in docs/voice.md §2.
|
||||||
|
//
|
||||||
|
// Extensibility rules (docs/protocol.md §8): never reuse/renumber tags; new oneof arms and
|
||||||
|
// fields are additive; gate new features behind capability strings in ClientHello/ServerHello.
|
||||||
|
syntax = "proto3";
|
||||||
|
package voicecat.v1;
|
||||||
|
|
||||||
|
// ── Envelope ──────────────────────────────────────────────────────────────────
|
||||||
|
message Envelope {
|
||||||
|
// Nonzero on a request; echoed in the matching response for correlation. 0 = unsolicited.
|
||||||
|
uint64 request_id = 1;
|
||||||
|
|
||||||
|
oneof body {
|
||||||
|
// Session / handshake (tags 10–19)
|
||||||
|
ClientHello client_hello = 10;
|
||||||
|
ServerHello server_hello = 11;
|
||||||
|
AuthRequest auth_request = 12;
|
||||||
|
AuthResult auth_result = 13;
|
||||||
|
Disconnect disconnect = 14;
|
||||||
|
Ping ping = 15;
|
||||||
|
Pong pong = 16;
|
||||||
|
|
||||||
|
// State sync (20–29)
|
||||||
|
ServerStateSnapshot server_state = 20;
|
||||||
|
ChannelEvent channel_event = 21;
|
||||||
|
UserEvent user_event = 22;
|
||||||
|
SubscribeRequest subscribe = 23;
|
||||||
|
|
||||||
|
// Channel operations (30–39)
|
||||||
|
JoinChannelRequest join_channel = 30;
|
||||||
|
JoinChannelResult join_channel_result = 31;
|
||||||
|
LeaveChannelRequest leave_channel = 32;
|
||||||
|
CreateChannelRequest create_channel = 33;
|
||||||
|
EditChannelRequest edit_channel = 34;
|
||||||
|
DeleteChannelRequest delete_channel = 35;
|
||||||
|
MoveUserRequest move_user = 36;
|
||||||
|
GenericResult generic_result = 37;
|
||||||
|
|
||||||
|
// Voice signaling — media is on UDP (40–49)
|
||||||
|
StreamAnnounce stream_announce = 40;
|
||||||
|
StreamAnnounceResult stream_announce_result = 41;
|
||||||
|
StreamStop stream_stop = 42;
|
||||||
|
StreamStateUpdate stream_state = 43;
|
||||||
|
UdpBinding udp_binding = 44;
|
||||||
|
|
||||||
|
// Text (50–59)
|
||||||
|
TextMessage text_message = 50;
|
||||||
|
TextMessageAck text_message_ack = 51;
|
||||||
|
TypingIndicator typing = 52;
|
||||||
|
|
||||||
|
// Moderation / permissions (60–69)
|
||||||
|
KickRequest kick = 60;
|
||||||
|
BanRequest ban = 61;
|
||||||
|
SetPermissionRequest set_permission = 62;
|
||||||
|
|
||||||
|
// Admin account management — privileged; accounts are admin-provisioned (70–79)
|
||||||
|
CreateAccountRequest create_account = 70;
|
||||||
|
ResetPasswordRequest reset_password = 71;
|
||||||
|
DeleteAccountRequest delete_account = 72;
|
||||||
|
ListAccountsRequest list_accounts = 73;
|
||||||
|
|
||||||
|
// Future families: file transfer = 100–109. Extension escape hatch = 200+.
|
||||||
|
Extension extension = 200;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Enums ──────────────────────────────────────────────────────────────────────
|
||||||
|
enum ChannelType { CHANNEL_PERMANENT = 0; CHANNEL_TEMPORARY = 1; }
|
||||||
|
enum ChannelMode { MODE_MONO = 0; MODE_STEREO = 1; }
|
||||||
|
enum StreamKind { STREAM_MIC = 0; STREAM_SCREEN_AUDIO = 1; STREAM_AUX_DEVICE = 2; }
|
||||||
|
enum TextScope { TEXT_CHANNEL = 0; TEXT_PRIVATE = 1; TEXT_SERVER = 2; }
|
||||||
|
enum OpusApplication { OPUS_VOIP = 0; OPUS_AUDIO = 1; OPUS_LOWDELAY = 2; }
|
||||||
|
|
||||||
|
// ── Common types ────────────────────────────────────────────────────────────────
|
||||||
|
message AudioConfig {
|
||||||
|
uint32 codec = 1; // 0 = OPUS
|
||||||
|
ChannelMode mode = 2;
|
||||||
|
uint32 sample_rate = 3; // 48000 recommended
|
||||||
|
uint32 bitrate_bps = 4;
|
||||||
|
uint32 frame_ms = 5; // 2.5/5/10/20/40/60
|
||||||
|
OpusApplication application = 6;
|
||||||
|
bool fec = 7;
|
||||||
|
uint32 expected_packet_loss = 8; // %
|
||||||
|
bool dtx = 9;
|
||||||
|
uint32 complexity = 10; // 0..10
|
||||||
|
}
|
||||||
|
|
||||||
|
message StreamInfo {
|
||||||
|
uint32 stream_id = 1; // unique within the user
|
||||||
|
uint32 ssrc = 2; // media-plane id assigned by server
|
||||||
|
StreamKind kind = 3;
|
||||||
|
AudioConfig audio = 4;
|
||||||
|
string label = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Channel {
|
||||||
|
uint32 id = 1;
|
||||||
|
uint32 parent_id = 2; // 0 = root
|
||||||
|
string name = 3;
|
||||||
|
string topic = 4;
|
||||||
|
bool password_protected = 5;
|
||||||
|
uint32 max_users = 6;
|
||||||
|
ChannelType type = 7;
|
||||||
|
AudioConfig audio = 8;
|
||||||
|
int32 order = 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
message User {
|
||||||
|
uint32 id = 1;
|
||||||
|
string nickname = 2;
|
||||||
|
bool is_guest = 3;
|
||||||
|
uint32 channel_id = 4;
|
||||||
|
bool self_mic_muted = 5;
|
||||||
|
bool self_deafened = 6;
|
||||||
|
bool server_muted = 7;
|
||||||
|
repeated StreamInfo streams = 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Permissions {
|
||||||
|
// Minimal v1 flag set; the moderation milestone expands this. Server-side is authoritative.
|
||||||
|
bool can_create_temp_channel = 1;
|
||||||
|
bool can_kick = 2;
|
||||||
|
bool can_ban = 3;
|
||||||
|
bool can_move_users = 4;
|
||||||
|
bool can_admin_accounts = 5;
|
||||||
|
bool is_admin = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Session / handshake ─────────────────────────────────────────────────────────
|
||||||
|
message ClientHello {
|
||||||
|
uint32 proto_version = 1;
|
||||||
|
repeated string features = 2; // "opus", "fec", "screen-audio", ...
|
||||||
|
string client_name = 3;
|
||||||
|
string client_version = 4;
|
||||||
|
string preferred_locale = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ServerHello {
|
||||||
|
uint32 proto_version = 1;
|
||||||
|
repeated string features = 2;
|
||||||
|
string server_name = 3;
|
||||||
|
string server_version = 4;
|
||||||
|
repeated string auth_methods = 5; // "guest", "password"
|
||||||
|
uint32 udp_port = 6;
|
||||||
|
bytes server_identity_fingerprint = 7; // Ed25519 fp for TOFU
|
||||||
|
}
|
||||||
|
|
||||||
|
message GuestAuth { string nickname = 1; }
|
||||||
|
message PasswordAuth { string username = 1; string password = 2; }
|
||||||
|
|
||||||
|
message AuthRequest {
|
||||||
|
oneof method {
|
||||||
|
GuestAuth guest = 1;
|
||||||
|
PasswordAuth password = 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message AuthResult {
|
||||||
|
bool ok = 1;
|
||||||
|
string error = 2;
|
||||||
|
uint64 session_id = 3;
|
||||||
|
User self = 4;
|
||||||
|
Permissions permissions = 5;
|
||||||
|
bytes udp_token = 6; // bind the UDP 5-tuple with this (security.md §3)
|
||||||
|
}
|
||||||
|
|
||||||
|
message Disconnect { uint32 code = 1; string reason = 2; }
|
||||||
|
message Ping { uint64 nonce = 1; }
|
||||||
|
message Pong { uint64 nonce = 1; }
|
||||||
|
|
||||||
|
// ── State sync ──────────────────────────────────────────────────────────────────
|
||||||
|
message ServerStateSnapshot {
|
||||||
|
repeated Channel channels = 1;
|
||||||
|
repeated User users = 2;
|
||||||
|
}
|
||||||
|
message ChannelEvent {
|
||||||
|
enum Kind { CREATED = 0; UPDATED = 1; DELETED = 2; }
|
||||||
|
Kind kind = 1;
|
||||||
|
Channel channel = 2;
|
||||||
|
uint32 deleted_id = 3;
|
||||||
|
}
|
||||||
|
message UserEvent {
|
||||||
|
enum Kind { JOINED = 0; LEFT = 1; UPDATED = 2; }
|
||||||
|
Kind kind = 1;
|
||||||
|
User user = 2;
|
||||||
|
uint32 left_id = 3;
|
||||||
|
}
|
||||||
|
message SubscribeRequest { repeated uint32 channel_ids = 1; bool presence = 2; }
|
||||||
|
|
||||||
|
// ── Channel operations ──────────────────────────────────────────────────────────
|
||||||
|
message JoinChannelRequest { uint32 channel_id = 1; string password = 2; }
|
||||||
|
message JoinChannelResult {
|
||||||
|
bool ok = 1; string error = 2;
|
||||||
|
uint32 channel_id = 3;
|
||||||
|
repeated User members = 4;
|
||||||
|
AudioConfig audio = 5; // authoritative channel Opus params
|
||||||
|
}
|
||||||
|
message LeaveChannelRequest {}
|
||||||
|
message CreateChannelRequest { Channel channel = 1; string password = 2; }
|
||||||
|
message EditChannelRequest { Channel channel = 1; string password = 2; }
|
||||||
|
message DeleteChannelRequest { uint32 channel_id = 1; }
|
||||||
|
message MoveUserRequest { uint32 user_id = 1; uint32 channel_id = 2; }
|
||||||
|
message GenericResult { bool ok = 1; uint32 code = 2; string message = 3; }
|
||||||
|
|
||||||
|
// ── Voice signaling ─────────────────────────────────────────────────────────────
|
||||||
|
message StreamAnnounce { StreamKind kind = 1; AudioConfig requested_audio = 2; string label = 3; }
|
||||||
|
message StreamAnnounceResult{ bool ok = 1; string error = 2; uint32 stream_id = 3; uint32 ssrc = 4; AudioConfig effective_audio = 5; }
|
||||||
|
message StreamStop { uint32 stream_id = 1; }
|
||||||
|
message StreamStateUpdate { uint32 user_id = 1; uint32 stream_id = 2; bool muted = 3; bool talking = 4; }
|
||||||
|
message UdpBinding { bytes udp_token = 1; bool ack = 2; }
|
||||||
|
|
||||||
|
// ── Text (ephemeral — server does not persist history, docs/protocol.md §5) ──────
|
||||||
|
message TextMessage {
|
||||||
|
TextScope scope = 1;
|
||||||
|
uint32 target_id = 2; // channel_id or user_id per scope
|
||||||
|
uint32 sender_id = 3; // set by server on relay
|
||||||
|
string body = 4; // UTF-8, server-bounded length
|
||||||
|
uint64 sent_at_unix_ms = 5; // server timestamp on relay
|
||||||
|
string client_msg_id = 6; // echoed in ack (dedup)
|
||||||
|
}
|
||||||
|
message TextMessageAck { string client_msg_id = 1; bool ok = 2; }
|
||||||
|
message TypingIndicator { TextScope scope = 1; uint32 target_id = 2; uint32 user_id = 3; }
|
||||||
|
|
||||||
|
// ── Moderation / permissions ─────────────────────────────────────────────────────
|
||||||
|
message KickRequest { uint32 user_id = 1; string reason = 2; }
|
||||||
|
message BanRequest { uint32 user_id = 1; string reason = 2; uint64 expires_unix_ms = 3; }
|
||||||
|
message SetPermissionRequest { uint32 user_id = 1; Permissions permissions = 2; }
|
||||||
|
|
||||||
|
// ── Admin account management (privileged) ────────────────────────────────────────
|
||||||
|
message CreateAccountRequest { string username = 1; string password = 2; }
|
||||||
|
message ResetPasswordRequest { string username = 1; string new_password = 2; }
|
||||||
|
message DeleteAccountRequest { string username = 1; }
|
||||||
|
message ListAccountsRequest {}
|
||||||
|
|
||||||
|
// ── Extension escape hatch ───────────────────────────────────────────────────────
|
||||||
|
message Extension { string ns = 1; bytes payload = 2; }
|
||||||
8
core/src/audio/audio_engine.cpp
Normal file
8
core/src/audio/audio_engine.cpp
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
#include "audio/audio_engine.h"
|
||||||
|
|
||||||
|
namespace voicecat::audio {
|
||||||
|
|
||||||
|
// M0 stub. Capture/playback (miniaudio), APM DSP, jitter buffer, and mixer land in M2/M3.
|
||||||
|
// See docs/voice.md §8–11.
|
||||||
|
|
||||||
|
} // namespace voicecat::audio
|
||||||
40
core/src/audio/audio_engine.h
Normal file
40
core/src/audio/audio_engine.h
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
* audio/audio_engine.h — capture/playback + DSP + jitter buffer + mixer.
|
||||||
|
*
|
||||||
|
* Design: docs/voice.md §8–11. Real-time path:
|
||||||
|
* capture(miniaudio) → APM(AEC/NS/AGC/VAD, send-side) → Opus encode → ...
|
||||||
|
* ... → Opus decode → per-user recv NS (listener-chosen) → gain/mute → mix → playback
|
||||||
|
*
|
||||||
|
* REAL-TIME RULE: audio-callback threads never allocate, lock, or block (architecture.md §3).
|
||||||
|
*
|
||||||
|
* STATUS: M0 stub.
|
||||||
|
*/
|
||||||
|
#ifndef VOICECAT_AUDIO_AUDIO_ENGINE_H
|
||||||
|
#define VOICECAT_AUDIO_AUDIO_ENGINE_H
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
namespace voicecat::audio {
|
||||||
|
|
||||||
|
// Adaptive per-ssrc jitter buffer (voice.md §5). TODO(M2).
|
||||||
|
class JitterBuffer {
|
||||||
|
public:
|
||||||
|
uint32_t target_depth_ms() const { return target_depth_ms_; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
uint32_t target_depth_ms_ = 40;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Owns miniaudio capture/playback, the APM instances, codecs, jitter buffers, and the mixer.
|
||||||
|
class AudioEngine {
|
||||||
|
public:
|
||||||
|
// TODO(M2): start/stop capture+playback; push/pull frames via lock-free ring buffers.
|
||||||
|
bool running() const { return running_; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool running_ = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace voicecat::audio
|
||||||
|
|
||||||
|
#endif // VOICECAT_AUDIO_AUDIO_ENGINE_H
|
||||||
7
core/src/codec/opus_codec.cpp
Normal file
7
core/src/codec/opus_codec.cpp
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
#include "codec/opus_codec.h"
|
||||||
|
|
||||||
|
namespace voicecat::codec {
|
||||||
|
|
||||||
|
// M0 stub. Brought up in M2. See docs/voice.md §3–4.
|
||||||
|
|
||||||
|
} // namespace voicecat::codec
|
||||||
39
core/src/codec/opus_codec.h
Normal file
39
core/src/codec/opus_codec.h
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
* codec/opus_codec.h — Opus encode/decode (libopus 1.6).
|
||||||
|
*
|
||||||
|
* Design: docs/voice.md §3–4. Per-channel AudioConfig (mono/stereo, bitrate, frame size,
|
||||||
|
* FEC, DTX, complexity). The server relays Opus payloads unmodified (no transcode).
|
||||||
|
*
|
||||||
|
* STATUS: M0 stub.
|
||||||
|
*/
|
||||||
|
#ifndef VOICECAT_CODEC_OPUS_CODEC_H
|
||||||
|
#define VOICECAT_CODEC_OPUS_CODEC_H
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
namespace voicecat::codec {
|
||||||
|
|
||||||
|
struct OpusParams {
|
||||||
|
uint32_t sample_rate = 48000;
|
||||||
|
uint32_t bitrate_bps = 24000;
|
||||||
|
uint32_t frame_ms = 20;
|
||||||
|
bool stereo = false;
|
||||||
|
bool fec = true;
|
||||||
|
bool dtx = true;
|
||||||
|
uint32_t complexity = 10;
|
||||||
|
uint32_t expected_packet_loss = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class OpusEncoder {
|
||||||
|
public:
|
||||||
|
// TODO(M2): init(params); encode(pcm, frame) -> opus bytes.
|
||||||
|
};
|
||||||
|
|
||||||
|
class OpusDecoder {
|
||||||
|
public:
|
||||||
|
// TODO(M2): init(params); decode(opus, out_pcm); PLC on loss; FEC from next packet.
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace voicecat::codec
|
||||||
|
|
||||||
|
#endif // VOICECAT_CODEC_OPUS_CODEC_H
|
||||||
48
core/src/core/client.cpp
Normal file
48
core/src/core/client.cpp
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
#include "core/client.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
constexpr vc_result kStub = VC_ERR_NOT_IMPLEMENTED;
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
vc_client::vc_client(const vc_config& cfg, vc_callbacks cb) : cfg_(cfg), cb_(cb) {}
|
||||||
|
|
||||||
|
vc_client::~vc_client() = default;
|
||||||
|
|
||||||
|
void vc_client::emit(const vc_event& ev) const {
|
||||||
|
if (cb_.on_event != nullptr) {
|
||||||
|
cb_.on_event(cb_.user, &ev);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Connection & auth ────────────────────────────────────────────────────────
|
||||||
|
// TODO(M1): drive the TLS 1.3 control channel + handshake state machine here, updating
|
||||||
|
// state_ and emitting VC_EVENT_CONNECTION_STATE as it advances. docs/protocol.md §4.
|
||||||
|
vc_result vc_client::connect(const char*, uint16_t) { return kStub; }
|
||||||
|
vc_result vc_client::disconnect() { return kStub; }
|
||||||
|
vc_result vc_client::authenticate_guest(const char*) { return kStub; }
|
||||||
|
vc_result vc_client::authenticate_user(const char*, const char*) { return kStub; }
|
||||||
|
|
||||||
|
// ── Channels ─────────────────────────────────────────────────────────────────
|
||||||
|
vc_result vc_client::join_channel(uint32_t, const char*) { return kStub; }
|
||||||
|
vc_result vc_client::leave_channel() { return kStub; }
|
||||||
|
|
||||||
|
// ── Local media streams ──────────────────────────────────────────────────────
|
||||||
|
// TODO(M2/M3): allocate a stream id, announce it over the control channel, and start the
|
||||||
|
// capture→APM→Opus→AEAD→UDP pipeline. docs/voice.md.
|
||||||
|
vc_result vc_client::stream_start(const vc_stream_desc&, uint32_t*) { return kStub; }
|
||||||
|
vc_result vc_client::stream_stop(uint32_t) { return kStub; }
|
||||||
|
vc_result vc_client::set_input_device(uint32_t, const char*) { return kStub; }
|
||||||
|
vc_result vc_client::set_input_mode(vc_input_mode) { return kStub; }
|
||||||
|
vc_result vc_client::set_push_to_talk(bool) { return kStub; }
|
||||||
|
vc_result vc_client::set_self_mute(bool, bool) { return kStub; }
|
||||||
|
vc_result vc_client::set_remote_stream(uint32_t, uint32_t, float, bool, bool) { return kStub; }
|
||||||
|
|
||||||
|
// ── Text ─────────────────────────────────────────────────────────────────────
|
||||||
|
vc_result vc_client::send_text(vc_text_scope, uint32_t, const char*) { return kStub; }
|
||||||
|
|
||||||
|
// ── Devices ──────────────────────────────────────────────────────────────────
|
||||||
|
vc_result vc_client::list_devices(vc_device_kind, vc_device_list* out) {
|
||||||
|
out->items = nullptr;
|
||||||
|
out->count = 0;
|
||||||
|
return kStub;
|
||||||
|
}
|
||||||
53
core/src/core/client.h
Normal file
53
core/src/core/client.h
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
/*
|
||||||
|
* client.h — the implementation type behind the opaque `vc_client*` handle.
|
||||||
|
*
|
||||||
|
* M0 skeleton: holds config/callbacks/state and returns VC_ERR_NOT_IMPLEMENTED for
|
||||||
|
* everything that needs a subsystem. As subsystems land (docs/architecture.md §2), this
|
||||||
|
* class wires them together: a net transport, a protocol state machine, a session model,
|
||||||
|
* and an audio engine, plus the event thread that drains into vc_callbacks.on_event.
|
||||||
|
*/
|
||||||
|
#ifndef VOICECAT_CORE_CLIENT_H
|
||||||
|
#define VOICECAT_CORE_CLIENT_H
|
||||||
|
|
||||||
|
#include "voicecat.h"
|
||||||
|
|
||||||
|
struct vc_client {
|
||||||
|
vc_client(const vc_config& cfg, vc_callbacks cb);
|
||||||
|
~vc_client();
|
||||||
|
|
||||||
|
vc_client(const vc_client&) = delete;
|
||||||
|
vc_client& operator=(const vc_client&) = delete;
|
||||||
|
|
||||||
|
vc_result connect(const char* host, uint16_t port);
|
||||||
|
vc_result disconnect();
|
||||||
|
vc_result authenticate_guest(const char* nickname);
|
||||||
|
vc_result authenticate_user(const char* username, const char* password);
|
||||||
|
|
||||||
|
vc_result join_channel(uint32_t channel_id, const char* password);
|
||||||
|
vc_result leave_channel();
|
||||||
|
|
||||||
|
vc_result stream_start(const vc_stream_desc& desc, uint32_t* out_stream_id);
|
||||||
|
vc_result stream_stop(uint32_t stream_id);
|
||||||
|
vc_result set_input_device(uint32_t stream_id, const char* device_id);
|
||||||
|
vc_result set_input_mode(vc_input_mode mode);
|
||||||
|
vc_result set_push_to_talk(bool active);
|
||||||
|
vc_result set_self_mute(bool mic_muted, bool deafened);
|
||||||
|
vc_result set_remote_stream(uint32_t user_id, uint32_t stream_id, float gain, bool muted,
|
||||||
|
bool noise_reduction);
|
||||||
|
|
||||||
|
vc_result send_text(vc_text_scope scope, uint32_t target_id, const char* utf8);
|
||||||
|
|
||||||
|
vc_result list_devices(vc_device_kind kind, vc_device_list* out);
|
||||||
|
|
||||||
|
vc_connection_state state() const { return state_; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Deliver an event to the host application. Safe to call with cb_.on_event == nullptr.
|
||||||
|
void emit(const vc_event& ev) const;
|
||||||
|
|
||||||
|
vc_config cfg_{};
|
||||||
|
vc_callbacks cb_{};
|
||||||
|
vc_connection_state state_ = VC_STATE_DISCONNECTED;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // VOICECAT_CORE_CLIENT_H
|
||||||
8
core/src/crypto/crypto.cpp
Normal file
8
core/src/crypto/crypto.cpp
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
#include "crypto/crypto.h"
|
||||||
|
|
||||||
|
namespace voicecat::crypto {
|
||||||
|
|
||||||
|
// M0 stub. Brought up in M1 (TLS 1.3 via mbedTLS) and M2 (media AEAD via libsodium).
|
||||||
|
// See docs/security.md §1–2.
|
||||||
|
|
||||||
|
} // namespace voicecat::crypto
|
||||||
40
core/src/crypto/crypto.h
Normal file
40
core/src/crypto/crypto.h
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
* crypto/crypto.h — TLS 1.3 (mbedTLS) and the media AEAD (libsodium).
|
||||||
|
*
|
||||||
|
* Design: docs/security.md. Control channel = TLS 1.3. Media = keys exported from the TLS
|
||||||
|
* session (RFC 5705 / 8446) + per-frame ChaCha20-Poly1305 with a counter nonce and a
|
||||||
|
* sliding-window replay filter. Encryption is MANDATORY — never add a plaintext path.
|
||||||
|
*
|
||||||
|
* STATUS: M0 stub.
|
||||||
|
*/
|
||||||
|
#ifndef VOICECAT_CRYPTO_CRYPTO_H
|
||||||
|
#define VOICECAT_CRYPTO_CRYPTO_H
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
namespace voicecat::crypto {
|
||||||
|
|
||||||
|
// TLS 1.3 endpoint wrapper (mbedTLS). Provides the keying-material exporter that seeds
|
||||||
|
// MediaCrypto, so the UDP path inherits the authenticated control session's trust.
|
||||||
|
class TlsContext {
|
||||||
|
public:
|
||||||
|
// TODO(M1): client/server handshake; read/write; export_keying_material(label,...).
|
||||||
|
};
|
||||||
|
|
||||||
|
// Per-frame media encryption. Abstracted so the backend (exported-key AEAD now; a DTLS 1.3
|
||||||
|
// backend later, if a permissive impl matures) is swappable without touching voice code.
|
||||||
|
class MediaCrypto {
|
||||||
|
public:
|
||||||
|
virtual ~MediaCrypto() = default;
|
||||||
|
// seal/open one voice frame; `aad` carries the routable header fields (e.g. ssrc).
|
||||||
|
// Returns bytes written, or -1 on failure (replay/auth). TODO(M2).
|
||||||
|
virtual long seal(const uint8_t* plain, size_t len, const uint8_t* aad, size_t aad_len,
|
||||||
|
uint8_t* out, size_t out_cap) = 0;
|
||||||
|
virtual long open(const uint8_t* sealed, size_t len, const uint8_t* aad, size_t aad_len,
|
||||||
|
uint8_t* out, size_t out_cap) = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace voicecat::crypto
|
||||||
|
|
||||||
|
#endif // VOICECAT_CRYPTO_CRYPTO_H
|
||||||
8
core/src/net/transport.cpp
Normal file
8
core/src/net/transport.cpp
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
#include "net/transport.h"
|
||||||
|
|
||||||
|
namespace voicecat::net {
|
||||||
|
|
||||||
|
// M0 stub. Subsystem brought up in M1 (TCP/TLS) and M2 (UDP). See docs/protocol.md,
|
||||||
|
// docs/voice.md, and AGENTS.md "Suggested first steps".
|
||||||
|
|
||||||
|
} // namespace voicecat::net
|
||||||
39
core/src/net/transport.h
Normal file
39
core/src/net/transport.h
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
* net/transport.h — TCP control channel + UDP media channel.
|
||||||
|
*
|
||||||
|
* Design: docs/architecture.md (Net thread), docs/protocol.md §1 (framing), docs/voice.md §2
|
||||||
|
* (UDP frame). Implementation will use standalone Asio (one reactor) for sockets/timers.
|
||||||
|
*
|
||||||
|
* STATUS: M0 stub — interfaces only, no Asio yet.
|
||||||
|
*/
|
||||||
|
#ifndef VOICECAT_NET_TRANSPORT_H
|
||||||
|
#define VOICECAT_NET_TRANSPORT_H
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace voicecat::net {
|
||||||
|
|
||||||
|
// Length-prefixed [u32 length][payload] framing over a TLS 1.3 byte stream (protocol.md §1).
|
||||||
|
class TcpControlChannel {
|
||||||
|
public:
|
||||||
|
// TODO(M1): connect(host, port), TLS handshake, send/recv framed Envelopes.
|
||||||
|
bool connected() const { return connected_; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool connected_ = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// UDP media channel: encrypted voice frames (voice.md §2), bound to a session via token.
|
||||||
|
class UdpMediaChannel {
|
||||||
|
public:
|
||||||
|
// TODO(M2): bind, send/recv AEAD-sealed voice frames, keepalive.
|
||||||
|
bool bound() const { return bound_; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool bound_ = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace voicecat::net
|
||||||
|
|
||||||
|
#endif // VOICECAT_NET_TRANSPORT_H
|
||||||
11
core/src/protocol/protocol.cpp
Normal file
11
core/src/protocol/protocol.cpp
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
#include "protocol/protocol.h"
|
||||||
|
|
||||||
|
namespace voicecat::protocol {
|
||||||
|
|
||||||
|
// M0 stub. The frame codec + protobuf Envelope dispatch are the first M1 task
|
||||||
|
// (AGENTS.md "Suggested first steps" #1). See docs/protocol.md §1–5.
|
||||||
|
bool FrameCodec::feed(const uint8_t*, size_t, std::vector<std::vector<uint8_t>>&) {
|
||||||
|
return true; // TODO(M1): real framing.
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace voicecat::protocol
|
||||||
33
core/src/protocol/protocol.h
Normal file
33
core/src/protocol/protocol.h
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
/*
|
||||||
|
* protocol/protocol.h — control-plane (de)serialization + routing.
|
||||||
|
*
|
||||||
|
* Design: docs/protocol.md. Wire format is a length-prefixed protobuf `Envelope`
|
||||||
|
* (core/proto/voicecat.proto). This layer parses frames into Envelopes, correlates
|
||||||
|
* request_id ↔ response, and dispatches to handlers. Media frames do NOT come through here
|
||||||
|
* (they use the fixed binary header in voice.md §2).
|
||||||
|
*
|
||||||
|
* STATUS: M0 stub — protobuf codegen is wired in CMake (commented) and turned on in M1.
|
||||||
|
*/
|
||||||
|
#ifndef VOICECAT_PROTOCOL_PROTOCOL_H
|
||||||
|
#define VOICECAT_PROTOCOL_PROTOCOL_H
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace voicecat::protocol {
|
||||||
|
|
||||||
|
constexpr uint32_t kProtocolVersion = 1; // docs/protocol.md §4
|
||||||
|
constexpr uint32_t kMaxFrameBytes = 16u * 1024 * 1024; // §1 oversized-frame guard
|
||||||
|
|
||||||
|
// Reads/writes [u32 length][payload] frames from a byte stream. TODO(M1).
|
||||||
|
class FrameCodec {
|
||||||
|
public:
|
||||||
|
// Append received bytes; pop complete frame payloads. Returns false on protocol error
|
||||||
|
// (e.g. length > kMaxFrameBytes).
|
||||||
|
bool feed(const uint8_t* data, size_t len, std::vector<std::vector<uint8_t>>& out_frames);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace voicecat::protocol
|
||||||
|
|
||||||
|
#endif // VOICECAT_PROTOCOL_PROTOCOL_H
|
||||||
8
core/src/session/session.cpp
Normal file
8
core/src/session/session.cpp
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
#include "session/session.h"
|
||||||
|
|
||||||
|
namespace voicecat::session {
|
||||||
|
|
||||||
|
// M0 stub. Channel tree, users, streams, permissions, and ephemeral text relay land in M1.
|
||||||
|
// See docs/protocol.md §5.
|
||||||
|
|
||||||
|
} // namespace voicecat::session
|
||||||
55
core/src/session/session.h
Normal file
55
core/src/session/session.h
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
/*
|
||||||
|
* session/session.h — domain model: channels, users, streams, permissions, text.
|
||||||
|
*
|
||||||
|
* Design: docs/protocol.md §5, docs/architecture.md §5. Shared by client (local mirror of
|
||||||
|
* server state) and server (authoritative). Text is ephemeral (no history). Accounts are
|
||||||
|
* admin-provisioned.
|
||||||
|
*
|
||||||
|
* STATUS: M0 stub.
|
||||||
|
*/
|
||||||
|
#ifndef VOICECAT_SESSION_SESSION_H
|
||||||
|
#define VOICECAT_SESSION_SESSION_H
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace voicecat::session {
|
||||||
|
|
||||||
|
struct Channel {
|
||||||
|
uint32_t id = 0;
|
||||||
|
uint32_t parent_id = 0;
|
||||||
|
std::string name;
|
||||||
|
bool password_protected = false;
|
||||||
|
uint32_t max_users = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Stream {
|
||||||
|
uint32_t stream_id = 0;
|
||||||
|
uint32_t ssrc = 0;
|
||||||
|
int kind = 0; // vc_stream_kind
|
||||||
|
std::string label;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct User {
|
||||||
|
uint32_t id = 0;
|
||||||
|
std::string nickname;
|
||||||
|
bool is_guest = true;
|
||||||
|
uint32_t channel_id = 0;
|
||||||
|
std::vector<Stream> streams;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mirror/authority for the channel tree + user list. TODO(M1): snapshot + delta apply.
|
||||||
|
class SessionModel {
|
||||||
|
public:
|
||||||
|
const std::vector<Channel>& channels() const { return channels_; }
|
||||||
|
const std::vector<User>& users() const { return users_; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::vector<Channel> channels_;
|
||||||
|
std::vector<User> users_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace voicecat::session
|
||||||
|
|
||||||
|
#endif // VOICECAT_SESSION_SESSION_H
|
||||||
137
core/src/voicecat.cpp
Normal file
137
core/src/voicecat.cpp
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
/*
|
||||||
|
* voicecat.cpp — C ABI implementation (M0 skeleton).
|
||||||
|
*
|
||||||
|
* Lifecycle (create/destroy) and trivial accessors are real. Everything that needs a
|
||||||
|
* subsystem (net/crypto/codec/protocol/session/audio) returns VC_ERR_NOT_IMPLEMENTED for
|
||||||
|
* now and is the work of M1+ (see AGENTS.md / docs/roadmap.md).
|
||||||
|
*/
|
||||||
|
#include "voicecat.h"
|
||||||
|
|
||||||
|
#include <new>
|
||||||
|
|
||||||
|
#include "core/client.h"
|
||||||
|
|
||||||
|
#define VC_STR2(x) #x
|
||||||
|
#define VC_STR(x) VC_STR2(x)
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
|
||||||
|
const char* vc_version_string(void) {
|
||||||
|
static const char* kVersion = VC_STR(VOICECAT_VERSION_MAJOR) "." VC_STR(
|
||||||
|
VOICECAT_VERSION_MINOR) "." VC_STR(VOICECAT_VERSION_PATCH);
|
||||||
|
return kVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* vc_result_string(vc_result code) {
|
||||||
|
switch (code) {
|
||||||
|
case VC_OK: return "ok";
|
||||||
|
case VC_ERR_NOT_IMPLEMENTED: return "not implemented";
|
||||||
|
case VC_ERR_INVALID_ARG: return "invalid argument";
|
||||||
|
case VC_ERR_NOT_CONNECTED: return "not connected";
|
||||||
|
case VC_ERR_ALREADY: return "already in requested state";
|
||||||
|
case VC_ERR_AUTH_FAILED: return "authentication failed";
|
||||||
|
case VC_ERR_PERMISSION_DENIED: return "permission denied";
|
||||||
|
case VC_ERR_TIMEOUT: return "timeout";
|
||||||
|
case VC_ERR_IO: return "i/o error";
|
||||||
|
case VC_ERR_PROTOCOL: return "protocol error";
|
||||||
|
case VC_ERR_CRYPTO: return "crypto error";
|
||||||
|
case VC_ERR_AUDIO: return "audio error";
|
||||||
|
case VC_ERR_INTERNAL: return "internal error";
|
||||||
|
}
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
vc_client* vc_client_create(const vc_config* cfg, vc_callbacks cb) {
|
||||||
|
if (cfg == nullptr) return nullptr;
|
||||||
|
return new (std::nothrow) vc_client(*cfg, cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
void vc_client_destroy(vc_client* c) { delete c; }
|
||||||
|
|
||||||
|
/* ── Everything below delegates to the (stub) client. ─────────────────────── */
|
||||||
|
|
||||||
|
vc_result vc_connect(vc_client* c, const char* host, uint16_t port) {
|
||||||
|
if (c == nullptr || host == nullptr) return VC_ERR_INVALID_ARG;
|
||||||
|
return c->connect(host, port);
|
||||||
|
}
|
||||||
|
|
||||||
|
vc_result vc_disconnect(vc_client* c) {
|
||||||
|
if (c == nullptr) return VC_ERR_INVALID_ARG;
|
||||||
|
return c->disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
vc_result vc_authenticate_guest(vc_client* c, const char* nickname) {
|
||||||
|
if (c == nullptr || nickname == nullptr) return VC_ERR_INVALID_ARG;
|
||||||
|
return c->authenticate_guest(nickname);
|
||||||
|
}
|
||||||
|
|
||||||
|
vc_result vc_authenticate_user(vc_client* c, const char* username, const char* password) {
|
||||||
|
if (c == nullptr || username == nullptr || password == nullptr) return VC_ERR_INVALID_ARG;
|
||||||
|
return c->authenticate_user(username, password);
|
||||||
|
}
|
||||||
|
|
||||||
|
vc_result vc_join_channel(vc_client* c, uint32_t channel_id, const char* password) {
|
||||||
|
if (c == nullptr) return VC_ERR_INVALID_ARG;
|
||||||
|
return c->join_channel(channel_id, password);
|
||||||
|
}
|
||||||
|
|
||||||
|
vc_result vc_leave_channel(vc_client* c) {
|
||||||
|
if (c == nullptr) return VC_ERR_INVALID_ARG;
|
||||||
|
return c->leave_channel();
|
||||||
|
}
|
||||||
|
|
||||||
|
vc_result vc_stream_start(vc_client* c, const vc_stream_desc* desc, uint32_t* out_stream_id) {
|
||||||
|
if (c == nullptr || desc == nullptr) return VC_ERR_INVALID_ARG;
|
||||||
|
return c->stream_start(*desc, out_stream_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
vc_result vc_stream_stop(vc_client* c, uint32_t stream_id) {
|
||||||
|
if (c == nullptr) return VC_ERR_INVALID_ARG;
|
||||||
|
return c->stream_stop(stream_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
vc_result vc_set_input_device(vc_client* c, uint32_t stream_id, const char* device_id) {
|
||||||
|
if (c == nullptr) return VC_ERR_INVALID_ARG;
|
||||||
|
return c->set_input_device(stream_id, device_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
vc_result vc_set_input_mode(vc_client* c, vc_input_mode mode) {
|
||||||
|
if (c == nullptr) return VC_ERR_INVALID_ARG;
|
||||||
|
return c->set_input_mode(mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
vc_result vc_set_push_to_talk(vc_client* c, int active) {
|
||||||
|
if (c == nullptr) return VC_ERR_INVALID_ARG;
|
||||||
|
return c->set_push_to_talk(active != 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
vc_result vc_set_self_mute(vc_client* c, int mic_muted, int deafened) {
|
||||||
|
if (c == nullptr) return VC_ERR_INVALID_ARG;
|
||||||
|
return c->set_self_mute(mic_muted != 0, deafened != 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
vc_result vc_set_remote_stream(vc_client* c, uint32_t user_id, uint32_t stream_id, float gain,
|
||||||
|
int muted, int noise_reduction) {
|
||||||
|
if (c == nullptr) return VC_ERR_INVALID_ARG;
|
||||||
|
return c->set_remote_stream(user_id, stream_id, gain, muted != 0, noise_reduction != 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
vc_result vc_send_text(vc_client* c, vc_text_scope scope, uint32_t target_id,
|
||||||
|
const char* utf8) {
|
||||||
|
if (c == nullptr || utf8 == nullptr) return VC_ERR_INVALID_ARG;
|
||||||
|
return c->send_text(scope, target_id, utf8);
|
||||||
|
}
|
||||||
|
|
||||||
|
vc_result vc_list_devices(vc_client* c, vc_device_kind kind, vc_device_list* out) {
|
||||||
|
if (c == nullptr || out == nullptr) return VC_ERR_INVALID_ARG;
|
||||||
|
return c->list_devices(kind, out);
|
||||||
|
}
|
||||||
|
|
||||||
|
void vc_free_device_list(vc_device_list* list) {
|
||||||
|
if (list == nullptr) return;
|
||||||
|
/* Stub: no allocation yet. Real impl frees list->items here. */
|
||||||
|
list->items = nullptr;
|
||||||
|
list->count = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // extern "C"
|
||||||
6
server/CMakeLists.txt
Normal file
6
server/CMakeLists.txt
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
file(GLOB_RECURSE VOICECAT_SERVER_SOURCES CONFIGURE_DEPENDS
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp")
|
||||||
|
|
||||||
|
add_executable(voicecat-server ${VOICECAT_SERVER_SOURCES})
|
||||||
|
target_link_libraries(voicecat-server PRIVATE voicecat::voicecat)
|
||||||
|
target_compile_features(voicecat-server PRIVATE cxx_std_20)
|
||||||
68
server/src/main.cpp
Normal file
68
server/src/main.cpp
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
/*
|
||||||
|
* voicecat-server entry point (M0 skeleton).
|
||||||
|
*
|
||||||
|
* Goal (docs/deployment.md): one command, zero config, encrypted + listening. Today it
|
||||||
|
* parses a few flags and prints what it would do.
|
||||||
|
*/
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstring>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "server.h"
|
||||||
|
#include "voicecat.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
void print_help(const char* argv0) {
|
||||||
|
std::printf(
|
||||||
|
"voicecat-server %s\n\n"
|
||||||
|
"Usage: %s [options]\n"
|
||||||
|
" --port <n> control+media port (default 8384)\n"
|
||||||
|
" --data-dir <path> data directory (default ./voicecat-data)\n"
|
||||||
|
" --name <name> server name\n"
|
||||||
|
" --no-guests disable guest access\n"
|
||||||
|
" --print-config print effective config and exit\n"
|
||||||
|
" --version print version and exit\n"
|
||||||
|
" --help this help\n",
|
||||||
|
vc_version_string(), argv0);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
int main(int argc, char** argv) {
|
||||||
|
voicecat::server::Config cfg;
|
||||||
|
bool print_config_only = false;
|
||||||
|
|
||||||
|
for (int i = 1; i < argc; ++i) {
|
||||||
|
const char* a = argv[i];
|
||||||
|
auto next = [&]() -> const char* { return (i + 1 < argc) ? argv[++i] : ""; };
|
||||||
|
if (std::strcmp(a, "--help") == 0) {
|
||||||
|
print_help(argv[0]);
|
||||||
|
return 0;
|
||||||
|
} else if (std::strcmp(a, "--version") == 0) {
|
||||||
|
std::printf("voicecat-server %s (protocol v%d)\n", vc_version_string(),
|
||||||
|
VOICECAT_PROTOCOL_VERSION);
|
||||||
|
return 0;
|
||||||
|
} else if (std::strcmp(a, "--port") == 0) {
|
||||||
|
cfg.bind_port = static_cast<uint16_t>(std::atoi(next()));
|
||||||
|
} else if (std::strcmp(a, "--data-dir") == 0) {
|
||||||
|
cfg.data_dir = next();
|
||||||
|
} else if (std::strcmp(a, "--name") == 0) {
|
||||||
|
cfg.server_name = next();
|
||||||
|
} else if (std::strcmp(a, "--no-guests") == 0) {
|
||||||
|
cfg.allow_guests = false;
|
||||||
|
} else if (std::strcmp(a, "--print-config") == 0) {
|
||||||
|
print_config_only = true;
|
||||||
|
} else {
|
||||||
|
std::fprintf(stderr, "unknown option: %s (try --help)\n", a);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::printf("[voicecat-server %s] starting\n", vc_version_string());
|
||||||
|
voicecat::server::Server server(cfg);
|
||||||
|
if (print_config_only) {
|
||||||
|
// run() currently just prints config; in M1 split this into a pure config dump.
|
||||||
|
}
|
||||||
|
return server.run();
|
||||||
|
}
|
||||||
20
server/src/server.cpp
Normal file
20
server/src/server.cpp
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
#include "server.h"
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
|
||||||
|
namespace voicecat::server {
|
||||||
|
|
||||||
|
int Server::run() {
|
||||||
|
// M0 stub: report what a real run WILL do, then exit. M1 brings up the TLS listener,
|
||||||
|
// session registry, and channel manager (docs/architecture.md §5).
|
||||||
|
std::printf(" server_name : %s\n", cfg_.server_name.c_str());
|
||||||
|
std::printf(" data_dir : %s\n", cfg_.data_dir.c_str());
|
||||||
|
std::printf(" bind_port : %u (TCP control + UDP media)\n", cfg_.bind_port);
|
||||||
|
std::printf(" allow_guests: %s\n", cfg_.allow_guests ? "true" : "false");
|
||||||
|
std::printf(" fingerprint : <generated on first run — TODO M1>\n");
|
||||||
|
std::printf("\n[voicecat-server] M0 skeleton: networking not implemented yet. "
|
||||||
|
"See docs/roadmap.md (M1) and AGENTS.md.\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace voicecat::server
|
||||||
40
server/src/server.h
Normal file
40
server/src/server.h
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
* server.h — voicecat-server skeleton.
|
||||||
|
*
|
||||||
|
* Design: docs/architecture.md §5, docs/deployment.md. Headless process that links the core.
|
||||||
|
* Responsibilities: connection manager (TLS), session registry, channel manager, text router,
|
||||||
|
* voice SFU relay, SQLite persistence. Zero-config: self-provisions Ed25519 identity + cert
|
||||||
|
* on first run, embedded SQLite, guests on by default.
|
||||||
|
*
|
||||||
|
* STATUS: M0 stub — prints config and exits; does not yet listen.
|
||||||
|
*/
|
||||||
|
#ifndef VOICECAT_SERVER_SERVER_H
|
||||||
|
#define VOICECAT_SERVER_SERVER_H
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace voicecat::server {
|
||||||
|
|
||||||
|
struct Config {
|
||||||
|
std::string server_name = "VoiceCat Server";
|
||||||
|
std::string data_dir = "voicecat-data";
|
||||||
|
uint16_t bind_port = 8384; // TCP + UDP (docs/deployment.md §2)
|
||||||
|
bool allow_guests = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
class Server {
|
||||||
|
public:
|
||||||
|
explicit Server(Config cfg) : cfg_(std::move(cfg)) {}
|
||||||
|
|
||||||
|
// TODO(M1): bind TLS control listener + UDP media socket; run the Asio loop until stop.
|
||||||
|
// Returns process exit code.
|
||||||
|
int run();
|
||||||
|
|
||||||
|
private:
|
||||||
|
Config cfg_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace voicecat::server
|
||||||
|
|
||||||
|
#endif // VOICECAT_SERVER_SERVER_H
|
||||||
8
tests/CMakeLists.txt
Normal file
8
tests/CMakeLists.txt
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# Tests use plain asserts + exit codes for now (no framework dependency in the skeleton).
|
||||||
|
# A real framework (e.g. Catch2/GoogleTest via vcpkg) can be added when VOICECAT_USE_VCPKG_DEPS
|
||||||
|
# is on. Behavior tests — not just "it compiles" — are how milestones are judged (AGENTS.md).
|
||||||
|
|
||||||
|
add_executable(test_smoke test_smoke.cpp)
|
||||||
|
target_link_libraries(test_smoke PRIVATE voicecat::voicecat)
|
||||||
|
target_compile_features(test_smoke PRIVATE cxx_std_20)
|
||||||
|
add_test(NAME smoke COMMAND test_smoke)
|
||||||
65
tests/test_smoke.cpp
Normal file
65
tests/test_smoke.cpp
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
/*
|
||||||
|
* test_smoke — verifies the core links and the C ABI behaves as specified for M0.
|
||||||
|
*
|
||||||
|
* This is intentionally a behavior test, not a "does it compile" check: it asserts the
|
||||||
|
* documented contract (version present, handle lifecycle, invalid-arg guards, and that
|
||||||
|
* unimplemented calls report VC_ERR_NOT_IMPLEMENTED rather than crashing).
|
||||||
|
*/
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
#include "voicecat.h"
|
||||||
|
|
||||||
|
static int g_failures = 0;
|
||||||
|
|
||||||
|
#define CHECK(cond) \
|
||||||
|
do { \
|
||||||
|
if (!(cond)) { \
|
||||||
|
std::printf("FAIL: %s (%s:%d)\n", #cond, __FILE__, __LINE__); \
|
||||||
|
++g_failures; \
|
||||||
|
} \
|
||||||
|
} while (0)
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
// Version + result strings are always available.
|
||||||
|
CHECK(vc_version_string() != nullptr);
|
||||||
|
CHECK(std::strlen(vc_version_string()) > 0);
|
||||||
|
CHECK(std::strcmp(vc_result_string(VC_OK), "ok") == 0);
|
||||||
|
CHECK(std::strcmp(vc_result_string(VC_ERR_NOT_IMPLEMENTED), "not implemented") == 0);
|
||||||
|
|
||||||
|
// Null-config create is rejected; valid create yields a handle.
|
||||||
|
vc_callbacks cb{};
|
||||||
|
CHECK(vc_client_create(nullptr, cb) == nullptr);
|
||||||
|
|
||||||
|
vc_config cfg{};
|
||||||
|
cfg.client_name = "test";
|
||||||
|
cfg.client_version = "0";
|
||||||
|
cfg.log_level = VC_LOG_OFF;
|
||||||
|
vc_client* c = vc_client_create(&cfg, cb);
|
||||||
|
CHECK(c != nullptr);
|
||||||
|
|
||||||
|
// Invalid-arg guards on the ABI.
|
||||||
|
CHECK(vc_connect(nullptr, "h", 1) == VC_ERR_INVALID_ARG);
|
||||||
|
CHECK(vc_connect(c, nullptr, 1) == VC_ERR_INVALID_ARG);
|
||||||
|
CHECK(vc_send_text(c, VC_TEXT_CHANNEL, 0, nullptr) == VC_ERR_INVALID_ARG);
|
||||||
|
|
||||||
|
// Unimplemented subsystems report NOT_IMPLEMENTED (not a crash) in the M0 skeleton.
|
||||||
|
CHECK(vc_connect(c, "127.0.0.1", 8384) == VC_ERR_NOT_IMPLEMENTED);
|
||||||
|
CHECK(vc_authenticate_guest(c, "nick") == VC_ERR_NOT_IMPLEMENTED);
|
||||||
|
CHECK(vc_join_channel(c, 1, nullptr) == VC_ERR_NOT_IMPLEMENTED);
|
||||||
|
|
||||||
|
vc_device_list dl;
|
||||||
|
CHECK(vc_list_devices(c, VC_DEVICE_INPUT, &dl) == VC_ERR_NOT_IMPLEMENTED);
|
||||||
|
CHECK(dl.count == 0);
|
||||||
|
vc_free_device_list(&dl);
|
||||||
|
|
||||||
|
vc_client_destroy(c);
|
||||||
|
vc_client_destroy(nullptr); // must be safe
|
||||||
|
|
||||||
|
if (g_failures == 0) {
|
||||||
|
std::printf("smoke: all checks passed\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
std::printf("smoke: %d failure(s)\n", g_failures);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
3
tools/vccli/CMakeLists.txt
Normal file
3
tools/vccli/CMakeLists.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
add_executable(vccli src/main.cpp)
|
||||||
|
target_link_libraries(vccli PRIVATE voicecat::voicecat)
|
||||||
|
target_compile_features(vccli PRIVATE cxx_std_20)
|
||||||
47
tools/vccli/src/main.cpp
Normal file
47
tools/vccli/src/main.cpp
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
* vccli — headless test client.
|
||||||
|
*
|
||||||
|
* This is the primary way the protocol is exercised and verified from M1 onward (see
|
||||||
|
* AGENTS.md). Each milestone's exit criterion is demonstrated by driving two vccli
|
||||||
|
* instances against a real voicecat-server. Today it just shows the C ABI is linkable.
|
||||||
|
*/
|
||||||
|
#include <cstdio>
|
||||||
|
|
||||||
|
#include "voicecat.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
void on_event(void* /*user*/, const vc_event* ev) {
|
||||||
|
std::printf("[event] type=%d state=%d result=%d text=%s\n", ev->type, ev->connection_state,
|
||||||
|
ev->result, ev->text ? ev->text : "");
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
int main(int argc, char** argv) {
|
||||||
|
std::printf("vccli — VoiceCat test client (core %s, protocol v%d)\n", vc_version_string(),
|
||||||
|
VOICECAT_PROTOCOL_VERSION);
|
||||||
|
|
||||||
|
vc_config cfg{};
|
||||||
|
cfg.client_name = "vccli";
|
||||||
|
cfg.client_version = vc_version_string();
|
||||||
|
cfg.log_level = VC_LOG_INFO;
|
||||||
|
|
||||||
|
vc_callbacks cb{};
|
||||||
|
cb.on_event = on_event;
|
||||||
|
|
||||||
|
vc_client* c = vc_client_create(&cfg, cb);
|
||||||
|
if (c == nullptr) {
|
||||||
|
std::fprintf(stderr, "failed to create client\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// M0: demonstrate the call surface. These return VC_ERR_NOT_IMPLEMENTED for now.
|
||||||
|
const char* host = (argc > 1) ? argv[1] : "127.0.0.1";
|
||||||
|
vc_result r = vc_connect(c, host, 8384);
|
||||||
|
std::printf("vc_connect(%s:8384) -> %d (%s)\n", host, r, vc_result_string(r));
|
||||||
|
|
||||||
|
vc_client_destroy(c);
|
||||||
|
std::printf("ok\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
21
vcpkg.json
Normal file
21
vcpkg.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://raw.githubusercontent.com/microsoft/vcpkg/master/scripts/vcpkg.schema.json",
|
||||||
|
"name": "voicecat",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "Self-hosted native voice & text chat. See docs/.",
|
||||||
|
"builtin-baseline": "0000000000000000000000000000000000000000",
|
||||||
|
"$comment-baseline": "TODO: set builtin-baseline to a real vcpkg commit SHA when first enabling VOICECAT_USE_VCPKG_DEPS. Until then the skeleton builds with deps OFF and this manifest is inert.",
|
||||||
|
"dependencies": [
|
||||||
|
{ "name": "opus", "$why": "voice codec (libopus 1.6) — docs/voice.md" },
|
||||||
|
{ "name": "libsodium", "$why": "Argon2id, ChaCha20-Poly1305 media AEAD, Ed25519 — docs/security.md" },
|
||||||
|
{ "name": "mbedtls", "$why": "TLS 1.3 control channel + keying-material exporter — docs/security.md" },
|
||||||
|
{ "name": "protobuf", "$why": "control-plane serialization — docs/protocol.md" },
|
||||||
|
{ "name": "sqlite3", "$why": "server accounts/state — docs/security.md" },
|
||||||
|
{ "name": "asio", "$why": "TCP/UDP/timers reactor — docs/architecture.md" },
|
||||||
|
{ "name": "miniaudio", "$why": "cross-platform capture/playback — docs/tech-stack.md" },
|
||||||
|
{ "name": "speexdsp", "$why": "resampling + jitter reference — docs/tech-stack.md" },
|
||||||
|
{ "name": "webrtc-audio-processing", "$why": "AEC/NS/AGC/VAD (APM) — docs/voice.md" },
|
||||||
|
{ "name": "spdlog", "$why": "logging — docs/tech-stack.md" }
|
||||||
|
],
|
||||||
|
"$license-note": "All of the above are permissive (BSD/MIT/ISC/Apache-2.0/public-domain). No GPL/LGPL — see docs/tech-stack.md §5."
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user