use crate::mixer::{MixerVoice, XmPitch, XmPitchTable};
use crate::xm::{
XmCell, XmEnvelope, XmFrequencyTable, XmHeader, XmInstrument, XmPattern, XmSampleLoopMode,
XmVolume,
};
#[rustfmt::skip]
const SINE_TABLE: [i8; 64] = [
0, 12, 25, 37, 49, 60, 71, 81,
90, 98, 106, 112, 117, 122, 125, 126,
127, 126, 125, 122, 117, 112, 106, 98,
90, 81, 71, 60, 49, 37, 25, 12,
0, -12, -25, -37, -49, -60, -71, -81,
-90, -98,-106,-112,-117,-122,-125,-126,
-127,-126,-125,-122,-117,-112,-106, -98,
-90, -81, -71, -60, -49, -37, -25, -12,
];
const FADEOUT_MAX: i32 = 65536;
#[derive(Clone, Debug, Default)]
pub struct XmChannel {
pub instrument: u8,
pub sample_in_instr: u8,
pub pattern_note: u8,
pub finetune: i8,
pub relative_note: i8,
pub volume: u8,
pub voice: MixerVoice,
pub effect: u8,
pub effect_param: u8,
pub key_on: bool,
pub vol_env_tick: u16,
pub vol_env_seg: u8,
pub vol_env_value: u8,
pub pan_env_tick: u16,
pub pan_env_seg: u8,
pub pan_env_value: u8,
pub fadeout: i32,
pub base_volume: u8,
pub base_panning: u8,
pub period: f32,
pub porta_target: f32,
pub porta_speed: u8,
pub vib_pos: u8,
pub vib_speed: u8,
pub vib_depth: u8,
pub auto_vib_pos: u8,
pub auto_vib_sweep_cnt: u16,
pub vol_slide_mem: u8,
pub vol_slide_col_mem: u8,
pub porta_updown_mem: u8,
pub note_delay_tick: u8,
pub note_cut_tick: u8,
pub pending_note: u8,
pub pending_instrument: u8,
pub pending_volume: u8,
}
pub struct XmPlayerState {
pub instruments: Vec<XmInstrument>,
pub patterns: Vec<XmPattern>,
pub order: Vec<u8>,
pub song_length: u16,
pub restart_position: u16,
pub pitch: XmPitch,
pub channels: Vec<XmChannel>,
pub speed: u8,
pub bpm: u8,
pub sample_rate: u32,
pub order_index: usize,
pub row: u16,
pub tick: u8,
pub tick_sample_cursor: u32,
pub ended: bool,
pub pending_order_jump: Option<u16>,
pub pending_break_row: Option<u16>,
pub loops: u16,
}
impl XmPlayerState {
pub fn new(
header: &XmHeader,
instruments: Vec<XmInstrument>,
patterns: Vec<XmPattern>,
sample_rate: u32,
) -> Self {
let pitch = XmPitch {
table: match header.frequency_table {
XmFrequencyTable::Amiga => XmPitchTable::Amiga,
XmFrequencyTable::Linear => XmPitchTable::Linear,
},
};
let n_ch = header.num_channels as usize;
let channels = (0..n_ch).map(|_| XmChannel::default()).collect();
let speed = header.default_tempo.max(1) as u8;
let bpm = header.default_bpm.max(1) as u8;
let order = header.order.clone();
XmPlayerState {
instruments,
patterns,
order,
song_length: header.song_length,
restart_position: header.restart_position,
pitch,
channels,
speed,
bpm,
sample_rate,
order_index: 0,
row: 0,
tick: 0,
tick_sample_cursor: 0,
ended: false,
pending_order_jump: None,
pending_break_row: None,
loops: 0,
}
}
pub fn samples_per_tick(&self) -> u32 {
((self.sample_rate as f32) * 2.5 / self.bpm as f32).max(1.0) as u32
}
fn cell_at(&self, row: u16, ch: usize) -> Option<XmCell> {
let pat_idx = *self.order.get(self.order_index)? as usize;
let p = self.patterns.get(pat_idx)?;
p.rows.get(row as usize)?.get(ch).copied()
}
fn resolve_sample(&self, pattern_note: u8, instrument: u8) -> Option<(usize, usize)> {
if instrument == 0 || pattern_note == 0 || pattern_note > 96 {
return None;
}
let inst_idx = (instrument as usize).checked_sub(1)?;
let inst = self.instruments.get(inst_idx)?;
if inst.samples.is_empty() {
return None;
}
let map_idx = (pattern_note - 1) as usize;
let sample_idx = if map_idx < inst.sample_map.len() {
inst.sample_map[map_idx] as usize
} else {
0
};
let sample_idx = sample_idx.min(inst.samples.len().saturating_sub(1));
Some((inst_idx, sample_idx))
}
fn enter_row(&mut self) {
for ch_idx in 0..self.channels.len() {
let Some(cell) = self.cell_at(self.row, ch_idx) else {
continue;
};
let is_tone_porta_cell = cell.effect_type == 0x03
|| cell.effect_type == 0x05
|| matches!(cell.volume_kind(), XmVolume::TonePorta(_));
let row_pattern_note = self.channels[ch_idx].pattern_note;
let instrument_change_resolved = if cell.instrument != 0 {
self.resolve_sample(row_pattern_note.max(49), cell.instrument)
} else {
None
};
let note_resolved = if cell.has_note() {
let inst = if cell.instrument != 0 {
cell.instrument
} else {
self.channels[ch_idx].instrument
};
self.resolve_sample(cell.note, inst)
} else {
None
};
let ch = &mut self.channels[ch_idx];
ch.effect = cell.effect_type;
ch.effect_param = cell.effect_param;
ch.note_delay_tick = 0;
ch.note_cut_tick = 0;
if cell.instrument != 0 {
ch.instrument = cell.instrument;
if let Some((i, s)) = instrument_change_resolved {
let sample = &self.instruments[i].samples[s];
ch.volume = sample.volume.min(64);
ch.base_volume = ch.volume;
ch.finetune = sample.finetune;
ch.relative_note = sample.relative_note;
ch.base_panning = sample.panning;
}
}
match cell.volume_kind() {
XmVolume::Empty => {}
XmVolume::SetVolume(v) => {
ch.volume = v.min(64);
ch.base_volume = ch.volume;
}
XmVolume::SetPanning(p) => {
ch.base_panning = (p as u16 * 17).min(255) as u8;
}
XmVolume::VolumeSlideUp(p) | XmVolume::VolumeSlideDown(p) => {
if p != 0 {
ch.vol_slide_col_mem = p;
}
}
XmVolume::FineVolumeSlideUp(p) => {
ch.volume = (ch.volume as u16 + p as u16).min(64) as u8;
ch.base_volume = ch.volume;
}
XmVolume::FineVolumeSlideDown(p) => {
ch.volume = ch.volume.saturating_sub(p);
ch.base_volume = ch.volume;
}
XmVolume::SetVibratoSpeed(p) => {
if p != 0 {
ch.vib_speed = p;
}
}
XmVolume::Vibrato(p) => {
if p != 0 {
ch.vib_depth = p;
}
}
XmVolume::PanningSlideLeft(_) | XmVolume::PanningSlideRight(_) => {
}
XmVolume::TonePorta(p) => {
if p != 0 {
ch.porta_speed = p << 4;
}
}
}
let ep = ch.effect_param;
match ch.effect {
0x01 | 0x02
if ep != 0 => {
ch.porta_updown_mem = ep;
}
0x03
if ep != 0 => {
ch.porta_speed = ep;
}
0x04 => {
let vx = ep >> 4;
let vy = ep & 0x0F;
if vx != 0 {
ch.vib_speed = vx;
}
if vy != 0 {
ch.vib_depth = vy;
}
}
0x05 | 0x06 | 0x0A
if ep != 0 => {
ch.vol_slide_mem = ep;
}
_ => {}
}
let table = self.pitch.table;
if is_tone_porta_cell && cell.has_note() && ch.period > 0.0 {
ch.pattern_note = cell.note;
if let Some((i, s)) = note_resolved {
let sample = &self.instruments[i].samples[s];
ch.finetune = sample.finetune;
ch.relative_note = sample.relative_note;
let real_note = (cell.note as i32 - 1) + ch.relative_note as i32;
ch.porta_target = note_to_period(table, real_note, ch.finetune as i32);
ch.sample_in_instr = s as u8;
}
} else if cell.has_note() {
ch.pattern_note = cell.note;
if let Some((i, s)) = note_resolved {
let sample = &self.instruments[i].samples[s];
ch.finetune = sample.finetune;
ch.relative_note = sample.relative_note;
if cell.instrument != 0 {
ch.volume = sample.volume.min(64);
ch.base_volume = ch.volume;
}
ch.base_panning = sample.panning;
let real_note = (cell.note as i32 - 1) + ch.relative_note as i32;
let period = note_to_period(table, real_note, ch.finetune as i32);
ch.period = period;
ch.porta_target = period;
let freq = period_to_freq(table, period);
ch.sample_in_instr = s as u8;
let v = ch.volume as f32 / 64.0;
let is_delay = cell.effect_type == 0x0E
&& (cell.effect_param >> 4) == 0x0D
&& (cell.effect_param & 0x0F) != 0;
if is_delay {
ch.note_delay_tick = cell.effect_param & 0x0F;
ch.pending_note = cell.note;
ch.pending_instrument = cell.instrument;
ch.pending_volume = cell.volume;
} else {
ch.voice.trigger(freq, v);
ch.key_on = true;
ch.vol_env_tick = 0;
ch.vol_env_seg = 0;
ch.vol_env_value = 64;
ch.pan_env_tick = 0;
ch.pan_env_seg = 0;
ch.pan_env_value = 32;
ch.fadeout = FADEOUT_MAX;
ch.vib_pos = 0;
ch.auto_vib_pos = 0;
ch.auto_vib_sweep_cnt = 0;
}
}
} else if cell.is_note_off() {
ch.key_on = false;
let inst_idx = ch.instrument.saturating_sub(1) as usize;
let has_vol_env = self
.instruments
.get(inst_idx)
.map(|i| i.volume_envelope.is_on() && !i.volume_envelope.points.is_empty())
.unwrap_or(false);
if !has_vol_env {
ch.voice.active = false;
}
}
apply_tick0_effect(ch);
}
for ch in &self.channels {
match ch.effect {
0x0B => {
self.pending_order_jump = Some(ch.effect_param as u16);
if self.pending_break_row.is_none() {
self.pending_break_row = Some(0);
}
}
0x0D => {
let row = (ch.effect_param >> 4) as u16 * 10 + (ch.effect_param & 0x0F) as u16;
self.pending_break_row = Some(row);
}
0x0F => {
if ch.effect_param == 0 {
self.ended = true;
} else if ch.effect_param < 0x20 {
self.speed = ch.effect_param;
} else {
self.bpm = ch.effect_param;
}
}
_ => {}
}
}
}
fn advance_tick(&mut self) {
if self.tick == 0 {
self.enter_row();
} else {
for ch_idx in 0..self.channels.len() {
let vol_col = self
.cell_at(self.row, ch_idx)
.map(|c| c.volume_kind())
.unwrap_or(XmVolume::Empty);
apply_tickn_effect(&mut self.channels[ch_idx], vol_col);
}
}
for ch_idx in 0..self.channels.len() {
let inst_idx = self.channels[ch_idx].instrument as usize;
if inst_idx == 0 {
continue;
}
let Some(inst) = self.instruments.get(inst_idx - 1) else {
continue;
};
let vol_env = tick_envelope(
&inst.volume_envelope,
self.channels[ch_idx].vol_env_tick,
self.channels[ch_idx].vol_env_seg,
self.channels[ch_idx].key_on,
64,
);
let pan_env = tick_envelope(
&inst.panning_envelope,
self.channels[ch_idx].pan_env_tick,
self.channels[ch_idx].pan_env_seg,
self.channels[ch_idx].key_on,
32,
);
let fadeout_step = inst.volume_fadeout as i32;
let inst_vib_rate = inst.vibrato_rate;
let inst_vib_depth = inst.vibrato_depth;
let inst_vib_sweep = inst.vibrato_sweep;
let vol_env_on =
inst.volume_envelope.is_on() && !inst.volume_envelope.points.is_empty();
let vol_col_kind = self
.cell_at(self.row, ch_idx)
.map(|c| c.volume_kind())
.unwrap_or(XmVolume::Empty);
let table = self.pitch.table;
let cur_tick = self.tick;
let ch = &mut self.channels[ch_idx];
ch.vol_env_tick = vol_env.next_tick;
ch.vol_env_seg = vol_env.next_seg;
ch.vol_env_value = vol_env.value;
ch.pan_env_tick = pan_env.next_tick;
ch.pan_env_seg = pan_env.next_seg;
ch.pan_env_value = pan_env.value;
if !ch.key_on {
ch.fadeout = (ch.fadeout - fadeout_step).max(0);
if ch.fadeout == 0 {
ch.voice.active = false;
}
}
if ch.note_cut_tick > 0 && cur_tick == ch.note_cut_tick {
ch.volume = 0;
ch.base_volume = 0;
}
if ch.note_delay_tick > 0 && cur_tick == ch.note_delay_tick {
let v = ch.volume as f32 / 64.0;
let freq = period_to_freq(table, ch.period);
ch.voice.trigger(freq, v);
ch.key_on = true;
ch.vol_env_tick = 0;
ch.vol_env_seg = 0;
ch.vol_env_value = 64;
ch.pan_env_tick = 0;
ch.pan_env_seg = 0;
ch.pan_env_value = 32;
ch.fadeout = FADEOUT_MAX;
ch.vib_pos = 0;
ch.auto_vib_pos = 0;
ch.auto_vib_sweep_cnt = 0;
ch.note_delay_tick = 0;
}
let env_scalar = if vol_env_on {
ch.vol_env_value as f32 / 64.0
} else {
1.0
};
let fade_scalar = ch.fadeout as f32 / FADEOUT_MAX as f32;
ch.voice.volume = (ch.volume as f32 / 64.0) * env_scalar * fade_scalar;
let mut period = ch.period;
if (ch.effect == 0x04
|| ch.effect == 0x06
|| matches!(vol_col_kind, XmVolume::Vibrato(_)))
&& ch.vib_depth > 0
{
let lfo = SINE_TABLE[(ch.vib_pos & 0x3F) as usize] as i32;
let offset = (lfo * ch.vib_depth as i32) / 32;
period += offset as f32;
if cur_tick > 0 {
ch.vib_pos = ch.vib_pos.wrapping_add(ch.vib_speed * 4) & 0x3F;
}
}
if inst_vib_depth > 0 && inst_vib_rate > 0 {
let sweep_amp =
if inst_vib_sweep == 0 || ch.auto_vib_sweep_cnt >= inst_vib_sweep as u16 {
1.0
} else {
ch.auto_vib_sweep_cnt as f32 / inst_vib_sweep as f32
};
let lfo = SINE_TABLE[(ch.auto_vib_pos >> 2) as usize] as f32;
let offset = lfo * inst_vib_depth as f32 * sweep_amp / 64.0;
period += offset;
ch.auto_vib_pos = ch.auto_vib_pos.wrapping_add(inst_vib_rate);
ch.auto_vib_sweep_cnt = ch.auto_vib_sweep_cnt.saturating_add(1);
}
if period > 1.0 {
ch.voice.freq = period_to_freq(table, period);
}
}
}
fn next_row(&mut self) {
if let Some(order) = self.pending_order_jump.take() {
self.order_index = order as usize;
self.row = self.pending_break_row.take().unwrap_or(0);
if self.order_index >= self.song_length as usize || self.order_index >= self.order.len()
{
self.maybe_end_or_restart();
}
return;
}
if let Some(row) = self.pending_break_row.take() {
self.row = row;
self.order_index += 1;
if self.order_index >= self.song_length as usize || self.order_index >= self.order.len()
{
self.maybe_end_or_restart();
}
return;
}
self.row += 1;
let pat_len = self
.order
.get(self.order_index)
.and_then(|&o| self.patterns.get(o as usize))
.map(|p| p.num_rows)
.unwrap_or(64);
if self.row >= pat_len {
self.row = 0;
self.order_index += 1;
if self.order_index >= self.song_length as usize || self.order_index >= self.order.len()
{
self.maybe_end_or_restart();
}
}
}
fn maybe_end_or_restart(&mut self) {
let restart = self.restart_position as usize;
if restart < self.song_length as usize && restart < self.order.len() && self.loops == 0 {
self.order_index = restart;
self.loops = self.loops.saturating_add(1);
} else {
self.ended = true;
}
}
pub fn render(&mut self, dst: &mut [i16]) -> usize {
assert!(dst.len() % 2 == 0);
let mut produced = 0usize;
let total_frames = dst.len() / 2;
let out_rate = self.sample_rate as f32;
let n_ch = self.channels.len().max(1);
let headroom = (n_ch as f32 / 2.0).max(1.0);
while produced < total_frames {
if self.ended {
break;
}
if self.tick_sample_cursor == 0 {
self.advance_tick();
}
let spt = self.samples_per_tick().max(1);
let remaining = spt.saturating_sub(self.tick_sample_cursor);
let want = (total_frames - produced).min(remaining as usize);
for _ in 0..want {
let mut l = 0.0f32;
let mut r = 0.0f32;
for (i, ch) in self.channels.iter_mut().enumerate() {
if ch.instrument == 0 {
continue;
}
let Some(inst) = self.instruments.get(ch.instrument as usize - 1) else {
continue;
};
let Some(sample) = inst.samples.get(ch.sample_in_instr as usize) else {
continue;
};
let s = ch.voice.render_one(sample, out_rate);
let pan_base = ch.base_panning as i32;
let env_pan = ch.pan_env_value as i32; let range = 128 - (pan_base - 128).abs();
let final_pan = pan_base + (env_pan - 32) * range / 32;
let final_pan = final_pan.clamp(0, 255) as f32 / 255.0;
let _ = i; l += s * (1.0 - final_pan);
r += s * final_pan;
}
let l = (l / headroom).clamp(-1.0, 1.0);
let r = (r / headroom).clamp(-1.0, 1.0);
let off = produced * 2;
dst[off] = (l * 32767.0) as i16;
dst[off + 1] = (r * 32767.0) as i16;
produced += 1;
}
self.tick_sample_cursor += want as u32;
if self.tick_sample_cursor >= spt {
self.tick_sample_cursor = 0;
self.tick += 1;
if self.tick >= self.speed {
self.tick = 0;
self.next_row();
}
}
}
produced
}
}
fn apply_tick0_effect(ch: &mut XmChannel) {
let ep = ch.effect_param;
let x = ep >> 4;
let y = ep & 0x0F;
match ch.effect {
0x0C => {
ch.volume = ep.min(64);
ch.base_volume = ch.volume;
}
0x0E => {
match x {
0x01
if y != 0 => {
ch.period = (ch.period - (y as f32) * 4.0).max(1.0);
}
0x02
if y != 0 => {
ch.period += (y as f32) * 4.0;
}
0x0A => {
ch.volume = (ch.volume as u16 + y as u16).min(64) as u8;
ch.base_volume = ch.volume;
}
0x0B => {
ch.volume = ch.volume.saturating_sub(y);
ch.base_volume = ch.volume;
}
0x0C => {
if y == 0 {
ch.volume = 0;
ch.base_volume = 0;
} else {
ch.note_cut_tick = y;
}
}
0x0D => {
}
_ => {}
}
}
0x0F => {
}
0x14 => {
ch.key_on = false;
}
0x21 => {
match x {
0x01
if y != 0 => {
ch.period = (ch.period - y as f32).max(1.0);
}
0x02
if y != 0 => {
ch.period += y as f32;
}
_ => {}
}
}
_ => {}
}
}
fn apply_tickn_effect(ch: &mut XmChannel, vol_col: XmVolume) {
let ep = ch.effect_param;
match ch.effect {
0x01 => {
let p = if ep != 0 { ep } else { ch.porta_updown_mem };
ch.period = (ch.period - (p as f32) * 4.0).max(1.0);
}
0x02 => {
let p = if ep != 0 { ep } else { ch.porta_updown_mem };
ch.period += (p as f32) * 4.0;
}
0x03 => {
let speed = (ch.porta_speed as f32) * 4.0;
if (ch.period - ch.porta_target).abs() <= speed {
ch.period = ch.porta_target;
} else if ch.period < ch.porta_target {
ch.period += speed;
} else {
ch.period -= speed;
}
}
0x05 => {
let speed = (ch.porta_speed as f32) * 4.0;
if (ch.period - ch.porta_target).abs() <= speed {
ch.period = ch.porta_target;
} else if ch.period < ch.porta_target {
ch.period += speed;
} else {
ch.period -= speed;
}
apply_vol_slide(ch, ch.vol_slide_mem);
}
0x06 => {
apply_vol_slide(ch, ch.vol_slide_mem);
}
0x0A => {
apply_vol_slide(ch, ch.vol_slide_mem);
}
_ => {}
}
match vol_col {
XmVolume::VolumeSlideUp(p) => {
let amt = if p != 0 { p } else { ch.vol_slide_col_mem };
ch.volume = (ch.volume as u16 + amt as u16).min(64) as u8;
ch.base_volume = ch.volume;
}
XmVolume::VolumeSlideDown(p) => {
let amt = if p != 0 { p } else { ch.vol_slide_col_mem };
ch.volume = ch.volume.saturating_sub(amt);
ch.base_volume = ch.volume;
}
XmVolume::TonePorta(_) => {
let speed = (ch.porta_speed as f32) * 4.0;
if (ch.period - ch.porta_target).abs() <= speed {
ch.period = ch.porta_target;
} else if ch.period < ch.porta_target {
ch.period += speed;
} else {
ch.period -= speed;
}
}
XmVolume::PanningSlideLeft(p) => {
ch.base_panning = ch.base_panning.saturating_sub(p);
}
XmVolume::PanningSlideRight(p) => {
ch.base_panning = ch.base_panning.saturating_add(p);
}
_ => {}
}
}
fn note_to_period(table: XmPitchTable, real_note: i32, finetune: i32) -> f32 {
match table {
XmPitchTable::Linear => {
10.0 * 12.0 * 16.0 * 4.0 - (real_note as f32) * 16.0 * 4.0 - (finetune as f32) / 2.0
}
XmPitchTable::Amiga => {
let n_mod = real_note.rem_euclid(12) as usize;
let n_div = real_note.div_euclid(12) as f32;
let ft = finetune as f32 / 16.0;
let ft_floor = ft.floor();
let frac = ft - ft_floor;
let base_idx = ((n_mod as isize) * 8 + ft_floor as isize).clamp(0, 95) as usize;
let next_idx = (base_idx + 1).min(95);
let p0 = XmPitch::PERIOD_TAB_PUB[base_idx] as f32;
let p1 = XmPitch::PERIOD_TAB_PUB[next_idx] as f32;
let p = p0 * (1.0 - frac) + p1 * frac;
(p * 16.0) / 2.0f32.powf(n_div)
}
}
}
fn period_to_freq(table: XmPitchTable, period: f32) -> f32 {
match table {
XmPitchTable::Linear => {
8363.0 * 2.0f32.powf((6.0 * 12.0 * 16.0 * 4.0 - period) / (12.0 * 16.0 * 4.0))
}
XmPitchTable::Amiga => {
if period <= 0.0 {
0.0
} else {
8363.0 * 1712.0 / period
}
}
}
}
fn apply_vol_slide(ch: &mut XmChannel, mem: u8) {
let hi = mem >> 4;
let lo = mem & 0x0F;
if hi != 0 {
ch.volume = (ch.volume as u16 + hi as u16).min(64) as u8;
} else if lo != 0 {
ch.volume = ch.volume.saturating_sub(lo);
}
ch.base_volume = ch.volume;
}
struct EnvelopeTick {
value: u8,
next_tick: u16,
next_seg: u8,
}
fn tick_envelope(
env: &XmEnvelope,
cur_tick: u16,
cur_seg: u8,
key_on: bool,
default_value: u8,
) -> EnvelopeTick {
if !env.is_on() || env.points.is_empty() {
return EnvelopeTick {
value: default_value,
next_tick: cur_tick,
next_seg: cur_seg,
};
}
let n = env.points.len();
let mut seg = (cur_seg as usize).min(n.saturating_sub(1));
let mut tick = cur_tick;
let value = eval_envelope_at(&env.points, seg, tick);
if env.has_sustain() && key_on {
let sp = (env.sustain_point as usize).min(n - 1);
if tick >= env.points[sp].0 {
tick = env.points[sp].0;
seg = sp.min(n.saturating_sub(2));
return EnvelopeTick {
value,
next_tick: tick,
next_seg: seg as u8,
};
}
}
tick = tick.saturating_add(1);
if env.has_loop() {
let le = (env.loop_end_point as usize).min(n - 1);
let ls = (env.loop_start_point as usize).min(le);
let loop_end_tick = env.points[le].0;
let loop_start_tick = env.points[ls].0;
if tick >= loop_end_tick && loop_end_tick > loop_start_tick {
tick = loop_start_tick;
seg = ls;
}
}
while seg + 1 < n && tick >= env.points[seg + 1].0 {
seg += 1;
}
let last_x = env.points[n - 1].0;
if tick > last_x {
tick = last_x;
}
EnvelopeTick {
value,
next_tick: tick,
next_seg: seg as u8,
}
}
fn eval_envelope_at(points: &[(u16, u16)], seg: usize, tick: u16) -> u8 {
let n = points.len();
if n == 0 {
return 0;
}
if seg >= n - 1 {
return points[n - 1].1.min(64) as u8;
}
let (x0, y0) = points[seg];
let (x1, y1) = points[seg + 1];
if x1 <= x0 {
return y0.min(64) as u8;
}
let t = tick.clamp(x0, x1);
let num = (y1 as i32 - y0 as i32) * (t as i32 - x0 as i32);
let den = (x1 as i32 - x0 as i32).max(1);
let y = y0 as i32 + num / den;
y.clamp(0, 64) as u8
}
#[allow(dead_code)]
fn _loop_mode_unused(m: XmSampleLoopMode) -> XmSampleLoopMode {
m
}
#[cfg(test)]
mod tests {
use super::*;
use crate::xm::XmEnvelope;
fn env_with_points(points: Vec<(u16, u16)>, type_bits: u8) -> XmEnvelope {
XmEnvelope {
points,
sustain_point: 0,
loop_start_point: 0,
loop_end_point: 0,
type_bits,
}
}
#[test]
fn envelope_disabled_returns_default() {
let env = env_with_points(vec![(0, 0), (10, 64)], 0);
let r = tick_envelope(&env, 5, 0, true, 64);
assert_eq!(r.value, 64);
}
#[test]
fn envelope_linear_interpolates() {
let env = env_with_points(vec![(0, 0), (10, 64)], 0x01);
let r = tick_envelope(&env, 5, 0, true, 64);
assert_eq!(r.value, 32);
}
#[test]
fn envelope_sustain_holds_while_key_on() {
let mut env = env_with_points(vec![(0, 0), (5, 64), (10, 0)], 0x01 | 0x02);
env.sustain_point = 1;
let r = tick_envelope(&env, 5, 1, true, 64);
assert_eq!(r.value, 64);
assert_eq!(r.next_tick, 5);
let r = tick_envelope(&env, 5, 1, false, 64);
assert_eq!(r.next_tick, 6);
}
#[test]
fn envelope_loop_wraps() {
let mut env = env_with_points(vec![(0, 0), (5, 64), (10, 32)], 0x01 | 0x04);
env.loop_start_point = 0;
env.loop_end_point = 2;
let r = tick_envelope(&env, 8, 1, true, 64);
assert_eq!(r.next_tick, 9);
let r = tick_envelope(&env, 9, 1, true, 64);
assert_eq!(r.next_tick, 0);
assert_eq!(r.next_seg, 0);
}
#[test]
fn envelope_past_last_point_holds() {
let env = env_with_points(vec![(0, 0), (5, 64)], 0x01);
let r = tick_envelope(&env, 100, 1, true, 64);
assert_eq!(r.value, 64);
assert_eq!(r.next_tick, 5);
}
}