sea-codec 0.8.0

Low-complexity, lossy audio codec designed for embedded devices
Documentation
use clap::{Arg, ArgAction, ArgMatches, Command};
use sea_codec::{
    decoder::SeaDecoder,
    encoder::{EncoderSettings, SeaEncoder},
};
use std::{
    io::{Read, Write},
    path::Path,
};
use wav::{read_wav, write_wav};

#[path = "../tests/wav.rs"]
mod wav;

fn get_encoder_settings(matches: &ArgMatches) -> EncoderSettings {
    let frames_per_chunk = matches
        .get_one::<String>("chunk-size")
        .unwrap()
        .parse::<u16>()
        .unwrap_or_else(|_| {
            eprintln!("Error: Failed to parse chunk size");
            std::process::exit(1);
        });

    if frames_per_chunk < 200 || frames_per_chunk > 32000 {
        eprintln!("Error: Chunk size must be between 200 and 32000");
        std::process::exit(1);
    }

    let scale_factor_bits = matches
        .get_one::<String>("scalefactor-bits")
        .unwrap()
        .parse::<u8>()
        .unwrap_or_else(|_| {
            eprintln!("Error: Failed to parse scale factor bits");
            std::process::exit(1);
        });

    if scale_factor_bits < 3 || scale_factor_bits > 5 {
        eprintln!("Error: Scale factor bits must be between 3 and 5");
        std::process::exit(1);
    }

    let scale_factor_frames = matches
        .get_one::<String>("scalefactor-distance")
        .unwrap()
        .parse::<u8>()
        .unwrap_or_else(|_| {
            eprintln!("Error: Failed to parse scale factor frames");
            std::process::exit(1);
        });

    if scale_factor_frames < 1 || frames_per_chunk % scale_factor_frames as u16 != 0 {
        eprintln!("Error: Scale factor frames must be a divisor of chunk size");
        std::process::exit(1);
    }

    let residual_bits = matches
        .get_one::<String>("bitrate")
        .unwrap()
        .parse::<f32>()
        .unwrap_or_else(|_| {
            eprintln!("Error: Failed to parse residual bits");
            std::process::exit(1);
        });

    if residual_bits < 1.0 || residual_bits > 8.0 {
        eprintln!("Error: Bitrate must be between 1.0 and 8.0");
        std::process::exit(1);
    }

    let vbr = matches.get_flag("vbr");
    let vbr_residual_beam_width = match matches.get_one::<String>("vbr-effort").map(String::as_str)
    {
        None | Some("fast") => 0,
        Some("low") => 1,
        Some("mid") => 2,
        Some("high") => 3,
        Some("ultra") => 4,
        Some(_) => unreachable!("clap validates --vbr-effort"),
    };

    if vbr {
        if !(1.5..=8.0).contains(&residual_bits) {
            eprintln!("Error: With VBR, bitrate must be between 1.5 and 8.0");
            std::process::exit(1);
        }
    } else {
        if residual_bits.fract() != 0.0 || !(1..=8).contains(&(residual_bits as i32)) {
            eprintln!("Error: Without VBR, bitrate must be an integer between 1 and 8");
            std::process::exit(1);
        }
    }

    EncoderSettings {
        scale_factor_bits,
        scale_factor_frames,
        residual_bits,
        vbr,
        vbr_residual_beam_width,
        frames_per_chunk,
        ..Default::default()
    }
}

fn main() {
    let matches = Command::new("seaconv")
        .about("Converts between .wav and .sea files")
        .arg(
            Arg::new("input")
                .help("The input file in LPCM LE .wav or .sea format")
                .required(true)
                .index(1),
        )
        .arg(
            Arg::new("output")
                .help("The output file to save the conversion result (.sea or .wav)")
                .required(true)
                .index(2),
        )
        .arg(
            Arg::new("chunk-size")
                .long("chunk-size")
                .short('c')
                .help("Sets the number of frames within a chunk")
                .default_value("5120"),
        )
        .arg(
            Arg::new("bitrate")
                .long("bitrate")
                .short('b')
                .help("Sets the bitrate for the conversion")
                .default_value("3"),
        )
        .arg(
            Arg::new("scalefactor-bits")
                .long("scalefactor-bits")
                .short('s')
                .help("Sets the bitrate for scale factors")
                .default_value("4"),
        )
        .arg(
            Arg::new("scalefactor-distance")
                .long("scalefactor-distance")
                .short('d')
                .help("Sets the distance between scale factors in frames")
                .default_value("20"),
        )
        .arg(
            Arg::new("vbr")
                .long("vbr")
                .short('v')
                .action(ArgAction::SetTrue)
                .help("Enables Variable Bit Rate (VBR)"),
        )
        .arg(
            Arg::new("vbr-effort")
                .long("vbr-effort")
                .value_parser(["fast", "low", "mid", "high", "ultra"])
                .default_value("fast")
                .help("VBR effort: fast=scalar; low=1, mid=2, high=3, ultra=4 beam paths"),
        )
        .arg(
            Arg::new("resample")
                .long("resample")
                .short('r')
                .help("Sets the target sample rate for resampling"),
        )
        .get_matches();

    let settings = get_encoder_settings(&matches);

    let input = matches.get_one::<String>("input").unwrap();
    let output = matches.get_one::<String>("output").unwrap();

    let input_ext = Path::new(input).extension().and_then(|ext| ext.to_str());
    let output_ext = Path::new(output).extension().and_then(|ext| ext.to_str());

    match (input_ext, output_ext) {
        (Some("wav"), Some("sea")) => {
            let input_wave = read_wav(&Path::new(input)).unwrap_or_else(|_| {
                eprintln!("Error: Failed to decode .wav file");
                std::process::exit(1);
            });

            let mut output_file = std::fs::File::create(output).unwrap_or_else(|_| {
                eprintln!("Error: Failed to create output file");
                std::process::exit(1);
            });

            #[allow(unused_mut)]
            let mut samples = input_wave.samples;
            #[allow(unused_mut)]
            let mut sample_rate = input_wave.sample_rate;

            if let Some(target_rate_str) = matches.get_one::<String>("resample") {
                let target_rate = target_rate_str.parse::<u32>().unwrap_or_else(|_| {
                    eprintln!("Error: Failed to parse resample rate");
                    std::process::exit(1);
                });

                #[cfg(not(feature = "resample"))]
                let _ = target_rate;

                #[cfg(feature = "resample")]
                {
                    samples = sea_codec::resample::resample(
                        &samples,
                        sample_rate,
                        target_rate,
                        input_wave.channels as u32,
                    );
                    sample_rate = target_rate;
                }
                #[cfg(not(feature = "resample"))]
                {
                    eprintln!("Error: Resampling feature is not enabled. Recompile with --features resample");
                    std::process::exit(1);
                }
            }

            let mut sea_encoder = SeaEncoder::from_slice(
                input_wave.channels as u8,
                sample_rate,
                Some(samples.len() as u32 / input_wave.channels as u32),
                settings,
                &samples,
            )
            .unwrap_or_else(|_| {
                eprintln!("Error: Failed to create encoder");
                std::process::exit(1);
            });

            let mut buf = Vec::new();
            while sea_encoder.encode_frame(&mut buf).unwrap_or_else(|_| {
                eprintln!("Error: Failed to encode frame");
                std::process::exit(1);
            }) {
                output_file.write_all(&buf).unwrap_or_else(|_| {
                    eprintln!("Error: Failed to write to output file");
                    std::process::exit(1);
                });
                buf.clear();
            }

            sea_encoder.finalize().unwrap_or_else(|_| {
                eprintln!("Error: Failed to finalize encoder");
                std::process::exit(1);
            });
        }
        (Some("sea"), Some("wav")) => {
            let mut input_file = std::fs::File::open(input).unwrap_or_else(|_| {
                eprintln!("Error: Failed to open input file");
                std::process::exit(1);
            });

            let mut content = Vec::new();
            input_file.read_to_end(&mut content).unwrap();

            let mut sea_decoded = Vec::<i16>::with_capacity(64 * 1024 * 1024);
            let mut sea_decoder = SeaDecoder::from_slice(&content).unwrap();

            while sea_decoder
                .decode_frame(&mut sea_decoded)
                .unwrap_or_else(|_| {
                    eprintln!("Error: Failed to decode frame");
                    std::process::exit(1);
                })
            {}

            let info = sea_decoder.get_header();
            #[allow(unused_mut)]
            let mut samples = sea_decoded;
            #[allow(unused_mut)]
            let mut sample_rate = info.sample_rate;

            if let Some(target_rate_str) = matches.get_one::<String>("resample") {
                let target_rate = target_rate_str.parse::<u32>().unwrap_or_else(|_| {
                    eprintln!("Error: Failed to parse resample rate");
                    std::process::exit(1);
                });

                #[cfg(not(feature = "resample"))]
                let _ = target_rate;

                #[cfg(feature = "resample")]
                {
                    samples = sea_codec::resample::resample(
                        &samples,
                        sample_rate,
                        target_rate,
                        info.channels as u32,
                    );
                    sample_rate = target_rate;
                }
                #[cfg(not(feature = "resample"))]
                {
                    eprintln!("Error: Resampling feature is not enabled. Recompile with --features resample");
                    std::process::exit(1);
                }
            }

            write_wav(
                samples.as_slice(),
                info.channels as u16,
                sample_rate,
                output,
            )
            .unwrap_or_else(|_| {
                eprintln!("Error: Failed to encode wav file");
                std::process::exit(1);
            });
        }
        _ => {
            eprintln!("Error: Invalid file extensions. Supported conversions are .wav to .sea and .sea to .wav");
            std::process::exit(1);
        }
    }
}