aboutsummaryrefslogblamecommitdiff
path: root/src/player.rs
blob: 6afc17a549c8a3b4547324ce3ada514d4fd66092 (plain) (tree)
1
2
3

                           
                                          



















































                                                                                                                                     














                                                                                                              















































                                                                                                                                                         







                                         
                              

                                     


















                                                                                                    
                                                                                                                

                                                                                                                                

                                                                                  








                                                                                                       
                                                                                                                      





                                                                                                                
                                                                                                                      
























                                                                                                                                             
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 }
    }
}