Création d'un décodeur générique
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -13,6 +13,7 @@
|
|||||||
**/*.aiff
|
**/*.aiff
|
||||||
**/*.wav
|
**/*.wav
|
||||||
**/*.opus
|
**/*.opus
|
||||||
|
**/*.mp4
|
||||||
xxx
|
xxx
|
||||||
/dcai/
|
/dcai/
|
||||||
**/.pmomusic.yml
|
**/.pmomusic.yml
|
||||||
|
|||||||
@@ -20,9 +20,7 @@ use tokio::{
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
common::ChannelReader,
|
common::ChannelReader,
|
||||||
decoder_common::{
|
decoder_common::{spawn_ingest_task, spawn_writer_task, CHANNEL_CAPACITY, DUPLEX_BUFFER_SIZE},
|
||||||
spawn_ingest_task, spawn_writer_task, CHANNEL_CAPACITY, DUPLEX_BUFFER_SIZE,
|
|
||||||
},
|
|
||||||
pcm::StreamInfo,
|
pcm::StreamInfo,
|
||||||
stream::ManagedAsyncReader,
|
stream::ManagedAsyncReader,
|
||||||
};
|
};
|
||||||
@@ -221,7 +219,9 @@ where
|
|||||||
}
|
}
|
||||||
let form_type = <[u8; 4]>::try_from(&form_header[8..12]).unwrap();
|
let form_type = <[u8; 4]>::try_from(&form_header[8..12]).unwrap();
|
||||||
if form_type != *b"AIFF" && form_type != *b"AIFC" {
|
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<CommChunk> = None;
|
let mut comm_chunk: Option<CommChunk> = None;
|
||||||
@@ -234,7 +234,8 @@ where
|
|||||||
Err(err) => return Err(err),
|
Err(err) => return Err(err),
|
||||||
};
|
};
|
||||||
let chunk_id = <[u8; 4]>::try_from(&header[..4]).unwrap();
|
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 {
|
let padded_size = if chunk_size % 2 == 0 {
|
||||||
chunk_size
|
chunk_size
|
||||||
@@ -258,7 +259,9 @@ where
|
|||||||
|
|
||||||
let compression = if form_type == *b"AIFC" {
|
let compression = if form_type == *b"AIFC" {
|
||||||
if data.len() < 22 {
|
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] {
|
match &data[18..22] {
|
||||||
b"NONE" => Compression::BigEndianPcm,
|
b"NONE" => Compression::BigEndianPcm,
|
||||||
@@ -290,13 +293,15 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
b"SSND" => {
|
b"SSND" => {
|
||||||
let comm = comm_chunk
|
let comm = comm_chunk.as_ref().ok_or_else(|| {
|
||||||
.as_ref()
|
AiffError::Decode("SSND chunk encountered before COMM".into())
|
||||||
.ok_or_else(|| AiffError::Decode("SSND chunk encountered before COMM".into()))?;
|
})?;
|
||||||
|
|
||||||
let header = aiff_reader.read_exact_vec(8)?;
|
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 offset =
|
||||||
let _block_size = u32::from_be_bytes([header[4], header[5], header[6], header[7]]) as usize;
|
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 {
|
if offset > 0 {
|
||||||
aiff_reader.skip(offset)?;
|
aiff_reader.skip(offset)?;
|
||||||
@@ -368,7 +373,9 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !stream_info_sent {
|
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(())
|
Ok(())
|
||||||
@@ -424,9 +431,8 @@ fn convert_be_pcm(mut chunk: Vec<u8>, bits_per_sample: u16) -> Result<Vec<u8>, A
|
|||||||
3 => {
|
3 => {
|
||||||
let mut out = Vec::with_capacity(chunk.len());
|
let mut out = Vec::with_capacity(chunk.len());
|
||||||
for sample in chunk.chunks(3) {
|
for sample in chunk.chunks(3) {
|
||||||
let value = ((sample[0] as i32) << 16)
|
let value =
|
||||||
| ((sample[1] as i32) << 8)
|
((sample[0] as i32) << 16) | ((sample[1] as i32) << 8) | (sample[2] as i32);
|
||||||
| (sample[2] as i32);
|
|
||||||
let value = if value & 0x0080_0000 != 0 {
|
let value = if value & 0x0080_0000 != 0 {
|
||||||
value | !0x00FF_FFFF
|
value | !0x00FF_FFFF
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
316
pmoflac/src/autodetect.rs
Normal file
316
pmoflac/src/autodetect.rs
Normal file
@@ -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<R>(reader: R) -> Result<DecodedAudioStream, DecodeAudioError>
|
||||||
|
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<io::Result<()>> {
|
||||||
|
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<FlacError>),
|
||||||
|
Mp3(crate::stream::ManagedAsyncReader<Mp3Error>),
|
||||||
|
OggVorbis(crate::stream::ManagedAsyncReader<OggError>),
|
||||||
|
OggOpus(crate::stream::ManagedAsyncReader<OggOpusError>),
|
||||||
|
Wav(crate::stream::ManagedAsyncReader<WavError>),
|
||||||
|
Aiff(crate::stream::ManagedAsyncReader<AiffError>),
|
||||||
|
}
|
||||||
|
|
||||||
|
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<io::Result<()>> {
|
||||||
|
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<DetectedFormat> {
|
||||||
|
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<DetectedFormat> {
|
||||||
|
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<R> {
|
||||||
|
prefix: Vec<u8>,
|
||||||
|
position: usize,
|
||||||
|
reader: R,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R> PrefixedReader<R> {
|
||||||
|
fn new(prefix: Vec<u8>, reader: R) -> Self {
|
||||||
|
Self {
|
||||||
|
prefix,
|
||||||
|
position: 0,
|
||||||
|
reader,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R: AsyncRead + Unpin> AsyncRead for PrefixedReader<R> {
|
||||||
|
fn poll_read(
|
||||||
|
mut self: Pin<&mut Self>,
|
||||||
|
cx: &mut Context<'_>,
|
||||||
|
buf: &mut ReadBuf<'_>,
|
||||||
|
) -> Poll<io::Result<()>> {
|
||||||
|
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<R: Unpin> Unpin for PrefixedReader<R> {}
|
||||||
|
unsafe impl<R: Send> Send for PrefixedReader<R> {}
|
||||||
@@ -111,11 +111,7 @@ where
|
|||||||
pcm_writer.shutdown().await.map_err(E::from)?;
|
pcm_writer.shutdown().await.map_err(E::from)?;
|
||||||
match blocking_handle.await {
|
match blocking_handle.await {
|
||||||
Ok(res) => res,
|
Ok(res) => res,
|
||||||
Err(err) => Err(E::from(format!(
|
Err(err) => Err(E::from(format!("{} task failed: {}", role, err))),
|
||||||
"{} task failed: {}",
|
|
||||||
role,
|
|
||||||
err
|
|
||||||
))),
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,26 +95,28 @@
|
|||||||
//! }
|
//! }
|
||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
|
pub mod aiff;
|
||||||
|
pub mod autodetect;
|
||||||
|
mod common;
|
||||||
pub mod decoder;
|
pub mod decoder;
|
||||||
|
mod decoder_common;
|
||||||
pub mod encoder;
|
pub mod encoder;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
mod pcm;
|
|
||||||
mod stream;
|
|
||||||
mod util;
|
|
||||||
pub mod mp3;
|
pub mod mp3;
|
||||||
pub mod ogg;
|
pub mod ogg;
|
||||||
pub mod opus;
|
pub mod opus;
|
||||||
pub mod aiff;
|
mod pcm;
|
||||||
|
mod stream;
|
||||||
|
mod util;
|
||||||
pub mod wav;
|
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 decoder::{decode_flac_stream, FlacDecodedStream};
|
||||||
pub use encoder::{encode_flac_stream, EncoderOptions, FlacEncodedStream};
|
pub use encoder::{encode_flac_stream, EncoderOptions, FlacEncodedStream};
|
||||||
pub use error::FlacError;
|
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 aiff::{decode_aiff_stream, AiffDecodedStream, AiffError};
|
|
||||||
pub use wav::{decode_wav_stream, WavDecodedStream, WavError};
|
|
||||||
pub use pcm::{PcmFormat, StreamInfo};
|
pub use pcm::{PcmFormat, StreamInfo};
|
||||||
|
pub use wav::{decode_wav_stream, WavDecodedStream, WavError};
|
||||||
|
|||||||
@@ -262,9 +262,7 @@ where
|
|||||||
|
|
||||||
let writer_handle = spawn_writer_task(pcm_rx, pcm_writer, blocking_handle, "mp3-decode");
|
let writer_handle = spawn_writer_task(pcm_rx, pcm_writer, blocking_handle, "mp3-decode");
|
||||||
|
|
||||||
let info = info_rx
|
let info = info_rx.await.map_err(|_| Mp3Error::ChannelClosed)??;
|
||||||
.await
|
|
||||||
.map_err(|_| Mp3Error::ChannelClosed)??;
|
|
||||||
let reader = ManagedAsyncReader::new("mp3-decode-writer", pcm_reader, writer_handle);
|
let reader = ManagedAsyncReader::new("mp3-decode-writer", pcm_reader, writer_handle);
|
||||||
|
|
||||||
Ok(Mp3DecodedStream { info, reader })
|
Ok(Mp3DecodedStream { info, reader })
|
||||||
|
|||||||
@@ -326,7 +326,8 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Pre-allocate for next iteration
|
// 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 {
|
if !produced_audio {
|
||||||
@@ -576,9 +577,9 @@ where
|
|||||||
let mut offset: usize = 0;
|
let mut offset: usize = 0;
|
||||||
for &seg_len in &segment_table {
|
for &seg_len in &segment_table {
|
||||||
let len = seg_len as usize;
|
let len = seg_len as usize;
|
||||||
let end = offset.checked_add(len).ok_or_else(|| {
|
let end = offset
|
||||||
OggError::Decode("segment length overflow".into())
|
.checked_add(len)
|
||||||
})?;
|
.ok_or_else(|| OggError::Decode("segment length overflow".into()))?;
|
||||||
if end > data.len() {
|
if end > data.len() {
|
||||||
return Err(OggError::Decode("segment exceeds page data".into()));
|
return Err(OggError::Decode("segment exceeds page data".into()));
|
||||||
}
|
}
|
||||||
@@ -656,4 +657,3 @@ mod crc {
|
|||||||
ret
|
ret
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,9 +18,7 @@ use tokio::{
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
common::ChannelReader,
|
common::ChannelReader,
|
||||||
decoder_common::{
|
decoder_common::{spawn_ingest_task, spawn_writer_task, CHANNEL_CAPACITY, DUPLEX_BUFFER_SIZE},
|
||||||
spawn_ingest_task, spawn_writer_task, CHANNEL_CAPACITY, DUPLEX_BUFFER_SIZE,
|
|
||||||
},
|
|
||||||
pcm::StreamInfo,
|
pcm::StreamInfo,
|
||||||
stream::ManagedAsyncReader,
|
stream::ManagedAsyncReader,
|
||||||
};
|
};
|
||||||
@@ -159,12 +157,14 @@ where
|
|||||||
|
|
||||||
while let Some(packet) = packet_reader.next_packet()? {
|
while let Some(packet) = packet_reader.next_packet()? {
|
||||||
if pcm_buffer.len() < MAX_FRAME_SAMPLES * channels {
|
if pcm_buffer.len() < MAX_FRAME_SAMPLES * channels {
|
||||||
pcm_buffer
|
pcm_buffer.resize(MAX_FRAME_SAMPLES * channels, 0);
|
||||||
.resize(MAX_FRAME_SAMPLES * channels, 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let decoded_frames =
|
let decoded_frames = decoder.decode(
|
||||||
decoder.decode(&packet, &mut pcm_buffer[..MAX_FRAME_SAMPLES * channels], false)?;
|
&packet,
|
||||||
|
&mut pcm_buffer[..MAX_FRAME_SAMPLES * channels],
|
||||||
|
false,
|
||||||
|
)?;
|
||||||
if decoded_frames == 0 {
|
if decoded_frames == 0 {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -240,7 +240,9 @@ impl OpusHead {
|
|||||||
}
|
}
|
||||||
let channels = data[9];
|
let channels = data[9];
|
||||||
if channels == 0 {
|
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]]);
|
let pre_skip = u16::from_le_bytes([data[10], data[11]]);
|
||||||
|
|||||||
@@ -25,11 +25,7 @@ impl<E> ManagedAsyncReader<E>
|
|||||||
where
|
where
|
||||||
E: std::error::Error,
|
E: std::error::Error,
|
||||||
{
|
{
|
||||||
pub fn new(
|
pub fn new(role: &'static str, inner: DuplexStream, join: JoinHandle<Result<(), E>>) -> Self {
|
||||||
role: &'static str,
|
|
||||||
inner: DuplexStream,
|
|
||||||
join: JoinHandle<Result<(), E>>,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
Self {
|
||||||
inner: Some(inner),
|
inner: Some(inner),
|
||||||
join: Some(join),
|
join: Some(join),
|
||||||
|
|||||||
@@ -18,9 +18,7 @@ use tokio::{
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
common::ChannelReader,
|
common::ChannelReader,
|
||||||
decoder_common::{
|
decoder_common::{spawn_ingest_task, spawn_writer_task, CHANNEL_CAPACITY, DUPLEX_BUFFER_SIZE},
|
||||||
spawn_ingest_task, spawn_writer_task, CHANNEL_CAPACITY, DUPLEX_BUFFER_SIZE,
|
|
||||||
},
|
|
||||||
pcm::StreamInfo,
|
pcm::StreamInfo,
|
||||||
stream::ManagedAsyncReader,
|
stream::ManagedAsyncReader,
|
||||||
};
|
};
|
||||||
@@ -109,7 +107,9 @@ where
|
|||||||
let mut chunk = [0u8; 4096];
|
let mut chunk = [0u8; 4096];
|
||||||
let read = self.reader.read(&mut chunk)?;
|
let read = self.reader.read(&mut chunk)?;
|
||||||
if read == 0 {
|
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.clear();
|
||||||
self.buffer.extend_from_slice(&chunk[..read]);
|
self.buffer.extend_from_slice(&chunk[..read]);
|
||||||
@@ -118,7 +118,6 @@ where
|
|||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// PCM format metadata extracted from the WAV `fmt ` chunk.
|
/// 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 audio_format = u16::from_le_bytes([bytes[0], bytes[1]]);
|
||||||
let channels = u16::from_le_bytes([bytes[2], bytes[3]]);
|
let channels = u16::from_le_bytes([bytes[2], bytes[3]]);
|
||||||
let sample_rate =
|
let sample_rate = u32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]);
|
||||||
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 bits_per_sample = u16::from_le_bytes([bytes[14], bytes[15]]);
|
||||||
let fmt = FmtChunk {
|
let fmt = FmtChunk {
|
||||||
audio_format,
|
audio_format,
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
use tokio::io::AsyncReadExt;
|
use tokio::io::AsyncReadExt;
|
||||||
|
|
||||||
use pmoflac::{
|
use pmoflac::{decode_aiff_stream, encode_flac_stream, EncoderOptions, PcmFormat, StreamInfo};
|
||||||
decode_aiff_stream, encode_flac_stream, EncoderOptions, PcmFormat, StreamInfo,
|
|
||||||
};
|
|
||||||
|
|
||||||
const TEST_AIFF: &str = "test_data/wood24.aiff";
|
const TEST_AIFF: &str = "test_data/wood24.aiff";
|
||||||
|
|
||||||
|
|||||||
47
pmoflac/tests/auto_decode.rs
Normal file
47
pmoflac/tests/auto_decode.rs
Normal file
@@ -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<dyn std::error::Error>> {
|
||||||
|
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<dyn std::error::Error>> {
|
||||||
|
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(())
|
||||||
|
}
|
||||||
@@ -224,7 +224,8 @@ async fn encoder_streams_without_buffering_all_input() -> Result<(), FlacError>
|
|||||||
let chunks_read_counter = slow_reader.chunks_read();
|
let chunks_read_counter = slow_reader.chunks_read();
|
||||||
|
|
||||||
// Start encoding
|
// 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
|
// Try to read some FLAC data before all PCM data has been consumed
|
||||||
let mut first_chunk = vec![0u8; 4096];
|
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;
|
tokio::time::sleep(Duration::from_millis(200)).await;
|
||||||
|
|
||||||
// Check that we've started getting output
|
// Check that we've started getting output
|
||||||
let (first_read, mut encoder_stream) = read_handle.await.map_err(|e| {
|
let (first_read, mut encoder_stream) =
|
||||||
FlacError::TaskJoin {
|
read_handle.await.map_err(|e| FlacError::TaskJoin {
|
||||||
role: "read-test",
|
role: "read-test",
|
||||||
details: e.to_string(),
|
details: e.to_string(),
|
||||||
}
|
})??;
|
||||||
})??;
|
|
||||||
|
|
||||||
assert!(first_read > 0, "Should have received some FLAC data");
|
assert!(first_read > 0, "Should have received some FLAC data");
|
||||||
assert!(
|
assert!(
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
use tokio::io::AsyncReadExt;
|
use tokio::io::AsyncReadExt;
|
||||||
|
|
||||||
use pmoflac::{
|
use pmoflac::{decode_mp3_stream, encode_flac_stream, EncoderOptions, PcmFormat, StreamInfo};
|
||||||
decode_mp3_stream, encode_flac_stream, EncoderOptions, PcmFormat, StreamInfo,
|
|
||||||
};
|
|
||||||
|
|
||||||
const TEST_MP3: &str = "test_data/file_example_MP3_5MG.mp3";
|
const TEST_MP3: &str = "test_data/file_example_MP3_5MG.mp3";
|
||||||
|
|
||||||
|
|||||||
@@ -256,7 +256,7 @@ async fn ogg_decoder_detects_corrupted_crc() -> Result<(), Box<dyn std::error::E
|
|||||||
// Find the second "OggS" (skip the first one to corrupt an audio page, not header)
|
// Find the second "OggS" (skip the first one to corrupt an audio page, not header)
|
||||||
let mut oggs_positions = Vec::new();
|
let mut oggs_positions = Vec::new();
|
||||||
for i in 0..bytes.len().saturating_sub(4) {
|
for i in 0..bytes.len().saturating_sub(4) {
|
||||||
if &bytes[i..i+4] == b"OggS" {
|
if &bytes[i..i + 4] == b"OggS" {
|
||||||
oggs_positions.push(i);
|
oggs_positions.push(i);
|
||||||
if oggs_positions.len() >= 2 {
|
if oggs_positions.len() >= 2 {
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
use tokio::io::AsyncReadExt;
|
use tokio::io::AsyncReadExt;
|
||||||
|
|
||||||
use pmoflac::{
|
use pmoflac::{decode_ogg_opus_stream, encode_flac_stream, EncoderOptions, PcmFormat, StreamInfo};
|
||||||
decode_ogg_opus_stream, encode_flac_stream, EncoderOptions, PcmFormat, StreamInfo,
|
|
||||||
};
|
|
||||||
|
|
||||||
const TEST_OPUS: &str = "test_data/music_orig.opus";
|
const TEST_OPUS: &str = "test_data/music_orig.opus";
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user