diff options
author | 2025-03-02 20:21:28 -0500 | |
---|---|---|
committer | 2025-03-02 20:21:28 -0500 | |
commit | e9aaf095a805f08fccc4407c8f1dc60780382f63 (patch) | |
tree | 5fa279250be742132b290e9637856857ecfe0a15 | |
parent | 1b75753a315064a97c14742babcc783dedb7bf6f (diff) | |
download | it2midi-e9aaf095a805f08fccc4407c8f1dc60780382f63.tar.xz |
Actually starting the rewrite.
-rw-r--r-- | src/main.rs | 29 | ||||
-rw-r--r-- | src/player.rs | 169 |
2 files changed, 188 insertions, 10 deletions
diff --git a/src/main.rs b/src/main.rs index e7520c7..8ae139b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,19 +2,28 @@ mod itfile; mod midifile; mod portmod; mod convert; +mod player; mod utils; fn main() -> Result<(), itfile::Error> { - match itfile::load("/home/chrisoft/Music/mods/nb_tear of the sun.it") - { - Ok(f) => + /*match itfile::load("/home/chrisoft/Music/mods/nb_tear of the sun.it") { - let mut conv = convert::Converter::new(&f); - conv.convert(); - let mf = conv.result(); - midifile::write_file("output.mid", &mf)?; - Ok(()) + Ok(f) => + { + let mut conv = convert::Converter::new(&f); + conv.convert(); + let mf = conv.result(); + midifile::write_file("output.mid", &mf)?; + Ok(()) + } + Err(e) => Err(e) + }?;*/ + match itfile::load("/home/chrisoft/Music/mods/nb_tear of the sun.it") { + Ok(f) => { + let player = player::BasePlayer::new(&f); + player::Player::play(&player); + Ok(()) + } + Err(e) => Err(e) } - Err(e) => Err(e) - } } diff --git a/src/player.rs b/src/player.rs new file mode 100644 index 0000000..d33b622 --- /dev/null +++ b/src/player.rs @@ -0,0 +1,169 @@ +use crate::utils::Rational; +use crate::itfile::ITFile; +use crate::portmod::Effect; + +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_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 } + } +} |