diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 8b9d402..f4da146 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -161,6 +161,12 @@ dependencies = [ "x11rb", ] +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + [[package]] name = "async-broadcast" version = "0.7.2" @@ -683,6 +689,12 @@ dependencies = [ "libloading 0.8.6", ] +[[package]] +name = "claxon" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bfbf56724aa9eca8afa4fcfadeb479e722935bb2a0900c2d37e0cc477af0688" + [[package]] name = "clipboard-win" version = "5.4.0" @@ -1260,6 +1272,15 @@ version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "endi" version = "1.1.0" @@ -1995,6 +2016,7 @@ dependencies = [ "log", "once_cell", "rdev", + "rodio", "rubato", "serde", "serde_json", @@ -2060,6 +2082,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "hound" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62adaabb884c94955b19907d60019f4e145d091c75345379e70d1ee696f7854f" + [[package]] name = "html5ever" version = "0.29.1" @@ -2604,6 +2632,17 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" +[[package]] +name = "lewton" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "777b48df9aaab155475a83a7df3070395ea1ac6902f5cd062b8f2b028075c030" +dependencies = [ + "byteorder", + "ogg", + "tinyvec", +] + [[package]] name = "libappindicator" version = "0.9.0" @@ -3363,6 +3402,15 @@ dependencies = [ "cc", ] +[[package]] +name = "ogg" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6951b4e8bf21c8193da321bcce9c9dd2e13c858fe078bf9054a288b419ae5d6e" +dependencies = [ + "byteorder", +] + [[package]] name = "once_cell" version = "1.20.2" @@ -4161,6 +4209,19 @@ version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3df6368f71f205ff9c33c076d170dd56ebf68e8161c733c0caa07a7a5509ed53" +[[package]] +name = "rodio" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ceb6607dd738c99bc8cb28eff249b7cd5c8ec88b9db96c0608c1480d140fb1" +dependencies = [ + "claxon", + "cpal", + "hound", + "lewton", + "symphonia", +] + [[package]] name = "rubato" version = "0.16.1" @@ -4743,6 +4804,55 @@ dependencies = [ "serde_json", ] +[[package]] +name = "symphonia" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "815c942ae7ee74737bb00f965fa5b5a2ac2ce7b6c01c0cc169bbeaf7abd5f5a9" +dependencies = [ + "lazy_static", + "symphonia-bundle-mp3", + "symphonia-core", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-bundle-mp3" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c01c2aae70f0f1fb096b6f0ff112a930b1fb3626178fba3ae68b09dce71706d4" +dependencies = [ + "lazy_static", + "log", + "symphonia-core", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-core" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "798306779e3dc7d5231bd5691f5a813496dc79d3f56bf82e25789f2094e022c3" +dependencies = [ + "arrayvec", + "bitflags 1.3.2", + "bytemuck", + "lazy_static", + "log", +] + +[[package]] +name = "symphonia-metadata" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc622b9841a10089c5b18e99eb904f4341615d5aa55bbf4eedde1be721a4023c" +dependencies = [ + "encoding_rs", + "lazy_static", + "log", + "symphonia-core", +] + [[package]] name = "syn" version = "1.0.109" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index eb9e1c4..067cfb1 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -42,6 +42,7 @@ tauri-plugin-store = "2" tauri-plugin-os = "2" enigo = "0.5.0" tauri-plugin-process = "2" +rodio = "0.20.1" [target.'cfg(target_os = "macos")'.dependencies] whisper-rs = { version = "0.13.2", features = ["metal"] } diff --git a/src-tauri/resources/default_settings.json b/src-tauri/resources/default_settings.json index 2dd52ed..2040dc9 100644 --- a/src-tauri/resources/default_settings.json +++ b/src-tauri/resources/default_settings.json @@ -2,7 +2,7 @@ "bindings": { "transcribe": { "id": "transcribe", - "name": "Transcribe", + "name": "Transcribe Keyboard Shortcut", "description": "Converts your speech into text.", "default_binding": "alt+space" } diff --git a/src-tauri/resources/rec_start.wav b/src-tauri/resources/rec_start.wav new file mode 100644 index 0000000..f2d01c5 Binary files /dev/null and b/src-tauri/resources/rec_start.wav differ diff --git a/src-tauri/resources/rec_stop.wav b/src-tauri/resources/rec_stop.wav new file mode 100644 index 0000000..6ac2b61 Binary files /dev/null and b/src-tauri/resources/rec_stop.wav differ diff --git a/src-tauri/src/actions.rs b/src-tauri/src/actions.rs index ef6a976..56ccd61 100644 --- a/src-tauri/src/actions.rs +++ b/src-tauri/src/actions.rs @@ -2,6 +2,8 @@ use crate::managers::audio::AudioRecordingManager; use crate::managers::transcription::TranscriptionManager; use crate::utils; use crate::utils::change_tray_icon; +use crate::utils::play_recording_start_sound; +use crate::utils::play_recording_stop_sound; use crate::utils::TrayIconState; use once_cell::sync::Lazy; use std::collections::HashMap; @@ -23,6 +25,9 @@ impl ShortcutAction for TranscribeAction { let binding_id = binding_id.to_string(); change_tray_icon(app, TrayIconState::Recording); + // Play audio feedback for recording start + play_recording_start_sound(app); + let rm = app.state::>(); rm.try_start_recording(&binding_id); } @@ -34,6 +39,9 @@ impl ShortcutAction for TranscribeAction { change_tray_icon(app, TrayIconState::Idle); + // Play audio feedback for recording stop + play_recording_stop_sound(app); + let binding_id = binding_id.to_string(); // Clone binding_id for the async task tauri::async_runtime::spawn(async move { diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index d6a3da3..41b5c76 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -168,6 +168,7 @@ pub fn run() { shortcut::change_binding, shortcut::reset_binding, shortcut::change_ptt_setting, + shortcut::change_audio_feedback_setting, trigger_update_check ]) .run(tauri::generate_context!()) diff --git a/src-tauri/src/settings.rs b/src-tauri/src/settings.rs index 021c9be..cf5edc6 100644 --- a/src-tauri/src/settings.rs +++ b/src-tauri/src/settings.rs @@ -17,6 +17,7 @@ pub struct ShortcutBinding { pub struct AppSettings { pub bindings: HashMap, pub push_to_talk: bool, + pub audio_feedback: bool, } pub const SETTINGS_STORE_PATH: &str = "settings_store.json"; @@ -47,6 +48,7 @@ pub fn get_default_settings() -> AppSettings { AppSettings { bindings, push_to_talk: true, + audio_feedback: false, } } diff --git a/src-tauri/src/shortcut.rs b/src-tauri/src/shortcut.rs index 3a0b047..fae9de7 100644 --- a/src-tauri/src/shortcut.rs +++ b/src-tauri/src/shortcut.rs @@ -103,6 +103,14 @@ pub fn change_ptt_setting(app: AppHandle, enabled: bool) -> Result<(), String> { Ok(()) } +#[tauri::command] +pub fn change_audio_feedback_setting(app: AppHandle, enabled: bool) -> Result<(), String> { + let mut settings = settings::get_settings(&app); + settings.audio_feedback = enabled; + settings::write_settings(&app, settings); + Ok(()) +} + fn _register_shortcut(app: &AppHandle, binding: ShortcutBinding) -> Result<(), String> { // Parse shortcut and return error if it fails let shortcut = match binding.current_binding.parse::() { diff --git a/src-tauri/src/utils.rs b/src-tauri/src/utils.rs index c312f07..6dd752f 100644 --- a/src-tauri/src/utils.rs +++ b/src-tauri/src/utils.rs @@ -1,8 +1,14 @@ +use crate::settings; + use enigo::Enigo; use enigo::Key; use enigo::Keyboard; use enigo::Settings; +use rodio::{Decoder, OutputStream, Sink}; +use std::fs::File; +use std::io::BufReader; +use std::thread; use tauri::image::Image; use tauri::tray::TrayIcon; use tauri::AppHandle; @@ -80,3 +86,68 @@ pub fn change_tray_icon(app: &AppHandle, icon: TrayIconState) { .expect("failed to set icon"), )); } + +/// Plays an audio resource from the resources directory. +/// Checks if audio feedback is enabled in settings before playing. +pub fn play_sound(app: &AppHandle, resource_path: &str) { + // Check if audio feedback is enabled + let settings = settings::get_settings(app); + if !settings.audio_feedback { + return; + } + + let app_handle = app.clone(); + let resource_path = resource_path.to_string(); + + // Spawn a new thread to play the audio without blocking the main thread + thread::spawn(move || { + // Get the path to the audio file in resources + let audio_path = match app_handle + .path() + .resolve(&resource_path, tauri::path::BaseDirectory::Resource) + { + Ok(path) => path, + Err(e) => { + eprintln!( + "Failed to resolve audio file path '{}': {}", + resource_path, e + ); + return; + } + }; + + // Try to play the audio file + if let Err(e) = play_audio_file(&audio_path) { + eprintln!("Failed to play sound '{}': {}", resource_path, e); + } + }); +} + +/// Convenience function to play the recording start sound +pub fn play_recording_start_sound(app: &AppHandle) { + play_sound(app, "resources/rec_start.wav"); +} + +/// Convenience function to play the recording stop sound +pub fn play_recording_stop_sound(app: &AppHandle) { + play_sound(app, "resources/rec_stop.wav"); +} + +fn play_audio_file(path: &std::path::Path) -> Result<(), Box> { + // Get a output stream handle to the default physical sound device + let (_stream, stream_handle) = OutputStream::try_default()?; + + // Load the audio file + let file = File::open(path)?; + let buf_reader = BufReader::new(file); + let source = Decoder::new(buf_reader)?; + + // Create a sink to play the audio + let sink = Sink::try_new(&stream_handle)?; + sink.append(source); + + // Wait for the audio to finish playing + sink.sleep_until_end(); + + Ok(()) +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index cca5ea6..51c53f7 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -14,9 +14,9 @@ { "title": "Handy", "width": 540, - "height": 400, - "minWidth": 460, - "minHeight": 440, + "height": 510, + "minWidth": 540, + "minHeight": 510, "resizable": true, "maximizable": false } diff --git a/src/App.tsx b/src/App.tsx index 5aed574..1ecb7a0 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,7 +7,7 @@ import Footer from "./components/footer"; function App() { return (
-
+
diff --git a/src/components/settings/KeyboardShortcuts.tsx b/src/components/settings/KeyboardShortcuts.tsx index 9c5609f..6018431 100644 --- a/src/components/settings/KeyboardShortcuts.tsx +++ b/src/components/settings/KeyboardShortcuts.tsx @@ -14,6 +14,8 @@ import ResetIcon from "../icons/ResetIcon"; export const KeyboardShortcuts: React.FC = () => { const [bindings, setBindings] = React.useState({}); const [pttEnabled, setPttEnabled] = React.useState(false); + const [audioFeedbackEnabled, setAudioFeedbackEnabled] = + React.useState(false); const [keyPressed, setKeyPressed] = useState([]); const [recordedKeys, setRecordedKeys] = useState([]); const [editingShortcutId, setEditingShortcutId] = useState( @@ -77,6 +79,7 @@ export const KeyboardShortcuts: React.FC = () => { const settings = SettingsSchema.parse(s); setBindings(settings.bindings); setPttEnabled(settings.push_to_talk); + setAudioFeedbackEnabled(settings.audio_feedback); }); }); }, []); @@ -232,6 +235,30 @@ export const KeyboardShortcuts: React.FC = () => {
+
+
+

Audio Feedback

+

Play sound when recording starts and stops

+
+ +
{Object.entries(bindings).map(([id, binding]) => (
{ return (
-
- {/*

Keyboard Shortcuts

*/} - -
+
); }; diff --git a/src/lib/types.ts b/src/lib/types.ts index 08b810f..59878a5 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -10,12 +10,13 @@ export const ShortcutBindingSchema = z.object({ export const ShortcutBindingsMapSchema = z.record( z.string(), - ShortcutBindingSchema + ShortcutBindingSchema, ); export const SettingsSchema = z.object({ bindings: ShortcutBindingsMapSchema, push_to_talk: z.boolean(), + audio_feedback: z.boolean(), }); export const BindingResponseSchema = z.object({