oplon 1.0.0

A no_std, integer-only OPL2/OPL3 (Yamaha YM3812/YMF262) FM synthesis core with a clean-room, compile-time-generated ROM set
Documentation
//! Public-API integration tests: exercise `oplon::Opl2` the way an external
//! crate would.

#![forbid(unsafe_code)]
use oplon::{ChannelPatch, OpPatch, Opl2};

#[test]
fn register_level_tone_and_release() {
    let mut opl = Opl2::new(44_100);
    for (r, v) in [
        (0x20u8, 0x21u8), // mod: EG-type sustaining, mult=1
        (0x23, 0x21),     // car: EG-type sustaining, mult=1
        (0x40, 0x3F),     // modulator silent
        (0x43, 0x00),     // carrier loud
        (0x60, 0xF0),     // fast attack, no decay (holds loud)
        (0x63, 0xF0),
        (0x80, 0x0F), // sustain level 0, release rate 15 (fast release)
        (0x83, 0x0F),
        (0xC0, 0x00),
        (0xA0, 0x98),
        (0xB0, 0x31), // key-on, block 4
    ] {
        opl.write_reg(r, v);
    }
    let mut peak = 0i32;
    for _ in 0..8820 {
        let (l, _r) = opl.render_frame();
        peak = peak.max(l.abs());
    }
    assert!(peak > 100, "silent tone (peak={peak})");
    assert!(opl.any_active());

    // Key-off and run out the release.
    opl.write_reg(0xB0, 0x11);
    for _ in 0..88_200 {
        opl.render_frame();
        if !opl.any_active() {
            break;
        }
    }
    assert!(!opl.any_active(), "did not release to silence");
}

#[test]
fn structured_patch_api() {
    let mut opl = Opl2::new(48_000);
    let patch = ChannelPatch {
        modulator: OpPatch {
            mul: 1,
            tl: 63,
            attack: 15,
            release: 7,
            sustaining: true,
            ..Default::default()
        },
        carrier: OpPatch {
            mul: 1,
            attack: 15,
            release: 7,
            sustaining: true,
            ..Default::default()
        },
        ..Default::default()
    };
    opl.set_patch(0, &patch);
    opl.set_frequency(0, 0x2AE, 4);
    opl.key_on(0);
    let mut peak = 0i32;
    for _ in 0..4800 {
        let (l, _r) = opl.render_frame();
        peak = peak.max(l.abs());
    }
    assert!(peak > 100, "structured-patch tone silent (peak={peak})");
    assert_eq!(opl.channel_count(), oplon::OPL2_CHANNELS);
}

#[test]
fn stereo_panning_routes_per_side() {
    let mut opl = Opl2::new(44_100);
    // Loud carrier on channel 0, panned hard left.
    for (r, v) in [(0x43u8, 0x00u8), (0x63, 0xF0), (0xA0, 0x98), (0xB0, 0x31)] {
        opl.write_reg(r, v);
    }
    opl.set_pan(0, true, false);
    let (mut lpk, mut rpk) = (0i32, 0i32);
    for _ in 0..8820 {
        let (l, r) = opl.render_frame();
        lpk = lpk.max(l.abs());
        rpk = rpk.max(r.abs());
    }
    assert!(lpk > 100, "left silent (peak={lpk})");
    assert_eq!(rpk, 0, "right should be muted, got {rpk}");
}

/// Robustness: a long stream of pseudo-random register writes on both ports,
/// interleaved with renders, must never panic (index-out-of-bounds, arithmetic
/// overflow, …). The chip has to tolerate arbitrary/garbage input — a VGM or a
/// ported driver can send anything.
#[test]
fn random_register_writes_never_panic() {
    let mut s: u32 = 0xDEAD_BEEF;
    let mut rng = || {
        s = s.wrapping_mul(1664525).wrapping_add(1013904223);
        s
    };
    for &rate in &[8_000u32, 44_100, 48_000, 192_000] {
        let mut opl = Opl2::new(rate);
        for i in 0..20_000u32 {
            let r = rng();
            let (reg, val) = ((r & 0xFF) as u8, ((r >> 8) & 0xFF) as u8);
            if (r >> 16) & 1 == 0 {
                opl.write_reg(reg, val);
            } else {
                opl.write_reg_high(reg, val);
            }
            if i % 3 == 0 {
                let _ = opl.render_frame();
            }
        }
    }
}