ponko 0.0.1

A fast 2D game engine for simulating millions of things: ECS on all cores, hot reload, an in-game inspector, AI-agent control, and a C++ API.
Documentation
//! Sounds, checked headless: loading and decoding work, playing is silent.

use ponko::*;

fn in_tick(f: impl FnMut(&mut Ctx) + 'static) {
    Game::new("audio", 1, 1).sim_system(f).run_headless(1, |_| {});
}

/// A mono 16-bit PCM WAV file.
fn wav(sample_rate: u32, samples: &[i16]) -> Vec<u8> {
    let data_len = (samples.len() * 2) as u32;
    let mut out = Vec::new();
    out.extend_from_slice(b"RIFF");
    out.extend_from_slice(&(36 + data_len).to_le_bytes());
    out.extend_from_slice(b"WAVEfmt ");
    out.extend_from_slice(&16u32.to_le_bytes());
    out.extend_from_slice(&1u16.to_le_bytes()); // PCM
    out.extend_from_slice(&1u16.to_le_bytes()); // mono
    out.extend_from_slice(&sample_rate.to_le_bytes());
    out.extend_from_slice(&(sample_rate * 2).to_le_bytes());
    out.extend_from_slice(&2u16.to_le_bytes());
    out.extend_from_slice(&16u16.to_le_bytes());
    out.extend_from_slice(b"data");
    out.extend_from_slice(&data_len.to_le_bytes());
    for s in samples {
        out.extend_from_slice(&s.to_le_bytes());
    }
    out
}

fn temp_file(name: &str, bytes: &[u8]) -> String {
    let path = std::env::temp_dir().join(format!("crabby-audio-{}-{name}", std::process::id()));
    std::fs::write(&path, bytes).unwrap();
    path.to_string_lossy().into_owned()
}

#[test]
fn generated_sounds_know_their_length() {
    in_tick(|ctx| {
        let beep = ctx.create_sound(8000, &vec![0.0; 4000]);
        assert!((beep.seconds() - 0.5).abs() < 1e-3, "{}", beep.seconds());
        let playing = ctx.play_sound(&beep);
        ctx.stop_sound(playing); // silent headless, but fine to call
    });
}

#[test]
fn wav_files_load() {
    let path = temp_file("tone.wav", &wav(22_050, &vec![1000; 22_050 / 4]));
    in_tick(move |ctx| {
        let tone = ctx.load_sound(&path);
        assert!((tone.seconds() - 0.25).abs() < 0.01, "{}", tone.seconds());
        ctx.play_sound_ex(
            &tone,
            SoundParams {
                volume: 0.5,
                speed: 2.0,
                looping: true,
            },
        );
    });
}

#[test]
#[should_panic(expected = "could not decode the sound")]
fn files_that_are_not_sound_say_so() {
    let path = temp_file("bad.ogg", b"this is not audio");
    Game::new("audio", 1, 1).run_headless(0, move |ctx| {
        ctx.load_sound(&path);
    });
}

#[test]
#[should_panic(expected = "not found in assets/")]
fn missing_sounds_say_where_they_looked() {
    Game::new("audio", 1, 1).run_headless(0, |ctx| {
        ctx.load_sound("no-such-sound.ogg");
    });
}

#[cfg(feature = "audio")]
#[test]
fn loading_a_sound_file_twice_gives_the_same_sound() {
    let path = temp_file("twice.wav", &wav(22_050, &vec![1000; 2205]));
    in_tick(move |ctx| {
        let a = ctx.load_sound(&path);
        let b = ctx.load_sound(&path);
        assert_eq!(a, b);
        assert_ne!(a, ctx.create_sound(8000, &[0.0; 80]));
    });
}