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::audio::AudioRecordingManager;
|
||||||
use crate::managers::history::HistoryManager;
|
use crate::managers::history::HistoryManager;
|
||||||
use crate::managers::transcription::TranscriptionManager;
|
use crate::managers::transcription::TranscriptionManager;
|
||||||
|
|
@ -183,12 +183,12 @@ impl ShortcutAction for TranscribeAction {
|
||||||
if is_always_on {
|
if is_always_on {
|
||||||
// Always-on mode: Play audio feedback immediately, then apply mute after sound finishes
|
// Always-on mode: Play audio feedback immediately, then apply mute after sound finishes
|
||||||
debug!("Always-on mode: Playing audio feedback immediately");
|
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 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::spawn(move || {
|
||||||
std::thread::sleep(std::time::Duration::from_millis(500));
|
play_feedback_sound_blocking(&app_clone, SoundType::Start);
|
||||||
rm_clone.apply_mute();
|
rm_clone.apply_mute();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -206,11 +206,10 @@ impl ShortcutAction for TranscribeAction {
|
||||||
let rm_clone = Arc::clone(&rm);
|
let rm_clone = Arc::clone(&rm);
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||||
debug!("Playing delayed audio feedback");
|
debug!("Handling delayed audio feedback/mute sequence");
|
||||||
play_feedback_sound(&app_clone, SoundType::Start);
|
// Helper handles disabled audio feedback by returning early, so we reuse it
|
||||||
|
// to keep mute sequencing consistent in every mode.
|
||||||
// Apply mute after audio feedback has time to play
|
play_feedback_sound_blocking(&app_clone, SoundType::Start);
|
||||||
std::thread::sleep(std::time::Duration::from_millis(500));
|
|
||||||
rm_clone.apply_mute();
|
rm_clone.apply_mute();
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
use crate::settings;
|
use crate::settings::SoundTheme;
|
||||||
|
use crate::settings::{self, AppSettings};
|
||||||
use cpal::traits::{DeviceTrait, HostTrait};
|
use cpal::traits::{DeviceTrait, HostTrait};
|
||||||
use log::{debug, error, warn};
|
use log::{debug, error, warn};
|
||||||
use rodio::OutputStreamBuilder;
|
use rodio::OutputStreamBuilder;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::BufReader;
|
use std::io::BufReader;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use tauri::{AppHandle, Manager};
|
use tauri::{AppHandle, Manager};
|
||||||
|
|
||||||
|
|
@ -12,73 +14,79 @@ pub enum SoundType {
|
||||||
Stop,
|
Stop,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Plays an audio resource from the specified directory.
|
fn resolve_sound_path(
|
||||||
fn play_sound(app: &AppHandle, resource_path: &str, base_dir: tauri::path::BaseDirectory) {
|
app: &AppHandle,
|
||||||
let app_handle = app.clone();
|
settings: &AppSettings,
|
||||||
let resource_path = resource_path.to_string();
|
sound_type: SoundType,
|
||||||
let volume = settings::get_settings(app).audio_feedback_volume;
|
) -> Option<PathBuf> {
|
||||||
|
let sound_file = get_sound_path(settings, sound_type);
|
||||||
thread::spawn(move || {
|
let base_dir = get_sound_base_dir(settings);
|
||||||
let audio_path = match app_handle.path().resolve(&resource_path, base_dir) {
|
app.path().resolve(&sound_file, base_dir).ok()
|
||||||
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 get_sound_path(app: &AppHandle, sound_type: SoundType) -> String {
|
fn get_sound_path(settings: &AppSettings, sound_type: SoundType) -> String {
|
||||||
let settings = settings::get_settings(app);
|
match (settings.sound_theme, sound_type) {
|
||||||
match sound_type {
|
(SoundTheme::Custom, SoundType::Start) => "custom_start.wav".to_string(),
|
||||||
SoundType::Start => match settings.sound_theme {
|
(SoundTheme::Custom, SoundType::Stop) => "custom_stop.wav".to_string(),
|
||||||
crate::settings::SoundTheme::Custom => "custom_start.wav".to_string(),
|
(_, SoundType::Start) => settings.sound_theme.to_start_path(),
|
||||||
_ => settings.sound_theme.to_start_path(),
|
(_, SoundType::Stop) => settings.sound_theme.to_stop_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_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) {
|
pub fn play_feedback_sound(app: &AppHandle, sound_type: SoundType) {
|
||||||
// Only play if audio feedback is enabled
|
|
||||||
let settings = settings::get_settings(app);
|
let settings = settings::get_settings(app);
|
||||||
if !settings.audio_feedback {
|
if !settings.audio_feedback {
|
||||||
return;
|
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);
|
pub fn play_feedback_sound_blocking(app: &AppHandle, sound_type: SoundType) {
|
||||||
let base_dir = if settings.sound_theme == crate::settings::SoundTheme::Custom {
|
let settings = settings::get_settings(app);
|
||||||
tauri::path::BaseDirectory::AppData
|
if !settings.audio_feedback {
|
||||||
} else {
|
return;
|
||||||
tauri::path::BaseDirectory::Resource
|
}
|
||||||
};
|
if let Some(path) = resolve_sound_path(app, &settings, sound_type) {
|
||||||
play_sound(app, &sound_file, base_dir);
|
play_sound_blocking(app, &path);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn play_test_sound(app: &AppHandle, sound_type: SoundType) {
|
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 settings = settings::get_settings(app);
|
||||||
let sound_file = get_sound_path(app, sound_type);
|
if let Some(path) = resolve_sound_path(app, &settings, sound_type) {
|
||||||
let base_dir = if settings.sound_theme == crate::settings::SoundTheme::Custom {
|
play_sound_async(app, path);
|
||||||
tauri::path::BaseDirectory::AppData
|
}
|
||||||
} else {
|
}
|
||||||
tauri::path::BaseDirectory::Resource
|
|
||||||
};
|
fn play_sound_async(app: &AppHandle, path: PathBuf) {
|
||||||
play_sound(app, &sound_file, base_dir);
|
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(
|
fn play_audio_file(
|
||||||
|
|
|
||||||
|
|
@ -33,8 +33,6 @@ export const SoundPicker: React.FC<SoundPickerProps> = ({
|
||||||
|
|
||||||
const handlePlayBothSounds = async () => {
|
const handlePlayBothSounds = async () => {
|
||||||
await playTestSound("start");
|
await playTestSound("start");
|
||||||
// Wait before playing stop sound
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 800));
|
|
||||||
await playTestSound("stop");
|
await playTestSound("stop");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue