it2play-rs 0.1.3

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

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(mut 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(),
        };
        // Sample rate/mixingBufferSize literally don't do anything since it's doing what generate_stream() ovverrides
        it2play_sys::Music_Init(SAMPLE_RATE.try_into().unwrap(), 1024, driver);
        it2play_sys::Music_LoadFromData(mod_data.as_mut_ptr(), 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();
    }
}

/// Sets `Song.GlobalVolume` -- should be an integer between 0 and 128
pub fn set_global_volume(vol: u16) {
    unsafe {
        it2play_sys::Song.GlobalVolume = vol;
    }
}

/// 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("it2play_rs::generate_stream: No output device available");

    let mut supported_cfgs = device.supported_output_configs().unwrap();
    let Some(config) = supported_cfgs.find(desired_config) else {
        panic!(
            "it2play_rs::generate_stream: Output device doesn't support desired parameters (f32 sample types/{}hz sample rate)",
            SAMPLE_RATE
        );
    };
    let config = config
        .with_sample_rate(SampleRate(SAMPLE_RATE.try_into().unwrap()))
        .config();
    let mut buffer = vec![0i16; 1024]; // <- Arbitrary buffer size, we'll be resizing it

    return device
        .build_output_stream(
            &config,
            move |data, _| read_f32(data, &mut buffer),
            |err| {
                dbg!(err);
            },
            None, // None=blocking, Some(Duration)=timeout
        )
        .unwrap();
}

/// Fills a 16-bit integer buffer. Alias to it2play_sys::Music_FillAudioBuffer.
/// This is the cheapest way to fill a buffer.
fn read_i16(stream: &mut [i16]) {
    unsafe {
        it2play_sys::Music_FillAudioBuffer(stream.as_mut_ptr(), (stream.len() / 2) as i32);
    };
}

/// Fills a 32-bit floating-point buffer
/// This is more expensive, takes up more RAM (for a i16 buffer), and may be lossy in the i16->f32 conversion.
fn read_f32(stream: &mut [f32], buffer: &mut Vec<i16>) {
    // NOTE: stream length is variable depending on the output device :(
    // We can use a signle buffer object in memory and just resize it if the stream length changes.
    if buffer.len() != stream.len() {
        buffer.resize(stream.len(), 0i16);
    }
    read_i16(buffer);
    // The conversion bit itself is a simple map, but credit goes to https://github.com/CenTdemeern1
    let converted_buffer: Vec<f32> = buffer
        .into_iter()
        .map(|x| (*x as f32) / (i16::MAX as f32))
        .collect();
    stream.copy_from_slice(&converted_buffer);
}

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