From bb2673f92c1bf277042892e9314d32c2f55e6287 Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Mon, 27 Oct 2025 20:42:16 +0100 Subject: [PATCH] =?UTF-8?q?Cr=C3=A9ation=20d'un=20d=C3=A9codeur=20g=C3=A9n?= =?UTF-8?q?=C3=A9rique?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + pmoflac/src/aiff.rs | 36 ++-- pmoflac/src/autodetect.rs | 316 ++++++++++++++++++++++++++++++++++ pmoflac/src/decoder_common.rs | 6 +- pmoflac/src/lib.rs | 18 +- pmoflac/src/mp3.rs | 4 +- pmoflac/src/ogg.rs | 10 +- pmoflac/src/opus.rs | 18 +- pmoflac/src/stream.rs | 6 +- pmoflac/src/wav.rs | 12 +- pmoflac/tests/aiff_decode.rs | 4 +- pmoflac/tests/auto_decode.rs | 47 +++++ pmoflac/tests/flac_streams.rs | 10 +- pmoflac/tests/mp3_decode.rs | 4 +- pmoflac/tests/ogg_decode.rs | 2 +- pmoflac/tests/opus_decode.rs | 4 +- 16 files changed, 427 insertions(+), 71 deletions(-) create mode 100644 pmoflac/src/autodetect.rs create mode 100644 pmoflac/tests/auto_decode.rs diff --git a/.gitignore b/.gitignore index de96f657..0dda1217 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ **/*.aiff **/*.wav **/*.opus +**/*.mp4 xxx /dcai/ **/.pmomusic.yml diff --git a/pmoflac/src/aiff.rs b/pmoflac/src/aiff.rs index fae286c9..cafbd183 100644 --- a/pmoflac/src/aiff.rs +++ b/pmoflac/src/aiff.rs @@ -20,9 +20,7 @@ use tokio::{ use crate::{ common::ChannelReader, - decoder_common::{ - spawn_ingest_task, spawn_writer_task, CHANNEL_CAPACITY, DUPLEX_BUFFER_SIZE, - }, + decoder_common::{spawn_ingest_task, spawn_writer_task, CHANNEL_CAPACITY, DUPLEX_BUFFER_SIZE}, pcm::StreamInfo, stream::ManagedAsyncReader, }; @@ -221,7 +219,9 @@ where } let form_type = <[u8; 4]>::try_from(&form_header[8..12]).unwrap(); if form_type != *b"AIFF" && form_type != *b"AIFC" { - return Err(AiffError::Decode("unsupported FORM type (expected AIFF/AIFC)".into())); + return Err(AiffError::Decode( + "unsupported FORM type (expected AIFF/AIFC)".into(), + )); } let mut comm_chunk: Option = None; @@ -234,7 +234,8 @@ where Err(err) => return Err(err), }; let chunk_id = <[u8; 4]>::try_from(&header[..4]).unwrap(); - let chunk_size = u32::from_be_bytes([header[4], header[5], header[6], header[7]]) as usize; + let chunk_size = + u32::from_be_bytes([header[4], header[5], header[6], header[7]]) as usize; let padded_size = if chunk_size % 2 == 0 { chunk_size @@ -258,7 +259,9 @@ where let compression = if form_type == *b"AIFC" { if data.len() < 22 { - return Err(AiffError::Decode("AIFC COMM chunk missing compression type".into())); + return Err(AiffError::Decode( + "AIFC COMM chunk missing compression type".into(), + )); } match &data[18..22] { b"NONE" => Compression::BigEndianPcm, @@ -290,13 +293,15 @@ where } } b"SSND" => { - let comm = comm_chunk - .as_ref() - .ok_or_else(|| AiffError::Decode("SSND chunk encountered before COMM".into()))?; + let comm = comm_chunk.as_ref().ok_or_else(|| { + AiffError::Decode("SSND chunk encountered before COMM".into()) + })?; let header = aiff_reader.read_exact_vec(8)?; - let offset = u32::from_be_bytes([header[0], header[1], header[2], header[3]]) as usize; - let _block_size = u32::from_be_bytes([header[4], header[5], header[6], header[7]]) as usize; + let offset = + u32::from_be_bytes([header[0], header[1], header[2], header[3]]) as usize; + let _block_size = + u32::from_be_bytes([header[4], header[5], header[6], header[7]]) as usize; if offset > 0 { aiff_reader.skip(offset)?; @@ -368,7 +373,9 @@ where } if !stream_info_sent { - return Err(AiffError::Decode("no SSND chunk found in AIFF stream".into())); + return Err(AiffError::Decode( + "no SSND chunk found in AIFF stream".into(), + )); } Ok(()) @@ -424,9 +431,8 @@ fn convert_be_pcm(mut chunk: Vec, bits_per_sample: u16) -> Result, A 3 => { let mut out = Vec::with_capacity(chunk.len()); for sample in chunk.chunks(3) { - let value = ((sample[0] as i32) << 16) - | ((sample[1] as i32) << 8) - | (sample[2] as i32); + let value = + ((sample[0] as i32) << 16) | ((sample[1] as i32) << 8) | (sample[2] as i32); let value = if value & 0x0080_0000 != 0 { value | !0x00FF_FFFF } else { diff --git a/pmoflac/src/autodetect.rs b/pmoflac/src/autodetect.rs new file mode 100644 index 00000000..8bac1bf6 --- /dev/null +++ b/pmoflac/src/autodetect.rs @@ -0,0 +1,316 @@ +use std::{ + cmp, io, + pin::Pin, + task::{Context, Poll}, +}; + +use tokio::io::{AsyncRead, AsyncReadExt, ReadBuf}; + +use crate::{ + decode_aiff_stream, decode_flac_stream, decode_mp3_stream, decode_ogg_opus_stream, + decode_ogg_vorbis_stream, decode_wav_stream, pcm::StreamInfo, AiffDecodedStream, AiffError, + FlacDecodedStream, FlacError, Mp3DecodedStream, Mp3Error, OggDecodedStream, OggError, + OggOpusDecodedStream, OggOpusError, WavDecodedStream, WavError, +}; + +const MAX_SNIFF_BYTES: usize = 64 * 1024; +const READ_CHUNK: usize = 4096; + +#[derive(thiserror::Error, Debug)] +pub enum DecodeAudioError { + #[error("I/O error: {0}")] + Io(#[from] io::Error), + #[error("unknown or unsupported audio format")] + UnknownFormat, + #[error("FLAC decode error: {0}")] + Flac(#[from] FlacError), + #[error("MP3 decode error: {0}")] + Mp3(#[from] Mp3Error), + #[error("Ogg/Vorbis decode error: {0}")] + Vorbis(#[from] OggError), + #[error("Ogg/Opus decode error: {0}")] + Opus(#[from] OggOpusError), + #[error("WAV decode error: {0}")] + Wav(#[from] WavError), + #[error("AIFF decode error: {0}")] + Aiff(#[from] AiffError), +} + +pub async fn decode_audio_stream(reader: R) -> Result +where + R: AsyncRead + Unpin + Send + 'static, +{ + let mut reader = reader; + let mut initial = Vec::new(); + let mut tmp = vec![0u8; READ_CHUNK]; + let mut detected = detect_format(&initial); + + while detected.is_none() && initial.len() < MAX_SNIFF_BYTES { + let read = reader.read(&mut tmp).await?; + if read == 0 { + break; + } + initial.extend_from_slice(&tmp[..read]); + detected = detect_format(&initial); + } + + let format = detected.ok_or(DecodeAudioError::UnknownFormat)?; + let prefixed = PrefixedReader::new(initial, reader); + + let stream = match format { + DetectedFormat::Flac => { + let stream = decode_flac_stream(prefixed).await?; + DecodedAudioStream::Flac(stream) + } + DetectedFormat::Mp3 => { + let stream = decode_mp3_stream(prefixed).await?; + DecodedAudioStream::Mp3(stream) + } + DetectedFormat::OggVorbis => { + let stream = decode_ogg_vorbis_stream(prefixed).await?; + DecodedAudioStream::OggVorbis(stream) + } + DetectedFormat::OggOpus => { + let stream = decode_ogg_opus_stream(prefixed).await?; + DecodedAudioStream::OggOpus(stream) + } + DetectedFormat::Wav => { + let stream = decode_wav_stream(prefixed).await?; + DecodedAudioStream::Wav(stream) + } + DetectedFormat::Aiff => { + let stream = decode_aiff_stream(prefixed).await?; + DecodedAudioStream::Aiff(stream) + } + }; + + Ok(stream) +} + +pub enum DecodedAudioStream { + Flac(FlacDecodedStream), + Mp3(Mp3DecodedStream), + OggVorbis(OggDecodedStream), + OggOpus(OggOpusDecodedStream), + Wav(WavDecodedStream), + Aiff(AiffDecodedStream), +} + +impl DecodedAudioStream { + pub fn info(&self) -> &StreamInfo { + match self { + DecodedAudioStream::Flac(inner) => inner.info(), + DecodedAudioStream::Mp3(inner) => inner.info(), + DecodedAudioStream::OggVorbis(inner) => inner.info(), + DecodedAudioStream::OggOpus(inner) => inner.info(), + DecodedAudioStream::Wav(inner) => inner.info(), + DecodedAudioStream::Aiff(inner) => inner.info(), + } + } + + pub async fn wait(self) -> Result<(), DecodeAudioError> { + match self { + DecodedAudioStream::Flac(inner) => inner.wait().await.map_err(DecodeAudioError::from), + DecodedAudioStream::Mp3(inner) => inner.wait().await.map_err(DecodeAudioError::from), + DecodedAudioStream::OggVorbis(inner) => { + inner.wait().await.map_err(DecodeAudioError::from) + } + DecodedAudioStream::OggOpus(inner) => { + inner.wait().await.map_err(DecodeAudioError::from) + } + DecodedAudioStream::Wav(inner) => inner.wait().await.map_err(DecodeAudioError::from), + DecodedAudioStream::Aiff(inner) => inner.wait().await.map_err(DecodeAudioError::from), + } + } + + pub fn into_reader(self) -> (StreamInfo, DecodedReader) { + match self { + DecodedAudioStream::Flac(inner) => { + let (info, reader) = inner.into_parts(); + (info, DecodedReader::Flac(reader)) + } + DecodedAudioStream::Mp3(inner) => { + let (info, reader) = inner.into_parts(); + (info, DecodedReader::Mp3(reader)) + } + DecodedAudioStream::OggVorbis(inner) => { + let (info, reader) = inner.into_parts(); + (info, DecodedReader::OggVorbis(reader)) + } + DecodedAudioStream::OggOpus(inner) => { + let (info, reader) = inner.into_parts(); + (info, DecodedReader::OggOpus(reader)) + } + DecodedAudioStream::Wav(inner) => { + let (info, reader) = inner.into_parts(); + (info, DecodedReader::Wav(reader)) + } + DecodedAudioStream::Aiff(inner) => { + let (info, reader) = inner.into_parts(); + (info, DecodedReader::Aiff(reader)) + } + } + } +} + +impl AsyncRead for DecodedAudioStream { + fn poll_read( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + match self.get_mut() { + DecodedAudioStream::Flac(inner) => Pin::new(inner).poll_read(cx, buf), + DecodedAudioStream::Mp3(inner) => Pin::new(inner).poll_read(cx, buf), + DecodedAudioStream::OggVorbis(inner) => Pin::new(inner).poll_read(cx, buf), + DecodedAudioStream::OggOpus(inner) => Pin::new(inner).poll_read(cx, buf), + DecodedAudioStream::Wav(inner) => Pin::new(inner).poll_read(cx, buf), + DecodedAudioStream::Aiff(inner) => Pin::new(inner).poll_read(cx, buf), + } + } +} + +pub enum DecodedReader { + Flac(crate::stream::ManagedAsyncReader), + Mp3(crate::stream::ManagedAsyncReader), + OggVorbis(crate::stream::ManagedAsyncReader), + OggOpus(crate::stream::ManagedAsyncReader), + Wav(crate::stream::ManagedAsyncReader), + Aiff(crate::stream::ManagedAsyncReader), +} + +impl DecodedReader { + pub async fn wait(self) -> Result<(), DecodeAudioError> { + match self { + DecodedReader::Flac(inner) => inner.wait().await.map_err(DecodeAudioError::from), + DecodedReader::Mp3(inner) => inner.wait().await.map_err(DecodeAudioError::from), + DecodedReader::OggVorbis(inner) => inner.wait().await.map_err(DecodeAudioError::from), + DecodedReader::OggOpus(inner) => inner.wait().await.map_err(DecodeAudioError::from), + DecodedReader::Wav(inner) => inner.wait().await.map_err(DecodeAudioError::from), + DecodedReader::Aiff(inner) => inner.wait().await.map_err(DecodeAudioError::from), + } + } +} + +impl AsyncRead for DecodedReader { + fn poll_read( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + match self.get_mut() { + DecodedReader::Flac(inner) => Pin::new(inner).poll_read(cx, buf), + DecodedReader::Mp3(inner) => Pin::new(inner).poll_read(cx, buf), + DecodedReader::OggVorbis(inner) => Pin::new(inner).poll_read(cx, buf), + DecodedReader::OggOpus(inner) => Pin::new(inner).poll_read(cx, buf), + DecodedReader::Wav(inner) => Pin::new(inner).poll_read(cx, buf), + DecodedReader::Aiff(inner) => Pin::new(inner).poll_read(cx, buf), + } + } +} + +fn detect_format(bytes: &[u8]) -> Option { + if bytes.len() >= 4 && &bytes[..4] == b"fLaC" { + return Some(DetectedFormat::Flac); + } + if bytes.len() >= 12 && &bytes[..4] == b"RIFF" && &bytes[8..12] == b"WAVE" { + return Some(DetectedFormat::Wav); + } + if bytes.len() >= 12 + && &bytes[..4] == b"FORM" + && (&bytes[8..12] == b"AIFF" || &bytes[8..12] == b"AIFC") + { + return Some(DetectedFormat::Aiff); + } + if let Some(fmt) = detect_ogg(bytes) { + return Some(fmt); + } + if is_mp3(bytes) { + return Some(DetectedFormat::Mp3); + } + None +} + +fn detect_ogg(bytes: &[u8]) -> Option { + if bytes.len() < 27 || &bytes[..4] != b"OggS" { + return None; + } + let segment_count = bytes[26] as usize; + let header_len = 27 + segment_count; + if bytes.len() < header_len { + return None; + } + let mut packet_len = 0usize; + for lace in &bytes[27..27 + segment_count] { + packet_len += *lace as usize; + if *lace < 255 { + break; + } + } + if bytes.len() < header_len + packet_len { + return None; + } + let packet = &bytes[header_len..header_len + packet_len]; + if packet.starts_with(b"OpusHead") { + Some(DetectedFormat::OggOpus) + } else if packet.starts_with(b"\x01vorbis") { + Some(DetectedFormat::OggVorbis) + } else { + None + } +} + +fn is_mp3(bytes: &[u8]) -> bool { + if bytes.len() >= 3 && &bytes[..3] == b"ID3" { + return true; + } + if bytes.len() >= 2 && bytes[0] == 0xFF && (bytes[1] & 0xE0) == 0xE0 { + return true; + } + false +} + +enum DetectedFormat { + Flac, + Mp3, + OggVorbis, + OggOpus, + Wav, + Aiff, +} + +struct PrefixedReader { + prefix: Vec, + position: usize, + reader: R, +} + +impl PrefixedReader { + fn new(prefix: Vec, reader: R) -> Self { + Self { + prefix, + position: 0, + reader, + } + } +} + +impl AsyncRead for PrefixedReader { + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + if self.position < self.prefix.len() && buf.remaining() > 0 { + let remaining = self.prefix.len() - self.position; + let to_copy = cmp::min(remaining, buf.remaining()); + buf.put_slice(&self.prefix[self.position..self.position + to_copy]); + self.position += to_copy; + return Poll::Ready(Ok(())); + } + Pin::new(&mut self.reader).poll_read(cx, buf) + } +} + +impl Unpin for PrefixedReader {} +unsafe impl Send for PrefixedReader {} diff --git a/pmoflac/src/decoder_common.rs b/pmoflac/src/decoder_common.rs index 886a5960..a228949c 100644 --- a/pmoflac/src/decoder_common.rs +++ b/pmoflac/src/decoder_common.rs @@ -111,11 +111,7 @@ where pcm_writer.shutdown().await.map_err(E::from)?; match blocking_handle.await { Ok(res) => res, - Err(err) => Err(E::from(format!( - "{} task failed: {}", - role, - err - ))), + Err(err) => Err(E::from(format!("{} task failed: {}", role, err))), } }) } diff --git a/pmoflac/src/lib.rs b/pmoflac/src/lib.rs index ede50d7f..8332763b 100644 --- a/pmoflac/src/lib.rs +++ b/pmoflac/src/lib.rs @@ -95,26 +95,28 @@ //! } //! ``` +pub mod aiff; +pub mod autodetect; +mod common; pub mod decoder; +mod decoder_common; pub mod encoder; pub mod error; -mod pcm; -mod stream; -mod util; pub mod mp3; pub mod ogg; pub mod opus; -pub mod aiff; +mod pcm; +mod stream; +mod util; pub mod wav; -mod common; -mod decoder_common; +pub use aiff::{decode_aiff_stream, AiffDecodedStream, AiffError}; +pub use autodetect::{decode_audio_stream, DecodeAudioError, DecodedAudioStream, DecodedReader}; pub use decoder::{decode_flac_stream, FlacDecodedStream}; pub use encoder::{encode_flac_stream, EncoderOptions, FlacEncodedStream}; pub use error::FlacError; pub use mp3::{decode_mp3_stream, Mp3DecodedStream, Mp3Error}; pub use ogg::{decode_ogg_vorbis_stream, OggDecodedStream, OggError}; pub use opus::{decode_ogg_opus_stream, OggOpusDecodedStream, OggOpusError}; -pub use aiff::{decode_aiff_stream, AiffDecodedStream, AiffError}; -pub use wav::{decode_wav_stream, WavDecodedStream, WavError}; pub use pcm::{PcmFormat, StreamInfo}; +pub use wav::{decode_wav_stream, WavDecodedStream, WavError}; diff --git a/pmoflac/src/mp3.rs b/pmoflac/src/mp3.rs index ac50aba1..fb388ce7 100644 --- a/pmoflac/src/mp3.rs +++ b/pmoflac/src/mp3.rs @@ -262,9 +262,7 @@ where let writer_handle = spawn_writer_task(pcm_rx, pcm_writer, blocking_handle, "mp3-decode"); - let info = info_rx - .await - .map_err(|_| Mp3Error::ChannelClosed)??; + let info = info_rx.await.map_err(|_| Mp3Error::ChannelClosed)??; let reader = ManagedAsyncReader::new("mp3-decode-writer", pcm_reader, writer_handle); Ok(Mp3DecodedStream { info, reader }) diff --git a/pmoflac/src/ogg.rs b/pmoflac/src/ogg.rs index e859e4a5..3add0354 100644 --- a/pmoflac/src/ogg.rs +++ b/pmoflac/src/ogg.rs @@ -326,7 +326,8 @@ where } // Pre-allocate for next iteration - pcm_bytes = Vec::with_capacity(info.max_block_size as usize * info.channels as usize * 2); + pcm_bytes = + Vec::with_capacity(info.max_block_size as usize * info.channels as usize * 2); } if !produced_audio { @@ -576,9 +577,9 @@ where let mut offset: usize = 0; for &seg_len in &segment_table { let len = seg_len as usize; - let end = offset.checked_add(len).ok_or_else(|| { - OggError::Decode("segment length overflow".into()) - })?; + let end = offset + .checked_add(len) + .ok_or_else(|| OggError::Decode("segment length overflow".into()))?; if end > data.len() { return Err(OggError::Decode("segment exceeds page data".into())); } @@ -656,4 +657,3 @@ mod crc { ret } } - diff --git a/pmoflac/src/opus.rs b/pmoflac/src/opus.rs index 5e6e8f7a..fa9aad0f 100644 --- a/pmoflac/src/opus.rs +++ b/pmoflac/src/opus.rs @@ -18,9 +18,7 @@ use tokio::{ use crate::{ common::ChannelReader, - decoder_common::{ - spawn_ingest_task, spawn_writer_task, CHANNEL_CAPACITY, DUPLEX_BUFFER_SIZE, - }, + decoder_common::{spawn_ingest_task, spawn_writer_task, CHANNEL_CAPACITY, DUPLEX_BUFFER_SIZE}, pcm::StreamInfo, stream::ManagedAsyncReader, }; @@ -159,12 +157,14 @@ where while let Some(packet) = packet_reader.next_packet()? { if pcm_buffer.len() < MAX_FRAME_SAMPLES * channels { - pcm_buffer - .resize(MAX_FRAME_SAMPLES * channels, 0); + pcm_buffer.resize(MAX_FRAME_SAMPLES * channels, 0); } - let decoded_frames = - decoder.decode(&packet, &mut pcm_buffer[..MAX_FRAME_SAMPLES * channels], false)?; + let decoded_frames = decoder.decode( + &packet, + &mut pcm_buffer[..MAX_FRAME_SAMPLES * channels], + false, + )?; if decoded_frames == 0 { continue; } @@ -240,7 +240,9 @@ impl OpusHead { } let channels = data[9]; if channels == 0 { - return Err(OggOpusError::Decode("Opus channel count must be > 0".into())); + return Err(OggOpusError::Decode( + "Opus channel count must be > 0".into(), + )); } let pre_skip = u16::from_le_bytes([data[10], data[11]]); diff --git a/pmoflac/src/stream.rs b/pmoflac/src/stream.rs index a5f96df2..bc36de45 100644 --- a/pmoflac/src/stream.rs +++ b/pmoflac/src/stream.rs @@ -25,11 +25,7 @@ impl ManagedAsyncReader where E: std::error::Error, { - pub fn new( - role: &'static str, - inner: DuplexStream, - join: JoinHandle>, - ) -> Self { + pub fn new(role: &'static str, inner: DuplexStream, join: JoinHandle>) -> Self { Self { inner: Some(inner), join: Some(join), diff --git a/pmoflac/src/wav.rs b/pmoflac/src/wav.rs index 1394c5b7..f539dd45 100644 --- a/pmoflac/src/wav.rs +++ b/pmoflac/src/wav.rs @@ -18,9 +18,7 @@ use tokio::{ use crate::{ common::ChannelReader, - decoder_common::{ - spawn_ingest_task, spawn_writer_task, CHANNEL_CAPACITY, DUPLEX_BUFFER_SIZE, - }, + decoder_common::{spawn_ingest_task, spawn_writer_task, CHANNEL_CAPACITY, DUPLEX_BUFFER_SIZE}, pcm::StreamInfo, stream::ManagedAsyncReader, }; @@ -109,7 +107,9 @@ where 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())); + return Err(WavError::Decode( + "unexpected EOF while skipping chunk".into(), + )); } self.buffer.clear(); self.buffer.extend_from_slice(&chunk[..read]); @@ -118,7 +118,6 @@ where } Ok(()) } - } /// PCM format metadata extracted from the WAV `fmt ` chunk. @@ -252,8 +251,7 @@ where } 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 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, diff --git a/pmoflac/tests/aiff_decode.rs b/pmoflac/tests/aiff_decode.rs index e00ce3d4..3436f4c9 100644 --- a/pmoflac/tests/aiff_decode.rs +++ b/pmoflac/tests/aiff_decode.rs @@ -1,8 +1,6 @@ use tokio::io::AsyncReadExt; -use pmoflac::{ - decode_aiff_stream, encode_flac_stream, EncoderOptions, PcmFormat, StreamInfo, -}; +use pmoflac::{decode_aiff_stream, encode_flac_stream, EncoderOptions, PcmFormat, StreamInfo}; const TEST_AIFF: &str = "test_data/wood24.aiff"; diff --git a/pmoflac/tests/auto_decode.rs b/pmoflac/tests/auto_decode.rs new file mode 100644 index 00000000..c9281d5a --- /dev/null +++ b/pmoflac/tests/auto_decode.rs @@ -0,0 +1,47 @@ +use tokio::io::AsyncReadExt; + +use pmoflac::{autodetect::decode_audio_stream, encode_flac_stream, EncoderOptions, PcmFormat}; + +const SAMPLE_FILES: &[&str] = &[ + "test_data/1abaa2c7fb4302e20ac570e79857b700.32bits-44.1Khz.flac", + "test_data/file_example_MP3_5MG.mp3", + "test_data/file_example_OOG_5MG.ogg", + "test_data/music_orig.opus", + "test_data/music_orig.wav", + "test_data/wood24.aiff", +]; + +#[tokio::test] +async fn decode_audio_stream_recognises_formats() -> Result<(), Box> { + for path in SAMPLE_FILES { + let file = tokio::fs::File::open(path).await?; + let mut stream = decode_audio_stream(file).await?; + let info = stream.info().clone(); + assert!(info.sample_rate > 0); + assert!(info.channels > 0); + let mut pcm = Vec::new(); + stream.read_to_end(&mut pcm).await?; + assert!(!pcm.is_empty()); + stream.wait().await?; + } + Ok(()) +} + +#[tokio::test] +async fn decode_audio_stream_transcodes_to_flac() -> Result<(), Box> { + let file = tokio::fs::File::open("test_data/file_example_MP3_5MG.mp3").await?; + let stream = decode_audio_stream(file).await?; + let (info, reader) = stream.into_reader(); + let format = PcmFormat { + sample_rate: info.sample_rate, + channels: info.channels, + bits_per_sample: info.bits_per_sample, + }; + + let mut flac_stream = encode_flac_stream(reader, format, EncoderOptions::default()).await?; + let mut flac_data = Vec::new(); + flac_stream.read_to_end(&mut flac_data).await?; + assert!(flac_data.starts_with(b"fLaC")); + flac_stream.wait().await?; + Ok(()) +} diff --git a/pmoflac/tests/flac_streams.rs b/pmoflac/tests/flac_streams.rs index 4c0cde8a..eda5ece3 100644 --- a/pmoflac/tests/flac_streams.rs +++ b/pmoflac/tests/flac_streams.rs @@ -224,7 +224,8 @@ async fn encoder_streams_without_buffering_all_input() -> Result<(), FlacError> let chunks_read_counter = slow_reader.chunks_read(); // Start encoding - let mut encoder_stream = encode_flac_stream(slow_reader, format, EncoderOptions::default()).await?; + let mut encoder_stream = + encode_flac_stream(slow_reader, format, EncoderOptions::default()).await?; // Try to read some FLAC data before all PCM data has been consumed let mut first_chunk = vec![0u8; 4096]; @@ -247,12 +248,11 @@ async fn encoder_streams_without_buffering_all_input() -> Result<(), FlacError> tokio::time::sleep(Duration::from_millis(200)).await; // Check that we've started getting output - let (first_read, mut encoder_stream) = read_handle.await.map_err(|e| { - FlacError::TaskJoin { + let (first_read, mut encoder_stream) = + read_handle.await.map_err(|e| FlacError::TaskJoin { role: "read-test", details: e.to_string(), - } - })??; + })??; assert!(first_read > 0, "Should have received some FLAC data"); assert!( diff --git a/pmoflac/tests/mp3_decode.rs b/pmoflac/tests/mp3_decode.rs index 5524bf4a..8809277f 100644 --- a/pmoflac/tests/mp3_decode.rs +++ b/pmoflac/tests/mp3_decode.rs @@ -1,8 +1,6 @@ use tokio::io::AsyncReadExt; -use pmoflac::{ - decode_mp3_stream, encode_flac_stream, EncoderOptions, PcmFormat, StreamInfo, -}; +use pmoflac::{decode_mp3_stream, encode_flac_stream, EncoderOptions, PcmFormat, StreamInfo}; const TEST_MP3: &str = "test_data/file_example_MP3_5MG.mp3"; diff --git a/pmoflac/tests/ogg_decode.rs b/pmoflac/tests/ogg_decode.rs index 307a254f..d1930769 100644 --- a/pmoflac/tests/ogg_decode.rs +++ b/pmoflac/tests/ogg_decode.rs @@ -256,7 +256,7 @@ async fn ogg_decoder_detects_corrupted_crc() -> Result<(), Box= 2 { break; diff --git a/pmoflac/tests/opus_decode.rs b/pmoflac/tests/opus_decode.rs index d71ece50..5cd34912 100644 --- a/pmoflac/tests/opus_decode.rs +++ b/pmoflac/tests/opus_decode.rs @@ -1,8 +1,6 @@ use tokio::io::AsyncReadExt; -use pmoflac::{ - decode_ogg_opus_stream, encode_flac_stream, EncoderOptions, PcmFormat, StreamInfo, -}; +use pmoflac::{decode_ogg_opus_stream, encode_flac_stream, EncoderOptions, PcmFormat, StreamInfo}; const TEST_OPUS: &str = "test_data/music_orig.opus";