Files
pmomusic/pmoaudiocache/src/streaming.rs

162 lines
5.1 KiB
Rust
Raw Normal View History

//! Audio streaming transformer built on pmoflac.
2025-10-27 17:30:54 +01:00
//!
//! This module wires the generic `pmocache` download pipeline with the new
//! streaming transcode helper provided by `pmoflac`. Any supported codec
//! (FLAC, MP3, OGG/Vorbis, Opus, WAV, AIFF) is converted to FLAC on the fly,
//! while native FLAC input is forwarded byte-for-byte without re-encoding.
2025-10-27 17:30:54 +01:00
use bytes::Bytes;
use pmocache::download::TransformMetadata;
2025-10-27 17:30:54 +01:00
use pmocache::StreamTransformer;
use pmoflac::{transcode_to_flac_stream, AudioCodec, TranscodeOptions};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
2025-10-27 17:30:54 +01:00
/// Creates the transformer consumed by the audio cache.
2025-10-27 17:30:54 +01:00
pub fn create_streaming_flac_transformer() -> StreamTransformer {
Box::new(|input, mut file, context| {
2025-10-27 17:30:54 +01:00
Box::pin(async move {
let byte_stream = input.into_byte_stream();
let reader = StreamToAsyncRead::new(byte_stream);
2025-10-27 17:30:54 +01:00
let transcode = transcode_to_flac_stream(reader, TranscodeOptions::default())
.await
.map_err(|e| format!("Audio transcode error: {}", e))?;
2025-10-27 17:30:54 +01:00
let codec = transcode.input_codec();
let info = transcode.input_stream_info().clone();
log_stream_info(codec, &info);
let mode = if transcode.is_passthrough() {
"passthrough"
} else {
"transcode"
};
context
.set_metadata(TransformMetadata {
mode: Some(mode.to_string()),
input_codec: Some(codec_to_string(codec)),
details: None,
})
.await;
2025-10-27 17:30:54 +01:00
let mut flac_stream = transcode.into_stream();
let mut buffer = vec![0u8; 64 * 1024];
let mut total_written = 0u64;
2025-10-27 17:30:54 +01:00
loop {
let read = flac_stream
.read(&mut buffer)
.await
.map_err(|e| format!("Failed to read FLAC data: {}", e))?;
2025-10-27 17:30:54 +01:00
if read == 0 {
break;
}
2025-10-27 17:30:54 +01:00
file.write_all(&buffer[..read])
.await
.map_err(|e| format!("Failed to write FLAC file: {}", e))?;
2025-10-27 17:30:54 +01:00
total_written += read as u64;
context.report_progress(total_written);
}
2025-10-27 17:30:54 +01:00
file.flush()
.await
.map_err(|e| format!("Failed to flush FLAC file: {}", e))?;
2025-10-27 17:30:54 +01:00
flac_stream
.wait()
.await
.map_err(|e| format!("FLAC encoder error: {}", e))?;
2025-10-27 17:30:54 +01:00
Ok(())
})
})
2025-10-27 17:30:54 +01:00
}
fn log_stream_info(codec: AudioCodec, info: &pmoflac::StreamInfo) {
2025-10-27 17:30:54 +01:00
tracing::debug!(
"Detected codec {:?}: {} Hz, {} channels, {} bits/sample (passthrough={})",
codec,
info.sample_rate,
info.channels,
info.bits_per_sample,
codec == AudioCodec::Flac
2025-10-27 17:30:54 +01:00
);
}
fn codec_to_string(codec: AudioCodec) -> String {
match codec {
AudioCodec::Flac => "flac",
AudioCodec::Mp3 => "mp3",
AudioCodec::OggVorbis => "ogg_vorbis",
AudioCodec::OggOpus => "ogg_opus",
AudioCodec::Wav => "wav",
AudioCodec::Aiff => "aiff",
}
.to_string()
}
/// Adapter exposing a byte stream as `AsyncRead`.
2025-10-27 17:30:54 +01:00
struct StreamToAsyncRead {
stream: futures_util::stream::BoxStream<'static, Result<Bytes, String>>,
current_chunk: Option<Bytes>,
offset: usize,
2025-10-27 17:30:54 +01:00
}
impl StreamToAsyncRead {
fn new(
stream: std::pin::Pin<Box<dyn futures_util::Stream<Item = Result<Bytes, String>> + Send>>,
2025-10-27 17:30:54 +01:00
) -> Self {
use futures_util::StreamExt;
Self {
stream: stream.boxed(),
current_chunk: None,
offset: 0,
2025-10-27 17:30:54 +01:00
}
}
}
impl tokio::io::AsyncRead for StreamToAsyncRead {
fn poll_read(
mut self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
buf: &mut tokio::io::ReadBuf<'_>,
) -> std::task::Poll<std::io::Result<()>> {
use futures_util::StreamExt;
use std::task::Poll;
loop {
if let Some(chunk) = &self.current_chunk {
if self.offset < chunk.len() {
let available = chunk.len() - self.offset;
let to_copy = available.min(buf.remaining());
buf.put_slice(&chunk[self.offset..self.offset + to_copy]);
self.offset += to_copy;
2025-10-27 17:30:54 +01:00
return Poll::Ready(Ok(()));
}
self.current_chunk = None;
self.offset = 0;
2025-10-27 17:30:54 +01:00
}
match self.stream.poll_next_unpin(cx) {
Poll::Ready(Some(Ok(chunk))) => {
if chunk.is_empty() {
continue;
}
self.current_chunk = Some(chunk);
}
Poll::Ready(Some(Err(e))) => {
return Poll::Ready(Err(std::io::Error::new(std::io::ErrorKind::Other, e)));
2025-10-27 17:30:54 +01:00
}
Poll::Ready(None) => return Poll::Ready(Ok(())),
Poll::Pending => return Poll::Pending,
2025-10-27 17:30:54 +01:00
}
}
}
}
impl Unpin for StreamToAsyncRead {}