Skip to content

Commit

Permalink
refactor: introduce AudioFile abstraction for unified stream handling
Browse files Browse the repository at this point in the history
Adds a new AudioFile type that provides:
- Unified interface for encrypted and unencrypted content
- Consistent buffering behavior across all streams
- Thread-safe Read/Seek implementations
- Integration with Symphonia decoder
- Clear documentation of error conditions

This change simplifies the audio pipeline by abstracting stream
handling details away from the player implementation.
  • Loading branch information
roderickvd committed Jan 16, 2025
1 parent a4e317d commit b851e5f
Show file tree
Hide file tree
Showing 7 changed files with 392 additions and 154 deletions.
6 changes: 4 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@ and [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/).
## [Unreleased]

### Added
- [audio_file] Add unified `AudioFile` abstraction for audio stream handling
- [decoder] New Symphonia-based audio decoder for improved performance and quality:
- Higher audio quality (`f32` processing instead of `i16`)
- More robust AAC in ADTS format support
- WAV support for podcasts
- More robust AAC support in both ADTS and MP4 formats
- WAV support (for some podcasts)
- Faster seeking in MP3 files
- Faster decoder initialization
- Lower memory usage
Expand All @@ -21,6 +22,7 @@ and [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/).
- [decrypt] Improve buffer management performance and efficiency
- [docs] Remove incorrect mention of "Hi-Res" audio quality
- [player] Default to mono audio for podcasts to prevent garbled sound when channel count is missing
- [track] Return `AudioFile` instead of raw download stream

## [0.8.1] - 2025-01-11

Expand Down
196 changes: 196 additions & 0 deletions src/audio_file.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
//! Provides the `AudioFile` abstraction for handling audio stream playback.
//!
//! This module implements a unified interface for both encrypted and unencrypted audio files,
//! abstracting away the complexity of handling different stream types while providing
//! necessary traits for media playback.
//!
//! //! # Examples
//!
//! ```no_run
//! use pleezer::audio_file::AudioFile;
//! use std::io::{Read, Seek, SeekFrom};
//!
//! // Create audio file, handling potential errors
//! let mut audio = AudioFile::try_from_download(&track, download)?;
//!
//! // Check if seeking is supported
//! if audio.is_seekable() {
//! audio.seek(SeekFrom::Start(1000))?;
//! }
//!
//! // Read data, handling I/O errors
//! let mut buf = vec![0; 1024];
//! match audio.read(&mut buf) {
//! Ok(n) => println!("Read {n} bytes"),
//! Err(e) => eprintln!("Read error: {e}"),
//! }
//! ```
use std::io::{Read, Seek};

use stream_download::{storage::StorageProvider, StreamDownload};
use symphonia::core::io::MediaSource;

use crate::{decrypt::Decrypt, error::Result, track::Track};

/// Combines Read and Seek traits for audio stream handling.
///
/// This trait requires thread-safety (Send + Sync) to enable:
/// * Concurrent playback and downloading
/// * Safe sharing between threads
/// * Integration with async runtimes
pub trait ReadSeek: Read + Seek + Send + Sync {}

// Blanket implementation for any type that implements both Read and Seek
impl<T: Read + Seek + Send + Sync> ReadSeek for T {}

/// Represents an audio file stream that can be either encrypted or unencrypted.
///
/// `AudioFile` provides a unified interface for handling audio streams, automatically
/// managing encryption/decryption when needed while maintaining direct pass-through
/// for unencrypted content for optimal performance.
pub struct AudioFile {
/// The underlying stream implementation, either a direct stream or a decryptor
inner: Box<dyn ReadSeek>,

/// Indicates if seeking operations are supported (false for livestreams)
is_seekable: bool,

/// The total size of the audio file in bytes, if known
byte_len: Option<u64>,
}

impl AudioFile {
/// Creates a new `AudioFile` from a track and its download stream.
///
/// This method automatically determines whether decryption is needed and sets up
/// the appropriate stream handler:
/// * For encrypted tracks: wraps the download in a `Decrypt` handler
/// * For unencrypted tracks: uses the download stream directly
///
/// # Arguments
///
/// * `track` - The track metadata containing encryption information
/// * `download` - The underlying download stream
///
/// # Type Parameters
///
/// * `P` - The storage provider type implementing `StorageProvider`
///
/// # Returns
///
/// A new `AudioFile` configured for the track
///
/// # Errors
///
/// * `Error::Unimplemented` - Track uses unsupported encryption
/// * `Error::PermissionDenied` - Decryption key not available
/// * `Error::InvalidData` - Failed to create decryptor
/// * Standard I/O errors from stream setup
pub fn try_from_download<P>(track: &Track, download: StreamDownload<P>) -> Result<Self>
where
P: StorageProvider + Sync + 'static,
P::Reader: Sync,
{
let is_seekable = !track.is_livestream();
let byte_len = track.file_size();

let result = if track.is_encrypted() {
let decryptor = Decrypt::new(track, download)?;
Self {
inner: Box::new(decryptor),
is_seekable,
byte_len,
}
} else {
Self {
inner: Box::new(download),
is_seekable,
byte_len,
}
};

Ok(result)
}
}

/// Implements reading from the audio stream.
///
/// This implementation delegates all read operations directly to the underlying stream,
/// whether it's a decrypted stream or raw download stream, providing transparent
/// handling of encrypted and unencrypted content.
///
/// # Arguments
///
/// * `buf` - Buffer to read data into
///
/// # Returns
///
/// Number of bytes read, or 0 at end of stream
///
/// # Errors
///
/// Propagates errors from the underlying stream:
/// * `InvalidInput` - Buffer position invalid
/// * `InvalidData` - Decryption failed
/// * Standard I/O errors
impl Read for AudioFile {
#[inline]
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
self.inner.read(buf)
}
}

/// Implements seeking within the audio stream.
///
/// This implementation delegates all seek operations directly to the underlying stream.
/// Note that seeking may not be available for livestreams, which can be checked via
/// the `is_seekable()` method.
///
/// # Arguments
///
/// * `pos` - Seek position (Start/Current/End)
///
/// # Returns
///
/// New position in the stream
///
/// # Errors
///
/// Propagates errors from the underlying stream:
/// * `InvalidInput` - Invalid seek position
/// * `UnexpectedEof` - Seek beyond end of file
/// * `Unsupported` - Seeking from end with unknown size
impl Seek for AudioFile {
#[inline]
fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
self.inner.seek(pos)
}
}

/// Implements the `MediaSource` trait required by Symphonia for media playback.
///
/// This implementation provides metadata about the stream's capabilities and properties:
/// - Seekability: determined by whether the track is a livestream
/// - Byte length: provided if known from the track metadata
impl MediaSource for AudioFile {
/// Returns whether seeking is supported in this audio stream.
///
/// # Returns
/// * `true` for normal audio files
/// * `false` for livestreams
#[inline]
fn is_seekable(&self) -> bool {
self.is_seekable
}

/// Returns the total size of the audio stream in bytes, if known.
///
/// # Returns
/// * `Some(u64)` - The size in bytes if known
/// * `None` - If the size is unknown (e.g., for livestreams)
#[inline]
fn byte_len(&self) -> Option<u64> {
self.byte_len
}
}
48 changes: 31 additions & 17 deletions src/decoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,27 @@
//!
//! This module provides a decoder that:
//! * Supports multiple formats (AAC/ADTS, FLAC, MP3, MP4, WAV)
//! * Enables efficient seeking
//! * Enables seeking with format-specific handling
//! * Handles both constant and variable bitrate streams
//! * Processes audio in floating point
//!
//! # Format Support
//!
//! Format-specific optimizations:
//! * MP3: Fast seeking for CBR streams using coarse mode
//! * FLAC: Native seeking with frame boundaries
//! * AAC: Proper ADTS frame synchronization
//! * WAV: Direct PCM access
//! Supported formats and characteristics:
//! * AAC: ADTS framing
//! * FLAC: Lossless compression
//! * MP3: Fast coarse seeking for CBR streams
//! * MP4: AAC audio in MP4 container
//! * WAV: Uncompressed PCM
//!
//! # Performance
//!
//! The decoder is optimized for:
//! * Low memory usage (reuses sample buffers)
//! * Fast initialization
//! * Efficient seeking
//! * Optimized CBR MP3 seeking
//! * Robust error recovery
//! * Direct pass-through for unencrypted streams
use std::time::Duration;

Expand All @@ -31,7 +33,7 @@ use symphonia::{
codecs::{CodecRegistry, DecoderOptions},
errors::Error as SymphoniaError,
formats::{FormatOptions, FormatReader, SeekMode, SeekTo},
io::{MediaSource, MediaSourceStream},
io::{MediaSource, MediaSourceStream, MediaSourceStreamOptions},
meta::MetadataOptions,
probe::{Hint, Probe},
},
Expand All @@ -42,6 +44,7 @@ use symphonia::{
};

use crate::{
audio_file::AudioFile,
error::{Error, Result},
player::{SampleFormat, DEFAULT_SAMPLE_RATE},
protocol::Codec,
Expand All @@ -52,19 +55,20 @@ use crate::{
/// Audio decoder supporting multiple formats through Symphonia.
///
/// Features:
/// * Format-specific optimizations
/// * Efficient seeking modes
/// * Multi-format support
/// * Optimized MP3 CBR seeking
/// * Buffer reuse
/// * Error recovery
/// * Transparent handling of encrypted and unencrypted streams
///
/// # Example
/// ```no_run
/// use pleezer::decoder::Decoder;
/// use symphonia::core::io::MediaSourceStream;
/// use pleezer::audio_file::AudioFile;
///
/// let track = /* ... */;
/// let stream = MediaSourceStream::new(/* ... */);
/// let mut decoder = Decoder::new(&track, stream)?;
/// let file = /* AudioFile instance ... */;
/// let mut decoder = Decoder::new(&track, file)?;
///
/// // Seek to 1 minute
/// decoder.try_seek(std::time::Duration::from_secs(60))?;
Expand Down Expand Up @@ -107,12 +111,17 @@ pub struct Decoder {
const MAX_RETRIES: usize = 3;

impl Decoder {
/// Creates a new decoder for the given track and media stream.
/// Creates a new decoder for the given track and audio file.
///
/// Optimizes decoder initialization by:
/// * Using format-specific decoders when codec is known
/// * Selecting appropriate seek mode (coarse for CBR, accurate for VBR)
/// * Enabling coarse seeking for CBR MP3 content
/// * Pre-allocating buffers based on format parameters
/// * Using direct pass-through for unencrypted content
///
/// # Arguments
/// * `track` - Track metadata including codec information
/// * `file` - Unified audio file interface handling encryption transparently
///
/// # Errors
///
Expand All @@ -121,7 +130,9 @@ impl Decoder {
/// * Codec initialization fails
/// * Required track is not found
/// * Stream parameters are invalid
pub fn new(track: &Track, stream: MediaSourceStream) -> Result<Self> {
pub fn new(track: &Track, file: AudioFile) -> Result<Self> {
let stream = MediaSourceStream::new(Box::new(file), MediaSourceStreamOptions::default());

// We know the codec for all tracks except podcasts, so be as specific as possible.
let mut hint = Hint::new();
let mut codecs = CodecRegistry::default();
Expand All @@ -141,7 +152,7 @@ impl Decoder {
probes.register_all::<MpaReader>();
}
Codec::MP4 => {
// MP4 files can contain any type of audio codec, but most likely AAC.
// MP4 files can contain many audio codecs, but most likely AAC.
codecs.register_all::<AacDecoder>();
probes.register_all::<IsoMp4Reader>();
}
Expand Down Expand Up @@ -196,6 +207,9 @@ impl Decoder {
);
let decoder = codecs.make(codec_params, &DecoderOptions::default())?;

// Update the codec parameters with the actual decoder parameters.
let codec_params = decoder.codec_params();

let sample_rate = codec_params.sample_rate.unwrap_or(DEFAULT_SAMPLE_RATE);
let channels = codec_params.channels.map_or_else(
|| track.typ().default_channels(),
Expand Down
Loading

0 comments on commit b851e5f

Please sign in to comment.