From 23871482611f07a7088dc8ecc6cb96450c83a96a Mon Sep 17 00:00:00 2001 From: bakgio <76126058+bakgio@users.noreply.github.com> Date: Mon, 30 Mar 2026 19:04:52 +0300 Subject: [PATCH 1/2] feat: add #[non_exhaustive] to public enums and fix doc comments for 0.2.0 Why Prepare the 0.2.0 release with semver-safe public enums and accurate documentation. Non-exhaustive enums allow adding variants in future minor releases without breaking downstream match arms. What - Add #[non_exhaustive] to AudexError, FrameData, KeyType, FLACErrorKind, StandardField, SkipReason, and TrueAudioTags - Fix doc comments across ~50 files: correct spec values (genre counts, sample rates, bit depths, channel counts, byte offsets), fix broken code examples, and clarify method behavior - Bump version to 0.2.0 in Cargo.toml, wasm/Cargo.toml, and README.md - Add cargo-semver-checks CI job - Add CHANGELOG entry for 0.2.0 --- .github/workflows/ci.yml | 13 +++++++ CHANGELOG.md | 6 ++++ Cargo.toml | 2 +- README.md | 8 ++--- src/aac.rs | 8 ++--- src/ac3.rs | 3 +- src/aiff.rs | 2 +- src/asf/file.rs | 32 +++++++++--------- src/asf/mod.rs | 5 +-- src/asf/util.rs | 2 ++ src/constants.rs | 11 +++--- src/diff.rs | 10 ++++-- src/dsdiff.rs | 9 +++-- src/dsf.rs | 2 +- src/easyid3.rs | 7 ++-- src/easymp4.rs | 15 ++++++--- src/file.rs | 11 +++--- src/flac.rs | 11 +++--- src/id3/file.rs | 17 ++++------ src/id3/frames.rs | 17 +++++----- src/id3/id3v1.rs | 5 +-- src/id3/mod.rs | 73 +++++++++++++++++++++------------------- src/id3/specs.rs | 2 +- src/id3/tags.rs | 24 ++++++++----- src/id3/util.rs | 18 +++++----- src/lib.rs | 36 ++++++++++++++------ src/limits.rs | 25 ++++++++------ src/m4a.rs | 2 +- src/mp3/file.rs | 33 +++++++----------- src/mp3/mod.rs | 11 +++--- src/mp3/util.rs | 31 ++++++++++------- src/mp4/atom.rs | 1 + src/mp4/file.rs | 22 ++++++------ src/mp4/mod.rs | 4 +-- src/mp4/util.rs | 4 +-- src/musepack.rs | 11 +++--- src/ogg.rs | 7 ++-- src/oggflac.rs | 10 +++--- src/oggopus.rs | 6 ++-- src/oggspeex.rs | 12 +++---- src/oggtheora.rs | 2 +- src/oggvorbis.rs | 6 ++-- src/optimfrog.rs | 3 +- src/replaygain.rs | 59 ++++++++++++++------------------ src/snapshot.rs | 18 +++++----- src/tagmap/mod.rs | 14 +++++--- src/tagmap/normalize.rs | 1 + src/tags.rs | 5 +-- src/tak.rs | 11 +++--- src/trueaudio.rs | 32 +++++++++++------- src/util.rs | 52 +++++++++++++--------------- src/vorbis.rs | 17 +++++----- src/wave.rs | 6 ++-- src/wavpack.rs | 2 +- wasm/Cargo.toml | 4 +-- wasm/src/audio_file.rs | 3 +- wasm/src/error.rs | 1 + 57 files changed, 423 insertions(+), 341 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c10c5c5..77cc8f8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -101,6 +101,19 @@ jobs: env: RUSTDOCFLAGS: -Dwarnings + semver: + name: Semver Check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - uses: dtolnay/rust-toolchain@efa25f7f19611383d5b0ccf2d1c8914531636bf9 # stable + with: + toolchain: stable + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2.9.1 + - name: Install cargo-semver-checks + run: cargo install cargo-semver-checks --locked + - run: cargo semver-checks -p audex + audit: name: Security Audit runs-on: ubuntu-latest diff --git a/CHANGELOG.md b/CHANGELOG.md index d6d7b1c..921a62b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +# 0.2.0 (March 30, 2026) + +- Added `#[non_exhaustive]` to all library-defined public enums for forward-compatible matching by downstream crates +- Fixed doc comments across multiple modules to accurately reflect API signatures and behavior +- Added semver compatibility checking to CI + # 0.1.0 (March 29, 2026) - Initial crate release diff --git a/Cargo.toml b/Cargo.toml index 8b720d7..693307e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ members = ["wasm"] [package] name = "audex" -version = "0.1.0" +version = "0.2.0" edition = "2024" rust-version = "1.85" authors = ["bakgio"] diff --git a/README.md b/README.md index 9bb4038..e767191 100644 --- a/README.md +++ b/README.md @@ -32,12 +32,12 @@ ```toml [dependencies] -audex = "0.1.0" +audex = "0.2.0" # With optional features: -# audex = { version = "0.1.0", features = ["async"] } -# audex = { version = "0.1.0", features = ["serde"] } -# audex = { version = "0.1.0", features = ["tracing"] } +# audex = { version = "0.2.0", features = ["async"] } +# audex = { version = "0.2.0", features = ["serde"] } +# audex = { version = "0.2.0", features = ["tracing"] } ``` ## Feature Flags diff --git a/src/aac.rs b/src/aac.rs index fc8eadd..732668e 100644 --- a/src/aac.rs +++ b/src/aac.rs @@ -39,8 +39,8 @@ //! //! - **Lossy compression**: Smaller file sizes than lossless formats //! - **Profiles**: LC (Low Complexity), HE-AAC (High Efficiency), HE-AAC v2 -//! - **Sample rates**: 8 kHz to 96 kHz -//! - **Channels**: Mono, stereo, or multichannel (up to 48 channels) +//! - **Sample rates**: 7.35 kHz to 96 kHz +//! - **Channels**: Mono, stereo, or multichannel (up to 8 channels via standard configurations) //! - **Bitrate**: Variable or constant (typically 128-320 kbps for music) //! //! ## Examples @@ -554,9 +554,9 @@ impl ProgramConfigElement { /// /// # Fields /// -/// - **`channels`**: Number of audio channels (1=mono, 2=stereo, up to 48 for multichannel) +/// - **`channels`**: Number of audio channels (1=mono, 2=stereo, up to 8 via standard ADTS configurations) /// - **`length`**: Total duration of the audio file -/// - **`sample_rate`**: Audio sampling rate in Hz (8000 to 96000) +/// - **`sample_rate`**: Audio sampling rate in Hz (7350 to 96000) /// - **`bitrate`**: Average bitrate in bits per second /// - **`stream_type`**: Container format ("ADTS" or "ADIF") /// diff --git a/src/ac3.rs b/src/ac3.rs index ad0aabd..711887b 100644 --- a/src/ac3.rs +++ b/src/ac3.rs @@ -31,7 +31,8 @@ //! - **Compression**: Lossy (improved perceptual coding) //! - **Bitrate**: 32 kbps to 6.144 Mbps //! - **Sample Rates**: 32 kHz, 44.1 kHz, 48 kHz -//! - **Channels**: 1.0 to 7.1 (including height channels) +//! - **Channels**: Core header layouts from 1.0 to 5.1 are parsed; dependent-stream +//! extensions such as full 7.1 layouts are not currently expanded here //! - **File Extension**: `.ec3`, `.eac3` //! - **MIME Type**: `audio/eac3` //! diff --git a/src/aiff.rs b/src/aiff.rs index b00701c..1fce18f 100644 --- a/src/aiff.rs +++ b/src/aiff.rs @@ -53,7 +53,7 @@ use tokio::io::{AsyncSeekExt, AsyncWriteExt}; pub struct AIFFChunk { /// 4-character chunk identifier (e.g., `"COMM"`, `"SSND"`, `"ID3 "`) pub id: String, - /// Size of the chunk as declared in the header (may include padding) + /// Total size of the chunk including the 8-byte header (ID + size fields) pub size: u32, /// Byte offset of the chunk header from the start of the file pub offset: u64, diff --git a/src/asf/file.rs b/src/asf/file.rs index c327a30..d0a7965 100644 --- a/src/asf/file.rs +++ b/src/asf/file.rs @@ -33,11 +33,11 @@ pub struct ASFInfo { pub length: f64, /// Audio sample rate in Hz (from StreamProperties format data) pub sample_rate: u32, - /// Average bitrate in bits per second (from FileProperties) + /// Average bitrate in bits per second (from StreamProperties format data) pub bitrate: u32, /// Number of audio channels (from StreamProperties format data) pub channels: u16, - /// Codec type identifier (e.g. "audio") + /// Codec info string (e.g. "Windows Media Audio 9 Standard") pub codec_type: String, /// Codec name (e.g. "Windows Media Audio V2") pub codec_name: String, @@ -45,7 +45,7 @@ pub struct ASFInfo { pub codec_description: String, /// Maximum instantaneous bitrate in bps (from FileProperties) pub max_bitrate: Option, - /// Preroll time in 100-nanosecond units (subtracted from play_duration) + /// Preroll time in milliseconds (subtracted from play_duration) pub preroll: Option, /// Broadcast/seekable flags from FileProperties pub flags: Option, @@ -355,9 +355,9 @@ impl ASF { /// than 512 MB before any allocation occurs, returning /// `AudexError::InvalidData` instead. /// - /// For very large ASF/WMA files you may need to use - /// [`crate::limits::ParseLimits::permissive()`], but be aware that - /// peak memory usage will be roughly equal to the file size. + /// Files larger than this in-memory writer guard cannot currently be saved + /// through this path. Peak memory usage for eligible files is roughly equal + /// to the file size. pub fn save(&mut self) -> Result<()> { debug_event!("saving ASF attributes"); if let Some(path) = self.filename.clone() { @@ -420,12 +420,11 @@ impl ASF { /// # Memory usage /// /// This method reads the entire file into memory so that the header can be - /// resized in place. Peak memory consumption is approximately **2x the - /// file size** (the read buffer plus the re-serialized output), bounded by + /// resized in place. Peak memory consumption is roughly equal to the file + /// size (the header is re-rendered separately but is small), bounded by /// `crate::limits::MAX_IN_MEMORY_WRITER_FILE`. For large ASF files this can be - /// significant. Prefer the file-path-based [`save`](Self::save) method - /// when working with large files on disk, as it can operate with lower - /// peak memory by truncating or extending the file in place. + /// significant. The file-path-based [`save`](Self::save) method uses + /// the same code path internally. fn save_to_writer_inner( &mut self, writer: &mut dyn ReadWriteSeek, @@ -1437,8 +1436,9 @@ impl FileType for ASF { /// ASF tags are always present in this format. /// - /// This method returns an error since tags cannot be added to a format - /// that inherently always contains tag metadata. + /// The trait-level `add_tags` returns an error since tags already exist. + /// Note: the inherent method `ASF::add_tags()` returns `Ok(())` for API + /// compatibility; this trait method is only reached via `FileType::add_tags(&mut asf)`. /// /// # Errors /// @@ -1451,8 +1451,10 @@ impl FileType for ASF { /// use audex::FileType; /// /// let mut asf = ASF::load("file.wma")?; - /// // Tags are always present, so add_tags() will fail - /// assert!(asf.add_tags().is_err()); + /// // Inherent method returns Ok (tags already present) + /// assert!(asf.add_tags().is_ok()); + /// // Trait method returns Err + /// assert!(FileType::add_tags(&mut asf).is_err()); /// # Ok::<(), audex::AudexError>(()) /// ``` fn add_tags(&mut self) -> Result<()> { diff --git a/src/asf/mod.rs b/src/asf/mod.rs index bd06b3b..dc8bde7 100644 --- a/src/asf/mod.rs +++ b/src/asf/mod.rs @@ -190,8 +190,9 @@ pub const BYTEARRAY: u16 = 0x0001; /// Boolean attribute type (0x0002) /// -/// Represents a true/false value. Stored as a 32-bit value where -/// 0 = false and any non-zero value = true. +/// Represents a true/false value. Stored as a 32-bit value in Extended Content +/// Description context, or a 16-bit value in Metadata/MetadataLibrary context. +/// Exactly 1 = true and all other values (including other non-zero) = false. pub const BOOL: u16 = 0x0002; /// Double Word (32-bit) attribute type (0x0003) diff --git a/src/asf/util.rs b/src/asf/util.rs index 95777a8..09a7651 100644 --- a/src/asf/util.rs +++ b/src/asf/util.rs @@ -10,6 +10,7 @@ use thiserror::Error; /// ASF-specific error types for parsing and validation failures #[derive(Debug, Clone)] +#[non_exhaustive] pub enum ASFError { /// Data within an ASF object is invalid or malformed InvalidData(String), @@ -36,6 +37,7 @@ impl std::error::Error for ASFError {} /// Comprehensive ASF utility error types #[derive(Debug, Error)] +#[non_exhaustive] pub enum ASFUtilError { #[error("Invalid GUID format: {0}")] InvalidGuid(String), diff --git a/src/constants.rs b/src/constants.rs index cb214cc..197e42e 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -1,12 +1,13 @@ //! Constants used across the library, primarily ID3v1 genres //! -//! This module provides the complete ID3v1 genre table standard and utilities +//! This module provides the complete ID3v1 genre table and utilities //! for parsing and converting genre information across different tagging formats. //! //! # ID3v1 Genre System //! -//! ID3v1 uses a single byte (0-255) to represent genres. The standard defines -//! 192 genres (0-191), with some extensions adding more. This module provides +//! ID3v1 uses a single byte (0-255) to represent genres. The original ID3v1 +//! specification defines 80 genres (0-79). Winamp later extended this to +//! 192 genres (0-191), which became the de facto standard. This module provides //! the complete mapping between numeric IDs and genre names. //! //! # Usage Examples @@ -79,12 +80,12 @@ //! //! # Format-Specific Considerations //! -//! - **ID3v1**: Must use numeric genre ID (0-255), stored as single byte +//! - **ID3v1**: Must use numeric genre ID (0-191), stored as single byte (255 = unset) //! - **ID3v2**: Can use numeric ID, text, or hybrid format (e.g., "(17)Rock") //! - **Vorbis/FLAC**: Always freeform text, numeric IDs not used //! - **MP4/M4A**: Typically numeric for standard genres, can use custom text -/// ID3v1 genre list - complete 192-entry table (indices 0-191) +/// ID3v1 genre list - complete 192-entry Winamp-extended table (indices 0-191) pub const GENRES: &[&str] = &[ // 0-9 "Blues", diff --git a/src/diff.rs b/src/diff.rs index e855038..65d0d37 100644 --- a/src/diff.rs +++ b/src/diff.rs @@ -171,7 +171,13 @@ pub struct DiffOptions { /// Strip format-specific prefixes from custom tag keys so that freeform /// tags can be compared across formats. For example, /// `"id3:TXXX:Songwriter"` and `"vorbis:Songwriter"` both become - /// `"Songwriter"`. Default: `false`. + /// `"songwriter"` (lowercased). Default: `false`. + /// + /// **Note:** This option is only consumed by + /// [`diff_normalized_with_options`](crate::diff::diff_normalized_with_options). + /// The standard [`diff_with_options`](crate::diff::diff_with_options) and + /// [`diff_items_with_options`](crate::diff::diff_items_with_options) functions + /// ignore this field. pub normalize_custom_keys: bool, } @@ -803,7 +809,7 @@ fn tag_map_to_items(map: &TagMap) -> Vec<(String, Vec)> { /// /// `"id3:TXXX:Songwriter"`, `"vorbis:Songwriter"`, /// `"mp4:----:com.apple.itunes:Songwriter"`, `"ape:Songwriter"`, and -/// `"asf:Songwriter"` all become `"Songwriter"`. +/// `"asf:Songwriter"` all become `"songwriter"` (lowercased). fn tag_map_to_items_normalized(map: &TagMap) -> Vec<(String, Vec)> { let mut items: Vec<(String, Vec)> = Vec::new(); diff --git a/src/dsdiff.rs b/src/dsdiff.rs index 7a8ec2f..ffaae34 100644 --- a/src/dsdiff.rs +++ b/src/dsdiff.rs @@ -8,11 +8,10 @@ //! File structure: //! - FRM8 chunk (root container) //! - FVER chunk (format version) -//! - PROP chunk (properties container) -//! SND chunk (sound properties) -//! FS chunk (sample rate) -//! CHNL chunk (channel configuration) -//! CMPR chunk (compression type) +//! - PROP chunk (properties container, form type "SND ") +//! - FS chunk (sample rate) +//! - CHNL chunk (channel configuration) +//! - CMPR chunk (compression type) //! - DSD/DST chunk (audio data) use crate::tags::PaddingInfo; diff --git a/src/dsf.rs b/src/dsf.rs index 62a90e1..19d01f5 100644 --- a/src/dsf.rs +++ b/src/dsf.rs @@ -15,7 +15,7 @@ //! //! ## File Structure //! -//! A DSF file consists of three chunks: +//! A DSF file consists of three mandatory chunks and an optional tag: //! - **DSD chunk** (28 bytes): File signature, total size, and metadata offset //! - **fmt chunk**: Audio format details (sample rate, channels, block size) //! - **data chunk**: Interleaved DSD audio samples diff --git a/src/easyid3.rs b/src/easyid3.rs index 2e73fc0..cc6259d 100644 --- a/src/easyid3.rs +++ b/src/easyid3.rs @@ -217,6 +217,7 @@ pub struct EasyID3 { /// Custom error for EasyID3 key operations #[derive(Debug, thiserror::Error)] +#[non_exhaustive] pub enum EasyID3Error { /// The provided key name is not valid or cannot be mapped to an ID3 frame. #[error("Invalid key: {0}")] @@ -499,7 +500,7 @@ impl EasyID3 { /// This method mutates a **global** registry shared by all `EasyID3` /// instances in the process. A registration on one instance immediately /// affects every other instance. If two threads register the same key - /// with different frame IDs, the last writer wins silently. + /// with different frame IDs, the second registration will fail with an error. /// /// For deterministic behavior, register all custom keys once during /// application startup before creating worker threads. @@ -609,8 +610,8 @@ impl EasyID3 { /// # Global State Warning /// /// This method mutates a **global** registry shared by all `EasyID3` - /// instances in the process. See [`register_text_key`](Self::register_text_key) - /// for details on the implications of global-state mutation. + /// instances in the process. Unlike [`register_text_key`](Self::register_text_key), + /// this method silently overwrites any existing registration for the same key. /// /// # Arguments /// * `key` - The easy key name (e.g., "compatible_brands") diff --git a/src/easymp4.rs b/src/easymp4.rs index 5680c1e..2bbdd7c 100644 --- a/src/easymp4.rs +++ b/src/easymp4.rs @@ -193,6 +193,7 @@ use std::path::Path; /// Key mapping types for different kinds of metadata values #[derive(Debug, Clone, PartialEq)] +#[non_exhaustive] pub enum KeyType { /// Text values (strings) Text, @@ -1032,9 +1033,14 @@ impl FileType for EasyMP4 { /// Creates a new empty tag structure if none exists. If tags already exist, /// returns an error. /// + /// Note: the inherent method `EasyMP4::add_tags()` returns + /// `AudexError::ParseError` on failure. This trait method returns + /// `AudexError::InvalidOperation` and is reached via + /// `FileType::add_tags(&mut mp4)`. + /// /// # Errors /// - /// Returns `AudexError::ParseError` if tags already exist. + /// Returns `AudexError::InvalidOperation` if tags already exist. /// /// # Examples /// @@ -1043,10 +1049,9 @@ impl FileType for EasyMP4 { /// use audex::FileType; /// /// let mut mp4 = EasyMP4::load("song.m4a")?; - /// // Add tags if they don't exist - /// let _ = mp4.add_tags(); // Will error if tags exist, which is fine - /// mp4.set("title", vec!["My Song".to_string()])?; - /// mp4.save()?; + /// if mp4.tags().is_none() { + /// mp4.add_tags()?; + /// } /// # Ok::<(), audex::AudexError>(()) /// ``` fn add_tags(&mut self) -> Result<()> { diff --git a/src/file.rs b/src/file.rs index 7aa3c96..405b55c 100644 --- a/src/file.rs +++ b/src/file.rs @@ -66,13 +66,14 @@ //! - MP4/M4A: "ftyp" atom //! - And more... //! -//! 2. **Extension Matching**: Falls back to file extension when headers are ambiguous +//! 2. **Extension Matching**: Adds to the score when the file extension matches a known format //! -//! 3. **Content Analysis**: Some formats require deeper inspection +//! 3. **Content Analysis**: Some formats add further score from deeper inspection //! //! Each format provides a scoring function that returns a confidence level as an `i32`. -//! Higher scores indicate higher confidence. The format with the highest positive -//! score is selected for loading. +//! Higher scores indicate higher confidence. The library evaluates the registered +//! formats, sums their scoring factors, and selects the highest positive score for +//! loading. //! //! ```no_run //! # fn main() -> Result<(), audex::AudexError> { @@ -149,7 +150,7 @@ //! //! ### Dynamic Type (File) - Method-Based Access //! -//! The [`File`] type (a factory struct whose `load()` returns `Result<``DynamicFileType``>`) +//! The [`File`] type (a factory struct whose `load()` returns `Result`) //! provides method-based access with a uniform interface regardless of the underlying format: //! //! ```no_run diff --git a/src/flac.rs b/src/flac.rs index 1d041ba..363f7f6 100644 --- a/src/flac.rs +++ b/src/flac.rs @@ -36,7 +36,7 @@ //! println!("Sample rate: {} Hz", flac.info.sample_rate); //! println!("Bit depth: {} bits", flac.info.bits_per_sample); //! println!("Channels: {}", flac.info.channels); -//! println!("Duration: {:?}", flac.info.total_samples); +//! println!("Total samples: {}", flac.info.total_samples); //! //! // Read Vorbis Comment tags using the Tags trait //! if let Some(ref tags) = flac.tags { @@ -127,7 +127,7 @@ use std::time::Duration; /// /// # Examples /// -/// ``` +/// ```no_run /// use audex::flac::{FLAC, FLACParseOptions}; /// /// // Use strict parsing (less forgiving) @@ -139,7 +139,9 @@ use std::time::Duration; /// ..Default::default() /// }; /// -/// let flac = FLAC::with_options(options); +/// // Load a file with custom options +/// let flac = FLAC::from_file_with_options("audio.flac", options)?; +/// # Ok::<(), audex::AudexError>(()) /// ``` /// /// # Default Behavior @@ -253,6 +255,7 @@ pub struct FLACError { /// issues (invalid headers) to data corruption (oversized blocks) to /// partial successes where some blocks failed but others loaded. #[derive(Debug, Clone, PartialEq)] +#[non_exhaustive] pub enum FLACErrorKind { /// The FLAC file header is invalid or missing. /// @@ -3835,7 +3838,7 @@ pub struct FLACStreamInfo { /// Bits per sample (bit depth) /// - /// Valid range: 4 to 32 bits per sample. + /// Valid range: 1 to 32 bits per sample. /// Common values: 16 (CD quality), 24 (high-resolution), 32 (float or int). pub bits_per_sample: u16, diff --git a/src/id3/file.rs b/src/id3/file.rs index 805d79c..a0deaeb 100644 --- a/src/id3/file.rs +++ b/src/id3/file.rs @@ -58,13 +58,14 @@ pub struct ID3 { pub filename: Option, /// Parsed header from the loaded file _header: Option, - /// ID3 version as (2, major, revision) — e.g. (2, 4, 0) for ID3v2.4 + /// ID3 version as (2, major, revision) for ID3v2 — e.g. (2, 4, 0) for ID3v2.4. + /// Set to (1, 1, 0) when only an ID3v1 tag is present. _version: (u8, u8, u8), /// Raw byte data of frames not recognized by the parser pub unknown_frames: Vec>, /// Padding bytes found after the last frame in the loaded tag _padding: usize, - /// Legacy strict-parsing mode (deprecated, kept for compatibility) + /// Strict-parsing mode (defaults to `true`) pub pedantic: bool, /// Cached text values extracted from frames, enabling `Tags::get` /// to return borrowed `&[String]` slices. Kept in sync with the @@ -142,10 +143,7 @@ impl Metadata for ID3 { } impl ID3 { - /// Create new ID3 instance - /// - /// If any arguments are given, the load is called with them. If no - /// arguments are given then an empty ID3 object is created. + /// Create a new empty ID3 instance pub fn new() -> Self { Self { tags: ID3Tags::new(), @@ -456,8 +454,7 @@ impl ID3 { /// Get module name identifier pub const MODULE: &'static str = "audex.id3"; - /// Get ID3 version - /// Returns ID3 tag version as a tuple (major, minor, revision) + /// Get ID3 version as (2, major, revision) -- e.g. (2, 4, 0) for ID3v2.4 pub fn version(&self) -> (u8, u8, u8) { if let Some(ref header) = self._header { (2, header.major_version, header.revision) @@ -489,7 +486,7 @@ impl ID3 { } } - /// Get total size of ID3 tag including header + /// Get total size of ID3 tag body (excludes the 10-byte header) pub fn size(&self) -> u32 { if let Some(ref header) = self._header { header.size @@ -1311,7 +1308,7 @@ impl ID3 { /// # Arguments /// * `filething` - Optional path to save to (uses stored filename if None) /// * `v1` - ID3v1 save options (REMOVE, UPDATE, or CREATE) - /// * `v2_version` - ID3v2 version (2, 3, or 4) + /// * `v2_version` - ID3v2 version (3 or 4) /// * `v23_sep` - Optional separator for multi-value fields in v2.3 /// /// # Returns diff --git a/src/id3/frames.rs b/src/id3/frames.rs index 1df77cc..e564fe3 100644 --- a/src/id3/frames.rs +++ b/src/id3/frames.rs @@ -2,7 +2,7 @@ //! //! This module contains comprehensive implementations for all ID3v2 frame types, //! providing full compatibility with ID3v2.2, ID3v2.3, and ID3v2.4 standards. -//! Over 185+ frame types are supported, covering everything from basic text +//! Over 160 frame types are supported, covering everything from basic text //! information to complex binary data like pictures and chapter markers. //! //! # Frame Type Categories @@ -687,6 +687,7 @@ impl fmt::Display for ChannelType { /// This enum is used internally by the frame parsing/serialization pipeline /// and by [`FrameRegistry`] to decode raw frame bytes into structured data. #[derive(Debug, Clone)] +#[non_exhaustive] pub enum FrameData { /// Text information frames (T***) Text { @@ -3880,8 +3881,8 @@ pub trait HasEncoding { /// Convert encoding if needed for target ID3 version /// /// This method checks if the current encoding is valid for the target - /// version and converts it if necessary. For ID3v2.3, prefers Latin1 - /// when all text fits, otherwise falls back to UTF-16. + /// version and converts it if necessary. For ID3v2.3, UTF-8 and + /// UTF-16BE are converted to UTF-16 (with BOM). /// /// # Arguments /// * `version` - Target ID3 version as (major, minor) tuple @@ -4237,7 +4238,7 @@ impl Frame for COMM { impl Default for COMM { fn default() -> Self { Self { - encoding: TextEncoding::default(), // Uses Utf8 per specs.rs + encoding: TextEncoding::default(), // Utf16 (see specs.rs #[default]) language: *b"XXX", // Default language code description: String::new(), text: String::new(), @@ -5326,7 +5327,7 @@ impl Frame for APIC { impl Default for APIC { fn default() -> Self { Self { - encoding: TextEncoding::default(), // Uses Utf8 per specs.rs + encoding: TextEncoding::default(), // Utf16 (see specs.rs #[default]) mime: String::new(), type_: PictureType::CoverFront, // Default picture type desc: String::new(), @@ -5470,7 +5471,7 @@ impl Frame for USLT { impl Default for USLT { fn default() -> Self { Self { - encoding: TextEncoding::default(), // Uses Utf8 per specs.rs + encoding: TextEncoding::default(), // Utf16 (see specs.rs #[default]) language: *b"XXX", // Default language code description: String::new(), text: String::new(), @@ -7508,7 +7509,7 @@ impl Default for COMR { pub struct ENCR { /// Owner identifier (URL or email) pub owner: String, - /// Method symbol (must be > 0x80) + /// Method symbol (must be >= 0x80) pub method_symbol: u8, /// Encryption data pub data: Vec, @@ -7580,7 +7581,7 @@ impl Default for ENCR { pub struct GRID { /// Owner identifier (URL or email) pub owner: String, - /// Group symbol (must be > 0x80) + /// Group symbol (must be >= 0x80) pub group_symbol: u8, /// Group data pub data: Vec, diff --git a/src/id3/id3v1.rs b/src/id3/id3v1.rs index d0cf2e5..fed0292 100644 --- a/src/id3/id3v1.rs +++ b/src/id3/id3v1.rs @@ -20,7 +20,8 @@ type ID3v1FindResult = ( /// Parsed ID3v1/v1.1 tag (128 bytes at end of file) /// -/// All string fields are trimmed and null-terminated. Track number is +/// All string fields are parsed from fixed-width, null-padded fields and returned +/// as trimmed Rust strings. Track number is /// `Some` for ID3v1.1 tags and `None` for plain ID3v1 tags. #[derive(Debug, Clone)] pub struct ID3v1Tag { @@ -159,7 +160,7 @@ impl ID3v1Tag { } } - /// Check if tag has any meaningful data + /// Check if tag has any meaningful data (ignores genre field) pub fn is_empty(&self) -> bool { self.title.is_empty() && self.artist.is_empty() diff --git a/src/id3/mod.rs b/src/id3/mod.rs index 0c1fef6..e542058 100644 --- a/src/id3/mod.rs +++ b/src/id3/mod.rs @@ -62,19 +62,21 @@ //! ## Reading Tags //! //! ```no_run -//! use audex::id3::{ID3Tags, load}; +//! use audex::id3::ID3; //! //! // Load ID3 tags from a file -//! let tags = load("song.mp3").unwrap(); +//! let id3 = ID3::load_from_file("song.mp3").unwrap(); //! //! // Get text frames -//! let title_frames = tags.getall("TIT2"); +//! let title_frames = id3.tags.getall("TIT2"); //! if let Some(title) = title_frames.first() { -//! println!("Title: {}", title.description()); +//! if let Some(values) = title.text_values() { +//! println!("Title: {}", values.join("; ")); +//! } //! } //! //! // Get all frames of a certain type -//! for frame in tags.getall("COMM") { +//! for frame in id3.tags.getall("COMM") { //! println!("Comment: {}", frame.description()); //! } //! ``` @@ -151,19 +153,21 @@ //! ## Modifying Existing Tags //! //! ```no_run -//! use audex::id3::load; +//! use audex::id3::ID3; //! //! // Load existing tags -//! let mut tags = load("song.mp3").unwrap(); +//! let mut id3 = ID3::load_from_file("song.mp3").unwrap(); //! //! // Remove all frames of a specific type -//! tags.delall("TIT2"); +//! id3.tags.delall("TIT2"); //! //! // Add new value -//! tags.add_text_frame("TIT2", vec!["New Title".to_string()]).unwrap(); +//! id3.tags +//! .add_text_frame("TIT2", vec!["New Title".to_string()]) +//! .unwrap(); //! //! // Save changes -//! tags.save("song.mp3", 1, 4, None, None).unwrap(); +//! id3.save().unwrap(); //! ``` //! //! # Advanced Features @@ -198,11 +202,12 @@ //! Operations return `Result` for proper error handling: //! //! ```no_run -//! use audex::id3::load; +//! use audex::id3::ID3; //! -//! match load("song.mp3") { -//! Ok(tags) => { +//! match ID3::load_from_file("song.mp3") { +//! Ok(id3) => { //! // Process tags +//! let _ = id3.tags.getall("TIT2"); //! } //! Err(e) => { //! eprintln!("Failed to load tags: {}", e); @@ -569,31 +574,28 @@ pub fn clear>(filething: P) -> Result<()> { ID3Tags::clear_file(filething, true, true) } -/// Load ID3 tags from a file +/// Load ID3 tags from a file. /// -/// Reads and parses ID3v2 tags from the specified audio file. Supports all -/// ID3v2 versions (2.2, 2.3, and 2.4) and automatically detects the version. +/// This convenience function delegates to [`ID3Tags::load`]. At present, that +/// lower-level file-loading path is still stubbed and returns +/// [`AudexError::NotImplementedMethod`](crate::AudexError::NotImplementedMethod). +/// For working file-based loading, use [`ID3::load_from_file`](crate::id3::ID3::load_from_file). /// /// # Parameters /// /// * `filething` - Path to the audio file to read tags from /// -/// # Returns -/// -/// Returns an `ID3Tags` instance containing all parsed frames, or an error -/// if the file cannot be read or contains invalid tag data. -/// /// # Examples /// /// ```no_run -/// use audex::id3::load; -/// -/// // Load tags from file -/// let tags = load("song.mp3").unwrap(); -/// -/// // Access frames -/// for frame in tags.getall("TIT2") { -/// println!("Title: {}", frame.description()); +/// // Preferred approach — use ID3 directly: +/// use audex::id3::ID3; +/// +/// let id3 = ID3::load_from_file("song.mp3").unwrap(); +/// for frame in id3.tags.getall("TIT2") { +/// if let Some(values) = frame.text_values() { +/// println!("Title: {}", values.join("; ")); +/// } /// } /// ``` pub fn load>(filething: P) -> Result { @@ -610,14 +612,14 @@ pub mod util; /// ID3v2 version constants /// /// This module provides constants for the supported ID3v2 versions. -/// Each constant is a tuple of (minor_version, revision) values. +/// Each constant is a tuple of (major_version, revision) values. pub mod version { /// ID3v2.2.0 version identifier /// /// Legacy format with 3-character frame IDs. Limited features but /// maximum compatibility with very old players. /// - /// Format: (minor_version=2, revision=0) + /// Format: (major_version=2, revision=0) pub const ID3V22: (u8, u8) = (2, 0); /// ID3v2.3.0 version identifier @@ -625,7 +627,7 @@ pub mod version { /// Most widely supported ID3v2 version. 4-character frame IDs, /// good balance of features and compatibility. /// - /// Format: (minor_version=3, revision=0) + /// Format: (major_version=3, revision=0) pub const ID3V23: (u8, u8) = (3, 0); /// ID3v2.4.0 version identifier @@ -633,7 +635,7 @@ pub mod version { /// Latest ID3v2 standard with full UTF-8 support, improved date handling, /// and additional features. Used as default for new tags. /// - /// Format: (minor_version=4, revision=0) + /// Format: (major_version=4, revision=0) pub const ID3V24: (u8, u8) = (4, 0); } @@ -645,8 +647,9 @@ pub mod flags { /// Unsynchronization flag (bit 7) /// /// Indicates that unsynchronization has been applied to prevent false - /// MPEG sync signals within the tag data. When set, all 0xFF bytes - /// followed by bytes >= 0xE0 have a 0x00 byte inserted between them. + /// MPEG sync signals within the tag data. When set, 0x00 is inserted + /// after any 0xFF that is followed by a byte >= 0xE0 or == 0x00, and + /// after any trailing 0xFF at the end of the data. pub const UNSYNCHRONIZATION: u8 = 0x80; /// Extended header flag (bit 6) diff --git a/src/id3/specs.rs b/src/id3/specs.rs index bbb6d79..4ae435f 100644 --- a/src/id3/specs.rs +++ b/src/id3/specs.rs @@ -23,7 +23,7 @@ pub struct FrameHeader { pub size: u32, /// Frame flags parsed from the 2-byte flags field pub flags: FrameFlags, - /// ID3v2 version as (major, minor) — e.g. (4, 0) for v2.4 + /// ID3v2 version as (2, major) — e.g. (2, 4) for v2.4, (2, 3) for v2.3 pub version: (u8, u8), /// Whether the global unsynchronization flag is set on the tag header pub global_unsync: bool, diff --git a/src/id3/tags.rs b/src/id3/tags.rs index 82cfbb9..9a3ef81 100644 --- a/src/id3/tags.rs +++ b/src/id3/tags.rs @@ -41,10 +41,10 @@ const _V11: (u8, u8) = (1, 1); /// /// Represents the 10-byte header at the start of every ID3v2 tag. Contains /// the version number, flags (unsynchronization, extended header, experimental, -/// footer), and total tag size. Used during both reading and writing. +/// footer), and tag body size. Used during both reading and writing. #[derive(Debug, Clone)] pub struct ID3Header { - /// Version tuple (major, minor, revision) + /// Version tuple (2, major, revision) -- e.g. (2, 4, 0) for ID3v2.4 pub version: (u8, u8, u8), /// Header flags pub flags: u8, @@ -508,7 +508,7 @@ fn parse_size_as_bpi(bytes: &[u8]) -> Result { /// to preserve unknown frames or write an ID3v1 tag alongside ID3v2. #[derive(Debug, Clone)] pub struct ID3SaveConfig { - /// Target ID3v2 major version (2, 3, or 4) + /// Target ID3v2 major version (3 or 4; only v2.3 and v2.4 are supported for writing) pub v2_version: u8, /// Target ID3v2 minor version (usually 0) pub v2_minor: u8, @@ -516,7 +516,9 @@ pub struct ID3SaveConfig { pub v23_sep: String, /// Multi-value separator byte for ID3v2.3 text frames pub v23_separator: u8, - /// Padding bytes to add after tag data (`None` = no padding) + /// Padding bytes to add after tag data. + /// `None` = no padding, `Some(n)` = add `n` bytes of padding. + /// Default: `Some(1024)`. pub padding: Option, /// Whether to merge compatible duplicate frames during save pub merge_frames: bool, @@ -627,9 +629,13 @@ impl ExtendedID3Header { /// /// Stores ID3v2 frames in a sorted dictionary (`BTreeMap`) keyed by a /// hash-key string derived from the frame ID and disambiguating data -/// (e.g. `"TIT2"`, `"TXXX:BARCODE"`, `"APIC:#0"`). Implements the -/// [`Tags`] trait for unified tag access and the [`Metadata`] trait for -/// file I/O. +/// (e.g. `"TIT2"`, `"TXXX:BARCODE"`, `"APIC:Front Cover"`). Implements the +/// [`Metadata`] trait for file I/O. +/// +/// **Note:** The [`Tags`] trait is implemented, but `Tags::get()` always +/// returns `None` on `ID3Tags` due to lifetime restrictions. Use the +/// [`ID3`](crate::id3::ID3) wrapper for a working `Tags::get()` +/// implementation backed by an internal values cache. /// /// Frames are stored as trait objects (`Box`) to support the /// many different frame types (text, URL, picture, comment, etc.). @@ -653,7 +659,7 @@ pub struct ID3Tags { text_cache: RefCell>>, /// Tag flags from header (unsynchronization, extended header, experimental, footer) pub f_flags: u8, - /// Total tag size (including header) + /// Tag body size (excludes the 10-byte header) pub size: u32, /// Filename the tag was loaded from or will be saved to pub filename: Option, @@ -3308,7 +3314,7 @@ impl ID3Tags { crate::id3::file::clear(filething.as_ref(), clear_v1, clear_v2) } - /// Get the total size of the ID3 tag, including header + /// Get the estimated size of the serialized ID3 tag body (frames + padding), excluding the 10-byte header pub fn size(&self) -> usize { // Estimate size based on current frame data let config = ID3SaveConfig::default(); diff --git a/src/id3/util.rs b/src/id3/util.rs index 5ab7a38..dd78ad4 100644 --- a/src/id3/util.rs +++ b/src/id3/util.rs @@ -222,8 +222,9 @@ impl ID3SaveConfig { /// Unsynchronization is an encoding scheme that ensures ID3v2 tag data /// never contains the byte sequence `0xFF 0xE0`–`0xFF 0xFF`, which could /// be mistaken for an MPEG sync word by naive decoders. It works by -/// inserting a `0x00` byte after every `0xFF` byte during encoding, and -/// removing those inserted bytes during decoding. +/// inserting a `0x00` byte after `0xFF` when the following byte is +/// `>= 0xE0` or `== 0x00`, or when `0xFF` is the last byte. Decoding +/// reverses this by removing the inserted `0x00` bytes. pub struct Unsynch; impl Unsynch { @@ -316,11 +317,10 @@ impl Unsynch { false } - /// Encode data with unsynchronization /// Encode data with unsynchronization. /// - /// Inserts `0x00` after every `0xFF` byte to prevent false MPEG sync - /// detection in legacy players. + /// Inserts `0x00` after `0xFF` when the next byte is `>= 0xE0`, + /// `== 0x00`, or when `0xFF` is at the end of the data. pub fn encode(value: &[u8]) -> Vec { // Split data on 0xFF bytes let fragments: Vec<&[u8]> = value.split(|&b| b == 0xFF).collect(); @@ -667,9 +667,11 @@ impl From for u32 { /// Remove unsynchronization from data. /// -/// Returns an error if the data contains invalid unsynchronization sequences -/// (e.g. 0xFF followed by 0xE0 or higher), rather than silently returning -/// the raw undecoded data. Callers can then decide how to handle the failure. +/// Delegates to [`Unsynch::decode`], which is intentionally lenient: +/// non-conformant sequences (e.g. 0xFF followed by a byte >= 0xE0 without +/// a protection byte) are passed through unchanged rather than rejected. +/// The `Result` return type is kept for API compatibility but the current +/// implementation always returns `Ok`. pub fn remove_unsynchronization(data: &[u8]) -> Result> { Unsynch::decode(data) } diff --git a/src/lib.rs b/src/lib.rs index 55d283d..e3cd36a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -290,13 +290,26 @@ //! # APEv2 and ASF Tag Access //! //! Formats using APEv2 tags (Monkey's Audio, Musepack, WavPack, TAK, -//! OptimFROG) and ASF/WMA files store tag values internally as `APEValue` and -//! `ASFAttribute` types respectively, not as plain strings. Because of this, the -//! unified `Tags::get()` trait method currently returns `None` for these formats. -//! TrueAudio files may use either ID3v1/v2 or APEv2 tags; when APEv2 tags are -//! present, the same limitation applies. +//! OptimFROG) and ASF/WMA files store tag values internally as [`APEValue`](apev2::APEValue) +//! and [`ASFAttribute`](asf::attrs::ASFAttribute) types respectively, not as plain strings. +//! The `get()`, `set()`, `remove()`, and `keys()` methods on the file types +//! handle conversion automatically, so the unified interface works across all +//! formats: //! -//! **To read tags from these formats**, use the native format-specific API: +//! ```no_run +//! use audex::wavpack::WavPack; +//! use audex::FileType; +//! +//! let wv = WavPack::load("song.wv")?; +//! // get() converts APEValue to Vec automatically +//! if let Some(values) = wv.get("Title") { +//! println!("Title: {}", values[0]); +//! } +//! # Ok::<(), audex::AudexError>(()) +//! ``` +//! +//! For direct access to the underlying typed values (e.g., binary data in +//! APEv2 or typed attributes in ASF), use the native format-specific API: //! //! ```no_run //! use audex::wavpack::WavPack; @@ -304,7 +317,7 @@ //! //! let wv = WavPack::load("song.wv")?; //! if let Some(ref tags) = wv.tags { -//! // Use the native APEv2 get() which returns Option<&APEValue> +//! // Native APEv2 get() returns Option<&APEValue> //! if let Some(value) = tags.get("Title") { //! println!("Title: {}", value.as_string().unwrap_or_default()); //! } @@ -312,9 +325,11 @@ //! # Ok::<(), audex::AudexError>(()) //! ``` //! -//! Writing tags through `set()` works correctly for all formats, as the trait -//! implementation converts `Vec` to the appropriate internal type. -//! The `keys()` method also works correctly for all formats. +//! **Note:** The low-level [`Tags`] trait impl on [`APEv2Tags`](apev2::APEv2Tags) +//! and [`ASFTags`](asf::attrs::ASFTags) returns `None` from `get()` because +//! that trait returns `&[String]` which cannot reference the non-string +//! internal storage. Always use the file-level methods (e.g., `WavPack::get()`, +//! `ASF::get()`) for the converting interface. //! //! # Troubleshooting //! @@ -470,6 +485,7 @@ pub mod wavpack; /// This enum covers all error conditions that can occur when loading, parsing, /// or saving audio files and their metadata. #[derive(Error, Debug)] +#[non_exhaustive] pub enum AudexError { /// Standard I/O error (file not found, permission denied, etc.) #[error("IO error: {0}")] diff --git a/src/limits.rs b/src/limits.rs index 694316e..b47deb9 100644 --- a/src/limits.rs +++ b/src/limits.rs @@ -12,18 +12,21 @@ //! //! # Specification Compatibility //! -//! The defaults are chosen to accommodate every format's hard specification -//! ceiling so that valid files are never rejected: +//! The per-format constants and default limits are summarized below. +//! Note that some library ceilings are lower than the format specification +//! allows; use [`ParseLimits::permissive()`] for files near those limits: //! -//! | Format | Spec tag ceiling | Spec image ceiling | -//! |------------|---------------------------|----------------------| -//! | ID3v2 | 256 MB (2²⁸ − 1 synchsafe) | same as tag | -//! | FLAC | ~16 MB (2²⁴ − 1 per block) | ~16 MB (24-bit) | -//! | APEv2 | no hard spec limit | same as item | -//! | Vorbis | 10 MB per comment | same as comment | -//! | MP4 | 256 MB (atom data buffer) | same as atom | -//! | AIFF/WAVE | 256 MB (chunk data) | same as chunk | -//! | ASF | 64-bit object sizes | same as object | +//! | Format | Effective parse ceiling | Spec ceiling | +//! |------------|----------------------------------|--------------------------| +//! | ID3v2 | 256 MB (2²⁸ − 1 synchsafe) | 256 MB (synchsafe) | +//! | FLAC | ~16 MB (2²⁴ − 1 per block) | ~16 MB (24-bit) | +//! | APEv2 | no hard limit | no hard spec limit | +//! | Vorbis | 10 MB per comment | ~4 GB (32-bit length) | +//! | MP4 | 256 MB (atom data buffer) | ~unlimited (64-bit ext.) | +//! | AIFF/WAVE | 256 MB (chunk data) | ~4 GB (32-bit size) | +//! +//! Separately, ASF saving uses a whole-file in-memory rewrite path guarded by +//! [`MAX_IN_MEMORY_WRITER_FILE`], currently 512 MiB. //! //! The default limits (8 MB tags / 16 MB images) are safe for untrusted //! input. If you need to accept spec-legal files with very large metadata, diff --git a/src/m4a.rs b/src/m4a.rs index fd5a4c7..40c29fd 100644 --- a/src/m4a.rs +++ b/src/m4a.rs @@ -5,7 +5,7 @@ //! //! # M4A vs MP4 //! -//! M4A is simply a renamed MP4 container specifically for audio files (no video track). +//! M4A is a naming convention for MP4 containers that are typically used for audio files. //! Internally, this module uses the exact same implementation as the [`mp4`](crate::mp4) module. //! The file format, structure, and metadata handling are identical. //! diff --git a/src/mp3/file.rs b/src/mp3/file.rs index 488febf..4ddf5f5 100644 --- a/src/mp3/file.rs +++ b/src/mp3/file.rs @@ -54,7 +54,7 @@ use tokio::io::{AsyncReadExt, AsyncSeekExt}; /// /// // Read ID3 tags if present /// if let Some(ref tags) = mp3.tags { -/// if let Some(title) = tags.get("TIT2") { +/// if let Some(title) = tags.get_text_values("TIT2") { /// println!("Title: {:?}", title); /// } /// } @@ -119,20 +119,6 @@ pub struct MP3 { } impl MP3 { - /// Creates a new empty MP3 instance with default values. - /// - /// This creates an MP3 struct with default audio information and no tags. - /// Typically you would use [`MP3::from_file`] or [`MP3::load`](FileType::load) instead. - /// - /// # Returns - /// - /// Returns an MP3 instance with: - /// - Default (zero) audio information - /// - No tags - /// - No filename - /// - /// # Examples - /// /// Extract ReplayGain value from an RVA2 frame. /// Returns the gain (plain number) or peak as a string. fn get_rva2_replaygain(&self, track_type: &str, is_gain: bool) -> Option> { @@ -164,6 +150,11 @@ impl MP3 { None } + /// Creates a new empty MP3 instance with default values. + /// + /// This creates an MP3 struct with default audio information and no tags. + /// Typically you would use [`MP3::from_file`] or [`MP3::load`](FileType::load) instead. + /// /// ``` /// use audex::mp3::MP3; /// @@ -200,7 +191,9 @@ impl MP3 { /// - The file is not a valid MP3 file (no MPEG frame sync found) /// - The MPEG headers are corrupted or invalid /// - /// Note: Missing ID3 tags is not considered an error; the `tags` field will be `None`. + /// Note: missing ID3 tags are not considered an error; the `tags` field will be `None`. + /// The current implementation also treats ID3 parsing failures as `None` rather than + /// surfacing them separately. /// /// # Examples /// @@ -216,7 +209,7 @@ impl MP3 { /// /// // Access tags via the Tags trait /// if let Some(tags) = &mp3.tags { - /// if let Some(title) = tags.get("TIT2") { + /// if let Some(title) = tags.get_text_values("TIT2") { /// println!("Title: {:?}", title); /// } /// } @@ -260,7 +253,7 @@ impl MP3 { /// /// This method writes any changes made to the tags back to the file. Only the tags /// are modified; the audio data remains unchanged. By default, this: - /// - Creates ID3v2.3 tags if they don't exist + /// - Saves the current ID3 tags if `self.tags` is present /// - Updates existing ID3v1 tags or creates them if tags are present /// - Preserves the original file's audio data /// @@ -322,7 +315,7 @@ impl MP3 { /// * `v2_version` - Target ID3v2 version: /// - `3` = ID3v2.3 (default, most compatible) /// - `4` = ID3v2.4 (newer features, less compatible) - /// * `v23_sep` - Separator for multi-value text frames in ID3v2.3 (default: null byte) + /// * `v23_sep` - Separator for multi-value text frames in ID3v2.3 (default: "/") /// /// # Returns /// @@ -968,7 +961,7 @@ impl FileType for MP3 { // Check specific sync patterns that specification looks for if header.starts_with(&[0xFF, 0xF2]) || // MPEG-2 Layer III header.starts_with(&[0xFF, 0xF3]) || // MPEG-2 Layer III - header.starts_with(&[0xFF, 0xFA]) || // MPEG-2.5 Layer III + header.starts_with(&[0xFF, 0xFA]) || // MPEG-1 Layer III header.starts_with(&[0xFF, 0xFB]) { // MPEG-1 Layer III diff --git a/src/mp3/mod.rs b/src/mp3/mod.rs index 5159af9..95d2c4f 100644 --- a/src/mp3/mod.rs +++ b/src/mp3/mod.rs @@ -43,7 +43,7 @@ //! //! // Read ID3 tags using the Tags trait //! if let Some(ref tags) = mp3.tags { -//! if let Some(title) = tags.get("TIT2") { +//! if let Some(title) = tags.get_text_values("TIT2") { //! println!("Title: {:?}", title); //! } //! } @@ -400,8 +400,8 @@ pub enum MPEGVersion { pub enum MPEGLayer { /// MPEG Layer I /// - /// The simplest MPEG audio layer. Uses 384 samples per frame for MPEG-1 - /// (192 for MPEG-2). Provides the lowest compression ratio but fastest + /// The simplest MPEG audio layer. Uses 384 samples per frame for all + /// MPEG versions. Provides the lowest compression ratio but fastest /// encoding/decoding. Rarely used in practice. Layer1, @@ -415,8 +415,9 @@ pub enum MPEGLayer { /// MPEG Layer III (MP3) /// /// The most complex and widely-used MPEG audio layer. Uses 1152 samples - /// per frame. Provides the best compression ratio through advanced - /// psychoacoustic modeling and Huffman coding. This is the "MP3" format. + /// per frame for MPEG-1 and 576 for MPEG-2/2.5. Provides the best + /// compression ratio through advanced psychoacoustic modeling and + /// Huffman coding. This is the "MP3" format. Layer3, } diff --git a/src/mp3/util.rs b/src/mp3/util.rs index 2de767c..ca89bfd 100644 --- a/src/mp3/util.rs +++ b/src/mp3/util.rs @@ -477,19 +477,23 @@ pub struct LAMEHeader { /// was removed during encoding to improve compression. pub lowpass_filter: i32, - /// Encoding quality setting (0-9) + /// Encoding quality setting (0-9, or -1 if unknown) /// /// - `0`: Highest quality (slowest encoding) /// - `9`: Lowest quality (fastest encoding) + /// - `-1`: Unknown / not set (only from the `new()` code path; + /// `from_bytes()` produces `0` when the VBR scale is unset) /// /// LAME's `-q` parameter. pub quality: i32, - /// VBR quality setting (0-9) + /// VBR quality setting (0-10, or -1 if unknown) /// /// For VBR modes, this indicates the target quality level: /// - `0`: Highest quality (~245 kbps average) - /// - `9`: Lowest quality (~65 kbps average) + /// - `9`-`10`: Lowest quality (~65 kbps average) + /// - `-1`: Unknown / not set (only from the `new()` code path; + /// `from_bytes()` produces `0` when the VBR scale is unset) /// /// LAME's `-V` parameter. pub vbr_quality: i32, @@ -576,7 +580,7 @@ pub struct LAMEHeader { /// Indicates which noise shaping algorithm was used (0-3 in LAME). pub noise_shaping: i32, - /// MP3 gain adjustment (-127 to 127) + /// MP3 gain adjustment (-128 to 127) /// /// Global gain adjustment applied to all audio. The actual gain factor /// is calculated as `2^(mp3_gain / 4)`. @@ -631,7 +635,9 @@ impl LAMEHeader { /// /// NOTE: This method and `from_bytes()` both parse the same LAME header /// structure using different approaches (BitReader vs Cursor). Changes to - /// one must be mirrored in the other to keep them consistent. + /// one should be mirrored in the other. Caveat: when `vbr_scale < 0`, + /// `new()` keeps the default quality/vbr_quality of -1, while `from_bytes()` + /// sets them to 0. pub fn new(xing: &XingHeader, data: &[u8]) -> std::result::Result { if data.len() < 27 { return Err(LAMEError::new("Not enough data")); @@ -879,8 +885,9 @@ impl LAMEHeader { /// Parse extended LAME header. /// /// NOTE: This method and `new()` both parse the same LAME header structure - /// using different approaches (Cursor vs BitReader). Changes to one must - /// be mirrored in the other to keep them consistent. + /// using different approaches (Cursor vs BitReader). Changes to one should + /// be mirrored in the other. Caveat: when `vbr_scale < 0`, `from_bytes()` + /// sets quality/vbr_quality to 0, while `new()` keeps the default of -1. pub fn from_bytes(data: &[u8], xing: &XingHeader) -> Result { // Skip the 20-byte version string to get to the extended data if data.len() < 20 + 27 { @@ -1382,7 +1389,7 @@ impl XingHeader { /// /// # Differences from Xing/Info /// -/// - **Location**: VBRI headers appear at a fixed offset (32 bytes) after the +/// - **Location**: VBRI headers appear at a fixed offset (36 bytes) after the /// first MPEG sync, while Xing headers appear at variable offsets. /// - **Encoder**: Used by Fraunhofer encoder; LAME uses Xing/Info headers. /// - **TOC Format**: VBRI uses a different table of contents structure with @@ -1416,10 +1423,10 @@ pub struct VBRIHeader { /// Currently only version 1 is defined and supported. pub version: i32, - /// Audio quality indicator (0-100) + /// Audio quality indicator (0-65535) /// /// Higher values indicate higher quality encoding. Interpretation - /// is encoder-specific. + /// is encoder-specific. Stored as a 16-bit unsigned integer. pub quality: i32, /// Total file size in bytes (excluding ID3 tags) @@ -2289,8 +2296,8 @@ const MIN_FRAME_SIZE: u32 = 24; /// Calculate frame size in bytes - matches specification implementation. /// -/// Returns 0 if `sample_rate` is 0, since no valid frame can exist without -/// a defined sample rate. +/// Returns `MIN_FRAME_SIZE` (24) if `sample_rate` is 0, since no valid frame +/// can exist without a defined sample rate. pub fn calculate_frame_size( version: MPEGVersion, layer: MPEGLayer, diff --git a/src/mp4/atom.rs b/src/mp4/atom.rs index a2df6c5..6aeac06 100644 --- a/src/mp4/atom.rs +++ b/src/mp4/atom.rs @@ -153,6 +153,7 @@ const SKIP_SIZE: &[(&[u8; 4], usize)] = &[(b"meta", 4)]; /// assert!(!AtomType::Mdat.is_container()); /// ``` #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[non_exhaustive] pub enum AtomType { Unknown([u8; 4]), // File structure diff --git a/src/mp4/file.rs b/src/mp4/file.rs index c66fee5..59a9393 100644 --- a/src/mp4/file.rs +++ b/src/mp4/file.rs @@ -72,8 +72,8 @@ pub enum AtomDataType { /// Shift-JIS encoded text. /// - /// Legacy Japanese character encoding. Deprecated except for special - /// Japanese characters not representable in UTF-8. + /// Legacy Japanese character encoding. Retained only for backward + /// compatibility with older Japanese-market files. Sjis = 3, /// HTML formatted text. @@ -122,7 +122,7 @@ pub enum AtomDataType { /// Duration in milliseconds. /// - /// Stored as a 32-bit integer. + /// Can be 32-bit or 64-bit integer. Duration = 16, /// Date and time in UTC. @@ -455,7 +455,7 @@ impl std::ops::Deref for MP4FreeForm { /// /// # Fields /// -/// - **`start`**: Chapter start position in seconds from the beginning of the file +/// - **`start`**: Chapter start position in seconds from the beginning of playback /// - **`title`**: Human-readable chapter name or description /// /// # Examples @@ -508,12 +508,13 @@ impl Chapter { /// # Structure /// /// - **`chapters`**: Ordered list of chapter markers with start times and titles -/// - **`timescale`**: Optional timescale value from the movie header (used for time calculations) +/// - **`timescale`**: Optional timescale value from the movie header /// - **`duration`**: Optional total duration from the movie header /// /// # Atom Location /// /// Chapters are stored in the `moov.udta.chpl` atom path within the MP4 container. +/// The `moov.mvhd` atom is also required for loading chapters. /// /// # Examples /// @@ -847,7 +848,7 @@ impl MP4Chapters { /// # Structure /// /// - **`tags`**: Standard iTunes metadata fields (©nam, ©ART, ©alb, etc.) -/// - **`covers`**: Embedded cover artwork images (JPEG or PNG) +/// - **`covers`**: Embedded cover artwork images (for example JPEG, PNG, GIF, or BMP) /// - **`freeforms`**: Custom freeform metadata for non-standard fields /// - **`failed_atoms`**: Raw data from atoms that couldn't be parsed /// @@ -966,7 +967,7 @@ pub struct MP4Tags { } impl MP4Tags { - /// Create a new empty MP4Tags with Audex vendor tag + /// Create a new empty MP4Tags pub fn new() -> Self { Self::default() } @@ -1632,8 +1633,8 @@ impl MP4Tags { /// /// This method reads the entire file into an in-memory `Vec`, then /// performs atom rewriting on a `Cursor` over that buffer. Peak memory - /// consumption is approximately **2x the file size** (original buffer - /// plus the modified copy). A size guard rejects files larger than + /// consumption is roughly one whole-file buffer plus temporary rewrite + /// allocations, not two independent full-file copies. A size guard rejects files larger than /// `crate::limits::MAX_IN_MEMORY_WRITER_FILE` before allocating. For large MP4 /// files, prefer the file-path-based [`save`](Self::save) method which /// operates directly on the file handle and avoids buffering the entire @@ -3454,7 +3455,6 @@ impl crate::tags::MetadataFields for MP4Tags { /// /// - **mp4a**: AAC (Advanced Audio Coding) - Most common /// - **alac**: Apple Lossless (ALAC) -/// - **mp4v**: MPEG-4 Visual (video, not audio) /// - **ac-3**: Dolby Digital (AC-3) #[derive(Debug, Clone, Default, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] @@ -4004,7 +4004,7 @@ impl FileType for MP4 { /// /// # Errors /// - /// Returns `AudexError::ParseError` if tags already exist. + /// Returns `AudexError::InvalidOperation` if tags already exist. /// /// # Examples /// diff --git a/src/mp4/mod.rs b/src/mp4/mod.rs index e557ea2..51cf736 100644 --- a/src/mp4/mod.rs +++ b/src/mp4/mod.rs @@ -10,7 +10,7 @@ //! - **Audio codecs**: AAC, ALAC (Apple Lossless), and other MPEG-4 audio formats //! - **Metadata**: iTunes-style tags using the `©` atoms and custom metadata //! - **Chapters**: Chapter markers for audiobooks and podcasts -//! - **Artwork**: Embedded cover art (PNG, JPEG) +//! - **Artwork**: Embedded cover art (commonly PNG/JPEG, with GIF/BMP support as well) //! - **Atom structure**: Full atom tree parsing and manipulation //! //! ## File Extensions @@ -41,7 +41,7 @@ //! // Load an M4A file //! let mut mp4 = MP4::load("song.m4a").unwrap(); //! -//! // Access audio information (fields are Option types) +//! // Access audio information (most fields are Option types; codec/codec_description are String) //! if let Some(duration) = mp4.info.length { //! println!("Duration: {:.2} seconds", duration.as_secs_f64()); //! } diff --git a/src/mp4/util.rs b/src/mp4/util.rs index cb6b0cc..4d7ddd4 100644 --- a/src/mp4/util.rs +++ b/src/mp4/util.rs @@ -68,9 +68,7 @@ pub fn clear>(path: P) -> Result<()> { /// Remove tags from a file asynchronously /// -/// Clear metadata from MP4 file using async I/O operations. -/// Wraps the synchronous clear operation in a blocking task -/// to prevent blocking the async runtime. +/// Clear metadata from MP4 file using native async I/O operations. #[cfg(feature = "async")] pub async fn clear_async>(path: P) -> Result<()> { use crate::mp4::atom::Atoms; diff --git a/src/musepack.rs b/src/musepack.rs index 0a938b5..e403e21 100644 --- a/src/musepack.rs +++ b/src/musepack.rs @@ -16,7 +16,7 @@ //! - **Compression**: Lossy (psychoacoustic model) //! - **Bitrate**: 120-500 kbps (quality levels 4-10) //! - **Sample Rates**: 32 kHz, 37.8 kHz, 44.1 kHz, 48 kHz -//! - **Channels**: 1-2 (mono/stereo) +//! - **Channels**: 1-2 (SV4-7 hardcode stereo, SV8 parses from header) //! - **Quality Focus**: Transparency at 180+ kbps //! - **File Extension**: `.mpc` //! - **MIME Type**: `audio/x-musepack` @@ -76,7 +76,7 @@ use tokio::fs::File as TokioFile; #[cfg(feature = "async")] use tokio::io::{AsyncReadExt, AsyncSeekExt}; -/// Musepack stream versions 4-7 sample rates +/// Musepack sample rates (used by stream versions 4-8) const RATES: [u32; 4] = [44100, 48000, 37800, 32000]; /// Parse a Musepack SV8 variable-length integer from a reader. @@ -874,6 +874,11 @@ impl FileType for Musepack { /// Creates a new empty tag structure if none exists. If tags already exist, /// returns an error. /// + /// Note: the inherent method `Musepack::add_tags()` returns + /// `AudexError::MusepackHeaderError` on failure. This trait method + /// returns `AudexError::InvalidOperation` and is reached via + /// `FileType::add_tags(&mut mpc)`. + /// /// # Errors /// /// Returns `AudexError::InvalidOperation` if tags already exist. @@ -888,8 +893,6 @@ impl FileType for Musepack { /// if mpc.tags.is_none() { /// mpc.add_tags()?; /// } - /// mpc.set("title", vec!["My Song".to_string()])?; - /// mpc.save()?; /// # Ok::<(), audex::AudexError>(()) /// ``` fn add_tags(&mut self) -> Result<()> { diff --git a/src/ogg.rs b/src/ogg.rs index 3942bed..803f9e4 100644 --- a/src/ogg.rs +++ b/src/ogg.rs @@ -121,7 +121,7 @@ //! vec![6, 7, 8, 9, 10], //! ]; //! -//! // Convert to Ogg pages (sequence starts at 0, max page size 4096, wiggle room 2048) +//! // Convert to Ogg pages (sequence starts at 0, target page size 4096, wiggle room 2048) //! let pages = OggPage::from_packets(packets, 0, 4096, 2048); //! println!("Created {} pages", pages.len()); //! # Ok(()) @@ -287,8 +287,9 @@ pub struct OggPage { /// - Bit 2 (0x04): End of stream (EOS) flag pub header_type: u8, - /// Granule position - codec-specific time/position marker - /// Set to -1 for pages with incomplete packets + /// Granule position - codec-specific time/position marker. + /// A value of -1 means "no granule position set" per the Ogg spec. + /// When parsing from a file the raw header value is used as-is. pub position: i64, /// Bitstream serial number - unique identifier for this logical stream diff --git a/src/oggflac.rs b/src/oggflac.rs index 63dacdb..125160c 100644 --- a/src/oggflac.rs +++ b/src/oggflac.rs @@ -33,7 +33,7 @@ //! - **Compression**: Lossless (bit-perfect reproduction) //! - **Sample Rates**: Up to 655,350 Hz //! - **Channels**: 1-8 channels -//! - **Bit Depth**: 4-32 bits per sample +//! - **Bit Depth**: 1-32 bits per sample //! - **File Extension**: `.oga` or `.ogg` //! - **MIME Type**: `audio/x-oggflac` //! @@ -45,7 +45,7 @@ //! - **Multi-value fields**: Multiple artists, genres, etc. //! - **UTF-8 encoding**: Full Unicode support //! - **Standard fields**: TITLE, ARTIST, ALBUM, DATE, TRACKNUMBER, etc. -//! - **Case-insensitive keys**: Field names normalized to uppercase +//! - **Case-insensitive keys**: Field names normalized to lowercase //! - **Embedded pictures**: Via METADATA_BLOCK_PICTURE field //! //! # Basic Usage @@ -306,7 +306,7 @@ impl From for AudexError { /// - **`max_blocksize`**: Maximum block size in samples (typically 16-65535) /// - **`sample_rate`**: Sample rate in Hz (1-655350 Hz) /// - **`channels`**: Number of audio channels (1-8) -/// - **`bits_per_sample`**: Bits per sample (4-32 bits) +/// - **`bits_per_sample`**: Bits per sample (1-32 bits) /// - **`total_samples`**: Total number of PCM samples in the stream /// - **`length`**: Duration of the audio calculated from total samples and sample rate /// - **`serial_number`**: Ogg logical stream serial number for this FLAC stream @@ -349,7 +349,7 @@ pub struct OggFLACStreamInfo { pub sample_rate: u32, /// Number of audio channels (1-8) pub channels: u16, - /// Bits per sample (4-32) + /// Bits per sample (1-32) pub bits_per_sample: u16, /// Total number of PCM samples in the stream pub total_samples: u64, @@ -578,7 +578,7 @@ impl StreamInfo for OggFLACStreamInfo { /// # Tag Format /// /// Vorbis Comments store metadata as UTF-8 key-value pairs: -/// - Keys are case-insensitive (normalized to uppercase) +/// - Keys are case-insensitive (normalized to lowercase) /// - Values are UTF-8 strings /// - Multiple values per key are supported /// - Common fields: TITLE, ARTIST, ALBUM, DATE, TRACKNUMBER, GENRE, etc. diff --git a/src/oggopus.rs b/src/oggopus.rs index a6de2cf..378e5df 100644 --- a/src/oggopus.rs +++ b/src/oggopus.rs @@ -23,7 +23,7 @@ //! ## Audio Characteristics //! //! - **Sample rate**: Always operates at 48 kHz internally (original rate preserved in metadata) -//! - **Bitrate range**: 6 kbps to 510 kbps per channel +//! - **Bitrate range**: 6 kbps to 510 kbps total //! - **Channels**: Mono, stereo, or multichannel (up to 255 channels) //! - **Frame sizes**: Flexible from 2.5ms to 60ms //! - **Hybrid codec**: Combines SILK (speech) and CELT (music) codecs @@ -258,7 +258,7 @@ impl From for AudexError { /// - **`channels`**: Number of output channels (1=mono, 2=stereo, etc.) /// - **`sample_rate`**: Always 48000 Hz (Opus internally operates at 48 kHz) /// - **`pre_skip`**: Number of samples to discard from decoder output at start -/// - **`version`**: Opus version (currently 1, only major version 0 supported) +/// - **`version`**: Opus header version byte (e.g. 1 means major=0, minor=1) /// - **`gain`**: Output gain in Q7.8 dB format (divide by 256 to get dB) /// - **`channel_mapping_family`**: Channel mapping family (0=mono/stereo, 1=surround, 255=undefined) /// - **`stream_count`**: Number of encoded streams (for multichannel) @@ -1010,7 +1010,7 @@ impl OpusTags { /// /// Ogg Opus uses the Ogg container format with Opus-encoded audio: /// - **Extension**: `.opus` (standard) -/// - **MIME type**: `audio/opus` +/// - **MIME type**: `audio/ogg` (or `audio/ogg; codecs=opus`) /// - **Codec**: Opus (hybrid SILK + CELT codec) /// - **Sample rate**: Always 48 kHz internally /// diff --git a/src/oggspeex.rs b/src/oggspeex.rs index 081d145..e292bf5 100644 --- a/src/oggspeex.rs +++ b/src/oggspeex.rs @@ -48,7 +48,7 @@ //! - **Multi-value fields**: Multiple values per tag (e.g., multiple artists) //! - **UTF-8 encoding**: Full Unicode support for international text //! - **Standard fields**: TITLE, ARTIST, ALBUM, DATE, GENRE, DESCRIPTION, etc. -//! - **Case-insensitive keys**: Field names normalized to uppercase +//! - **Case-insensitive keys**: Field names normalized to lowercase //! - **Embedded pictures**: Via METADATA_BLOCK_PICTURE field //! //! # Basic Usage @@ -334,7 +334,7 @@ impl From for AudexError { /// - **`length`**: Duration of the audio calculated from final page granule position /// - **`channels`**: Number of audio channels (1 for mono, 2 for stereo) /// - **`sample_rate`**: Sample rate in Hz (8000, 16000, or 32000) -/// - **`bitrate`**: Target bitrate in bps (-1 for VBR, 0+ for CBR) +/// - **`bitrate`**: Target bitrate in bps (`None` for VBR/unknown, `Some(n)` for known bitrate) /// - **`serial`**: Ogg logical stream serial number /// /// # Speex-Specific Fields @@ -344,7 +344,7 @@ impl From for AudexError { /// - **`mode`**: Encoding mode (0=narrowband, 1=wideband, 2=ultra-wideband) /// - **`mode_bitstream_version`**: Bitstream version for the mode /// - **`nb_channels`**: Number of channels (duplicates `channels`) -/// - **`nb_frames`**: Number of frames per packet +/// - **`nb_frames`**: Number of frames (not parsed from the header; always 0) /// - **`frame_size`**: Frame size in samples /// - **`vbr`**: Variable bitrate flag (0=CBR, 1=VBR) /// - **`frames_per_packet`**: Frames bundled per Ogg packet @@ -405,7 +405,7 @@ pub struct SpeexInfo { pub channels: u16, /// Sample rate in Hz (8000, 16000, or 32000) pub sample_rate: u32, - /// Bitrate in bps (-1 for VBR, 0+ for CBR) + /// Bitrate in bps (`None` for VBR/unknown, `Some(n)` for known bitrate) pub bitrate: Option, /// Ogg logical stream serial number pub serial: u32, @@ -420,7 +420,7 @@ pub struct SpeexInfo { pub mode_bitstream_version: i32, /// Number of channels (same as `channels`) pub nb_channels: u32, - /// Number of frames (reserved, typically 0) + /// Number of frames (not parsed from the header; always 0) pub nb_frames: i32, /// Frame size in samples pub frame_size: u32, @@ -649,7 +649,7 @@ impl StreamInfo for SpeexInfo { /// # Tag Format /// /// Vorbis Comments are UTF-8 key-value pairs: -/// - Keys are case-insensitive (normalized to uppercase) +/// - Keys are case-insensitive (normalized to lowercase) /// - Values are UTF-8 strings /// - Multiple values per key are supported /// - Common fields: TITLE, ARTIST, ALBUM, DATE, GENRE, DESCRIPTION, etc. diff --git a/src/oggtheora.rs b/src/oggtheora.rs index 0f859e3..52a00be 100644 --- a/src/oggtheora.rs +++ b/src/oggtheora.rs @@ -48,7 +48,7 @@ //! - **Multi-value fields**: Multiple values per tag //! - **UTF-8 encoding**: Full Unicode support //! - **Standard fields**: TITLE, ARTIST, ALBUM, DATE, DESCRIPTION, etc. -//! - **Case-insensitive keys**: Normalized to uppercase +//! - **Case-insensitive keys**: Normalized to lowercase //! - **Video-specific tags**: ENCODER, COPYRIGHT, LICENSE, etc. //! //! # Basic Usage diff --git a/src/oggvorbis.rs b/src/oggvorbis.rs index 7899d4e..3c19e04 100644 --- a/src/oggvorbis.rs +++ b/src/oggvorbis.rs @@ -189,6 +189,7 @@ use tokio::io::{AsyncSeekExt, BufReader as TokioBufReader}; /// } /// ``` #[derive(Debug, thiserror::Error)] +#[non_exhaustive] pub enum OggVorbisError { #[error("Ogg Vorbis error: {0}")] General(String), @@ -229,6 +230,7 @@ pub enum OggVorbisError { /// } /// ``` #[derive(Debug, thiserror::Error)] +#[non_exhaustive] pub enum OggVorbisHeaderError { #[error("Header error: {0}")] InvalidHeader(String), @@ -665,7 +667,7 @@ pub struct OggVorbis { /// # Fields /// /// - **`length`**: Total duration of the audio file -/// - **`bitrate`**: Average bitrate in bits per second (calculated from file size and duration) +/// - **`bitrate`**: Average bitrate in bits per second (derived from identification header fields) /// - **`sample_rate`**: Audio sample rate in Hz (typically 44100 or 48000) /// - **`channels`**: Number of audio channels (1=mono, 2=stereo, etc.) /// - **`serial`**: Ogg logical bitstream serial number (unique stream identifier) @@ -677,7 +679,7 @@ pub struct OggVorbis { /// # Bitrate Information /// /// Vorbis uses variable bitrate (VBR) encoding by default. The bitrate fields provide hints: -/// - **bitrate**: Actual average calculated from file +/// - **bitrate**: Derived from the identification header's nominal/max/min bitrate fields /// - **nominal_bitrate**: Encoder's target quality setting /// - **max_bitrate/min_bitrate**: Bitrate constraints (often unused) /// diff --git a/src/optimfrog.rs b/src/optimfrog.rs index a2e0d4b..68a0dba 100644 --- a/src/optimfrog.rs +++ b/src/optimfrog.rs @@ -4,7 +4,8 @@ //! focused on achieving maximum compression ratios. OptimFROG specializes in reducing //! file sizes to the absolute minimum while maintaining bit-perfect audio restoration. //! -//! **Note**: Only OptimFROG versions 4.5 and higher are supported. +//! **Note**: The current parser reads OptimFROG stream headers without enforcing a +//! minimum encoder version. //! //! # File Format //! diff --git a/src/replaygain.rs b/src/replaygain.rs index 2f344f7..89b92c3 100644 --- a/src/replaygain.rs +++ b/src/replaygain.rs @@ -1,7 +1,7 @@ //! ReplayGain utility module for loudness normalization metadata. //! -//! Provides a unified interface for reading and writing ReplayGain metadata -//! across different audio formats (MP3, FLAC, Ogg Vorbis, Ogg Opus, etc.). +//! Provides ReplayGain parsing, validation, and formatting helpers, plus +//! helpers for formats that store ReplayGain in Vorbis-style comments. //! //! # Overview //! @@ -41,11 +41,8 @@ //! These formats use Vorbis Comment tags with standardized field names: //! //! ```ignore -//! // Note: This example requires actual audio files on the filesystem. -//! use audex::flac::FLAC; //! use audex::replaygain::{ReplayGainInfo, to_vorbis_comments}; -//! -//! let mut flac = FLAC::load("song.flac")?; +//! use std::collections::HashMap; //! //! // Create ReplayGain information //! let rg = ReplayGainInfo::with_both( @@ -53,12 +50,12 @@ //! 0.95, // Track peak (0.0 to 1.0+) //! -5.0, // Album gain in dB //! 0.98, // Album peak -//! ); +//! )?; //! -//! // Note: FLAC uses VCommentDict which may require conversion -//! // to HashMap> for this function +//! let mut comments: HashMap> = HashMap::new(); +//! to_vorbis_comments(&rg, &mut comments); //! -//! flac.save()?; +//! assert_eq!(comments["REPLAYGAIN_TRACK_GAIN"][0], "-3.50 dB"); //! ``` //! //! The following Vorbis Comment fields are used: @@ -66,28 +63,23 @@ //! - `REPLAYGAIN_TRACK_PEAK` - Track peak amplitude (e.g., "0.995117") //! - `REPLAYGAIN_ALBUM_GAIN` - Album gain in dB //! - `REPLAYGAIN_ALBUM_PEAK` - Album peak amplitude -//! - `REPLAYGAIN_REFERENCE_LOUDNESS` - Reference level (usually 89.0 dB SPL) +//! - `REPLAYGAIN_REFERENCE_LOUDNESS` - Reference level when explicitly stored +//! (usually 89.0 dB SPL) //! //! ## MP3 (ID3v2 TXXX Frames) //! -//! ReplayGain can also be stored in ID3v2 tags using TXXX frames: +//! ReplayGain values for ID3v2 TXXX frames can be formatted with these helpers: //! //! ```ignore -//! // Note: This example requires actual audio files on the filesystem. -//! use audex::mp3::MP3; -//! use audex::id3::{ID3Tags, Frame}; //! use audex::replaygain::{format_gain, format_peak}; -//! use audex::FileType; //! -//! let mut mp3 = MP3::load("song.mp3")?; +//! // Use these strings when creating TXXX frames such as +//! // REPLAYGAIN_TRACK_GAIN and REPLAYGAIN_TRACK_PEAK. +//! let track_gain_str = format_gain(-3.5_f32)?; // "-3.50 dB" +//! let track_peak_str = format_peak(0.95_f32)?; // "0.950000" //! -//! // Write ReplayGain as ID3v2 TXXX frames -//! // Access ID3 tags and add UserText frames for ReplayGain -//! // The format_gain and format_peak functions help format values correctly -//! let track_gain_str = format_gain(-3.5); // "-3.50 dB" -//! let track_peak_str = format_peak(0.95); // "0.950000" -//! -//! mp3.save()?; +//! assert_eq!(track_gain_str, "-3.50 dB"); +//! assert_eq!(track_peak_str, "0.950000"); //! ``` //! //! # Advanced Examples @@ -104,7 +96,7 @@ //! // Get adjustment factor for playback //! if let Some(factor) = rg.track_adjustment_factor() { //! println!("Multiply audio samples by {:.3} for normalization", factor); -//! // -3.5 dB = ~0.708x volume multiplier +//! // -3.5 dB = ~0.669x volume multiplier //! } //! ``` //! @@ -134,7 +126,7 @@ //! let rg = ReplayGainInfo::with_both( //! track_gain, track_peak, //! album_gain, album_peak, -//! ); +//! )?; //! //! // Note: Use ReplayGain info with format-specific tag implementations //! } @@ -145,17 +137,16 @@ //! Remove all ReplayGain information from a file: //! //! ```ignore -//! // Note: This example requires actual audio files on the filesystem. //! use audex::replaygain::clear_vorbis_comments; -//! use audex::oggvorbis::OggVorbis; -//! use audex::FileType; -//! -//! let mut vorbis = OggVorbis::load("song.ogg")?; +//! use std::collections::HashMap; //! -//! // Note: clear_vorbis_comments works with HashMap> -//! // For OggVorbis tags (VCommentDict), use the Tags trait methods instead +//! let mut comments: HashMap> = HashMap::from([ +//! ("REPLAYGAIN_TRACK_GAIN".to_string(), vec!["-3.50 dB".to_string()]), +//! ("REPLAYGAIN_TRACK_PEAK".to_string(), vec!["0.950000".to_string()]), +//! ]); //! -//! vorbis.save()?; +//! clear_vorbis_comments(&mut comments); +//! assert!(comments.is_empty()); //! ``` //! //! # Reference diff --git a/src/snapshot.rs b/src/snapshot.rs index 384edff..1bc8ad0 100644 --- a/src/snapshot.rs +++ b/src/snapshot.rs @@ -13,8 +13,9 @@ //! schema so that consumers do not need to understand every audio format. //! //! For lossless round-tripping that preserves format-specific fields (e.g. -//! MP4 freeform atoms), use `to_snapshot_with_raw()` which also populates -//! the `raw_tags` field with a JSON value of the underlying tag container. +//! MP4 freeform atoms), use `to_snapshot_with_raw()` which attempts to populate +//! the `raw_tags` field with a JSON value of the underlying tag container when +//! that format supports raw-tag serialization. use std::collections::HashMap; @@ -31,14 +32,13 @@ use std::collections::HashMap; /// /// # Deserialization safety /// -/// The `Deserialize` impl enforces resource limits (maximum tag count, -/// cumulative string size, and `raw_tags` structural bounds) regardless -/// of how deserialization is invoked. Callers do not need to use a -/// special entry point -- `serde_json::from_str::(input)` -/// is safe for untrusted input up to the configured limits. +/// The `Deserialize` impl enforces resource limits on the decoded structure +/// (maximum tag count, cumulative string size, and `raw_tags` structural +/// bounds) regardless of how deserialization is invoked. /// -/// For an additional upfront byte-length check that rejects oversized -/// payloads before any parsing occurs, see [`TagSnapshot::from_json_str`]. +/// For untrusted input, callers should still apply an outer transport/body-size +/// limit. [`TagSnapshot::from_json_str`] adds an upfront byte-length check that +/// rejects oversized payloads before parsing begins. #[derive(Debug, Clone)] #[cfg_attr(feature = "serde", derive(serde::Serialize))] pub struct TagSnapshot { diff --git a/src/tagmap/mod.rs b/src/tagmap/mod.rs index f54c75a..0d40af5 100644 --- a/src/tagmap/mod.rs +++ b/src/tagmap/mod.rs @@ -50,10 +50,13 @@ pub use normalize::TagSystem; /// Canonical metadata field names that can be mapped across all tag formats. /// /// Each variant represents a semantic concept (e.g. "the track title") rather -/// than a format-specific key, allowing lossless round-trip conversion between -/// ID3v2 frames, Vorbis Comment keys, MP4 atoms, APEv2 keys, and ASF attributes. +/// than a format-specific key, enabling conversion between ID3v2 frames, +/// Vorbis Comment keys, MP4 atoms, APEv2 keys, and ASF attributes. Note that +/// round-trip conversion may be lossy (e.g. ID3v2.3 TYER/TDAT merge into a +/// single field, and not all formats map all fields). #[derive(Debug, Clone, PartialEq, Eq, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[non_exhaustive] pub enum StandardField { Title, Artist, @@ -326,12 +329,12 @@ impl TagMap { self.custom.remove(key); } - /// Iterate over all standard fields and their values. + /// Return all standard fields and their values. pub fn standard_fields(&self) -> Vec<(&StandardField, &[String])> { self.fields.iter().map(|(k, v)| (k, v.as_slice())).collect() } - /// Iterate over all custom fields and their values. + /// Return all custom fields and their values. pub fn custom_fields(&self) -> Vec<(&str, &[String])> { self.custom .iter() @@ -370,7 +373,7 @@ impl TagMap { } } - /// Returns true if the tag map contains no fields (standard or custom). + /// Returns true if the tag map contains no fields (standard, custom, or pictures). pub fn is_empty(&self) -> bool { self.fields.is_empty() && self.custom.is_empty() && self.pictures.is_empty() } @@ -471,6 +474,7 @@ impl fmt::Display for ConversionReport { /// Reason a field was not written during tag conversion. #[derive(Debug, Clone, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[non_exhaustive] pub enum SkipReason { /// The destination format has no equivalent field. UnsupportedByTarget, diff --git a/src/tagmap/normalize.rs b/src/tagmap/normalize.rs index 35a5528..1b118c2 100644 --- a/src/tagmap/normalize.rs +++ b/src/tagmap/normalize.rs @@ -19,6 +19,7 @@ use serde::{Deserialize, Serialize}; /// normalization functions to apply the correct parsing rules. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[non_exhaustive] pub enum TagSystem { /// ID3v2 (used by MP3, AIFF, WAV, DSF, DSDIFF) ID3v2, diff --git a/src/tags.rs b/src/tags.rs index c2e2e83..1663a48 100644 --- a/src/tags.rs +++ b/src/tags.rs @@ -150,7 +150,7 @@ //! ## APEv2 (APE, WavPack, Musepack) //! - Key-value items //! - Supports both text and binary values -//! - Case-sensitive keys +//! - Case-insensitive keys (original case preserved) //! //! # Padding Information //! @@ -192,6 +192,7 @@ use std::path::Path; /// - **Low threshold**: 1 KiB + 0.1% of trailing data /// /// If current padding exceeds the high threshold, it's reduced to the low threshold. +/// If current padding is non-negative and within the high threshold, it's kept as-is. /// If current padding is insufficient (negative), padding is added to reach the low threshold. /// /// # Examples @@ -696,7 +697,7 @@ pub trait Tags { /// id3.set("TPE1", vec!["New Artist".to_string()]); /// /// // Save back to file -/// id3.save_to_path(Some("song.mp3"))?; +/// id3.save()?; /// # Ok(()) /// # } /// ``` diff --git a/src/tak.rs b/src/tak.rs index 622819f..7ab9131 100644 --- a/src/tak.rs +++ b/src/tak.rs @@ -16,8 +16,8 @@ //! //! - **Compression**: Lossless (bit-perfect reproduction) //! - **Sample Rates**: Up to 384 kHz -//! - **Bit Depth**: 8-24 bits per sample -//! - **Channels**: 1-8 channels +//! - **Bit Depth**: 8-39 bits per sample +//! - **Channels**: 1-16 channels //! - **Compression Ratio**: Typically 40-60% of original size //! - **File Extension**: `.tak` //! - **MIME Type**: `audio/x-tak` @@ -733,6 +733,11 @@ impl FileType for TAK { /// Creates a new empty tag structure if none exists. If tags already exist, /// returns an error. /// + /// Note: the inherent method `TAK::add_tags()` returns + /// `AudexError::TAKHeaderError` on failure. This trait method + /// returns `AudexError::InvalidOperation` and is reached via + /// `FileType::add_tags(&mut tak)`. + /// /// # Errors /// /// Returns `AudexError::InvalidOperation` if tags already exist. @@ -747,8 +752,6 @@ impl FileType for TAK { /// if tak.tags.is_none() { /// tak.add_tags()?; /// } - /// tak.set("title", vec!["My Song".to_string()])?; - /// tak.save()?; /// # Ok::<(), audex::AudexError>(()) /// ``` fn add_tags(&mut self) -> Result<()> { diff --git a/src/trueaudio.rs b/src/trueaudio.rs index 905e008..d5ab289 100644 --- a/src/trueaudio.rs +++ b/src/trueaudio.rs @@ -16,7 +16,7 @@ //! //! - **Compression**: Lossless (bit-perfect reproduction) //! - **Sample Rates**: 8 kHz to 192 kHz (and higher) -//! - **Bit Depth**: 8, 16, or 24 bits per sample +//! - **Bit Depth**: 8, 16, 24, or 32 bits per sample //! - **Channels**: 1-8 channels //! - **Frame Size**: Fixed at 1.04 seconds of audio //! - **File Extension**: `.tta` @@ -42,7 +42,7 @@ //! // Access stream information //! println!("Sample Rate: {} Hz", tta.info.sample_rate); //! -//! // TrueAudio supports both ID3 and APEv2 tags via the unified tags_mut() interface +//! // tags_mut() returns ID3 tags when present (use assign_ape_tags() for APEv2) //! if let Some(tags) = tta.tags_mut() { //! tags.set("TIT2", vec!["Song Title".to_string()]); //! tags.set("TPE1", vec!["Artist Name".to_string()]); @@ -187,6 +187,7 @@ impl TrueAudioStreamInfo { /// Tag types supported by TrueAudio #[derive(Debug)] +#[non_exhaustive] #[allow(clippy::large_enum_variant)] pub enum TrueAudioTags { /// Default ID3 tags (like the ID3FileType) @@ -209,7 +210,7 @@ impl TrueAudioTags { self.keys().is_empty() } - /// Set a tag value (unified interface for both ID3 and APE) + /// Set a tag value (APE tags only; returns error for ID3 tags) pub fn set(&mut self, key: &str, value: APEValue) -> Result<()> { match self { TrueAudioTags::ID3(_) => { @@ -222,7 +223,7 @@ impl TrueAudioTags { } } - /// Get a tag value (unified interface) + /// Get a tag value (APE tags only; returns None for ID3 tags) pub fn get(&self, key: &str) -> Option<&APEValue> { match self { TrueAudioTags::ID3(_) => None, // ID3 tags don't use APEValue @@ -230,7 +231,7 @@ impl TrueAudioTags { } } - /// Remove a tag (unified interface) + /// Remove a tag (APE tags only; returns error for ID3 tags) pub fn remove(&mut self, key: &str) -> Result<()> { match self { TrueAudioTags::ID3(_) => { @@ -343,7 +344,10 @@ impl TrueAudio { if let Some(tags) = &self.tags { if !tags.keys().is_empty() { - output.push_str(" (with ID3 tags)"); + match tags { + TrueAudioTags::ID3(_) => output.push_str(" (with ID3 tags)"), + TrueAudioTags::APE(_) => output.push_str(" (with APE tags)"), + } } } @@ -545,9 +549,9 @@ impl TrueAudio { /// Save tags to the TrueAudio file asynchronously. /// - /// Writes the current APE tags back to the file. If the file has ID3 tags, - /// they will be converted and saved as APE tags (the async implementation - /// only supports APEv2 tag format). + /// Writes the current APE tags back to the file. The async implementation + /// only supports APEv2 tags; attempting to save ID3-backed tags returns an + /// error instead of converting them. /// /// # Returns /// A `Result` indicating success or an error @@ -830,7 +834,10 @@ impl FileType for TrueAudio { /// Adds empty APEv2 tags to the file. /// /// Creates a new empty APE tag structure if none exists. If tags already exist, - /// returns an error. TrueAudio files typically use APE tags rather than ID3. + /// returns an error. + /// + /// Note: the inherent method `TrueAudio::add_tags()` creates ID3 tags instead. + /// This trait method is reached via `FileType::add_tags(&mut tta)`. /// /// # Errors /// @@ -844,10 +851,11 @@ impl FileType for TrueAudio { /// /// let mut tta = TrueAudio::load("song.tta")?; /// if tta.tags.is_none() { + /// // Inherent method creates ID3 tags /// tta.add_tags()?; + /// // Trait method creates APE tags instead: + /// // FileType::add_tags(&mut tta)?; /// } - /// tta.set("title", vec!["My Song".to_string()])?; - /// tta.save()?; /// # Ok::<(), audex::AudexError>(()) /// ``` fn add_tags(&mut self) -> Result<()> { diff --git a/src/util.rs b/src/util.rs index 3e9d728..2ad3f3d 100644 --- a/src/util.rs +++ b/src/util.rs @@ -19,49 +19,45 @@ //! //! ### File I/O with Memory Fallback //! -//! The loadfile system provides automatic memory fallback when filesystem operations fail: +//! The loadfile system provides automatic memory fallback for eligible writable +//! operations when filesystem operations fail: //! //! ```rust,ignore -//! use audex::util::{loadfile_process, LoadFileOptions, loadfile_guard}; +//! use audex::util::{loadfile_process, LoadFileOptions}; //! //! # fn main() -> Result<(), Box> { //! // Basic usage for reading //! let file_thing = loadfile_process("path/to/file.txt", &LoadFileOptions::read_function())?; //! -//! // Usage with automatic write-back via RAII guard -//! let mut guard = loadfile_guard("path/to/file.txt", &LoadFileOptions::write_function())?; -//! // Modifications to guard will be automatically written back on drop -//! -//! // Manual write-back control -//! guard.write_back()?; // Explicit write-back -//! let file_thing = guard.into_inner(); // Take ownership without write-back +//! // Usage for writing +//! let file_thing = loadfile_process("path/to/file.txt", &LoadFileOptions::write_function())?; //! # Ok(()) //! # } //! ``` //! //! The loadfile system automatically handles: -//! - Memory fallback when filesystem operations fail (EOPNOTSUPP) +//! - Memory fallback for eligible writable operations when filesystem operations fail (EOPNOTSUPP) //! - Write-back from memory to disk when needed //! - Proper error conversion from IO errors to AudexError -//! - RAII-based resource management +//! +//! Pure read paths normally operate on the original file handle directly rather than +//! falling back to an in-memory copy. //! //! ### Binary Data Processing //! //! Utilities for reading and writing binary data in various formats: //! -//! ```rust,ignore -//! use audex::util::{CData, BinaryCursor}; +//! ```rust,no_run +//! use std::io::Cursor; +//! use audex::util::BitReader; //! //! # fn main() -> Result<(), Box> { -//! // Reading little-endian integers +//! // Reading binary data with BitReader //! let data = vec![0x01, 0x02, 0x03, 0x04]; -//! let value = CData::uint32_le(&data)?; -//! assert_eq!(value, 0x04030201); -//! -//! // Using BinaryCursor for structured parsing -//! let mut reader = BinaryCursor::new(&data); -//! let byte = reader.read_u8()?; -//! let word = reader.read_u16_le()?; +//! let mut cursor = Cursor::new(data); +//! let mut reader = BitReader::new(&mut cursor)?; +//! let byte = reader.read_bits(8)?; +//! let nibble = reader.read_bits(4)?; //! # Ok(()) //! # } //! ``` @@ -71,23 +67,18 @@ //! Functions for handling various text encodings common in audio metadata: //! //! ```rust,ignore -//! use audex::util::{decode_text, detect_bom, normalize_encoding}; +//! use audex::util::{decode_text, detect_bom}; //! //! # fn main() -> Result<(), Box> { //! // Decode text with encoding detection -//! // Note: UTF-16 support depends on platform encoding availability //! let data = b"\xFF\xFEH\x00e\x00l\x00l\x00o\x00"; // UTF-16 LE with BOM -//! let (text, _encoding, _bom) = decode_text(data, Some("utf-16"), "replace", false)?; +//! let (text, _encoding, _bom) = decode_text(data, Some("utf-16le"), "replace", true)?; //! assert_eq!(text, "Hello"); //! //! // Detect BOM (Byte Order Mark) //! if let Some((encoding, bom_size)) = detect_bom(data) { //! println!("Detected encoding: {}, BOM size: {}", encoding, bom_size); //! } -//! -//! // Normalize encoding names (e.g., "UTF8" -> "utf-8") -//! let normalized = normalize_encoding("UTF8"); -//! assert_eq!(normalized, "utf-8"); //! # Ok(()) //! # } //! ``` @@ -1800,6 +1791,7 @@ pub fn seek_end(fileobj: &mut File, offset: u64) -> Result { /// Input types for the openfile function #[derive(Debug)] +#[non_exhaustive] pub enum FileInput { /// A filesystem path to open Path(PathBuf), @@ -1859,7 +1851,7 @@ impl Default for FallbackOptions { /// for problematic filesystems (like FUSE). /// /// # Arguments -/// * `path_or_file` - Either a filesystem path or an existing file handle +/// * `path_or_file` - A filesystem path, an existing file handle, or an in-memory buffer /// * `options` - File opening options (read/write/create permissions) /// * `fallback_options` - Configuration for memory fallback behavior /// @@ -3302,6 +3294,7 @@ pub type MemoryFileThing = FileThing>>; /// FileThing that can handle both file and memory operations #[derive(Debug)] +#[non_exhaustive] pub enum AnyFileThing { File(FileFileThing), Memory(MemoryFileThing), @@ -3526,6 +3519,7 @@ impl Seek for FileThing { /// Different types of file arguments that can be passed #[derive(Debug)] +#[non_exhaustive] pub enum FileOrPath { File(File), Path(PathBuf), diff --git a/src/vorbis.rs b/src/vorbis.rs index 89e6c90..e5ef1bf 100644 --- a/src/vorbis.rs +++ b/src/vorbis.rs @@ -115,6 +115,7 @@ use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64}; /// These errors occur when Vorbis Comment data is malformed, contains invalid /// UTF-8 text, or violates the Vorbis Comment specification. #[derive(Debug, Clone, PartialEq)] +#[non_exhaustive] pub enum VorbisError { /// The framing bit was not set or is invalid. /// @@ -178,6 +179,7 @@ impl std::error::Error for VorbisError {} /// let mode = ErrorMode::Ignore; /// ``` #[derive(Debug, Clone, Copy, PartialEq, Default)] +#[non_exhaustive] pub enum ErrorMode { /// Fail immediately on any invalid data. /// @@ -271,9 +273,9 @@ pub fn is_valid_key(key: &str) -> bool { /// # Key Normalization /// /// Per the Vorbis Comment specification, field names are case-insensitive. This -/// implementation normalizes all keys to lowercase internally: +/// implementation normalizes all keys to lowercase for internal lookup: /// - "TITLE", "Title", and "title" all map to the same field -/// - Original case is not preserved +/// - Original case is preserved in the `data` vector /// /// # Framing Bit /// @@ -356,7 +358,7 @@ pub struct VComment { /// /// This string typically contains the name and version of the encoding /// software. For files created by this library, this defaults to - /// "audex {version}". + /// `"Audex {version}"`. pub vendor: String, /// The comment data as (key, value) pairs, preserving original order. @@ -1560,14 +1562,13 @@ impl VComment { let comment_string = Self::decode_string(comment_bytes, errors)?; if let Some((key, value)) = comment_string.split_once('=') { - // Normalize key to lowercase per Vorbis specification + // Validate with lowercase key per spec, but preserve original case let normalized_key = key.to_lowercase(); if is_valid_key(&normalized_key) { let value_string = value.to_string(); - // Store normalized lowercase key for consistency - self.data - .push((normalized_key.clone(), value_string.clone())); - // Update tags HashMap for efficient lookups + // Store original case key in data for round-trip preservation + self.data.push((key.to_string(), value_string.clone())); + // Use lowercase key for tags HashMap (case-insensitive lookups) self.tags .entry(normalized_key) .or_default() diff --git a/src/wave.rs b/src/wave.rs index 514952c..a8567b9 100644 --- a/src/wave.rs +++ b/src/wave.rs @@ -1,8 +1,8 @@ //! WAV/WAVE format support //! //! Comprehensive support for the Microsoft WAVE audio file format with ID3v2 tag support. -//! WAVE files use the RIFF container format to store uncompressed or losslessly compressed -//! audio data. +//! WAVE files use the RIFF container format to store uncompressed, lossless, or lossy +//! audio data depending on the codec in the `fmt ` chunk. //! //! # Format Overview //! @@ -155,7 +155,7 @@ pub struct RiffChunk { pub offset: u64, /// Absolute file offset where chunk data begins (after the 8-byte header) pub data_offset: u64, - /// Actual data size (may differ from `size` due to padding) + /// Actual data size (same as `size` in this implementation) pub data_size: u32, } diff --git a/src/wavpack.rs b/src/wavpack.rs index 788d328..088fc00 100644 --- a/src/wavpack.rs +++ b/src/wavpack.rs @@ -16,7 +16,7 @@ //! //! - **Sample Rates**: 6 kHz to 192 kHz //! - **Bit Depth**: 8, 16, 24, or 32 bits per sample -//! - **Channels**: 1-2 (stereo), with multichannel support in version 5+ +//! - **Channels**: Currently parsed as mono or stereo by this implementation //! - **Special**: DSD (Direct Stream Digital) support //! - **File Extension**: `.wv` (main), `.wvc` (correction file for hybrid mode) //! - **MIME Type**: `audio/x-wavpack` diff --git a/wasm/Cargo.toml b/wasm/Cargo.toml index 942a453..4b9caed 100644 --- a/wasm/Cargo.toml +++ b/wasm/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "audex-wasm" -version = "0.1.0" +version = "0.2.0" edition = "2024" rust-version = "1.85" authors = ["bakgio"] @@ -16,7 +16,7 @@ exclude = ["tests/*"] crate-type = ["cdylib", "rlib"] [dependencies] -audex = { version = "0.1.0", path = "..", features = ["serde"] } +audex = { version = "0.2.0", path = "..", features = ["serde"] } wasm-bindgen = "0.2" js-sys = "0.3" serde = { version = "1.0", features = ["derive"] } diff --git a/wasm/src/audio_file.rs b/wasm/src/audio_file.rs index 22781db..237a987 100644 --- a/wasm/src/audio_file.rs +++ b/wasm/src/audio_file.rs @@ -901,7 +901,8 @@ where impl AudioFile { /// Embed cover art as an APIC frame (ID3-based formats only). /// - /// Accepts raw image bytes (JPEG or PNG) and a MIME type string. + /// Accepts raw image bytes and an image MIME type string (for example JPEG, + /// PNG, GIF, BMP, or other `image/*` types). /// Returns an error for non-ID3 formats. #[wasm_bindgen(js_name = "setCoverArt")] pub fn set_cover_art(&mut self, data: &[u8], mime_type: &str) -> Result<(), JsValue> { diff --git a/wasm/src/error.rs b/wasm/src/error.rs index 5a7fc1e..f1858cb 100644 --- a/wasm/src/error.rs +++ b/wasm/src/error.rs @@ -120,6 +120,7 @@ fn error_name(e: &audex::AudexError) -> String { audex::AudexError::ID3FrameTooShort { .. } => "ID3FrameTooShort", audex::AudexError::DepthLimitExceeded { .. } => "DepthLimitExceeded", audex::AudexError::InternalError(_) => "InternalError", + _ => "UnknownError", } .to_string() } From 1f13aac4a795aaaadf68d527dab1217f950eae6a Mon Sep 17 00:00:00 2001 From: bakgio <76126058+bakgio@users.noreply.github.com> Date: Mon, 30 Mar 2026 19:35:40 +0300 Subject: [PATCH 2/2] docs: clean up intra-doc link paths Why - Redundant fully-qualified paths (crate::diff::, crate::AudexError::) clutter doc comments when Rust resolves them implicitly - MAX_IN_MEMORY_WRITER_FILE needed an explicit path for correct resolution from the module-level doc block What - diff.rs: drop crate::diff:: prefixes on same-module links - id3/mod.rs: drop crate:: prefix on AudexError link - limits.rs: add explicit crate::limits:: path for cross-scope link --- src/diff.rs | 6 +++--- src/id3/mod.rs | 2 +- src/limits.rs | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/diff.rs b/src/diff.rs index 65d0d37..128bc0a 100644 --- a/src/diff.rs +++ b/src/diff.rs @@ -174,9 +174,9 @@ pub struct DiffOptions { /// `"songwriter"` (lowercased). Default: `false`. /// /// **Note:** This option is only consumed by - /// [`diff_normalized_with_options`](crate::diff::diff_normalized_with_options). - /// The standard [`diff_with_options`](crate::diff::diff_with_options) and - /// [`diff_items_with_options`](crate::diff::diff_items_with_options) functions + /// [`diff_normalized_with_options`]. + /// The standard [`diff_with_options`] and + /// [`diff_items_with_options`] functions /// ignore this field. pub normalize_custom_keys: bool, } diff --git a/src/id3/mod.rs b/src/id3/mod.rs index e542058..a39176d 100644 --- a/src/id3/mod.rs +++ b/src/id3/mod.rs @@ -578,7 +578,7 @@ pub fn clear>(filething: P) -> Result<()> { /// /// This convenience function delegates to [`ID3Tags::load`]. At present, that /// lower-level file-loading path is still stubbed and returns -/// [`AudexError::NotImplementedMethod`](crate::AudexError::NotImplementedMethod). +/// [`AudexError::NotImplementedMethod`]. /// For working file-based loading, use [`ID3::load_from_file`](crate::id3::ID3::load_from_file). /// /// # Parameters diff --git a/src/limits.rs b/src/limits.rs index b47deb9..bb15dcb 100644 --- a/src/limits.rs +++ b/src/limits.rs @@ -26,7 +26,7 @@ //! | AIFF/WAVE | 256 MB (chunk data) | ~4 GB (32-bit size) | //! //! Separately, ASF saving uses a whole-file in-memory rewrite path guarded by -//! [`MAX_IN_MEMORY_WRITER_FILE`], currently 512 MiB. +//! [`MAX_IN_MEMORY_WRITER_FILE`](crate::limits::MAX_IN_MEMORY_WRITER_FILE), currently 512 MiB. //! //! The default limits (8 MB tags / 16 MB images) are safe for untrusted //! input. If you need to accept spec-legal files with very large metadata,