chore: remove skeleton build mode and stub #ifdef scaffolding
Some checks failed
Build Linux Binaries / linux/amd64 (push) Has been cancelled
Build Linux Binaries / linux/arm64 (push) Has been cancelled

Drop the M0 no-deps skeleton preset and all VOICECAT_HAS_NET/AUDIO/OPUS/NS
guards that it required. Every subsystem is fully implemented; the stub
#else paths were dead code that added noise to every header and source file.

- CMakePresets.json: remove skeleton configure/build/test entries
- CMakeLists.txt (root/core/tests): remove VOICECAT_USE_VCPKG_DEPS option
  and guards; all targets now build unconditionally
- 17 C++ source files: unwrap HAS_* guards, delete stub #else blocks
- apm_processor.cpp: delete ApmPassthrough no-op class; create() always
  returns RnnoiseProcessor
- 18 test files: remove HAS_* guards and stub int main() skip bodies
- docs/building.md: remove skeleton from preset table and prose

VOICECAT_HAS_LOOPBACK (Windows WASAPI loopback platform gate) unchanged.
29/29 ctest green.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-30 11:32:22 +01:00
parent 9a20953c08
commit 2c8178fa02
55 changed files with 25 additions and 675 deletions

View File

@@ -14,11 +14,6 @@ if(CMAKE_SYSTEM_NAME STREQUAL "iOS")
endif() endif()
# ── Options ─────────────────────────────────────────────────────────────────── # ── 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_SERVER "Build voicecat-server" ON)
option(VOICECAT_BUILD_TOOLS "Build the vccli headless test client" ON) option(VOICECAT_BUILD_TOOLS "Build the vccli headless test client" ON)
option(VOICECAT_BUILD_TESTS "Build tests" ON) option(VOICECAT_BUILD_TESTS "Build tests" ON)
@@ -60,9 +55,7 @@ endif()
if(VOICECAT_BUILD_TOOLS) if(VOICECAT_BUILD_TOOLS)
add_subdirectory(tools/vccli) add_subdirectory(tools/vccli)
if(VOICECAT_USE_VCPKG_DEPS) add_subdirectory(tools/voicecat-admin)
add_subdirectory(tools/voicecat-admin)
endif()
endif() endif()
if(VOICECAT_BUILD_TESTS) if(VOICECAT_BUILD_TESTS)
@@ -71,5 +64,4 @@ if(VOICECAT_BUILD_TESTS)
endif() endif()
message(STATUS "VoiceCat ${PROJECT_VERSION} configured " message(STATUS "VoiceCat ${PROJECT_VERSION} configured "
"(vcpkg deps: ${VOICECAT_USE_VCPKG_DEPS}, " "(server: ${VOICECAT_BUILD_SERVER}, tools: ${VOICECAT_BUILD_TOOLS})")
"server: ${VOICECAT_BUILD_SERVER}, tools: ${VOICECAT_BUILD_TOOLS})")

View File

@@ -12,17 +12,6 @@
"VOICECAT_USE_VCPKG_DEPS": "ON" "VOICECAT_USE_VCPKG_DEPS": "ON"
} }
}, },
{
"name": "skeleton",
"displayName": "Skeleton (no third-party deps)",
"description": "Builds the stub skeleton with just a C++20 compiler — no vcpkg needed. Subsystems return VC_ERR_NOT_IMPLEMENTED. Good for 'does the repo even build' smoke checks. Runs 2 tests (smoke + frame_codec).",
"generator": "Ninja",
"binaryDir": "${sourceDir}/build/skeleton",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"VOICECAT_USE_VCPKG_DEPS": "OFF"
}
},
{ {
"name": "dev", "name": "dev",
"displayName": "Dev (full real-deps build, vcpkg)", "displayName": "Dev (full real-deps build, vcpkg)",
@@ -130,7 +119,6 @@
} }
], ],
"buildPresets": [ "buildPresets": [
{ "name": "skeleton", "configurePreset": "skeleton" },
{ "name": "dev", "configurePreset": "dev" }, { "name": "dev", "configurePreset": "dev" },
{ "name": "release", "configurePreset": "release" }, { "name": "release", "configurePreset": "release" },
{ "name": "server-release", "configurePreset": "server-release" }, { "name": "server-release", "configurePreset": "server-release" },
@@ -140,7 +128,6 @@
{ "name": "apple-ios-sim", "configurePreset": "apple-ios-sim" } { "name": "apple-ios-sim", "configurePreset": "apple-ios-sim" }
], ],
"testPresets": [ "testPresets": [
{ "name": "skeleton", "configurePreset": "skeleton", "output": { "outputOnFailure": true } },
{ "name": "dev", "configurePreset": "dev", "output": { "outputOnFailure": true } }, { "name": "dev", "configurePreset": "dev", "output": { "outputOnFailure": true } },
{ "name": "release", "configurePreset": "release", "output": { "outputOnFailure": true } } { "name": "release", "configurePreset": "release", "output": { "outputOnFailure": true } }
] ]

View File

@@ -42,8 +42,7 @@ set_target_properties(voicecat PROPERTIES
CXX_VISIBILITY_PRESET hidden CXX_VISIBILITY_PRESET hidden
VISIBILITY_INLINES_HIDDEN ON) VISIBILITY_INLINES_HIDDEN ON)
if(VOICECAT_USE_VCPKG_DEPS) find_package(protobuf CONFIG REQUIRED)
find_package(protobuf CONFIG REQUIRED)
find_package(unofficial-sodium CONFIG REQUIRED) find_package(unofficial-sodium CONFIG REQUIRED)
find_package(MbedTLS CONFIG REQUIRED) find_package(MbedTLS CONFIG REQUIRED)
find_package(asio CONFIG REQUIRED) find_package(asio CONFIG REQUIRED)
@@ -129,4 +128,3 @@ if(VOICECAT_USE_VCPKG_DEPS)
# Signal to C++ code that the real networking/crypto stack is available. # Signal to C++ code that the real networking/crypto stack is available.
target_compile_definitions(voicecat PUBLIC VOICECAT_HAS_NET) target_compile_definitions(voicecat PUBLIC VOICECAT_HAS_NET)
endif()

View File

@@ -8,13 +8,9 @@
* Design: docs/architecture.md §4. Everything here is async + event-driven — calls return * 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. * immediately and results/state changes arrive via the vc_callbacks.on_event callback.
* *
* STATUS: real, behind VOICECAT_HAS_NET (the `dev`/`release`/`server-release` presets — * STATUS: real. Control plane, voice, multi-stream, device enumeration, VAD/PTT, and stereo
* vcpkg deps on; see docs/building.md). As of M3, control plane, voice, multi-stream, device * playback all work via core/src/core/client.cpp. webrtc AEC/NS/AGC remains an inert passthrough
* enumeration, VAD/PTT, and stereo playback all work for real via core/src/core/client.cpp. * (no Windows/MSVC port upstream — docs/voice.md §8/§11, PROGRESS.md).
* The no-deps `skeleton` preset still links a stub vc_client that returns VC_ERR_NOT_IMPLEMENTED
* for everything below `connect`, purely to keep that skeleton build green. webrtc AEC/NS/AGC
* remains an inert passthrough regardless of preset (no Windows/MSVC port upstream —
* docs/voice.md §8/§11, PROGRESS.md).
*/ */
#ifndef VOICECAT_H #ifndef VOICECAT_H
#define VOICECAT_H #define VOICECAT_H
@@ -56,7 +52,7 @@ extern "C" {
/* ── Result codes ─────────────────────────────────────────────────────────── */ /* ── Result codes ─────────────────────────────────────────────────────────── */
typedef enum vc_result { typedef enum vc_result {
VC_OK = 0, VC_OK = 0,
VC_ERR_NOT_IMPLEMENTED = 1, /* skeleton stub */ VC_ERR_NOT_IMPLEMENTED = 1,
VC_ERR_INVALID_ARG = 2, VC_ERR_INVALID_ARG = 2,
VC_ERR_NOT_CONNECTED = 3, VC_ERR_NOT_CONNECTED = 3,
VC_ERR_ALREADY = 4, VC_ERR_ALREADY = 4,

View File

@@ -5,9 +5,7 @@
#include <chrono> #include <chrono>
#include <cmath> #include <cmath>
#ifdef VOICECAT_HAS_NS
#include "rnnoise.h" #include "rnnoise.h"
#endif
namespace voicecat::audio { namespace voicecat::audio {
@@ -19,15 +17,6 @@ int64_t steady_now_ms() {
} }
} // namespace } // namespace
// ── ApmPassthrough ────────────────────────────────────────────────────────────
// No-op: returns true (VAD always open), does not modify PCM.
// Replaced by WebrtcApmProcessor when VOICECAT_HAS_APM is defined.
class ApmPassthrough final : public ApmProcessor {
public:
void process_render(const int16_t*, int, int) override {}
bool process_capture(int16_t*, int, int) override { return true; }
};
// ── EnergyVadProcessor ────────────────────────────────────────────────────── // ── EnergyVadProcessor ──────────────────────────────────────────────────────
// Lightweight, dependency-free energy/RMS VAD — see apm_processor.h's create_vad() doc comment // Lightweight, dependency-free energy/RMS VAD — see apm_processor.h's create_vad() doc comment
// for why this exists instead of a real APM. No AEC (process_render is a no-op); doesn't modify // for why this exists instead of a real APM. No AEC (process_render is a no-op); doesn't modify
@@ -62,7 +51,6 @@ class EnergyVadProcessor final : public ApmProcessor {
int64_t last_voice_ms_ = 0; // epoch start -> gate begins closed until first loud frame int64_t last_voice_ms_ = 0; // epoch start -> gate begins closed until first loud frame
}; };
#ifdef VOICECAT_HAS_NS
// ── RnnoiseProcessor ───────────────────────────────────────────────────────── // ── RnnoiseProcessor ─────────────────────────────────────────────────────────
// Real noise suppression via vendored RNNoise (third_party/rnnoise; docs/voice.md §10-11). // Real noise suppression via vendored RNNoise (third_party/rnnoise; docs/voice.md §10-11).
// RNNoise is a mono, 48 kHz, fixed 480-sample (10 ms) speech denoiser; our engine clock is fixed // RNNoise is a mono, 48 kHz, fixed 480-sample (10 ms) speech denoiser; our engine clock is fixed
@@ -103,14 +91,9 @@ class RnnoiseProcessor final : public ApmProcessor {
float in_[kFrame]; float in_[kFrame];
float out_[kFrame]; float out_[kFrame];
}; };
#endif // VOICECAT_HAS_NS
std::unique_ptr<ApmProcessor> ApmProcessor::create() { std::unique_ptr<ApmProcessor> ApmProcessor::create() {
#ifdef VOICECAT_HAS_NS
return std::make_unique<RnnoiseProcessor>(); return std::make_unique<RnnoiseProcessor>();
#else
return std::make_unique<ApmPassthrough>();
#endif
} }
std::unique_ptr<ApmProcessor> ApmProcessor::create_vad(float rms_threshold, std::unique_ptr<ApmProcessor> ApmProcessor::create_vad(float rms_threshold,

View File

@@ -1,7 +1,5 @@
#ifdef VOICECAT_HAS_AUDIO
#define MINIAUDIO_IMPLEMENTATION #define MINIAUDIO_IMPLEMENTATION
#include <miniaudio.h> #include <miniaudio.h>
#endif
#include "audio/audio_engine.h" #include "audio/audio_engine.h"
@@ -41,7 +39,6 @@ constexpr int32_t kStarveSamples = 48000 * 120 / 1000; // reseed when clock
// defense-in-depth against any future regression that skips remove_stream. // defense-in-depth against any future regression that skips remove_stream.
constexpr int32_t kPlcCapSamples = 48000 * 2; // 2 s @ 48 kHz constexpr int32_t kPlcCapSamples = 48000 * 2; // 2 s @ 48 kHz
#ifdef VOICECAT_HAS_AUDIO
// device_id encoding (DeviceInfo::id / AudioParams::*_device_id): a hex string of the raw // device_id encoding (DeviceInfo::id / AudioParams::*_device_id): a hex string of the raw
// ma_device_id bytes. Opaque on purpose — names aren't guaranteed unique, and this is the only // ma_device_id bytes. Opaque on purpose — names aren't guaranteed unique, and this is the only
// stable handle miniaudio accepts back for device selection. Internal contract only; never // stable handle miniaudio accepts back for device selection. Internal contract only; never
@@ -76,7 +73,6 @@ bool hex_decode_device_id(const std::string& hex, ma_device_id* out) {
} }
return true; return true;
} }
#endif // VOICECAT_HAS_AUDIO
} // namespace } // namespace
// ── JitterBuffer ───────────────────────────────────────────────────────────── // ── JitterBuffer ─────────────────────────────────────────────────────────────
@@ -197,19 +193,14 @@ AudioEngine::AudioEngine() = default;
AudioEngine::~AudioEngine() { AudioEngine::~AudioEngine() {
stop(); stop();
#ifdef VOICECAT_HAS_AUDIO
stop_loopback_capture(); // loopback has an independent lifecycle — close it before the context stop_loopback_capture(); // loopback has an independent lifecycle — close it before the context
if (context_inited_) { if (context_inited_) {
ma_context_uninit(&context_); ma_context_uninit(&context_);
context_inited_ = false; context_inited_ = false;
} }
#endif
#ifdef VOICECAT_HAS_OPUS
if (dred_dec_) { opus_dred_decoder_destroy(dred_dec_); dred_dec_ = nullptr; } if (dred_dec_) { opus_dred_decoder_destroy(dred_dec_); dred_dec_ = nullptr; }
#endif
} }
#ifdef VOICECAT_HAS_AUDIO
ma_context_config AudioEngine::make_context_config() { ma_context_config AudioEngine::make_context_config() {
ma_context_config cfg = ma_context_config_init(); ma_context_config cfg = ma_context_config_init();
// iOS: leave AVAudioSession entirely to the Swift layer (IOSAudioRouter). See the // iOS: leave AVAudioSession entirely to the Swift layer (IOSAudioRouter). See the
@@ -219,7 +210,6 @@ ma_context_config AudioEngine::make_context_config() {
cfg.coreaudio.noAudioSessionDeactivate = MA_TRUE; // don't setActive(false) on device uninit cfg.coreaudio.noAudioSessionDeactivate = MA_TRUE; // don't setActive(false) on device uninit
return cfg; return cfg;
} }
#endif
bool AudioEngine::start(const AudioParams& p, CaptureCallback capture_cb) { bool AudioEngine::start(const AudioParams& p, CaptureCallback capture_cb) {
if (running_.load()) return false; if (running_.load()) return false;
@@ -227,14 +217,11 @@ bool AudioEngine::start(const AudioParams& p, CaptureCallback capture_cb) {
capture_cb_ = std::move(capture_cb); capture_cb_ = std::move(capture_cb);
frame_samples_ = static_cast<int>(p.sample_rate / 1000 * p.frame_ms); frame_samples_ = static_cast<int>(p.sample_rate / 1000 * p.frame_ms);
running_.store(true, std::memory_order_release); running_.store(true, std::memory_order_release);
#ifdef VOICECAT_HAS_OPUS
if (!dred_dec_) { if (!dred_dec_) {
int err = 0; int err = 0;
dred_dec_ = opus_dred_decoder_create(&err); // null on failure — DRED silently disabled dred_dec_ = opus_dred_decoder_create(&err); // null on failure — DRED silently disabled
} }
#endif
#ifdef VOICECAT_HAS_AUDIO
// Pre-allocate capture accumulators before the devices start so on_capture / on_loopback // Pre-allocate capture accumulators before the devices start so on_capture / on_loopback
// never allocate on the RT thread. count=0 means "empty"; the buf is sized to exactly one // never allocate on the RT thread. count=0 means "empty"; the buf is sized to exactly one
// encoder frame so a memcpy into it can never overrun. The mic accumulator is sized to // encoder frame so a memcpy into it can never overrun. The mic accumulator is sized to
@@ -247,9 +234,7 @@ bool AudioEngine::start(const AudioParams& p, CaptureCallback capture_cb) {
capture_accum_.count = 0; capture_accum_.count = 0;
loopback_accum_.buf.assign(static_cast<size_t>(frame_samples_), 0); loopback_accum_.buf.assign(static_cast<size_t>(frame_samples_), 0);
loopback_accum_.count = 0; loopback_accum_.count = 0;
#endif
#ifdef VOICECAT_HAS_AUDIO
// Own the ma_context (lazily, reused across restarts) so miniaudio does not touch // Own the ma_context (lazily, reused across restarts) so miniaudio does not touch
// AVAudioSession on iOS — the Swift IOSAudioRouter is the sole session owner. Without // AVAudioSession on iOS — the Swift IOSAudioRouter is the sole session owner. Without
// this, ma_device_init(nullptr, ...) below would reset the session category to Record/ // this, ma_device_init(nullptr, ...) below would reset the session category to Record/
@@ -342,14 +327,12 @@ bool AudioEngine::start(const AudioParams& p, CaptureCallback capture_cb) {
mixer_timer_stop_.store(false, std::memory_order_release); mixer_timer_stop_.store(false, std::memory_order_release);
mixer_timer_thread_ = std::thread([this] { run_mixer_timer(); }); mixer_timer_thread_ = std::thread([this] { run_mixer_timer(); });
} }
#endif // VOICECAT_HAS_AUDIO
return true; return true;
} }
std::vector<DeviceInfo> AudioEngine::enumerate_devices(bool capture) { std::vector<DeviceInfo> AudioEngine::enumerate_devices(bool capture) {
std::vector<DeviceInfo> result; std::vector<DeviceInfo> result;
#ifdef VOICECAT_HAS_AUDIO
// Use the no-AVAudioSession-management config here too: device enumeration runs at // Use the no-AVAudioSession-management config here too: device enumeration runs at
// SessionState init (refreshDevices) and on settings views, and a default-config context // SessionState init (refreshDevices) and on settings views, and a default-config context
// would call setCategory()/setActive() on iOS, disrupting the session the Swift layer owns. // would call setCategory()/setActive() on iOS, disrupting the session the Swift layer owns.
@@ -377,16 +360,12 @@ std::vector<DeviceInfo> AudioEngine::enumerate_devices(bool capture) {
} }
ma_context_uninit(&ctx); ma_context_uninit(&ctx);
#else
(void)capture;
#endif // VOICECAT_HAS_AUDIO
return result; return result;
} }
void AudioEngine::stop() { void AudioEngine::stop() {
if (!running_.exchange(false)) return; if (!running_.exchange(false)) return;
#ifdef VOICECAT_HAS_AUDIO
// External playback: stop + join the mixer-timer thread before tearing down state it reads. // External playback: stop + join the mixer-timer thread before tearing down state it reads.
mixer_timer_stop_.store(true, std::memory_order_release); mixer_timer_stop_.store(true, std::memory_order_release);
if (mixer_timer_thread_.joinable()) mixer_timer_thread_.join(); if (mixer_timer_thread_.joinable()) mixer_timer_thread_.join();
@@ -400,11 +379,9 @@ void AudioEngine::stop() {
ma_device_uninit(&playback_device_); ma_device_uninit(&playback_device_);
playback_started_ = false; playback_started_ = false;
} }
#endif
} }
bool AudioEngine::suspend() { bool AudioEngine::suspend() {
#ifdef VOICECAT_HAS_AUDIO
if (!running_.load(std::memory_order_acquire)) return true; if (!running_.load(std::memory_order_acquire)) return true;
// External playback: pause the mixer timer (there is no playback device to stop). This // External playback: pause the mixer timer (there is no playback device to stop). This
// matches the VPIO renderer going down during an AVAudioSession interruption. // matches the VPIO renderer going down during an AVAudioSession interruption.
@@ -416,13 +393,9 @@ bool AudioEngine::suspend() {
if (capture_started_) ok &= (ma_device_stop(&capture_device_) == MA_SUCCESS); if (capture_started_) ok &= (ma_device_stop(&capture_device_) == MA_SUCCESS);
if (playback_started_) ok &= (ma_device_stop(&playback_device_) == MA_SUCCESS); if (playback_started_) ok &= (ma_device_stop(&playback_device_) == MA_SUCCESS);
return ok; return ok;
#else
return true;
#endif
} }
bool AudioEngine::resume() { bool AudioEngine::resume() {
#ifdef VOICECAT_HAS_AUDIO
if (!running_.load(std::memory_order_acquire)) return true; if (!running_.load(std::memory_order_acquire)) return true;
// External playback: relaunch the mixer timer (mixer_scratch_ is still sized from start()). // External playback: relaunch the mixer timer (mixer_scratch_ is still sized from start()).
if (external_playback_ && !mixer_timer_thread_.joinable()) { if (external_playback_ && !mixer_timer_thread_.joinable()) {
@@ -433,9 +406,6 @@ bool AudioEngine::resume() {
if (capture_started_) ok &= (ma_device_start(&capture_device_) == MA_SUCCESS); if (capture_started_) ok &= (ma_device_start(&capture_device_) == MA_SUCCESS);
if (playback_started_) ok &= (ma_device_start(&playback_device_) == MA_SUCCESS); if (playback_started_) ok &= (ma_device_start(&playback_device_) == MA_SUCCESS);
return ok; return ok;
#else
return true;
#endif
} }
void AudioEngine::inject_capture(int kind, const int16_t* pcm, size_t samples_per_channel, void AudioEngine::inject_capture(int kind, const int16_t* pcm, size_t samples_per_channel,
@@ -530,12 +500,10 @@ void AudioEngine::remove_stream(uint32_t ssrc) {
std::lock_guard lk(streams_mu_); std::lock_guard lk(streams_mu_);
auto it = streams_.find(ssrc); auto it = streams_.find(ssrc);
if (it != streams_.end()) { if (it != streams_.end()) {
#ifdef VOICECAT_HAS_OPUS
if (it->second.dred_state_) { if (it->second.dred_state_) {
opus_dred_free(it->second.dred_state_); opus_dred_free(it->second.dred_state_);
it->second.dred_state_ = nullptr; it->second.dred_state_ = nullptr;
} }
#endif
streams_.erase(it); streams_.erase(it);
} }
} }
@@ -590,7 +558,6 @@ int32_t AudioEngine::stream_playout_depth_samples(uint32_t ssrc) const {
return static_cast<int32_t>(*newest - it->second.playout_ts); return static_cast<int32_t>(*newest - it->second.playout_ts);
} }
#ifdef VOICECAT_HAS_OPUS
void AudioEngine::init_recv_stream(uint32_t ssrc, const codec::OpusParams& p, void AudioEngine::init_recv_stream(uint32_t ssrc, const codec::OpusParams& p,
uint32_t user_id, uint32_t stream_id, bool is_voice) { uint32_t user_id, uint32_t stream_id, bool is_voice) {
std::lock_guard lk(streams_mu_); std::lock_guard lk(streams_mu_);
@@ -616,7 +583,6 @@ void AudioEngine::init_recv_stream(uint32_t ssrc, const codec::OpusParams& p,
stream.dred_state_ = opus_dred_alloc(&err); // null on failure — falls back to PLC stream.dred_state_ = opus_dred_alloc(&err); // null on failure — falls back to PLC
} }
} }
#endif
void AudioEngine::set_pcm_sink(PcmSink cb, void* user) { void AudioEngine::set_pcm_sink(PcmSink cb, void* user) {
pcm_sink_user_.store(user, std::memory_order_relaxed); pcm_sink_user_.store(user, std::memory_order_relaxed);
@@ -628,8 +594,6 @@ void AudioEngine::set_mixed_output_sink(MixedSink cb, void* user) {
mixed_sink_.store(cb, std::memory_order_release); mixed_sink_.store(cb, std::memory_order_release);
} }
#ifdef VOICECAT_HAS_AUDIO
void AudioEngine::capture_data_cb(ma_device* dev, void* /*out*/, void AudioEngine::capture_data_cb(ma_device* dev, void* /*out*/,
const void* in, ma_uint32 frame_count) { const void* in, ma_uint32 frame_count) {
auto* self = static_cast<AudioEngine*>(dev->pUserData); auto* self = static_cast<AudioEngine*>(dev->pUserData);
@@ -678,7 +642,6 @@ void AudioEngine::on_playback(int16_t* out, ma_uint32 frames) {
std::unique_lock lk(streams_mu_, std::try_to_lock); std::unique_lock lk(streams_mu_, std::try_to_lock);
if (!lk) return; // contended: emit silence this period if (!lk) return; // contended: emit silence this period
#ifdef VOICECAT_HAS_OPUS
std::vector<int32_t> mix(frames * pb_channels, 0); std::vector<int32_t> mix(frames * pb_channels, 0);
for (auto& [ssrc, stream] : streams_) { for (auto& [ssrc, stream] : streams_) {
@@ -747,7 +710,6 @@ void AudioEngine::on_playback(int16_t* out, ma_uint32 frames) {
// embeds in the next packet) → PLC comfort noise. DRED and FEC both need the // embeds in the next packet) → PLC comfort noise. DRED and FEC both need the
// *next* packet already buffered, so copy it once and try each in turn. // *next* packet already buffered, so copy it once and try each in turn.
n = -1; n = -1;
#ifdef VOICECAT_HAS_OPUS
uint32_t next_ts = stream.playout_ts + static_cast<uint32_t>(frame_samples); uint32_t next_ts = stream.playout_ts + static_cast<uint32_t>(frame_samples);
size_t psz = stream.jitter.try_copy_front_payload( size_t psz = stream.jitter.try_copy_front_payload(
next_ts, stream.dred_payload_scratch_.data(), next_ts, stream.dred_payload_scratch_.data(),
@@ -777,7 +739,6 @@ void AudioEngine::on_playback(int16_t* out, ma_uint32 frames) {
stream.dred_payload_scratch_.data(), static_cast<int>(psz), stream.dred_payload_scratch_.data(), static_cast<int>(psz),
stream.decode_scratch.data(), frame_samples, /*fec=*/true); stream.decode_scratch.data(), frame_samples, /*fec=*/true);
} }
#endif
// 3. PLC: synthesize a continuation when no redundancy is available. // 3. PLC: synthesize a continuation when no redundancy is available.
if (n <= 0) { if (n <= 0) {
n = stream.decoder.decode(nullptr, 0, stream.decode_scratch.data(), n = stream.decoder.decode(nullptr, 0, stream.decode_scratch.data(),
@@ -858,10 +819,6 @@ void AudioEngine::on_playback(int16_t* out, ma_uint32 frames) {
int32_t s = static_cast<int32_t>(static_cast<float>(mix[i]) * ovol); int32_t s = static_cast<int32_t>(static_cast<float>(mix[i]) * ovol);
out[i] = static_cast<int16_t>(std::clamp(s, -32768, 32767)); out[i] = static_cast<int16_t>(std::clamp(s, -32768, 32767));
} }
#else
(void)out;
(void)frames;
#endif
} }
// External-playback timer (iOS VPIO): with no hardware playback device to "pull" frames, this // External-playback timer (iOS VPIO): with no hardware playback device to "pull" frames, this
@@ -986,11 +943,4 @@ bool AudioEngine::start_loopback_capture(int /*kind*/, int /*channels*/) { retur
void AudioEngine::stop_loopback_capture() {} void AudioEngine::stop_loopback_capture() {}
#endif // VOICECAT_HAS_LOOPBACK #endif // VOICECAT_HAS_LOOPBACK
#endif // VOICECAT_HAS_AUDIO
#ifndef VOICECAT_HAS_AUDIO
bool AudioEngine::start_loopback_capture(int /*kind*/, int /*channels*/) { return false; }
void AudioEngine::stop_loopback_capture() {}
#endif
} // namespace voicecat::audio } // namespace voicecat::audio

View File

@@ -25,14 +25,10 @@
#include <unordered_map> #include <unordered_map>
#include <vector> #include <vector>
#ifdef VOICECAT_HAS_AUDIO
// miniaudio single-header — MINIAUDIO_IMPLEMENTATION defined in audio_engine.cpp // miniaudio single-header — MINIAUDIO_IMPLEMENTATION defined in audio_engine.cpp
#include <miniaudio.h> #include <miniaudio.h>
#endif
#ifdef VOICECAT_HAS_OPUS
#include "codec/opus_codec.h" #include "codec/opus_codec.h"
#endif
#include "audio/apm_processor.h" #include "audio/apm_processor.h"
@@ -222,7 +218,6 @@ class AudioEngine {
// bounded-depth invariant. Returns 0 if the stream is unknown or not yet playing out. // bounded-depth invariant. Returns 0 if the stream is unknown or not yet playing out.
int32_t stream_playout_depth_samples(uint32_t ssrc) const; int32_t stream_playout_depth_samples(uint32_t ssrc) const;
#ifdef VOICECAT_HAS_OPUS
// Configure the Opus decoder for an incoming ssrc (must be called before // Configure the Opus decoder for an incoming ssrc (must be called before
// push_recv_frame for that ssrc). user_id/stream_id identify the source for the // push_recv_frame for that ssrc). user_id/stream_id identify the source for the
// pcm_sink_ callback. is_voice marks a MIC stream so the receive-side NR pass knows it may // pcm_sink_ callback. is_voice marks a MIC stream so the receive-side NR pass knows it may
@@ -230,7 +225,6 @@ class AudioEngine {
// Thread-safe. // Thread-safe.
void init_recv_stream(uint32_t ssrc, const codec::OpusParams& p, void init_recv_stream(uint32_t ssrc, const codec::OpusParams& p,
uint32_t user_id, uint32_t stream_id, bool is_voice); uint32_t user_id, uint32_t stream_id, bool is_voice);
#endif
// External PCM tap: callback fired once per decoded Opus frame per remote stream, on the // External PCM tap: callback fired once per decoded Opus frame per remote stream, on the
// playback (RT) thread. Matching signature to vc_pcm_sink_cb (cast at the C-ABI boundary). // playback (RT) thread. Matching signature to vc_pcm_sink_cb (cast at the C-ABI boundary).
@@ -254,7 +248,6 @@ class AudioEngine {
// is samples per channel; total samples written = samples_per_channel * channels. // is samples per channel; total samples written = samples_per_channel * channels.
void inject_capture(int kind, const int16_t* pcm, size_t samples_per_channel, int channels); void inject_capture(int kind, const int16_t* pcm, size_t samples_per_channel, int channels);
#ifdef VOICECAT_HAS_AUDIO
// TEST-ONLY — exposes the playback mixer without a real ma_device, so tests can verify // TEST-ONLY — exposes the playback mixer without a real ma_device, so tests can verify
// stereo mixing end-to-end (no audio hardware needed). Same logic the real playback // stereo mixing end-to-end (no audio hardware needed). Same logic the real playback
// callback uses; safe to call any time after start() (no ma_device touched). // callback uses; safe to call any time after start() (no ma_device touched).
@@ -312,7 +305,6 @@ class AudioEngine {
} }
} }
} }
#endif
#ifdef VOICECAT_HAS_LOOPBACK #ifdef VOICECAT_HAS_LOOPBACK
// TEST-ONLY — drives the loopback accumulator directly with an explicit callback, the // TEST-ONLY — drives the loopback accumulator directly with an explicit callback, the
@@ -354,7 +346,6 @@ class AudioEngine {
#endif #endif
private: private:
#ifdef VOICECAT_HAS_AUDIO
static void capture_data_cb(ma_device*, void*, const void*, ma_uint32); static void capture_data_cb(ma_device*, void*, const void*, ma_uint32);
static void playback_data_cb(ma_device*, void*, const void*, ma_uint32); static void playback_data_cb(ma_device*, void*, const void*, ma_uint32);
void on_capture(const int16_t* pcm, ma_uint32 frames); void on_capture(const int16_t* pcm, ma_uint32 frames);
@@ -403,7 +394,6 @@ class AudioEngine {
bool loopback_started_ = false; bool loopback_started_ = false;
int loopback_kind_ = 0; int loopback_kind_ = 0;
int loopback_channels_ = 1; // channel count the loopback device was opened with int loopback_channels_ = 1; // channel count the loopback device was opened with
#endif
#endif #endif
AudioParams params_{}; AudioParams params_{};
@@ -441,9 +431,7 @@ class AudioEngine {
// Per remote stream (protected by streams_mu_). // Per remote stream (protected by streams_mu_).
struct RemoteStream { struct RemoteStream {
JitterBuffer jitter; JitterBuffer jitter;
#ifdef VOICECAT_HAS_OPUS
codec::OpusDecoder decoder; codec::OpusDecoder decoder;
#endif
float gain = 1.0f; float gain = 1.0f;
bool mute = false; bool mute = false;
uint32_t playout_ts = 0; uint32_t playout_ts = 0;
@@ -484,9 +472,7 @@ class AudioEngine {
// DRED: pre-allocated scratch for loss recovery. dred_state_ is per-stream; see // DRED: pre-allocated scratch for loss recovery. dred_state_ is per-stream; see
// AudioEngine::dred_dec_ (shared). Allocated in init_recv_stream(); freed in remove_stream(). // AudioEngine::dred_dec_ (shared). Allocated in init_recv_stream(); freed in remove_stream().
#ifdef VOICECAT_HAS_OPUS
::OpusDRED* dred_state_ = nullptr; ::OpusDRED* dred_state_ = nullptr;
#endif
std::vector<uint8_t> dred_payload_scratch_; // pre-sized to 4000 bytes std::vector<uint8_t> dred_payload_scratch_; // pre-sized to 4000 bytes
// In-band FEC: whether the sender negotiated OPUS_SET_INBAND_FEC for this stream. // In-band FEC: whether the sender negotiated OPUS_SET_INBAND_FEC for this stream.
@@ -574,9 +560,7 @@ class AudioEngine {
std::atomic<void*> mixed_sink_user_{nullptr}; std::atomic<void*> mixed_sink_user_{nullptr};
bool external_playback_ = false; bool external_playback_ = false;
#ifdef VOICECAT_HAS_OPUS
::OpusDREDDecoder* dred_dec_ = nullptr; // shared DRED decoder; null if unsupported ::OpusDREDDecoder* dred_dec_ = nullptr; // shared DRED decoder; null if unsupported
#endif
static constexpr int64_t kTalkHangoverMs = 300; static constexpr int64_t kTalkHangoverMs = 300;
}; };

View File

@@ -2,8 +2,6 @@
namespace voicecat::codec { namespace voicecat::codec {
#ifdef VOICECAT_HAS_OPUS
// Map an intended channel/capture sample rate (Hz) to the Opus max-bandwidth constant. The // Map an intended channel/capture sample rate (Hz) to the Opus max-bandwidth constant. The
// codec always runs at 48 kHz internally (docs/voice.md §3); this caps the bandwidth the // codec always runs at 48 kHz internally (docs/voice.md §3); this caps the bandwidth the
// encoder will select so a channel can request narrowband/wideband audio for low-bitrate rooms. // encoder will select so a channel can request narrowband/wideband audio for low-bitrate rooms.
@@ -103,16 +101,4 @@ void OpusDecoder::destroy() {
if (dec_) { opus_decoder_destroy(dec_); dec_ = nullptr; } if (dec_) { opus_decoder_destroy(dec_); dec_ = nullptr; }
} }
#else // !VOICECAT_HAS_OPUS — stubs
bool OpusEncoder::init(const OpusParams&) { err_ = "OPUS not compiled in"; return false; }
int OpusEncoder::encode(const int16_t*, int, uint8_t*, int) { return -1; }
void OpusEncoder::destroy() {}
bool OpusDecoder::init(const OpusParams&) { err_ = "OPUS not compiled in"; return false; }
int OpusDecoder::decode(const uint8_t*, int, int16_t*, int, bool) { return -1; }
void OpusDecoder::destroy() {}
#endif // VOICECAT_HAS_OPUS
} // namespace voicecat::codec } // namespace voicecat::codec

View File

@@ -10,9 +10,7 @@
#include <cstdint> #include <cstdint>
#include <vector> #include <vector>
#ifdef VOICECAT_HAS_OPUS
#include <opus/opus.h> #include <opus/opus.h>
#endif
namespace voicecat::codec { namespace voicecat::codec {
@@ -73,11 +71,7 @@ class OpusEncoder {
const char* error_string() const { return err_; } const char* error_string() const { return err_; }
private: private:
#ifdef VOICECAT_HAS_OPUS
::OpusEncoder* enc_ = nullptr; ::OpusEncoder* enc_ = nullptr;
#else
void* enc_ = nullptr;
#endif
int frame_samples_ = 0; int frame_samples_ = 0;
int channels_ = 1; int channels_ = 1;
const char* err_ = nullptr; const char* err_ = nullptr;
@@ -104,10 +98,8 @@ class OpusDecoder {
// Decode a lost frame using pre-parsed DRED state from the next received packet. // Decode a lost frame using pre-parsed DRED state from the next received packet.
// dred_offset=0 means the frame immediately before the next packet. // dred_offset=0 means the frame immediately before the next packet.
// Returns frame_samples on success, -1 if DRED unavailable or decode failed. // Returns frame_samples on success, -1 if DRED unavailable or decode failed.
#ifdef VOICECAT_HAS_OPUS
int decode_dred(::OpusDRED* dred, int32_t dred_offset, int16_t* out_pcm, int max_samples); int decode_dred(::OpusDRED* dred, int32_t dred_offset, int16_t* out_pcm, int max_samples);
::OpusDecoder* raw() const { return dec_; } ::OpusDecoder* raw() const { return dec_; }
#endif
void destroy(); void destroy();
@@ -117,11 +109,7 @@ class OpusDecoder {
const char* error_string() const { return err_; } const char* error_string() const { return err_; }
private: private:
#ifdef VOICECAT_HAS_OPUS
::OpusDecoder* dec_ = nullptr; ::OpusDecoder* dec_ = nullptr;
#else
void* dec_ = nullptr;
#endif
int frame_samples_ = 0; int frame_samples_ = 0;
int channels_ = 1; int channels_ = 1;
const char* err_ = nullptr; const char* err_ = nullptr;

View File

@@ -1,7 +1,5 @@
#include "core/client.h" #include "core/client.h"
#ifdef VOICECAT_HAS_NET
#ifdef _WIN32 #ifdef _WIN32
# ifndef WIN32_LEAN_AND_MEAN # ifndef WIN32_LEAN_AND_MEAN
# define WIN32_LEAN_AND_MEAN # define WIN32_LEAN_AND_MEAN
@@ -1867,7 +1865,6 @@ vc_result vc_client::test_inject_capture(uint32_t stream_id, const int16_t* pcm,
} }
vc_result vc_client::list_devices(vc_device_kind kind, vc_device_list* out) { vc_result vc_client::list_devices(vc_device_kind kind, vc_device_list* out) {
#ifdef VOICECAT_HAS_AUDIO
// Works in any connection state — device pickers need to populate pre-connect. // Works in any connection state — device pickers need to populate pre-connect.
auto devices = voicecat::audio::AudioEngine::enumerate_devices(kind == VC_DEVICE_INPUT); auto devices = voicecat::audio::AudioEngine::enumerate_devices(kind == VC_DEVICE_INPUT);
@@ -1885,12 +1882,6 @@ vc_result vc_client::list_devices(vc_device_kind kind, vc_device_list* out) {
out->items = items; out->items = items;
out->count = devices.size(); out->count = devices.size();
return VC_OK; return VC_OK;
#else
(void)kind;
out->items = nullptr;
out->count = 0;
return VC_ERR_NOT_IMPLEMENTED;
#endif
} }
// ── M4: channel/user/stream snapshot getters ───────────────────────────────── // ── M4: channel/user/stream snapshot getters ─────────────────────────────────
@@ -2209,102 +2200,3 @@ void vc_client::run_talk_timer() {
std::this_thread::sleep_for(std::chrono::milliseconds(kTalkPollMs)); std::this_thread::sleep_for(std::chrono::milliseconds(kTalkPollMs));
} }
} }
#else // !VOICECAT_HAS_NET
// ── M0 stub implementations ───────────────────────────────────────────────────
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) cb_.on_event(cb_.user, &ev);
}
vc_result vc_client::connect(const char*, uint16_t) { return VC_ERR_NOT_IMPLEMENTED; }
vc_result vc_client::disconnect() { return VC_ERR_NOT_IMPLEMENTED; }
vc_result vc_client::authenticate_guest(const char*) { return VC_ERR_NOT_IMPLEMENTED; }
vc_result vc_client::authenticate_user(const char*, const char*) { return VC_ERR_NOT_IMPLEMENTED; }
vc_result vc_client::join_channel(uint32_t, const char*) { return VC_ERR_NOT_IMPLEMENTED; }
vc_result vc_client::leave_channel() { return VC_ERR_NOT_IMPLEMENTED; }
vc_result vc_client::join_voice() { return VC_ERR_NOT_IMPLEMENTED; }
vc_result vc_client::leave_voice() { return VC_ERR_NOT_IMPLEMENTED; }
vc_result vc_client::stream_start(const vc_stream_desc&, uint32_t*) { return VC_ERR_NOT_IMPLEMENTED; }
vc_result vc_client::stream_stop(uint32_t) { return VC_ERR_NOT_IMPLEMENTED; }
vc_result vc_client::set_input_device(uint32_t, const char*) { return VC_ERR_NOT_IMPLEMENTED; }
vc_result vc_client::set_capture_channels(uint32_t, uint32_t) { return VC_ERR_NOT_IMPLEMENTED; }
vc_result vc_client::set_input_mode(vc_input_mode) { return VC_ERR_NOT_IMPLEMENTED; }
vc_result vc_client::set_vad_threshold(float) { return VC_ERR_NOT_IMPLEMENTED; }
vc_result vc_client::set_input_gain(float) { return VC_ERR_NOT_IMPLEMENTED; }
vc_result vc_client::set_input_noise_reduction(bool) { return VC_ERR_NOT_IMPLEMENTED; }
vc_result vc_client::set_push_to_talk(bool) { return VC_ERR_NOT_IMPLEMENTED; }
vc_result vc_client::set_self_mute(bool, bool) { return VC_ERR_NOT_IMPLEMENTED; }
vc_result vc_client::set_remote_stream(uint32_t, uint32_t, float, bool, bool) {
return VC_ERR_NOT_IMPLEMENTED;
}
vc_result vc_client::get_remote_stream(uint32_t, uint32_t, vc_remote_stream_state*) {
return VC_ERR_NOT_IMPLEMENTED;
}
vc_result vc_client::send_text(vc_text_scope, uint32_t, const char*) { return VC_ERR_NOT_IMPLEMENTED; }
vc_result vc_client::list_devices(vc_device_kind, vc_device_list* out) {
out->items = nullptr;
out->count = 0;
return VC_ERR_NOT_IMPLEMENTED;
}
vc_result vc_client::get_stream_audio_config(uint32_t, uint32_t, vc_audio_config*) {
return VC_ERR_NOT_IMPLEMENTED;
}
vc_result vc_client::stream_feed_pcm(uint32_t, const int16_t*, size_t, uint32_t) {
return VC_ERR_NOT_IMPLEMENTED;
}
vc_result vc_client::set_pcm_sink(vc_pcm_sink_cb, void*) {
return VC_ERR_NOT_IMPLEMENTED;
}
vc_result vc_client::set_mixed_output_sink(vc_mixed_output_cb, void*) {
return VC_ERR_NOT_IMPLEMENTED;
}
vc_result vc_client::set_external_playback(bool) {
return VC_ERR_NOT_IMPLEMENTED;
}
vc_result vc_client::test_inject_capture(uint32_t, const int16_t*, size_t) {
return VC_ERR_NOT_IMPLEMENTED;
}
vc_result vc_client::list_channels(vc_channel_list* out) {
out->items = nullptr;
out->count = 0;
return VC_ERR_NOT_IMPLEMENTED;
}
vc_result vc_client::list_users(vc_user_list* out) {
out->items = nullptr;
out->count = 0;
return VC_ERR_NOT_IMPLEMENTED;
}
vc_result vc_client::list_user_streams(uint32_t, vc_stream_summary_list* out) {
out->items = nullptr;
out->count = 0;
return VC_ERR_NOT_IMPLEMENTED;
}
vc_result vc_client::confirm_server_identity(bool) { return VC_ERR_NOT_IMPLEMENTED; }
vc_result vc_client::get_server_identity_display(char*, size_t, size_t* out_len) {
if (out_len) *out_len = 0;
return VC_ERR_NOT_IMPLEMENTED;
}
vc_result vc_client::kick_user(uint32_t, const char*) { return VC_ERR_NOT_IMPLEMENTED; }
vc_result vc_client::ban_user(uint32_t, const char*, uint64_t) { return VC_ERR_NOT_IMPLEMENTED; }
vc_result vc_client::set_permission(uint32_t, const vc_permissions*) { return VC_ERR_NOT_IMPLEMENTED; }
vc_result vc_client::set_server_mute(uint32_t, bool, bool) { return VC_ERR_NOT_IMPLEMENTED; }
vc_result vc_client::move_user(uint32_t, uint32_t) { return VC_ERR_NOT_IMPLEMENTED; }
vc_result vc_client::create_channel(const vc_channel_info*) { return VC_ERR_NOT_IMPLEMENTED; }
vc_result vc_client::edit_channel(const vc_channel_info*) { return VC_ERR_NOT_IMPLEMENTED; }
vc_result vc_client::delete_channel(uint32_t) { return VC_ERR_NOT_IMPLEMENTED; }
vc_result vc_client::create_account(const char*, const char*) { return VC_ERR_NOT_IMPLEMENTED; }
vc_result vc_client::reset_password(const char*, const char*) { return VC_ERR_NOT_IMPLEMENTED; }
vc_result vc_client::delete_account(const char*) { return VC_ERR_NOT_IMPLEMENTED; }
vc_result vc_client::list_accounts() { return VC_ERR_NOT_IMPLEMENTED; }
vc_result vc_client::get_account_list(vc_account_list*) { return VC_ERR_NOT_IMPLEMENTED; }
vc_result vc_client::get_permissions(vc_permissions*) { return VC_ERR_NOT_IMPLEMENTED; }
vc_result vc_client::audio_suspend() { return VC_ERR_NOT_IMPLEMENTED; }
vc_result vc_client::audio_resume() { return VC_ERR_NOT_IMPLEMENTED; }
vc_result vc_client::audio_restart() { return VC_ERR_NOT_IMPLEMENTED; }
#endif // VOICECAT_HAS_NET

View File

@@ -6,8 +6,6 @@
#include "voicecat.h" #include "voicecat.h"
#ifdef VOICECAT_HAS_NET
#include <atomic> #include <atomic>
#include <condition_variable> #include <condition_variable>
#include <deque> #include <deque>
@@ -29,8 +27,6 @@
#include "session/session.h" #include "session/session.h"
#include "proto/voicecat.pb.h" #include "proto/voicecat.pb.h"
#endif // VOICECAT_HAS_NET
struct vc_client { struct vc_client {
vc_client(const vc_config& cfg, vc_callbacks cb); vc_client(const vc_config& cfg, vc_callbacks cb);
~vc_client(); ~vc_client();
@@ -118,11 +114,7 @@ struct vc_client {
vc_result get_permissions(vc_permissions* out); vc_result get_permissions(vc_permissions* out);
vc_connection_state state() const { vc_connection_state state() const {
#ifdef VOICECAT_HAS_NET
return state_net_.load(std::memory_order_acquire); return state_net_.load(std::memory_order_acquire);
#else
return state_;
#endif
} }
private: private:
@@ -131,7 +123,6 @@ struct vc_client {
vc_config cfg_{}; vc_config cfg_{};
vc_callbacks cb_{}; vc_callbacks cb_{};
#ifdef VOICECAT_HAS_NET
// ── M1: TCP/TLS control channel ───────────────────────────────────────────── // ── M1: TCP/TLS control channel ─────────────────────────────────────────────
std::atomic<vc_connection_state> state_net_{VC_STATE_DISCONNECTED}; std::atomic<vc_connection_state> state_net_{VC_STATE_DISCONNECTED};
@@ -339,12 +330,8 @@ struct vc_client {
// capture callback never allocates or races this pointer. // capture callback never allocates or races this pointer.
std::unique_ptr<voicecat::audio::ApmProcessor> mic_ns_; std::unique_ptr<voicecat::audio::ApmProcessor> mic_ns_;
// teardown_voice() is called both from run_io()'s own cleanup (on the io_thread_, when // teardown_voice() is called from run_io() and disconnect() concurrently; this mutex
// the read loop exits) and from disconnect() (on the caller's thread) -- without // makes it idempotent (avoids a double-join race on udp_thread_/talk_timer_thread_).
// serializing those two call sites, both can see udp_thread_/talk_timer_thread_ as
// joinable() at the same time and race to join() the same std::thread object (UB; an
// intermittent "No such process" std::system_error on Windows is the typical symptom).
// This mutex makes teardown_voice() idempotent under concurrent calls.
std::mutex teardown_mu_; std::mutex teardown_mu_;
// ── io_thread_ entry point ────────────────────────────────────────────────── // ── io_thread_ entry point ──────────────────────────────────────────────────
@@ -432,10 +419,6 @@ struct vc_client {
// Convenience event emitters. // Convenience event emitters.
void emit_error(vc_result r, const char* text); void emit_error(vc_result r, const char* text);
void emit_disconnected(vc_result r, const char* reason); void emit_disconnected(vc_result r, const char* reason);
#else // !VOICECAT_HAS_NET
vc_connection_state state_{VC_STATE_DISCONNECTED};
#endif
}; };
#endif // VOICECAT_CORE_CLIENT_H #endif // VOICECAT_CORE_CLIENT_H

View File

@@ -4,13 +4,10 @@
* Used for Argon2id password hashing (deliberately slow) and TLS handshakes so * Used for Argon2id password hashing (deliberately slow) and TLS handshakes so
* neither blocks the net thread. Real-time audio threads never use this. * neither blocks the net thread. Real-time audio threads never use this.
* *
* Requires VOICECAT_HAS_NET (Asio). Undefined when building without deps.
*/ */
#ifndef VOICECAT_CORE_WORKER_POOL_H #ifndef VOICECAT_CORE_WORKER_POOL_H
#define VOICECAT_CORE_WORKER_POOL_H #define VOICECAT_CORE_WORKER_POOL_H
#ifdef VOICECAT_HAS_NET
#include <asio/thread_pool.hpp> #include <asio/thread_pool.hpp>
#include <asio/post.hpp> #include <asio/post.hpp>
#include <cstddef> #include <cstddef>
@@ -37,5 +34,4 @@ class WorkerPool {
} // namespace voicecat } // namespace voicecat
#endif // VOICECAT_HAS_NET
#endif // VOICECAT_CORE_WORKER_POOL_H #endif // VOICECAT_CORE_WORKER_POOL_H

View File

@@ -1,7 +1,5 @@
#include "crypto/crypto.h" #include "crypto/crypto.h"
#ifdef VOICECAT_HAS_NET
#include <cstring> #include <cstring>
#include <fstream> #include <fstream>
#include <sstream> #include <sstream>
@@ -406,5 +404,3 @@ long SodiumMediaCrypto::open(const uint8_t* sealed, size_t len, const uint8_t* a
} }
} // namespace voicecat::crypto } // namespace voicecat::crypto
#endif // VOICECAT_HAS_NET

View File

@@ -12,8 +12,6 @@
#include <cstdint> #include <cstdint>
#include <string> #include <string>
#ifdef VOICECAT_HAS_NET
#include <array> #include <array>
#include <filesystem> #include <filesystem>
#include <functional> #include <functional>
@@ -186,25 +184,4 @@ class SodiumMediaCrypto final : public MediaCrypto {
} // namespace voicecat::crypto } // namespace voicecat::crypto
#else // !VOICECAT_HAS_NET — skeleton stubs
namespace voicecat::crypto {
class MediaCrypto {
public:
virtual ~MediaCrypto() = default;
virtual long seal(const uint8_t*, size_t, const uint8_t*, size_t, uint8_t*, size_t) = 0;
virtual long open(const uint8_t*, size_t, const uint8_t*, size_t, uint8_t*, size_t) = 0;
};
class SodiumMediaCrypto final : public MediaCrypto {
public:
long seal(const uint8_t*, size_t, const uint8_t*, size_t, uint8_t*, size_t) override { return -1; }
long open(const uint8_t*, size_t, const uint8_t*, size_t, uint8_t*, size_t) override { return -1; }
uint64_t peek_send_counter() const { return 0; }
};
} // namespace voicecat::crypto
#endif // VOICECAT_HAS_NET
#endif // VOICECAT_CRYPTO_CRYPTO_H #endif // VOICECAT_CRYPTO_CRYPTO_H

View File

@@ -1,7 +1,5 @@
#include "crypto/tofu_store.h" #include "crypto/tofu_store.h"
#ifdef VOICECAT_HAS_NET
#include <fstream> #include <fstream>
#include <sstream> #include <sstream>
#include <stdexcept> #include <stdexcept>
@@ -93,5 +91,3 @@ void TofuStore::save() const {
} }
} // namespace voicecat::crypto } // namespace voicecat::crypto
#endif // VOICECAT_HAS_NET

View File

@@ -7,8 +7,6 @@
#ifndef VOICECAT_CRYPTO_TOFU_STORE_H #ifndef VOICECAT_CRYPTO_TOFU_STORE_H
#define VOICECAT_CRYPTO_TOFU_STORE_H #define VOICECAT_CRYPTO_TOFU_STORE_H
#ifdef VOICECAT_HAS_NET
#include <array> #include <array>
#include <filesystem> #include <filesystem>
#include <mutex> #include <mutex>
@@ -62,5 +60,4 @@ class TofuStore {
} // namespace voicecat::crypto } // namespace voicecat::crypto
#endif // VOICECAT_HAS_NET
#endif // VOICECAT_CRYPTO_TOFU_STORE_H #endif // VOICECAT_CRYPTO_TOFU_STORE_H

View File

@@ -1,7 +1,5 @@
#include "net/transport.h" #include "net/transport.h"
#ifdef VOICECAT_HAS_NET
#include <cstring> #include <cstring>
#include "crypto/crypto.h" #include "crypto/crypto.h"
@@ -483,5 +481,3 @@ asio::ip::udp::endpoint UdpMediaChannel::local_endpoint() const {
} }
} // namespace voicecat::net } // namespace voicecat::net
#endif // VOICECAT_HAS_NET

View File

@@ -4,9 +4,8 @@
* Design: docs/architecture.md (Net thread), docs/protocol.md §1 (framing). * Design: docs/architecture.md (Net thread), docs/protocol.md §1 (framing).
* Implementation uses standalone Asio for sockets and timers. * Implementation uses standalone Asio for sockets and timers.
* *
* The real classes are compiled only when VOICECAT_HAS_NET is defined (dev/release/ * Design: docs/architecture.md (Net thread), docs/protocol.md §1 (framing).
* server-release — vcpkg deps on). The skeleton-preset stub definitions below keep the * Implementation uses standalone Asio for sockets and timers.
* no-deps build green.
*/ */
#ifndef VOICECAT_NET_TRANSPORT_H #ifndef VOICECAT_NET_TRANSPORT_H
#define VOICECAT_NET_TRANSPORT_H #define VOICECAT_NET_TRANSPORT_H
@@ -14,8 +13,6 @@
#include <cstdint> #include <cstdint>
#include <string> #include <string>
#ifdef VOICECAT_HAS_NET
#define ASIO_STANDALONE 1 #define ASIO_STANDALONE 1
#include <asio.hpp> #include <asio.hpp>
@@ -226,21 +223,4 @@ class UdpMediaChannel {
} // namespace voicecat::net } // namespace voicecat::net
#else // !VOICECAT_HAS_NET — skeleton stubs for the dev preset
namespace voicecat::net {
class TcpControlChannel {
public:
bool connected() const { return false; }
};
class UdpMediaChannel {
public:
bool bound() const { return false; }
};
} // namespace voicecat::net
#endif // VOICECAT_HAS_NET
#endif // VOICECAT_NET_TRANSPORT_H #endif // VOICECAT_NET_TRANSPORT_H

View File

@@ -1,7 +1,5 @@
#include "protocol/envelope.h" #include "protocol/envelope.h"
#ifdef VOICECAT_HAS_NET
#include "protocol/protocol.h" #include "protocol/protocol.h"
#include <atomic> #include <atomic>
@@ -30,5 +28,3 @@ uint64_t next_request_id() {
} }
} // namespace voicecat::protocol } // namespace voicecat::protocol
#endif // VOICECAT_HAS_NET

View File

@@ -5,13 +5,10 @@
* envelopes without touching proto types directly. All callers that do need * envelopes without touching proto types directly. All callers that do need
* the proto types can #include the generated header alongside this one. * the proto types can #include the generated header alongside this one.
* *
* Requires VOICECAT_HAS_NET (protobuf codegen).
*/ */
#ifndef VOICECAT_PROTOCOL_ENVELOPE_H #ifndef VOICECAT_PROTOCOL_ENVELOPE_H
#define VOICECAT_PROTOCOL_ENVELOPE_H #define VOICECAT_PROTOCOL_ENVELOPE_H
#ifdef VOICECAT_HAS_NET
#include <cstddef> #include <cstddef>
#include <cstdint> #include <cstdint>
#include <vector> #include <vector>
@@ -35,5 +32,4 @@ uint64_t next_request_id();
} // namespace voicecat::protocol } // namespace voicecat::protocol
#endif // VOICECAT_HAS_NET
#endif // VOICECAT_PROTOCOL_ENVELOPE_H #endif // VOICECAT_PROTOCOL_ENVELOPE_H

View File

@@ -23,8 +23,6 @@ std::pair<const User*, const Stream*> SessionModel::find_user_by_ssrc(uint32_t s
return {nullptr, nullptr}; return {nullptr, nullptr};
} }
#ifdef VOICECAT_HAS_NET
namespace { namespace {
void copy_channel_audio(Channel& ch, const voicecat::v1::AudioConfig& a) { void copy_channel_audio(Channel& ch, const voicecat::v1::AudioConfig& a) {
@@ -160,6 +158,4 @@ void SessionModel::apply_channel_event(const voicecat::v1::ChannelEvent& ev) {
} }
} }
#endif // VOICECAT_HAS_NET
} // namespace voicecat::session } // namespace voicecat::session

View File

@@ -12,9 +12,7 @@
#include <utility> #include <utility>
#include <vector> #include <vector>
#ifdef VOICECAT_HAS_NET
#include "proto/voicecat.pb.h" #include "proto/voicecat.pb.h"
#endif
namespace voicecat::session { namespace voicecat::session {
@@ -88,11 +86,9 @@ class SessionModel {
// Returns {nullptr, nullptr} if not found. // Returns {nullptr, nullptr} if not found.
std::pair<const User*, const Stream*> find_user_by_ssrc(uint32_t ssrc) const; std::pair<const User*, const Stream*> find_user_by_ssrc(uint32_t ssrc) const;
#ifdef VOICECAT_HAS_NET
void apply_snapshot(const voicecat::v1::ServerStateSnapshot& snap); void apply_snapshot(const voicecat::v1::ServerStateSnapshot& snap);
void apply_user_event(const voicecat::v1::UserEvent& ev); void apply_user_event(const voicecat::v1::UserEvent& ev);
void apply_channel_event(const voicecat::v1::ChannelEvent& ev); void apply_channel_event(const voicecat::v1::ChannelEvent& ev);
#endif
private: private:
std::vector<Channel> channels_; std::vector<Channel> channels_;

View File

@@ -24,8 +24,7 @@ and *how to drive the binaries by hand*.
| Preset | Binary dir | Deps | Build type | Server | Tools | Tests | Strip | Platform | What it's for | | Preset | Binary dir | Deps | Build type | Server | Tools | Tests | Strip | Platform | What it's for |
|--------|-----------|------|------------|--------|-------|-------|-------|----------|---------------| |--------|-----------|------|------------|--------|-------|-------|-------|----------|---------------|
| `vcpkg-common` | — | vcpkg | — | — | — | — | — | all | Hidden base. Sets the vcpkg toolchain wrapper ([`cmake/voicecat-toolchain.cmake`](../cmake/voicecat-toolchain.cmake)) which auto-resolves the triplet from the host platform. Not used directly. | | `vcpkg-common` | — | vcpkg | — | — | — | — | — | all | Hidden base. Sets the vcpkg toolchain wrapper ([`cmake/voicecat-toolchain.cmake`](../cmake/voicecat-toolchain.cmake)) which auto-resolves the triplet from the host platform. Not used directly. |
| `skeleton` | `build/skeleton` | none | Debug | ON | ON | ON | no | all | The M0 skeleton. Compiles with just a C++20 compiler — no `VCPKG_ROOT` needed. Subsystems are stubs (`VC_ERR_NOT_IMPLEMENTED`). Good for "does the repo even build" sanity checks. Runs 2 tests (smoke + frame_codec). | | `dev` | `build/dev` | vcpkg | Debug | ON | ON | ON | no | all | **The one you actually want.** Day-to-day development: real protocol, crypto, voice, server — everything. Builds server + tools + tests (29 tests). Works on Windows, Linux, and macOS (triplet auto-resolved). |
| `dev` | `build/dev` | vcpkg | Debug | ON | ON | ON | no | all | **The one you actually want.** Day-to-day development: real protocol, crypto, voice, server — everything. Builds server + tools + tests (21 tests). Works on Windows, Linux, and macOS (triplet auto-resolved). |
| `release` | `build/release` | vcpkg | Release | ON | ON | ON | no | all | Optimized build with the full test suite. Use to run tests against optimized code, profile, or catch optimizer-sensitive bugs. Symbols kept (not stripped) so stack traces and profiling remain useful. | | `release` | `build/release` | vcpkg | Release | ON | ON | ON | no | all | Optimized build with the full test suite. Use to run tests against optimized code, profile, or catch optimizer-sensitive bugs. Symbols kept (not stripped) so stack traces and profiling remain useful. |
| `server-release` | `build/server-release` | vcpkg | Release | ON | ON | OFF | **yes** | all | Production-shaped build for deployment. Optimized + stripped binaries (`-s`), no tests. This is what you'd ship/run — see [docs/deployment.md](deployment.md). | | `server-release` | `build/server-release` | vcpkg | Release | ON | ON | OFF | **yes** | all | Production-shaped build for deployment. Optimized + stripped binaries (`-s`), no tests. This is what you'd ship/run — see [docs/deployment.md](deployment.md). |
| `windows-client` | `build/windows-client` | vcpkg | Release | OFF | OFF | OFF | no | Windows | Produces a redistributable `voicecat.dll` for the C# WinForms client (M4). Static MinGW runtime — no `libgcc_s_seh-1.dll` etc. See [clients/windows/README.md](../clients/windows/README.md). | | `windows-client` | `build/windows-client` | vcpkg | Release | OFF | OFF | OFF | no | Windows | Produces a redistributable `voicecat.dll` for the C# WinForms client (M4). Static MinGW runtime — no `libgcc_s_seh-1.dll` etc. See [clients/windows/README.md](../clients/windows/README.md). |
@@ -33,10 +32,9 @@ and *how to drive the binaries by hand*.
| `apple-ios` | `build/apple-ios` | vcpkg | Release | OFF | OFF | OFF | no | macOS→iOS | **Scaffolding** — cross-compiled static `libvoicecat.a` for iOS device (`arm64-ios`). One XCFramework slice. Not yet CI-validated. | | `apple-ios` | `build/apple-ios` | vcpkg | Release | OFF | OFF | OFF | no | macOS→iOS | **Scaffolding** — cross-compiled static `libvoicecat.a` for iOS device (`arm64-ios`). One XCFramework slice. Not yet CI-validated. |
| `apple-ios-sim` | `build/apple-ios-sim` | vcpkg | Release | OFF | OFF | OFF | no | macOS→iOS sim | **Scaffolding** — cross-compiled static `libvoicecat.a` for iOS simulator (`arm64-ios-sim`). One XCFramework slice. Not yet CI-validated. | | `apple-ios-sim` | `build/apple-ios-sim` | vcpkg | Release | OFF | OFF | OFF | no | macOS→iOS sim | **Scaffolding** — cross-compiled static `libvoicecat.a` for iOS simulator (`arm64-ios-sim`). One XCFramework slice. Not yet CI-validated. |
So in practice there are three presets that matter for day-to-day work: So in practice there are two presets that matter for day-to-day work:
- **`dev`** — everything: real protocol, real voice, real manual testing. This is the loop you run constantly. - **`dev`** — everything: real protocol, real voice, real manual testing. This is the loop you run constantly.
- **`release`** — same suite, optimized. Run it when you want to check optimized behavior or profile. - **`release`** — same suite, optimized. Run it when you want to check optimized behavior or profile.
- **`skeleton`** — fast no-deps build to confirm the stub path still compiles (CI smoke check).
The rest are purpose-specific: `server-release` for deployment, `windows-client` for the DLL, The rest are purpose-specific: `server-release` for deployment, `windows-client` for the DLL,
`apple-*` for Apple platform slices. `apple-*` for Apple platform slices.
@@ -63,9 +61,9 @@ The preset set was cleaned up on 2026-06-18 (see `PROGRESS.md`). The old names m
| Old name | New name | Notes | | Old name | New name | Notes |
|----------|----------|-------| |----------|----------|-------|
| `dev` | `skeleton` | Renamed to reflect its actual purpose (no-deps stub smoke check). |
| `m1-dev` | `dev` | Renamed — the project is past M5, so milestone-named presets were misleading. This is now the default development preset. | | `m1-dev` | `dev` | Renamed — the project is past M5, so milestone-named presets were misleading. This is now the default development preset. |
| `m2-dev` | *(dropped)* | Was cache-identical to `m1-dev` (same flags, same triplet, only the binary dir differed). Removed. | | `m2-dev` | *(dropped)* | Was cache-identical to `m1-dev` (same flags, same triplet, only the binary dir differed). Removed. |
| `skeleton` | *(dropped)* | Removed — was a no-deps stub build mode used during M0. All subsystems are now fully implemented; the stub `#ifdef` scaffolding has been deleted. |
| `server-release` | `server-release` | Unchanged name; now stripped (`-s`) and auto-triplet. | | `server-release` | `server-release` | Unchanged name; now stripped (`-s`) and auto-triplet. |
| *(new)* | `release` | New: optimized build with tests on, symbols kept. | | *(new)* | `release` | New: optimized build with tests on, symbols kept. |
| `windows-client` | `windows-client` | Unchanged name; triplet now auto-resolved. | | `windows-client` | `windows-client` | Unchanged name; triplet now auto-resolved. |
@@ -77,7 +75,7 @@ was run.
## 2. One-time setup for the real-deps presets ## 2. One-time setup for the real-deps presets
`dev`, `release`, `server-release`, `windows-client`, and `apple-*` all need `VCPKG_ROOT` All presets except `vcpkg-common` need `VCPKG_ROOT`
pointing at a bootstrapped vcpkg checkout: pointing at a bootstrapped vcpkg checkout:
```bash ```bash

View File

@@ -1,7 +1,5 @@
#include "conn_session.h" #include "conn_session.h"
#ifdef VOICECAT_HAS_NET
#include <algorithm> #include <algorithm>
#include <chrono> #include <chrono>
#include <cstdio> #include <cstdio>
@@ -845,5 +843,3 @@ void ConnSession::send_disconnect_and_close(uint32_t code, const std::string& re
} }
} // namespace voicecat::server } // namespace voicecat::server
#endif // VOICECAT_HAS_NET

View File

@@ -11,8 +11,6 @@
#ifndef VOICECAT_SERVER_CONN_SESSION_H #ifndef VOICECAT_SERVER_CONN_SESSION_H
#define VOICECAT_SERVER_CONN_SESSION_H #define VOICECAT_SERVER_CONN_SESSION_H
#ifdef VOICECAT_HAS_NET
#include <array> #include <array>
#include <atomic> #include <atomic>
#include <chrono> #include <chrono>
@@ -184,5 +182,4 @@ class ConnSession : public std::enable_shared_from_this<ConnSession> {
} // namespace voicecat::server } // namespace voicecat::server
#endif // VOICECAT_HAS_NET
#endif // VOICECAT_SERVER_CONN_SESSION_H #endif // VOICECAT_SERVER_CONN_SESSION_H

View File

@@ -1,7 +1,5 @@
#include "db.h" #include "db.h"
#ifdef VOICECAT_HAS_NET
#include <chrono> #include <chrono>
#include <cstring> #include <cstring>
#include <random> #include <random>
@@ -612,5 +610,3 @@ int64_t Database::now_unix() const {
} }
} // namespace voicecat::server } // namespace voicecat::server
#endif // VOICECAT_HAS_NET

View File

@@ -9,8 +9,6 @@
#ifndef VOICECAT_SERVER_DB_H #ifndef VOICECAT_SERVER_DB_H
#define VOICECAT_SERVER_DB_H #define VOICECAT_SERVER_DB_H
#ifdef VOICECAT_HAS_NET
#include <cstdint> #include <cstdint>
#include <optional> #include <optional>
#include <string> #include <string>
@@ -152,5 +150,4 @@ class Database {
} // namespace voicecat::server } // namespace voicecat::server
#endif // VOICECAT_HAS_NET
#endif // VOICECAT_SERVER_DB_H #endif // VOICECAT_SERVER_DB_H

View File

@@ -1,7 +1,5 @@
#include "identity.h" #include "identity.h"
#ifdef VOICECAT_HAS_NET
#include <filesystem> #include <filesystem>
#include <stdexcept> #include <stdexcept>
@@ -35,5 +33,3 @@ bool ServerIdentityManager::init(const std::filesystem::path& data_dir,
} }
} // namespace voicecat::server } // namespace voicecat::server
#endif // VOICECAT_HAS_NET

View File

@@ -7,8 +7,6 @@
#ifndef VOICECAT_SERVER_IDENTITY_H #ifndef VOICECAT_SERVER_IDENTITY_H
#define VOICECAT_SERVER_IDENTITY_H #define VOICECAT_SERVER_IDENTITY_H
#ifdef VOICECAT_HAS_NET
#include <filesystem> #include <filesystem>
#include <string> #include <string>
@@ -36,5 +34,4 @@ class ServerIdentityManager {
} // namespace voicecat::server } // namespace voicecat::server
#endif // VOICECAT_HAS_NET
#endif // VOICECAT_SERVER_IDENTITY_H #endif // VOICECAT_SERVER_IDENTITY_H

View File

@@ -1,7 +1,5 @@
#include "media_relay.h" #include "media_relay.h"
#ifdef VOICECAT_HAS_NET
#include <array> #include <array>
#include <chrono> #include <chrono>
#include <cstdio> #include <cstdio>
@@ -179,5 +177,3 @@ void MediaRelay::on_udp_frame(const uint8_t* data, size_t len,
} }
} // namespace voicecat::server } // namespace voicecat::server
#endif // VOICECAT_HAS_NET

View File

@@ -16,8 +16,6 @@
#ifndef VOICECAT_SERVER_MEDIA_RELAY_H #ifndef VOICECAT_SERVER_MEDIA_RELAY_H
#define VOICECAT_SERVER_MEDIA_RELAY_H #define VOICECAT_SERVER_MEDIA_RELAY_H
#ifdef VOICECAT_HAS_NET
#include <memory> #include <memory>
#define ASIO_STANDALONE 1 #define ASIO_STANDALONE 1
@@ -71,5 +69,4 @@ class MediaRelay {
} // namespace voicecat::server } // namespace voicecat::server
#endif // VOICECAT_HAS_NET
#endif // VOICECAT_SERVER_MEDIA_RELAY_H #endif // VOICECAT_SERVER_MEDIA_RELAY_H

View File

@@ -3,8 +3,6 @@
#include <cstdio> #include <cstdio>
#include <csignal> #include <csignal>
#ifdef VOICECAT_HAS_NET
#define ASIO_STANDALONE 1 #define ASIO_STANDALONE 1
#include <asio.hpp> #include <asio.hpp>
#include <asio/signal_set.hpp> #include <asio/signal_set.hpp>
@@ -224,21 +222,3 @@ void Server::stop() {
} }
} // namespace voicecat::server } // namespace voicecat::server
#else // !VOICECAT_HAS_NET
namespace voicecat::server {
int Server::run() {
std::fprintf(stderr, "[server] stub: VOICECAT_HAS_NET not defined (build with dev preset)\n");
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\n", cfg_.bind_port);
return 0;
}
void Server::stop() {}
} // namespace voicecat::server
#endif // VOICECAT_HAS_NET

View File

@@ -1,7 +1,5 @@
#include "session_registry.h" #include "session_registry.h"
#ifdef VOICECAT_HAS_NET
#include <atomic> #include <atomic>
#include <chrono> #include <chrono>
#include <mutex> #include <mutex>
@@ -554,5 +552,3 @@ std::optional<voicecat::v1::AudioConfig> SessionRegistry::channel_audio_config(
} }
} // namespace voicecat::server } // namespace voicecat::server
#endif // VOICECAT_HAS_NET

View File

@@ -8,8 +8,6 @@
#ifndef VOICECAT_SERVER_SESSION_REGISTRY_H #ifndef VOICECAT_SERVER_SESSION_REGISTRY_H
#define VOICECAT_SERVER_SESSION_REGISTRY_H #define VOICECAT_SERVER_SESSION_REGISTRY_H
#ifdef VOICECAT_HAS_NET
#include <array> #include <array>
#include <atomic> #include <atomic>
#include <cstdint> #include <cstdint>
@@ -223,4 +221,3 @@ class SessionRegistry {
} // namespace voicecat::server } // namespace voicecat::server
#endif // VOICECAT_SERVER_SESSION_REGISTRY_H #endif // VOICECAT_SERVER_SESSION_REGISTRY_H
#endif // VOICECAT_SERVER_SESSION_REGISTRY_H

View File

@@ -6,7 +6,6 @@ target_link_libraries(test_smoke PRIVATE voicecat::voicecat)
target_compile_features(test_smoke PRIVATE cxx_std_20) target_compile_features(test_smoke PRIVATE cxx_std_20)
add_test(NAME smoke COMMAND test_smoke) add_test(NAME smoke COMMAND test_smoke)
# frame_codec has no third-party deps; runs under both skeleton and dev.
# Needs core/src on the include path to reach internal headers (protocol/, session/, etc.). # Needs core/src on the include path to reach internal headers (protocol/, session/, etc.).
add_executable(test_frame_codec test_frame_codec.cpp) add_executable(test_frame_codec test_frame_codec.cpp)
target_link_libraries(test_frame_codec PRIVATE voicecat::voicecat) target_link_libraries(test_frame_codec PRIVATE voicecat::voicecat)
@@ -14,8 +13,7 @@ target_compile_features(test_frame_codec PRIVATE cxx_std_20)
target_include_directories(test_frame_codec PRIVATE ${CMAKE_SOURCE_DIR}/core/src) target_include_directories(test_frame_codec PRIVATE ${CMAKE_SOURCE_DIR}/core/src)
add_test(NAME frame_codec COMMAND test_frame_codec) add_test(NAME frame_codec COMMAND test_frame_codec)
if(VOICECAT_USE_VCPKG_DEPS) set(VC_TEST_INTERNAL_INCLUDES
set(VC_TEST_INTERNAL_INCLUDES
${CMAKE_SOURCE_DIR}/core/src ${CMAKE_SOURCE_DIR}/core/src
${CMAKE_SOURCE_DIR}/server/src ${CMAKE_SOURCE_DIR}/server/src
${CMAKE_BINARY_DIR}/core/generated) # protobuf-generated headers ${CMAKE_BINARY_DIR}/core/generated) # protobuf-generated headers
@@ -247,4 +245,3 @@ if(VOICECAT_USE_VCPKG_DEPS)
target_include_directories(test_recv_noise_reduction PRIVATE ${VC_TEST_INTERNAL_INCLUDES}) target_include_directories(test_recv_noise_reduction PRIVATE ${VC_TEST_INTERNAL_INCLUDES})
add_test(NAME recv_noise_reduction COMMAND test_recv_noise_reduction) add_test(NAME recv_noise_reduction COMMAND test_recv_noise_reduction)
set_tests_properties(recv_noise_reduction PROPERTIES TIMEOUT 90) set_tests_properties(recv_noise_reduction PROPERTIES TIMEOUT 90)
endif()

View File

@@ -14,8 +14,6 @@
*/ */
#include <cstdio> #include <cstdio>
#ifdef VOICECAT_HAS_NET
#include <atomic> #include <atomic>
#include <chrono> #include <chrono>
#include <cmath> #include <cmath>
@@ -309,12 +307,3 @@ int main() {
std::printf("channel_samplerate: %d failure(s)\n", g_failures); std::printf("channel_samplerate: %d failure(s)\n", g_failures);
return 1; return 1;
} }
#else // !VOICECAT_HAS_NET
int main() {
std::printf("channel_samplerate: SKIP (VOICECAT_HAS_NET not defined)\n");
return 0;
}
#endif // VOICECAT_HAS_NET

View File

@@ -21,8 +21,6 @@
*/ */
#include <cstdio> #include <cstdio>
#ifdef VOICECAT_HAS_NET
#include <atomic> #include <atomic>
#include <chrono> #include <chrono>
#include <condition_variable> #include <condition_variable>
@@ -333,12 +331,3 @@ int main() {
std::printf("channel_user_list_abi: %d failure(s)\n", g_failures); std::printf("channel_user_list_abi: %d failure(s)\n", g_failures);
return 1; return 1;
} }
#else // !VOICECAT_HAS_NET
int main() {
std::printf("channel_user_list_abi: SKIP (VOICECAT_HAS_NET not defined)\n");
return 0;
}
#endif // VOICECAT_HAS_NET

View File

@@ -9,8 +9,6 @@
*/ */
#include <cstdio> #include <cstdio>
#ifdef VOICECAT_HAS_NET
#include <atomic> #include <atomic>
#include <chrono> #include <chrono>
#include <condition_variable> #include <condition_variable>
@@ -210,12 +208,3 @@ int main() {
std::printf("disconnect_left: %d failure(s)\n", g_failures); std::printf("disconnect_left: %d failure(s)\n", g_failures);
return 1; return 1;
} }
#else // !VOICECAT_HAS_NET
int main() {
std::printf("disconnect_left: SKIP (VOICECAT_HAS_NET not defined)\n");
return 0;
}
#endif // VOICECAT_HAS_NET

View File

@@ -9,8 +9,6 @@
*/ */
#include <cstdio> #include <cstdio>
#ifdef VOICECAT_HAS_NET
#include <atomic> #include <atomic>
#include <chrono> #include <chrono>
#include <condition_variable> #include <condition_variable>
@@ -268,12 +266,3 @@ int main() {
std::printf("dred_toggle: %d failure(s)\n", g_failures); std::printf("dred_toggle: %d failure(s)\n", g_failures);
return 1; return 1;
} }
#else // !VOICECAT_HAS_NET
int main() {
std::printf("dred_toggle: SKIP (VOICECAT_HAS_NET not defined)\n");
return 0;
}
#endif // VOICECAT_HAS_NET

View File

@@ -15,8 +15,6 @@
*/ */
#include <cstdio> #include <cstdio>
#ifdef VOICECAT_HAS_NET
#include <atomic> #include <atomic>
#include <chrono> #include <chrono>
#include <condition_variable> #include <condition_variable>
@@ -445,12 +443,3 @@ int main() {
std::printf("external_pcm: %d failure(s)\n", g_failures); std::printf("external_pcm: %d failure(s)\n", g_failures);
return 1; return 1;
} }
#else // !VOICECAT_HAS_NET
int main() {
std::printf("external_pcm: SKIP (VOICECAT_HAS_NET not defined)\n");
return 0;
}
#endif // VOICECAT_HAS_NET

View File

@@ -17,8 +17,6 @@
*/ */
#include <cstdio> #include <cstdio>
#ifdef VOICECAT_HAS_NET
#include <atomic> #include <atomic>
#include <chrono> #include <chrono>
#include <cmath> #include <cmath>
@@ -318,12 +316,3 @@ int main() {
std::printf("frame_ms_reframe: %d failure(s)\n", g_failures); std::printf("frame_ms_reframe: %d failure(s)\n", g_failures);
return 1; return 1;
} }
#else // !VOICECAT_HAS_NET
int main() {
std::printf("frame_ms_reframe: SKIP (VOICECAT_HAS_NET not defined)\n");
return 0;
}
#endif // VOICECAT_HAS_NET

View File

@@ -10,8 +10,6 @@
#include <cstdio> #include <cstdio>
#include <cstring> #include <cstring>
#ifdef VOICECAT_HAS_NET
#include <atomic> #include <atomic>
#include <chrono> #include <chrono>
#include <condition_variable> #include <condition_variable>
@@ -255,12 +253,3 @@ int main() {
std::printf("m1_integration: %d failure(s)\n", g_failures); std::printf("m1_integration: %d failure(s)\n", g_failures);
return 1; return 1;
} }
#else // !VOICECAT_HAS_NET
int main() {
std::printf("m1_integration: SKIP (VOICECAT_HAS_NET not defined)\n");
return 0;
}
#endif // VOICECAT_HAS_NET

View File

@@ -14,8 +14,6 @@
#include <cstdio> #include <cstdio>
#include <cstring> #include <cstring>
#ifdef VOICECAT_HAS_NET
#include <atomic> #include <atomic>
#include <chrono> #include <chrono>
#include <condition_variable> #include <condition_variable>
@@ -53,9 +51,7 @@
#include "server.h" #include "server.h"
#include "db.h" #include "db.h"
#ifdef VOICECAT_HAS_OPUS
#include "codec/opus_codec.h" #include "codec/opus_codec.h"
#endif
using namespace voicecat; using namespace voicecat;
using namespace voicecat::net; using namespace voicecat::net;
@@ -441,7 +437,6 @@ int main() {
constexpr int kFramesToSend = 50; constexpr int kFramesToSend = 50;
constexpr int kFrameSamples = 960; // 20 ms @48 kHz constexpr int kFrameSamples = 960; // 20 ms @48 kHz
#ifdef VOICECAT_HAS_OPUS
voicecat::codec::OpusEncoder enc; voicecat::codec::OpusEncoder enc;
{ {
voicecat::codec::OpusParams p; voicecat::codec::OpusParams p;
@@ -450,24 +445,17 @@ int main() {
p.fec = true; p.fec = true;
CHECK(enc.init(p)); CHECK(enc.init(p));
} }
#endif
std::vector<uint8_t> aead_buf(4096); // scratch std::vector<uint8_t> aead_buf(4096); // scratch
for (int i = 0; i < kFramesToSend; ++i) { for (int i = 0; i < kFramesToSend; ++i) {
auto pcm = make_sine_frame(i, kFrameSamples); auto pcm = make_sine_frame(i, kFrameSamples);
#ifdef VOICECAT_HAS_OPUS
uint8_t opus_buf[1000]; uint8_t opus_buf[1000];
int opus_len = enc.encode(pcm.data(), kFrameSamples, opus_buf, sizeof(opus_buf)); int opus_len = enc.encode(pcm.data(), kFrameSamples, opus_buf, sizeof(opus_buf));
if (opus_len <= 0) continue; if (opus_len <= 0) continue;
const uint8_t* payload = opus_buf; const uint8_t* payload = opus_buf;
size_t payload_len = static_cast<size_t>(opus_len); size_t payload_len = static_cast<size_t>(opus_len);
#else
// Fallback: use raw PCM as synthetic payload
const uint8_t* payload = reinterpret_cast<const uint8_t*>(pcm.data());
size_t payload_len = pcm.size() * sizeof(int16_t);
#endif
// Build the voice frame header (AAD). // Build the voice frame header (AAD).
VoiceFrame hdr; VoiceFrame hdr;
@@ -494,9 +482,7 @@ int main() {
std::this_thread::sleep_for(std::chrono::milliseconds(20)); std::this_thread::sleep_for(std::chrono::milliseconds(20));
} }
#ifdef VOICECAT_HAS_OPUS
enc.destroy(); enc.destroy();
#endif
// ── B collects received frames (2s window after last send) ──────────────── // ── B collects received frames (2s window after last send) ────────────────
int recv_count = 0, decrypt_ok = 0; int recv_count = 0, decrypt_ok = 0;
@@ -541,12 +527,3 @@ int main() {
std::printf("m2_voice: %d failure(s)\n", g_failures); std::printf("m2_voice: %d failure(s)\n", g_failures);
return 1; return 1;
} }
#else // !VOICECAT_HAS_NET
int main() {
std::printf("m2_voice: SKIP (VOICECAT_HAS_NET not defined)\n");
return 0;
}
#endif // VOICECAT_HAS_NET

View File

@@ -20,8 +20,6 @@
*/ */
#include <cstdio> #include <cstdio>
#ifdef VOICECAT_HAS_NET
#include <atomic> #include <atomic>
#include <chrono> #include <chrono>
#include <cmath> #include <cmath>
@@ -387,12 +385,3 @@ int main() {
std::printf("m3_multistream: %d failure(s)\n", g_failures); std::printf("m3_multistream: %d failure(s)\n", g_failures);
return 1; return 1;
} }
#else // !VOICECAT_HAS_NET
int main() {
std::printf("m3_multistream: SKIP (VOICECAT_HAS_NET not defined)\n");
return 0;
}
#endif // VOICECAT_HAS_NET

View File

@@ -13,8 +13,6 @@
#include <cstdio> #include <cstdio>
#include <cstring> #include <cstring>
#ifdef VOICECAT_HAS_NET
#include <atomic> #include <atomic>
#include <chrono> #include <chrono>
#include <condition_variable> #include <condition_variable>
@@ -265,12 +263,3 @@ int main() {
std::printf("m5_admin_accounts: %d failure(s)\n", g_failures); std::printf("m5_admin_accounts: %d failure(s)\n", g_failures);
return 1; return 1;
} }
#else // !VOICECAT_HAS_NET
int main() {
std::printf("m5_admin_accounts: SKIP (VOICECAT_HAS_NET not defined)\n");
return 0;
}
#endif // VOICECAT_HAS_NET

View File

@@ -12,8 +12,6 @@
#include <cstdio> #include <cstdio>
#include <cstring> #include <cstring>
#ifdef VOICECAT_HAS_NET
#include <atomic> #include <atomic>
#include <chrono> #include <chrono>
#include <condition_variable> #include <condition_variable>
@@ -308,12 +306,3 @@ int main() {
std::printf("m5_channel_crud: %d failure(s)\n", g_failures); std::printf("m5_channel_crud: %d failure(s)\n", g_failures);
return 1; return 1;
} }
#else // !VOICECAT_HAS_NET
int main() {
std::printf("m5_channel_crud: SKIP (VOICECAT_HAS_NET not defined)\n");
return 0;
}
#endif // VOICECAT_HAS_NET

View File

@@ -10,8 +10,6 @@
#include <cstdio> #include <cstdio>
#include <cstring> #include <cstring>
#ifdef VOICECAT_HAS_NET
#include <atomic> #include <atomic>
#include <chrono> #include <chrono>
#include <condition_variable> #include <condition_variable>
@@ -320,12 +318,3 @@ int main() {
std::printf("m5_kick_ban_move_mute: %d failure(s)\n", g_failures); std::printf("m5_kick_ban_move_mute: %d failure(s)\n", g_failures);
return 1; return 1;
} }
#else // !VOICECAT_HAS_NET
int main() {
std::printf("m5_kick_ban_move_mute: SKIP (VOICECAT_HAS_NET not defined)\n");
return 0;
}
#endif // VOICECAT_HAS_NET

View File

@@ -12,8 +12,6 @@
#include <cstdio> #include <cstdio>
#include <cstring> #include <cstring>
#ifdef VOICECAT_HAS_NET
#include <atomic> #include <atomic>
#include <chrono> #include <chrono>
#include <condition_variable> #include <condition_variable>
@@ -255,12 +253,3 @@ int main() {
std::printf("m5_permissions: %d failure(s)\n", g_failures); std::printf("m5_permissions: %d failure(s)\n", g_failures);
return 1; return 1;
} }
#else // !VOICECAT_HAS_NET
int main() {
std::printf("m5_permissions: SKIP (VOICECAT_HAS_NET not defined)\n");
return 0;
}
#endif // VOICECAT_HAS_NET

View File

@@ -20,14 +20,6 @@ static int g_failures = 0;
++g_failures; \ ++g_failures; \
}} while (0) }} while (0)
#ifndef VOICECAT_HAS_OPUS
int main() {
std::printf("opus_codec: VOICECAT_HAS_OPUS not defined — skipped\n");
return 0;
}
#else
static constexpr int kSampleRate = 48000; static constexpr int kSampleRate = 48000;
static constexpr int kFrameMs = 20; static constexpr int kFrameMs = 20;
@@ -145,5 +137,3 @@ int main() {
std::printf("opus_codec: %d test(s) FAILED\n", g_failures); std::printf("opus_codec: %d test(s) FAILED\n", g_failures);
return 1; return 1;
} }
#endif // VOICECAT_HAS_OPUS

View File

@@ -13,8 +13,6 @@
*/ */
#include <cstdio> #include <cstdio>
#ifdef VOICECAT_HAS_NET
#include <atomic> #include <atomic>
#include <chrono> #include <chrono>
#include <condition_variable> #include <condition_variable>
@@ -195,12 +193,3 @@ int main() {
std::printf("reaper_timeout: %d failure(s)\n", g_failures); std::printf("reaper_timeout: %d failure(s)\n", g_failures);
return 1; return 1;
} }
#else // !VOICECAT_HAS_NET
int main() {
std::printf("reaper_timeout: SKIP (VOICECAT_HAS_NET not defined)\n");
return 0;
}
#endif // VOICECAT_HAS_NET

View File

@@ -1,9 +1,8 @@
/* /*
* test_smoke — verifies the core links and the C ABI behaves as specified for M0. * test_smoke — verifies the core links and the C ABI behaves as specified.
* *
* This is intentionally a behavior test, not a "does it compile" check: it asserts the * 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 * documented contract (version present, handle lifecycle, invalid-arg guards).
* unimplemented calls report VC_ERR_NOT_IMPLEMENTED rather than crashing).
*/ */
#include <cstdio> #include <cstdio>
#include <cstring> #include <cstring>
@@ -25,8 +24,6 @@ int main() {
CHECK(vc_version_string() != nullptr); CHECK(vc_version_string() != nullptr);
CHECK(std::strlen(vc_version_string()) > 0); CHECK(std::strlen(vc_version_string()) > 0);
CHECK(std::strcmp(vc_result_string(VC_OK), "ok") == 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. // Null-config create is rejected; valid create yields a handle.
vc_callbacks cb{}; vc_callbacks cb{};
CHECK(vc_client_create(nullptr, cb) == nullptr); CHECK(vc_client_create(nullptr, cb) == nullptr);
@@ -43,34 +40,26 @@ int main() {
CHECK(vc_connect(c, nullptr, 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); CHECK(vc_send_text(c, VC_TEXT_CHANNEL, 0, nullptr) == VC_ERR_INVALID_ARG);
// Under skeleton preset: NOT_IMPLEMENTED. Under dev: VC_OK (async connect).
vc_result rc_connect = vc_connect(c, "127.0.0.1", 8384); vc_result rc_connect = vc_connect(c, "127.0.0.1", 8384);
CHECK(rc_connect == VC_ERR_NOT_IMPLEMENTED || rc_connect == VC_OK); CHECK(rc_connect == VC_OK);
// Auth before connected (or on a stub) → NOT_CONNECTED or NOT_IMPLEMENTED. // Auth before connected → NOT_CONNECTED.
{ {
vc_config cfg2 = cfg; vc_config cfg2 = cfg;
vc_client* c2 = vc_client_create(&cfg2, cb); vc_client* c2 = vc_client_create(&cfg2, cb);
vc_result rc_auth = vc_authenticate_guest(c2, "nick"); vc_result rc_auth = vc_authenticate_guest(c2, "nick");
CHECK(rc_auth == VC_ERR_NOT_IMPLEMENTED || rc_auth == VC_ERR_NOT_CONNECTED); CHECK(rc_auth == VC_ERR_NOT_CONNECTED);
vc_client_destroy(c2); vc_client_destroy(c2);
} }
// join_channel before connected → NOT_CONNECTED or NOT_IMPLEMENTED. // join_channel before connected → NOT_CONNECTED.
vc_result rc_join = vc_join_channel(c, 1, nullptr); vc_result rc_join = vc_join_channel(c, 1, nullptr);
CHECK(rc_join == VC_ERR_NOT_IMPLEMENTED || rc_join == VC_ERR_NOT_CONNECTED); CHECK(rc_join == VC_ERR_NOT_CONNECTED);
vc_device_list dl{}; vc_device_list dl{};
vc_result rc_devices = vc_list_devices(c, VC_DEVICE_INPUT, &dl); vc_result rc_devices = vc_list_devices(c, VC_DEVICE_INPUT, &dl);
#ifdef VOICECAT_HAS_AUDIO // Never assert count > 0 — a headless CI agent may report zero audio devices.
// Real device enumeration is wired up once miniaudio is linked in (post-M3 follow-up).
// Never assert count > 0 here — a headless CI build agent may legitimately report zero
// audio devices; only that the call itself succeeded.
CHECK(rc_devices == VC_OK); CHECK(rc_devices == VC_OK);
#else
CHECK(rc_devices == VC_ERR_NOT_IMPLEMENTED);
CHECK(dl.count == 0);
#endif
vc_free_device_list(&dl); vc_free_device_list(&dl);
vc_client_destroy(c); vc_client_destroy(c);

View File

@@ -19,8 +19,6 @@
*/ */
#include <cstdio> #include <cstdio>
#ifdef VOICECAT_HAS_NET
#include <atomic> #include <atomic>
#include <chrono> #include <chrono>
#include <condition_variable> #include <condition_variable>
@@ -374,12 +372,3 @@ int main() {
std::printf("tofu_flow: %d failure(s)\n", g_failures); std::printf("tofu_flow: %d failure(s)\n", g_failures);
return 1; return 1;
} }
#else // !VOICECAT_HAS_NET
int main() {
std::printf("tofu_flow: SKIP (VOICECAT_HAS_NET not defined)\n");
return 0;
}
#endif // VOICECAT_HAS_NET

View File

@@ -18,8 +18,6 @@
*/ */
#include <cstdio> #include <cstdio>
#ifdef VOICECAT_HAS_NET
#include <atomic> #include <atomic>
#include <chrono> #include <chrono>
#include <cmath> #include <cmath>
@@ -35,12 +33,8 @@
#include "server.h" #include "server.h"
#include "db.h" #include "db.h"
#ifdef VOICECAT_HAS_AUDIO
#include "audio/audio_engine.h" #include "audio/audio_engine.h"
#endif
#ifdef VOICECAT_HAS_OPUS
#include "codec/opus_codec.h" #include "codec/opus_codec.h"
#endif
// ── Event tracking (same shape as test_m3_multistream.cpp) ────────────────────── // ── Event tracking (same shape as test_m3_multistream.cpp) ──────────────────────
@@ -159,7 +153,6 @@ static void test_device_enumeration() {
for (vc_device_kind kind : {VC_DEVICE_INPUT, VC_DEVICE_OUTPUT}) { for (vc_device_kind kind : {VC_DEVICE_INPUT, VC_DEVICE_OUTPUT}) {
vc_device_list dl{}; vc_device_list dl{};
vc_result r = vc_list_devices(c, kind, &dl); vc_result r = vc_list_devices(c, kind, &dl);
#ifdef VOICECAT_HAS_AUDIO
CHECK(r == VC_OK); CHECK(r == VC_OK);
// Headless CI build agents may legitimately report zero devices — never assert // Headless CI build agents may legitimately report zero devices — never assert
// count > 0, only that the call itself succeeded and the list is well-formed. // count > 0, only that the call itself succeeded and the list is well-formed.
@@ -167,9 +160,6 @@ static void test_device_enumeration() {
CHECK(dl.items[i].id != nullptr); CHECK(dl.items[i].id != nullptr);
CHECK(dl.items[i].name != nullptr); CHECK(dl.items[i].name != nullptr);
} }
#else
CHECK(r == VC_ERR_NOT_IMPLEMENTED);
#endif
vc_free_device_list(&dl); vc_free_device_list(&dl);
vc_free_device_list(&dl); // idempotent — must not crash on a second call vc_free_device_list(&dl); // idempotent — must not crash on a second call
} }
@@ -179,7 +169,6 @@ static void test_device_enumeration() {
} }
// ── 4. Stereo playback mixer (white-box, no audio hardware needed) ────────────── // ── 4. Stereo playback mixer (white-box, no audio hardware needed) ──────────────
#if defined(VOICECAT_HAS_AUDIO) && defined(VOICECAT_HAS_OPUS)
static void test_stereo_mix() { static void test_stereo_mix() {
voicecat::audio::AudioEngine engine; voicecat::audio::AudioEngine engine;
voicecat::audio::AudioParams p; voicecat::audio::AudioParams p;
@@ -245,7 +234,7 @@ static void test_stereo_mix() {
// and mixes — asserting L != R across the frame. A mono-downmixed-then-upmixed bitstream would // and mixes — asserting L != R across the frame. A mono-downmixed-then-upmixed bitstream would
// have L == R. Mirrors test_stereo_mix but routes the encode side through the loopback // have L == R. Mirrors test_stereo_mix but routes the encode side through the loopback
// accumulator path that the fix touches (feed_loopback_for_test → on_loopback's accumulator). // accumulator path that the fix touches (feed_loopback_for_test → on_loopback's accumulator).
#if defined(VOICECAT_HAS_LOOPBACK) && defined(VOICECAT_HAS_OPUS) #if defined(VOICECAT_HAS_LOOPBACK)
static void test_loopback_stereo_capture() { static void test_loopback_stereo_capture() {
voicecat::audio::AudioEngine engine; voicecat::audio::AudioEngine engine;
voicecat::audio::AudioParams p; voicecat::audio::AudioParams p;
@@ -382,14 +371,12 @@ static void test_playout_resync() {
engine.stop(); engine.stop();
std::printf("test_playout_resync: ok (energy=%lld)\n", static_cast<long long>(energy)); std::printf("test_playout_resync: ok (energy=%lld)\n", static_cast<long long>(energy));
} }
#endif // VOICECAT_HAS_AUDIO && VOICECAT_HAS_OPUS
// ── 5. Capture-frame accumulation (white-box, no audio hardware needed) ────────── // ── 5. Capture-frame accumulation (white-box, no audio hardware needed) ──────────
// Regression for the capture-side analogue of the playback ring fix: miniaudio's capture // Regression for the capture-side analogue of the playback ring fix: miniaudio's capture
// callback fires at the hardware period (commonly 480 samples on WASAPI shared mode), while // callback fires at the hardware period (commonly 480 samples on WASAPI shared mode), while
// opus_encode() requires exactly frame_samples_ (960). Sub-frame chunks must be accumulated; // opus_encode() requires exactly frame_samples_ (960). Sub-frame chunks must be accumulated;
// the callback must receive exactly 960-sample frames regardless of input chunk size. // the callback must receive exactly 960-sample frames regardless of input chunk size.
#ifdef VOICECAT_HAS_AUDIO
static void test_capture_frame_accumulation() { static void test_capture_frame_accumulation() {
voicecat::audio::AudioEngine engine; voicecat::audio::AudioEngine engine;
voicecat::audio::AudioParams p; voicecat::audio::AudioParams p;
@@ -436,7 +423,6 @@ static void test_capture_frame_accumulation() {
engine.stop(); engine.stop();
std::printf("test_capture_frame_accumulation: ok (callbacks=%d)\n", call_count.load()); std::printf("test_capture_frame_accumulation: ok (callbacks=%d)\n", call_count.load());
} }
#endif // VOICECAT_HAS_AUDIO
// ── 2/3. VAD + PTT gate, through the real ABI against a real server ───────────── // ── 2/3. VAD + PTT gate, through the real ABI against a real server ─────────────
static void test_vad_and_ptt_gate() { static void test_vad_and_ptt_gate() {
@@ -601,7 +587,6 @@ static void test_vad_and_ptt_gate() {
// downmix). Mirrors test_loopback_stereo_capture but routes through the mic capture // downmix). Mirrors test_loopback_stereo_capture but routes through the mic capture
// accumulator (feed_capture_for_test with channels=2) instead of the loopback path. // accumulator (feed_capture_for_test with channels=2) instead of the loopback path.
// This is the headless CI test for the iOS stereo built-in mic feature (Part D). // This is the headless CI test for the iOS stereo built-in mic feature (Part D).
#if defined(VOICECAT_HAS_AUDIO) && defined(VOICECAT_HAS_OPUS)
static void test_stereo_mic_capture() { static void test_stereo_mic_capture() {
voicecat::audio::AudioEngine engine; voicecat::audio::AudioEngine engine;
voicecat::audio::AudioParams p; voicecat::audio::AudioParams p;
@@ -674,7 +659,6 @@ static void test_stereo_mic_capture() {
std::printf("test_stereo_mic_capture: ok (total_diff=%lld, seen_channels=%d)\n", std::printf("test_stereo_mic_capture: ok (total_diff=%lld, seen_channels=%d)\n",
static_cast<long long>(total_diff), seen_channels); static_cast<long long>(total_diff), seen_channels);
} }
#endif
// ── 6. Stereo mic capture on a MONO channel (downmix safety) ────────────────── // ── 6. Stereo mic capture on a MONO channel (downmix safety) ──────────────────
// A stereo mic (vc_set_capture_channels=2) can be enabled while on a mono channel. The mic // A stereo mic (vc_set_capture_channels=2) can be enabled while on a mono channel. The mic
@@ -683,7 +667,6 @@ static void test_stereo_mic_capture() {
// opus_encode makes it read 2× the samples it should (wrong pitch / garbage). This mirrors that // opus_encode makes it read 2× the samples it should (wrong pitch / garbage). This mirrors that
// fold and proves the result is a valid mono bitstream that decodes to the expected averaged // fold and proves the result is a valid mono bitstream that decodes to the expected averaged
// signal, rather than half-length junk. // signal, rather than half-length junk.
#if defined(VOICECAT_HAS_AUDIO) && defined(VOICECAT_HAS_OPUS)
static void test_stereo_mic_mono_channel() { static void test_stereo_mic_mono_channel() {
voicecat::codec::OpusParams mono_params; voicecat::codec::OpusParams mono_params;
mono_params.stereo = false; // mono channel — encoder is mono mono_params.stereo = false; // mono channel — encoder is mono
@@ -727,11 +710,9 @@ static void test_stereo_mic_mono_channel() {
std::printf("test_stereo_mic_mono_channel: ok (opus_len=%d, energy=%lld)\n", std::printf("test_stereo_mic_mono_channel: ok (opus_len=%d, energy=%lld)\n",
opus_len, static_cast<long long>(energy)); opus_len, static_cast<long long>(energy));
} }
#endif
int main() { int main() {
test_device_enumeration(); test_device_enumeration();
#if defined(VOICECAT_HAS_AUDIO) && defined(VOICECAT_HAS_OPUS)
test_stereo_mix(); test_stereo_mix();
#if defined(VOICECAT_HAS_LOOPBACK) #if defined(VOICECAT_HAS_LOOPBACK)
test_loopback_stereo_capture(); test_loopback_stereo_capture();
@@ -739,10 +720,7 @@ int main() {
test_stereo_mic_capture(); test_stereo_mic_capture();
test_stereo_mic_mono_channel(); test_stereo_mic_mono_channel();
test_playout_resync(); test_playout_resync();
#endif
#ifdef VOICECAT_HAS_AUDIO
test_capture_frame_accumulation(); test_capture_frame_accumulation();
#endif
test_vad_and_ptt_gate(); test_vad_and_ptt_gate();
if (g_failures == 0) { if (g_failures == 0) {
@@ -752,12 +730,3 @@ int main() {
std::printf("vad_ptt_devices: %d failure(s)\n", g_failures); std::printf("vad_ptt_devices: %d failure(s)\n", g_failures);
return 1; return 1;
} }
#else // !VOICECAT_HAS_NET
int main() {
std::printf("vad_ptt_devices: SKIP (VOICECAT_HAS_NET not defined)\n");
return 0;
}
#endif // VOICECAT_HAS_NET

View File

@@ -9,8 +9,6 @@
*/ */
#include <cstdio> #include <cstdio>
#ifdef VOICECAT_HAS_NET
#include <atomic> #include <atomic>
#include <chrono> #include <chrono>
#include <condition_variable> #include <condition_variable>
@@ -253,12 +251,3 @@ int main() {
std::printf("voice_client_abi: %d failure(s)\n", g_failures); std::printf("voice_client_abi: %d failure(s)\n", g_failures);
return 1; return 1;
} }
#else // !VOICECAT_HAS_NET
int main() {
std::printf("voice_client_abi: SKIP (VOICECAT_HAS_NET not defined)\n");
return 0;
}
#endif // VOICECAT_HAS_NET

View File

@@ -14,8 +14,6 @@
#include <optional> #include <optional>
#include <string> #include <string>
#ifdef VOICECAT_HAS_NET
#include "db.h" #include "db.h"
using namespace voicecat::server; using namespace voicecat::server;
@@ -141,12 +139,3 @@ int main(int argc, char** argv) {
print_usage(argv[0]); print_usage(argv[0]);
return 1; return 1;
} }
#else // !VOICECAT_HAS_NET
int main() {
std::fprintf(stderr, "voicecat-admin requires VOICECAT_HAS_NET (build with dev preset)\n");
return 1;
}
#endif // VOICECAT_HAS_NET