Files
voice-cat/core/src/voicecat.cpp
Talon bad9c7533a
Some checks failed
Build Linux Binaries / linux/amd64 (push) Has been cancelled
Build Linux Binaries / linux/arm64 (push) Has been cancelled
feat(audio): real noise suppression via vendored RNNoise (send + receive)
The two-sided NR plumbing (RemoteStream::recv_ns + the per-listener
vc_set_remote_stream noise_reduction toggle) was wired but inert:
ApmProcessor::create() returned a no-op passthrough, because the
originally-planned webrtc-audio-processing has no working Windows/macOS
build. Drop in RNNoise as the real backend behind the same ApmProcessor
interface, lighting up both NR paths.

- Vendor RNNoise (BSD-3 + CC0) at third_party/rnnoise/ — the vcpkg port
  is !windows !arm, so it can't cover our primary targets. Shrunk int8
  model (78MB -> 11.7MB via upstream scripts/shrink_model.sh), built as a
  standalone C static lib with no RTCD (portable scalar path on x86,
  auto-NEON on arm64) under -DDISABLE_DEBUG_FLOAT. Model is baked in
  (rnnoise_create(NULL)); no runtime file.
- New RnnoiseProcessor (core/src/audio/apm_processor.cpp) selected by
  ApmProcessor::create() when VOICECAT_HAS_NS. Mono/48kHz/480-sample;
  our clock is fixed 48kHz and Opus frame sizes are multiples of 480, so
  no resampling. RT-safe: allocates at construction, lock-free in the
  capture/playback callbacks.
- Receive-side: lit up via the factory; gated to mono streams (a stereo
  stream is a screen-audio share, not voice).
- Send-side (new): vc_set_input_noise_reduction(client, enable) ABI +
  vc_client::mic_ns_, run before input gain/VAD in on_capture_frame. A
  stereo mic is downmixed to mono ONLY when NR is on — with NR off a
  stereo mic keeps full stereo (never collapse mic quality unasked).
- Enable C as a project language for the vendored lib.
- New noise_suppression test: white noise through ApmProcessor::create()
  drops ~99.9% RMS. ctest --preset dev green, 28/28. windows-client DLL
  builds clean with vc_set_input_noise_reduction exported, system-only deps.
- Docs synced: voice.md §10, tech-stack.md §1/§5, third_party/README.md,
  vcpkg.json note, PROGRESS.md, CLAUDE.md.

Client on/off UI toggles (Windows/macOS/iOS) are the remaining follow-up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 13:30:54 +02:00

356 lines
12 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* voicecat.cpp — C ABI implementation.
*
* Lifecycle (create/destroy) and trivial accessors are always real. Everything else below
* just delegates to vc_client (core/src/core/client.cpp): under VOICECAT_HAS_NET
* (`dev`/`release`/`server-release` — see docs/building.md) that's the real M1M3 implementation;
* under the no-deps `skeleton` preset, client.cpp's `#else` branch returns VC_ERR_NOT_IMPLEMENTED
* for all of it, to keep that skeleton build green.
*/
#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 vc_client (real or stub, per preset above). ───────── */
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_vad_threshold(vc_client* c, float threshold) {
if (c == nullptr) return VC_ERR_INVALID_ARG;
return c->set_vad_threshold(threshold);
}
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_output_volume(vc_client* c, float gain) {
if (c == nullptr) return VC_ERR_INVALID_ARG;
return c->set_output_volume(gain);
}
vc_result vc_set_input_gain(vc_client* c, float gain) {
if (c == nullptr) return VC_ERR_INVALID_ARG;
return c->set_input_gain(gain);
}
vc_result vc_set_input_noise_reduction(vc_client* c, int enable) {
if (c == nullptr) return VC_ERR_INVALID_ARG;
return c->set_input_noise_reduction(enable != 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_get_remote_stream(vc_client* c, uint32_t user_id, uint32_t stream_id,
vc_remote_stream_state* out) {
if (c == nullptr) return VC_ERR_INVALID_ARG;
return c->get_remote_stream(user_id, stream_id, out);
}
vc_result vc_get_stream_audio_config(vc_client* c, uint32_t user_id, uint32_t stream_id,
vc_audio_config* out) {
if (c == nullptr || out == nullptr) return VC_ERR_INVALID_ARG;
return c->get_stream_audio_config(user_id, stream_id, out);
}
vc_result vc_test_inject_capture(vc_client* c, uint32_t stream_id, const int16_t* pcm,
size_t samples) {
if (c == nullptr || pcm == nullptr) return VC_ERR_INVALID_ARG;
return c->stream_feed_pcm(stream_id, pcm, samples, 1);
}
vc_result vc_stream_feed_pcm(vc_client* c, uint32_t stream_id, const int16_t* pcm,
size_t samples_per_channel, uint32_t channels) {
if (c == nullptr || pcm == nullptr) return VC_ERR_INVALID_ARG;
if (channels != 1 && channels != 2) return VC_ERR_INVALID_ARG;
return c->stream_feed_pcm(stream_id, pcm, samples_per_channel, channels);
}
vc_result vc_set_pcm_sink(vc_client* c, vc_pcm_sink_cb cb, void* user) {
if (c == nullptr) return VC_ERR_INVALID_ARG;
return c->set_pcm_sink(cb, user);
}
vc_result vc_set_mixed_output_sink(vc_client* c, vc_mixed_output_cb cb, void* user) {
if (c == nullptr) return VC_ERR_INVALID_ARG;
return c->set_mixed_output_sink(cb, user);
}
vc_result vc_set_external_playback(vc_client* c, int enable) {
if (c == nullptr) return VC_ERR_INVALID_ARG;
return c->set_external_playback(enable != 0);
}
vc_result vc_set_capture_channels(vc_client* c, uint32_t stream_id, uint32_t channels) {
if (c == nullptr) return VC_ERR_INVALID_ARG;
return c->set_capture_channels(stream_id, channels);
}
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 || list->items == nullptr) return;
for (size_t i = 0; i < list->count; ++i) {
delete[] list->items[i].id;
delete[] list->items[i].name;
}
delete[] list->items;
list->items = nullptr;
list->count = 0;
}
vc_result vc_list_channels(vc_client* c, vc_channel_list* out) {
if (c == nullptr || out == nullptr) return VC_ERR_INVALID_ARG;
return c->list_channels(out);
}
void vc_free_channel_list(vc_channel_list* list) {
if (list == nullptr || list->items == nullptr) return;
for (size_t i = 0; i < list->count; ++i) {
delete[] list->items[i].name;
delete[] list->items[i].topic;
}
delete[] list->items;
list->items = nullptr;
list->count = 0;
}
vc_result vc_list_users(vc_client* c, vc_user_list* out) {
if (c == nullptr || out == nullptr) return VC_ERR_INVALID_ARG;
return c->list_users(out);
}
void vc_free_user_list(vc_user_list* list) {
if (list == nullptr || list->items == nullptr) return;
for (size_t i = 0; i < list->count; ++i) delete[] list->items[i].nickname;
delete[] list->items;
list->items = nullptr;
list->count = 0;
}
vc_result vc_list_user_streams(vc_client* c, uint32_t user_id, vc_stream_summary_list* out) {
if (c == nullptr || out == nullptr) return VC_ERR_INVALID_ARG;
return c->list_user_streams(user_id, out);
}
void vc_free_stream_summary_list(vc_stream_summary_list* list) {
if (list == nullptr || list->items == nullptr) return;
for (size_t i = 0; i < list->count; ++i) delete[] list->items[i].label;
delete[] list->items;
list->items = nullptr;
list->count = 0;
}
vc_result vc_confirm_server_identity(vc_client* c, int accept) {
if (c == nullptr) return VC_ERR_INVALID_ARG;
return c->confirm_server_identity(accept != 0);
}
vc_result vc_get_server_identity_display(vc_client* c, char* out_buf, size_t buf_cap,
size_t* out_len) {
if (c == nullptr) return VC_ERR_INVALID_ARG;
return c->get_server_identity_display(out_buf, buf_cap, out_len);
}
vc_result vc_kick_user(vc_client* c, uint32_t user_id, const char* reason) {
if (c == nullptr) return VC_ERR_INVALID_ARG;
return c->kick_user(user_id, reason);
}
vc_result vc_ban_user(vc_client* c, uint32_t user_id, const char* reason,
uint64_t expires_unix_ms) {
if (c == nullptr) return VC_ERR_INVALID_ARG;
return c->ban_user(user_id, reason, expires_unix_ms);
}
vc_result vc_set_permission(vc_client* c, uint32_t user_id, const vc_permissions* perms) {
if (c == nullptr || perms == nullptr) return VC_ERR_INVALID_ARG;
return c->set_permission(user_id, perms);
}
vc_result vc_set_server_mute(vc_client* c, uint32_t user_id, int muted, int deafened) {
if (c == nullptr) return VC_ERR_INVALID_ARG;
return c->set_server_mute(user_id, muted != 0, deafened != 0);
}
vc_result vc_move_user(vc_client* c, uint32_t user_id, uint32_t channel_id) {
if (c == nullptr) return VC_ERR_INVALID_ARG;
return c->move_user(user_id, channel_id);
}
vc_result vc_create_channel(vc_client* c, const vc_channel_info* info) {
if (c == nullptr || info == nullptr) return VC_ERR_INVALID_ARG;
return c->create_channel(info);
}
vc_result vc_edit_channel(vc_client* c, const vc_channel_info* info) {
if (c == nullptr || info == nullptr) return VC_ERR_INVALID_ARG;
return c->edit_channel(info);
}
vc_result vc_delete_channel(vc_client* c, uint32_t channel_id) {
if (c == nullptr) return VC_ERR_INVALID_ARG;
return c->delete_channel(channel_id);
}
vc_result vc_create_account(vc_client* c, const char* username, const char* password) {
if (c == nullptr || username == nullptr || password == nullptr) return VC_ERR_INVALID_ARG;
return c->create_account(username, password);
}
vc_result vc_reset_password(vc_client* c, const char* username, const char* new_password) {
if (c == nullptr || username == nullptr || new_password == nullptr) return VC_ERR_INVALID_ARG;
return c->reset_password(username, new_password);
}
vc_result vc_delete_account(vc_client* c, const char* username) {
if (c == nullptr || username == nullptr) return VC_ERR_INVALID_ARG;
return c->delete_account(username);
}
vc_result vc_list_accounts(vc_client* c) {
if (c == nullptr) return VC_ERR_INVALID_ARG;
return c->list_accounts();
}
vc_result vc_get_account_list(vc_client* c, vc_account_list* out) {
if (c == nullptr || out == nullptr) return VC_ERR_INVALID_ARG;
return c->get_account_list(out);
}
void vc_free_account_list(vc_account_list* list) {
if (list == nullptr || list->items == nullptr) return;
for (size_t i = 0; i < list->count; ++i) delete[] list->items[i].username;
delete[] list->items;
list->items = nullptr;
list->count = 0;
}
vc_result vc_get_permissions(vc_client* c, vc_permissions* out) {
if (c == nullptr || out == nullptr) return VC_ERR_INVALID_ARG;
return c->get_permissions(out);
}
vc_result vc_audio_suspend(vc_client* c) {
if (c == nullptr) return VC_ERR_INVALID_ARG;
return c->audio_suspend();
}
vc_result vc_audio_resume(vc_client* c) {
if (c == nullptr) return VC_ERR_INVALID_ARG;
return c->audio_resume();
}
vc_result vc_audio_restart(vc_client* c) {
if (c == nullptr) return VC_ERR_INVALID_ARG;
return c->audio_restart();
}
} // extern "C"