灵符lingfu

ling-lang

↓ 0 downloads

Ling - The Omniglot Systems Language

Keywords systems, polyglot, ai, crypto, game
lingfu install ling-lang
LicenseApache-2.0 OR MIT
AuthorsLing Lin, Sanny Lin
READMECode / FilesVersionsDependency GraphDependentsSecurityBuildsSettings

Code / Files (995)

crates/ling-kernel/src/drivers/mixer.rs
//! OS audio mixer + pentatonic synthesizer -- the audio layer between the
//! AC'97 DMA ring (`ac97.rs`) and everything that makes sound.
//!
//! Model, per the design request: Windows-audio-shaped, deliberately NOT a
//! JACK-style routing graph. Every app (window kind / shell / player) owns
//! one implicit stream with its own volume; all active streams are
//! software-mixed into the one hardware ring ("shared mode"). One stream
//! may claim *exclusive mode* (ASIO-flavored takeover): while claimed,
//! only that stream is audible -- others keep rendering into silence
//! rather than blocking, so releasing exclusivity is glitchless.
//!
//! The synthesizer is real synthesis, not samples: pentatonic-tuned
//! (C-major pentatonic, two octaves) triangle/sine voices with attack/
//! exponential-decay envelopes, mixed at the AC'97's native 48 kHz. The
//! sine is Bhaskara's 7th-century approximation (no libm in no_std;
//! max error ~1.8% -- inaudible for chimes). "Sound themes" are named
//! per-event note sequences (boot/click/open/close/error/login), all in
//! the same pentatonic tuning so nothing can sound dissonant next to
//! anything else -- the property the pentatonic scale was chosen for.
//!
//! The tape player ("play" in lsh) is a stream like any other: a seeded
//! generative pentatonic track. Separately, a real *PCM sample* source
//! (`pcm_*`) plays actual WAV audio from lingfs -- now that multi-block
//! files exist, a real song fits -- resampling to 48 kHz and mixing it in
//! alongside the synth voices. That's what the desktop Media player uses.

use crate::drivers::ac97;

pub const SAMPLE_RATE: u32 = 48_000;

// -- Streams (one per app) --------------------------------------------------

pub const APP_SYSTEM: usize = 0; // jingles, UI feedback
pub const APP_SHELL: usize = 1; // lsh beeps
pub const APP_PLAYER: usize = 2; // the tape player
pub const APP_ABOUT: usize = 3;
pub const APP_SETTINGS: usize = 4;
pub const APP_FILES: usize = 5;
pub const APP_DAW: usize = 6; // the LingStation window
pub const NUM_STREAMS: usize = 7;

static STREAM_NAMES: [&str; NUM_STREAMS] =
    ["system", "shell", "player", "about", "settings", "files", "lingstation"];

static mut STREAM_VOL: [u32; NUM_STREAMS] = [80; NUM_STREAMS];
static mut MASTER_VOL: u32 = 80;
static mut EXCLUSIVE: i32 = -1;

pub fn stream_count() -> usize {
    NUM_STREAMS
}

pub fn stream_name(i: usize) -> &'static str {
    STREAM_NAMES.get(i).copied().unwrap_or("")
}

pub fn stream_volume(i: usize) -> u32 {
    unsafe { STREAM_VOL.get(i).copied().unwrap_or(0) }
}

pub fn set_stream_volume(i: usize, vol: u32) {
    unsafe {
        if i < NUM_STREAMS {
            STREAM_VOL[i] = vol.min(100);
        }
    }
}

pub fn master_volume() -> u32 {
    unsafe { MASTER_VOL }
}

pub fn set_master_volume(vol: u32) {
    unsafe { MASTER_VOL = vol.min(100) };
}

/// Claim exclusive mode for a stream (ASIO-style takeover): every other
/// stream renders but is muted at the mix stage. Returns false if a
/// different stream already holds it.
pub fn exclusive_claim(i: usize) -> bool {
    unsafe {
        if EXCLUSIVE >= 0 && EXCLUSIVE != i as i32 {
            return false;
        }
        EXCLUSIVE = i as i32;
        true
    }
}

pub fn exclusive_release(i: usize) {
    unsafe {
        if EXCLUSIVE == i as i32 {
            EXCLUSIVE = -1;
        }
    }
}

pub fn exclusive_holder() -> i32 {
    unsafe { EXCLUSIVE }
}

// -- Synth voices ------------------------------------------------------------

const MAX_VOICES: usize = 12;

#[derive(Clone, Copy)]
struct Voice {
    active: bool,
    stream: usize,
    phase: f64,      // 0..1
    phase_inc: f64,  // freq / SAMPLE_RATE
    level: f64,      // current envelope level 0..1
    attack: f64,     // per-sample level increment while attacking
    decay: f64,      // per-sample multiplicative decay after attack
    attacking: bool,
    sine: bool,      // false = triangle
    /// Samples until this voice starts (scheduler delay for jingle notes).
    delay: u32,
    /// Stereo pan: -1.0 hard left, 0 center, +1.0 hard right.
    pan: f64,
}

const SILENT_VOICE: Voice = Voice {
    active: false,
    stream: 0,
    phase: 0.0,
    phase_inc: 0.0,
    level: 0.0,
    attack: 0.0,
    decay: 0.0,
    attacking: false,
    sine: false,
    delay: 0,
    pan: 0.0,
};

static mut VOICES: [Voice; MAX_VOICES] = [SILENT_VOICE; MAX_VOICES];

// Downmix everything to centre (Settings > Audio > Channels = Mono).
static mut MONO: bool = false;
// Output level meter (0..100 per channel), fast attack / slow decay.
static mut PEAK_L: u32 = 0;
static mut PEAK_R: u32 = 0;

pub fn set_mono(m: bool) {
    unsafe { MONO = m };
}
pub fn mono() -> bool {
    unsafe { MONO }
}
pub fn output_level_l() -> u32 {
    unsafe { PEAK_L }
}
pub fn output_level_r() -> u32 {
    unsafe { PEAK_R }
}

// Honest device facts (the AC'97 driver is fixed 48kHz/16-bit stereo, PCM-out
// only -- see its module doc), surfaced read-only in the Audio settings tab.
pub fn sample_rate() -> u32 {
    SAMPLE_RATE
}
pub fn bit_depth() -> u32 {
    16
}
pub fn buffer_frames() -> u32 {
    ac97::FRAMES_PER_BUFFER as u32
}
pub fn ring_buffers() -> u32 {
    ac97::NUM_BUFFERS as u32
}
pub fn has_capture() -> bool {
    false
}

/// Play a short test ding panned to one side: -1 left, 0 centre, +1 right.
pub fn test_tone(pan: i32) {
    let p = pan.clamp(-1, 1) as f64;
    unsafe {
        let voices = &mut *&raw mut VOICES;
        let Some(v) = voices.iter_mut().find(|v| !v.active) else { return };
        let dur_samples = 0.28 * SAMPLE_RATE as f64;
        *v = Voice {
            active: true,
            stream: APP_SETTINGS,
            phase: 0.0,
            phase_inc: 880.0 / SAMPLE_RATE as f64, // A5
            level: 0.0,
            attack: 1.0 / (0.004 * SAMPLE_RATE as f64),
            decay: libm_free_exp_decay(dur_samples),
            attacking: true,
            sine: true,
            delay: 0,
            pan: p,
        };
    }
}

/// Bhaskara I's sine approximation over 0..pi, mirrored for the full
/// cycle. Input phase 0..1, output -1..1.
fn fast_sin(phase: f64) -> f64 {
    let (p, sign) = if phase < 0.5 { (phase * 2.0, 1.0) } else { ((phase - 0.5) * 2.0, -1.0) };
    // x in 0..pi expressed via p in 0..1: sin = 16p(1-p) / (5 - 4p(1-p))
    let t = p * (1.0 - p);
    sign * (16.0 * t) / (5.0 - 4.0 * t)
}

fn triangle(phase: f64) -> f64 {
    if phase < 0.25 {
        phase * 4.0
    } else if phase < 0.75 {
        2.0 - phase * 4.0
    } else {
        phase * 4.0 - 4.0
    }
}

/// Start one voice. `freq_centihz` is Hz*100 (flat-u64 FFI has no floats),
/// `dur_ms` shapes the decay so the note audibly ends around then,
/// `delay_ms` schedules it into the future (jingle sequencing).
pub fn note(stream: usize, freq_centihz: u32, dur_ms: u32, delay_ms: u32, sine: bool) {
    let freq = freq_centihz as f64 / 100.0;
    if freq <= 0.0 {
        return;
    }
    unsafe {
        let voices = &mut *&raw mut VOICES;
        let Some(v) = voices.iter_mut().find(|v| !v.active) else { return };
        // Decay constant: level falls to ~1% over dur_ms.
        let dur_samples = (dur_ms.max(30) as f64 / 1000.0) * SAMPLE_RATE as f64;
        *v = Voice {
            active: true,
            stream: stream.min(NUM_STREAMS - 1),
            phase: 0.0,
            phase_inc: freq / SAMPLE_RATE as f64,
            level: 0.0,
            attack: 1.0 / (0.004 * SAMPLE_RATE as f64), // 4ms attack
            decay: libm_free_exp_decay(dur_samples),
            attacking: true,
            sine,
            delay: (delay_ms as u64 * SAMPLE_RATE as u64 / 1000) as u32,
            pan: 0.0,
        };
    }
}

/// decay-per-sample d with d^n = 0.01 for n = dur_samples, computed
/// without libm: d = exp(ln(0.01)/n), approximated via (1 + x/k)^k with
/// enough halvings -- cheap, once per note.
fn libm_free_exp_decay(dur_samples: f64) -> f64 {
    let x = -4.605_170_186 / dur_samples; // ln(0.01)
    // exp(x) ~ (1 + x/64)^64 for the small negative x this sees.
    let mut base = 1.0 + x / 64.0;
    for _ in 0..6 {
        base *= base;
    }
    base.clamp(0.0, 0.999_999)
}

// -- Pentatonic scale + sound themes ----------------------------------------

/// C-major pentatonic, C4..A5, in centihertz. Every jingle and the
/// generative tape track index into this -- one tuning everywhere.
pub const PENTATONIC_CENTIHZ: [u32; 10] = [
    26163, 29366, 32963, 39200, 44000, // C4 D4 E4 G4 A4
    52325, 58733, 65925, 78399, 88000, // C5 D5 E5 G5 A5
];

pub const EVENT_BOOT: usize = 0;
pub const EVENT_CLICK: usize = 1;
pub const EVENT_OPEN: usize = 2;
pub const EVENT_CLOSE: usize = 3;
pub const EVENT_ERROR: usize = 4;
pub const EVENT_LOGIN: usize = 5;
pub const EVENT_PING: usize = 6; // Messenger attention nudge

/// One jingle note: (pentatonic index, start delay ms, duration ms).
type JingleNote = (usize, u32, u32);

struct SoundTheme {
    name: &'static str,
    /// Indexed by EVENT_*; a slice of scheduled notes (empty = silent).
    events: [&'static [JingleNote]; 7],
}

static SOUND_THEMES: [SoundTheme; 3] = [
    SoundTheme {
        name: "Dawn Chimes",
        events: [
            // boot: rising C-E-G-A-C5 sparkle
            &[(0, 0, 350), (2, 120, 350), (3, 240, 350), (4, 360, 400), (5, 520, 700)],
            &[(7, 0, 60)],                            // click: short high tick
            &[(3, 0, 140), (5, 70, 220)],             // open: two rising
            &[(5, 0, 140), (3, 70, 220)],             // close: two falling
            &[(1, 0, 260), (1, 180, 380)],            // error: low double-knock
            &[(2, 0, 200), (4, 130, 200), (7, 260, 500)], // login: triad up
            &[(8, 0, 130), (9, 110, 260)],            // ping: bright quick two-note
        ],
    },
    SoundTheme {
        name: "Jade Bells",
        events: [
            &[(4, 0, 500), (7, 200, 500), (9, 400, 800)],
            &[(9, 0, 50)],
            &[(5, 0, 180), (8, 90, 260)],
            &[(8, 0, 180), (5, 90, 260)],
            &[(0, 0, 300), (0, 220, 420)],
            &[(4, 0, 240), (7, 150, 240), (9, 300, 600)],
            &[(9, 0, 140), (9, 120, 280)],
        ],
    },
    SoundTheme {
        name: "Silent",
        events: [&[], &[], &[], &[], &[], &[], &[]],
    },
];

static mut SOUND_THEME: usize = 0;

pub fn sound_theme_count() -> usize {
    SOUND_THEMES.len()
}

pub fn sound_theme() -> usize {
    unsafe { SOUND_THEME }
}

pub fn set_sound_theme(i: usize) {
    unsafe { SOUND_THEME = i % SOUND_THEMES.len() };
}

pub fn sound_theme_name(i: usize) -> &'static str {
    SOUND_THEMES.get(i).map(|t| t.name).unwrap_or("")
}

/// Fire one UI sound event through the active sound theme (system stream,
/// shared mode -- a click never needs exclusivity).
pub fn jingle(event: usize) {
    let theme = &SOUND_THEMES[unsafe { SOUND_THEME } % SOUND_THEMES.len()];
    let Some(notes) = theme.events.get(event) else { return };
    for &(pent, delay, dur) in notes.iter() {
        note(APP_SYSTEM, PENTATONIC_CENTIHZ[pent % 10], dur, delay, event == EVENT_BOOT);
    }
}

// -- Tape player (generative pentatonic track) -------------------------------

pub const PLAYER_STOPPED: u32 = 0;
pub const PLAYER_PLAYING: u32 = 1;
pub const PLAYER_PAUSED: u32 = 2;

static mut PLAYER_STATE: u32 = PLAYER_STOPPED;
static mut PLAYER_POS_SAMPLES: u64 = 0;
static mut PLAYER_LEN_SAMPLES: u64 = 0;
static mut PLAYER_SEED: u64 = 0;
static mut PLAYER_NEXT_NOTE_AT: u64 = 0;
static mut PLAYER_RNG: u64 = 0;

/// Load a generative track: `seed` picks the melody, `len_ms` its length.
/// Real state for a real transport -- position advances only while
/// samples are actually rendered into the ring.
pub fn player_load(seed: u64, len_ms: u32) {
    unsafe {
        PLAYER_SEED = seed;
        PLAYER_RNG = seed | 1;
        PLAYER_POS_SAMPLES = 0;
        PLAYER_NEXT_NOTE_AT = 0;
        PLAYER_LEN_SAMPLES = len_ms as u64 * SAMPLE_RATE as u64 / 1000;
        PLAYER_STATE = PLAYER_STOPPED;
    }
}

pub fn player_play() {
    unsafe {
        if PLAYER_LEN_SAMPLES > 0 {
            PLAYER_STATE = PLAYER_PLAYING;
        }
    }
}

pub fn player_pause() {
    unsafe {
        if PLAYER_STATE == PLAYER_PLAYING {
            PLAYER_STATE = PLAYER_PAUSED;
        } else if PLAYER_STATE == PLAYER_PAUSED {
            PLAYER_STATE = PLAYER_PLAYING;
        }
    }
}

pub fn player_stop() {
    unsafe {
        PLAYER_STATE = PLAYER_STOPPED;
        PLAYER_POS_SAMPLES = 0;
        PLAYER_NEXT_NOTE_AT = 0;
        PLAYER_RNG = PLAYER_SEED | 1;
    }
}

pub fn player_state() -> u32 {
    unsafe { PLAYER_STATE }
}

pub fn player_pos_ms() -> u64 {
    unsafe { PLAYER_POS_SAMPLES * 1000 / SAMPLE_RATE as u64 }
}

pub fn player_len_ms() -> u64 {
    unsafe { PLAYER_LEN_SAMPLES * 1000 / SAMPLE_RATE as u64 }
}

static mut LINE_BUF: [u8; 64] = [0; 64];

/// The tape deck's one-line ASCII transport, built kernel-side (a
/// variable-length bar is a loop `.ling` can't write):
/// `[>] |######--------------| 00:12 / 01:00`. State glyph: `>` playing,
/// `=` paused, `.` stopped. Redraw with `\r` for the in-place effect.
pub fn player_line() -> &'static str {
    let pos = player_pos_ms();
    let len = player_len_ms().max(1);
    let filled = ((pos * 20) / len).min(20) as usize;
    let state = player_state();
    unsafe {
        let buf = &mut *&raw mut LINE_BUF;
        let mut n = 0;
        buf[n] = b'[';
        buf[n + 1] = match state {
            PLAYER_PLAYING => b'>',
            PLAYER_PAUSED => b'=',
            _ => b'.',
        };
        buf[n + 2] = b']';
        buf[n + 3] = b' ';
        buf[n + 4] = b'|';
        n += 5;
        for i in 0..20 {
            buf[n + i] = if i < filled { b'#' } else { b'-' };
        }
        n += 20;
        buf[n] = b'|';
        buf[n + 1] = b' ';
        n += 2;
        for (t, sep) in [(pos, true), (len, false)] {
            let total_s = t / 1000;
            let (m, s) = (total_s / 60, total_s % 60);
            buf[n] = b'0' + ((m / 10) % 10) as u8;
            buf[n + 1] = b'0' + (m % 10) as u8;
            buf[n + 2] = b':';
            buf[n + 3] = b'0' + (s / 10) as u8;
            buf[n + 4] = b'0' + (s % 10) as u8;
            n += 5;
            if sep {
                buf[n] = b' ';
                buf[n + 1] = b'/';
                buf[n + 2] = b' ';
                n += 3;
            }
        }
        core::str::from_utf8(&buf[..n]).unwrap_or("")
    }
}

fn player_advance(frames: usize) {
    unsafe {
        if PLAYER_STATE != PLAYER_PLAYING {
            return;
        }
        // Schedule melody notes as playback crosses eighth-note boundaries
        // (120 BPM -> 250ms per eighth). xorshift picks pentatonic steps;
        // same seed = same melody, honestly reproducible.
        let step = SAMPLE_RATE as u64 / 4;
        let end = PLAYER_POS_SAMPLES + frames as u64;
        while PLAYER_NEXT_NOTE_AT < end {
            PLAYER_RNG ^= PLAYER_RNG << 13;
            PLAYER_RNG ^= PLAYER_RNG >> 7;
            PLAYER_RNG ^= PLAYER_RNG << 17;
            let r = PLAYER_RNG;
            // Rest ~1/4 of the time; otherwise a note, longer on beats.
            if r % 4 != 0 {
                let idx = (r >> 8) as usize % 10;
                let on_beat = (PLAYER_NEXT_NOTE_AT / step) % 4 == 0;
                let dur = if on_beat { 420 } else { 200 };
                note(APP_PLAYER, PENTATONIC_CENTIHZ[idx], dur, 0, on_beat);
            }
            PLAYER_NEXT_NOTE_AT += step;
        }
        PLAYER_POS_SAMPLES = end;
        if PLAYER_POS_SAMPLES >= PLAYER_LEN_SAMPLES {
            player_stop();
        }
    }
}

// -- The mix ------------------------------------------------------------------

// -- PCM sample source (real WAV playback) ----------------------------------
// Plays raw PCM from a caller-owned buffer (media.rs's lingfs read buffer),
// resampled to 48 kHz and summed into the mix on the player stream. u8 and
// s16le, mono or stereo. Fractional resampling via a 16.16 cursor.
pub const PCM_STOPPED: u32 = 0;
pub const PCM_PLAYING: u32 = 1;
pub const PCM_PAUSED: u32 = 2;

static mut PCM_PTR: *const u8 = core::ptr::null();
static mut PCM_FRAMES: u64 = 0; // total source frames
static mut PCM_RATE: u32 = 22050;
static mut PCM_CH: u8 = 1;
static mut PCM_BITS: u8 = 8;
static mut PCM_POS: u64 = 0; // 16.16 fixed-point source frame index
static mut PCM_STEP: u64 = 0; // src frames per output frame, 16.16
static mut PCM_STATE: u32 = PCM_STOPPED;
static mut PCM_LOOP: bool = false; // wrap to start at end instead of stopping

/// Point the PCM player at a raw sample buffer. `frames` is the number of
/// whole sample frames (already accounting for channels). The buffer must
/// outlive playback (media.rs keeps it in a static). Does not start.
pub fn pcm_load(ptr: *const u8, frames: u64, rate: u32, channels: u8, bits: u8) {
    unsafe {
        PCM_PTR = ptr;
        PCM_FRAMES = frames;
        PCM_RATE = rate.max(1);
        PCM_CH = channels.max(1);
        PCM_BITS = if bits == 16 { 16 } else { 8 };
        PCM_POS = 0;
        PCM_STEP = ((rate as u64) << 16) / SAMPLE_RATE as u64;
        PCM_STATE = PCM_STOPPED;
        PCM_LOOP = false;
    }
}

/// Enable/disable seamless looping (wrap to the start at end-of-buffer rather
/// than stopping). Used by the boot/startup music.
pub fn pcm_set_loop(on: bool) {
    unsafe { PCM_LOOP = on };
}

pub fn pcm_play() {
    unsafe {
        if !PCM_PTR.is_null() && PCM_FRAMES > 0 {
            PCM_STATE = PCM_PLAYING;
        }
    }
}
pub fn pcm_pause() {
    unsafe {
        PCM_STATE = match PCM_STATE {
            PCM_PLAYING => PCM_PAUSED,
            PCM_PAUSED => PCM_PLAYING,
            s => s,
        };
    }
}
pub fn pcm_stop() {
    unsafe {
        PCM_STATE = PCM_STOPPED;
        PCM_POS = 0;
    }
}
pub fn pcm_state() -> u32 {
    unsafe { PCM_STATE }
}
pub fn pcm_pos_ms() -> u64 {
    unsafe { (PCM_POS >> 16) * 1000 / PCM_RATE as u64 }
}
pub fn pcm_len_ms() -> u64 {
    unsafe { PCM_FRAMES * 1000 / PCM_RATE as u64 }
}

/// One source sample (mono-summed, -1.0..1.0) at the current cursor, or 0
/// if not playing / past the end. Advances the cursor by one output frame.
unsafe fn pcm_next() -> f64 {
    if PCM_STATE != PCM_PLAYING || PCM_PTR.is_null() {
        return 0.0;
    }
    let mut idx = PCM_POS >> 16;
    if idx >= PCM_FRAMES {
        if PCM_LOOP && PCM_FRAMES > 0 {
            PCM_POS = 0;
            idx = 0;
        } else {
            PCM_STATE = PCM_STOPPED;
            PCM_POS = 0;
            return 0.0;
        }
    }
    let ch = PCM_CH as u64;
    let bytes_per_sample = (PCM_BITS / 8) as u64;
    let frame_bytes = ch * bytes_per_sample;
    let base = (idx * frame_bytes) as usize;
    let mut sum = 0.0f64;
    for c in 0..ch as usize {
        let off = base + c * bytes_per_sample as usize;
        let v = if PCM_BITS == 16 {
            let lo = *PCM_PTR.add(off) as i16;
            let hi = *PCM_PTR.add(off + 1) as i16;
            let s = (lo | (hi << 8)) as i16;
            s as f64 / 32768.0
        } else {
            // u8 PCM is unsigned, centered at 128.
            (*PCM_PTR.add(off) as f64 - 128.0) / 128.0
        };
        sum += v;
    }
    PCM_POS += PCM_STEP;
    sum / ch as f64
}

/// Render `buf` (interleaved stereo i16) from all active voices, applying
/// per-stream volume, exclusive mode, and master volume.
fn render(buf: &mut [i16]) {
    let frames = buf.len() / 2;
    player_advance(frames);
    unsafe {
        let voices = &mut *&raw mut VOICES;
        let excl = EXCLUSIVE;
        let mono = MONO;
        let mut buf_peak_l = 0i16;
        let mut buf_peak_r = 0i16;
        // the LingStation sequencer renders a whole block at once; it is None (free) while nothing plays
        let daw = crate::drivers::daw::block(frames);
        let daw_gain = if excl >= 0 && excl as usize != APP_DAW { 0.0 } else { STREAM_VOL[APP_DAW] as f64 / 100.0 };
        // LingStationX (the compiled app) playing a project through lingdaw;
        // silenced while another app holds exclusive output.
        let lsx = crate::lingdaw::block(frames);
        let lsx_gain = if excl >= 0 { 0.0 } else { 1.0 };
        // Programs' sound effects and music (drivers::sfx).
        let mut fx: alloc::vec::Vec<i16> = alloc::vec![0i16; frames * 2];
        if crate::drivers::sfx::active() {
            crate::drivers::sfx::mix_into(&mut fx);
        }
        for f in 0..frames {
            let mut accl = 0.0f64;
            let mut accr = 0.0f64;
            if let Some(d) = lsx {
                accl += d[f * 2] as f64 / 32768.0 * lsx_gain;
                accr += d[f * 2 + 1] as f64 / 32768.0 * lsx_gain;
            }
            accl += fx[f * 2] as f64 / 32768.0 * lsx_gain;
            accr += fx[f * 2 + 1] as f64 / 32768.0 * lsx_gain;
            for v in voices.iter_mut() {
                if !v.active {
                    continue;
                }
                if v.delay > 0 {
                    // Scheduled note not yet due -- burn its delay one
                    // sample at a time so jingle timing is sample-exact.
                    v.delay -= 1;
                    continue;
                }
                let s = if v.sine { fast_sin(v.phase) } else { triangle(v.phase) };
                v.phase += v.phase_inc;
                if v.phase >= 1.0 {
                    v.phase -= 1.0;
                }
                if v.attacking {
                    v.level += v.attack;
                    if v.level >= 1.0 {
                        v.level = 1.0;
                        v.attacking = false;
                    }
                } else {
                    v.level *= v.decay;
                    if v.level < 0.001 {
                        v.active = false;
                        continue;
                    }
                }
                let stream_gain = if excl >= 0 && excl as usize != v.stream {
                    0.0
                } else {
                    STREAM_VOL[v.stream] as f64 / 100.0
                };
                let contrib = s * v.level * stream_gain * 0.22; // headroom for ~4 voices
                // Pan: centre stays full-volume on both channels; extremes hard
                // pan. Mono forces centre.
                let pan = if mono { 0.0 } else { v.pan };
                let (lg, rg) = if pan <= 0.0 { (1.0, 1.0 + pan) } else { (1.0 - pan, 1.0) };
                accl += contrib * lg;
                accr += contrib * rg;
            }
            if let Some(d) = daw {
                if f * 2 + 1 < d.len() {
                    accl += d[f * 2] as f64 / 32768.0 * daw_gain;
                    accr += d[f * 2 + 1] as f64 / 32768.0 * daw_gain;
                }
            }
            // Real PCM (WAV) on the player stream, honoring exclusive mode.
            let pcm = pcm_next();
            if pcm != 0.0 {
                let pgain = if excl >= 0 && excl as usize != APP_PLAYER {
                    0.0
                } else {
                    STREAM_VOL[APP_PLAYER] as f64 / 100.0
                };
                accl += pcm * pgain * 0.9;
                accr += pcm * pgain * 0.9;
            }
            let master = MASTER_VOL as f64 / 100.0;
            let sl = (accl * master * 32767.0).clamp(-32767.0, 32767.0) as i16;
            let sr = (accr * master * 32767.0).clamp(-32767.0, 32767.0) as i16;
            buf[f * 2] = sl;
            buf[f * 2 + 1] = sr;
            if sl.abs() > buf_peak_l {
                buf_peak_l = sl.abs();
            }
            if sr.abs() > buf_peak_r {
                buf_peak_r = sr.abs();
            }
        }
        // Update the meter: instant rise to this buffer's peak, slow fall.
        let tl = (buf_peak_l as u32 * 100) / 32767;
        let tr = (buf_peak_r as u32 * 100) / 32767;
        PEAK_L = tl.max(PEAK_L.saturating_sub(4));
        PEAK_R = tr.max(PEAK_R.saturating_sub(4));
    }
}

/// Initialize the hardware; true if an AC'97 device exists.
pub fn init() -> bool {
    ac97::init()
}

/// Keep the DMA ring fed -- call once per WM frame / shell-loop iteration.
/// Cheap no-op when the ring is already full or no device exists.
pub fn pump() {
    if !ac97::present() {
        return;
    }
    ac97::pump(&mut render);
}

← Back to index