feat(audio): input/output volume sliders in Settings
Adds Discord-style app-internal gain controls under each device picker: input volume scales the captured mic (applied before the meter/gate/encode, so it also moves the mic meter), output volume scales the mixed playback (on top of per-peer volumes). PeerSpeak-only — no system/other-app effect. Both persist in config (input_volume/output_volume, serde default 1.0 for backward compat) and read live by the audio loops via f32-bit atomics, so they take effect mid-call. Sliders apply live on drag and save on release. The standalone mic-test monitor applies the same input gain so the test meter reflects it. Reuses the existing apply_volume helper (unity fast-path + i16 saturation). Tests: config backward-compat + round-trip for the new fields (gain math itself is covered by the existing apply_volume tests). 65 lib tests, clippy clean. Field-verified: input slider moves the mic-test meter. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -40,11 +40,21 @@ fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn default_volume() -> f32 {
|
||||
1.0
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct AppConfig {
|
||||
pub input_device: String,
|
||||
pub output_device: String,
|
||||
pub noise_gate_threshold: f32,
|
||||
/// App-internal capture gain applied to the mic before encode (1.0 = unity).
|
||||
#[serde(default = "default_volume")]
|
||||
pub input_volume: f32,
|
||||
/// App-internal playback gain applied to the mixed output (1.0 = unity).
|
||||
#[serde(default = "default_volume")]
|
||||
pub output_volume: f32,
|
||||
#[serde(default)]
|
||||
pub network_mode: NetworkMode,
|
||||
/// Route audio through PipeWire's echo-cancel module (AEC + noise suppression).
|
||||
@@ -77,6 +87,8 @@ impl Default for AppConfig {
|
||||
input_device: "".to_string(),
|
||||
output_device: "".to_string(),
|
||||
noise_gate_threshold: 0.01,
|
||||
input_volume: 1.0,
|
||||
output_volume: 1.0,
|
||||
network_mode: NetworkMode::default(),
|
||||
echo_cancellation_enabled: false,
|
||||
notifications_enabled: true,
|
||||
@@ -142,6 +154,9 @@ mod tests {
|
||||
assert_eq!(deserialized.network_mode, NetworkMode::RelayNoDiscovery);
|
||||
assert!(!deserialized.echo_cancellation_enabled);
|
||||
assert!(deserialized.notifications_enabled);
|
||||
// Configs predating the volume sliders must load at unity gain.
|
||||
assert_eq!(deserialized.input_volume, 1.0);
|
||||
assert_eq!(deserialized.output_volume, 1.0);
|
||||
assert!(deserialized.custom_sound_self_join.is_none());
|
||||
assert!(deserialized.custom_sound_peer_join.is_none());
|
||||
assert!(deserialized.custom_sound_peer_leave.is_none());
|
||||
@@ -152,6 +167,31 @@ mod tests {
|
||||
assert!(deserialized.custom_sound_reconnect_failed.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_input_output_volume_fields() {
|
||||
// Default impl is unity gain.
|
||||
let def = AppConfig::default();
|
||||
assert_eq!(def.input_volume, 1.0);
|
||||
assert_eq!(def.output_volume, 1.0);
|
||||
|
||||
// Missing in JSON → unity (serde default).
|
||||
let missing = r#"{"input_device":"","output_device":"","noise_gate_threshold":0.01}"#;
|
||||
let cfg_missing: AppConfig = serde_json::from_str(missing).unwrap();
|
||||
assert_eq!(cfg_missing.input_volume, 1.0);
|
||||
assert_eq!(cfg_missing.output_volume, 1.0);
|
||||
|
||||
// Explicit non-unity values are preserved across a round-trip.
|
||||
let cfg = AppConfig {
|
||||
input_volume: 1.5,
|
||||
output_volume: 0.25,
|
||||
..AppConfig::default()
|
||||
};
|
||||
let round_tripped: AppConfig =
|
||||
serde_json::from_str(&serde_json::to_string(&cfg).unwrap()).unwrap();
|
||||
assert_eq!(round_tripped.input_volume, 1.5);
|
||||
assert_eq!(round_tripped.output_volume, 0.25);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_notifications_enabled_specifically() {
|
||||
let missing_notifications = r#"{"input_device":"","output_device":"","noise_gate_threshold":0.01}"#;
|
||||
|
||||
Reference in New Issue
Block a user