feat: add “mute while recording” setting (#257)
* feat: mute while recording * refactor: move MuteWhileRecording from AdvancedSettings to DebugSettings
This commit is contained in:
parent
65c1e2dbcb
commit
7c04ee85a7
10 changed files with 112 additions and 0 deletions
46
src-tauri/Cargo.lock
generated
46
src-tauri/Cargo.lock
generated
|
|
@ -947,6 +947,21 @@ dependencies = [
|
||||||
"libc",
|
"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]]
|
[[package]]
|
||||||
name = "crc"
|
name = "crc"
|
||||||
version = "3.3.0"
|
version = "3.3.0"
|
||||||
|
|
@ -2264,6 +2279,7 @@ dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
"cpal",
|
"cpal",
|
||||||
|
"cpvc",
|
||||||
"enigo",
|
"enigo",
|
||||||
"env_logger 0.11.8",
|
"env_logger 0.11.8",
|
||||||
"flate2",
|
"flate2",
|
||||||
|
|
@ -3090,6 +3106,33 @@ version = "0.2.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de"
|
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]]
|
[[package]]
|
||||||
name = "libredox"
|
name = "libredox"
|
||||||
version = "0.1.10"
|
version = "0.1.10"
|
||||||
|
|
@ -3671,10 +3714,13 @@ version = "0.3.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e1eebcea8b0dbff5f7c8504f3107c68fc061a3eb44932051c8cf8a68d969c3b2"
|
checksum = "e1eebcea8b0dbff5f7c8504f3107c68fc061a3eb44932051c8cf8a68d969c3b2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"block2 0.6.2",
|
||||||
"dispatch2",
|
"dispatch2",
|
||||||
|
"libc",
|
||||||
"objc2 0.6.3",
|
"objc2 0.6.3",
|
||||||
"objc2-core-audio-types",
|
"objc2-core-audio-types",
|
||||||
"objc2-core-foundation",
|
"objc2-core-foundation",
|
||||||
|
"objc2-foundation 0.3.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,7 @@ rusqlite = { version = "0.32.1", features = ["bundled"] }
|
||||||
tar = "0.4.44"
|
tar = "0.4.44"
|
||||||
flate2 = "1.0"
|
flate2 = "1.0"
|
||||||
transcribe-rs = "0.1.4"
|
transcribe-rs = "0.1.4"
|
||||||
|
cpvc = "0.4.1"
|
||||||
|
|
||||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||||
tauri-plugin-autostart = "2.5.1"
|
tauri-plugin-autostart = "2.5.1"
|
||||||
|
|
|
||||||
|
|
@ -237,6 +237,7 @@ pub fn run() {
|
||||||
shortcut::update_custom_words,
|
shortcut::update_custom_words,
|
||||||
shortcut::suspend_binding,
|
shortcut::suspend_binding,
|
||||||
shortcut::resume_binding,
|
shortcut::resume_binding,
|
||||||
|
shortcut::change_mute_while_recording_setting,
|
||||||
trigger_update_check,
|
trigger_update_check,
|
||||||
commands::cancel_operation,
|
commands::cancel_operation,
|
||||||
commands::get_app_dir_path,
|
commands::get_app_dir_path,
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,7 @@ pub struct AudioRecordingManager {
|
||||||
recorder: Arc<Mutex<Option<AudioRecorder>>>,
|
recorder: Arc<Mutex<Option<AudioRecorder>>>,
|
||||||
is_open: Arc<Mutex<bool>>,
|
is_open: Arc<Mutex<bool>>,
|
||||||
is_recording: Arc<Mutex<bool>>,
|
is_recording: Arc<Mutex<bool>>,
|
||||||
|
initial_volume: Arc<Mutex<Option<u8>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AudioRecordingManager {
|
impl AudioRecordingManager {
|
||||||
|
|
@ -79,6 +80,7 @@ impl AudioRecordingManager {
|
||||||
recorder: Arc::new(Mutex::new(None)),
|
recorder: Arc::new(Mutex::new(None)),
|
||||||
is_open: Arc::new(Mutex::new(false)),
|
is_open: Arc::new(Mutex::new(false)),
|
||||||
is_recording: Arc::new(Mutex::new(false)),
|
is_recording: Arc::new(Mutex::new(false)),
|
||||||
|
initial_volume: Arc::new(Mutex::new(None)),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Always-on? Open immediately.
|
// Always-on? Open immediately.
|
||||||
|
|
@ -100,6 +102,16 @@ impl AudioRecordingManager {
|
||||||
|
|
||||||
let start_time = Instant::now();
|
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
|
let vad_path = self
|
||||||
.app_handle
|
.app_handle
|
||||||
.path()
|
.path()
|
||||||
|
|
@ -154,6 +166,12 @@ impl AudioRecordingManager {
|
||||||
return;
|
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 let Some(rec) = self.recorder.lock().unwrap().as_mut() {
|
||||||
// If still recording, stop first.
|
// If still recording, stop first.
|
||||||
if *self.is_recording.lock().unwrap() {
|
if *self.is_recording.lock().unwrap() {
|
||||||
|
|
|
||||||
|
|
@ -161,6 +161,8 @@ pub struct AppSettings {
|
||||||
pub paste_method: PasteMethod,
|
pub paste_method: PasteMethod,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub clipboard_handling: ClipboardHandling,
|
pub clipboard_handling: ClipboardHandling,
|
||||||
|
#[serde(default)]
|
||||||
|
pub mute_while_recording: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_model() -> String {
|
fn default_model() -> String {
|
||||||
|
|
@ -260,6 +262,7 @@ pub fn get_default_settings() -> AppSettings {
|
||||||
history_limit: default_history_limit(),
|
history_limit: default_history_limit(),
|
||||||
paste_method: PasteMethod::default(),
|
paste_method: PasteMethod::default(),
|
||||||
clipboard_handling: ClipboardHandling::default(),
|
clipboard_handling: ClipboardHandling::default(),
|
||||||
|
mute_while_recording: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -292,6 +292,15 @@ pub fn change_clipboard_handling_setting(app: AppHandle, handling: String) -> Re
|
||||||
Ok(())
|
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.
|
/// 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
|
/// We allow single non-modifier keys (e.g. "f5" or "space") but disallow
|
||||||
/// modifier-only combos (e.g. "ctrl" or "ctrl+shift").
|
/// modifier-only combos (e.g. "ctrl" or "ctrl+shift").
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import { PasteMethodSetting } from "./PasteMethod";
|
||||||
import { ClipboardHandlingSetting } from "./ClipboardHandling";
|
import { ClipboardHandlingSetting } from "./ClipboardHandling";
|
||||||
import { AlwaysOnMicrophone } from "./AlwaysOnMicrophone";
|
import { AlwaysOnMicrophone } from "./AlwaysOnMicrophone";
|
||||||
import { SoundPicker } from "./SoundPicker";
|
import { SoundPicker } from "./SoundPicker";
|
||||||
|
import { MuteWhileRecording } from "./MuteWhileRecording";
|
||||||
|
|
||||||
export const DebugSettings: React.FC = () => {
|
export const DebugSettings: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
|
|
@ -22,6 +23,7 @@ export const DebugSettings: React.FC = () => {
|
||||||
<AppDataDirectory descriptionMode="tooltip" grouped={true} />
|
<AppDataDirectory descriptionMode="tooltip" grouped={true} />
|
||||||
<HistoryLimit descriptionMode="tooltip" grouped={true} />
|
<HistoryLimit descriptionMode="tooltip" grouped={true} />
|
||||||
<AlwaysOnMicrophone descriptionMode="tooltip" grouped={true} />
|
<AlwaysOnMicrophone descriptionMode="tooltip" grouped={true} />
|
||||||
|
<MuteWhileRecording descriptionMode="tooltip" grouped={true} />
|
||||||
</SettingsGroup>
|
</SettingsGroup>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
28
src/components/settings/MuteWhileRecording.tsx
Normal file
28
src/components/settings/MuteWhileRecording.tsx
Normal file
|
|
@ -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<MuteWhileRecordingToggleProps> = React.memo(
|
||||||
|
({ descriptionMode = "tooltip", grouped = false }) => {
|
||||||
|
const { getSetting, updateSetting, isUpdating } = useSettings();
|
||||||
|
|
||||||
|
const muteEnabled = getSetting("mute_while_recording") ?? false;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToggleSwitch
|
||||||
|
checked={muteEnabled}
|
||||||
|
onChange={(enabled) => 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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
@ -65,6 +65,7 @@ export const SettingsSchema = z.object({
|
||||||
history_limit: z.number().optional().default(5),
|
history_limit: z.number().optional().default(5),
|
||||||
paste_method: PasteMethodSchema.optional().default("ctrl_v"),
|
paste_method: PasteMethodSchema.optional().default("ctrl_v"),
|
||||||
clipboard_handling: ClipboardHandlingSchema.optional().default("dont_modify"),
|
clipboard_handling: ClipboardHandlingSchema.optional().default("dont_modify"),
|
||||||
|
mute_while_recording: z.boolean().optional().default(false),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const BindingResponseSchema = z.object({
|
export const BindingResponseSchema = z.object({
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,7 @@ const DEFAULT_SETTINGS: Partial<Settings> = {
|
||||||
debug_mode: false,
|
debug_mode: false,
|
||||||
custom_words: [],
|
custom_words: [],
|
||||||
history_limit: 5,
|
history_limit: 5,
|
||||||
|
mute_while_recording: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const DEFAULT_AUDIO_DEVICE: AudioDevice = {
|
const DEFAULT_AUDIO_DEVICE: AudioDevice = {
|
||||||
|
|
@ -101,6 +102,8 @@ const settingUpdaters: {
|
||||||
clipboard_handling: (value) =>
|
clipboard_handling: (value) =>
|
||||||
invoke("change_clipboard_handling_setting", { handling: value }),
|
invoke("change_clipboard_handling_setting", { handling: value }),
|
||||||
history_limit: (value) => invoke("update_history_limit", { limit: 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<SettingsStore>()(
|
export const useSettingsStore = create<SettingsStore>()(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue