aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-11-04 01:31:11 +0100
committersalmonik <salmonik@tuta.io>2026-11-04 01:31:11 +0100
commitb9eaa579dac2ae912e3be9a3aa3e73ddcb3416e1 (patch)
tree6082f23150d0c64ae1ac8aeebc5393468263ad82
parenttts: attach the generated WAV without a temp file (diff)
downloadsenator-sperma-b9eaa579dac2ae912e3be9a3aa3e73ddcb3416e1.tar.gz
senator-sperma-b9eaa579dac2ae912e3be9a3aa3e73ddcb3416e1.zip
music: decode compressed playback without a temp file
-rw-r--r--src/music.rs10
-rw-r--r--src/voice/mod.rs36
2 files changed, 34 insertions, 12 deletions
diff --git a/src/music.rs b/src/music.rs
index ac88631..acec278 100644
--- a/src/music.rs
+++ b/src/music.rs
@@ -575,14 +575,8 @@ async fn build_input(request: &TrackRequest, http: &reqwest::Client) -> Result<I
575 info!("compressed to {} bytes", compressed_bytes.len()); 575 info!("compressed to {} bytes", compressed_bytes.len());
576 576
577 // Decode the compressed OGG via ffmpeg (symphonia lacks Opus support). 577 // Decode the compressed OGG via ffmpeg (symphonia lacks Opus support).
578 let compressed_path = format!( 578 // Piping keeps the intermediate out of TEMP_PATH entirely.
579 "{}/play_compressed_{}.ogg", 579 let input = voice::ffmpeg_decode_bytes(compressed_bytes).await?;
580 crate::config::TEMP_PATH,
581 Uuid::new_v4()
582 );
583 let _ = tokio::fs::create_dir_all(crate::config::TEMP_PATH).await;
584 tokio::fs::write(&compressed_path, &compressed_bytes).await?;
585 let input = voice::ffmpeg_decode_file(&compressed_path).await?;
586 Ok(input) 580 Ok(input)
587 } else { 581 } else {
588 match request.kind { 582 match request.kind {
diff --git a/src/voice/mod.rs b/src/voice/mod.rs
index d8308eb..2866f54 100644
--- a/src/voice/mod.rs
+++ b/src/voice/mod.rs
@@ -342,15 +342,17 @@ impl tokio::io::AsyncRead for FfmpegChild {
342 } 342 }
343} 343}
344 344
345/// Decode a local audio file via ffmpeg to raw f32 PCM at 48 kHz stereo. 345/// Decode an in-memory audio file via ffmpeg to raw f32 PCM at 48 kHz stereo.
346/// No reconnect flags (those are for HTTP streams only). 346///
347pub async fn ffmpeg_decode_file(path: &str) -> Result<songbird::input::Input> { 347/// Used for compressed playback so the intermediate file never has to touch
348/// disk (and therefore cannot leak into `TEMP_PATH`).
349pub async fn ffmpeg_decode_bytes(data: Vec<u8>) -> Result<songbird::input::Input> {
348 use std::process::Stdio; 350 use std::process::Stdio;
349 351
350 let mut child = tokio::process::Command::new("ffmpeg") 352 let mut child = tokio::process::Command::new("ffmpeg")
351 .args([ 353 .args([
352 "-i", 354 "-i",
353 path, 355 "pipe:0",
354 "-vn", 356 "-vn",
355 "-f", 357 "-f",
356 "f32le", 358 "f32le",
@@ -360,17 +362,43 @@ pub async fn ffmpeg_decode_file(path: &str) -> Result<songbird::input::Input> {
360 "2", 362 "2",
361 "pipe:1", 363 "pipe:1",
362 ]) 364 ])
365 .stdin(Stdio::piped())
363 .stdout(Stdio::piped()) 366 .stdout(Stdio::piped())
364 .stderr(Stdio::piped()) 367 .stderr(Stdio::piped())
365 .kill_on_drop(true) 368 .kill_on_drop(true)
366 .spawn() 369 .spawn()
367 .map_err(|e| Error::Custom(format!("ffmpeg decode spawn: {e}")))?; 370 .map_err(|e| Error::Custom(format!("ffmpeg decode spawn: {e}")))?;
368 371
372 let mut stdin = child
373 .stdin
374 .take()
375 .ok_or_else(|| Error::Custom("ffmpeg stdin not captured".into()))?;
369 let stdout = child 376 let stdout = child
370 .stdout 377 .stdout
371 .take() 378 .take()
372 .ok_or_else(|| Error::Custom("ffmpeg stdout not captured".into()))?; 379 .ok_or_else(|| Error::Custom("ffmpeg stdout not captured".into()))?;
373 380
381 tokio::spawn(async move {
382 use tokio::io::AsyncWriteExt;
383 let _ = stdin.write_all(&data).await;
384 // Dropping stdin gives ffmpeg EOF.
385 });
386
387 if let Some(stderr) = child.stderr.take() {
388 tokio::spawn(async move {
389 use tokio::io::AsyncReadExt;
390 let mut buf = Vec::with_capacity(4096);
391 let mut reader = tokio::io::BufReader::new(stderr);
392 let _ = reader.read_to_end(&mut buf).await;
393 if !buf.is_empty() {
394 warn!(
395 "ffmpeg (decode) stderr: {}",
396 String::from_utf8_lossy(&buf).trim_end()
397 );
398 }
399 });
400 }
401
374 let ffmpeg_child = FfmpegChild { 402 let ffmpeg_child = FfmpegChild {
375 _child: child, 403 _child: child,
376 stdout, 404 stdout,