use std::iter::zip;
use crate::{
Key, MooInstructions, NATIVE_SAMPLE_RATE, SampleRate, SampleT, Timing,
event::{DEFAULT_BASICKEY, DEFAULT_KEY, DEFAULT_TUNING, DEFAULT_VELOCITY, DEFAULT_VOLUME},
pulse_frequency::PULSE_FREQ,
util::ArrayLenExt as _,
voice::{Voice, VoiceFlags, VoiceTone},
};
#[repr(transparent)]
#[derive(Clone, Copy, PartialEq, Eq)]
pub struct UnitIdx(pub u8);
impl UnitIdx {
#[must_use]
pub fn usize(self) -> usize {
usize::from(self.0)
}
}
pub const MAX_CHANNEL: u8 = 2;
pub const MAX_CH_LEN: usize = MAX_CHANNEL as usize;
pub type PanTimeBuf = [i32; 64];
#[derive(Clone)]
#[doc = include_str!("../doc/svg/units-to-final.svg")]
#[doc = include_str!("../doc/svg/pantime-render.svg")]
pub struct Unit {
pub name: String,
pub key_now: Key,
pub key_start: Key,
pub key_margin: Key,
pub porta_pos: SampleT,
pub porta_destination: SampleT,
pub pan_vols: [i16; MAX_CH_LEN],
pub pan_time_offs: [PanTimeOff; MAX_CH_LEN],
pub pan_time_bufs: [PanTimeBuf; MAX_CH_LEN],
pub volume: i16,
pub velocity: i16,
pub group: GroupIdx,
pub tuning: f32,
pub voice_idx: usize,
pub tones: [VoiceTone; MAX_CH_LEN],
pub mute: bool,
}
pub type PanTimeOff = u8;
impl Default for Unit {
fn default() -> Self {
Self {
name: String::default(),
key_now: Default::default(),
key_start: Default::default(),
key_margin: Default::default(),
porta_pos: Default::default(),
porta_destination: Default::default(),
pan_vols: Default::default(),
pan_time_offs: Default::default(),
pan_time_bufs: [[0; _]; _],
volume: Default::default(),
velocity: Default::default(),
group: GroupIdx::default(),
tuning: Default::default(),
tones: [VoiceTone::default(), VoiceTone::default()],
voice_idx: 0,
mute: false,
}
}
}
#[derive(Clone, Copy, Debug, Default)]
#[repr(transparent)]
pub struct GroupIdx(pub u8);
impl GroupIdx {
#[expect(clippy::cast_possible_truncation)]
pub const MAX: Self = Self((GroupSamples::LEN - 1) as u8);
#[must_use]
pub const fn usize(self) -> usize {
self.0 as usize
}
}
pub type GroupSamples = [i32; 7];
impl Unit {
pub(crate) fn tone_init(&mut self) {
self.group = GroupIdx(0);
self.velocity = DEFAULT_VELOCITY.cast_signed();
self.volume = DEFAULT_VOLUME.cast_signed();
self.tuning = DEFAULT_TUNING;
self.porta_destination = 0;
self.porta_pos = 0;
for i in 0..MAX_CHANNEL {
self.pan_vols[i as usize] = 64;
self.pan_time_offs[i as usize] = 0;
}
}
#[expect(
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
clippy::cast_sign_loss
)]
pub(crate) fn tone_envelope(&mut self, voices: &[Voice]) {
let Some(voice) = voices.get(self.voice_idx) else {
eprintln!("Invalid voice idx");
return;
};
for (voice_inst, voice_tone) in zip(&voice.insts, &mut self.tones) {
if voice_tone.life_count > 0 && !voice_inst.env.is_empty() {
if voice_tone.on_count > 0 {
if voice_tone.env_pos < voice_inst.env.len() {
voice_tone.env_volume = voice_inst.env[voice_tone.env_pos];
voice_tone.env_pos += 1;
}
} else {
voice_tone.env_volume = (i32::from(voice_tone.env_start)
+ (0 - i32::from(voice_tone.env_start)) * voice_tone.env_pos as i32
/ i32::try_from(voice_inst.env_release).unwrap())
as u8;
voice_tone.env_pos += 1;
}
}
}
}
pub(crate) const fn tone_key_on(&mut self) {
self.key_now = self.key_start + self.key_margin;
self.key_start = self.key_now;
self.key_margin = 0;
}
pub(crate) fn tone_zero_lives(&mut self) {
for ch in 0..MAX_CHANNEL as usize {
self.tones[ch].life_count = 0;
}
}
pub(crate) const fn tone_key(&mut self, key: Key) {
self.key_start = self.key_now;
self.key_margin = key - self.key_start;
self.porta_pos = 0;
}
pub(crate) fn tone_pan_volume(&mut self, vol: u8) {
self.pan_vols[0] = 64;
self.pan_vols[1] = 64;
if vol >= 64 {
self.pan_vols[0] = 128 - i16::from(vol);
} else {
self.pan_vols[1] = i16::from(vol);
}
}
pub(crate) fn tone_pan_time(&mut self, offset: u8, sps: SampleRate) {
if offset >= 64 {
self.pan_time_offs[0] = calc_pan_time(offset - 64, sps);
self.pan_time_offs[1] = 0;
} else {
self.pan_time_offs[0] = 0;
self.pan_time_offs[1] = calc_pan_time(64 - offset, sps);
}
}
pub(crate) const fn tone_supple(
&self,
group_smps: &mut GroupSamples,
ch: u8,
time_pan_index: usize,
) {
let idx = (time_pan_index.wrapping_sub(self.pan_time_offs[ch as usize] as usize))
& (PanTimeBuf::LEN - 1);
group_smps[self.group.usize()] += self.pan_time_bufs[ch as usize][idx];
}
#[expect(clippy::cast_possible_truncation)]
pub(crate) fn tone_increment_key(&mut self) -> i32 {
if self.porta_destination != 0 && self.key_margin != 0 {
if self.porta_pos < self.porta_destination {
self.porta_pos += 1;
self.key_now = (f64::from(self.key_start)
+ f64::from(self.key_margin) * f64::from(self.porta_pos)
/ f64::from(self.porta_destination)) as i32;
} else {
self.key_now = self.key_start + self.key_margin;
self.key_start = self.key_now;
self.key_margin = 0;
}
} else {
self.key_now = self.key_start + self.key_margin;
}
self.key_now
}
pub(crate) fn tone_increment_sample(&mut self, freq: f32, voices: &[Voice]) {
let voice = &voices[self.voice_idx];
for ((voice_inst, voice_tone), voice_unit) in
zip(&voice.insts, &mut self.tones).zip(&voice.units)
{
if voice_tone.life_count > 0 {
voice_tone.life_count -= 1;
}
if voice_tone.life_count > 0 {
voice_tone.on_count -= 1;
voice_tone.smp_pos += f64::from(voice_tone.offset_freq * self.tuning * freq);
if voice_tone.smp_pos >= f64::from(voice_inst.num_samples) {
if voice_unit.flags.contains(VoiceFlags::WAVE_LOOP) {
if voice_tone.smp_pos >= f64::from(voice_inst.num_samples) {
voice_tone.smp_pos -= f64::from(voice_inst.num_samples);
}
if voice_tone.smp_pos >= f64::from(voice_inst.num_samples) {
voice_tone.smp_pos = 0.;
}
} else {
voice_tone.life_count = 0;
}
}
if voice_tone.on_count == 0 && !voice_inst.env.is_empty() {
voice_tone.env_start = voice_tone.env_volume;
voice_tone.env_pos = 0;
}
}
}
}
pub(crate) const fn set_voice(&mut self, idx: usize) {
self.voice_idx = idx;
self.key_now = DEFAULT_KEY;
self.key_margin = 0;
self.key_start = DEFAULT_KEY;
}
pub(crate) fn new() -> Self {
Self {
name: "<no name>".into(),
..Default::default()
}
}
#[expect(
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
clippy::cast_sign_loss
)]
pub(crate) fn reset_voice(
&mut self,
ins: &MooInstructions,
mut voice_idx: usize,
timing: Timing,
) {
if voice_idx >= ins.voices.len() {
eprintln!("Error: Voice index out of bounds. Setting to 0.");
voice_idx = 0;
}
self.set_voice(voice_idx);
let Some(voice) = &ins.voices.get(voice_idx) else {
eprintln!("Error: Song doesn't have any voices");
return;
};
for ((vu, inst), tone) in zip(&voice.units, &voice.insts).zip(&mut self.tones) {
tone.life_count = 0;
tone.on_count = 0;
tone.smp_pos = 0.0;
tone.env_release_clock = (inst.env_release as f32 / ins.samples_per_tick) as u32;
tone.offset_freq = if vu.flags.contains(VoiceFlags::BEAT_FIT) {
(inst.num_samples as f32 * timing.bpm)
/ (f32::from(NATIVE_SAMPLE_RATE) * 60. * vu.tuning)
} else {
#[expect(clippy::cast_sign_loss)]
(PULSE_FREQ.get((DEFAULT_BASICKEY as usize).wrapping_sub(vu.basic_key as usize))
* vu.tuning)
};
}
}
#[expect(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
pub(crate) fn tone_sample(
&mut self,
time_pan_index: usize,
smooth_smp: SampleRate,
voices: &[Voice],
) {
let voice = &voices[self.voice_idx];
for ch in 0..i32::from(MAX_CHANNEL) {
let mut time_pan_buf: i32 = 0;
for ((voice_tone, voice_inst), vu) in zip(&self.tones, &voice.insts).zip(&voice.units) {
if voice_inst.sample_buf.is_empty() {
continue;
}
let smp_w: &[i16] = bytemuck::cast_slice(&voice_inst.sample_buf);
let mut work: i32 = 0;
if voice_tone.life_count > 0 {
let pos: i32 = (voice_tone.smp_pos as i32) * 4 + ch * 2;
if let Some(w_sample) = smp_w.get(pos as usize / 2) {
work += i32::from(*w_sample);
}
work = (work * i32::from(self.velocity)) / 128;
work = (work * i32::from(self.volume)) / 128;
work = work * i32::from(self.pan_vols[ch as usize]) / 64;
if !voice_inst.env.is_empty() {
work = work * i32::from(voice_tone.env_volume) / 128;
}
if vu.flags.contains(VoiceFlags::SMOOTH)
&& voice_tone.life_count < i32::from(smooth_smp)
{
work = work * voice_tone.life_count / i32::from(smooth_smp);
}
}
time_pan_buf += work;
}
self.pan_time_bufs[ch as usize][time_pan_index] = time_pan_buf;
}
}
}
fn calc_pan_time(mut offset: u8, out_sps: SampleRate) -> u8 {
if offset > 63 {
offset = 63;
}
((u32::from(offset) * u32::from(NATIVE_SAMPLE_RATE)) / u32::from(out_sps))
.try_into()
.unwrap_or(0)
}