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

@@ -167,6 +167,42 @@ async fn run_app<B: ratatui::backend::Backend, T: tdlib::TdClientTrait>(
app.needs_redraw = true;
}
// Обновляем позицию воспроизведения голосового сообщения
{
let mut stop_playback = false;
if let Some(ref mut playback) = app.playback_state {
use crate::tdlib::PlaybackStatus;
match playback.status {
PlaybackStatus::Playing => {
let prev_second = playback.position as u32;
if let Some(last_tick) = app.last_playback_tick {
let delta = last_tick.elapsed().as_secs_f32();
playback.position += delta;
}
app.last_playback_tick = Some(std::time::Instant::now());
// Проверяем завершение воспроизведения
if playback.position >= playback.duration
|| app.audio_player.as_ref().map_or(false, |p| p.is_stopped())
{
stop_playback = true;
}
// Перерисовка только при смене секунды (не 60 FPS)
if playback.position as u32 != prev_second || stop_playback {
app.needs_redraw = true;
}
}
_ => {
app.last_playback_tick = None;
}
}
}
if stop_playback {
app.stop_playback();
app.last_playback_tick = None;
}
}
// Рендерим только если есть изменения
if app.needs_redraw {
terminal.draw(|f| ui::render(f, app))?;
@@ -185,6 +221,9 @@ async fn run_app<B: ratatui::backend::Backend, T: tdlib::TdClientTrait>(
// Graceful shutdown
should_stop.store(true, Ordering::Relaxed);
// Останавливаем воспроизведение голосового (убиваем ffplay)
app.stop_playback();
// Закрываем TDLib клиент
let _ = tdlib_rs::functions::close(app.td_client.client_id()).await;