Corrections mineurs sur pmoflac
This commit is contained in:
3
Cargo.lock
generated
3
Cargo.lock
generated
@@ -2734,11 +2734,14 @@ dependencies = [
|
|||||||
"lewton",
|
"lewton",
|
||||||
"libc",
|
"libc",
|
||||||
"libflac-sys",
|
"libflac-sys",
|
||||||
|
"lofty",
|
||||||
"minimp3",
|
"minimp3",
|
||||||
"opus",
|
"opus",
|
||||||
|
"pmometadata",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"thiserror 1.0.69",
|
"thiserror 1.0.69",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -13,13 +13,16 @@ path = "src/lib.rs"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
bytes = "1.6"
|
bytes = "1.6"
|
||||||
claxon = "0.4"
|
claxon = "0.4"
|
||||||
|
lewton = "0.10"
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
libflac-sys = { version = "0.3.3", default-features = false, features = ["build-flac"] }
|
libflac-sys = { version = "0.3.3", default-features = false, features = ["build-flac"] }
|
||||||
|
lofty = "0.22"
|
||||||
|
minimp3 = "0.5"
|
||||||
|
opus = "0.3"
|
||||||
|
pmometadata = { path = "../pmometadata" }
|
||||||
thiserror = "1.0"
|
thiserror = "1.0"
|
||||||
tokio = { version = "1.37", features = ["rt", "rt-multi-thread", "macros", "sync", "io-util", "fs"] }
|
tokio = { version = "1.37", features = ["rt", "rt-multi-thread", "macros", "sync", "io-util", "fs"] }
|
||||||
minimp3 = "0.5"
|
tracing = "0.1"
|
||||||
lewton = "0.10"
|
|
||||||
opus = "0.3"
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "3.10"
|
tempfile = "3.10"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use std::{
|
use std::{
|
||||||
cmp, io,
|
io,
|
||||||
pin::Pin,
|
pin::Pin,
|
||||||
task::{Context, Poll},
|
task::{Context, Poll},
|
||||||
};
|
};
|
||||||
@@ -8,9 +8,9 @@ use tokio::io::{AsyncRead, AsyncReadExt, ReadBuf};
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
decode_aiff_stream, decode_flac_stream, decode_mp3_stream, decode_ogg_opus_stream,
|
decode_aiff_stream, decode_flac_stream, decode_mp3_stream, decode_ogg_opus_stream,
|
||||||
decode_ogg_vorbis_stream, decode_wav_stream, pcm::StreamInfo, AiffDecodedStream, AiffError,
|
decode_ogg_vorbis_stream, decode_wav_stream, pcm::StreamInfo, prefixed_reader::PrefixedReader,
|
||||||
FlacDecodedStream, FlacError, Mp3DecodedStream, Mp3Error, OggDecodedStream, OggError,
|
AiffDecodedStream, AiffError, FlacDecodedStream, FlacError, Mp3DecodedStream, Mp3Error,
|
||||||
OggOpusDecodedStream, OggOpusError, WavDecodedStream, WavError,
|
OggDecodedStream, OggError, OggOpusDecodedStream, OggOpusError, WavDecodedStream, WavError,
|
||||||
};
|
};
|
||||||
|
|
||||||
const MAX_SNIFF_BYTES: usize = 64 * 1024;
|
const MAX_SNIFF_BYTES: usize = 64 * 1024;
|
||||||
@@ -290,39 +290,3 @@ enum DetectedFormat {
|
|||||||
Wav,
|
Wav,
|
||||||
Aiff,
|
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> {}
|
|
||||||
|
|||||||
@@ -102,11 +102,13 @@ pub mod decoder;
|
|||||||
mod decoder_common;
|
mod decoder_common;
|
||||||
pub mod encoder;
|
pub mod encoder;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
|
pub mod metadata;
|
||||||
pub mod mp3;
|
pub mod mp3;
|
||||||
pub mod ogg;
|
pub mod ogg;
|
||||||
mod ogg_common;
|
mod ogg_common;
|
||||||
pub mod opus;
|
pub mod opus;
|
||||||
mod pcm;
|
mod pcm;
|
||||||
|
mod prefixed_reader;
|
||||||
mod stream;
|
mod stream;
|
||||||
pub mod transcode;
|
pub mod transcode;
|
||||||
mod util;
|
mod util;
|
||||||
@@ -117,6 +119,7 @@ pub use autodetect::{decode_audio_stream, DecodeAudioError, DecodedAudioStream,
|
|||||||
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 metadata::AudioFileMetadata;
|
||||||
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};
|
||||||
|
|||||||
297
pmoflac/src/metadata.rs
Normal file
297
pmoflac/src/metadata.rs
Normal file
@@ -0,0 +1,297 @@
|
|||||||
|
//! Audio file metadata extraction using lofty.
|
||||||
|
//!
|
||||||
|
//! This module provides utilities to extract both technical audio properties
|
||||||
|
//! (sample rate, channels, bit depth) and artistic tags (title, artist, album)
|
||||||
|
//! from audio files in various formats.
|
||||||
|
//!
|
||||||
|
//! # Examples
|
||||||
|
//!
|
||||||
|
//! ```no_run
|
||||||
|
//! use pmoflac::AudioFileMetadata;
|
||||||
|
//! use std::path::Path;
|
||||||
|
//!
|
||||||
|
//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
//! let metadata = AudioFileMetadata::from_file(Path::new("audio.flac"))?;
|
||||||
|
//!
|
||||||
|
//! println!("Title: {:?}", metadata.title);
|
||||||
|
//! println!("Artist: {:?}", metadata.artist);
|
||||||
|
//! println!("Sample rate: {:?} Hz", metadata.sample_rate);
|
||||||
|
//! println!("Duration: {:?} seconds", metadata.duration_secs);
|
||||||
|
//! # Ok(())
|
||||||
|
//! # }
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
use lofty::{config::ParseOptions, prelude::*, probe::Probe};
|
||||||
|
use std::{io::Cursor, path::Path};
|
||||||
|
|
||||||
|
/// Comprehensive audio file metadata including both technical properties and artistic tags.
|
||||||
|
///
|
||||||
|
/// This struct is populated from audio file tags using the lofty library,
|
||||||
|
/// which supports FLAC, MP3, Ogg Vorbis, Opus, WAV, AIFF, and other formats.
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct AudioFileMetadata {
|
||||||
|
// Artistic tags
|
||||||
|
/// Track title
|
||||||
|
pub title: Option<String>,
|
||||||
|
/// Artist name
|
||||||
|
pub artist: Option<String>,
|
||||||
|
/// Album name
|
||||||
|
pub album: Option<String>,
|
||||||
|
/// Year of release
|
||||||
|
pub year: Option<u32>,
|
||||||
|
/// Music genre
|
||||||
|
pub genre: Option<String>,
|
||||||
|
/// Track number in the album
|
||||||
|
pub track_number: Option<u32>,
|
||||||
|
/// Total number of tracks in the album
|
||||||
|
pub track_total: Option<u32>,
|
||||||
|
/// Disc number (for multi-disc albums)
|
||||||
|
pub disc_number: Option<u32>,
|
||||||
|
/// Total number of discs
|
||||||
|
pub disc_total: Option<u32>,
|
||||||
|
|
||||||
|
// Technical audio properties
|
||||||
|
/// Duration in seconds
|
||||||
|
pub duration_secs: Option<u64>,
|
||||||
|
/// Sample rate in Hz (e.g., 44100, 48000)
|
||||||
|
pub sample_rate: Option<u32>,
|
||||||
|
/// Number of audio channels (1 = mono, 2 = stereo, etc.)
|
||||||
|
pub channels: Option<u8>,
|
||||||
|
/// Bitrate in bits per second
|
||||||
|
pub bitrate: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AudioFileMetadata {
|
||||||
|
/// Extracts metadata from an audio file on disk.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `path` - Path to the audio file
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// Returns `AudioFileMetadata` on success, or a `lofty::error::LoftyError` if
|
||||||
|
/// the file cannot be read or parsed.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```no_run
|
||||||
|
/// use pmoflac::AudioFileMetadata;
|
||||||
|
/// use std::path::Path;
|
||||||
|
///
|
||||||
|
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
/// let metadata = AudioFileMetadata::from_file(Path::new("song.flac"))?;
|
||||||
|
/// println!("Duration: {:?}s", metadata.duration_secs);
|
||||||
|
/// # Ok(())
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
|
pub fn from_file(path: &Path) -> Result<Self, lofty::error::LoftyError> {
|
||||||
|
tracing::debug!(path = %path.display(), "Extracting metadata from file");
|
||||||
|
|
||||||
|
let tagged_file = Probe::open(path)?.options(ParseOptions::new()).read()?;
|
||||||
|
|
||||||
|
let metadata = Self::from_tagged_file(tagged_file);
|
||||||
|
|
||||||
|
tracing::debug!(
|
||||||
|
path = %path.display(),
|
||||||
|
title = ?metadata.title,
|
||||||
|
artist = ?metadata.artist,
|
||||||
|
duration_secs = ?metadata.duration_secs,
|
||||||
|
"Metadata extracted successfully"
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(metadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extracts metadata from audio file bytes in memory.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `data` - Audio file data as a byte slice
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// Returns `AudioFileMetadata` on success, or a `lofty::error::LoftyError` if
|
||||||
|
/// the data cannot be parsed.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```no_run
|
||||||
|
/// use pmoflac::AudioFileMetadata;
|
||||||
|
///
|
||||||
|
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
/// let audio_bytes: &[u8] = &[/* ... */];
|
||||||
|
/// let metadata = AudioFileMetadata::from_bytes(audio_bytes)?;
|
||||||
|
/// # Ok(())
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
|
pub fn from_bytes(data: &[u8]) -> Result<Self, lofty::error::LoftyError> {
|
||||||
|
tracing::debug!(size = data.len(), "Extracting metadata from bytes");
|
||||||
|
|
||||||
|
let cursor = Cursor::new(data);
|
||||||
|
let tagged_file = Probe::new(cursor)
|
||||||
|
.guess_file_type()?
|
||||||
|
.options(ParseOptions::new())
|
||||||
|
.read()?;
|
||||||
|
|
||||||
|
Ok(Self::from_tagged_file(tagged_file))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Internal helper to extract metadata from a lofty `TaggedFile`.
|
||||||
|
fn from_tagged_file(tagged_file: lofty::file::TaggedFile) -> Self {
|
||||||
|
let properties = tagged_file.properties();
|
||||||
|
|
||||||
|
// Try to get the primary tag, or fall back to the first available tag
|
||||||
|
let tag = tagged_file
|
||||||
|
.primary_tag()
|
||||||
|
.or_else(|| tagged_file.first_tag());
|
||||||
|
|
||||||
|
// Initialize with technical properties
|
||||||
|
let mut metadata = Self {
|
||||||
|
title: None,
|
||||||
|
artist: None,
|
||||||
|
album: None,
|
||||||
|
year: None,
|
||||||
|
genre: None,
|
||||||
|
track_number: None,
|
||||||
|
track_total: None,
|
||||||
|
disc_number: None,
|
||||||
|
disc_total: None,
|
||||||
|
duration_secs: Some(properties.duration().as_secs()),
|
||||||
|
sample_rate: properties.sample_rate(),
|
||||||
|
channels: properties.channels(),
|
||||||
|
bitrate: properties.audio_bitrate(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Extract artistic tags if available
|
||||||
|
if let Some(tag) = tag {
|
||||||
|
metadata.title = tag.title().map(|s| s.to_string());
|
||||||
|
metadata.artist = tag.artist().map(|s| s.to_string());
|
||||||
|
metadata.album = tag.album().map(|s| s.to_string());
|
||||||
|
metadata.year = tag.year();
|
||||||
|
metadata.genre = tag.genre().map(|s| s.to_string());
|
||||||
|
metadata.track_number = tag.track();
|
||||||
|
metadata.track_total = tag.track_total();
|
||||||
|
metadata.disc_number = tag.disk();
|
||||||
|
metadata.disc_total = tag.disk_total();
|
||||||
|
} else {
|
||||||
|
tracing::warn!("No tags found in audio file");
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a formatted duration string in H:MM:SS format (DIDL-Lite compatible).
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use pmoflac::AudioFileMetadata;
|
||||||
|
///
|
||||||
|
/// let mut metadata = AudioFileMetadata {
|
||||||
|
/// duration_secs: Some(3665), // 1 hour, 1 minute, 5 seconds
|
||||||
|
/// title: None,
|
||||||
|
/// artist: None,
|
||||||
|
/// album: None,
|
||||||
|
/// year: None,
|
||||||
|
/// genre: None,
|
||||||
|
/// track_number: None,
|
||||||
|
/// track_total: None,
|
||||||
|
/// disc_number: None,
|
||||||
|
/// disc_total: None,
|
||||||
|
/// sample_rate: None,
|
||||||
|
/// channels: None,
|
||||||
|
/// bitrate: None,
|
||||||
|
/// };
|
||||||
|
///
|
||||||
|
/// assert_eq!(metadata.duration_formatted(), "1:01:05");
|
||||||
|
/// ```
|
||||||
|
pub fn duration_formatted(&self) -> String {
|
||||||
|
match self.duration_secs {
|
||||||
|
Some(secs) => {
|
||||||
|
let hours = secs / 3600;
|
||||||
|
let minutes = (secs % 3600) / 60;
|
||||||
|
let seconds = secs % 60;
|
||||||
|
format!("{}:{:02}:{:02}", hours, minutes, seconds)
|
||||||
|
}
|
||||||
|
None => "0:00:00".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a collection key in the format "artist:album" for grouping tracks.
|
||||||
|
///
|
||||||
|
/// This is useful for organizing tracks into albums in a music library.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use pmoflac::AudioFileMetadata;
|
||||||
|
///
|
||||||
|
/// let metadata = AudioFileMetadata {
|
||||||
|
/// artist: Some("Pink Floyd".to_string()),
|
||||||
|
/// album: Some("The Dark Side of the Moon".to_string()),
|
||||||
|
/// title: None,
|
||||||
|
/// year: None,
|
||||||
|
/// genre: None,
|
||||||
|
/// track_number: None,
|
||||||
|
/// track_total: None,
|
||||||
|
/// disc_number: None,
|
||||||
|
/// disc_total: None,
|
||||||
|
/// duration_secs: None,
|
||||||
|
/// sample_rate: None,
|
||||||
|
/// channels: None,
|
||||||
|
/// bitrate: None,
|
||||||
|
/// };
|
||||||
|
///
|
||||||
|
/// assert_eq!(metadata.collection_key(), "Pink Floyd:The Dark Side of the Moon");
|
||||||
|
/// ```
|
||||||
|
pub fn collection_key(&self) -> String {
|
||||||
|
match (&self.artist, &self.album) {
|
||||||
|
(Some(artist), Some(album)) => format!("{}:{}", artist, album),
|
||||||
|
(Some(artist), None) => artist.clone(),
|
||||||
|
(None, Some(album)) => album.clone(),
|
||||||
|
(None, None) => "Unknown".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_duration_formatted() {
|
||||||
|
let mut meta = AudioFileMetadata::default();
|
||||||
|
|
||||||
|
meta.duration_secs = Some(65);
|
||||||
|
assert_eq!(meta.duration_formatted(), "0:01:05");
|
||||||
|
|
||||||
|
meta.duration_secs = Some(3665);
|
||||||
|
assert_eq!(meta.duration_formatted(), "1:01:05");
|
||||||
|
|
||||||
|
meta.duration_secs = Some(0);
|
||||||
|
assert_eq!(meta.duration_formatted(), "0:00:00");
|
||||||
|
|
||||||
|
meta.duration_secs = None;
|
||||||
|
assert_eq!(meta.duration_formatted(), "0:00:00");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_collection_key() {
|
||||||
|
let mut meta = AudioFileMetadata::default();
|
||||||
|
|
||||||
|
meta.artist = Some("Artist".to_string());
|
||||||
|
meta.album = Some("Album".to_string());
|
||||||
|
assert_eq!(meta.collection_key(), "Artist:Album");
|
||||||
|
|
||||||
|
meta.album = None;
|
||||||
|
assert_eq!(meta.collection_key(), "Artist");
|
||||||
|
|
||||||
|
meta.artist = None;
|
||||||
|
meta.album = Some("Album".to_string());
|
||||||
|
assert_eq!(meta.collection_key(), "Album");
|
||||||
|
|
||||||
|
meta.album = None;
|
||||||
|
assert_eq!(meta.collection_key(), "Unknown");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -66,9 +66,9 @@ impl PcmFormat {
|
|||||||
384000,
|
384000,
|
||||||
];
|
];
|
||||||
if !STANDARD_RATES.contains(&self.sample_rate) {
|
if !STANDARD_RATES.contains(&self.sample_rate) {
|
||||||
eprintln!(
|
tracing::warn!(
|
||||||
"Warning: non-standard sample rate {} Hz (valid but unusual)",
|
sample_rate = %self.sample_rate,
|
||||||
self.sample_rate
|
"non-standard sample rate (valid but unusual)"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,18 +79,18 @@ impl PcmFormat {
|
|||||||
|
|
||||||
// FLAC officially supports 4-32 bits/sample
|
// FLAC officially supports 4-32 bits/sample
|
||||||
if self.bits_per_sample < 4 {
|
if self.bits_per_sample < 4 {
|
||||||
eprintln!(
|
tracing::warn!(
|
||||||
"Warning: bits_per_sample={} is less than 4 (unusual for FLAC)",
|
bits_per_sample = %self.bits_per_sample,
|
||||||
self.bits_per_sample
|
"bits_per_sample is less than 4 (unusual for FLAC)"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Warn about common bit depths
|
// Warn about common bit depths
|
||||||
const COMMON_BIT_DEPTHS: &[u8] = &[8, 16, 24, 32];
|
const COMMON_BIT_DEPTHS: &[u8] = &[8, 16, 24, 32];
|
||||||
if !COMMON_BIT_DEPTHS.contains(&self.bits_per_sample) {
|
if !COMMON_BIT_DEPTHS.contains(&self.bits_per_sample) {
|
||||||
eprintln!(
|
tracing::warn!(
|
||||||
"Warning: non-standard bit depth {} (valid but unusual)",
|
bits_per_sample = %self.bits_per_sample,
|
||||||
self.bits_per_sample
|
"non-standard bit depth (valid but unusual)"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
64
pmoflac/src/prefixed_reader.rs
Normal file
64
pmoflac/src/prefixed_reader.rs
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
//! Utility for reading from a prefix buffer followed by an underlying reader.
|
||||||
|
//!
|
||||||
|
//! This module provides `PrefixedReader`, which allows reading from an in-memory
|
||||||
|
//! buffer first, then seamlessly continuing with an underlying async reader.
|
||||||
|
//!
|
||||||
|
//! This is particularly useful for format detection where we need to peek at the
|
||||||
|
//! beginning of a stream before processing it.
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
cmp, io,
|
||||||
|
pin::Pin,
|
||||||
|
task::{Context, Poll},
|
||||||
|
};
|
||||||
|
|
||||||
|
use tokio::io::{AsyncRead, ReadBuf};
|
||||||
|
|
||||||
|
/// An async reader that reads from a prefix buffer before delegating to an underlying reader.
|
||||||
|
///
|
||||||
|
/// This is useful when you've already consumed some bytes from a stream (e.g., for format
|
||||||
|
/// detection) and need to replay them before continuing with the rest of the stream.
|
||||||
|
pub(crate) struct PrefixedReader<R> {
|
||||||
|
prefix: Vec<u8>,
|
||||||
|
position: usize,
|
||||||
|
reader: R,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R> PrefixedReader<R> {
|
||||||
|
/// Creates a new `PrefixedReader` with the given prefix and underlying reader.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `prefix` - Bytes to read first before delegating to the reader
|
||||||
|
/// * `reader` - The underlying async reader to use after the prefix is exhausted
|
||||||
|
pub 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 we still have bytes in the prefix, read from there first
|
||||||
|
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(()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefix exhausted, delegate to underlying reader
|
||||||
|
Pin::new(&mut self.reader).poll_read(cx, buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R: Unpin> Unpin for PrefixedReader<R> {}
|
||||||
|
unsafe impl<R: Send> Send for PrefixedReader<R> {}
|
||||||
@@ -15,7 +15,8 @@ use tokio::io::{AsyncRead, AsyncReadExt, BufReader, ReadBuf};
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
autodetect::{decode_audio_stream, DecodeAudioError, DecodedAudioStream},
|
autodetect::{decode_audio_stream, DecodeAudioError, DecodedAudioStream},
|
||||||
encode_flac_stream, EncoderOptions, FlacEncodedStream, FlacError, PcmFormat, StreamInfo,
|
encode_flac_stream, prefixed_reader::PrefixedReader, EncoderOptions, FlacEncodedStream,
|
||||||
|
FlacError, PcmFormat, StreamInfo,
|
||||||
};
|
};
|
||||||
|
|
||||||
const READ_CHUNK: usize = 4096;
|
const READ_CHUNK: usize = 4096;
|
||||||
@@ -306,43 +307,6 @@ fn parse_flac_stream_info(block: &[u8]) -> Option<StreamInfo> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
unsafe impl<R: Send> Send for PrefixedReader<R> {}
|
|
||||||
impl<R: Unpin> Unpin for PrefixedReader<R> {}
|
|
||||||
|
|
||||||
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 = remaining.min(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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
Reference in New Issue
Block a user