灵符lingfu
L

ling-daw

↓ 0 downloads

Ling DAW engine: synth, physical-modeling drums, effects, mixer, sequencer, project I/O

lingfu install ling-daw
LicenseApache-2.0 OR MIT
AuthorsLing Lin <taellinglin@gmail.com>, Sanny Lin <SannyLing53@gmail.com>
READMECode / FilesVersionsDependency GraphDependentsSecurityBuildsSettings

Code / Files (91)

src/tracker.rs
//! Tracker module import: ProTracker / FastTracker / ScreamTracker / Impulse Tracker / OpenMPT files (`.mod .s3m .xm .it .mptm`).
//!
//! A module is a set of samples and a grid of patterns. The importer keeps both:
//!
//! * every sample (8 / 16-bit, mono, stereo mixed down, IT214 compressed too) goes into a SoundFont 2 file written next to the project, one
//!   preset per tracker instrument (note-to-sample maps become key zones, loops loop, the fade-out becomes the release), so the DAW's own
//!   SoundFont player plays the module's real sounds;
//! * the patterns are played through in song order (position jumps and pattern breaks followed, repeats cut off) and every note becomes a
//!   MIDI-style note on the track of its instrument, with its volume, its panning, its length (until the next note on that channel, a
//!   note-off, or a note cut) and the song's tempo changes.
//!
//! Pitch bends, vibrato, arpeggios, sample offsets and volume slides are not modelled (they are counted in the report). OpenMPT's
//! `.mptm` is Impulse Tracker's format with extensions and reads as `.it`; MO3 (compressed) is not supported.
use crate::midi_import::{ImportedMidi, ImportedTrack};
use crate::dsp::sampler::{Config, Zone};
use crate::model::Note;
use std::collections::HashSet;

// ── bytes ─────────────────────────────────────────────────────────────────────

struct Rd<'a> {
    d: &'a [u8],
}
impl Rd<'_> {
    fn u8(&self, o: usize) -> u8 {
        self.d.get(o).copied().unwrap_or(0)
    }
    fn u16(&self, o: usize) -> u16 {
        self.u8(o) as u16 | (self.u8(o + 1) as u16) << 8
    }
    fn u16be(&self, o: usize) -> u16 {
        (self.u8(o) as u16) << 8 | self.u8(o + 1) as u16
    }
    fn u32(&self, o: usize) -> u32 {
        self.u16(o) as u32 | (self.u16(o + 2) as u32) << 16
    }
    fn i8(&self, o: usize) -> i8 {
        self.u8(o) as i8
    }
    fn text(&self, o: usize, n: usize) -> String {
        let end = (o + n).min(self.d.len());
        if o >= end {
            return String::new();
        }
        let s: String = self.d[o..end].iter().take_while(|b| **b != 0).map(|b| if *b >= 32 && *b < 127 { *b as char } else { ' ' }).collect();
        s.trim().to_string()
    }
    fn slice(&self, o: usize, n: usize) -> &[u8] {
        let a = o.min(self.d.len());
        &self.d[a..(a + n).min(self.d.len())]
    }
}

// ── the module ────────────────────────────────────────────────────────────────

#[derive(Clone, Debug, Default)]
pub struct Smp {
    pub name: String,
    /// mono, 16-bit
    pub data: Vec<i16>,
    /// sample rate at the sample's own pitch (the root note)
    pub rate: u32,
    pub loop_start: u32,
    pub loop_end: u32,
    pub looped: bool,
    /// default volume 0..64
    pub volume: u8,
    pub pan: Option<f32>,
    /// fine tuning in cents
    pub cents: f32,
    /// semitones the sample sounds higher than its root (XM relative note)
    pub rel_note: i32,
    /// the loop runs forward and back
    pub pingpong: bool,
    /// the sample's own global volume in dB (IT)
    pub vol_db: f32,
}

#[derive(Clone, Debug, Default)]
pub struct Ins {
    pub name: String,
    pub smps: Vec<Smp>,
    /// MIDI key 0..127 -> index into `smps` (255 = silent)
    pub map: Vec<u8>,
    /// per key: semitones the key plays above itself (Impulse Tracker's note table)
    pub shift: Vec<i8>,
    pub fadeout_s: f32,
    /// the volume envelope: (tick, volume 0..64) points, the sustain point and whether it loops / sustains
    pub env: Vec<(u16, u8)>,
    pub env_sustain: Option<usize>,
    pub env_loop: Option<(usize, usize)>,
    /// the panning envelope (tick, -1..1) and the pitch envelope (tick, semitones) with their sustain / loop points
    pub pan_env: Vec<(u16, f32)>,
    pub pan_sustain: Option<usize>,
    pub pan_loop: Option<(usize, usize)>,
    pub pitch_env: Vec<(u16, f32)>,
    pub pitch_sustain: Option<usize>,
    pub pitch_loop: Option<(usize, usize)>,
    /// the instrument's default filter (Impulse Tracker, 0..127 each) and its envelope (factor 0..1 on the cutoff)
    pub cutoff: Option<u8>,
    pub reso: Option<u8>,
    pub filter_env: Vec<(u16, f32)>,
    pub filter_sustain: Option<usize>,
    pub filter_loop: Option<(usize, usize)>,
    /// random variation per note, 0..1: volume, pan, cutoff, resonance
    pub swing: [f32; 4],
    /// seconds for the fade-out after a key off to reach silence (0 = none)
    pub fade_s: f32,
    /// the instrument's global volume in dB (IT)
    pub gain_db: f32,
    /// the instrument's default pan (IT), which overrides the sample's
    pub pan: Option<f32>,
}

#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Fx {
    None,
    Speed(u8),
    Tempo(u8),
    Jump(u8),
    Break(u8),
    Vol(u8),
    Pan(f32),
    Cut(u8),
    Delay(u8),
    Off(u8),
    /// arpeggio: the two offsets above the note (0, 0 = use the last)
    Arp(u8, u8),
    /// pitch slide: signed units per tick (positive up); 0 = the last one
    Porta(i16),
    /// a slide done once at the start of the row
    FinePorta(i16),
    XFinePorta(i16),
    /// tone portamento towards the next note, units per tick (0 = the last)
    Tone(u8),
    Vib(u8, u8),
    FineVib(u8, u8),
    /// volume slide per tick (+ up) and once per row
    VolSlide(i8),
    FineVol(i8),
    ToneVol(i8),
    VibVol(i8),
    Trem(u8, u8),
    /// retrigger every `interval` ticks, with a volume change mode
    Retrig(u8, u8),
    Offset(u8),
    Filter(u8),
    /// pattern loop (0 = set the start row, n = repeat n times) and pattern delay (repeat the row n times)
    PatLoop(u8),
    PatDelay(u8),
}

pub const NOTE_NONE: i16 = -1;
pub const NOTE_OFF: i16 = 200;
pub const NOTE_CUT: i16 = 201;

#[derive(Clone, Copy, Debug)]
pub struct Cell {
    /// MIDI key, or NOTE_NONE / NOTE_OFF / NOTE_CUT
    pub note: i16,
    pub inst: u8,
    /// volume column 0..64, -1 = none
    pub vol: i16,
    pub pan: Option<f32>,
    pub fx: Fx,
    /// the volume column's effect (slides, vibrato, tone portamento)
    pub vfx: Fx,
}
impl Default for Cell {
    fn default() -> Cell {
        Cell { note: NOTE_NONE, inst: 0, vol: -1, pan: None, fx: Fx::None, vfx: Fx::None }
    }
}

pub struct Module {
    pub title: String,
    pub format: &'static str,
    pub nch: usize,
    pub speed: u8,
    pub tempo: u8,
    pub orders: Vec<usize>,
    /// pattern -> row -> channel
    pub patterns: Vec<Vec<Vec<Cell>>>,
    /// 1-based in the patterns: instrument i is `ins[i - 1]`
    pub ins: Vec<Ins>,
    pub chan_pan: Vec<f32>,
    /// effects that were not modelled
    pub ignored: usize,
    /// pitch slides are in even frequency steps (XM / IT linear mode) rather than in Amiga periods
    pub linear: bool,
    /// how loud the module mixes (global / mixing volume), 1 = a full-scale mix of the importer
    pub gain: f32,
}

fn one_sample_ins(s: Smp) -> Ins {
    Ins { name: s.name.clone(), smps: vec![s], map: vec![0; 128], shift: vec![0; 128], fadeout_s: 0.02, ..Ins::default() }
}

/// signed 8-bit / 16-bit PCM bytes -> i16
fn pcm(d: &[u8], bits16: bool, signed: bool) -> Vec<i16> {
    if bits16 {
        d.chunks_exact(2).map(|c| { let v = u16::from_le_bytes([c[0], c[1]]); if signed { v as i16 } else { (v ^ 0x8000) as i16 } }).collect()
    } else {
        d.iter().map(|b| { let v = if signed { *b as i8 as i16 } else { *b as i16 - 128 }; v << 8 }).collect()
    }
}

// ── detection ─────────────────────────────────────────────────────────────────

pub fn looks_like_module(d: &[u8]) -> bool {
    d.starts_with(b"Extended Module: ") || d.starts_with(b"IMPM") || (d.len() > 0x30 && &d[0x2c..0x30] == b"SCRM") || mod_channels(d).is_some()
}

fn mod_channels(d: &[u8]) -> Option<usize> {
    if d.len() < 1084 {
        return None;
    }
    let s = &d[1080..1084];
    match s {
        b"M.K." | b"M!K!" | b"FLT4" | b"4CHN" | b"N.T." => Some(4),
        b"6CHN" => Some(6),
        b"8CHN" | b"FLT8" | b"CD81" | b"OCTA" => Some(8),
        _ if s[2..4] == *b"CH" || s[2..4] == *b"CN" => {
            let n = (s[0] as char).to_digit(10)? * 10 + (s[1] as char).to_digit(10)?;
            if (2..=32).contains(&n) { Some(n as usize) } else { None }
        }
        _ => None,
    }
}

pub fn parse_module(d: &[u8]) -> Result<Module, String> {
    if d.starts_with(b"Extended Module: ") {
        parse_xm(d)
    } else if d.starts_with(b"IMPM") {
        parse_it(d)
    } else if d.len() > 0x30 && &d[0x2c..0x30] == b"SCRM" {
        parse_s3m(d)
    } else if mod_channels(d).is_some() {
        parse_mod(d)
    } else if d.starts_with(b"MO3") {
        Err("MO3 modules are compressed and cannot be read: save the module as .it or .xm in OpenMPT".into())
    } else {
        Err("not a tracker module".into())
    }
}

thread_local! {
    /// effects that were not modelled, by name (for tests and the report)
    pub static IGNORED: std::cell::RefCell<std::collections::BTreeMap<String, usize>> = std::cell::RefCell::new(std::collections::BTreeMap::new());
}
fn ign(ig: &mut usize, what: String) {
    *ig += 1;
    IGNORED.with(|m| *m.borrow_mut().entry(what).or_insert(0) += 1);
}

// ── effect decoding ───────────────────────────────────────────────────────────

fn slide_val(x: u8, y: u8) -> i8 {
    if x > 0 {
        x as i8
    } else {
        -(y as i8)
    }
}

/// ProTracker command (MOD, and the first sixteen XM effects)
fn dec_pt(cmd: u8, par: u8, ig: &mut usize) -> Fx {
    let (x, y) = (par >> 4, par & 15);
    match cmd {
        0 => {
            if par == 0 {
                Fx::None
            } else {
                Fx::Arp(x, y)
            }
        }
        1 => Fx::Porta(par as i16),
        2 => Fx::Porta(-(par as i16)),
        3 => Fx::Tone(par),
        4 => Fx::Vib(x, y),
        5 => Fx::ToneVol(slide_val(x, y)),
        6 => Fx::VibVol(slide_val(x, y)),
        7 => Fx::Trem(x, y),
        8 => Fx::Pan((par as f32 - 128.0) / 128.0),
        9 => Fx::Offset(par),
        0xA => Fx::VolSlide(slide_val(x, y)),
        0xB => Fx::Jump(par),
        0xC => Fx::Vol(par.min(64)),
        0xD => Fx::Break(x * 10 + y),
        0xE => match x {
            1 => Fx::FinePorta(y as i16),
            2 => Fx::FinePorta(-(y as i16)),
            6 => Fx::PatLoop(y),
            0xE => Fx::PatDelay(y),
            8 => Fx::Pan((y as f32 - 7.5) / 7.5),
            9 => {
                if y > 0 {
                    Fx::Retrig(y, 0)
                } else {
                    Fx::None
                }
            }
            0xA => Fx::FineVol(y as i8),
            0xB => Fx::FineVol(-(y as i8)),
            0xC => Fx::Cut(y),
            0xD => Fx::Delay(y),
            _ => {
                if par != 0 {
                    ign(ig, format!("E{:X}", x));
                }
                Fx::None
            }
        },
        0xF => {
            if par == 0 {
                Fx::None
            } else if par < 32 {
                Fx::Speed(par)
            } else {
                Fx::Tempo(par)
            }
        }
        _ => {
            ign(ig, format!("{:X}", cmd));
            Fx::None
        }
    }
}

/// FastTracker II effect (0..35 = 0-9 A-Z)
fn dec_xm(eff: u8, par: u8, ig: &mut usize) -> Fx {
    let (x, y) = (par >> 4, par & 15);
    match eff {
        0..=15 => dec_pt(eff, par, ig),
        0x14 => Fx::Off(par),
        0x1B => {
            if y > 0 {
                Fx::Retrig(y, x)
            } else {
                Fx::None
            }
        }
        0x21 => match x {
            1 => Fx::XFinePorta(y as i16),
            2 => Fx::XFinePorta(-(y as i16)),
            _ => Fx::None,
        },
        0x10 | 0x11 | 0x15 | 0x19 | 0x1D => {
            ign(ig, format!("xm{:X}", eff));
            Fx::None
        }
        _ => {
            ign(ig, format!("xm{:X}", eff));
            Fx::None
        }
    }
}

/// XM volume column: (set volume, pan, effect)
fn xm_vcol(v: u8) -> (i16, Option<f32>, Fx) {
    match v {
        0x10..=0x50 => ((v - 0x10) as i16, None, Fx::None),
        0x60..=0x6F => (-1, None, Fx::VolSlide(-((v & 15) as i8))),
        0x70..=0x7F => (-1, None, Fx::VolSlide((v & 15) as i8)),
        0x80..=0x8F => (-1, None, Fx::FineVol(-((v & 15) as i8))),
        0x90..=0x9F => (-1, None, Fx::FineVol((v & 15) as i8)),
        0xA0..=0xAF => (-1, None, Fx::Vib(v & 15, 0)),
        0xB0..=0xBF => (-1, None, Fx::Vib(0, v & 15)),
        0xC0..=0xCF => (-1, Some(((v & 15) as f32 - 7.5) / 7.5), Fx::None),
        0xF0..=0xFF => (-1, None, Fx::Tone((v & 15) * 16)),
        _ => (-1, None, Fx::None),
    }
}

/// ScreamTracker 3 / Impulse Tracker effect (A = 1 ...)
fn dec_it(cmd: u8, par: u8, it: bool, ig: &mut usize) -> Fx {
    let (x, y) = (par >> 4, par & 15);
    let dslide = |ig: &mut usize| -> Fx {
        let _ = ig;
        if par == 0 {
            Fx::VolSlide(0)
        } else if y == 0 {
            Fx::VolSlide(x as i8)
        } else if x == 0 {
            Fx::VolSlide(-(y as i8))
        } else if y == 0xF {
            Fx::FineVol(x as i8)
        } else if x == 0xF {
            Fx::FineVol(-(y as i8))
        } else {
            Fx::VolSlide(0)
        }
    };
    match cmd {
        0 => Fx::None,
        1 => {
            if par > 0 {
                Fx::Speed(par)
            } else {
                Fx::None
            }
        }
        2 => Fx::Jump(par),
        3 => Fx::Break(if it { par } else { x * 10 + y }),
        4 => dslide(ig),
        5 => {
            if par >= 0xF0 {
                Fx::FinePorta(-((par & 15) as i16))
            } else if par >= 0xE0 {
                Fx::XFinePorta(-((par & 15) as i16))
            } else {
                Fx::Porta(-(par as i16))
            }
        }
        6 => {
            if par >= 0xF0 {
                Fx::FinePorta((par & 15) as i16)
            } else if par >= 0xE0 {
                Fx::XFinePorta((par & 15) as i16)
            } else {
                Fx::Porta(par as i16)
            }
        }
        7 => Fx::Tone(par),
        8 => Fx::Vib(x, y),
        21 => Fx::FineVib(x, y),
        10 => Fx::Arp(x, y),
        11 => match dslide(ig) {
            Fx::VolSlide(v) | Fx::FineVol(v) => Fx::VibVol(v),
            f => f,
        },
        12 => match dslide(ig) {
            Fx::VolSlide(v) | Fx::FineVol(v) => Fx::ToneVol(v),
            f => f,
        },
        15 => Fx::Offset(par),
        17 => {
            if y > 0 {
                Fx::Retrig(y, x)
            } else {
                Fx::None
            }
        }
        18 => Fx::Trem(x, y),
        19 => match x {
            8 => Fx::Pan((y as f32 - 7.5) / 7.5),
            0xB => Fx::PatLoop(y),
            0xE => Fx::PatDelay(y),
            0xC => Fx::Cut(y),
            0xD => Fx::Delay(y),
            _ => {
                if par != 0 {
                    ign(ig, format!("S{:X}", x));
                }
                Fx::None
            }
        },
        20 => {
            if par >= 32 {
                Fx::Tempo(par)
            } else {
                Fx::None
            }
        }
        24 if it => Fx::Pan((par as f32 - 128.0) / 128.0),
        26 if it => Fx::Filter(par),
        _ => {
            ign(ig, format!("it{}{:02X}", (b'A' + cmd.max(1) - 1) as char, par));
            Fx::None
        }
    }
}

/// Impulse Tracker volume column: (set volume, pan, effect)
fn it_vcol(v: u8) -> (i16, Option<f32>, Fx) {
    match v {
        0..=64 => (v as i16, None, Fx::None),
        65..=74 => (-1, None, Fx::FineVol((v - 65) as i8)),
        75..=84 => (-1, None, Fx::FineVol(-((v - 75) as i8))),
        85..=94 => (-1, None, Fx::VolSlide((v - 85) as i8)),
        95..=104 => (-1, None, Fx::VolSlide(-((v - 95) as i8))),
        105..=114 => (-1, None, Fx::Porta(-(((v - 105) as i16) * 4))),
        115..=124 => (-1, None, Fx::Porta(((v - 115) as i16) * 4)),
        128..=192 => (-1, Some((v as f32 - 128.0 - 32.0) / 32.0), Fx::None),
        193..=202 => (-1, None, Fx::Tone([0, 1, 4, 8, 16, 32, 64, 96, 128, 255][(v - 193) as usize])),
        203..=212 => (-1, None, Fx::Vib(0, v - 203)),
        _ => (-1, None, Fx::None),
    }
}

// ── MOD ───────────────────────────────────────────────────────────────────────

const PT_PERIODS: [u16; 36] = [856, 808, 762, 720, 678, 640, 604, 570, 538, 508, 480, 453, 428, 404, 381, 360, 339, 320, 302, 285, 269, 254, 240, 226, 214, 202, 190, 180, 170, 160, 151, 143, 135, 127, 120, 113];

fn parse_mod(d: &[u8]) -> Result<Module, String> {
    let r = Rd { d };
    let nch = mod_channels(d).ok_or("not a MOD")?;
    let title = r.text(0, 20);
    let mut smps: Vec<Smp> = vec![];
    let mut lens = vec![];
    for i in 0..31 {
        let o = 20 + i * 30;
        let len = r.u16be(o + 22) as usize * 2;
        let ft = (r.u8(o + 24) & 15) as i32;
        let ft = if ft > 7 { ft - 16 } else { ft };
        let (ls, ll) = (r.u16be(o + 26) as usize * 2, r.u16be(o + 28) as usize * 2);
        smps.push(Smp { name: r.text(o, 22), rate: 8363, volume: r.u8(o + 25).min(64), cents: ft as f32 * 12.5, looped: ll > 2, loop_start: ls as u32, loop_end: (ls + ll) as u32, ..Smp::default() });
        lens.push(len);
    }
    let song_len = (r.u8(950) as usize).clamp(1, 128);
    let orders: Vec<usize> = (0..song_len).map(|i| r.u8(952 + i) as usize).collect();
    let npat = orders.iter().max().copied().unwrap_or(0) + 1;
    let mut patterns = vec![];
    let mut ignored = 0;
    let mut pos = 1084;
    let (mut speed, mut tempo) = (6u8, 125u8);
    for _ in 0..npat {
        let mut rows = vec![];
        for _ in 0..64 {
            let mut row = vec![];
            for _ in 0..nch {
                let (b0, b1, b2, b3) = (r.u8(pos), r.u8(pos + 1), r.u8(pos + 2), r.u8(pos + 3));
                pos += 4;
                let period = ((b0 & 15) as u16) << 8 | b1 as u16;
                let inst = (b0 & 0xf0) | (b2 >> 4);
                let (cmd, par) = (b2 & 15, b3);
                let note = if period == 0 {
                    NOTE_NONE
                } else {
                    // C-2 (period 428) plays the sample as recorded: that is MIDI 60
                    let idx = (0..36).min_by(|a, b| ((period as f32).ln() - (PT_PERIODS[*a] as f32).ln()).abs().total_cmp(&((period as f32).ln() - (PT_PERIODS[*b] as f32).ln()).abs())).unwrap_or(12);
                    48 + idx as i16
                };
                let mut c = Cell { note, inst, ..Cell::default() };
                c.fx = dec_pt(cmd, par, &mut ignored);
                if let Fx::Vol(v) = c.fx {
                    c.vol = v as i16;
                    c.fx = Fx::None;
                }
                row.push(c);
            }
            rows.push(row);
        }
        patterns.push(rows);
    }
    // sample data follows the patterns
    for (i, s) in smps.iter_mut().enumerate() {
        let n = lens[i];
        let raw = r.slice(pos, n);
        pos += n;
        s.data = pcm(raw, false, true);
        if s.data.len() <= 1 {
            s.data.clear();
        }
        s.loop_end = s.loop_end.min(s.data.len() as u32);
        if s.loop_start >= s.loop_end {
            s.looped = false;
        }
    }
    let _ = (&mut speed, &mut tempo);
    // the Amiga's panning: channels 0 and 3 left, 1 and 2 right
    let chan_pan = (0..nch).map(|c| if (c & 3) == 0 || (c & 3) == 3 { -0.5 } else { 0.5 }).collect();
    Ok(Module { title, format: "ProTracker MOD", nch, speed: 6, tempo: 125, orders, patterns, ins: smps.into_iter().map(one_sample_ins).collect(), chan_pan, ignored, linear: false, gain: 0.45 })
}

// ── S3M ───────────────────────────────────────────────────────────────────────

fn parse_s3m(d: &[u8]) -> Result<Module, String> {
    let r = Rd { d };
    let title = r.text(0, 28);
    let (ordnum, insnum, patnum) = (r.u16(0x20) as usize, r.u16(0x22) as usize, r.u16(0x24) as usize);
    let ffi = r.u16(0x2a);
    let (speed, tempo) = (r.u8(0x31).max(1), r.u8(0x32).max(32));
    let default_pan = r.u8(0x35) == 252;
    // channel settings: 0..7 left, 8..15 right, 255 unused
    let mut chan_map = vec![usize::MAX; 32];
    let mut nch = 0;
    let mut chan_pan = vec![];
    for c in 0..32 {
        let s = r.u8(0x40 + c);
        if s < 16 {
            chan_map[c] = nch;
            nch += 1;
            chan_pan.push(if s < 8 { -0.5 } else { 0.5 });
        }
    }
    let nch = nch.max(1);
    let ord_at = 0x60;
    let mut orders: Vec<usize> = vec![];
    for i in 0..ordnum {
        let o = r.u8(ord_at + i);
        if o == 255 {
            break;
        }
        if o < 254 {
            orders.push(o as usize);
        }
    }
    let ins_ptr = ord_at + ordnum;
    let pat_ptr = ins_ptr + insnum * 2;
    let mut ins = vec![];
    for i in 0..insnum {
        let o = r.u16(ins_ptr + i * 2) as usize * 16;
        if o == 0 || o + 0x50 > d.len() {
            ins.push(Ins::default());
            continue;
        }
        let kind = r.u8(o);
        let name = r.text(o + 0x30, 28);
        if kind != 1 {
            ins.push(Ins { name, ..Ins::default() });
            continue;
        }
        let memseg = (r.u8(o + 0x0d) as usize) << 16 | r.u16(o + 0x0e) as usize;
        let (len, ls, le) = (r.u32(o + 0x10) as usize, r.u32(o + 0x14), r.u32(o + 0x18));
        let (vol, flags, c2) = (r.u8(o + 0x1c), r.u8(o + 0x1f), r.u32(o + 0x20));
        let (stereo, bits16) = (flags & 2 != 0, flags & 4 != 0);
        let bytes = len * if bits16 { 2 } else { 1 } * if stereo { 2 } else { 1 };
        let raw = r.slice(memseg * 16, bytes);
        let mut data = pcm(&raw[..if stereo { raw.len() / 2 } else { raw.len() }], bits16, ffi == 1);
        data.truncate(len);
        if stereo {
            // left block then right block: keep the left
        }
        let looped = flags & 1 != 0 && le > ls && (le as usize) <= data.len() + 1;
        ins.push(one_sample_ins(Smp { name, data, rate: c2.clamp(1000, 192000), volume: vol.min(64), looped, loop_start: ls, loop_end: le.min(len as u32), pan: None, ..Smp::default() }));
    }
    let mut patterns = vec![];
    let mut ignored = 0;
    for p in 0..patnum {
        let o = r.u16(pat_ptr + p * 2) as usize * 16;
        let mut rows = vec![vec![Cell::default(); nch]; 64];
        if o != 0 && o + 2 <= d.len() {
            let end = (o + 2 + r.u16(o) as usize).min(d.len());
            let mut q = o + 2;
            let mut row = 0;
            while row < 64 && q < end {
                let what = r.u8(q);
                q += 1;
                if what == 0 {
                    row += 1;
                    continue;
                }
                let ch = (what & 31) as usize;
                let mut c = Cell::default();
                if what & 32 != 0 {
                    let (n, i) = (r.u8(q), r.u8(q + 1));
                    q += 2;
                    c.note = match n {
                        255 => NOTE_NONE,
                        254 => NOTE_CUT,
                        _ => ((n >> 4) as i16) * 12 + (n & 15) as i16,
                    };
                    c.inst = i;
                }
                if what & 64 != 0 {
                    let v = r.u8(q);
                    q += 1;
                    if v <= 64 {
                        c.vol = v as i16;
                    }
                }
                if what & 128 != 0 {
                    let (cmd, par) = (r.u8(q), r.u8(q + 1));
                    q += 2;
                    c.fx = dec_it(cmd, par, false, &mut ignored);
                }
                if ch < 32 && chan_map[ch] != usize::MAX && row < 64 {
                    rows[row][chan_map[ch]] = c;
                }
            }
        }
        patterns.push(rows);
    }
    let _ = default_pan;
    Ok(Module { title, format: "ScreamTracker S3M", nch, speed, tempo, orders, patterns, ins, chan_pan, ignored, linear: false, gain: (r.u8(0x30).min(64) as f32 / 64.0) * ((r.u8(0x33) & 127) as f32 / 128.0).max(0.1) * 1.2 })
}

// ── XM ────────────────────────────────────────────────────────────────────────

fn parse_xm(d: &[u8]) -> Result<Module, String> {
    let r = Rd { d };
    let title = r.text(17, 20);
    let hsize = r.u32(60) as usize;
    let (songlen, nch, npat, nins) = (r.u16(64) as usize, r.u16(68) as usize, r.u16(70) as usize, r.u16(72) as usize);
    let (speed, tempo) = (r.u16(76).clamp(1, 31) as u8, r.u16(78).clamp(32, 255) as u8);
    if nch == 0 || nch > 128 {
        return Err("bad XM channel count".into());
    }
    let orders: Vec<usize> = (0..songlen.min(256)).map(|i| r.u8(80 + i) as usize).filter(|p| *p < npat.max(1)).collect();
    let mut pos = 60 + hsize;
    let mut patterns = vec![];
    let mut ignored = 0;
    for _ in 0..npat {
        let hl = r.u32(pos) as usize;
        let rows_n = r.u16(pos + 5) as usize;
        let psize = r.u16(pos + 7) as usize;
        let mut q = pos + hl;
        let end = q + psize;
        let mut rows = vec![vec![Cell::default(); nch]; rows_n.min(256)];
        if psize > 0 {
            for row in rows.iter_mut() {
                for cell in row.iter_mut() {
                    if q >= end {
                        break;
                    }
                    let first = r.u8(q);
                    let (mut note, mut inst, mut vol, mut eff, mut par) = (0u8, 0u8, 0u8, 0u8, 0u8);
                    if first & 0x80 != 0 {
                        q += 1;
                        for (bit, slot) in [(1u8, &mut note), (2, &mut inst), (4, &mut vol), (8, &mut eff), (16, &mut par)] {
                            if first & bit != 0 {
                                *slot = r.u8(q);
                                q += 1;
                            }
                        }
                    } else {
                        note = first;
                        inst = r.u8(q + 1);
                        vol = r.u8(q + 2);
                        eff = r.u8(q + 3);
                        par = r.u8(q + 4);
                        q += 5;
                    }
                    let mut c = Cell { inst, ..Cell::default() };
                    c.note = match note {
                        0 => NOTE_NONE,
                        97 => NOTE_OFF,
                        n if n <= 96 => n as i16 + 11,
                        _ => NOTE_NONE,
                    };
                    let (cv, cp, cfx) = xm_vcol(vol);
                    c.vol = cv;
                    c.pan = cp;
                    c.vfx = cfx;
                    c.fx = dec_xm(eff, par, &mut ignored);
                    if let Fx::Vol(v) = c.fx {
                        c.vol = v as i16;
                        c.fx = Fx::None;
                    }
                    *cell = c;
                }
            }
        }
        pos = end;
        patterns.push(rows);
    }
    // instruments
    let mut ins = vec![];
    for _ in 0..nins {
        let isz = r.u32(pos) as usize;
        if isz < 29 || pos + isz > d.len() + 1 {
            ins.push(Ins::default());
            pos += isz.max(29);
            continue;
        }
        let name = r.text(pos + 4, 22);
        let ns = r.u16(pos + 27) as usize;
        let mut inst = Ins { name, map: vec![255; 128], shift: vec![0; 128], fadeout_s: 0.02, ..Ins::default() };
        let mut hdrs = vec![];
        if ns > 0 {
            let shs = r.u32(pos + 29) as usize;
            for k in 0..96 {
                inst.map[k + 12] = r.u8(pos + 33 + k);
            }
            let fo = r.u16(pos + 239);
            inst.fadeout_s = if fo == 0 { 0.02 } else { (32768.0 / fo as f32 * 0.02).clamp(0.02, 6.0) };
            inst.fade_s = if fo == 0 { 0.0 } else { (32768.0 / fo as f32 * 2.5 / tempo as f32).clamp(0.0, 8.0) };
            // the volume envelope: up to 12 (tick, value) points
            let (nvol, vsus, vls, vle, vtype) = (r.u8(pos + 225) as usize, r.u8(pos + 227) as usize, r.u8(pos + 228) as usize, r.u8(pos + 229) as usize, r.u8(pos + 233));
            let (npan, psus, pls, ple, ptype) = (r.u8(pos + 226) as usize, r.u8(pos + 230) as usize, r.u8(pos + 231) as usize, r.u8(pos + 232) as usize, r.u8(pos + 234));
            if ptype & 1 != 0 && npan >= 2 {
                for k in 0..npan.min(12) {
                    inst.pan_env.push((r.u16(pos + 177 + k * 4), (r.u16(pos + 179 + k * 4).min(64) as f32 - 32.0) / 32.0));
                }
                if ptype & 2 != 0 && psus < inst.pan_env.len() {
                    inst.pan_sustain = Some(psus);
                }
                if ptype & 4 != 0 && pls <= ple && ple < inst.pan_env.len() {
                    inst.pan_loop = Some((pls, ple));
                }
            }
            if vtype & 1 != 0 && nvol >= 2 {
                for k in 0..nvol.min(12) {
                    inst.env.push((r.u16(pos + 129 + k * 4), r.u16(pos + 131 + k * 4).min(64) as u8));
                }
                if vtype & 2 != 0 && vsus < inst.env.len() {
                    inst.env_sustain = Some(vsus);
                }
                if vtype & 4 != 0 && vls <= vle && vle < inst.env.len() {
                    inst.env_loop = Some((vls, vle));
                }
            }
            let mut sp = pos + isz;
            for _ in 0..ns {
                hdrs.push(sp);
                sp += shs.max(40);
            }
            pos = sp;
            for &h in &hdrs {
                let len = r.u32(h) as usize;
                let (ls, ll) = (r.u32(h + 4) as usize, r.u32(h + 8) as usize);
                let ty = r.u8(h + 14);
                let bits16 = ty & 0x10 != 0;
                let bytes = r.slice(pos, len);
                pos += len;
                // delta-coded
                let mut data: Vec<i16> = vec![];
                if bits16 {
                    let mut acc = 0i16;
                    for c in bytes.chunks_exact(2) {
                        acc = acc.wrapping_add(i16::from_le_bytes([c[0], c[1]]));
                        data.push(acc);
                    }
                } else {
                    let mut acc = 0i8;
                    for b in bytes {
                        acc = acc.wrapping_add(*b as i8);
                        data.push((acc as i16) << 8);
                    }
                }
                let div = if bits16 { 2 } else { 1 };
                let (ls, ll) = ((ls / div) as u32, (ll / div) as u32);
                let looped = ty & 3 != 0 && ll > 1;
                inst.smps.push(Smp {
                    name: r.text(h + 18, 22),
                    data,
                    rate: 8363,
                    loop_start: ls,
                    loop_end: (ls + ll).min(len as u32 / div as u32),
                    looped,
                    volume: r.u8(h + 12).min(64),
                    pan: Some((r.u8(h + 15) as f32 - 128.0) / 128.0),
                    cents: r.i8(h + 13) as f32 / 128.0 * 100.0,
                    rel_note: r.i8(h + 16) as i32,
                    pingpong: ty & 3 == 2,
                    vol_db: 0.0,
                });
            }
        } else {
            pos += isz;
        }
        ins.push(inst);
    }
    let chan_pan = vec![0.0; nch];
    let linear = r.u16(74) & 1 != 0;
    Ok(Module { title, format: "FastTracker XM", nch, speed, tempo, orders, patterns, ins, chan_pan, ignored, linear, gain: 0.38 })
}

// ── IT / MPTM ─────────────────────────────────────────────────────────────────

/// IT214 / IT215 sample decompression
fn it_decompress(src: &[u8], len: usize, bits16: bool, it215: bool) -> Vec<i16> {
    struct Bits<'a> {
        d: &'a [u8],
        pos: usize,
        buf: u32,
        n: u32,
    }
    impl Bits<'_> {
        fn read(&mut self, mut n: u32) -> u32 {
            let mut v = 0u32;
            let mut shift = 0;
            while n > 0 {
                if self.n == 0 {
                    self.buf = self.d.get(self.pos).copied().unwrap_or(0) as u32;
                    self.pos += 1;
                    self.n = 8;
                }
                let take = n.min(self.n);
                v |= (self.buf & ((1 << take) - 1)) << shift;
                self.buf >>= take;
                self.n -= take;
                shift += take;
                n -= take;
            }
            v
        }
    }
    let mut out: Vec<i16> = Vec::with_capacity(len);
    let mut p = 0usize;
    let block_max = if bits16 { 0x4000 } else { 0x8000 };
    while out.len() < len && p + 2 <= src.len() {
        let csize = src[p] as usize | (src[p + 1] as usize) << 8;
        p += 2;
        let data = &src[p.min(src.len())..(p + csize).min(src.len())];
        p += csize;
        let want = block_max.min(len - out.len());
        let mut b = Bits { d: data, pos: 0, buf: 0, n: 0 };
        let mut width: u32 = if bits16 { 17 } else { 9 };
        let (mut d1, mut d2) = (0i32, 0i32);
        let mut got = 0;
        while got < want {
            if b.pos > data.len() + 4 {
                break;
            }
            let mut v = b.read(width) as i64;
            if width < 7 {
                if v == 1i64 << (width - 1) {
                    let nw = b.read(if bits16 { 4 } else { 3 }) + 1;
                    width = if nw < width { nw } else { nw + 1 };
                    continue;
                }
            } else if width < if bits16 { 17 } else { 9 } {
                let (i, span) = if bits16 { (((0xFFFFu32 >> (17 - width)) + 8) as i64, 16) } else { (((0xFFu32 >> (9 - width)) + 4) as i64, 8) };
                let j = i - span;
                if v > j && v <= i {
                    let nw = (v - j) as u32;
                    width = if nw < width { nw } else { nw + 1 };
                    continue;
                }
            } else if bits16 && width >= 18 || !bits16 && width >= 10 {
                got += 1;
                continue;
            } else if v >= if bits16 { 0x10000 } else { 0x100 } {
                width = (v + 1) as u32 & 0xFF;
                continue;
            }
            // a value
            let w = width;
            let full = if bits16 { 16 } else { 8 };
            if w < full {
                let shift = full - w;
                v = if bits16 { ((v as i16) << shift >> shift) as i64 } else { (((v as i8) << shift) >> shift) as i64 };
            }
            d1 = d1.wrapping_add(v as i32);
            let cur = if bits16 { d1 as i16 as i32 } else { d1 as i8 as i32 };
            d1 = cur;
            d2 = d2.wrapping_add(cur);
            let o = if it215 { d2 } else { cur };
            out.push(if bits16 { o as i16 } else { ((o as i8) as i16) << 8 });
            got += 1;
        }
        // a damaged block: keep the length right
        while got < want {
            out.push(0);
            got += 1;
        }
    }
    out.resize(len, 0);
    out
}

fn parse_it(d: &[u8]) -> Result<Module, String> {
    let r = Rd { d };
    let title = r.text(4, 26);
    let (ordnum, insnum, smpnum, patnum) = (r.u16(0x20) as usize, r.u16(0x22) as usize, r.u16(0x24) as usize, r.u16(0x26) as usize);
    let (cmwt, flags) = (r.u16(0x2a), r.u16(0x2c));
    let use_ins = flags & 4 != 0;
    let (speed, tempo) = (r.u8(0x32).max(1), r.u8(0x33).max(32));
    let chan_pan_raw: Vec<u8> = (0..64).map(|c| r.u8(0x40 + c)).collect();
    let mut orders = vec![];
    for i in 0..ordnum.min(1024) {
        let o = r.u8(0xc0 + i);
        if o == 255 {
            break;
        }
        if o < 254 {
            orders.push(o as usize);
        }
    }
    let ins_ptr = 0xc0 + ordnum;
    let smp_ptr = ins_ptr + insnum * 4;
    let pat_ptr = smp_ptr + smpnum * 4;
    // samples first: instruments point at them
    let mut smps: Vec<Option<Smp>> = vec![];
    for i in 0..smpnum {
        let o = r.u32(smp_ptr + i * 4) as usize;
        if o == 0 || o + 0x50 > d.len() || &d[o..o + 4] != b"IMPS" {
            smps.push(None);
            continue;
        }
        let (gvl, fl, vol) = (r.u8(o + 0x11), r.u8(o + 0x12), r.u8(o + 0x13));
        let name = r.text(o + 0x14, 26);
        let (cvt, dfp) = (r.u8(o + 0x2e), r.u8(o + 0x2f));
        let (len, ls, le, c5, sls, sle, ptr) = (r.u32(o + 0x30) as usize, r.u32(o + 0x34), r.u32(o + 0x38), r.u32(o + 0x3c), r.u32(o + 0x40), r.u32(o + 0x44), r.u32(o + 0x48) as usize);
        let (present, bits16, stereo, comp) = (fl & 1 != 0, fl & 2 != 0, fl & 4 != 0, fl & 8 != 0);
        let mut data = vec![];
        if present && len > 0 && len < 1 << 28 && ptr < d.len() {
            let signed = cvt & 1 != 0;
            let chans = if stereo { 2 } else { 1 };
            let mut chan_data: Vec<Vec<i16>> = vec![];
            let mut at = ptr;
            for _ in 0..chans {
                if comp {
                    // each channel is its own run of compressed blocks; walk it to find where it ends
                    let block_max = if bits16 { 0x4000 } else { 0x8000 };
                    let mut left = len;
                    let mut q = at;
                    while left > 0 && q + 2 <= d.len() {
                        q += 2 + r.u16(q) as usize;
                        left = left.saturating_sub(block_max);
                    }
                    let v = it_decompress(r.slice(at, q.saturating_sub(at)), len, bits16, cvt & 4 != 0);
                    chan_data.push(v);
                    at = q;
                } else {
                    let bytes = len * if bits16 { 2 } else { 1 };
                    chan_data.push(pcm(r.slice(at, bytes), bits16, signed));
                    at += bytes;
                }
            }
            data = chan_data.remove(0);
            if let Some(right) = chan_data.first() {
                for (l, rr) in data.iter_mut().zip(right) {
                    *l = ((*l as i32 + *rr as i32) / 2) as i16;
                }
            }
        }
        let (looped, lstart, lend) = if fl & 16 != 0 && le > ls { (true, ls, le) } else if fl & 32 != 0 && sle > sls { (true, sls, sle) } else { (false, 0, 0) };
        smps.push(Some(Smp {
            name,
            rate: c5.clamp(1000, 384000),
            volume: vol.min(64),
            pan: if dfp & 128 != 0 { Some(((dfp & 127).min(64) as f32 - 32.0) / 32.0) } else { None },
            loop_start: lstart,
            loop_end: lend.min(data.len() as u32),
            looped: looped && (lend as usize) <= data.len() + 1,
            pingpong: fl & 64 != 0,
            vol_db: if gvl >= 64 { 0.0 } else { 20.0 * (gvl.max(1) as f32 / 64.0).log10() },
            data,
            ..Smp::default()
        }));
    }
    let mut ins = vec![];
    if use_ins {
        for i in 0..insnum {
            let o = r.u32(ins_ptr + i * 4) as usize;
            if o == 0 || o + 0x80 > d.len() || &d[o..o + 4] != b"IMPI" {
                ins.push(Ins::default());
                continue;
            }
            let _ = cmwt;
            let fadeout = r.u16(o + 0x14);
            let gbv = r.u8(o + 0x18).min(128);
            let dfp = r.u8(o + 0x19);
            let name = r.text(o + 0x20, 26);
            let mut inst = Ins { name, map: vec![255; 128], shift: vec![0; 128], fadeout_s: if fadeout == 0 { 0.02 } else { (4096.0 / fadeout as f32 * 0.02).clamp(0.02, 5.0) }, ..Ins::default() };
            inst.pan = if dfp & 0x80 == 0 { Some((dfp.min(64) as f32 - 32.0) / 32.0 * 0.8) } else { None };
            inst.gain_db = if gbv >= 128 { 0.0 } else { 20.0 * (gbv.max(1) as f32 / 128.0).log10() };
            let mut used: Vec<usize> = vec![];
            // volume envelope (flags, count, loop begin / end, sustain begin / end, then (value, tick) nodes) and the fade-out
            if r.u8(o + 0x130) & 1 != 0 {
                let cnt = (r.u8(o + 0x131) as usize).min(25);
                for k in 0..cnt {
                    inst.env.push((r.u16(o + 0x136 + k * 3 + 1), r.u8(o + 0x136 + k * 3).min(64)));
                }
                if r.u8(o + 0x130) & 4 != 0 && (r.u8(o + 0x134) as usize) < inst.env.len() {
                    inst.env_sustain = Some(r.u8(o + 0x134) as usize);
                }
                if r.u8(o + 0x130) & 2 != 0 && (r.u8(o + 0x132) as usize) <= (r.u8(o + 0x133) as usize) && (r.u8(o + 0x133) as usize) < inst.env.len() {
                    inst.env_loop = Some((r.u8(o + 0x132) as usize, r.u8(o + 0x133) as usize));
                }
            }
            // the panning (0x182) and pitch / filter (0x1D4) envelopes: signed -32..32 nodes
            let (ifc, ifr) = (r.u8(o + 0x3A), r.u8(o + 0x3B));
            inst.swing[0] = (r.u8(o + 0x1A).min(100) as f32) / 100.0;
            inst.swing[1] = (r.u8(o + 0x1B).min(64) as f32) / 64.0;
            inst.cutoff = if ifc & 0x80 != 0 { Some(ifc & 0x7F) } else { None };
            inst.reso = if ifr & 0x80 != 0 { Some(ifr & 0x7F) } else { None };
            for (base_off, is_pan) in [(0x182usize, true), (0x1D4usize, false)] {
                let flags = r.u8(o + base_off);
                if flags & 1 == 0 {
                    continue;
                }
                let filter_env = !is_pan && flags & 0x80 != 0;
                let cnt = (r.u8(o + base_off + 1) as usize).min(25);
                let mut pts: Vec<(u16, f32)> = vec![];
                for k in 0..cnt {
                    let raw = r.i8(o + base_off + 6 + k * 3) as f32;
                    // pan: -1..1; filter: the factor (node + 32) / 64 of the cutoff; pitch: semitones
                    pts.push((r.u16(o + base_off + 6 + k * 3 + 1), if is_pan { raw / 32.0 } else if filter_env { (raw + 32.0) / 64.0 } else { raw }));
                }
                if pts.len() < 2 {
                    continue;
                }
                let sus = if flags & 4 != 0 && (r.u8(o + base_off + 4) as usize) < pts.len() { Some(r.u8(o + base_off + 4) as usize) } else { None };
                let lp = if flags & 2 != 0 && r.u8(o + base_off + 2) <= r.u8(o + base_off + 3) && (r.u8(o + base_off + 3) as usize) < pts.len() { Some((r.u8(o + base_off + 2) as usize, r.u8(o + base_off + 3) as usize)) } else { None };
                if is_pan {
                    inst.pan_env = pts;
                    inst.pan_sustain = sus;
                    inst.pan_loop = lp;
                } else if filter_env {
                    inst.filter_env = pts;
                    inst.filter_sustain = sus;
                    inst.filter_loop = lp;
                } else {
                    inst.pitch_env = pts;
                    inst.pitch_sustain = sus;
                    inst.pitch_loop = lp;
                }
            }
            inst.fade_s = if fadeout == 0 { 0.0 } else { (2048.0 / fadeout as f32 * 2.5 / tempo as f32).clamp(0.0, 8.0) };
            for n in 0..120usize {
                let (play, s) = (r.u8(o + 0x40 + n * 2) as i32, r.u8(o + 0x41 + n * 2) as usize);
                if s == 0 || s > smps.len() || smps[s - 1].is_none() {
                    continue;
                }
                let idx = match used.iter().position(|u| *u == s - 1) {
                    Some(k) => k,
                    None => {
                        used.push(s - 1);
                        inst.smps.push(smps[s - 1].clone().unwrap());
                        used.len() - 1
                    }
                };
                inst.map[n] = idx as u8;
                inst.shift[n] = (play - n as i32).clamp(-127, 127) as i8;
            }
            ins.push(inst);
        }
    } else {
        for s in &smps {
            ins.push(match s {
                Some(s) => one_sample_ins(s.clone()),
                None => Ins::default(),
            });
        }
    }
    // patterns
    let nch = 64;
    let mut patterns = vec![];
    let mut ignored = 0;
    let mut max_ch = 0usize;
    for p in 0..patnum {
        let o = r.u32(pat_ptr + p * 4) as usize;
        if o == 0 || o + 8 > d.len() {
            patterns.push(vec![vec![Cell::default(); nch]; 64]);
            continue;
        }
        let (plen, rows_n) = (r.u16(o) as usize, r.u16(o + 2) as usize);
        let rows_n = rows_n.clamp(1, 1024);
        let mut rows = vec![vec![Cell::default(); nch]; rows_n];
        let end = (o + 8 + plen).min(d.len());
        let mut q = o + 8;
        let mut row = 0usize;
        let mut last_mask = [0u8; 64];
        let mut last: Vec<(u8, u8, u8, u8, u8)> = vec![(0, 0, 0, 0, 0); 64];
        while row < rows_n && q < end {
            let cv = r.u8(q);
            q += 1;
            if cv == 0 {
                row += 1;
                continue;
            }
            let ch = ((cv - 1) & 63) as usize;
            let mask = if cv & 128 != 0 {
                let m = r.u8(q);
                q += 1;
                last_mask[ch] = m;
                m
            } else {
                last_mask[ch]
            };
            let mut cell = Cell::default();
            let l = &mut last[ch];
            if mask & 1 != 0 {
                l.0 = r.u8(q);
                q += 1;
            }
            if mask & 2 != 0 {
                l.1 = r.u8(q);
                q += 1;
            }
            if mask & 4 != 0 {
                l.2 = r.u8(q);
                q += 1;
            }
            if mask & 8 != 0 {
                l.3 = r.u8(q);
                l.4 = r.u8(q + 1);
                q += 2;
            }
            let has_note = mask & (1 | 16) != 0;
            let has_ins = mask & (2 | 32) != 0;
            let has_vol = mask & (4 | 64) != 0;
            let has_cmd = mask & (8 | 128) != 0;
            if has_note {
                cell.note = match l.0 {
                    254 => NOTE_CUT,
                    255 => NOTE_OFF,
                    n if n < 120 => n as i16,
                    _ => NOTE_NONE,
                };
            }
            if has_ins {
                cell.inst = l.1;
            }
            if has_vol {
                let (cv, cp, cfx) = it_vcol(l.2);
                cell.vol = cv;
                cell.pan = cp;
                cell.vfx = cfx;
            }
            if has_cmd {
                cell.fx = dec_it(l.3, l.4, true, &mut ignored);
            }
            if row < rows_n {
                rows[row][ch] = cell;
                max_ch = max_ch.max(ch + 1);
            }
        }
        patterns.push(rows);
    }
    let nch = max_ch.max(1);
    for p in patterns.iter_mut() {
        for row in p.iter_mut() {
            row.truncate(nch);
        }
    }
    let chan_pan: Vec<f32> = (0..nch).map(|c| match chan_pan_raw[c] { v if v <= 64 => (v as f32 - 32.0) / 32.0 * 0.8, _ => 0.0 }).collect();
    Ok(Module { title, format: if d.len() > 0x2d && r.u16(0x28) >= 0x889 { "OpenMPT module (MPTM / IT)" } else { "Impulse Tracker IT" }, nch, speed, tempo, orders, patterns, ins, chan_pan, ignored, linear: flags & 8 != 0, gain: (r.u8(0x30).min(128) as f32 / 128.0) * (r.u8(0x31).min(128) as f32 / 128.0) * 1.0 })
}

// ── playing the patterns into notes, pitch bends and volume lanes ─────────────────

/// One track of the result: an instrument's notes, or (when a channel uses slides, vibrato or volume effects) one channel of it with its own
/// pitch-bend and volume lanes.
pub struct PTrack {
    /// 0-based instrument index
    pub ins: usize,
    /// the channel when the track carries its own lanes
    pub chan: Option<usize>,
    pub notes: Vec<Note>,
    /// (beat, 0..1 with 0.5 = centre)
    pub bend: Vec<(f64, f32)>,
    pub bend_range: f32,
    /// (beat, 0..1.5 track volume)
    pub vol: Vec<(f64, f32)>,
    /// filter lanes from Zxx: (beat, 0..1 cutoff) and (beat, 0..1 resonance)
    pub cut: Vec<(f64, f32)>,
    pub res: Vec<(f64, f32)>,
}

pub struct Played {
    pub tracks: Vec<PTrack>,
    pub tempos: Vec<(f64, f64)>,
    pub beats: f64,
    pub dropped: usize,
    /// effects turned into notes / lanes, for the report
    pub arps: usize,
    pub slides: usize,
    pub retrigs: usize,
}

struct Open {
    start: f64,
    key: u8,
    vel: f32,
    pan: f32,
    ins: usize,
}

#[derive(Default)]
struct Ch {
    ins: usize,
    key: i32,
    open: Option<Open>,
    semis: f32,
    tone: Option<f32>,
    vol: f32,
    pan: f32,
    pending: Option<(u32, i32)>,
    cut_at: Option<u32>,
    arp: Option<(u8, u8)>,
    arp_off: i32,
    retrig: Option<(u8, u8)>,
    m_porta: i16,
    m_tone: u8,
    m_vib: (u8, u8),
    m_vol: i8,
    m_arp: (u8, u8),
    m_trem: (u8, u8),
    vib_pos: f32,
    trem_pos: f32,
    last_bend: f32,
    last_vol: f32,
    /// the channel's filter (Impulse Tracker units 0..127)
    cutv: f32,
    resv: f32,
}

fn vol_change(mode: u8, v: f32) -> f32 {
    match mode {
        1 => v - 1.0,
        2 => v - 2.0,
        3 => v - 4.0,
        4 => v - 8.0,
        5 => v - 16.0,
        6 => v * 2.0 / 3.0,
        7 => v * 0.5,
        9 => v + 1.0,
        10 => v + 2.0,
        11 => v + 4.0,
        12 => v + 8.0,
        13 => v + 16.0,
        14 => v * 1.5,
        15 => v * 2.0,
        _ => v,
    }
    .clamp(0.0, 64.0)
}

fn is_pitch_fx(f: Fx) -> bool {
    matches!(f, Fx::Porta(_) | Fx::FinePorta(_) | Fx::XFinePorta(_) | Fx::Tone(_) | Fx::ToneVol(_) | Fx::Vib(..) | Fx::FineVib(..) | Fx::VibVol(_))
}
fn is_filter_fx(f: Fx) -> bool {
    matches!(f, Fx::Filter(_))
}
fn is_vol_fx(f: Fx) -> bool {
    matches!(f, Fx::VolSlide(_) | Fx::FineVol(_) | Fx::ToneVol(_) | Fx::VibVol(_) | Fx::Trem(..))
}

pub fn play(m: &Module) -> Played {
    let nch = m.nch;
    // channels that use pitch / volume effects get their own tracks with lanes
    let mut variant = vec![false; nch];
    for &o in &m.orders {
        if let Some(pat) = m.patterns.get(o) {
            for row in pat {
                for (ch, c) in row.iter().enumerate().take(nch) {
                    if is_pitch_fx(c.fx) || is_pitch_fx(c.vfx) || is_vol_fx(c.fx) || is_vol_fx(c.vfx) || is_filter_fx(c.fx) {
                        variant[ch] = true;
                    }
                }
            }
        }
    }
    let mut tracks: Vec<PTrack> = vec![];
    let mut tix: std::collections::HashMap<(usize, usize), usize> = std::collections::HashMap::new();
    // lane points are kept in semitones / volume units until the range is known
    let mut raw_bend: Vec<Vec<(f64, f32)>> = vec![];
    let mut raw_vol: Vec<Vec<(f64, f32)>> = vec![];
    let (mut speed, mut tempo) = (m.speed.max(1) as f64, m.tempo.max(32) as f64);
    let bpm = |sp: f64, tp: f64| (6.0 * tp / sp).clamp(20.0, 400.0);
    let mut tempos: Vec<(f64, f64)> = vec![(0.0, bpm(speed, tempo))];
    let mut chs: Vec<Ch> = (0..nch).map(|_| Ch { vol: 64.0, last_vol: -1.0, cutv: 127.0, ..Ch::default() }).collect();
    let (mut dropped, mut arps, mut slides, mut retrigs) = (0usize, 0usize, 0usize, 0usize);
    let mut beat = 0.0f64;
    let mut visited: HashSet<(usize, usize, usize)> = HashSet::new();
    let (mut oi, mut row, mut guard) = (0usize, 0usize, 0usize);
    // pattern loops: the start row and the repeats left, per channel; `pass` makes a repeated row count as a new visit
    let mut loop_start = vec![0usize; nch];
    let mut loop_left = vec![0u32; nch];
    let mut pass = 0usize;

    // the track index of an (instrument, channel) pair
    macro_rules! track_of {
        ($ins:expr, $ch:expr) => {{
            let key = ($ins, if variant[$ch] { $ch + 1 } else { 0 });
            *tix.entry(key).or_insert_with(|| {
                tracks.push(PTrack { ins: $ins, chan: if key.1 > 0 { Some($ch) } else { None }, notes: vec![], bend: vec![], bend_range: 2.0, vol: vec![], cut: vec![], res: vec![] });
                raw_bend.push(vec![]);
                raw_vol.push(vec![]);
                tracks.len() - 1
            })
        }};
    }

    while oi < m.orders.len() && guard < 400_000 {
        guard += 1;
        let Some(pat) = m.patterns.get(m.orders[oi]) else {
            oi += 1;
            row = 0;
            continue;
        };
        if row >= pat.len() {
            oi += 1;
            row = 0;
            continue;
        }
        if !visited.insert((oi, row, pass)) {
            break;
        }
        let cells = &pat[row];
        let (mut jump, mut brk): (Option<usize>, Option<usize>) = (None, None);
        for c in cells.iter() {
            match c.fx {
                Fx::Speed(sp) if sp > 0 => speed = sp as f64,
                Fx::Tempo(t) if t >= 32 => tempo = t as f64,
                Fx::Jump(o) => jump = Some(o as usize),
                Fx::Break(r) => brk = Some(r as usize),
                _ => {}
            }
        }
        // pattern loop / delay of this row
        let mut loop_to: Option<usize> = None;
        let mut delay_rows = 0u32;
        for (ch, c) in cells.iter().enumerate().take(nch) {
            match c.fx {
                Fx::PatLoop(0) => loop_start[ch] = row,
                Fx::PatLoop(n) => {
                    if loop_left[ch] == 0 {
                        loop_left[ch] = n as u32;
                        loop_to = Some(loop_start[ch]);
                    } else {
                        loop_left[ch] -= 1;
                        if loop_left[ch] > 0 {
                            loop_to = Some(loop_start[ch]);
                        }
                    }
                }
                Fx::PatDelay(n) => delay_rows = delay_rows.max(n as u32),
                _ => {}
            }
        }
        let b_now = bpm(speed, tempo);
        if tempos.last().map(|t| (t.1 - b_now).abs() > 0.05).unwrap_or(true) {
            tempos.push((beat, b_now));
        }
        let ticks = speed.round().max(1.0) as u32;
        let tick_len = 0.25 / ticks as f64;

        // ── the row: notes, volumes and the effects that start now
        for ch in 0..nch.min(cells.len()) {
            let c = cells[ch];
            let st = &mut chs[ch];
            if c.inst > 0 {
                st.ins = c.inst as usize;
            }
            if let Some(p) = c.pan {
                st.pan = p;
            } else if let Fx::Pan(p) = c.fx {
                st.pan = p;
            }
            let tone_row = matches!(c.fx, Fx::Tone(_) | Fx::ToneVol(_)) || matches!(c.vfx, Fx::Tone(_));
            let delay = if let Fx::Delay(d) = c.fx { d as u32 } else { 0 };
            st.pending = None;
            st.cut_at = None;
            st.retrig = None;
            // arpeggio state of the row
            st.arp = match c.fx {
                Fx::Arp(x, y) => {
                    let (x, y) = if x == 0 && y == 0 { st.m_arp } else { (x, y) };
                    st.m_arp = (x, y);
                    Some((x, y))
                }
                _ => None,
            };
            match c.note {
                NOTE_OFF | NOTE_CUT => {
                    close_note(&mut tracks, &mut tix, &variant, &mut raw_bend, &mut raw_vol, st, ch, beat, c.note == NOTE_CUT);
                }
                n if n >= 0 => {
                    if tone_row && st.open.is_some() {
                        st.tone = Some(n as f32 - st.key as f32);
                    } else if delay < ticks {
                        st.pending = Some((delay, n as i32));
                    }
                }
                _ => {}
            }
            if c.vol >= 0 {
                st.vol = c.vol as f32;
                if c.note < 0 && st.open.is_some() {
                    // a volume change without a note acts on the playing one (lane tracks follow it below)
                }
            }
            if let Fx::Filter(par) = c.fx {
                if par <= 0x7F {
                    st.cutv = par as f32;
                } else if par <= 0x8F {
                    st.resv = (par - 0x80) as f32 * 8.0;
                }
                if st.open.is_some() && variant[ch] {
                    let ins0 = st.open.as_ref().map(|o| o.ins).unwrap_or(0);
                    let ti = track_of!(ins0, ch);
                    tracks[ti].cut.push((beat, st.cutv));
                    tracks[ti].res.push((beat, st.resv));
                }
            }
            match c.fx {
                Fx::Cut(d) => st.cut_at = Some(d as u32),
                Fx::Off(d) => st.cut_at = Some(d as u32),
                Fx::Retrig(i, v) => st.retrig = Some((i, v)),
                Fx::Offset(_) | Fx::Filter(_) => {}
                _ => {}
            }
            // a row without arpeggio returns a held arpeggio to its note
            if st.arp.is_none() && st.arp_off != 0 && st.open.is_some() {
                let k = st.key;
                reopen(&mut tracks, &mut tix, &variant, &mut raw_bend, &mut raw_vol, st, ch, beat, k);
                st.arp_off = 0;
            }
        }

        // ── the ticks of the row
        for t in 0..ticks {
            let bt = beat + t as f64 * tick_len;
            for ch in 0..nch.min(cells.len()) {
                let c = cells[ch];
                let st = &mut chs[ch];
                // trigger
                if let Some((d, n)) = st.pending {
                    if d == t {
                        st.pending = None;
                        close_note(&mut tracks, &mut tix, &variant, &mut raw_bend, &mut raw_vol, st, ch, bt, true);
                        let ins_i = st.ins;
                        let ins_pan = ins_i.checked_sub(1).and_then(|k| m.ins.get(k)).and_then(|ins| ins.pan);
                        let smp = ins_i.checked_sub(1).and_then(|k| m.ins.get(k)).and_then(|ins| ins.map.get((n.clamp(0, 127)) as usize).copied().and_then(|si| ins.smps.get(si as usize)));
                        match smp {
                            Some(sm) if !sm.data.is_empty() => {
                                if c.vol < 0 {
                                    st.vol = sm.volume as f32;
                                }
                                st.key = n;
                                st.semis = 0.0;
                                st.tone = None;
                                st.vib_pos = 0.0;
                                st.trem_pos = 0.0;
                                st.arp_off = 0;
                                let pan = c.pan.or(if let Fx::Pan(p) = c.fx { Some(p) } else { None }).or(ins_pan).or(sm.pan.map(|p| p * 0.7)).unwrap_or(m.chan_pan.get(ch).copied().unwrap_or(0.0)).clamp(-1.0, 1.0);
                                st.pan = pan;
                                if let Some(ic) = m.ins.get(ins_i.saturating_sub(1)).and_then(|i| i.cutoff) {
                                    st.cutv = ic as f32;
                                    st.resv = m.ins.get(ins_i.saturating_sub(1)).and_then(|i| i.reso).unwrap_or(0) as f32;
                                }
                                open_note(&mut tracks, &mut tix, &variant, &mut raw_bend, &mut raw_vol, st, ch, bt, n.clamp(0, 127) as u8);
                            }
                            _ => dropped += 1,
                        }
                    }
                }
                if st.open.is_none() {
                    continue;
                }
                let first = t == 0;
                // effects: the row's and the volume column's
                for f in [c.fx, c.vfx] {
                    match f {
                        Fx::Porta(u) => {
                            let u = if u == 0 { st.m_porta } else { u };
                            if u != 0 {
                                st.m_porta = u;
                            }
                            if !first {
                                st.semis += slide_semis(m, st, u);
                                slides += (t == 1) as usize;
                            }
                        }
                        Fx::FinePorta(u) if first => {
                            st.semis += slide_semis(m, st, u);
                            slides += 1;
                        }
                        Fx::XFinePorta(u) if first => {
                            st.semis += slide_semis(m, st, u) / 4.0;
                            slides += 1;
                        }
                        Fx::Tone(_) | Fx::ToneVol(_) => {
                            let u = if let Fx::Tone(x) = f { x } else { 0 };
                            let u = if u == 0 { st.m_tone } else { u };
                            if u != 0 {
                                st.m_tone = u;
                            }
                            if !first {
                                if let Some(target) = st.tone {
                                    let step = slide_semis(m, st, u as i16).abs();
                                    if st.semis < target {
                                        st.semis = (st.semis + step).min(target);
                                    } else {
                                        st.semis = (st.semis - step).max(target);
                                    }
                                    slides += (t == 1) as usize;
                                }
                            }
                        }
                        _ => {}
                    }
                    match f {
                        Fx::Vib(x, y) | Fx::FineVib(x, y) => {
                            let (x, y) = (if x == 0 { st.m_vib.0 } else { x }, if y == 0 { st.m_vib.1 } else { y });
                            st.m_vib = (x, y);
                            if !first {
                                st.vib_pos += x as f32 / 64.0;
                            }
                        }
                        Fx::VibVol(_) => {
                            let (x, y) = st.m_vib;
                            if !first {
                                st.vib_pos += x as f32 / 64.0;
                            }
                            let _ = y;
                        }
                        _ => {}
                    }
                    match f {
                        Fx::VolSlide(v) | Fx::ToneVol(v) | Fx::VibVol(v) => {
                            let v = if v == 0 { st.m_vol } else { v };
                            if v != 0 {
                                st.m_vol = v;
                            }
                            if !first {
                                st.vol = (st.vol + v as f32).clamp(0.0, 64.0);
                            }
                        }
                        Fx::FineVol(v) if first => st.vol = (st.vol + v as f32).clamp(0.0, 64.0),
                        Fx::Trem(x, y) => {
                            let (x, y) = (if x == 0 { st.m_trem.0 } else { x }, if y == 0 { st.m_trem.1 } else { y });
                            st.m_trem = (x, y);
                            if !first {
                                st.trem_pos += x as f32 / 64.0;
                            }
                        }
                        _ => {}
                    }
                }
                // retrigger
                if let Some((iv, vm)) = st.retrig {
                    if t > 0 && iv > 0 && t % iv as u32 == 0 {
                        st.vol = vol_change(vm, st.vol);
                        let k = st.key;
                        reopen(&mut tracks, &mut tix, &variant, &mut raw_bend, &mut raw_vol, st, ch, bt, k);
                        retrigs += 1;
                    }
                }
                // arpeggio: a new note for every tick that changes the offset
                if let Some((x, y)) = st.arp {
                    let off = [0, x as i32, y as i32][(t % 3) as usize];
                    if off != st.arp_off {
                        st.arp_off = off;
                        let k = (st.key + off).clamp(0, 127);
                        reopen(&mut tracks, &mut tix, &variant, &mut raw_bend, &mut raw_vol, st, ch, bt, k);
                        arps += 1;
                    }
                }
                // cut / key off
                if st.cut_at == Some(t) {
                    st.cut_at = None;
                    close_note(&mut tracks, &mut tix, &variant, &mut raw_bend, &mut raw_vol, st, ch, bt, matches!(c.fx, Fx::Cut(_)));
                    continue;
                }
                // lanes of channels that carry their own
                if variant[ch] {
                    let vib = {
                        let (_, y) = st.m_vib;
                        let on = matches!(c.fx, Fx::Vib(..) | Fx::FineVib(..) | Fx::VibVol(_)) || matches!(c.vfx, Fx::Vib(..));
                        if on {
                            let depth = if matches!(c.fx, Fx::FineVib(..)) { y as f32 / 64.0 } else { y as f32 / 16.0 };
                            (st.vib_pos * std::f32::consts::TAU).sin() * depth
                        } else {
                            0.0
                        }
                    };
                    let trem = if matches!(c.fx, Fx::Trem(..)) || matches!(c.vfx, Fx::Trem(..)) { (st.trem_pos * std::f32::consts::TAU).sin() * st.m_trem.1 as f32 * 3.0 } else { 0.0 };
                    let ti = track_of!(st.open.as_ref().map(|o| o.ins).unwrap_or(0), ch);
                    let bend = st.semis + vib;
                    if (bend - st.last_bend).abs() > 0.004 || first {
                        raw_bend[ti].push((bt, bend));
                        st.last_bend = bend;
                    }
                    let v = (st.vol + trem).clamp(0.0, 64.0);
                    if (v - st.last_vol).abs() > 0.4 || first {
                        raw_vol[ti].push((bt, v));
                        st.last_vol = v;
                    }
                }
            }
        }
        beat += 0.25 * (1.0 + delay_rows as f64);
        if let Some(r0) = loop_to {
            pass += 1;
            row = r0;
            continue;
        }
        match (jump, brk) {
            (Some(o), b) => {
                oi = o;
                row = b.unwrap_or(0);
            }
            (None, Some(b)) => {
                oi += 1;
                row = b;
            }
            (None, None) => row += 1,
        }
    }
    for (ch, st) in chs.iter_mut().enumerate() {
        close_note(&mut tracks, &mut tix, &variant, &mut raw_bend, &mut raw_vol, st, ch, beat, false);
    }
    // finish the lanes: bend range from the biggest slide, points thinned
    for (ti, t) in tracks.iter_mut().enumerate() {
        t.notes.sort_by(|a, b| a.start.total_cmp(&b.start).then(a.key.cmp(&b.key)));
        let maxb = raw_bend[ti].iter().fold(0.0f32, |a, p| a.max(p.1.abs()));
        let range = maxb.ceil().clamp(1.0, 24.0);
        t.bend_range = range;
        let mut pts: Vec<(f64, f32)> = vec![];
        for (b, v) in &raw_bend[ti] {
            let val = (0.5 + v / (2.0 * range)).clamp(0.0, 1.0);
            if let Some(l) = pts.last() {
                if (l.1 - val).abs() < 0.0006 && *b - l.0 < 2.0 {
                    continue;
                }
            }
            pts.push((*b, val));
        }
        if pts.iter().all(|p| (p.1 - 0.5).abs() < 0.002) {
            pts.clear();
        }
        t.bend = pts;
        let mut vp: Vec<(f64, f32)> = vec![];
        for (b, v) in &raw_vol[ti] {
            let val = (v / 64.0 * 0.9).clamp(0.0, 1.5);
            if let Some(l) = vp.last() {
                if (l.1 - val).abs() < 0.004 && *b - l.0 < 2.0 {
                    continue;
                }
            }
            vp.push((*b, val));
        }
        t.vol = vp;
        let mut cp: Vec<(f64, f32)> = vec![];
        for (b, c) in &t.cut {
            let val = it_cut_norm(*c);
            if cp.last().map(|l| (l.1 - val).abs() < 0.002 && *b - l.0 < 4.0).unwrap_or(false) {
                continue;
            }
            cp.push((*b, val));
        }
        t.cut = if cp.iter().all(|p| p.1 > 0.995) { vec![] } else { cp };
        let mut rp: Vec<(f64, f32)> = vec![];
        for (b, r) in &t.res {
            let val = (r / 127.0).clamp(0.0, 1.0);
            if rp.last().map(|l| (l.1 - val).abs() < 0.002 && *b - l.0 < 4.0).unwrap_or(false) {
                continue;
            }
            rp.push((*b, val));
        }
        t.res = if rp.iter().all(|p| p.1 < 0.002) { vec![] } else { rp };
        // keep the lanes a sensible size
        if t.bend.len() > 30000 {
            let k = t.bend.len() / 30000 + 1;
            t.bend = t.bend.iter().enumerate().filter(|(i, _)| i % k == 0).map(|(_, p)| *p).collect();
        }
        if t.vol.len() > 30000 {
            let k = t.vol.len() / 30000 + 1;
            t.vol = t.vol.iter().enumerate().filter(|(i, _)| i % k == 0).map(|(_, p)| *p).collect();
        }
    }
    Played { tracks, tempos, beats: beat, dropped, arps, slides, retrigs }
}

/// semitones a slide of `u` units moves the pitch on one tick
fn slide_semis(m: &Module, st: &Ch, u: i16) -> f32 {
    if u == 0 {
        return 0.0;
    }
    if m.linear {
        return u as f32 / 16.0;
    }
    // Amiga periods: C-5 is 428; going up shortens the period
    let p = 428.0 * 2f32.powf(-((st.key as f32 + st.semis) - 60.0) / 12.0);
    let np = (p - u as f32).max(20.0);
    12.0 * (p / np).log2()
}

#[allow(clippy::too_many_arguments)]
fn open_note(tracks: &mut Vec<PTrack>, tix: &mut std::collections::HashMap<(usize, usize), usize>, variant: &[bool], raw_bend: &mut Vec<Vec<(f64, f32)>>, raw_vol: &mut Vec<Vec<(f64, f32)>>, st: &mut Ch, ch: usize, bt: f64, key: u8) {
    let ins = st.ins.saturating_sub(1);
    let lane = variant[ch];
    let vel = if lane { 1.0 } else { (st.vol / 64.0).clamp(0.03, 1.0) };
    st.open = Some(Open { start: bt, key, vel, pan: st.pan, ins });
    if lane {
        let k = (ins, ch + 1);
        let ti = *tix.entry(k).or_insert_with(|| {
            tracks.push(PTrack { ins, chan: Some(ch), notes: vec![], bend: vec![], bend_range: 2.0, vol: vec![], cut: vec![], res: vec![] });
            raw_bend.push(vec![]);
            raw_vol.push(vec![]);
            tracks.len() - 1
        });
        // hold the old value up to the new note, then jump
        if let Some(l) = raw_bend[ti].last().copied() {
            raw_bend[ti].push((bt - 0.0004, l.1));
        }
        raw_bend[ti].push((bt, st.semis));
        if let Some(l) = raw_vol[ti].last().copied() {
            raw_vol[ti].push((bt - 0.0004, l.1));
        }
        raw_vol[ti].push((bt, st.vol));
        tracks[ti].cut.push((bt, st.cutv));
        tracks[ti].res.push((bt, st.resv));
        st.last_bend = st.semis;
        st.last_vol = st.vol;
    }
}

#[allow(clippy::too_many_arguments)]
fn close_note(tracks: &mut Vec<PTrack>, tix: &mut std::collections::HashMap<(usize, usize), usize>, variant: &[bool], raw_bend: &mut Vec<Vec<(f64, f32)>>, raw_vol: &mut Vec<Vec<(f64, f32)>>, st: &mut Ch, ch: usize, end: f64, hard: bool) {
    let Some(o) = st.open.take() else { return };
    let len = (end - o.start).max(0.03);
    let k = (o.ins, if variant[ch] { ch + 1 } else { 0 });
    let ti = *tix.entry(k).or_insert_with(|| {
        tracks.push(PTrack { ins: o.ins, chan: if k.1 > 0 { Some(ch) } else { None }, notes: vec![], bend: vec![], bend_range: 2.0, vol: vec![], cut: vec![], res: vec![] });
        raw_bend.push(vec![]);
        raw_vol.push(vec![]);
        tracks.len() - 1
    });
    let mut n = Note::new(o.start, len, o.key, o.vel);
    n.pan = o.pan;
    // reso 0 tells the Sampler this note is cut, not released (a new note on the channel, a note cut): no fade-out tail
    n.reso = if hard { 0.0 } else { 0.5 };
    tracks[ti].notes.push(n);
}

/// end the playing note and start the same instrument again on `key` (arpeggio steps, retriggers)
#[allow(clippy::too_many_arguments)]
fn reopen(tracks: &mut Vec<PTrack>, tix: &mut std::collections::HashMap<(usize, usize), usize>, variant: &[bool], raw_bend: &mut Vec<Vec<(f64, f32)>>, raw_vol: &mut Vec<Vec<(f64, f32)>>, st: &mut Ch, ch: usize, bt: f64, key: i32) {
    close_note(tracks, tix, variant, raw_bend, raw_vol, st, ch, bt, true);
    if st.ins > 0 {
        open_note(tracks, tix, variant, raw_bend, raw_vol, st, ch, bt, key.clamp(0, 127) as u8);
    }
}

// ── the Sampler of a module ───────────────────────────────────────────────────

/// ADSR (attack, decay, sustain 0..1, release) in seconds from a volume envelope and a fade-out
fn adsr_of(ins: &Ins, sec_per_tick: f32) -> (f32, f32, f32, f32) {
    let release_floor = 0.012;
    if ins.env.len() < 2 {
        return (0.0, 0.2, 1.0, if ins.fade_s > 0.0 { ins.fade_s.min(6.0) } else { release_floor });
    }
    let peak = ins.env.iter().map(|p| p.1).max().unwrap_or(64).max(1) as f32;
    let t_of = |i: usize| ins.env[i].0 as f32 * sec_per_tick;
    let ipeak = ins.env.iter().position(|p| p.1 as f32 >= peak - 0.5).unwrap_or(0);
    let attack = t_of(ipeak);
    let (sustain, decay, rel_env) = match ins.env_sustain {
        Some(si) if si >= ipeak => {
            let level = ins.env[si].1 as f32 / peak;
            let tail = t_of(ins.env.len() - 1) - t_of(si);
            (level, (t_of(si) - t_of(ipeak)).max(0.0), tail.max(0.0))
        }
        _ => {
            let last = ins.env.len() - 1;
            // no sustain point: it runs to the end of the envelope on its own; keep the last level
            (ins.env[last].1 as f32 / peak, (t_of(last) - t_of(ipeak)).max(0.0), 0.0)
        }
    };
    (attack, decay, sustain.clamp(0.0, 1.0), (rel_env.max(ins.fade_s)).clamp(release_floor, 6.0))
}

/// an Impulse Tracker cutoff (0..127, frequency 110 * 2^(0.25 + c / 24)) as the Sampler's 0..1 cutoff; 127 is open
pub fn it_cut_norm(c: f32) -> f32 {
    if c >= 127.0 {
        return 1.0;
    }
    let f = 110.0 * 2f32.powf(0.25 + c / 24.0);
    ((f / 40.0).ln() / (20000.0f32 / 40.0).ln()).clamp(0.0, 1.0)
}

fn norm_a(s: f32) -> f32 {
    ((s - 0.0005).max(0.0) / 4.0).sqrt().clamp(0.0, 1.0)
}
fn norm_d(s: f32) -> f32 {
    ((s - 0.005).max(0.0) / 4.0).sqrt().clamp(0.0, 1.0)
}
fn norm_r(s: f32) -> f32 {
    ((s - 0.004).max(0.0) / 6.0).sqrt().clamp(0.0, 1.0)
}

fn write_wav(path: &std::path::Path, data: &[i16], rate: u32) -> Result<(), String> {
    let spec = hound::WavSpec { channels: 1, sample_rate: rate.clamp(1000, 384000), bits_per_sample: 16, sample_format: hound::SampleFormat::Int };
    let mut w = hound::WavWriter::create(path, spec).map_err(|e| format!("{}: {e}", path.display()))?;
    for v in data {
        w.write_sample(*v).map_err(|e| e.to_string())?;
    }
    w.finalize().map_err(|e| e.to_string())
}

/// The Sampler config (JSON, absolute file paths) of one tracker instrument, with its WAV files written to `dir`, and the instrument parameters.
pub fn sampler_for(ins: &Ins, idx: usize, dir: &std::path::Path, sec_per_tick: f32) -> Result<Option<(String, Vec<(String, f32)>)>, String> {
    if ins.smps.is_empty() {
        return Ok(None);
    }
    std::fs::create_dir_all(dir).map_err(|e| format!("{}: {e}", dir.display()))?;
    let mut zones: Vec<Zone> = vec![];
    // zones: runs of keys with the same sample and note shift
    let mut files: Vec<Option<String>> = vec![None; ins.smps.len()];
    let mut k = 0usize;
    while k < 128 {
        let si = ins.map.get(k).copied().unwrap_or(255);
        if si == 255 || si as usize >= ins.smps.len() || ins.smps[si as usize].data.is_empty() {
            k += 1;
            continue;
        }
        let sh = ins.shift.get(k).copied().unwrap_or(0);
        let mut e = k;
        while e + 1 < 128 && ins.map.get(e + 1).copied() == Some(si) && ins.shift.get(e + 1).copied().unwrap_or(0) == sh {
            e += 1;
        }
        let smp = &ins.smps[si as usize];
        let file = match &files[si as usize] {
            Some(f) => f.clone(),
            None => {
                let name: String = smp.name.chars().map(|c| if c.is_alphanumeric() { c } else { '_' }).collect();
                let f = dir.join(format!("{:02}_{:02}_{name}.wav", idx + 1, si)).to_string_lossy().replace(std::path::MAIN_SEPARATOR, "/");
                write_wav(std::path::Path::new(&f), &smp.data, smp.rate)?;
                files[si as usize] = Some(f.clone());
                f
            }
        };
        let mut z = Zone::new(if smp.name.trim().is_empty() { &ins.name } else { &smp.name }, &file);
        z.lo = k as u8;
        z.hi = e as u8;
        z.root = (60 - sh as i32 - smp.rel_note).clamp(0, 127) as u8;
        z.tune = smp.cents;
        // the part is panned by its notes (sample, instrument and channel pan are folded in there)
        z.pan = 0.0;
        z.vol = (smp.vol_db + ins.gain_db).clamp(-48.0, 18.0);
        let frames = smp.data.len() as u32;
        z.loop_end = frames;
        if smp.looped && smp.loop_end > smp.loop_start + 2 {
            z.loop_mode = if smp.pingpong { 2 } else { 1 };
            z.loop_start = smp.loop_start.min(frames);
            z.loop_end = smp.loop_end.min(frames);
        }
        zones.push(z);
        k = e + 1;
    }
    if zones.is_empty() {
        return Ok(None);
    }
    let (a, d, s, r) = adsr_of(ins, sec_per_tick);
    let mut cfg = Config { name: ins.name.clone(), zones, attack: norm_a(a), decay: norm_d(d), sustain: s, release: norm_r(r), ..Config::default() };
    if ins.pan_env.len() >= 2 {
        cfg.pan_env = crate::dsp::sampler::Env { pts: ins.pan_env.iter().map(|p| (p.0 as f32 * sec_per_tick, p.1)).collect(), sustain: ins.pan_sustain, looped: ins.pan_loop };
    }
    cfg.rnd_vol = ins.swing[0];
    cfg.rnd_pan = ins.swing[1];
    cfg.rnd_cut = ins.swing[2];
    cfg.rnd_res = ins.swing[3];
    if let Some(c) = ins.cutoff {
        cfg.it_cut = c as f32;
        cfg.cutoff = it_cut_norm(c as f32);
    }
    if let Some(r) = ins.reso {
        cfg.reso = (r as f32 / 127.0).clamp(0.0, 1.0);
    }
    if ins.filter_env.len() >= 2 {
        cfg.filter_env = crate::dsp::sampler::Env { pts: ins.filter_env.iter().map(|p| (p.0 as f32 * sec_per_tick, p.1)).collect(), sustain: ins.filter_sustain, looped: ins.filter_loop };
    }
    if ins.pitch_env.len() >= 2 {
        cfg.pitch_env = crate::dsp::sampler::Env { pts: ins.pitch_env.iter().map(|p| (p.0 as f32 * sec_per_tick, p.1)).collect(), sustain: ins.pitch_sustain, looped: ins.pitch_loop };
    }
    if ins.env.len() >= 2 {
        // the real multi-point envelope, in seconds
        cfg.env = ins.env.iter().map(|p| (p.0 as f32 * sec_per_tick, p.1 as f32 / 64.0)).collect();
        cfg.env_sustain = ins.env_sustain;
        cfg.env_loop = ins.env_loop;
        cfg.fade = ins.fade_s;
    } else if ins.fade_s > 0.0 {
        // no envelope: the sound fades out after the key-off
        cfg.env = vec![(0.0, 1.0), (0.001, 1.0)];
        cfg.env_sustain = Some(1);
        cfg.fade = ins.fade_s;
    }
    let json = serde_json::to_string(&cfg).map_err(|e| e.to_string())?;
    Ok(Some((json, vec![])))
}

// ── the import ───────────────────────────────────────────────────────────────────

/// What was imported, for the status line.
pub struct Report {
    pub format: &'static str,
    pub title: String,
    pub channels: usize,
    pub instruments: usize,
    pub notes: usize,
    pub ignored: usize,
    pub dropped: usize,
    pub arps: usize,
    pub slides: usize,
    pub retrigs: usize,
}

/// Parse a module, write its samples (beside `dir`) and return it as parts, each with the Sampler that plays the module's own samples.
pub fn parse(data: &[u8], path: &str, dir: &std::path::Path) -> Result<(ImportedMidi, Report), String> {
    let m = parse_module(data)?;
    let played = play(&m);
    let stem: String = std::path::Path::new(path).file_stem().map(|s| s.to_string_lossy().into_owned()).unwrap_or_else(|| "module".into());
    let safe: String = stem.chars().map(|c| if c.is_alphanumeric() || c == '-' { c } else { '_' }).collect();
    // a digest keeps two modules of the same name from overwriting each other's samples
    let mut h: u32 = 2166136261;
    for b in data.iter().step_by((data.len() / 4096).max(1)) {
        h = (h ^ *b as u32).wrapping_mul(16777619);
    }
    let sdir = dir.join(format!("{safe}_{h:08x}"));
    let spt = 2.5 / m.tempo.max(32) as f32;
    let mut samplers: std::collections::HashMap<usize, Option<(String, Vec<(String, f32)>)>> = std::collections::HashMap::new();
    let mut tracks = vec![];
    let mut total = 0usize;
    for pt in &played.tracks {
        if pt.notes.is_empty() {
            continue;
        }
        let ins = &m.ins[pt.ins.min(m.ins.len().saturating_sub(1))];
        let cfg = match samplers.get(&pt.ins) {
            Some(c) => c.clone(),
            None => {
                let c = sampler_for(ins, pt.ins, &sdir, spt)?;
                samplers.insert(pt.ins, c.clone());
                c
            }
        };
        let Some((json, _)) = cfg else { continue };
        let mut name = if ins.name.trim().is_empty() { format!("Instrument {}", pt.ins + 1) } else { ins.name.trim().to_string() };
        if let Some(c) = pt.chan {
            name = format!("{name} (ch {})", c + 1);
        }
        total += pt.notes.len();
        tracks.push(ImportedTrack {
            name,
            plugin: String::new(),
            named: true,
            program: Some(0),
            bank: 0,
            channel: 0,
            notes: pt.notes.clone(),
            volume: 0.8 * m.gain,
            pan: 0.0,
            vol_auto: pt.vol.iter().map(|(b, v)| (*b, v * m.gain)).collect(),
            pan_auto: vec![],
            bend_auto: pt.bend.clone(),
            bend_range: pt.bend_range,
            samples: vec![],
            sampler: Some((json, vec![])),
            percussive: looks_percussive(&ins.name),
            extra_auto: { let mut v = vec![]; if !pt.cut.is_empty() { v.push(("cutoff".to_string(), pt.cut.clone())); } if !pt.res.is_empty() { v.push(("reso".to_string(), pt.res.clone())); } v },
        });
    }
    let bpm = played.tempos.first().map(|t| t.1);
    let rep = Report { format: m.format, title: m.title.clone(), channels: m.nch, instruments: samplers.len(), notes: total, ignored: m.ignored, dropped: played.dropped, arps: played.arps, slides: played.slides, retrigs: played.retrigs };
    Ok((ImportedMidi { tracks, bpm, tempos: played.tempos, length_beats: played.beats }, rep))
}

/// an instrument name that sounds like a drum or percussion piece
pub fn looks_percussive(name: &str) -> bool {
    let n = name.to_lowercase();
    ["kick", "bass drum", "bassdrum", "snare", "hat", "cymbal", "crash", "ride", "splash", "tom", "clap", "rim", "cowbell", "conga", "bongo", "wood block", "woodblock", "tambourine", "shaker", "perc", "drum", "cabasa", "claves", "triangle", "agogo", "timbale", "stick"].iter().any(|w| n.contains(w))
}

← Back to index