feat(audio): per-stream mix controls in Windows client + vc_get_remote_stream getter
PerUserTuningDialog previously broadcast one gain/mute/NR set to *all* of a user's streams, even though the core mixer (AudioEngine::RemoteStream) and the C ABI (vc_set_remote_stream) were already per-stream. The UI had no per-mix controls anywhere. Reworks the dialog to enumerate ListUserStreams on open and render one row per stream (kind + label + Gain + Mute + NR), each wiring only to its own stream_id. Adds a read-back ABI counterpart, vc_get_remote_stream, so the dialog opens at the listener's actual current per-stream settings (defaults 1.0/unmuted/NR-off) rather than always 100%. Additive ABI change only; no existing symbols touched. Tests: test_m3_multistream extended with getter round-trip assertions; new C# smoke test exercises the full P/Invoke marshaling path with two clients. Docs: voice.md §10 notes the getter. NR checkbox keeps its honest 'passthrough' label (NS DSP still unbuilt per §8).
This commit is contained in:
@@ -334,6 +334,17 @@ void AudioEngine::set_stream_noise_reduction(uint32_t ssrc, bool enable) {
|
||||
}
|
||||
}
|
||||
|
||||
bool AudioEngine::get_stream_state(uint32_t ssrc, float& gain, bool& mute, bool& noise_reduction) {
|
||||
std::lock_guard lk(streams_mu_);
|
||||
auto it = streams_.find(ssrc);
|
||||
if (it == streams_.end()) return false;
|
||||
const auto& s = it->second;
|
||||
gain = s.gain;
|
||||
mute = s.mute;
|
||||
noise_reduction = s.noise_reduction_enabled;
|
||||
return true;
|
||||
}
|
||||
|
||||
void AudioEngine::remove_stream(uint32_t ssrc) {
|
||||
std::lock_guard lk(streams_mu_);
|
||||
streams_.erase(ssrc);
|
||||
|
||||
@@ -158,6 +158,10 @@ class AudioEngine {
|
||||
// Listener-chosen, local-only noise reduction on a specific remote stream (docs/voice.md
|
||||
// §10) — lazily instantiates an ApmProcessor on first enable, frees it on disable.
|
||||
void set_stream_noise_reduction(uint32_t ssrc, bool enable);
|
||||
// Read back a stream's current receive-side state. Returns true and fills *out if the
|
||||
// stream is known (even if defaults — gain=1, mute=false, nr=false), false if it has
|
||||
// never been seen (no RemoteStream entry yet).
|
||||
bool get_stream_state(uint32_t ssrc, float& gain, bool& mute, bool& noise_reduction);
|
||||
void remove_stream(uint32_t ssrc);
|
||||
|
||||
// Edge-triggered talk-state transitions since the last call (docs/voice.md §7: talk state
|
||||
|
||||
@@ -1360,6 +1360,38 @@ vc_result vc_client::set_remote_stream(uint32_t user_id, uint32_t stream_id, flo
|
||||
return VC_OK;
|
||||
}
|
||||
|
||||
vc_result vc_client::get_remote_stream(uint32_t user_id, uint32_t stream_id,
|
||||
vc_remote_stream_state* out) {
|
||||
if (out == nullptr) return VC_ERR_INVALID_ARG;
|
||||
if (state_net_.load(std::memory_order_acquire) != VC_STATE_CONNECTED) return VC_ERR_NOT_CONNECTED;
|
||||
uint32_t ssrc = 0;
|
||||
bool found = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(session_model_mu_);
|
||||
const auto* user = session_model_.find_user(user_id);
|
||||
if (user) {
|
||||
for (const auto& s : user->streams) {
|
||||
if (s.stream_id == stream_id) { ssrc = s.ssrc; found = true; break; }
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!found) return VC_ERR_INVALID_ARG;
|
||||
// The (user, stream) is known. The RemoteStream entry may not exist yet if the listener
|
||||
// has neither set any control nor received audio for it — in that case report the
|
||||
// defaults (docs/voice.md §10) so the UI opens at 100/unmuted/NR-off.
|
||||
float gain = 1.0f; bool mute = false; bool nr = false;
|
||||
if (audio_engine_.get_stream_state(ssrc, gain, mute, nr)) {
|
||||
out->gain = gain;
|
||||
out->muted = mute ? 1 : 0;
|
||||
out->noise_reduction = nr ? 1 : 0;
|
||||
} else {
|
||||
out->gain = 1.0f;
|
||||
out->muted = 0;
|
||||
out->noise_reduction = 0;
|
||||
}
|
||||
return VC_OK;
|
||||
}
|
||||
|
||||
vc_result vc_client::get_stream_audio_config(uint32_t user_id, uint32_t stream_id,
|
||||
vc_audio_config* out) {
|
||||
if (state_net_.load(std::memory_order_acquire) != VC_STATE_CONNECTED) return VC_ERR_NOT_CONNECTED;
|
||||
@@ -1775,6 +1807,9 @@ vc_result vc_client::set_self_mute(bool, bool) { return VC_ERR_NOT_I
|
||||
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;
|
||||
|
||||
@@ -55,6 +55,8 @@ struct vc_client {
|
||||
vc_result set_self_mute(bool mic_muted, bool deafened);
|
||||
vc_result set_remote_stream(uint32_t user_id, uint32_t stream_id, float gain, bool muted,
|
||||
bool noise_reduction);
|
||||
vc_result get_remote_stream(uint32_t user_id, uint32_t stream_id,
|
||||
vc_remote_stream_state* out);
|
||||
|
||||
vc_result send_text(vc_text_scope scope, uint32_t target_id, const char* utf8);
|
||||
|
||||
|
||||
@@ -123,6 +123,12 @@ vc_result vc_set_remote_stream(vc_client* c, uint32_t user_id, uint32_t stream_i
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user