feat: complete Phase 12 — voice playback ticker, cache, config, and UI

Add playback position ticker in event loop with 1s UI refresh rate,
integrate VoiceCache for downloaded voice files, add [audio] config
section (cache_size_mb, auto_download_voice), and render progress bar
with waveform visualization in message bubbles.

Fix race conditions in AudioPlayer: add `starting` flag to prevent
false `is_stopped()` during ffplay startup, guard pid cleanup so old
threads don't overwrite newer process pids. Implement `resume_from()`
with ffplay `-ss` for real audio seek on unpause (-1s rewind).

Kill ffplay on app exit via `stop_playback()` in shutdown + Drop impl.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Mikhail Kilin
2026-02-09 16:37:02 +03:00
parent 7bc264198f
commit 8a467b6418
13 changed files with 278 additions and 48 deletions

View File

@@ -107,6 +107,8 @@ pub struct App<T: TdClientTrait = TdClient> {
pub voice_cache: Option<crate::audio::VoiceCache>,
/// Состояние текущего воспроизведения
pub playback_state: Option<crate::tdlib::PlaybackState>,
/// Время последнего тика для обновления позиции воспроизведения
pub last_playback_tick: Option<std::time::Instant>,
}
impl<T: TdClientTrait> App<T> {
@@ -126,6 +128,8 @@ impl<T: TdClientTrait> App<T> {
let mut state = ListState::default();
state.select(Some(0));
let audio_cache_size_mb = config.audio.cache_size_mb;
#[cfg(feature = "images")]
let image_cache = Some(crate::media::cache::ImageCache::new(
config.images.cache_size_mb,
@@ -169,8 +173,9 @@ impl<T: TdClientTrait> App<T> {
last_image_render_time: None,
// Voice playback
audio_player: crate::audio::AudioPlayer::new().ok(),
voice_cache: crate::audio::VoiceCache::new().ok(),
voice_cache: crate::audio::VoiceCache::new(audio_cache_size_mb).ok(),
playback_state: None,
last_playback_tick: None,
}
}
@@ -198,6 +203,7 @@ impl<T: TdClientTrait> App<T> {
player.stop();
}
self.playback_state = None;
self.last_playback_tick = None;
self.status_message = None;
}