aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Chris Xiong <chirs241097@gmail.com> 2025-03-02 20:21:28 -0500
committerGravatar Chris Xiong <chirs241097@gmail.com> 2025-03-02 20:21:28 -0500
commite9aaf095a805f08fccc4407c8f1dc60780382f63 (patch)
tree5fa279250be742132b290e9637856857ecfe0a15
parent1b75753a315064a97c14742babcc783dedb7bf6f (diff)
downloadit2midi-e9aaf095a805f08fccc4407c8f1dc60780382f63.tar.xz
Actually starting the rewrite.
-rw-r--r--src/main.rs29
-rw-r--r--src/player.rs169
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 }
+ }
+}