it2play_rs/
lib.rs

1use cpal::{
2    BufferSize, OutputCallbackInfo, SampleFormat, SampleRate, SupportedBufferSize,
3    SupportedStreamConfigRange,
4};
5use cpal::{
6    Stream,
7    traits::{DeviceTrait, HostTrait},
8};
9
10const SAMPLE_RATE: usize = 48000;
11
12/// Impulse Tracker 2 drivers. Currently there's a
13/// Sound Blaser 16 emulated thingy and a high quality driver, supporting stereo samples.
14pub enum IT2Driver {
15    SB16, // Actually uses SB16MMX because who'd want SB16 drivers without filters?
16    HQ,
17}
18
19/// Loads a module from a stream of bytes.
20/// Due to the way it2play is designed, you can only load one module at a time.
21/// For example:
22/// ```
23/// // Includes file.it with a compiled binary, and plays it with high quality drivers.
24/// it2play::load_bytes(Vec::from(include_bytes!("/path/to/file.it")), it2play_rs::IT2Driver::HQ);
25/// ```
26pub fn load_bytes(mut mod_data: Vec<u8>, driver: IT2Driver) {
27    unsafe {
28        let driver = match driver {
29            IT2Driver::SB16 => it2play_sys::DRIVER_SB16MMX.try_into().unwrap(),
30            IT2Driver::HQ => it2play_sys::DRIVER_HQ.try_into().unwrap(),
31        };
32        // Sample rate/mixingBufferSize literally don't do anything since it's doing what generate_stream() ovverrides
33        it2play_sys::Music_Init(SAMPLE_RATE.try_into().unwrap(), 1024, driver);
34        it2play_sys::Music_LoadFromData(mod_data.as_mut_ptr(), mod_data.len().try_into().unwrap());
35    }
36}
37
38/// Frees memory after playing a song.
39/// > I... don't think you need this? But better safe than sorry considering it's C
40/// > -Spike
41pub fn free() {
42    unsafe {
43        it2play_sys::Music_FreeSong();
44        it2play_sys::Music_Close();
45    }
46}
47
48/// Plays a module at the order specified.
49/// If a module is currently playing, `Music_Stop()` will be called by it2play before attempting to play the module.
50pub fn play(order: u16) {
51    // TODO could throw errors if load_bytes wasn't called
52    unsafe {
53        it2play_sys::Song.ProcessRow = 0;
54        it2play_sys::Music_PlaySong(order);
55    }
56}
57
58/// Stops a module from playing.
59pub fn stop() {
60    unsafe {
61        it2play_sys::Music_Stop();
62    }
63}
64
65/// Sets `Song.GlobalVolume` -- should be an integer between 0 and 128
66pub fn set_global_volume(vol: u16) {
67    unsafe {
68        it2play_sys::Song.GlobalVolume = vol;
69    }
70}
71
72/// Generates a cpal stream.
73pub fn generate_stream() -> Stream {
74    // Set up cpal, build stream
75    let host = cpal::default_host();
76    let device = host
77        .default_output_device()
78        .expect("it2play_rs::generate_stream: No output device available");
79
80    let mut supported_cfgs = device.supported_output_configs().unwrap();
81    let Some(config) = supported_cfgs.find(desired_config) else {
82        panic!(
83            "it2play_rs::generate_stream: Output device doesn't support desired parameters (f32 sample types/{}hz sample rate)",
84            SAMPLE_RATE
85        );
86    };
87    let config = config
88        .with_sample_rate(SampleRate(SAMPLE_RATE.try_into().unwrap()))
89        .config();
90    let mut buffer = vec![0i16; 1024]; // <- Arbitrary buffer size, we'll be resizing it
91
92    return device
93        .build_output_stream(
94            &config,
95            move |data, _| read_f32(data, &mut buffer),
96            |err| {
97                dbg!(err);
98            },
99            None, // None=blocking, Some(Duration)=timeout
100        )
101        .unwrap();
102}
103
104/// Fills a 16-bit integer buffer. Alias to it2play_sys::Music_FillAudioBuffer.
105/// This is the cheapest way to fill a buffer.
106fn read_i16(stream: &mut [i16]) {
107    unsafe {
108        it2play_sys::Music_FillAudioBuffer(stream.as_mut_ptr(), (stream.len() / 2) as i32);
109    };
110}
111
112/// Fills a 32-bit floating-point buffer
113/// This is more expensive, takes up more RAM (for a i16 buffer), and may be lossy in the i16->f32 conversion.
114fn read_f32(stream: &mut [f32], buffer: &mut Vec<i16>) {
115    // NOTE: stream length is variable depending on the output device :(
116    // We can use a signle buffer object in memory and just resize it if the stream length changes.
117    if buffer.len() != stream.len() {
118        buffer.resize(stream.len(), 0i16);
119    }
120    read_i16(buffer);
121    // The conversion bit itself is a simple map, but credit goes to https://github.com/CenTdemeern1
122    let converted_buffer: Vec<f32> = buffer
123        .into_iter()
124        .map(|x| (*x as f32) / (i16::MAX as f32))
125        .collect();
126    stream.copy_from_slice(&converted_buffer);
127}
128
129/// Finds a cpal config that supports 32-bit floating-point sample formats, and a sample rate of `SAMPLE_RATE`.
130fn desired_config(cfg: &SupportedStreamConfigRange) -> bool {
131    cfg.channels() == 2
132        && cfg.sample_format() == SampleFormat::F32
133        && cfg.max_sample_rate() >= SampleRate(SAMPLE_RATE.try_into().unwrap())
134}