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:
+29
-1
@@ -43,6 +43,12 @@ pub enum AppMessage {
|
||||
PeerVolumeChanged(EndpointId, f32),
|
||||
InputDeviceSelected(AudioDevice),
|
||||
OutputDeviceSelected(AudioDevice),
|
||||
/// Live input-gain drag (applies immediately, persisted on release).
|
||||
InputVolumeChanged(f32),
|
||||
/// Live output-gain drag (applies immediately, persisted on release).
|
||||
OutputVolumeChanged(f32),
|
||||
/// Persist the current config to disk (slider release).
|
||||
PersistConfig,
|
||||
NoiseGateChanged(f32),
|
||||
/// Live value while dragging the gate handle on the meter — updates the gate
|
||||
/// immediately but does not persist (saved once on release via NoiseGateChanged).
|
||||
@@ -129,6 +135,8 @@ impl Default for AppState {
|
||||
let config = AppConfig::load();
|
||||
notify::set_enabled(config.notifications_enabled);
|
||||
let _ = controller.send(CoreCommand::SetNoiseGateThreshold(config.noise_gate_threshold));
|
||||
let _ = controller.send(CoreCommand::SetInputVolume(config.input_volume));
|
||||
let _ = controller.send(CoreCommand::SetOutputVolume(config.output_volume));
|
||||
let _ = controller.send(CoreCommand::SetNetworkMode(config.network_mode));
|
||||
let all_devices = enumerate_audio_devices();
|
||||
let input_devices: Vec<_> = all_devices.iter().filter(|d| d.is_input).cloned().collect();
|
||||
@@ -372,6 +380,18 @@ fn update(state: &mut AppState, message: AppMessage) -> Task<AppMessage> {
|
||||
state.config.save();
|
||||
state.selected_output = Some(dev);
|
||||
}
|
||||
AppMessage::InputVolumeChanged(vol) => {
|
||||
// Live apply; disk write deferred to release (PersistConfig).
|
||||
state.config.input_volume = vol;
|
||||
let _ = state.controller.send(CoreCommand::SetInputVolume(vol));
|
||||
}
|
||||
AppMessage::OutputVolumeChanged(vol) => {
|
||||
state.config.output_volume = vol;
|
||||
let _ = state.controller.send(CoreCommand::SetOutputVolume(vol));
|
||||
}
|
||||
AppMessage::PersistConfig => {
|
||||
state.config.save();
|
||||
}
|
||||
AppMessage::NoiseGateChanged(val) => {
|
||||
state.config.noise_gate_threshold = val;
|
||||
state.config.save();
|
||||
@@ -641,6 +661,10 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
|
||||
state.selected_input.as_ref(),
|
||||
AppMessage::InputDeviceSelected,
|
||||
).width(iced::Length::Fill),
|
||||
text(format!("Input Volume (mic): {:.0}%", state.config.input_volume * 100.0)).size(11).color(color_subtext),
|
||||
slider(0.0..=2.0, state.config.input_volume, AppMessage::InputVolumeChanged)
|
||||
.step(0.05)
|
||||
.on_release(AppMessage::PersistConfig),
|
||||
].spacing(4).width(iced::Length::Fill),
|
||||
column![
|
||||
text("Output Device").size(12).color(Color::from_rgb8(180, 180, 180)),
|
||||
@@ -649,8 +673,12 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
|
||||
state.selected_output.as_ref(),
|
||||
AppMessage::OutputDeviceSelected,
|
||||
).width(iced::Length::Fill),
|
||||
text(format!("Output Volume: {:.0}%", state.config.output_volume * 100.0)).size(11).color(color_subtext),
|
||||
slider(0.0..=2.0, state.config.output_volume, AppMessage::OutputVolumeChanged)
|
||||
.step(0.05)
|
||||
.on_release(AppMessage::PersistConfig),
|
||||
].spacing(4).width(iced::Length::Fill),
|
||||
].spacing(20).width(iced::Length::Fill),
|
||||
].spacing(20).align_y(iced::alignment::Vertical::Top).width(iced::Length::Fill),
|
||||
vertical_space(12.0),
|
||||
row![
|
||||
column![
|
||||
|
||||
Reference in New Issue
Block a user