it2play-rs 0.1.0

Safe Rust bindings to it2play, an Impulse Tracker module player.
Documentation
use cpal::{
    traits::{DeviceTrait, HostTrait},
    Stream,
};
use cpal::{SampleFormat, SampleRate, SupportedStreamConfigRange};


const SAMPLE_RATE : usize = 48000;

/// Impulse Tracker 2 drivers. Currently there's a
/// Sound Blaser 16 emulated thingy and a high quality driver, supporting stereo samples.
pub enum IT2Driver {
    SB16, // Actually uses SB16MMX because who'd want SB16 drivers without filters?
    HQ
}

/// Loads a module from a stream of bytes.
/// Due to the way it2play is designed, you can only load one module at a time.
/// For example:
/// ```
/// // Includes file.it with a compiled binary, and plays it with high quality drivers.
/// it2play::load_bytes(Vec::from(include_bytes!("/path/to/file.it")), it2play_rs::IT2Driver::HQ);
/// ```
pub fn load_bytes(mod_data: Vec<u8>, driver: IT2Driver) {
    unsafe {
        let driver = match driver {
            IT2Driver::SB16 => it2play_sys::DRIVER_SB16MMX.try_into().unwrap(),
            IT2Driver::HQ => it2play_sys::DRIVER_HQ.try_into().unwrap(),
        };
        // These literally don't do anything                 ↓      ↓
        it2play_sys::Music_Init(SAMPLE_RATE.try_into().unwrap(), 1024, driver);
        it2play_sys::Music_LoadFromData(mod_data.as_ptr() as *mut u8, mod_data.len().try_into().unwrap());
    }
}

/// Frees memory after playing a song.
/// > I... don't think you need this? But better safe than sorry considering it's C
/// > -Spike
pub fn free() {
    unsafe {
        it2play_sys::Music_FreeSong();
        it2play_sys::Music_Close();
    }
}

/// Plays a module at the order specified.
/// If a module is currently playing, `Music_Stop()` will be called by it2play before attempting to play the module.
pub fn play(order: u16) {
    // TODO could throw errors if load_bytes wasn't called
    unsafe {
        it2play_sys::Song.ProcessRow = 0;
        it2play_sys::Music_PlaySong(order);
    }
}


/// Stops a module from playing.
pub fn stop() {
    unsafe {
        it2play_sys::Music_Stop();
    }
}

/// Generates a cpal stream.
pub fn generate_stream() -> Stream {
    // Set up cpal, build stream
    let host = cpal::default_host();
    let device = host
        .default_output_device()
        .expect("ModulePlayer: No output device available");

    let mut supported_cfgs = device.supported_output_configs().unwrap();
    let Some(config) = supported_cfgs.find(desired_config) else {
        panic!("ModulePlayer: Output device doesn't support desired parameters");
    };
    let config = config.with_sample_rate(SampleRate(SAMPLE_RATE.try_into().unwrap())).config();

    
    return device
        .build_output_stream(
            &config,
            move |data: &mut [i16], _cpal| read(config.sample_rate.0 as _, data),
            |err| {
                dbg!(err);
            },
            None, // None=blocking, Some(Duration)=timeout
        )
        .unwrap();
}


/// Fills a 16-bit integer buffer.
fn read(_rate: i32, stream: &mut [i16]) {
    unsafe {
        it2play_sys::Music_FillAudioBuffer(stream.as_mut_ptr(), (stream.len() / 2).try_into().unwrap());
    };
}

/// Finds a cpal config that supports 16-bit integer sample formats, and a sample rate of `SAMPLE_RATE`.
fn desired_config(cfg: &SupportedStreamConfigRange) -> bool {
    cfg.channels() == 2
        && cfg.sample_format() == SampleFormat::I16
        && cfg.max_sample_rate() >= SampleRate(SAMPLE_RATE.try_into().unwrap())
}