From 7c04ee85a7e643a42c786bb50112aaddc006501c Mon Sep 17 00:00:00 2001 From: Maicon Moreira <50086618+Maicon-Moreira@users.noreply.github.com> Date: Tue, 28 Oct 2025 21:49:33 -0300 Subject: [PATCH] =?UTF-8?q?feat:=20add=20=E2=80=9Cmute=20while=20recording?= =?UTF-8?q?=E2=80=9D=20setting=20(#257)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: mute while recording * refactor: move MuteWhileRecording from AdvancedSettings to DebugSettings --- src-tauri/Cargo.lock | 46 +++++++++++++++++++ src-tauri/Cargo.toml | 1 + src-tauri/src/lib.rs | 1 + src-tauri/src/managers/audio.rs | 18 ++++++++ src-tauri/src/settings.rs | 3 ++ src-tauri/src/shortcut.rs | 9 ++++ src/components/settings/DebugSettings.tsx | 2 + .../settings/MuteWhileRecording.tsx | 28 +++++++++++ src/lib/types.ts | 1 + src/stores/settingsStore.ts | 3 ++ 10 files changed, 112 insertions(+) create mode 100644 src/components/settings/MuteWhileRecording.tsx diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 42b3683..231f103 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -947,6 +947,21 @@ dependencies = [ "libc", ] +[[package]] +name = "cpvc" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30cefc7f947361fa9b9b2709391ac9ab8970f8150bcdbc7dab6287399f5fda34" +dependencies = [ + "alsa", + "core-foundation 0.10.1", + "libpulse-binding", + "libpulse-sys", + "objc2-core-audio", + "objc2-core-audio-types", + "windows 0.61.3", +] + [[package]] name = "crc" version = "3.3.0" @@ -2264,6 +2279,7 @@ dependencies = [ "anyhow", "chrono", "cpal", + "cpvc", "enigo", "env_logger 0.11.8", "flate2", @@ -3090,6 +3106,33 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" +[[package]] +name = "libpulse-binding" +version = "2.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "909eb3049e16e373680fe65afe6e2a722ace06b671250cc4849557bc57d6a397" +dependencies = [ + "bitflags 2.10.0", + "libc", + "libpulse-sys", + "num-derive", + "num-traits", + "winapi", +] + +[[package]] +name = "libpulse-sys" +version = "1.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d74371848b22e989f829cc1621d2ebd74960711557d8b45cfe740f60d0a05e61" +dependencies = [ + "libc", + "num-derive", + "num-traits", + "pkg-config", + "winapi", +] + [[package]] name = "libredox" version = "0.1.10" @@ -3671,10 +3714,13 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1eebcea8b0dbff5f7c8504f3107c68fc061a3eb44932051c8cf8a68d969c3b2" dependencies = [ + "block2 0.6.2", "dispatch2", + "libc", "objc2 0.6.3", "objc2-core-audio-types", "objc2-core-foundation", + "objc2-foundation 0.3.2", ] [[package]] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 4a14876..74b068e 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -65,6 +65,7 @@ rusqlite = { version = "0.32.1", features = ["bundled"] } tar = "0.4.44" flate2 = "1.0" transcribe-rs = "0.1.4" +cpvc = "0.4.1" [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] tauri-plugin-autostart = "2.5.1" diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index e2e1bbb..62dfb4d 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -237,6 +237,7 @@ pub fn run() { shortcut::update_custom_words, shortcut::suspend_binding, shortcut::resume_binding, + shortcut::change_mute_while_recording_setting, trigger_update_check, commands::cancel_operation, commands::get_app_dir_path, diff --git a/src-tauri/src/managers/audio.rs b/src-tauri/src/managers/audio.rs index 4301798..fe7810e 100644 --- a/src-tauri/src/managers/audio.rs +++ b/src-tauri/src/managers/audio.rs @@ -58,6 +58,7 @@ pub struct AudioRecordingManager { recorder: Arc>>, is_open: Arc>, is_recording: Arc>, + initial_volume: Arc>>, } impl AudioRecordingManager { @@ -79,6 +80,7 @@ impl AudioRecordingManager { recorder: Arc::new(Mutex::new(None)), is_open: Arc::new(Mutex::new(false)), is_recording: Arc::new(Mutex::new(false)), + initial_volume: Arc::new(Mutex::new(None)), }; // Always-on? Open immediately. @@ -100,6 +102,16 @@ impl AudioRecordingManager { let start_time = Instant::now(); + let settings = get_settings(&self.app_handle); + let mut initial_volume_guard = self.initial_volume.lock().unwrap(); + + if settings.mute_while_recording { + *initial_volume_guard = Some(cpvc::get_system_volume()); + cpvc::set_system_volume(0); + } else { + *initial_volume_guard = None; + } + let vad_path = self .app_handle .path() @@ -154,6 +166,12 @@ impl AudioRecordingManager { return; } + let mut initial_volume_guard = self.initial_volume.lock().unwrap(); + if let Some(vol) = *initial_volume_guard { + cpvc::set_system_volume(vol); + } + *initial_volume_guard = None; + if let Some(rec) = self.recorder.lock().unwrap().as_mut() { // If still recording, stop first. if *self.is_recording.lock().unwrap() { diff --git a/src-tauri/src/settings.rs b/src-tauri/src/settings.rs index af290d8..8f86148 100644 --- a/src-tauri/src/settings.rs +++ b/src-tauri/src/settings.rs @@ -161,6 +161,8 @@ pub struct AppSettings { pub paste_method: PasteMethod, #[serde(default)] pub clipboard_handling: ClipboardHandling, + #[serde(default)] + pub mute_while_recording: bool, } fn default_model() -> String { @@ -260,6 +262,7 @@ pub fn get_default_settings() -> AppSettings { history_limit: default_history_limit(), paste_method: PasteMethod::default(), clipboard_handling: ClipboardHandling::default(), + mute_while_recording: false, } } diff --git a/src-tauri/src/shortcut.rs b/src-tauri/src/shortcut.rs index c9cf0b3..011c46f 100644 --- a/src-tauri/src/shortcut.rs +++ b/src-tauri/src/shortcut.rs @@ -292,6 +292,15 @@ pub fn change_clipboard_handling_setting(app: AppHandle, handling: String) -> Re Ok(()) } +#[tauri::command] +pub fn change_mute_while_recording_setting(app: AppHandle, enabled: bool) -> Result<(), String> { + let mut settings = settings::get_settings(&app); + settings.mute_while_recording = enabled; + settings::write_settings(&app, settings); + + Ok(()) +} + /// Determine whether a shortcut string contains at least one non-modifier key. /// We allow single non-modifier keys (e.g. "f5" or "space") but disallow /// modifier-only combos (e.g. "ctrl" or "ctrl+shift"). diff --git a/src/components/settings/DebugSettings.tsx b/src/components/settings/DebugSettings.tsx index 05a5aaf..0e4fd43 100644 --- a/src/components/settings/DebugSettings.tsx +++ b/src/components/settings/DebugSettings.tsx @@ -7,6 +7,7 @@ import { PasteMethodSetting } from "./PasteMethod"; import { ClipboardHandlingSetting } from "./ClipboardHandling"; import { AlwaysOnMicrophone } from "./AlwaysOnMicrophone"; import { SoundPicker } from "./SoundPicker"; +import { MuteWhileRecording } from "./MuteWhileRecording"; export const DebugSettings: React.FC = () => { return ( @@ -22,6 +23,7 @@ export const DebugSettings: React.FC = () => { + ); diff --git a/src/components/settings/MuteWhileRecording.tsx b/src/components/settings/MuteWhileRecording.tsx new file mode 100644 index 0000000..a547bfe --- /dev/null +++ b/src/components/settings/MuteWhileRecording.tsx @@ -0,0 +1,28 @@ +import React from "react"; +import { ToggleSwitch } from "../ui/ToggleSwitch"; +import { useSettings } from "../../hooks/useSettings"; + +interface MuteWhileRecordingToggleProps { + descriptionMode?: "inline" | "tooltip"; + grouped?: boolean; +} + +export const MuteWhileRecording: React.FC = React.memo( + ({ descriptionMode = "tooltip", grouped = false }) => { + const { getSetting, updateSetting, isUpdating } = useSettings(); + + const muteEnabled = getSetting("mute_while_recording") ?? false; + + return ( + updateSetting("mute_while_recording", enabled)} + isUpdating={isUpdating("mute_while_recording")} + label="Mute while recording" + description="Automatically mute all sound output while Handy is recording, then restore it when finished." + descriptionMode={descriptionMode} + grouped={grouped} + /> + ); + }, +); \ No newline at end of file diff --git a/src/lib/types.ts b/src/lib/types.ts index 2dccdfe..bc282a4 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -65,6 +65,7 @@ export const SettingsSchema = z.object({ history_limit: z.number().optional().default(5), paste_method: PasteMethodSchema.optional().default("ctrl_v"), clipboard_handling: ClipboardHandlingSchema.optional().default("dont_modify"), + mute_while_recording: z.boolean().optional().default(false), }); export const BindingResponseSchema = z.object({ diff --git a/src/stores/settingsStore.ts b/src/stores/settingsStore.ts index c5e7745..d57b82f 100644 --- a/src/stores/settingsStore.ts +++ b/src/stores/settingsStore.ts @@ -53,6 +53,7 @@ const DEFAULT_SETTINGS: Partial = { debug_mode: false, custom_words: [], history_limit: 5, + mute_while_recording: false, }; const DEFAULT_AUDIO_DEVICE: AudioDevice = { @@ -101,6 +102,8 @@ const settingUpdaters: { clipboard_handling: (value) => invoke("change_clipboard_handling_setting", { handling: value }), history_limit: (value) => invoke("update_history_limit", { limit: value }), + mute_while_recording: (value) => + invoke("change_mute_while_recording_setting", { enabled: value }), }; export const useSettingsStore = create()(