sample rec sound

This commit is contained in:
CJ Pais 2025-06-27 12:54:29 -07:00
parent d2c73bda7a
commit ab79f77747
14 changed files with 219 additions and 10 deletions

110
src-tauri/Cargo.lock generated
View file

@ -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"

View file

@ -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"] }

View file

@ -2,7 +2,7 @@
"bindings": {
"transcribe": {
"id": "transcribe",
"name": "Transcribe",
"name": "Transcribe Keyboard Shortcut",
"description": "Converts your speech into text.",
"default_binding": "alt+space"
}

BIN
src-tauri/resources/rec.wav Normal file

Binary file not shown.

View file

@ -2,6 +2,7 @@ use crate::managers::audio::AudioRecordingManager;
use crate::managers::transcription::TranscriptionManager;
use crate::utils;
use crate::utils::change_tray_icon;
use crate::utils::play_recording_sound;
use crate::utils::TrayIconState;
use once_cell::sync::Lazy;
use std::collections::HashMap;
@ -23,6 +24,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_sound(app);
let rm = app.state::<Arc<AudioRecordingManager>>();
rm.try_start_recording(&binding_id);
}
@ -34,6 +38,9 @@ impl ShortcutAction for TranscribeAction {
change_tray_icon(app, TrayIconState::Idle);
// Play audio feedback for recording stop
play_recording_sound(app);
let binding_id = binding_id.to_string(); // Clone binding_id for the async task
tauri::async_runtime::spawn(async move {

View file

@ -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!())

View file

@ -17,6 +17,7 @@ pub struct ShortcutBinding {
pub struct AppSettings {
pub bindings: HashMap<String, ShortcutBinding>,
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: true,
}
}

View file

@ -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::<Shortcut>() {

View file

@ -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,52 @@ pub fn change_tray_icon(app: &AppHandle, icon: TrayIconState) {
.expect("failed to set icon"),
));
}
pub fn play_recording_sound(app: &AppHandle) {
// Check if audio feedback is enabled
let settings = settings::get_settings(app);
if !settings.audio_feedback {
return;
}
let app_handle = app.clone();
// Spawn a new thread to play the audio without blocking the main thread
thread::spawn(move || {
// Get the path to the rec.wav file in resources
let audio_path = match app_handle
.path()
.resolve("resources/rec.wav", tauri::path::BaseDirectory::Resource)
{
Ok(path) => path,
Err(e) => {
eprintln!("Failed to resolve audio file path: {}", e);
return;
}
};
// Try to play the audio file
if let Err(e) = play_audio_file(&audio_path) {
eprintln!("Failed to play recording sound: {}", e);
}
});
}
fn play_audio_file(path: &std::path::Path) -> Result<(), Box<dyn std::error::Error>> {
// 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(())
}

View file

@ -14,9 +14,9 @@
{
"title": "Handy",
"width": 540,
"height": 400,
"minWidth": 460,
"minHeight": 440,
"height": 510,
"minWidth": 540,
"minHeight": 510,
"resizable": true,
"maximizable": false
}

View file

@ -7,7 +7,7 @@ import Footer from "./components/footer";
function App() {
return (
<div className="min-h-screen flex flex-col w-full">
<div className="flex flex-col items-center pt-6 gap-4 px-4 flex-1">
<div className="flex flex-col items-center pt-6 gap-8 px-4 flex-1">
<HandyTextLogo width={300} />
<AccessibilityPermissions />
<Settings />

View file

@ -14,6 +14,8 @@ import ResetIcon from "../icons/ResetIcon";
export const KeyboardShortcuts: React.FC = () => {
const [bindings, setBindings] = React.useState<ShortcutBindingsMap>({});
const [pttEnabled, setPttEnabled] = React.useState<boolean>(false);
const [audioFeedbackEnabled, setAudioFeedbackEnabled] =
React.useState<boolean>(true);
const [keyPressed, setKeyPressed] = useState<string[]>([]);
const [recordedKeys, setRecordedKeys] = useState<string[]>([]);
const [editingShortcutId, setEditingShortcutId] = useState<string | null>(
@ -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 = () => {
<div className="relative w-11 h-6 bg-mid-gray/20 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-logo-primary rounded-full peer peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-logo-primary"></div>
</label>
</div>
<div className="flex items-center justify-between p-4 rounded-lg border border-mid-gray/20 ">
<div className="max-w-2/3">
<h3 className="text-sm font-medium ">Audio Feedback</h3>
<p className="text-sm">Play sound when recording starts and stops</p>
</div>
<label className="inline-flex items-center cursor-pointer">
<input
type="checkbox"
value=""
className="sr-only peer"
checked={audioFeedbackEnabled}
onChange={(e) => {
console.log("change audio feedback setting", e.target.checked);
const newValue = e.target.checked;
setAudioFeedbackEnabled(newValue);
invoke("change_audio_feedback_setting", {
enabled: newValue,
});
}}
/>
<div className="relative w-11 h-6 bg-mid-gray/20 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-logo-primary rounded-full peer peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-logo-primary"></div>
</label>
</div>
{Object.entries(bindings).map(([id, binding]) => (
<div
key={id}

View file

@ -5,10 +5,7 @@ import { KeyboardShortcuts } from "./KeyboardShortcuts";
export const Settings: React.FC = () => {
return (
<div className="max-w-3xl w-full mx-auto space-y-4">
<div className="mt-8">
{/* <h2 className="text-xl font-semibold mb-2">Keyboard Shortcuts</h2> */}
<KeyboardShortcuts />
</div>
<KeyboardShortcuts />
</div>
);
};

View file

@ -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({