diff --git a/tracker/agb-tracker-interop/src/lib.rs b/tracker/agb-tracker-interop/src/lib.rs index 2abb6a9c..829276e5 100644 --- a/tracker/agb-tracker-interop/src/lib.rs +++ b/tracker/agb-tracker-interop/src/lib.rs @@ -126,7 +126,7 @@ impl quote::ToTokens for Envelope<'_> { let amount = amount.iter().map(|value| { let value = value.to_raw(); - quote! { agb_tracker::__private::agb_tracker_interop::Num::from_raw(#value) } + quote! { agb_tracker::__private::Num::from_raw(#value) } }); let sustain = match sustain { @@ -143,13 +143,15 @@ impl quote::ToTokens for Envelope<'_> { }; tokens.append_all(quote! { - const AMOUNTS: &[agb_tracker::__private::agb_tracker_interop::Num] = &[#(#amount),*]; + { + const AMOUNTS: &[agb_tracker::__private::Num] = &[#(#amount),*]; - agb_tracker::__private::agb_tracker_interop::Envelope { - amount: AMOUNTS, - sustain: #sustain, - loop_start: #loop_start, - loop_end: #loop_end, + agb_tracker::__private::agb_tracker_interop::Envelope { + amount: AMOUNTS, + sustain: #sustain, + loop_start: #loop_start, + loop_end: #loop_end, + } } }); } diff --git a/tracker/agb-tracker/src/lib.rs b/tracker/agb-tracker/src/lib.rs index 5c09eb51..8bf186f6 100644 --- a/tracker/agb-tracker/src/lib.rs +++ b/tracker/agb-tracker/src/lib.rs @@ -90,6 +90,7 @@ pub use agb_tracker_interop::Track; pub struct Tracker { track: &'static Track<'static>, channels: Vec, + envelopes: Vec>, frame: Num, tick: u32, @@ -101,12 +102,18 @@ pub struct Tracker { current_pattern: usize, } +#[derive(Default)] struct TrackerChannel { channel_id: Option, base_speed: Num, volume: Num, } +struct EnvelopeState { + frame: usize, + envelope_id: usize, +} + #[derive(Clone)] struct GlobalSettings { ticks_per_step: u32, @@ -118,11 +125,10 @@ impl Tracker { /// Create a new tracker playing a specified track. See the [example](crate#example) for how to use the tracker. pub fn new(track: &'static Track<'static>) -> Self { let mut channels = Vec::new(); - channels.resize_with(track.num_channels, || TrackerChannel { - channel_id: None, - base_speed: 0.into(), - volume: 0.into(), - }); + channels.resize_with(track.num_channels, Default::default); + + let mut envelopes = Vec::new(); + envelopes.resize_with(track.num_channels, || None); let global_settings = GlobalSettings { ticks_per_step: track.ticks_per_step, @@ -132,6 +138,7 @@ impl Tracker { Self { track, channels, + envelopes, frame: 0.into(), first: true, @@ -148,6 +155,7 @@ impl Tracker { /// See the [example](crate#example) for how to use the tracker. pub fn step(&mut self, mixer: &mut Mixer) { if !self.increment_frame() { + self.update_envelopes(mixer); return; } @@ -183,7 +191,37 @@ impl Tracker { ); } - self.increment_step(); + self.update_envelopes(mixer); + } + + fn update_envelopes(&mut self, mixer: &mut Mixer) { + for (channel, envelope_state_option) in self.channels.iter_mut().zip(&mut self.envelopes) { + if let Some(envelope_state) = envelope_state_option { + let envelope = &self.track.envelopes[envelope_state.envelope_id]; + + if !channel.update_volume_envelope(mixer, envelope_state.frame, envelope) { + envelope_state_option.take(); + } else { + envelope_state.frame += 1; + + if let Some(sustain) = envelope.sustain { + if envelope_state.frame >= sustain { + envelope_state.frame = sustain; + } + } + + if let Some(loop_end) = envelope.loop_end { + if envelope_state.frame >= loop_end { + envelope_state.frame = envelope.loop_start.unwrap_or(0); + } + } + + if envelope_state.frame >= envelope.amount.len() { + envelope_state_option.take(); + } + } + } + } } fn increment_frame(&mut self) -> bool { @@ -220,8 +258,6 @@ impl Tracker { false } } - - fn increment_step(&mut self) {} } impl TrackerChannel { @@ -350,6 +386,31 @@ impl TrackerChannel { _ => {} } } + + #[must_use] + fn update_volume_envelope( + &self, + mixer: &mut Mixer<'_>, + frame: usize, + envelope: &agb_tracker_interop::Envelope<'_>, + ) -> bool { + if let Some(channel) = self + .channel_id + .as_ref() + .and_then(|channel_id| mixer.channel(channel_id)) + { + let amount = envelope.amount[frame]; + + channel.volume( + (self.volume * amount.change_base()) + .try_change_base() + .unwrap(), + ); + true + } else { + false + } + } } #[cfg(test)] diff --git a/tracker/agb-xm-core/src/lib.rs b/tracker/agb-xm-core/src/lib.rs index 4d5a4e8d..7a935ca0 100644 --- a/tracker/agb-xm-core/src/lib.rs +++ b/tracker/agb-xm-core/src/lib.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, env, error::Error, fs, path::Path}; +use std::{collections::HashMap, error::Error, fs, path::Path}; use agb_tracker_interop::PatternEffect; use proc_macro2::TokenStream; @@ -9,7 +9,7 @@ use syn::LitStr; use agb_fixnum::Num; -use xmrs::{envelope, prelude::*, xm::xmmodule::XmModule}; +use xmrs::{prelude::*, xm::xmmodule::XmModule}; pub fn agb_xm_core(args: TokenStream) -> TokenStream { let input = match syn::parse::(args.into()) { @@ -489,7 +489,46 @@ struct EnvelopeData { } impl From<&xmrs::envelope::Envelope> for EnvelopeData { - fn from(value: &xmrs::envelope::Envelope) -> Self { - todo!() + fn from(e: &xmrs::envelope::Envelope) -> Self { + let mut amounts = vec![Num::new(e.point[0].value as i16) / 0xff]; + + // it should be sampled at 50fps, but we're sampling at 60fps, so need to do a bit of cheating here. + for frame in 1..(e.point.last().unwrap().frame * 60 / 50) { + let xm_frame = (frame * 50 / 60).max(1); + let index = e + .point + .iter() + .rposition(|point| point.frame < xm_frame) + .unwrap(); + + let first_point = &e.point[index]; + let second_point = &e.point[index + 1]; + + let amount = EnvelopePoint::lerp(first_point, second_point, xm_frame) / 255.0; + let amount = Num::from_raw((amount * (1 << 8) as f32) as i16); + + amounts.push(amount); + } + + let sustain = if e.sustain_enabled { + Some(e.point[e.sustain_point as usize].frame as usize * 60 / 50) + } else { + None + }; + let (loop_start, loop_end) = if e.loop_enabled { + ( + Some(e.point[e.loop_start_point as usize].frame as usize * 60 / 50), + Some(e.point[e.loop_end_point as usize].frame as usize * 60 / 50), + ) + } else { + (None, None) + }; + + EnvelopeData { + amounts, + sustain, + loop_start, + loop_end, + } } }