sample rec sound
This commit is contained in:
parent
d2c73bda7a
commit
ab79f77747
14 changed files with 219 additions and 10 deletions
110
src-tauri/Cargo.lock
generated
110
src-tauri/Cargo.lock
generated
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"] }
|
||||
|
|
|
|||
|
|
@ -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
BIN
src-tauri/resources/rec.wav
Normal file
Binary file not shown.
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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!())
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>() {
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,9 +14,9 @@
|
|||
{
|
||||
"title": "Handy",
|
||||
"width": 540,
|
||||
"height": 400,
|
||||
"minWidth": 460,
|
||||
"minHeight": 440,
|
||||
"height": 510,
|
||||
"minWidth": 540,
|
||||
"minHeight": 510,
|
||||
"resizable": true,
|
||||
"maximizable": false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
Loading…
Reference in a new issue