From 667befbf8db41870f5896d60266b93a84fe553fe Mon Sep 17 00:00:00 2001 From: Corwin Date: Tue, 20 Jan 2026 01:57:36 +0000 Subject: [PATCH 1/2] remove allocation from tracker --- tracker/agb-midi-core/src/lib.rs | 7 ++- tracker/agb-tracker-interop/src/lib.rs | 75 ++++++++++++++++++++------ tracker/agb-tracker/src/lib.rs | 59 +++++++++++++------- tracker/agb-tracker/src/mixer.rs | 6 +-- tracker/desktop-player/Cargo.toml | 2 +- 5 files changed, 104 insertions(+), 45 deletions(-) diff --git a/tracker/agb-midi-core/src/lib.rs b/tracker/agb-midi-core/src/lib.rs index 8f4329792..bcc85b214 100644 --- a/tracker/agb-midi-core/src/lib.rs +++ b/tracker/agb-midi-core/src/lib.rs @@ -1,5 +1,4 @@ use std::{ - borrow::Cow, collections::HashMap, error::Error, fs::{self, File}, @@ -8,7 +7,7 @@ use std::{ }; use agb_fixnum::Num; -use agb_tracker_interop::{Envelope, Pattern, PatternEffect, PatternSlot, Sample, Track}; +use agb_tracker_interop::{Data, Envelope, Pattern, PatternEffect, PatternSlot, Sample, Track}; use midly::{Format, MetaMessage, Smf, Timing, TrackEventKind}; use rustysynth::SoundFont; @@ -368,12 +367,12 @@ pub fn parse_midi(midi_info: &MidiInfo) -> Track { Track { samples: samples.into(), envelopes: envelopes.into(), - patterns: Cow::from(vec![Pattern { + patterns: Data::from(vec![Pattern { length: pattern.len() / resulting_num_channels, start_position: 0, }]), pattern_data: pattern.into(), - patterns_to_play: Cow::from(vec![0]), + patterns_to_play: Data::from(vec![0]), num_channels: resulting_num_channels, frames_per_tick: Num::from_f64(frames_per_tick), ticks_per_step: 1, diff --git a/tracker/agb-tracker-interop/src/lib.rs b/tracker/agb-tracker-interop/src/lib.rs index 99c789eba..99d031be0 100644 --- a/tracker/agb-tracker-interop/src/lib.rs +++ b/tracker/agb-tracker-interop/src/lib.rs @@ -1,17 +1,58 @@ #![cfg_attr(not(feature = "std"), no_std)] -extern crate alloc; +use core::ops::Deref; use agb_fixnum::Num; -use alloc::borrow::Cow; + +#[derive(Debug, Clone)] +pub enum Data +where + Content: 'static, +{ + Static(&'static [Content]), + #[cfg(feature = "std")] + Owned(Box<[Content]>), +} + +#[cfg(feature = "std")] +impl From> for Data { + fn from(value: Vec) -> Self { + Data::Owned(value.into_boxed_slice()) + } +} + +impl Deref for Data +where + Content: 'static, +{ + type Target = [Content]; + + fn deref(&self) -> &[Content] { + match self { + Data::Static(items) => items, + #[cfg(feature = "std")] + Data::Owned(items) => &*items, + } + } +} + +impl Data { + pub fn get_static(&self) -> &'static [Content] { + match self { + Data::Static(items) => items, + #[cfg(feature = "std")] + Data::Owned(_) => panic!("Cannot get static from owned data"), + } + } +} #[derive(Debug)] pub struct Track { - pub samples: Cow<'static, [Sample]>, - pub envelopes: Cow<'static, [Envelope]>, - pub pattern_data: Cow<'static, [PatternSlot]>, - pub patterns: Cow<'static, [Pattern]>, - pub patterns_to_play: Cow<'static, [usize]>, + pub samples: Data, + pub envelopes: Data, + pub pattern_data: Data, + pub patterns: Data, + pub patterns_to_play: Data, pub num_channels: usize, pub frames_per_tick: Num, @@ -21,7 +62,7 @@ pub struct Track { #[derive(Debug, Clone)] pub struct Sample { - pub data: Cow<'static, [u8]>, + pub data: Data, pub should_loop: bool, pub restart_point: u32, pub volume: Num, @@ -63,7 +104,7 @@ pub struct PatternSlot { #[derive(Debug, Clone)] pub struct Envelope { - pub amount: Cow<'static, [Num]>, + pub amount: Data>, pub sustain: Option, pub loop_start: Option, pub loop_end: Option, @@ -176,9 +217,9 @@ impl quote::ToTokens for Track { tokens.append_all(quote! { { - use alloc::borrow::Cow; use agb_tracker::__private::agb_tracker_interop::*; use agb_tracker::__private::Num; + use agb_tracker::__private::static_data; static SAMPLES: &[Sample] = &[#(#samples),*]; static PATTERN_DATA: &[PatternSlot] = &[#(#pattern_data),*]; @@ -187,11 +228,11 @@ impl quote::ToTokens for Track { static ENVELOPES: &[Envelope] = &[#(#envelopes),*]; agb_tracker::Track { - samples: Cow::Borrowed(SAMPLES), - envelopes: Cow::Borrowed(ENVELOPES), - pattern_data: Cow::Borrowed(PATTERN_DATA), - patterns: Cow::Borrowed(PATTERNS), - patterns_to_play: Cow::Borrowed(PATTERNS_TO_PLAY), + samples: static_data(SAMPLES), + envelopes: static_data(ENVELOPES), + pattern_data: static_data(PATTERN_DATA), + patterns: static_data(PATTERNS), + patterns_to_play: static_data(PATTERNS_TO_PLAY), frames_per_tick: Num::from_raw(#frames_per_tick), num_channels: #num_channels, @@ -246,7 +287,7 @@ impl quote::ToTokens for Envelope { static AMOUNTS: &[agb_tracker::__private::Num] = &[#(#amount),*]; agb_tracker::__private::agb_tracker_interop::Envelope { - amount: Cow::Borrowed(AMOUNTS), + amount: static_data(AMOUNTS), sustain: #sustain, loop_start: #loop_start, loop_end: #loop_end, @@ -301,7 +342,7 @@ impl quote::ToTokens for Sample { static SAMPLE_DATA: &[u8] = &AlignmentWrapper(*#samples).0; agb_tracker::__private::agb_tracker_interop::Sample { - data: Cow::Borrowed(SAMPLE_DATA), + data: static_data(SAMPLE_DATA), should_loop: #should_loop, restart_point: #restart_point, volume: agb_tracker::__private::Num::from_raw(#volume), diff --git a/tracker/agb-tracker/src/lib.rs b/tracker/agb-tracker/src/lib.rs index 5f61734cd..74961e154 100644 --- a/tracker/agb-tracker/src/lib.rs +++ b/tracker/agb-tracker/src/lib.rs @@ -63,13 +63,16 @@ //! Currently, only XM is implemented, however, more formats could be added in future depending //! on demand. +#[cfg(not(feature = "agb"))] extern crate alloc; mod lookups; mod mixer; +use agb_fixnum::num; +#[cfg(feature = "agb")] +use agb_tracker_interop::Data; use agb_tracker_interop::{Jump, PatternEffect, Sample, Waveform}; -use alloc::vec::Vec; pub use mixer::{Mixer, SoundChannel}; @@ -98,18 +101,25 @@ pub use agb_midi::include_midi; pub mod __private { pub use agb_fixnum::Num; pub use agb_tracker_interop; + use agb_tracker_interop::Data; + + pub const fn static_data(data: &'static [Content]) -> Data { + Data::Static(data) + } } /// A reference to a track. You should create this using one of the include macros. pub use agb_tracker_interop::Track; +const NUMBER_OF_CHANNELS: usize = 8; + /// Stores the required state in order to play tracker music. pub struct TrackerInner<'track, TChannelId> { track: &'track Track, - channels: Vec, - envelopes: Vec>, + channels: [TrackerChannel; NUMBER_OF_CHANNELS], + envelopes: [Option; NUMBER_OF_CHANNELS], - mixer_channels: Vec>, + mixer_channels: [Option; NUMBER_OF_CHANNELS], frame: Num, tick: u32, @@ -187,14 +197,12 @@ struct GlobalSettings { 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); + assert!(track.num_channels < NUMBER_OF_CHANNELS); + let channels = [const { TrackerChannel::new() }; NUMBER_OF_CHANNELS]; - let mut envelopes = Vec::new(); - envelopes.resize_with(track.num_channels, || None); + let envelopes = [const { None }; NUMBER_OF_CHANNELS]; - let mut mixer_channels = Vec::new(); - mixer_channels.resize_with(track.num_channels, || None); + let mixer_channels = [const { None }; NUMBER_OF_CHANNELS]; let global_settings = GlobalSettings { ticks_per_step: track.ticks_per_step, @@ -460,6 +468,26 @@ impl<'track, TChannelId> TrackerInner<'track, TChannelId> { } impl TrackerChannel { + const fn new() -> Self { + Self { + original_speed: num!(0), + base_speed: num!(0), + volume: num!(0), + vibrato: Waves { + waveform: Waveform::Sine, + frame: 0, + speed: 0, + amount: num!(0), + enable: false, + }, + current_volume: num!(0), + current_speed: num!(0), + current_panning: num!(0), + is_playing: false, + current_pos: None, + } + } + fn reset(&mut self, sample: &Sample) { self.volume = sample.volume.change_base(); self.current_volume = self.volume; @@ -672,15 +700,8 @@ fn main(_gba: agb::Gba) -> ! { #[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) => - // Safety: should be good by construction, but it'll blow up if you try and play it and it isn't aligned - unsafe { agb::sound::mixer::SoundData::new(data) }, - alloc::borrow::Cow::Owned(_) => { - unimplemented!("Must use borrowed COW data for tracker") - } - }) + fn new(data: &Data) -> Self { + Self::new(unsafe { agb::sound::mixer::SoundData::new(data.get_static()) }) } fn stop(&mut self) { diff --git a/tracker/agb-tracker/src/mixer.rs b/tracker/agb-tracker/src/mixer.rs index dcb1cc60d..266d67b34 100644 --- a/tracker/agb-tracker/src/mixer.rs +++ b/tracker/agb-tracker/src/mixer.rs @@ -1,12 +1,10 @@ #![allow(missing_docs)] use agb_fixnum::Num; -use alloc::borrow::Cow; +use agb_tracker_interop::Data; pub trait SoundChannel { - // I need a reference to a cow here to support the static data correctly - #[allow(clippy::ptr_arg)] - fn new(data: &Cow<'static, [u8]>) -> Self; + fn new(data: &Data) -> Self; fn stop(&mut self); fn pause(&mut self) -> &mut Self; diff --git a/tracker/desktop-player/Cargo.toml b/tracker/desktop-player/Cargo.toml index de75e480f..1dec0870b 100644 --- a/tracker/desktop-player/Cargo.toml +++ b/tracker/desktop-player/Cargo.toml @@ -8,7 +8,7 @@ description = "A way to play XM files on desktop as they would on the gba withou repository = "https://github.com/agbrs/agb" [dependencies] -agb_xm_core = { version = "0.22.6", path = "../agb-xm-core" } +agb_xm_core = { version = "0.22.6", path = "../agb-xm-core", features = ["alloc"] } agb_tracker = { version = "0.22.6", path = "../agb-tracker", default-features = false } agb_fixnum = { version = "0.22.6", path = "../../agb-fixnum" } From 4849bde96421d525e8a575b68a7a2f5958076f43 Mon Sep 17 00:00:00 2001 From: Corwin Date: Tue, 20 Jan 2026 02:23:48 +0000 Subject: [PATCH 2/2] fix the desktop player --- tracker/agb-tracker-interop/src/lib.rs | 2 +- tracker/desktop-player/Cargo.toml | 3 ++- tracker/desktop-player/src/mixer.rs | 9 +++++---- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/tracker/agb-tracker-interop/src/lib.rs b/tracker/agb-tracker-interop/src/lib.rs index 99d031be0..8e267e407 100644 --- a/tracker/agb-tracker-interop/src/lib.rs +++ b/tracker/agb-tracker-interop/src/lib.rs @@ -31,7 +31,7 @@ where match self { Data::Static(items) => items, #[cfg(feature = "std")] - Data::Owned(items) => &*items, + Data::Owned(items) => items, } } } diff --git a/tracker/desktop-player/Cargo.toml b/tracker/desktop-player/Cargo.toml index 1dec0870b..2eebbd584 100644 --- a/tracker/desktop-player/Cargo.toml +++ b/tracker/desktop-player/Cargo.toml @@ -8,9 +8,10 @@ description = "A way to play XM files on desktop as they would on the gba withou repository = "https://github.com/agbrs/agb" [dependencies] -agb_xm_core = { version = "0.22.6", path = "../agb-xm-core", features = ["alloc"] } +agb_xm_core = { version = "0.22.6", path = "../agb-xm-core" } agb_tracker = { version = "0.22.6", path = "../agb-tracker", default-features = false } agb_fixnum = { version = "0.22.6", path = "../../agb-fixnum" } +agb_tracker_interop = { version = "0.22.6", path = "../agb-tracker-interop", features = ["std"] } anyhow = "1" xmrs = "=0.8.5" diff --git a/tracker/desktop-player/src/mixer.rs b/tracker/desktop-player/src/mixer.rs index 2c427bbd5..34917cb44 100644 --- a/tracker/desktop-player/src/mixer.rs +++ b/tracker/desktop-player/src/mixer.rs @@ -1,5 +1,6 @@ use agb_fixnum::Num; -use std::{borrow::Cow, num::Wrapping}; +use agb_tracker_interop::Data; +use std::num::Wrapping; const BUFFER_SIZE: usize = 560; const NUM_CHANNELS: usize = 8; @@ -81,7 +82,7 @@ impl Mixer { } pub struct SoundChannel { - data: Cow<'static, [u8]>, + data: Data, pos: Num, should_loop: bool, restart_point: Num, @@ -110,7 +111,7 @@ impl std::fmt::Debug for SoundChannel { } impl SoundChannel { - fn new(data: Cow<'static, [u8]>) -> Self { + fn new(data: Data) -> Self { Self { data: data.clone(), @@ -129,7 +130,7 @@ impl SoundChannel { pub struct SoundChannelId(usize, Wrapping); impl agb_tracker::SoundChannel for SoundChannel { - fn new(data: &Cow<'static, [u8]>) -> Self { + fn new(data: &Data) -> Self { Self::new(data.clone()) }