Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions src/aac.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")
///
Expand Down
3 changes: 2 additions & 1 deletion src/ac3.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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`
//!
Expand Down
2 changes: 1 addition & 1 deletion src/aiff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
32 changes: 17 additions & 15 deletions src/asf/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,19 +33,19 @@ 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,
/// Codec description string from the CodecList object
pub codec_description: String,
/// Maximum instantaneous bitrate in bps (from FileProperties)
pub max_bitrate: Option<u32>,
/// Preroll time in 100-nanosecond units (subtracted from play_duration)
/// Preroll time in milliseconds (subtracted from play_duration)
pub preroll: Option<u64>,
/// Broadcast/seekable flags from FileProperties
pub flags: Option<u32>,
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
///
Expand All @@ -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<()> {
Expand Down
5 changes: 3 additions & 2 deletions src/asf/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions src/asf/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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),
Expand Down
11 changes: 6 additions & 5 deletions src/constants.rs
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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",
Expand Down
10 changes: 8 additions & 2 deletions src/diff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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`].
/// The standard [`diff_with_options`] and
/// [`diff_items_with_options`] functions
/// ignore this field.
pub normalize_custom_keys: bool,
}

Expand Down Expand Up @@ -803,7 +809,7 @@ fn tag_map_to_items(map: &TagMap) -> Vec<(String, Vec<String>)> {
///
/// `"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<String>)> {
let mut items: Vec<(String, Vec<String>)> = Vec::new();

Expand Down
9 changes: 4 additions & 5 deletions src/dsdiff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/dsf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 4 additions & 3 deletions src/easyid3.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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}")]
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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")
Expand Down
15 changes: 10 additions & 5 deletions src/easymp4.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
///
Expand All @@ -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<()> {
Expand Down
11 changes: 6 additions & 5 deletions src/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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> {
Expand Down Expand Up @@ -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<DynamicFileType>`)
//! provides method-based access with a uniform interface regardless of the underlying format:
//!
//! ```no_run
Expand Down
Loading
Loading