agb/tracker/agb-tracker/src/lib.rs

488 lines
16 KiB
Rust
Raw Normal View History

2023-07-12 21:10:05 +10:00
#![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))]
2023-07-24 05:18:55 +10:00
#![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};
2023-07-24 05:18:55 +10:00
//!
//! const DB_TOFFE: Track = include_xm!("examples/db_toffe.xm");
2023-07-24 05:18:55 +10:00
//!
//! #[agb::entry]
//! fn main(mut gba: Gba) -> ! {
//! let vblank_provider = agb::interrupt::VBlank::get();
//!
//! let mut mixer = gba.mixer.mixer(Frequency::Hz18157);
//! 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 18157Hz 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.
2023-07-12 21:10:05 +10:00
2023-07-13 02:36:41 +10:00
extern crate alloc;
2023-07-17 08:57:11 +10:00
use agb_tracker_interop::{PatternEffect, Sample};
2023-07-13 03:52:29 +10:00
use alloc::vec::Vec;
2023-07-13 02:36:41 +10:00
use agb::{
fixnum::Num,
sound::mixer::{ChannelId, Mixer, SoundChannel},
};
2023-07-13 02:36:41 +10:00
2023-07-24 05:18:55 +10:00
/// Import an XM file. Only available if you have the `xm` feature enabled (enabled by default).
2023-07-13 00:38:09 +10:00
#[cfg(feature = "xm")]
pub use agb_xm::include_xm;
2023-07-13 00:38:09 +10:00
2023-07-24 05:18:55 +10:00
#[doc(hidden)]
pub mod __private {
pub use agb::fixnum::Num;
pub use agb_tracker_interop;
}
2023-07-13 00:38:09 +10:00
/// A reference to a track. You should create this using one of the include macros.
pub use agb_tracker_interop::Track;
2023-07-13 00:38:09 +10:00
2023-07-24 05:18:55 +10:00
/// Stores the required state in order to play tracker music.
2023-07-13 02:36:41 +10:00
pub struct Tracker {
track: &'static Track<'static>,
2023-07-17 08:57:11 +10:00
channels: Vec<TrackerChannel>,
envelopes: Vec<Option<EnvelopeState>>,
2023-07-13 02:36:41 +10:00
frame: Num<u32, 8>,
tick: u32,
2023-07-17 09:27:20 +10:00
first: bool,
2023-08-05 07:30:49 +10:00
global_settings: GlobalSettings,
2023-07-13 02:36:41 +10:00
current_row: usize,
current_pattern: usize,
}
#[derive(Default)]
2023-07-17 08:57:11 +10:00
struct TrackerChannel {
channel_id: Option<ChannelId>,
base_speed: Num<u32, 16>,
2023-07-24 07:10:25 +10:00
volume: Num<i32, 8>,
2023-07-17 08:57:11 +10:00
}
struct EnvelopeState {
frame: usize,
envelope_id: usize,
2023-08-05 09:24:11 +10:00
finished: bool,
2023-08-06 08:51:12 +10:00
fadeout: Num<i32, 8>,
}
2023-08-05 07:30:49 +10:00
#[derive(Clone)]
struct GlobalSettings {
ticks_per_step: u32,
frames_per_tick: Num<u32, 8>,
2023-08-06 07:41:55 +10:00
volume: Num<i32, 8>,
2023-08-05 07:30:49 +10:00
}
2023-07-13 02:36:41 +10:00
impl Tracker {
2023-07-24 05:18:55 +10:00
/// Create a new tracker playing a specified track. See the [example](crate#example) for how to use the tracker.
2023-07-13 02:36:41 +10:00
pub fn new(track: &'static Track<'static>) -> Self {
2023-07-13 03:52:29 +10:00
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);
2023-07-13 02:36:41 +10:00
2023-08-05 07:30:49 +10:00
let global_settings = GlobalSettings {
ticks_per_step: track.ticks_per_step,
frames_per_tick: track.frames_per_tick,
2023-08-06 07:41:55 +10:00
volume: 1.into(),
2023-08-05 07:30:49 +10:00
};
2023-07-13 02:36:41 +10:00
Self {
track,
2023-07-13 03:52:29 +10:00
channels,
envelopes,
2023-07-13 02:36:41 +10:00
frame: 0.into(),
2023-07-17 09:27:20 +10:00
first: true,
tick: 0,
2023-08-05 07:30:49 +10:00
global_settings,
current_pattern: 0,
2023-08-05 07:30:49 +10:00
current_row: 0,
2023-07-13 02:36:41 +10:00
}
}
2023-07-24 05:18:55 +10:00
/// 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.
2023-07-13 02:36:41 +10:00
pub fn step(&mut self, mixer: &mut Mixer) {
2023-07-17 09:27:20 +10:00
if !self.increment_frame() {
self.update_envelopes(mixer);
2023-07-17 09:27:20 +10:00
return;
2023-07-13 02:36:41 +10:00
}
2023-07-13 04:06:55 +10:00
let pattern_to_play = self.track.patterns_to_play[self.current_pattern];
let current_pattern = &self.track.patterns[pattern_to_play];
2023-07-13 02:36:41 +10:00
2023-07-13 03:52:29 +10:00
let pattern_data_pos =
current_pattern.start_position + self.current_row * self.track.num_channels;
2023-07-13 02:36:41 +10:00
let pattern_slots =
2023-07-13 03:52:29 +10:00
&self.track.pattern_data[pattern_data_pos..pattern_data_pos + self.track.num_channels];
2023-07-13 02:36:41 +10:00
2023-08-05 09:24:11 +10:00
for (i, (channel, pattern_slot)) in self.channels.iter_mut().zip(pattern_slots).enumerate()
{
2023-07-17 09:27:20 +10:00
if pattern_slot.sample != 0 && self.tick == 0 {
let sample = &self.track.samples[pattern_slot.sample as usize - 1];
2023-08-06 07:41:55 +10:00
channel.play_sound(mixer, sample, &self.global_settings);
2023-08-05 09:24:11 +10:00
self.envelopes[i] = sample.volume_envelope.map(|envelope_id| EnvelopeState {
frame: 0,
envelope_id,
finished: false,
2023-08-06 08:51:12 +10:00
fadeout: sample.fadeout,
2023-08-05 09:24:11 +10:00
});
2023-07-13 02:36:41 +10:00
}
2023-07-17 08:57:11 +10:00
2023-07-19 22:53:46 +10:00
if self.tick == 0 {
channel.set_speed(mixer, pattern_slot.speed.change_base());
}
2023-08-05 07:30:49 +10:00
channel.apply_effect(
mixer,
&pattern_slot.effect1,
self.tick,
&mut self.global_settings,
2023-08-05 09:24:11 +10:00
&mut self.envelopes[i],
2023-08-05 07:30:49 +10:00
);
channel.apply_effect(
mixer,
&pattern_slot.effect2,
self.tick,
&mut self.global_settings,
2023-08-05 09:24:11 +10:00
&mut self.envelopes[i],
2023-08-05 07:30:49 +10:00
);
2023-07-13 02:36:41 +10:00
}
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];
2023-08-06 07:41:55 +10:00
if !channel.update_volume_envelope(
mixer,
2023-08-06 08:51:12 +10:00
envelope_state,
2023-08-06 07:41:55 +10:00
envelope,
&self.global_settings,
) {
envelope_state_option.take();
} else {
envelope_state.frame += 1;
2023-08-05 09:24:11 +10:00
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;
}
}
}
}
2023-07-13 02:36:41 +10:00
}
2023-07-17 09:27:20 +10:00
fn increment_frame(&mut self) -> bool {
if self.first {
self.first = false;
return true;
}
self.frame += 1;
2023-08-05 07:30:49 +10:00
if self.frame >= self.global_settings.frames_per_tick {
self.tick += 1;
2023-08-05 07:30:49 +10:00
self.frame -= self.global_settings.frames_per_tick;
2023-07-13 02:36:41 +10:00
2023-08-05 07:30:49 +10:00
if self.tick >= self.global_settings.ticks_per_step {
2023-07-17 09:27:20 +10:00
self.current_row += 1;
2023-07-13 02:36:41 +10:00
2023-07-17 09:27:20 +10:00
if self.current_row
>= self.track.patterns[self.track.patterns_to_play[self.current_pattern]].length
{
self.current_pattern += 1;
self.current_row = 0;
2023-07-13 04:06:55 +10:00
2023-07-17 09:27:20 +10:00
if self.current_pattern >= self.track.patterns_to_play.len() {
self.current_pattern = self.track.repeat;
2023-07-17 09:27:20 +10:00
}
2023-07-13 04:06:55 +10:00
}
2023-07-17 09:27:20 +10:00
self.tick = 0;
2023-07-13 02:36:41 +10:00
}
2023-07-17 09:27:20 +10:00
true
} else {
false
2023-07-13 02:36:41 +10:00
}
2023-07-12 21:10:05 +10:00
}
}
2023-07-17 08:57:11 +10:00
impl TrackerChannel {
2023-08-06 07:41:55 +10:00
fn play_sound(
&mut self,
mixer: &mut Mixer<'_>,
sample: &Sample<'static>,
global_settings: &GlobalSettings,
) {
2023-07-24 04:08:51 +10:00
if let Some(channel) = self
.channel_id
2023-07-17 08:57:11 +10:00
.take()
.and_then(|channel_id| mixer.channel(&channel_id))
2023-07-24 04:08:51 +10:00
{
channel.stop();
}
2023-07-17 08:57:11 +10:00
let mut new_channel = SoundChannel::new(sample.data);
2023-08-06 07:41:55 +10:00
new_channel.volume(
(sample.volume.change_base() * global_settings.volume)
.try_change_base()
.unwrap(),
);
2023-07-19 06:36:37 +10:00
2023-07-17 08:57:11 +10:00
if sample.should_loop {
new_channel
.should_loop()
.restart_point(sample.restart_point);
}
self.channel_id = mixer.play_sound(new_channel);
2023-07-24 07:10:25 +10:00
self.volume = sample.volume.change_base();
2023-07-17 08:57:11 +10:00
}
2023-07-19 22:53:46 +10:00
fn set_speed(&mut self, mixer: &mut Mixer<'_>, speed: Num<u32, 8>) {
2023-07-17 08:57:11 +10:00
if let Some(channel) = self
.channel_id
.as_ref()
2023-07-24 04:08:51 +10:00
.and_then(|channel_id| mixer.channel(channel_id))
2023-07-17 08:57:11 +10:00
{
if speed != 0.into() {
self.base_speed = speed.change_base();
2023-07-17 08:57:11 +10:00
}
channel.playback(self.base_speed.change_base());
2023-07-19 22:53:46 +10:00
}
}
2023-07-19 07:17:17 +10:00
2023-08-05 07:30:49 +10:00
fn apply_effect(
&mut self,
mixer: &mut Mixer<'_>,
effect: &PatternEffect,
tick: u32,
global_settings: &mut GlobalSettings,
2023-08-05 09:24:11 +10:00
envelope_state: &mut Option<EnvelopeState>,
2023-08-05 07:30:49 +10:00
) {
2023-07-19 22:53:46 +10:00
if let Some(channel) = self
.channel_id
.as_ref()
2023-07-24 04:08:51 +10:00
.and_then(|channel_id| mixer.channel(channel_id))
2023-07-19 22:53:46 +10:00
{
2023-07-17 08:57:11 +10:00
match effect {
PatternEffect::None => {}
PatternEffect::Stop => {
channel.volume(0);
2023-08-05 09:24:11 +10:00
if let Some(envelope_state) = envelope_state {
envelope_state.finished = true;
}
2023-07-17 08:57:11 +10:00
}
2023-07-17 09:27:20 +10:00
PatternEffect::Arpeggio(first, second) => {
match tick % 3 {
0 => channel.playback(self.base_speed.change_base()),
2023-07-19 07:17:17 +10:00
1 => channel.playback(first.change_base()),
2 => channel.playback(second.change_base()),
2023-07-17 09:27:20 +10:00
_ => unreachable!(),
};
}
2023-07-17 08:57:11 +10:00
PatternEffect::Panning(panning) => {
channel.panning(panning.change_base());
2023-07-17 08:57:11 +10:00
}
PatternEffect::Volume(volume) => {
2023-08-06 07:41:55 +10:00
channel.volume(
(volume.change_base() * global_settings.volume)
.try_change_base()
.unwrap(),
);
2023-07-24 07:10:25 +10:00
self.volume = volume.change_base();
2023-07-17 09:45:58 +10:00
}
PatternEffect::VolumeSlide(amount) => {
if tick != 0 {
2023-07-24 07:10:25 +10:00
self.volume = (self.volume + amount.change_base()).max(0.into());
2023-08-06 07:41:55 +10:00
channel.volume(
(self.volume * global_settings.volume)
.try_change_base()
.unwrap(),
);
2023-07-24 07:03:32 +10:00
}
}
PatternEffect::FineVolumeSlide(amount) => {
if tick == 0 {
2023-07-24 07:10:25 +10:00
self.volume = (self.volume + amount.change_base()).max(0.into());
2023-08-06 07:41:55 +10:00
channel.volume(
(self.volume * global_settings.volume)
.try_change_base()
.unwrap(),
);
2023-07-17 09:45:58 +10:00
}
2023-07-17 08:57:11 +10:00
}
2023-07-19 22:22:26 +10:00
PatternEffect::NoteCut(wait) => {
if tick == *wait {
channel.volume(0);
2023-08-05 09:24:11 +10:00
if let Some(envelope_state) = envelope_state {
envelope_state.finished = true;
}
2023-07-19 22:22:26 +10:00
}
}
2023-07-19 22:38:32 +10:00
PatternEffect::Portamento(amount) => {
2023-07-24 06:36:02 +10:00
if tick != 0 {
self.base_speed *= amount.change_base();
channel.playback(self.base_speed.change_base());
2023-07-24 06:36:02 +10:00
}
}
PatternEffect::TonePortamento(amount, target) => {
2023-08-06 07:41:55 +10:00
channel.volume(
(self.volume * global_settings.volume)
.try_change_base()
.unwrap(),
);
2023-08-05 10:33:48 +10:00
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());
}
2023-07-19 22:38:32 +10:00
}
channel.playback(self.base_speed.change_base());
2023-07-19 22:38:32 +10:00
}
2023-08-05 07:30:49 +10:00
// These are global effects handled below
2023-08-06 07:41:55 +10:00
PatternEffect::SetTicksPerStep(_)
| PatternEffect::SetFramesPerTick(_)
| PatternEffect::SetGlobalVolume(_)
| PatternEffect::GlobalVolumeSlide(_) => {}
2023-08-05 07:30:49 +10:00
}
}
// Some effects have to happen regardless of if we're actually playing anything
match effect {
PatternEffect::SetTicksPerStep(amount) => {
2023-08-06 07:41:55 +10:00
global_settings.ticks_per_step = *amount;
2023-08-05 07:30:49 +10:00
}
PatternEffect::SetFramesPerTick(new_frames_per_tick) => {
2023-08-06 07:41:55 +10:00
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());
2023-07-17 08:57:11 +10:00
}
2023-08-05 07:30:49 +10:00
_ => {}
2023-07-17 08:57:11 +10:00
}
}
#[must_use]
fn update_volume_envelope(
2023-08-06 08:51:12 +10:00
&mut self,
mixer: &mut Mixer<'_>,
2023-08-06 08:51:12 +10:00
envelope_state: &EnvelopeState,
envelope: &agb_tracker_interop::Envelope<'_>,
2023-08-06 07:41:55 +10:00
global_settings: &GlobalSettings,
) -> bool {
if let Some(channel) = self
.channel_id
.as_ref()
.and_then(|channel_id| mixer.channel(channel_id))
{
2023-08-06 08:51:12 +10:00
let amount = envelope.amount[envelope_state.frame];
if envelope_state.finished {
self.volume = (self.volume - envelope_state.fadeout).max(0.into());
}
channel.volume(
2023-08-06 07:41:55 +10:00
(self.volume * amount.change_base() * global_settings.volume)
.try_change_base()
.unwrap(),
);
2023-08-06 08:51:12 +10:00
self.volume != 0.into()
} else {
false
}
}
2023-07-17 08:57:11 +10:00
}
2023-07-12 21:10:05 +10:00
#[cfg(test)]
#[agb::entry]
fn main(gba: agb::Gba) -> ! {
loop {}
}