diff --git a/tracker/agb-tracker-interop/src/lib.rs b/tracker/agb-tracker-interop/src/lib.rs index f96086ae..cf655e05 100644 --- a/tracker/agb-tracker-interop/src/lib.rs +++ b/tracker/agb-tracker-interop/src/lib.rs @@ -7,16 +7,21 @@ pub struct Track<'a> { pub samples: &'a [Sample<'a>], pub pattern_data: &'a [PatternSlot], pub patterns: &'a [Pattern], + + pub frames_per_step: u16, } #[derive(Debug)] pub struct Sample<'a> { pub data: &'a [u8], + pub should_loop: bool, } #[derive(Debug)] pub struct Pattern { pub num_channels: usize, + pub length: usize, + pub start_position: usize, } #[derive(Debug)] @@ -32,9 +37,12 @@ impl<'a> quote::ToTokens for Track<'a> { fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { use quote::{quote, TokenStreamExt}; - let samples = self.samples; - let pattern_data = self.pattern_data; - let patterns = self.patterns; + let Track { + samples, + pattern_data, + patterns, + frames_per_step, + } = self; tokens.append_all(quote! { { @@ -48,25 +56,43 @@ impl<'a> quote::ToTokens for Track<'a> { samples: SAMPLES, pattern_data: PATTERN_DATA, patterns: PATTERNS, + + frames_per_step: #frames_per_step, } } }) } } +#[cfg(feature = "quote")] +struct ByteString<'a>(&'a [u8]); +#[cfg(feature = "quote")] +impl quote::ToTokens for ByteString<'_> { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + use quote::TokenStreamExt; + + tokens.append(proc_macro2::Literal::byte_string(self.0)); + } +} + #[cfg(feature = "quote")] impl<'a> quote::ToTokens for Sample<'a> { fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { use quote::{quote, TokenStreamExt}; - let self_as_u8s = self.data.iter().map(|i| *i as u8); + let self_as_u8s: Vec<_> = self.data.iter().map(|i| *i as u8).collect(); + let samples = ByteString(&self_as_u8s); + let should_loop = self.should_loop; tokens.append_all(quote! { { use agb_tracker_interop::*; - const SAMPLE_DATA: &[u8] = &[#(#self_as_u8s),*]; - agb_tracker_interop::Sample { data: SAMPLE_DATA } + #[repr(align(4))] + struct AlignmentWrapper([u8; N]); + + const SAMPLE_DATA: &[u8] = &AlignmentWrapper(*#samples).0; + agb_tracker_interop::Sample { data: SAMPLE_DATA, should_loop: #should_loop } } }); } @@ -109,7 +135,11 @@ impl quote::ToTokens for Pattern { fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { use quote::{quote, TokenStreamExt}; - let num_channels = self.num_channels; + let Pattern { + num_channels, + length, + start_position, + } = self; tokens.append_all(quote! { { @@ -117,6 +147,8 @@ impl quote::ToTokens for Pattern { Pattern { num_channels: #num_channels, + length: #length, + start_position: #start_position, } } }) diff --git a/tracker/agb-tracker/examples/basic.rs b/tracker/agb-tracker/examples/basic.rs index 2df214db..2581f1e0 100644 --- a/tracker/agb-tracker/examples/basic.rs +++ b/tracker/agb-tracker/examples/basic.rs @@ -3,7 +3,7 @@ use agb::sound::mixer::Frequency; use agb::Gba; -use agb_tracker::{import_xm, Track}; +use agb_tracker::{import_xm, Track, Tracker}; const AJOJ: Track = import_xm!("examples/ajoj.xm"); @@ -14,7 +14,10 @@ fn main(mut gba: Gba) -> ! { let mut mixer = gba.mixer.mixer(Frequency::Hz18157); mixer.enable(); + let mut tracker = Tracker::new(&AJOJ); + loop { + tracker.step(&mut mixer); mixer.frame(); vblank_provider.wait_for_vblank(); } diff --git a/tracker/agb-tracker/src/lib.rs b/tracker/agb-tracker/src/lib.rs index 07723f2d..f7bbc700 100644 --- a/tracker/agb-tracker/src/lib.rs +++ b/tracker/agb-tracker/src/lib.rs @@ -5,6 +5,12 @@ #![cfg_attr(test, reexport_test_harness_main = "test_main")] #![cfg_attr(test, test_runner(agb::test_runner::test_runner))] +extern crate alloc; + +use alloc::{vec, vec::Vec}; + +use agb::sound::mixer::{ChannelId, Mixer, SoundChannel}; + #[cfg(feature = "xm")] pub use agb_xm::import_xm; @@ -12,11 +18,86 @@ pub use agb_tracker_interop as __private; pub use __private::Track; -#[cfg(test)] -mod tests { - #[test_case] - fn it_works(_gba: &mut agb::Gba) { - assert_eq!(1, 1); +pub struct Tracker { + track: &'static Track<'static>, + channels: Vec>, + + step: u16, + current_row: usize, + current_pattern: usize, +} + +impl Tracker { + pub fn new(track: &'static Track<'static>) -> Self { + agb::println!("{}", track.frames_per_step); + + Self { + track, + channels: vec![], + + step: 0, + current_row: 0, + current_pattern: 0, + } + } + + pub fn step(&mut self, mixer: &mut Mixer) { + if self.step != 0 { + self.increment_step(); + return; // TODO: volume / pitch slides + } + + let current_pattern = &self.track.patterns[self.current_pattern]; + + let channels_to_play = current_pattern.num_channels; + self.channels.resize_with(channels_to_play, || None); + + let pattern_data_pos = current_pattern.start_position + self.current_row * channels_to_play; + let pattern_slots = + &self.track.pattern_data[pattern_data_pos..pattern_data_pos + channels_to_play]; + + for (channel_id, pattern_slot) in self.channels.iter_mut().zip(pattern_slots) { + if pattern_slot.sample == 0 { + // do nothing + } else { + if let Some(channel) = channel_id + .take() + .and_then(|channel_id| mixer.channel(&channel_id)) + { + channel.stop(); + } + + let sample = &self.track.samples[pattern_slot.sample - 1]; + let mut new_channel = SoundChannel::new(sample.data); + new_channel + .panning(pattern_slot.panning) + .volume(pattern_slot.volume) + .playback(pattern_slot.speed); + + if sample.should_loop { + new_channel.should_loop(); + } + + *channel_id = mixer.play_sound(new_channel); + } + } + + self.increment_step(); + } + + fn increment_step(&mut self) { + self.step += 1; + + if self.step == self.track.frames_per_step * 2 { + self.current_row += 1; + + if self.current_row > self.track.patterns[self.current_pattern].length { + self.current_pattern += 1; + self.current_row = 0; + } + + self.step = 0; + } } } diff --git a/tracker/agb-xm-core/src/lib.rs b/tracker/agb-xm-core/src/lib.rs index c7887ff5..ca643010 100644 --- a/tracker/agb-xm-core/src/lib.rs +++ b/tracker/agb-xm-core/src/lib.rs @@ -48,26 +48,50 @@ pub fn parse_module(module: &Module) -> TokenStream { let instruments = &module.instrument; let mut instruments_map = HashMap::new(); + struct SampleData { + data: Vec, + should_loop: bool, + fine_tune: f64, + relative_note: i8, + } + let mut samples = vec![]; for (instrument_index, instrument) in instruments.iter().enumerate() { let InstrumentType::Default(ref instrument) = instrument.instr_type else { continue; }; for (sample_index, sample) in instrument.sample.iter().enumerate() { - let sample = match &sample.data { - SampleDataType::Depth8(depth8) => depth8 - .iter() - .map(|value| *value as u8) - .collect::>() - .clone(), + let should_loop = !matches!(sample.flags, LoopType::No); + let fine_tune = sample.finetune as f64; + let relative_note = sample.relative_note; + + let mut sample = match &sample.data { + SampleDataType::Depth8(depth8) => { + depth8.iter().map(|value| *value as u8).collect::>() + } SampleDataType::Depth16(depth16) => depth16 .iter() .map(|sample| (sample >> 8) as i8 as u8) .collect::>(), }; + if should_loop { + sample.append(&mut sample.clone()); + sample.append(&mut sample.clone()); + sample.append(&mut sample.clone()); + sample.append(&mut sample.clone()); + sample.append(&mut sample.clone()); + sample.append(&mut sample.clone()); + sample.append(&mut sample.clone()); + } + instruments_map.insert((instrument_index, sample_index), samples.len()); - samples.push(sample); + samples.push(SampleData { + data: sample, + should_loop, + fine_tune, + relative_note, + }); } } @@ -76,6 +100,7 @@ pub fn parse_module(module: &Module) -> TokenStream { for pattern in &module.pattern { let mut num_channels = 0; + let start_pos = pattern_data.len(); for row in pattern.iter() { for slot in row { @@ -90,7 +115,7 @@ pub fn parse_module(module: &Module) -> TokenStream { let sample_slot = instrument.sample_for_note[slot.note as usize] as usize; instruments_map .get(&(instrument_index, sample_slot)) - .cloned() + .map(|sample_idx| sample_idx + 1) .unwrap_or(0) } else { 0 @@ -104,33 +129,77 @@ pub fn parse_module(module: &Module) -> TokenStream { slot.volume as i16 } / 64, ); - let speed = Num::new(1); // TODO: Calculate speed for the correct note here - let panning = Num::new(0); - pattern_data.push(agb_tracker_interop::PatternSlot { - volume, - speed, - panning, - sample, - }); + if sample == 0 { + // TODO should take into account previous sample played on this channel + pattern_data.push(agb_tracker_interop::PatternSlot { + volume: Num::new(0), + speed: Num::new(0), + panning: Num::new(0), + sample: 0, + }) + } else { + let sample_played = &samples[sample - 1]; + + let speed = note_to_speed( + slot.note, + sample_played.fine_tune, + sample_played.relative_note, + ); + let panning = Num::new(0); + + pattern_data.push(agb_tracker_interop::PatternSlot { + volume, + speed, + panning, + sample, + }); + } } num_channels = row.len(); } - patterns.push(agb_tracker_interop::Pattern { num_channels }); + patterns.push(agb_tracker_interop::Pattern { + num_channels, + length: pattern.len(), + start_position: start_pos, + }); } let samples: Vec<_> = samples .iter() - .map(|sample| agb_tracker_interop::Sample { data: &sample }) + .map(|sample| agb_tracker_interop::Sample { + data: &sample.data, + should_loop: sample.should_loop, + }) .collect(); + let frames_per_step = + ((60.0 * 60.0) / module.default_bpm as f64 / module.default_tempo as f64) as u16; + let interop = agb_tracker_interop::Track { samples: &samples, pattern_data: &pattern_data, patterns: &patterns, + + frames_per_step, }; quote!(#interop) } + +fn note_to_frequency(note: Note, fine_tune: f64, relative_note: i8) -> f64 { + let real_note = (note as usize as f64) + (relative_note as f64); + let period = 10.0 * 12.0 * 16.0 * 4.0 - (real_note as f64) * 16.0 * 4.0 - fine_tune / 2.0; + 8363.0 * 2.0f64.powf((6.0 * 12.0 * 16.0 * 4.0 - period) / (12.0 * 16.0 * 4.0)) +} + +fn note_to_speed(note: Note, fine_tune: f64, relative_note: i8) -> Num { + let frequency = note_to_frequency(note, fine_tune, relative_note); + + let gba_audio_frequency = 18157f64; + + let speed: f64 = frequency / gba_audio_frequency; + Num::from_raw((speed * (1 << 8) as f64) as u32) +} diff --git a/tracker/agb-xm-core/src/main.rs b/tracker/agb-xm-core/src/main.rs new file mode 100644 index 00000000..2e2ba88f --- /dev/null +++ b/tracker/agb-xm-core/src/main.rs @@ -0,0 +1,8 @@ +fn main() -> Result<(), Box> { + let module = agb_xm_core::load_module_from_file(&std::path::Path::new( + "../agb-tracker/examples/ajoj.xm", + ))?; + let output = agb_xm_core::parse_module(&module); + + Ok(()) +}