#![no_std] #![no_main] // This is required to allow writing tests #![cfg_attr(test, feature(custom_test_frameworks))] #![cfg_attr(test, reexport_test_harness_main = "test_main")] #![cfg_attr(test, test_runner(agb::test_runner::test_runner))] #![deny(missing_docs)] //! # agb_tracker //! `agb_tracker` is a library for playing tracker music on the Game Boy Advance (GBA) //! using the [`agb`](https://github.com/agbrs/agb) library. //! //! The default mechanism for playing background music using `agb` is to include a //! the entire music as a raw sound file. However, this can get very large (>8MB) for //! only a few minutes of music, taking up most of your limited ROM space. //! //! Using a tracker, you can store many minutes of music in only a few kB of ROM which makes //! the format much more space efficient at the cost of some CPU. //! //! This library uses about 20-30% of the GBA's CPU time per frame, for 4 channels but most of that is //! `agb`'s mixing. The main [`step`](Tracker::step()) function uses around 2000 cycles (<1%). //! //! # Example //! //! ```rust,no_run //! #![no_std] //! #![no_main] //! //! use agb::{Gba, sound::mixer::Frequency}; //! use agb_tracker::{include_xm, Track, Tracker}; //! //! static DB_TOFFE: Track = include_xm!("examples/db_toffe.xm"); //! //! #[agb::entry] //! fn main(mut gba: Gba) -> ! { //! let vblank_provider = agb::interrupt::VBlank::get(); //! //! let mut mixer = gba.mixer.mixer(Frequency::Hz32768); //! mixer.enable(); //! //! let mut tracker = Tracker::new(&DB_TOFFE); //! //! loop { //! tracker.step(&mut mixer); //! mixer.frame(); //! //! vblank_provider.wait_for_vblank(); //! } //! } //! ``` //! //! Note that currently you have to select 32768Hz as the frequency for the mixer. //! This restriction will be lifted in a future version. //! //! # Concepts //! //! The main concept of the `agb_tracker` crate is to move as much of the work to build //! time as possible to make the actual playing as fast as we can. The passed tracker file //! gets parsed and converted into a simplified format which is then played while the game //! is running. //! //! In theory, the format the tracker file gets converted into is agnostic to the base format. //! Currently, only XM is implemented, however, more formats could be added in future depending //! on demand. extern crate alloc; mod lookups; mod mixer; use agb_tracker_interop::{Jump, PatternEffect, Sample, Waveform}; use alloc::vec::Vec; pub use mixer::{Mixer, SoundChannel}; use agb_fixnum::Num; /// Import an XM file. Only available if you have the `xm` feature enabled (enabled by default). #[cfg(feature = "xm")] pub use agb_xm::include_xm; /// Import an S3M file. Only available if you have the `xm` feature enabled (enabled by default). #[cfg(feature = "xm")] pub use agb_xm::include_s3m; /// Import a MOD file. Only available if you have the `xm` feature enabled (enabled by default). #[cfg(feature = "xm")] pub use agb_xm::include_mod; /// Import a midi file. Only available if you have the `midi` feature enabled (enabled by default). /// This is currently experimental, and many types of MIDI file or MIDI features are not supported. /// /// Takes 2 arguments, an SF2 file and a midi file. #[cfg(feature = "midi")] pub use agb_midi::include_midi; #[doc(hidden)] pub mod __private { pub use agb_fixnum::Num; pub use agb_tracker_interop; } /// A reference to a track. You should create this using one of the include macros. pub use agb_tracker_interop::Track; /// Stores the required state in order to play tracker music. pub struct TrackerInner<'track, TChannelId> { track: &'track Track, channels: Vec, envelopes: Vec>, mixer_channels: Vec>, frame: Num, tick: u32, first: bool, global_settings: GlobalSettings, current_row: usize, current_pattern: usize, current_jump: Option, } #[derive(Default)] struct TrackerChannel { original_speed: Num, base_speed: Num, volume: Num, vibrato: Waves, current_volume: Num, current_speed: Num, current_panning: Num, is_playing: bool, // if some, should set the current position to this current_pos: Option, } #[derive(Default)] struct Waves { waveform: Waveform, frame: usize, speed: usize, amount: Num, enable: bool, } impl Waves { fn value(&self) -> Num { assert!(self.amount.abs() <= 1.into()); calculate_wave(self.waveform, self.amount, self.frame) } } fn calculate_wave(waveform: Waveform, amount: Num, frame: usize) -> Num { let lookup = match waveform { Waveform::Sine => lookups::SINE_LOOKUP, Waveform::Saw => lookups::SAW_LOOKUP, Waveform::Square => lookups::SQUARE_LOOKUP, }; (amount * lookup[frame] + 1).try_change_base().unwrap() } struct EnvelopeState { frame: usize, envelope_id: usize, finished: bool, fadeout: Num, vibrato_pos: usize, } #[derive(Clone)] struct GlobalSettings { ticks_per_step: u32, frames_per_tick: Num, volume: Num, } impl<'track, TChannelId> TrackerInner<'track, TChannelId> { /// Create a new tracker playing a specified track. See the [example](crate#example) for how to use the tracker. pub fn new(track: &'track Track) -> Self { let mut channels = Vec::new(); channels.resize_with(track.num_channels, Default::default); let mut envelopes = Vec::new(); envelopes.resize_with(track.num_channels, || None); let mut mixer_channels = Vec::new(); mixer_channels.resize_with(track.num_channels, || None); let global_settings = GlobalSettings { ticks_per_step: track.ticks_per_step, frames_per_tick: track.frames_per_tick, volume: 1.into(), }; Self { track, mixer_channels, channels, envelopes, frame: 0.into(), first: true, tick: 0, global_settings, current_pattern: 0, current_row: 0, current_jump: None, } } /// Call this once per frame before calling [`mixer.frame`](agb::sound::mixer::Mixer::frame()). /// See the [example](crate#example) for how to use the tracker. pub fn step>(&mut self, mixer: &mut M) { if !self.increment_frame() { self.update_envelopes(); self.realise(mixer); return; } let pattern_to_play = self.track.patterns_to_play[self.current_pattern]; let current_pattern = &self.track.patterns[pattern_to_play]; let pattern_data_pos = current_pattern.start_position + self.current_row * self.track.num_channels; let pattern_slots = &self.track.pattern_data[pattern_data_pos..pattern_data_pos + self.track.num_channels]; for (i, (channel, pattern_slot)) in self.channels.iter_mut().zip(pattern_slots).enumerate() { if pattern_slot.sample != 0 && self.tick == 0 { let sample = &self.track.samples[pattern_slot.sample as usize - 1]; if let Some(channel) = self.mixer_channels[i] .take() .and_then(|channel_id| mixer.channel(&channel_id)) { channel.stop(); } let mut new_channel = M::SoundChannel::new(&sample.data); if sample.should_loop { new_channel .should_loop() .restart_point(sample.restart_point); } self.mixer_channels[i] = mixer.play_sound(new_channel); channel.reset(sample); self.envelopes[i] = sample.volume_envelope.map(|envelope_id| EnvelopeState { frame: 0, envelope_id, finished: false, fadeout: sample.fadeout, vibrato_pos: 0, }); } if self.tick == 0 { channel.set_speed(pattern_slot.speed.change_base()); } channel.vibrato.enable = false; channel.apply_effect( &pattern_slot.effect1, self.tick, &mut self.global_settings, &mut self.envelopes[i], &mut self.current_jump, ); channel.apply_effect( &pattern_slot.effect2, self.tick, &mut self.global_settings, &mut self.envelopes[i], &mut self.current_jump, ); } self.update_envelopes(); self.realise(mixer); } /// Stops all channels. /// /// It is expected that you don't call step after this. But doing so will continue from /// where you left off. However, notes which were playing won't resume. pub fn stop>(&mut self, mixer: &mut M) { for channel_id in &mut self.mixer_channels { if let Some(channel) = channel_id .take() .and_then(|channel_id| mixer.channel(&channel_id)) { channel.stop(); } } } fn realise>(&mut self, mixer: &mut M) { for (i, (mixer_channel, tracker_channel)) in self .mixer_channels .iter() .zip(&mut self.channels) .enumerate() { tracker_channel.tick(); if let Some(channel) = mixer_channel .as_ref() .and_then(|channel_id| mixer.channel(channel_id)) { let mut current_speed = tracker_channel.current_speed; if tracker_channel.vibrato.speed != 0 && tracker_channel.vibrato.enable { current_speed *= tracker_channel.vibrato.value().change_base(); } else if let Some(envelope) = &mut self.envelopes[i] { let track_envelope = &self.track.envelopes[envelope.envelope_id]; if track_envelope.vib_speed != 0 { current_speed *= calculate_wave( track_envelope.vib_waveform, track_envelope.vib_amount.change_base(), envelope.vibrato_pos, ) .change_base(); envelope.vibrato_pos = (envelope.vibrato_pos + track_envelope.vib_speed as usize) % 64; } } channel.playback(current_speed.change_base()); channel.volume(tracker_channel.current_volume.try_change_base().unwrap()); channel.panning(tracker_channel.current_panning.try_change_base().unwrap()); if let Some(offset) = tracker_channel.current_pos.take() { channel.set_pos(offset as u32); } if tracker_channel.is_playing { channel.resume(); } else { channel.pause(); } } } } fn update_envelopes(&mut self) { 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(envelope_state, envelope, &self.global_settings) { envelope_state_option.take(); } else { envelope_state.frame += 1; if !envelope_state.finished { 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.frame = envelope.amount.len() - 1; } } } } } fn increment_frame(&mut self) -> bool { if self.first { self.first = false; return true; } self.frame += 1; if self.frame >= self.global_settings.frames_per_tick { self.tick += 1; self.frame -= self.global_settings.frames_per_tick; if self.tick >= self.global_settings.ticks_per_step { if let Some(jump) = self.current_jump.take() { self.handle_jump(jump); } else { self.current_row += 1; if self.current_row >= self.track.patterns[self.track.patterns_to_play[self.current_pattern]] .length { self.current_pattern += 1; self.current_row = 0; if self.current_pattern >= self.track.patterns_to_play.len() { self.current_pattern = self.track.repeat; } } } self.tick = 0; } true } else { false } } fn handle_jump(&mut self, jump: Jump) { match jump { Jump::Position { pattern } => { self.current_pattern = pattern as usize; self.current_row = 0; } Jump::PatternBreak { row } => { self.current_pattern += 1; self.current_row = row as usize; } Jump::Combined { pattern, row } => { self.current_pattern = pattern as usize; self.current_row = row as usize; } }; if self.current_pattern >= self.track.patterns_to_play.len() { self.current_pattern = self.track.repeat; } if self.current_row >= self.track.patterns[self.track.patterns_to_play[self.current_pattern]].length { // TODO: reconsider this default self.current_row = 0; } } } impl TrackerChannel { fn reset(&mut self, sample: &Sample) { self.volume = sample.volume.change_base(); self.current_volume = self.volume; self.current_panning = 0.into(); self.is_playing = true; } fn set_speed(&mut self, speed: Num) { if speed != 0.into() { self.base_speed = speed.change_base(); self.original_speed = self.base_speed; } self.current_speed = self.base_speed; } fn apply_effect( &mut self, effect: &PatternEffect, tick: u32, global_settings: &mut GlobalSettings, envelope_state: &mut Option, current_jump: &mut Option, ) { match effect { PatternEffect::None => {} PatternEffect::Stop => { self.current_volume = 0.into(); if let Some(envelope_state) = envelope_state { envelope_state.finished = true; } } PatternEffect::Arpeggio(first, second) => { match tick % 3 { 0 => self.current_speed = self.base_speed.change_base(), 1 => self.current_speed = first.change_base(), 2 => self.current_speed = second.change_base(), _ => unreachable!(), }; } PatternEffect::Panning(panning) => { self.current_panning = panning.change_base(); } PatternEffect::Volume(volume) => { self.current_volume = (volume.change_base() * global_settings.volume) .try_change_base() .unwrap(); self.volume = volume.change_base(); } PatternEffect::VolumeSlide(amount, keep_vibrato) => { if tick != 0 { self.volume = (self.volume + amount.change_base()).max(0.into()); self.current_volume = (self.volume * global_settings.volume) .try_change_base() .unwrap(); } self.vibrato.enable = *keep_vibrato; } PatternEffect::FineVolumeSlide(amount) => { if tick == 0 { self.volume = (self.volume + amount.change_base()).max(0.into()); self.current_volume = (self.volume * global_settings.volume) .try_change_base() .unwrap(); } } PatternEffect::NoteCut(wait) => { if tick == *wait { self.current_volume = 0.into(); if let Some(envelope_state) = envelope_state { envelope_state.finished = true; } } } PatternEffect::NoteDelay(wait) => { if tick < *wait { self.is_playing = false; } if tick == *wait { self.is_playing = true; self.current_volume = (self.volume * global_settings.volume) .try_change_base() .unwrap(); } } PatternEffect::Portamento(amount) => { if tick != 0 { self.base_speed *= amount.change_base(); self.current_speed = self.base_speed.change_base(); } } PatternEffect::FinePortamento(amount) => { if tick == 1 { self.base_speed *= amount.change_base(); self.current_speed = self.base_speed.change_base(); } } PatternEffect::TonePortamento(amount, target) => { self.current_volume = (self.volume * global_settings.volume) .try_change_base() .unwrap(); if tick != 0 { if *amount < 1.into() { self.base_speed = (self.base_speed * amount.change_base()).max(target.change_base()); } else { self.base_speed = (self.base_speed * amount.change_base()).min(target.change_base()); } } self.current_speed = self.base_speed.change_base(); } PatternEffect::PitchBend(amount) => { if tick == 0 { self.base_speed = self.original_speed * amount.change_base(); self.current_speed = self.base_speed.change_base(); } } PatternEffect::SetTicksPerStep(amount) => { global_settings.ticks_per_step = *amount; } PatternEffect::SetFramesPerTick(new_frames_per_tick) => { global_settings.frames_per_tick = *new_frames_per_tick; } PatternEffect::SetGlobalVolume(volume) => { global_settings.volume = *volume; } PatternEffect::GlobalVolumeSlide(volume_delta) => { global_settings.volume = (global_settings.volume + *volume_delta).clamp(0.into(), 1.into()); } PatternEffect::Vibrato(waveform, amount, speed) => { if *amount != 0.into() { self.vibrato.amount = amount.change_base(); } if *speed != 0 { self.vibrato.speed = *speed as usize; } self.vibrato.waveform = *waveform; self.vibrato.enable = true; } PatternEffect::Jump(jump) => { *current_jump = Some(jump.clone()); } PatternEffect::SampleOffset(offset) => { if tick == 0 { self.current_pos = Some(*offset); } } PatternEffect::Retrigger(volume_change, ticks) => { if tick % *ticks as u32 == 0 { match volume_change { agb_tracker_interop::RetriggerVolumeChange::DecreaseByOne => { self.volume = (self.volume - Num::new(1) / 64).max(0.into()); self.current_volume = (self.volume * global_settings.volume) .try_change_base() .unwrap(); } agb_tracker_interop::RetriggerVolumeChange::NoChange => {} } self.current_pos = Some(0); } } } } #[must_use] fn update_volume_envelope( &mut self, envelope_state: &EnvelopeState, envelope: &agb_tracker_interop::Envelope, global_settings: &GlobalSettings, ) -> bool { let amount = envelope.amount[envelope_state.frame]; if envelope_state.finished { self.volume = (self.volume - envelope_state.fadeout).max(0.into()); } self.current_volume = (self.volume * amount.change_base() * global_settings.volume) .try_change_base() .unwrap(); self.volume != 0.into() } fn tick(&mut self) { self.vibrato.frame = (self.vibrato.frame + self.vibrato.speed) % 64; } } #[cfg(all(test, feature = "agb"))] #[agb::entry] fn main(gba: agb::Gba) -> ! { loop {} } #[cfg(feature = "agb")] impl SoundChannel for agb::sound::mixer::SoundChannel { fn new(data: &alloc::borrow::Cow<'static, [u8]>) -> Self { Self::new(match data { alloc::borrow::Cow::Borrowed(data) => data, alloc::borrow::Cow::Owned(_) => { unimplemented!("Must use borrowed COW data for tracker") } }) } fn stop(&mut self) { self.stop(); } fn pause(&mut self) -> &mut Self { self.pause() } fn resume(&mut self) -> &mut Self { self.resume() } fn should_loop(&mut self) -> &mut Self { self.should_loop() } fn volume(&mut self, value: impl Into>) -> &mut Self { self.volume(value) } fn restart_point(&mut self, value: impl Into>) -> &mut Self { self.restart_point(value) } fn playback(&mut self, playback_speed: impl Into>) -> &mut Self { self.playback(playback_speed) } fn panning(&mut self, panning: impl Into>) -> &mut Self { self.panning(panning) } fn set_pos(&mut self, pos: impl Into>) -> &mut Self { self.set_pos(pos) } } #[cfg(feature = "agb")] impl Mixer for agb::sound::mixer::Mixer<'_> { type ChannelId = agb::sound::mixer::ChannelId; type SoundChannel = agb::sound::mixer::SoundChannel; fn channel(&mut self, channel_id: &Self::ChannelId) -> Option<&mut Self::SoundChannel> { self.channel(channel_id) } fn play_sound(&mut self, channel: Self::SoundChannel) -> Option { self.play_sound(channel) } } #[cfg(feature = "agb")] /// The type to use if you're using agb-tracker with agb pub type Tracker = TrackerInner<'static, agb::sound::mixer::ChannelId>;