Skip to main content

Crate ape_decoder

Crate ape_decoder 

Source
Expand description

§ape-decoder

CI Crates.io docs.rs License

Pure Rust decoder for Monkey’s Audio (APE) lossless audio files.

Built by OMBS.IO for echobox, our audiophile music app where lossless playback matters. We will also be contributing to the Symphonia audio framework to bring native APE codec support to the broader Rust audio ecosystem.

§Features

  • Decode APE files to raw PCM audio
  • All compression levels (Fast, Normal, High, Extra High, Insane)
  • All bit depths (8, 16, 24, 32-bit) and channel layouts (mono, stereo, multichannel)
  • Streaming frame-by-frame decode with iterator
  • Sample-level seeking
  • Multi-threaded parallel decoding
  • Range decoding (decode a subset of samples)
  • Progress callbacks with cancellation
  • APEv2 tag read/write/remove
  • ID3v2 tag parsing (v2.3 and v2.4)
  • MD5 quick verification (no decompression needed)
  • WAV header generation for APE-to-WAV export
  • No unsafe code

§Quick Start

use std::fs::File;
use std::io::BufReader;

let file = File::open("audio.ape").unwrap();
let mut reader = BufReader::new(file);

// Decode entire file to raw PCM bytes (little-endian, interleaved)
let pcm_data = ape_decoder::decode(&mut reader).unwrap();

§Streaming Decode

use ape_decoder::ApeDecoder;
use std::fs::File;
use std::io::BufReader;

let file = File::open("audio.ape").unwrap();
let mut decoder = ApeDecoder::new(BufReader::new(file)).unwrap();

// Access metadata
let info = decoder.info();
println!("{}Hz {}ch {}-bit, {} samples, {}ms",
    info.sample_rate, info.channels, info.bits_per_sample,
    info.total_samples, info.duration_ms);

// Decode frame by frame
for frame_result in decoder.frames() {
    let pcm_bytes = frame_result.unwrap();
    // process pcm_bytes...
}

§Seeking

// Seek to a specific sample (returns frame index + skip offset)
let pos = decoder.seek(44100)?; // seek to 1 second
println!("Frame {}, skip {} samples", pos.frame_index, pos.skip_samples);

// Or seek and decode in one call
let pcm_from_1s = decoder.decode_from(44100)?;

§Reading Tags

// APEv2 tags
if let Some(tag) = decoder.read_tag()? {
    println!("Title: {}", tag.title().unwrap_or("Unknown"));
    println!("Artist: {}", tag.artist().unwrap_or("Unknown"));

    // Access any field by name (case-insensitive)
    if let Some(year) = tag.get("Year") {
        println!("Year: {}", year);
    }
}

// ID3v2 tags (if present in file header)
if let Some(id3) = decoder.read_id3v2_tag()? {
    println!("Title: {}", id3.title().unwrap_or_default());
}

§Writing Tags

use ape_decoder::{ApeTag, write_tag};
use std::fs::OpenOptions;

let mut file = OpenOptions::new().read(true).write(true).open("audio.ape")?;

let mut tag = ApeTag::new();
tag.set("Title", "My Song");
tag.set("Artist", "My Band");
tag.set("Album", "My Album");
tag.set("Year", "2026");

write_tag(&mut file, &tag)?;

§Parallel Decode

// Decode using 4 threads (output is byte-identical to single-threaded)
let pcm = decoder.decode_all_parallel(4)?;

§Range Decode

// Decode only samples 44100..88200 (1 second starting at 1s)
let pcm = decoder.decode_range(44100, 88200)?;

§Progress Callback

let pcm = decoder.decode_all_with(|progress| {
    println!("{:.0}%", progress * 100.0);
    true // return false to cancel
})?;

§APE to WAV Export

let header = decoder.wav_header_data()
    .map(|h| h.to_vec())
    .unwrap_or_else(|| decoder.info().generate_wav_header());

let pcm = decoder.decode_all()?;

let mut wav = File::create("output.wav")?;
wav.write_all(&header)?;
wav.write_all(&pcm)?;

§MD5 Verification

// Quick verify without decompressing (checks stored MD5 hash)
if decoder.verify_md5()? {
    println!("File integrity OK");
}

§Supported Formats

Bit DepthChannelsStatus
8-bitMono/StereoSupported
16-bitMono/Stereo/MultichannelSupported
24-bitMono/Stereo/MultichannelSupported
32-bitMono/StereoSupported

All five compression levels: Fast (1000), Normal (2000), High (3000), Extra High (4000), Insane (5000).

§Testing & Verification

The decoder is validated through 127 automated tests covering all compression levels, bit depths, channel layouts, and edge cases:

  • Unit tests: byte-for-byte PCM comparison against C++ reference decoder output for 17 synthetic fixtures
  • Stress tests: 6 complex signals (chirp, multitone, transient, fade, square, intermod) at all 5 compression levels
  • CRC validation: per-frame CRC-32 verification and corruption detection tests
  • Fuzz testing: cargo-fuzz with 3 targets (full decode, frame decode, parser) to catch panics on malformed input

§Real-World Performance

Verified against a 633 MB vinyl rip (96 kHz, 24-bit stereo, 32 minutes):

C++ Reference (mac v12.53)Rust Decoder
Time1m 49s42s (~2.6x faster)
Output1,105,211,232 bytes PCM1,105,211,232 bytes PCM
Matchbyte-for-byte identicalbyte-for-byte identical

Run your own verification:

./scripts/verify_real_world.sh path/to/file.ape

§Limitations

  • Decode only (no encoder)
  • Requires APE file version >= 3950 (files created by Monkey’s Audio 3.95+)

§License

Licensed under either of:

at your option.

§Acknowledgments

Based on the Monkey’s Audio SDK by Matthew T. Ashland, licensed under the 3-clause BSD license.

Re-exports§

pub use decoder::decode;
pub use decoder::ApeDecoder;
pub use decoder::ApeInfo;
pub use decoder::FrameDecoder;
pub use decoder::FrameIterator;
pub use decoder::SeekResult;
pub use decoder::SourceFormat;
pub use error::ApeError;
pub use error::ApeResult;
pub use format::ApeFileInfo;
pub use id3v2::read_id3v2;
pub use id3v2::Id3v2Frame;
pub use id3v2::Id3v2Tag;
pub use tag::read_tag;
pub use tag::remove_tag;
pub use tag::write_tag;
pub use tag::ApeTag;
pub use tag::ApeTagField;
pub use tag::TagFieldType;

Modules§

decoder
error
format
id3v2
ID3v2.3 and ID3v2.4 tag reader for APE files.
tag