use crate::utils::Rational; use crate::itfile::ITFile; use crate::portmod::Effect; pub struct PlayerState { skip_row: Option, skip_ord: Option, current_ord: u8, current_row: u8, current_tick: u8, speed: u8, tempo: u8, rows_per_beat: u8, force_speed: Option, 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, ord: Option, 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_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 } } }) } } pub struct BasePlayerStateEx { loop_detected: bool } 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}) } fn process_row(&self, state: PlayerState, statex: Self::PlayerStateExtension) -> (PlayerState, Self::PlayerStateExtension) { 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}) }, (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}) }, (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 } } }