Rust API

The libflo crate provides native Rust encoding, decoding, and metadata support.

Installation

Add to your Cargo.toml:

[dependencies]
libflo-audio = { version = "0.1.2" }

Quick Start

use libflo_audio::{Encoder, decode, info};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Create 1 second of stereo silence
    let samples: Vec<f32> = vec![0.0; 44100 * 2];
    
    // Encode
    let encoder = Encoder::new(44100, 2, 16);
    let flo_data = encoder.encode(&samples, &[])?;
    
    // Decode
    let decoded = decode(&flo_data)?;
    
    // Info
    let file_info = info(&flo_data)?;
    println!("Duration: {}s", file_info.duration_secs);
    
    Ok(())
}

Lossless Encoding

Encoder

use libflo_audio::Encoder;

let encoder = Encoder::new(
    sample_rate,  // u32: e.g., 44100
    channels,     // u8: 1 (mono) or 2 (stereo)
    bit_depth     // u8: 16, 24, or 32
);

// Encode without metadata
let flo_data = encoder.encode(&samples, &[])?;

// Encode with metadata
let metadata = create_metadata();
let flo_data = encoder.encode(&samples, &metadata)?;

Sample Format

Samples must be:

  • Vec<f32> or &[f32]
  • Interleaved: [L, R, L, R, ...] for stereo
  • Normalized: -1.0 to 1.0
// Mono: [s0, s1, s2, ...]
// Stereo: [L0, R0, L1, R1, L2, R2, ...]

Lossy Encoding

LossyEncoder

use libflo_audio::{LossyEncoder, QualityPreset};

// With quality preset
let quality = QualityPreset::High.as_f32();
let mut encoder = LossyEncoder::new(44100, 2, quality);
let flo_data = encoder.encode_to_flo(&samples, &[])?;

Quality Presets

use libflo_audio::QualityPreset;

QualityPreset::Low         // 0.1  - ~48 kbps
QualityPreset::Medium      // 0.3  - ~128 kbps
QualityPreset::High        // 0.55 - ~192 kbps
QualityPreset::VeryHigh    // 0.75 - ~256 kbps
QualityPreset::Transparent // 0.95 - ~320 kbps

// Get f32 value
let quality: f32 = QualityPreset::High.as_f32();

Target Bitrate

use libflo_audio::{LossyEncoder, QualityPreset};

// Calculate quality from target bitrate
let quality = QualityPreset::from_bitrate(
    192,    // target kbps
    44100,  // sample rate
    2       // channels
).as_f32();

let mut encoder = LossyEncoder::new(44100, 2, quality);
let flo_data = encoder.encode_to_flo(&samples, &[])?;

Decoding

Auto-detect Mode

use libflo_audio::decode;

// Automatically detects lossless vs lossy
let samples: Vec<f32> = decode(&flo_data)?;

Decoder Struct

use libflo_audio::Decoder;

let decoder = Decoder::new();
let samples = decoder.decode(&flo_data)?;

File Information

use libflo_audio::info;

let file_info = info(&flo_data)?;

println!("Sample rate: {}", file_info.sample_rate);
println!("Channels: {}", file_info.channels);
println!("Bit depth: {}", file_info.bit_depth);
println!("Duration: {}s", file_info.duration_secs);
println!("Lossy: {}", file_info.is_lossy);
println!("Compression: {}x", file_info.compression_ratio);

AudioInfo Struct

pub struct AudioInfo {
    pub sample_rate: u32,
    pub channels: u8,
    pub bit_depth: u8,
    pub total_samples: u64,
    pub duration_secs: f64,
    pub is_lossy: bool,
    pub compression_ratio: f32,
}

Note: total_samples counts sample-frames (samples per channel). For UI/display, prefer using FloMetadata.length_ms (duration in ms) when available; otherwise compute duration from total_samples / sample_rate.


Validation

use libflo_audio::validate;

match validate(&flo_data) {
    Ok(true) => println!("File is valid"),
    Ok(false) => println!("CRC32 mismatch - file corrupted"),
    Err(e) => println!("Invalid file: {}", e),
}

Metadata

FloMetadata Struct

use libflo_audio::{FloMetadata, SectionType, PictureType};

let mut meta = FloMetadata::new();

// Basic fields
meta.title = Some("Track Title".to_string());
meta.artist = Some("Artist Name".to_string());
meta.album = Some("Album Name".to_string());
meta.year = Some(2026);
meta.genre = Some("Electronic".to_string());
meta.bpm = Some(128);

// Track numbering
meta.track_number = Some(1);
meta.track_total = Some(12);
meta.disc_number = Some(1);
meta.disc_total = Some(2);

// Serialize to MessagePack
let metadata_bytes = meta.to_msgpack()?;

// Use in encoding
let encoder = Encoder::new(44100, 2, 16);
let flo_data = encoder.encode(&samples, &metadata_bytes)?;

Section Markers

use libflo_audio::{FloMetadata, SectionType};

let mut meta = FloMetadata::new();

meta.add_section(0, SectionType::Intro, None);
meta.add_section(15000, SectionType::Verse, Some("Verse 1"));
meta.add_section(45000, SectionType::Chorus, None);
meta.add_section(75000, SectionType::Verse, Some("Verse 2"));
meta.add_section(105000, SectionType::Chorus, None);
meta.add_section(135000, SectionType::Bridge, None);
meta.add_section(165000, SectionType::Outro, None);

Section Types

pub enum SectionType {
    Intro, Verse, PreChorus, Chorus, PostChorus,
    Bridge, Breakdown, Drop, Buildup, Solo,
    Instrumental, Outro, Silence, Other
}

Cover Art

use libflo_audio::{FloMetadata, Picture, PictureType};
use std::fs;

let mut meta = FloMetadata::new();

let image_data = fs::read("cover.jpg")?;
meta.pictures = Some(vec![
    Picture {
        mime_type: "image/jpeg".to_string(),
        picture_type: PictureType::CoverFront,
        description: Some("Album Cover".to_string()),
        data: image_data,
    }
]);

Synchronized Lyrics

use libflo_audio::{FloMetadata, SyncedLyrics, LyricLine, LyricContentType};

let mut meta = FloMetadata::new();

meta.synced_lyrics = Some(vec![
    SyncedLyrics {
        language: Some("eng".to_string()),
        content_type: LyricContentType::Lyrics,
        description: None,
        lines: vec![
            LyricLine { timestamp_ms: 0, text: "First line of lyrics".to_string() },
            LyricLine { timestamp_ms: 5000, text: "Second line".to_string() },
            LyricLine { timestamp_ms: 10000, text: "Third line".to_string() },
        ],
    }
]);

Reading Metadata

use libflo_audio::get_metadata;

let meta = get_metadata(&flo_data)?;

if let Some(title) = meta.title {
    println!("Title: {}", title);
}

if let Some(sections) = meta.section_markers {
    for section in sections {
        println!("{:?} at {}ms", section.section_type, section.timestamp_ms);
    }
}

Low-Level API

Reader

use libflo_audio::Reader;

let reader = Reader::new();
let flo_file = reader.read(&data)?;

// Access header
println!("Version: {}.{}", flo_file.header.version_major, flo_file.header.version_minor);
println!("Sample rate: {}", flo_file.header.sample_rate);

// Access frames
for frame in &flo_file.frames {
    println!("Frame type: {}, samples: {}", frame.frame_type, frame.frame_samples);
}

Writer

use libflo_audio::{Writer, FloFile};

let writer = Writer::new();
let data = writer.write(&flo_file)?;

Error Handling

All functions return FloResult<T>:

use libflo_audio::{decode, FloError};

match decode(&data) {
    Ok(samples) => println!("Decoded {} samples", samples.len()),
    Err(FloError::InvalidMagic) => println!("Not a flo file"),
    Err(FloError::UnsupportedVersion(v)) => println!("Version {} not supported", v),
    Err(FloError::CrcMismatch) => println!("File is corrupted"),
    Err(e) => println!("Error: {:?}", e),
}

Error Types

pub enum FloError {
    InvalidMagic,
    UnsupportedVersion(u8),
    InvalidFrameType(u8),
    CrcMismatch,
    InsufficientData,
    EncodingError(String),
    DecodingError(String),
    MetadataError(String),
    IoError(std::io::Error),
}

Thread Safety

  • Encoder and Decoder are Send + Sync
  • Safe for concurrent encoding/decoding
  • No global state