use xmrs::prelude::*;
use super::Channel;
#[derive(Clone, Copy, Default, PartialEq, Eq)]
pub(crate) enum OplEdge {
#[default]
None,
Trigger,
KeyOff,
Cut,
}
#[derive(Clone, Default)]
pub(crate) struct OplChannelState {
pub instr: Option<usize>,
pub pitch: Option<Pitch>,
pub edge: OplEdge,
}
pub(crate) struct OplChannelUpdate {
pub edge: OplEdge,
pub held: Option<OplHeld>,
}
pub(crate) struct OplHeld {
pub instr: usize,
pub milli_hz: u32,
pub vol63: u8,
pub pan_l: bool,
pub pan_r: bool,
}
impl<'a> Channel<'a> {
fn row_has_note_event(&self) -> bool {
matches!(
self.current.event,
CellEvent::NoteOn { .. }
| CellEvent::NoteOnGhost { .. }
| CellEvent::NoteOff { .. }
| CellEvent::NoteCut
| CellEvent::NoteFade
| CellEvent::InstrReset
)
}
pub(super) fn row_targets_opl(&self) -> Option<usize> {
if !self.row_has_note_event() {
return None;
}
if self.current.has_instrument_column() {
match self.current_track_instrument {
Some(i) => match self.module.instrument.get(i).map(|x| &x.instr_type) {
Some(InstrumentType::Opl(_)) => Some(i),
_ => None,
},
None => self.opl.instr,
}
} else {
self.opl.instr
}
}
pub(super) fn tick0_opl(&mut self, opl_instr: usize) {
match self.current.event {
CellEvent::NoteOn { pitch, velocity } | CellEvent::NoteOnGhost { pitch, velocity } => {
self.opl.instr = Some(opl_instr);
self.opl.pitch = Some(pitch);
self.opl.edge = OplEdge::Trigger;
self.volume = velocity;
self.struck_this_tick = true;
if let Some(delta) = self.opl_finetune_delta(opl_instr) {
self.note = super::compose_played_pitch(pitch, delta);
self.period = self.period_helper.note_to_period(self.note);
self.effective_period = self.period;
}
}
CellEvent::InstrReset => {
self.opl.instr = Some(opl_instr);
self.opl.edge = OplEdge::Trigger;
self.struck_this_tick = true;
}
CellEvent::NoteOff { .. } | CellEvent::NoteFade => {
self.opl.edge = OplEdge::KeyOff;
}
CellEvent::NoteCut => {
self.opl.edge = OplEdge::Cut;
self.opl.instr = None;
}
CellEvent::None => {}
}
}
pub(super) fn opl_clear_for_sample(&mut self) {
if self.row_has_note_event() && self.opl.instr.is_some() {
self.opl.edge = OplEdge::Cut;
self.opl.instr = None;
self.opl.pitch = None;
}
}
pub(crate) fn take_opl_update(&mut self) -> Option<OplChannelUpdate> {
let edge = core::mem::take(&mut self.opl.edge);
let held = match (self.opl.instr, self.opl.pitch) {
(Some(i), Some(pitch)) => {
let module = self.module;
match module.instrument.get(i).map(|x| &x.instr_type) {
Some(InstrumentType::Opl(o)) => {
let (pan_l, pan_r) = self.opl_pan();
let _ = pitch; Some(OplHeld {
instr: i,
milli_hz: self.opl_current_milli_hz(),
vol63: self.opl_vol63(o),
pan_l,
pan_r,
})
}
_ => None,
}
}
_ => None,
};
if edge == OplEdge::None && held.is_none() {
None
} else {
Some(OplChannelUpdate { edge, held })
}
}
fn opl_finetune_delta(&self, instr_idx: usize) -> Option<PitchDelta> {
match self.module.instrument.get(instr_idx).map(|x| &x.instr_type) {
Some(InstrumentType::Opl(o)) => {
let rel_q88 = (o.relative_pitch as i16).saturating_mul(256);
let fine_q88 = o.finetune.into_q15().raw() >> 7;
Some(PitchDelta::from_q8_8_i16(rel_q88.saturating_add(fine_q88)))
}
_ => None,
}
}
fn opl_current_milli_hz(&self) -> u32 {
let mut period = self.period;
if matches!(
self.period_helper.freq_type,
FrequencyType::AmigaFrequencies
) {
period = period.saturating_add_signed(self.vibrato_value_raw());
}
let freq = self.period_helper.period_to_frequency(period);
let freq_hz_q8 = freq.raw_q24_8() as u64; ((freq_hz_q8 * 261_625) / (256 * 8363)) as u32
}
fn opl_vol63(&self, instr: &xmrs::tracker::instr_opl::InstrOpl) -> u8 {
let vol_q15 =
(self.volume.as_q15_i32() + self.tremolo_value_q15().raw() as i32).clamp(0, 32768);
let chan_q15 = (vol_q15 * self.channel_volume.as_q15_i32()) >> 15;
((instr.volume as i32 * chan_q15) >> 15).clamp(0, 63) as u8
}
fn opl_pan(&self) -> (bool, bool) {
let p = self.panning.as_q15_i32(); (p <= 21_760, p >= 10_880)
}
}