use crate::utils::Rational;
use crate::itfile::ITFile;
use crate::portmod::{Effect, NoteCommand};
pub struct PlayerState {
skip_row: Option<u8>,
skip_ord: Option<u8>,
current_ord: u8,
current_row: u8,
current_tick: u8,
speed: u8,
tempo: u8,
rows_per_beat: u8,
force_speed: Option<Rational>,
loop_start: u8,
loop_ctr: u8,
row_extension: u8,
in_rep: bool
}
pub trait Player {
type PlayerStateExtension;
fn initial_state(&self) -> (PlayerState, Self::PlayerStateExtension);
fn process_row(&self, state: PlayerState, custom_state: Self::PlayerStateExtension) -> (PlayerState, Self::PlayerStateExtension);
fn terminate_playback(&self, state: &PlayerState, custom_state: &Self::PlayerStateExtension) -> bool;
fn play(&self) {
let (mut state, mut state_ex) = self.initial_state();
while !self.terminate_playback(&state, &state_ex) {
(state, state_ex) = self.process_row(state, state_ex);
}
}
/// Used for effects Bxx, Cxx and SBx
///
/// passing None to row or ord if it's unused
fn skip_to(row: Option<u8>, ord: Option<u8>, state: PlayerState) -> PlayerState
{
println!("requested skipping to row {:?} of ord #{:?}, current ord #{}", row, ord, state.current_ord);
let r = PlayerState {
skip_row: if let Some(skip_row) = row { Some(skip_row) } else { state.skip_row },
skip_ord: if let Some(skip_ord) = ord { Some(skip_ord) } else {
if row.is_some() && state.skip_ord.is_none() { Some(state.current_ord + 1) } else { state.skip_ord }
},
..state
};
println!("skipping to {:?} {:?}", r.skip_row, r.skip_ord);
r
}
}
pub struct BasePlayer<'itfile> {
it: &'itfile ITFile
}
impl<'itfile> BasePlayer<'itfile> {
pub fn new(it: &ITFile) -> BasePlayer { BasePlayer{it} }
fn process_voice_commands(&self, state: &PlayerState, chmem: [ChannelMemory; 64]) -> [ChannelMemory; 64] {
let pat = self.it.orders[state.current_ord as usize] as usize;
std::array::from_fn(|ch| {
let itcell = self.it.patterns[pat].cell_at(state.current_row, ch as u8);
let cell = crate::portmod::Cell::from_it_cell(itcell);
let (note, pitch) = if let Some(ncmd) = cell.note {
match ncmd {
NoteCommand::NoteOn(n) => (Some(n), Some(n.into())),
_ => (None, None)
}
} else { (chmem[ch].note, chmem[ch].pitch) };
let inst = if cell.inst.is_some() { cell.inst } else { chmem[ch].inst };
ChannelMemory{note, inst, pitch}
})
}
fn process_transport_commands(&self, state: PlayerState) -> PlayerState {
let pat = self.it.orders[state.current_ord as usize] as usize;
(0..64).fold(state, |state, ch| {
let cell = self.it.patterns[pat].cell_at(state.current_row, ch);
let e = Effect::from_it_efx((cell.efx, cell.fxp));
if state.current_tick == 0 {
match e {
Effect::SetSpeed(s) => PlayerState{speed: s, ..state},
Effect::PattLoopStart => PlayerState{loop_start: state.current_row, ..state},
Effect::SetTempo(t) => PlayerState{tempo: t, ..state},
Effect::RowExtention(t) => PlayerState{row_extension: state.row_extension + t, ..state},
_ => state
}
}
else if state.current_tick == state.speed - 1 {
match e {
Effect::PosJump(p) => Self::skip_to(None, Some(p), state),
Effect::PattBreak(r) => Self::skip_to(Some(r), None, state),
Effect::PattLoop(c) =>
match state.loop_ctr
{
u8::MAX => PlayerState{loop_ctr: 1, .. Self::skip_to(Some(state.loop_start), Some(state.current_ord), state)},
_ if state.loop_ctr >= c => PlayerState{loop_ctr: !0, ..state},
_ => PlayerState{loop_ctr: state.loop_start + 1, .. Self::skip_to(Some(state.loop_start), Some(state.current_ord), state)}
},
Effect::PattDelay(c) =>
match state.loop_ctr
{
u8::MAX => PlayerState{loop_ctr: 1, in_rep: true, .. Self::skip_to(Some(state.current_row), Some(state.current_ord), state)},
_ if state.loop_ctr >= c => PlayerState{loop_ctr: !0, in_rep: false, ..state},
_ => PlayerState{loop_ctr: state.loop_ctr + 1, .. Self::skip_to(Some(state.current_row), Some(state.current_ord), state)}
},
Effect::TempoSlideDown(v) => PlayerState{tempo: state.tempo - v, ..state},
Effect::TempoSlideUp(v) => PlayerState{tempo: state.tempo + v, ..state},
_ => state
}
} else {
match e
{
Effect::TempoSlideDown(v) => PlayerState{tempo: state.tempo - v, ..state},
Effect::TempoSlideUp(v) => PlayerState{tempo: state.tempo + v, ..state},
_ => state
}
}
})
}
}
//TODO: proper voice system in the future
#[derive(Default)]
pub struct ChannelMemory {
pub note: Option<u8>,
pub inst: Option<u8>,
pub pitch: Option<Rational>,
}
pub struct BasePlayerStateEx {
loop_detected: bool,
channel_mems: [ChannelMemory; 64]
}
impl<'itfile> Player for BasePlayer<'itfile> {
type PlayerStateExtension = BasePlayerStateEx;
fn initial_state(&self) -> (PlayerState, Self::PlayerStateExtension) {
(PlayerState{
skip_row: None,
skip_ord: None,
current_ord: 0,
current_row: 0,
current_tick: 0,
speed: self.it.header.speed,
tempo: self.it.header.tempo,
rows_per_beat: self.it.trkext.as_ref().and_then(|mptext| mptext.rpb).unwrap_or(0) as u8,
force_speed: None,
loop_start: 0,
loop_ctr: u8::MAX,
row_extension: 0,
in_rep: false
}, BasePlayerStateEx{ loop_detected: false, channel_mems: std::array::from_fn(|_| Default::default()) })
}
fn process_row(&self, state: PlayerState, statex: Self::PlayerStateExtension) -> (PlayerState, Self::PlayerStateExtension) {
let chmem_next = self.process_voice_commands(&state, statex.channel_mems);
let statex = BasePlayerStateEx{channel_mems: chmem_next, ..statex};
let state = self.process_transport_commands(state);
let (state_next, statex) =
if state.current_tick + 1 < state.speed + state.row_extension { // still within the current row
(PlayerState{current_tick: state.current_tick + 1, ..state}, statex)
} else { // about to start the next row
let state = PlayerState{row_extension: 0, current_tick: 0, ..state};
let (state, statex) = match (state.skip_row, state.skip_ord) {
(Some(skip_row), Some(skip_ord)) => {
(PlayerState{current_row: skip_row, current_ord: skip_ord, ..state},
BasePlayerStateEx{loop_detected: skip_ord < state.current_ord && !state.loop_ctr == 0, ..statex})
},
(Some(skip_row), None) => { // this is also handled in Player::skip_to... only keep one of them?
(PlayerState{current_row: skip_row, current_ord: state.current_ord + 1, ..state}, statex)
},
(None, Some(skip_ord)) => {
(PlayerState{current_row: 0, current_ord: skip_ord, ..state},
BasePlayerStateEx{loop_detected: skip_ord < state.current_ord && !state.loop_ctr == 0, ..statex})
},
(None, None) => {
let (row, ord) = if state.current_row + 1 < self.it.patterns[self.it.orders[state.current_ord as usize] as usize].nrows {
(state.current_row + 1, state.current_ord)
} else {
(0, state.current_ord + 1)
};
(PlayerState{current_row: row, current_ord: ord, ..state}, statex)
}
};
let state = PlayerState{skip_row: None, skip_ord: None, ..state};
let state = if self.it.orders.get(state.current_ord as usize).is_some_and(|o| o == &0xfe) {
PlayerState{current_ord: state.current_ord + 1, ..state}
} else { state };
(state, statex)
};
(state_next, statex)
}
fn terminate_playback(&self, state: &PlayerState, custom_state: &Self::PlayerStateExtension) -> bool {
if custom_state.loop_detected { true }
else if let Some(o) = self.it.orders.get(state.current_ord as usize) {
o == &0xff
} else { true }
}
}