Ajout d'un lecteur wav
This commit is contained in:
@@ -104,6 +104,7 @@ mod util;
|
|||||||
pub mod mp3;
|
pub mod mp3;
|
||||||
pub mod ogg;
|
pub mod ogg;
|
||||||
pub mod opus;
|
pub mod opus;
|
||||||
|
pub mod wav;
|
||||||
mod common;
|
mod common;
|
||||||
mod decoder_common;
|
mod decoder_common;
|
||||||
|
|
||||||
@@ -113,4 +114,5 @@ pub use error::FlacError;
|
|||||||
pub use mp3::{decode_mp3_stream, Mp3DecodedStream, Mp3Error};
|
pub use mp3::{decode_mp3_stream, Mp3DecodedStream, Mp3Error};
|
||||||
pub use ogg::{decode_ogg_vorbis_stream, OggDecodedStream, OggError};
|
pub use ogg::{decode_ogg_vorbis_stream, OggDecodedStream, OggError};
|
||||||
pub use opus::{decode_ogg_opus_stream, OggOpusDecodedStream, OggOpusError};
|
pub use opus::{decode_ogg_opus_stream, OggOpusDecodedStream, OggOpusError};
|
||||||
|
pub use wav::{decode_wav_stream, WavDecodedStream, WavError};
|
||||||
pub use pcm::{PcmFormat, StreamInfo};
|
pub use pcm::{PcmFormat, StreamInfo};
|
||||||
|
|||||||
345
pmoflac/src/wav.rs
Normal file
345
pmoflac/src/wav.rs
Normal file
@@ -0,0 +1,345 @@
|
|||||||
|
//! # WAV (RIFF) Decoder Module
|
||||||
|
//!
|
||||||
|
//! Streaming WAV → PCM conversion with zero seeking. The decoder reads the RIFF
|
||||||
|
//! header incrementally, validates the format, and then streams `data` chunk
|
||||||
|
//! payload as little-endian PCM frames through the common async pipeline.
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
fmt,
|
||||||
|
io::{self, Read},
|
||||||
|
pin::Pin,
|
||||||
|
task::{Context, Poll},
|
||||||
|
};
|
||||||
|
|
||||||
|
use tokio::{
|
||||||
|
io::AsyncRead,
|
||||||
|
sync::{mpsc, oneshot},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
common::ChannelReader,
|
||||||
|
decoder_common::{
|
||||||
|
spawn_ingest_task, spawn_writer_task, CHANNEL_CAPACITY, DUPLEX_BUFFER_SIZE,
|
||||||
|
},
|
||||||
|
pcm::StreamInfo,
|
||||||
|
stream::ManagedAsyncReader,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Errors that can occur while decoding WAV data.
|
||||||
|
#[derive(thiserror::Error, Debug, Clone)]
|
||||||
|
pub enum WavError {
|
||||||
|
#[error("I/O error ({kind:?}): {message}")]
|
||||||
|
Io {
|
||||||
|
kind: io::ErrorKind,
|
||||||
|
message: String,
|
||||||
|
},
|
||||||
|
#[error("WAV decode error: {0}")]
|
||||||
|
Decode(String),
|
||||||
|
#[error("internal channel closed unexpectedly")]
|
||||||
|
ChannelClosed,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<io::Error> for WavError {
|
||||||
|
fn from(err: io::Error) -> Self {
|
||||||
|
WavError::Io {
|
||||||
|
kind: err.kind(),
|
||||||
|
message: err.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<String> for WavError {
|
||||||
|
fn from(value: String) -> Self {
|
||||||
|
WavError::Decode(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Streaming WAV reader state.
|
||||||
|
struct StreamingWavReader<E>
|
||||||
|
where
|
||||||
|
E: fmt::Display + std::error::Error,
|
||||||
|
{
|
||||||
|
reader: ChannelReader<E>,
|
||||||
|
buffer: Vec<u8>,
|
||||||
|
position: usize,
|
||||||
|
finished: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E> StreamingWavReader<E>
|
||||||
|
where
|
||||||
|
E: fmt::Display + std::error::Error,
|
||||||
|
{
|
||||||
|
fn new(reader: ChannelReader<E>) -> Self {
|
||||||
|
Self {
|
||||||
|
reader,
|
||||||
|
buffer: Vec::new(),
|
||||||
|
position: 0,
|
||||||
|
finished: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_exact(&mut self, len: usize) -> Result<&[u8], WavError> {
|
||||||
|
while self.buffer.len() - self.position < len {
|
||||||
|
if self.finished {
|
||||||
|
return Err(WavError::Decode("unexpected EOF in WAV header".into()));
|
||||||
|
}
|
||||||
|
let mut chunk = [0u8; 4096];
|
||||||
|
let read = self.reader.read(&mut chunk)?;
|
||||||
|
if read == 0 {
|
||||||
|
self.finished = true;
|
||||||
|
} else {
|
||||||
|
self.buffer.extend_from_slice(&chunk[..read]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let start = self.position;
|
||||||
|
let end = start + len;
|
||||||
|
self.position = end;
|
||||||
|
Ok(&self.buffer[start..end])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn skip(&mut self, mut len: usize) -> Result<(), WavError> {
|
||||||
|
while len > 0 {
|
||||||
|
let available = self.buffer.len() - self.position;
|
||||||
|
if available >= len {
|
||||||
|
self.position += len;
|
||||||
|
return Ok(());
|
||||||
|
} else {
|
||||||
|
self.position += available;
|
||||||
|
len -= available;
|
||||||
|
let mut chunk = [0u8; 4096];
|
||||||
|
let read = self.reader.read(&mut chunk)?;
|
||||||
|
if read == 0 {
|
||||||
|
return Err(WavError::Decode("unexpected EOF while skipping chunk".into()));
|
||||||
|
}
|
||||||
|
self.buffer.clear();
|
||||||
|
self.buffer.extend_from_slice(&chunk[..read]);
|
||||||
|
self.position = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// PCM format metadata extracted from the WAV `fmt ` chunk.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
struct FmtChunk {
|
||||||
|
audio_format: u16,
|
||||||
|
channels: u16,
|
||||||
|
sample_rate: u32,
|
||||||
|
bits_per_sample: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FmtChunk {
|
||||||
|
fn validate(&self) -> Result<(), WavError> {
|
||||||
|
if !(self.audio_format == 0x0001 || self.audio_format == 0x0003) {
|
||||||
|
return Err(WavError::Decode(format!(
|
||||||
|
"unsupported WAV audio format: {}",
|
||||||
|
self.audio_format
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
if self.channels == 0 {
|
||||||
|
return Err(WavError::Decode("WAV channel count must be > 0".into()));
|
||||||
|
}
|
||||||
|
if self.sample_rate == 0 {
|
||||||
|
return Err(WavError::Decode("WAV sample rate must be > 0".into()));
|
||||||
|
}
|
||||||
|
if self.bits_per_sample == 0 || self.bits_per_sample > 32 {
|
||||||
|
return Err(WavError::Decode(format!(
|
||||||
|
"unsupported bits per sample: {}",
|
||||||
|
self.bits_per_sample
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
if self.audio_format == 0x0001 {
|
||||||
|
match self.bits_per_sample {
|
||||||
|
8 | 16 | 24 | 32 => Ok(()),
|
||||||
|
_ => Err(WavError::Decode(format!(
|
||||||
|
"unsupported PCM bit depth: {}",
|
||||||
|
self.bits_per_sample
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Err(WavError::Decode(
|
||||||
|
"IEEE float WAV decoding is not yet supported".into(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bytes_per_sample(&self) -> usize {
|
||||||
|
((self.bits_per_sample as usize) + 7) / 8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An async stream that yields PCM decoded from WAV data.
|
||||||
|
pub struct WavDecodedStream {
|
||||||
|
info: StreamInfo,
|
||||||
|
reader: ManagedAsyncReader<WavError>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WavDecodedStream {
|
||||||
|
pub fn info(&self) -> &StreamInfo {
|
||||||
|
&self.info
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn into_parts(self) -> (StreamInfo, ManagedAsyncReader<WavError>) {
|
||||||
|
(self.info, self.reader)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn wait(self) -> Result<(), WavError> {
|
||||||
|
self.reader.wait().await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsyncRead for WavDecodedStream {
|
||||||
|
fn poll_read(
|
||||||
|
mut self: Pin<&mut Self>,
|
||||||
|
cx: &mut Context<'_>,
|
||||||
|
buf: &mut tokio::io::ReadBuf<'_>,
|
||||||
|
) -> Poll<io::Result<()>> {
|
||||||
|
Pin::new(&mut self.reader).poll_read(cx, buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decode a WAV stream into PCM audio.
|
||||||
|
pub async fn decode_wav_stream<R>(reader: R) -> Result<WavDecodedStream, WavError>
|
||||||
|
where
|
||||||
|
R: AsyncRead + Unpin + Send + 'static,
|
||||||
|
{
|
||||||
|
let (ingest_tx, ingest_rx) = mpsc::channel(CHANNEL_CAPACITY);
|
||||||
|
spawn_ingest_task::<_, WavError>(reader, ingest_tx);
|
||||||
|
|
||||||
|
let (pcm_tx, pcm_rx) = mpsc::channel(CHANNEL_CAPACITY);
|
||||||
|
let (pcm_reader, pcm_writer) = tokio::io::duplex(DUPLEX_BUFFER_SIZE);
|
||||||
|
let (info_tx, info_rx) = oneshot::channel::<Result<StreamInfo, WavError>>();
|
||||||
|
|
||||||
|
let blocking_handle = tokio::task::spawn_blocking(move || -> Result<(), WavError> {
|
||||||
|
let channel_reader = ChannelReader::<WavError>::new(ingest_rx);
|
||||||
|
let mut wav_reader = StreamingWavReader::new(channel_reader);
|
||||||
|
|
||||||
|
let riff = wav_reader.read_exact(12)?;
|
||||||
|
if &riff[0..4] != b"RIFF" {
|
||||||
|
return Err(WavError::Decode("missing RIFF header".into()));
|
||||||
|
}
|
||||||
|
if &riff[8..12] != b"WAVE" {
|
||||||
|
return Err(WavError::Decode("missing WAVE signature".into()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut fmt_chunk: Option<FmtChunk> = None;
|
||||||
|
let mut data_found = false;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let mut chunk_header = [0u8; 8];
|
||||||
|
match wav_reader.read_exact(8) {
|
||||||
|
Ok(bytes) => chunk_header.copy_from_slice(bytes),
|
||||||
|
Err(WavError::Decode(msg)) if msg.contains("unexpected EOF") => break,
|
||||||
|
Err(err) => return Err(err),
|
||||||
|
}
|
||||||
|
let chunk_id = &chunk_header[..4];
|
||||||
|
let chunk_size = u32::from_le_bytes([
|
||||||
|
chunk_header[4],
|
||||||
|
chunk_header[5],
|
||||||
|
chunk_header[6],
|
||||||
|
chunk_header[7],
|
||||||
|
]) as usize;
|
||||||
|
|
||||||
|
let padded_size = (chunk_size + 1) & !1; // align to even bytes
|
||||||
|
|
||||||
|
match chunk_id {
|
||||||
|
b"fmt " => {
|
||||||
|
let bytes = wav_reader.read_exact(chunk_size)?;
|
||||||
|
if chunk_size < 16 {
|
||||||
|
return Err(WavError::Decode("fmt chunk too small".into()));
|
||||||
|
}
|
||||||
|
let audio_format = u16::from_le_bytes([bytes[0], bytes[1]]);
|
||||||
|
let channels = u16::from_le_bytes([bytes[2], bytes[3]]);
|
||||||
|
let sample_rate =
|
||||||
|
u32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]);
|
||||||
|
let bits_per_sample = u16::from_le_bytes([bytes[14], bytes[15]]);
|
||||||
|
let fmt = FmtChunk {
|
||||||
|
audio_format,
|
||||||
|
channels,
|
||||||
|
sample_rate,
|
||||||
|
bits_per_sample,
|
||||||
|
};
|
||||||
|
fmt.validate()?;
|
||||||
|
fmt_chunk = Some(fmt);
|
||||||
|
if padded_size > chunk_size {
|
||||||
|
wav_reader.skip(padded_size - chunk_size)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b"data" => {
|
||||||
|
let fmt = fmt_chunk
|
||||||
|
.as_ref()
|
||||||
|
.ok_or_else(|| WavError::Decode("data chunk before fmt chunk".into()))?;
|
||||||
|
|
||||||
|
let info = StreamInfo {
|
||||||
|
sample_rate: fmt.sample_rate,
|
||||||
|
channels: fmt.channels as u8,
|
||||||
|
bits_per_sample: fmt.bits_per_sample as u8,
|
||||||
|
total_samples: None,
|
||||||
|
max_block_size: 0,
|
||||||
|
min_block_size: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
if info_tx.send(Ok(info.clone())).is_err() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut remaining = chunk_size;
|
||||||
|
let bytes_per_frame = fmt.bytes_per_sample() * fmt.channels as usize;
|
||||||
|
let mut buffer = vec![0u8; 4096];
|
||||||
|
|
||||||
|
while remaining > 0 {
|
||||||
|
let to_read = remaining.min(buffer.len());
|
||||||
|
let read = wav_reader.reader.read(&mut buffer[..to_read])?;
|
||||||
|
if read == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
remaining -= read;
|
||||||
|
let aligned = read - (read % bytes_per_frame);
|
||||||
|
if aligned > 0 {
|
||||||
|
if pcm_tx
|
||||||
|
.blocking_send(Ok(buffer[..aligned].to_vec()))
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if aligned < read {
|
||||||
|
return Err(WavError::Decode(
|
||||||
|
"incomplete frame at end of chunk".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if padded_size > chunk_size {
|
||||||
|
let mut pad = [0u8; 1];
|
||||||
|
wav_reader.reader.read_exact(&mut pad)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
data_found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
wav_reader.skip(chunk_size)?;
|
||||||
|
if padded_size > chunk_size {
|
||||||
|
wav_reader.skip(padded_size - chunk_size)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !data_found {
|
||||||
|
return Err(WavError::Decode("no data chunk found in WAV stream".into()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
|
||||||
|
let writer_handle = spawn_writer_task(pcm_rx, pcm_writer, blocking_handle, "wav-decode");
|
||||||
|
|
||||||
|
let info = info_rx.await.map_err(|_| WavError::ChannelClosed)??;
|
||||||
|
let reader = ManagedAsyncReader::new("wav-decode-writer", pcm_reader, writer_handle);
|
||||||
|
|
||||||
|
Ok(WavDecodedStream { info, reader })
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user