feat(clients): persist input settings, add mic input gain, fix iOS chat + VoiceOver
Input mode (VAD/PTT/Always-On), VAD threshold, and the new mic gain were
applied to the core + UI but never saved, so every relaunch reset to VAD
defaults. Each client now persists them and re-applies on connect:
- iOS: UserDefaults (SessionState.loadAndApplyVoiceSettings + setter writes)
- macOS: UserDefaults via MainWindowController didSet + loadPersistedAudioSettings
(settings window also restores the VAD slider from the stored threshold)
- Windows: new Models/VoiceSettings.cs (JSON at %AppData%\VoiceCat\voice.json,
mirrors FeedbackSettings) loaded/applied in MainForm
Add global send-side mic gain API vc_set_input_gain (applied to MIC PCM in
on_capture_frame before the VAD gate, clamped to int16) + Swift/C# bindings,
and a 0-300% (default 100%) mic-volume slider on all three clients.
Fix iOS chat: ChatView called sendText(scope:.channel) with no targetId (0),
so channel messages went nowhere; now passes session.currentChannelId.
Fix iOS per-user tuning for VoiceOver: the tuning sheet was long-press
.contextMenu only (invisible to VoiceOver); UserRow now also exposes the same
buttons via .accessibilityActions (no visual change).
Verified: core builds clean; ctest 24/27 (3 pre-existing teardown crashes,
reproduced with changes stashed); VoiceCatMac + VoiceCatiOS (arm64 sim) build
SUCCEEDED; VoiceCat.Interop dotnet build succeeded. Windows App not built
(WinForms can't build on macOS) — follows existing patterns.
This commit is contained in:
@@ -24,6 +24,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <csignal>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
@@ -1008,6 +1009,21 @@ void vc_client::on_capture_frame(int kind, const int16_t* pcm, int samples, int
|
||||
(self_mic_muted_.load(std::memory_order_acquire) ||
|
||||
server_muted_.load(std::memory_order_acquire))) return;
|
||||
|
||||
// Send-side mic input gain (vc_set_input_gain) — MIC only. Applied in place before the gate
|
||||
// so a boosted quiet mic also helps cross the VAD threshold. EnergyVadProcessor never writes
|
||||
// through its pointer, so the const_cast (same as the VAD path below) is safe; no allocation.
|
||||
if (kind == static_cast<int>(VC_STREAM_MIC)) {
|
||||
const float gain = input_gain_.load(std::memory_order_relaxed);
|
||||
if (gain != 1.0f) {
|
||||
int16_t* w = const_cast<int16_t*>(pcm);
|
||||
const int n = samples * std::max(1, channels);
|
||||
for (int i = 0; i < n; ++i) {
|
||||
int32_t v = static_cast<int32_t>(std::lround(w[i] * gain));
|
||||
w[i] = static_cast<int16_t>(std::clamp(v, -32768, 32767));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Send-side input gate (docs/voice.md §11) — MIC only. SCREEN_AUDIO/AUX_DEVICE always
|
||||
// bypass this: gating a screen-share on the user's own voice activity would silently drop
|
||||
// shared music/video audio whenever the user isn't talking, which defeats the feature.
|
||||
@@ -1452,6 +1468,11 @@ vc_result vc_client::set_output_volume(float gain) {
|
||||
return VC_OK;
|
||||
}
|
||||
|
||||
vc_result vc_client::set_input_gain(float gain) {
|
||||
input_gain_.store(gain < 0.0f ? 0.0f : gain, std::memory_order_relaxed);
|
||||
return VC_OK;
|
||||
}
|
||||
|
||||
vc_result vc_client::set_remote_stream(uint32_t user_id, uint32_t stream_id, float gain,
|
||||
bool muted, bool noise_reduction) {
|
||||
if (state_net_.load(std::memory_order_acquire) != VC_STATE_CONNECTED) return VC_ERR_NOT_CONNECTED;
|
||||
@@ -1968,6 +1989,7 @@ vc_result vc_client::set_input_device(uint32_t, const char*) { return VC_ERR_NOT
|
||||
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_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) {
|
||||
|
||||
@@ -55,6 +55,7 @@ struct vc_client {
|
||||
vc_result set_push_to_talk(bool active);
|
||||
vc_result set_self_mute(bool mic_muted, bool deafened);
|
||||
vc_result set_output_volume(float gain);
|
||||
vc_result set_input_gain(float gain);
|
||||
vc_result set_remote_stream(uint32_t user_id, uint32_t stream_id, float gain, bool muted,
|
||||
bool noise_reduction);
|
||||
vc_result get_remote_stream(uint32_t user_id, uint32_t stream_id,
|
||||
@@ -311,6 +312,7 @@ struct vc_client {
|
||||
std::atomic<vc_input_mode> current_input_mode_{VC_INPUT_VOICE_ACTIVATION};
|
||||
std::atomic<bool> ptt_active_{false};
|
||||
std::atomic<float> vad_threshold_{0.025f}; // remembered across mode switches
|
||||
std::atomic<float> input_gain_{1.0f}; // send-side MIC gain (vc_set_input_gain)
|
||||
|
||||
// External-playback mode (iOS VPIO): when true, ensure_audio_running() configures the
|
||||
// AudioEngine to skip its hardware playback device and drive the mixer on a timer instead,
|
||||
|
||||
@@ -122,6 +122,11 @@ vc_result vc_set_output_volume(vc_client* c, float gain) {
|
||||
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_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;
|
||||
|
||||
Reference in New Issue
Block a user