Improve audio feedback timing using real sound duration and trimmed WAV files (#349)
* feat: add sound duration retrieval for audio feedback * Trim trailing silence from pop_start.wav and pop_stop.wav * refactor: simplify sound file and base directory retrieval * feat: only delay mute if audio feedback is enabled * change the logic * simplify new logic * simplify get_sound_path and get_sound_base_dir, reorder functions * add implicit blocking to playTestSound * modify to my pref style --------- Co-authored-by: CJ Pais <cj@cjpais.com>
This commit is contained in:
parent
9c4b1e1f5c
commit
c2672638c9
5 changed files with 70 additions and 65 deletions
Binary file not shown.
Binary file not shown.
|
|
@ -1,4 +1,4 @@
|
|||
use crate::audio_feedback::{play_feedback_sound, SoundType};
|
||||
use crate::audio_feedback::{play_feedback_sound, play_feedback_sound_blocking, SoundType};
|
||||
use crate::managers::audio::AudioRecordingManager;
|
||||
use crate::managers::history::HistoryManager;
|
||||
use crate::managers::transcription::TranscriptionManager;
|
||||
|
|
@ -183,12 +183,12 @@ impl ShortcutAction for TranscribeAction {
|
|||
if is_always_on {
|
||||
// Always-on mode: Play audio feedback immediately, then apply mute after sound finishes
|
||||
debug!("Always-on mode: Playing audio feedback immediately");
|
||||
play_feedback_sound(app, SoundType::Start);
|
||||
|
||||
// Apply mute after audio feedback has time to play (500ms should be enough for most sounds)
|
||||
let rm_clone = Arc::clone(&rm);
|
||||
let app_clone = app.clone();
|
||||
// The blocking helper exits immediately if audio feedback is disabled,
|
||||
// so we can always reuse this thread to ensure mute happens right after playback.
|
||||
std::thread::spawn(move || {
|
||||
std::thread::sleep(std::time::Duration::from_millis(500));
|
||||
play_feedback_sound_blocking(&app_clone, SoundType::Start);
|
||||
rm_clone.apply_mute();
|
||||
});
|
||||
|
||||
|
|
@ -206,11 +206,10 @@ impl ShortcutAction for TranscribeAction {
|
|||
let rm_clone = Arc::clone(&rm);
|
||||
std::thread::spawn(move || {
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
debug!("Playing delayed audio feedback");
|
||||
play_feedback_sound(&app_clone, SoundType::Start);
|
||||
|
||||
// Apply mute after audio feedback has time to play
|
||||
std::thread::sleep(std::time::Duration::from_millis(500));
|
||||
debug!("Handling delayed audio feedback/mute sequence");
|
||||
// Helper handles disabled audio feedback by returning early, so we reuse it
|
||||
// to keep mute sequencing consistent in every mode.
|
||||
play_feedback_sound_blocking(&app_clone, SoundType::Start);
|
||||
rm_clone.apply_mute();
|
||||
});
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
use crate::settings;
|
||||
use crate::settings::SoundTheme;
|
||||
use crate::settings::{self, AppSettings};
|
||||
use cpal::traits::{DeviceTrait, HostTrait};
|
||||
use log::{debug, error, warn};
|
||||
use rodio::OutputStreamBuilder;
|
||||
use std::fs::File;
|
||||
use std::io::BufReader;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::thread;
|
||||
use tauri::{AppHandle, Manager};
|
||||
|
||||
|
|
@ -12,73 +14,79 @@ pub enum SoundType {
|
|||
Stop,
|
||||
}
|
||||
|
||||
/// Plays an audio resource from the specified directory.
|
||||
fn play_sound(app: &AppHandle, resource_path: &str, base_dir: tauri::path::BaseDirectory) {
|
||||
let app_handle = app.clone();
|
||||
let resource_path = resource_path.to_string();
|
||||
let volume = settings::get_settings(app).audio_feedback_volume;
|
||||
|
||||
thread::spawn(move || {
|
||||
let audio_path = match app_handle.path().resolve(&resource_path, base_dir) {
|
||||
Ok(path) => path.to_path_buf(),
|
||||
Err(e) => {
|
||||
error!(
|
||||
"Failed to resolve audio file path '{}': {}",
|
||||
resource_path, e
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let settings = settings::get_settings(&app_handle);
|
||||
let selected_device = settings.selected_output_device.clone();
|
||||
|
||||
if let Err(e) = play_audio_file(&audio_path, selected_device, volume) {
|
||||
error!("Failed to play sound '{}': {}", resource_path, e);
|
||||
}
|
||||
});
|
||||
fn resolve_sound_path(
|
||||
app: &AppHandle,
|
||||
settings: &AppSettings,
|
||||
sound_type: SoundType,
|
||||
) -> Option<PathBuf> {
|
||||
let sound_file = get_sound_path(settings, sound_type);
|
||||
let base_dir = get_sound_base_dir(settings);
|
||||
app.path().resolve(&sound_file, base_dir).ok()
|
||||
}
|
||||
|
||||
fn get_sound_path(app: &AppHandle, sound_type: SoundType) -> String {
|
||||
let settings = settings::get_settings(app);
|
||||
match sound_type {
|
||||
SoundType::Start => match settings.sound_theme {
|
||||
crate::settings::SoundTheme::Custom => "custom_start.wav".to_string(),
|
||||
_ => settings.sound_theme.to_start_path(),
|
||||
},
|
||||
SoundType::Stop => match settings.sound_theme {
|
||||
crate::settings::SoundTheme::Custom => "custom_stop.wav".to_string(),
|
||||
_ => settings.sound_theme.to_stop_path(),
|
||||
},
|
||||
fn get_sound_path(settings: &AppSettings, sound_type: SoundType) -> String {
|
||||
match (settings.sound_theme, sound_type) {
|
||||
(SoundTheme::Custom, SoundType::Start) => "custom_start.wav".to_string(),
|
||||
(SoundTheme::Custom, SoundType::Stop) => "custom_stop.wav".to_string(),
|
||||
(_, SoundType::Start) => settings.sound_theme.to_start_path(),
|
||||
(_, SoundType::Stop) => settings.sound_theme.to_stop_path(),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_sound_base_dir(settings: &AppSettings) -> tauri::path::BaseDirectory {
|
||||
match settings.sound_theme {
|
||||
SoundTheme::Custom => tauri::path::BaseDirectory::AppData,
|
||||
_ => tauri::path::BaseDirectory::Resource,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn play_feedback_sound(app: &AppHandle, sound_type: SoundType) {
|
||||
// Only play if audio feedback is enabled
|
||||
let settings = settings::get_settings(app);
|
||||
if !settings.audio_feedback {
|
||||
return;
|
||||
}
|
||||
if let Some(path) = resolve_sound_path(app, &settings, sound_type) {
|
||||
play_sound_async(app, path);
|
||||
}
|
||||
}
|
||||
|
||||
let sound_file = get_sound_path(app, sound_type);
|
||||
let base_dir = if settings.sound_theme == crate::settings::SoundTheme::Custom {
|
||||
tauri::path::BaseDirectory::AppData
|
||||
} else {
|
||||
tauri::path::BaseDirectory::Resource
|
||||
};
|
||||
play_sound(app, &sound_file, base_dir);
|
||||
pub fn play_feedback_sound_blocking(app: &AppHandle, sound_type: SoundType) {
|
||||
let settings = settings::get_settings(app);
|
||||
if !settings.audio_feedback {
|
||||
return;
|
||||
}
|
||||
if let Some(path) = resolve_sound_path(app, &settings, sound_type) {
|
||||
play_sound_blocking(app, &path);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn play_test_sound(app: &AppHandle, sound_type: SoundType) {
|
||||
// Always play test sound, regardless of audio_feedback setting
|
||||
let settings = settings::get_settings(app);
|
||||
let sound_file = get_sound_path(app, sound_type);
|
||||
let base_dir = if settings.sound_theme == crate::settings::SoundTheme::Custom {
|
||||
tauri::path::BaseDirectory::AppData
|
||||
} else {
|
||||
tauri::path::BaseDirectory::Resource
|
||||
};
|
||||
play_sound(app, &sound_file, base_dir);
|
||||
if let Some(path) = resolve_sound_path(app, &settings, sound_type) {
|
||||
play_sound_async(app, path);
|
||||
}
|
||||
}
|
||||
|
||||
fn play_sound_async(app: &AppHandle, path: PathBuf) {
|
||||
let app_handle = app.clone();
|
||||
thread::spawn(move || {
|
||||
if let Err(e) = play_sound_at_path(&app_handle, path.as_path()) {
|
||||
error!("Failed to play sound '{}': {}", path.display(), e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn play_sound_blocking(app: &AppHandle, path: &Path) {
|
||||
if let Err(e) = play_sound_at_path(app, path) {
|
||||
error!("Failed to play sound '{}': {}", path.display(), e);
|
||||
}
|
||||
}
|
||||
|
||||
fn play_sound_at_path(app: &AppHandle, path: &Path) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let settings = settings::get_settings(app);
|
||||
let volume = settings.audio_feedback_volume;
|
||||
let selected_device = settings.selected_output_device.clone();
|
||||
play_audio_file(path, selected_device, volume)
|
||||
}
|
||||
|
||||
fn play_audio_file(
|
||||
|
|
|
|||
|
|
@ -33,8 +33,6 @@ export const SoundPicker: React.FC<SoundPickerProps> = ({
|
|||
|
||||
const handlePlayBothSounds = async () => {
|
||||
await playTestSound("start");
|
||||
// Wait before playing stop sound
|
||||
await new Promise((resolve) => setTimeout(resolve, 800));
|
||||
await playTestSound("stop");
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue