use crate::core::instr_robsid::{PitchAction, ProgramEnd, VoiceProgram};
pub const MAX_STEPS: usize = 32;
#[derive(Clone, Copy, Default, PartialEq, Eq, Debug)]
pub struct FlatStep {
pub ctrl: u8,
pub bend: i16,
pub fixed_hi: u8,
pub is_fixed: bool,
}
#[derive(Clone, Copy)]
pub struct ProgramSteps {
steps: [FlatStep; MAX_STEPS],
len: u8,
loop_to: Option<u8>,
}
impl Default for ProgramSteps {
fn default() -> Self {
Self {
steps: [FlatStep::default(); MAX_STEPS],
len: 0,
loop_to: None,
}
}
}
impl ProgramSteps {
#[inline]
pub fn is_empty(&self) -> bool {
self.len == 0
}
pub fn from_model(p: &VoiceProgram) -> (Self, usize) {
let mut out = Self::default();
let n = p.steps.len().min(MAX_STEPS);
for (i, s) in p.steps.iter().take(n).enumerate() {
out.steps[i] = FlatStep {
ctrl: s.shape.to_ctrl(),
bend: match s.pitch {
PitchAction::Bend(b) => b,
PitchAction::Fixed(_) => 0,
},
fixed_hi: match s.pitch {
PitchAction::Fixed(h) => h,
PitchAction::Bend(_) => 0,
},
is_fixed: matches!(s.pitch, PitchAction::Fixed(_)),
};
}
out.len = n as u8;
out.loop_to = match p.end {
ProgramEnd::Hold => None,
ProgramEnd::Loop { step } => Some((step as usize).min(n.saturating_sub(1)) as u8),
};
(out, p.steps.len() - n)
}
}
#[derive(Clone, Copy, Default)]
pub struct ProgramState {
pub cursor: u8,
pub bend_acc: i16,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum ProgramPitch {
Bend,
FixedHi(u8),
Released,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct StepOutput {
pub ctrl: u8,
pub pitch: ProgramPitch,
pub masked: bool,
}
pub fn step(prog: &ProgramSteps, st: &mut ProgramState) -> Option<StepOutput> {
if prog.len == 0 {
return None;
}
let last = prog.len - 1;
if st.cursor >= prog.len {
match prog.loop_to {
Some(t) => st.cursor = t,
None => {
return Some(StepOutput {
ctrl: prog.steps[last as usize].ctrl,
pitch: ProgramPitch::Released,
masked: true,
})
}
}
}
let s = prog.steps[st.cursor as usize];
let out = if s.is_fixed {
StepOutput {
ctrl: s.ctrl,
pitch: ProgramPitch::FixedHi(s.fixed_hi),
masked: false,
}
} else {
st.bend_acc = st.bend_acc.wrapping_add(s.bend);
StepOutput {
ctrl: s.ctrl,
pitch: ProgramPitch::Bend,
masked: true,
}
};
st.cursor += 1;
Some(out)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::instr_robsid::{WaveShape, WaveStep};
use alloc::vec;
fn shape(ctrl: u8) -> WaveShape {
WaveShape::from_ctrl(ctrl)
}
fn bend(ctrl: u8, b: i16) -> WaveStep {
WaveStep {
shape: shape(ctrl),
pitch: PitchAction::Bend(b),
}
}
fn fixed(ctrl: u8, hi: u8) -> WaveStep {
WaveStep {
shape: shape(ctrl),
pitch: PitchAction::Fixed(hi),
}
}
#[test]
fn wave_shape_round_trips_every_control_byte() {
for b in 0u8..=255 {
assert_eq!(WaveShape::from_ctrl(b).to_ctrl(), b, "ctrl {b:#04x}");
}
}
#[test]
fn lion_heart_instr0_program_matches_the_replayer_table() {
let p = VoiceProgram {
steps: vec![
fixed(0x81, 0x20),
bend(0x41, 0x0401),
bend(0x40, 0x0240),
fixed(0x80, 0x30),
fixed(0x80, 0x15),
],
end: ProgramEnd::Hold,
};
let (flat, dropped) = ProgramSteps::from_model(&p);
assert_eq!(dropped, 0);
let mut st = ProgramState::default();
let o = step(&flat, &mut st).unwrap();
assert_eq!(o.pitch, ProgramPitch::FixedHi(0x20));
assert!(!o.masked, "a Fixed step bypasses the end-of-note gate mask");
assert_eq!(st.bend_acc, 0, "a Fixed step does not touch the bend");
let o = step(&flat, &mut st).unwrap();
assert_eq!(o.pitch, ProgramPitch::Bend);
assert!(o.masked);
assert_eq!(st.bend_acc, 0x0401);
let o = step(&flat, &mut st).unwrap();
assert_eq!(
st.bend_acc,
0x0401 + 0x0240,
"bends sum, they do not replace"
);
assert!(
!WaveShape::from_ctrl(o.ctrl).gate,
"step 3 releases the note mid-program"
);
assert_eq!(
step(&flat, &mut st).unwrap().pitch,
ProgramPitch::FixedHi(0x30)
);
assert_eq!(
step(&flat, &mut st).unwrap().pitch,
ProgramPitch::FixedHi(0x15)
);
for _ in 0..4 {
let o = step(&flat, &mut st).unwrap();
assert_eq!(
o.pitch,
ProgramPitch::Released,
"the terminator no longer pins the pitch"
);
assert_eq!(o.ctrl, 0x80, "the last step's waveform is what persists");
assert!(
o.masked,
"the terminator goes through the gate-masked write"
);
}
assert_eq!(
st.bend_acc,
0x0401 + 0x0240,
"the terminator must not keep bending the pitch"
);
}
#[test]
fn loop_cycles_and_keeps_accumulating() {
let p = VoiceProgram {
steps: vec![bend(0x11, 10), bend(0x21, 20), bend(0x41, 30)],
end: ProgramEnd::Loop { step: 1 },
};
let (flat, _) = ProgramSteps::from_model(&p);
let mut st = ProgramState::default();
let ctrls: alloc::vec::Vec<u8> =
(0..7).map(|_| step(&flat, &mut st).unwrap().ctrl).collect();
assert_eq!(ctrls, vec![0x11, 0x21, 0x41, 0x21, 0x41, 0x21, 0x41]);
assert_eq!(st.bend_acc, 10 + 20 + 30 + 20 + 30 + 20 + 30);
}
#[test]
fn out_of_range_loop_target_clamps() {
let p = VoiceProgram {
steps: vec![bend(0x11, 0), bend(0x21, 0)],
end: ProgramEnd::Loop { step: 99 },
};
let (flat, _) = ProgramSteps::from_model(&p);
let mut st = ProgramState::default();
for _ in 0..6 {
let o = step(&flat, &mut st).unwrap();
assert!(o.ctrl == 0x11 || o.ctrl == 0x21);
}
}
#[test]
fn oversized_program_reports_what_it_dropped() {
let p = VoiceProgram {
steps: vec![bend(0x11, 1); MAX_STEPS + 5],
end: ProgramEnd::Hold,
};
let (flat, dropped) = ProgramSteps::from_model(&p);
assert_eq!(dropped, 5);
assert!(!flat.is_empty());
}
#[test]
fn empty_program_yields_nothing() {
let (flat, _) = ProgramSteps::from_model(&VoiceProgram::default());
let mut st = ProgramState::default();
assert!(flat.is_empty());
assert!(step(&flat, &mut st).is_none());
}
}