XM tracker (#461)

Closes #446

One big problem with our games so far is that they are 95% music files.
If we want to add a game with more music, we can't at the moment.

This adds a tracker player which can play XM files easily in your games.
A lot of features from tracker files aren't supported yet, but enough to
make the examples sound at least half decent.

- [x] Changelog updated
This commit is contained in:
Gwilym Inzani 2023-07-25 20:29:31 +01:00 committed by GitHub
commit bddb77f5c9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 1597 additions and 231 deletions

View file

@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
### Added
- New tracker for playing XM files (see the `agb-tracker` crate).
- You can now declare where looping sound channels should restart.
### Changed
- Sound channel panning and volume options are now `Num<i16, 8>` rather than `Num<i16, 4>` for improved precision and sound quality.
### Fixed
- Mono looping samples will now correctly play to the end if it doesn't perfectly align with a buffer boundry and short samples now also loop correctly.
## [0.16.0] - 2023/07/18 ## [0.16.0] - 2023/07/18
### Added ### Added

View file

@ -16,11 +16,11 @@ without needing to have extensive knowledge of its low-level implementation.
agb provides the following features: agb provides the following features:
* Simple build process with minimal dependencies - Simple build process with minimal dependencies
* Built in importing of sprites, backgrounds, music and sound effects - Built in importing of sprites, backgrounds, music and sound effects
* High performance audio mixer - High performance audio mixer
* Easy to use sprite and tiled background usage - Easy to use sprite and tiled background usage
* A global allocator allowing for use of both `core` and `alloc` - A global allocator allowing for use of both `core` and `alloc`
The documentation for the latest release can be found on The documentation for the latest release can be found on
[docs.rs](https://docs.rs/agb/latest/agb/). [docs.rs](https://docs.rs/agb/latest/agb/).
@ -41,29 +41,29 @@ is a great place to get help from the creators and contributors.
Feel free to [create a new discussion in the Q&A category](https://github.com/agbrs/agb/discussions/new?category=Q-A) and we'll do our best to help! Feel free to [create a new discussion in the Q&A category](https://github.com/agbrs/agb/discussions/new?category=Q-A) and we'll do our best to help!
## Contributing to agb itself ## Contributing to agb itself
In order to contribute to agb itself, you will need a few extra tools on top of what you would need In order to contribute to agb itself, you will need a few extra tools on top of what you would need
to just write games for the Game Boy Advance using this library: to just write games for the Game Boy Advance using this library:
* Recent rustup, see [the rust website](https://www.rust-lang.org/tools/install) - Recent rustup, see [the rust website](https://www.rust-lang.org/tools/install)
for instructions for your operating system. for instructions for your operating system.
* You can update rustup with `rustup update`, or using your package manager - You can update rustup with `rustup update`, or using your package manager
if you obtained rustup in this way. if you obtained rustup in this way.
* libelf and cmake - libelf and cmake
* Debian and derivatives: `sudo apt install libelf-dev cmake` - Debian and derivatives: `sudo apt install libelf-dev cmake`
* Arch Linux and derivatives: `pacman -S libelf cmake` - Arch Linux and derivatives: `pacman -S libelf cmake`
* mgba-test-runner - mgba-test-runner
* Run `cargo install --path mgba-test-runner` inside this directory - Run `cargo install --path mgba-test-runner` inside this directory
* [The 'just' build tool](https://github.com/casey/just) - [The 'just' build tool](https://github.com/casey/just)
* Install with `cargo install just` - Install with `cargo install just`
* [mdbook](https://rust-lang.github.io/mdBook/index.html) - [mdbook](https://rust-lang.github.io/mdBook/index.html)
* Install with `cargo install mdbook` - Install with `cargo install mdbook`
* [miri](https://github.com/rust-lang/miri) - [miri](https://github.com/rust-lang/miri)
* Some of the unsafe code is tested using miri, install with `rustup component add miri` - Some of the unsafe code is tested using miri, install with `rustup component add miri`
With all of this installed, you should be able to run a full build of agb using by running With all of this installed, you should be able to run a full build of agb using by running
```sh ```sh
just ci just ci
``` ```
@ -87,6 +87,8 @@ for performant decimals.
`agb/examples` - basic examples often targeting 1 feature, you can run these using `just run-example <example-name>` `agb/examples` - basic examples often targeting 1 feature, you can run these using `just run-example <example-name>`
`tracker` - crates that make up the `agb-tracker` library which allows playing of tracker files
`book` - the source for the tutorial and website `book` - the source for the tutorial and website
`book/games` - games made as part of the tutorial `book/games` - games made as part of the tutorial
@ -108,10 +110,10 @@ Once agb reaches version 1.0, we will transition to stronger semantic versioning
agb would not be possible without the help from the following (non-exhaustive) list of projects: agb would not be possible without the help from the following (non-exhaustive) list of projects:
* The amazing work of the [rust-console](https://github.com/rust-console) for making this all possible in the first place - The amazing work of the [rust-console](https://github.com/rust-console) for making this all possible in the first place
* The [asefile](https://crates.io/crates/asefile) crate for loading aseprite files - The [asefile](https://crates.io/crates/asefile) crate for loading aseprite files
* [agbabi](https://github.com/felixjones/agbabi) for providing high performance alternatives to common methods - [agbabi](https://github.com/felixjones/agbabi) for providing high performance alternatives to common methods
* [mgba](https://mgba.io) for all the useful debugging / developer tools built in to the emulator - [mgba](https://mgba.io) for all the useful debugging / developer tools built in to the emulator
## Licence ## Licence

View file

@ -23,8 +23,7 @@ bilge = "0.1"
rustc-hash = { version = "1", default-features = false } rustc-hash = { version = "1", default-features = false }
[package.metadata.docs.rs] [package.metadata.docs.rs]
default-target = "thumbv6m-none-eabi" default-target = "thumbv4t-none-eabi"
targets = []
[profile.dev] [profile.dev]
opt-level = 3 opt-level = 3

View file

@ -25,7 +25,7 @@ fn main(mut gba: Gba) -> ! {
{ {
if let Some(channel) = mixer.channel(&channel_id) { if let Some(channel) = mixer.channel(&channel_id) {
let half: Num<i16, 4> = num!(0.5); let half: Num<i16, 8> = num!(0.5);
let half_usize: Num<u32, 8> = num!(0.5); let half_usize: Num<u32, 8> = num!(0.5);
match input.x_tri() { match input.x_tri() {
Tri::Negative => channel.panning(-half), Tri::Negative => channel.panning(-half),

View file

@ -1,108 +1,91 @@
.section .iwram.buffer_size .macro mono_add_fn_loop fn_name:req is_first:req is_loop:req
.global agb_rs__buffer_size
.balign 4
agb_rs__buffer_size:
.word 0
.macro mixer_add fn_name:req is_first:req
agb_arm_func \fn_name agb_arm_func \fn_name
@ Arguments @ Arguments
@ r0 - pointer to the data to be copied (u8 array) @ r0 - pointer to the sample data from the beginning
@ r1 - pointer to the sound buffer (i16 array which will alternate left and right channels, 32-bit aligned) @ r1 - pointer to the target sample buffer &[i32; BUFFER_SIZE]
@ r2 - playback speed (usize fixnum with 8 bits) @ r2 - BUFFER_SIZE - the length of the array in r1. Must be a multiple of 4
@ r3 - amount to modify the left channel by (u16 fixnum with 4 bits) @ r3 - (length - restart point) (how much to rewind by)
@ stack position 1 - amount to modify the right channel by (u16 fixnum with 4 bits) @ Stack position 1 - channel length
@ Stack position 2 - current channel position
@ Stack position 3 - the playback speed
@ Stack position 4 - the amount to multiply by
@ @
@ The sound buffer must be SOUND_BUFFER_SIZE * 2 in size = 176 * 2 @ Returns the new channel position
push {{r4-r8}} push {{r4-r11}}
ldr r7, [sp, #20] @ load the right channel modification amount into r7
cmp r7, r3 @ check if left and right channel need the same modifications
beq 3f @ same modification
4: @ modification fallback
orr r7, r7, r3, lsl #16 @ r7 now is the left channel followed by the right channel modifications.
mov r5, #0 @ current index we're reading from
ldr r8, =agb_rs__buffer_size @ the number of steps left
ldr r8, [r8]
ldr r4, [sp, #(8*4)] @ load the channel length into r4
ldr r5, [sp, #(9*4)] @ load the current channel position into r5
ldr r6, [sp, #(10*4)] @ load the playback speed into r6
ldr r12, [sp, #(11*4)] @ load the amount to multiply by into r12
@ The core loop
1: 1:
.rept 4 .ifc \is_first,false
add r4, r0, r5, asr #8 @ calculate the address of the next read from the sound buffer ldm r1, {{r7-r10}}
ldrsb r6, [r4] @ load the current sound sample to r6
add r5, r5, r2 @ calculate the position to read the next sample from
.ifc \is_first,true
mul r4, r6, r7 @ r4 = r6 * r7 (calculating both the left and right samples together)
.else
ldr r4, [r1] @ read the current value
mla r4, r6, r7, r4 @ r4 += r6 * r7 (calculating both the left and right samples together)
.endif .endif
str r4, [r1], #4 @ store the new value, and increment the pointer .irp reg, r7,r8,r9,r10
cmp r4, r5, lsr #8 @ check if we're overflowing
.ifc \is_loop,true
suble r5, r5, r3 @ if we are, subtract the overflow amount
.else
ble 2f @ if we are, zero the rest of the buffer
.endif
mov r11, r5, lsr #8 @ calculate the next location to get a value from
ldrsb r11, [r0, r11] @ load a single value
.ifc \is_first,true @ multiply the sample value, but only add if not the first call
mul \reg, r11, r12
.else
mla \reg, r11, r12, \reg
.endif
add r5, r5, r6 @ calculate the next sample read location
.endr .endr
subs r8, r8, #4 @ loop counter stmia r1!, {{r7-r10}}
bne 1b @ jump back if we're done with the loop
pop {{r4-r8}} subs r2, r2, #4
bx lr
3: @ same modification
@ check to see if this is a perfect power of 2
@ r5 is a scratch register, r7 = r3 = amount to modify
sub r5, r7, #1
ands r5, r5, r7
bne 4b @ not 0 means we need to do the full modification, jump to modification fallback
@ count leading zeros of r7 into r3
mov r3, #0
1:
add r3, r3, #1
lsrs r7, r7, #1
bne 1b bne 1b
sub r3, r3, #1 .ifc \is_loop,false
b 3f
mov r5, #0 @ current index we're reading from 2:
ldr r8, =agb_rs__buffer_size @ the number of steps left .ifc \is_first,true @ zero the rest of the buffer as this sample has ended
ldr r8, [r8] ands r7, r2, #3
sub r2, r2, r7
beq 5f
1: mov r8, #0
.rept 4 4:
add r4, r0, r5, asr #8 @ calculate the address of the next read from the sound buffer stmia r1!, {{r8}}
ldrsb r6, [r4] @ load the current sound sample to r6 subs r7, r7, #1
add r5, r5, r2 @ calculate the position to read the next sample from bne 4b
5:
lsl r6, r6, #16 .irp reg, r7,r8,r9,r10
orr r6, r6, lsr #16 mov \reg, #0
.endr
.ifc \is_first,true 5:
mov r4, r6, lsl r3 @ r4 = r6 << r3 stmia r1!, {{r7-r10}}
.else subs r2, r2, #4
ldr r4, [r1] @ read the current value bne 5b
add r4, r4, r6, lsl r3 @ r4 += r6 << r3 (calculating both the left and right samples together) .endif
3:
.endif .endif
str r4, [r1], #4 @ store the new value, and increment the pointer mov r0, r5 @ return the playback position
.endr pop {{r4-r11}}
subs r8, r8, #4 @ loop counter
bne 1b @ jump back if we're done with the loop
pop {{r4-r8}}
bx lr bx lr
agb_arm_end \fn_name agb_arm_end \fn_name
.endm .endm
mixer_add agb_rs__mixer_add false mono_add_fn_loop agb_rs__mixer_add_mono_loop_first true true
mixer_add agb_rs__mixer_add_first true mono_add_fn_loop agb_rs__mixer_add_mono_loop false true
mono_add_fn_loop agb_rs__mixer_add_mono_first true false
mono_add_fn_loop agb_rs__mixer_add_mono false false
.macro stereo_add_fn fn_name:req is_first:req .macro stereo_add_fn fn_name:req is_first:req
agb_arm_func \fn_name agb_arm_func \fn_name
@ -110,14 +93,14 @@ agb_arm_func \fn_name
@ r0 - pointer to the data to be copied (u8 array) @ r0 - pointer to the data to be copied (u8 array)
@ r1 - pointer to the sound buffer (i16 array which will alternate left and right channels, 32-bit aligned) @ r1 - pointer to the sound buffer (i16 array which will alternate left and right channels, 32-bit aligned)
@ r2 - volume to play the sound at @ r2 - volume to play the sound at
@ r3 - the buffer size
@ @
@ The sound buffer must be SOUND_BUFFER_SIZE * 2 in size = 176 * 2 @ The sound buffer must be SOUND_BUFFER_SIZE * 2 in size = 176 * 2
push {{r4-r11}} push {{r4-r11}}
ldr r5, =0x00000FFF ldr r5, =0x00000FFF
ldr r8, =agb_rs__buffer_size mov r8, r3
ldr r8, [r8]
.macro add_stereo_sample sample_reg:req .macro add_stereo_sample sample_reg:req
ldrsh r6, [r0], #2 @ load the current sound sample to r6 ldrsh r6, [r0], #2 @ load the current sound sample to r6
@ -174,6 +157,9 @@ agb_arm_end \fn_name
stereo_add_fn agb_rs__mixer_add_stereo false stereo_add_fn agb_rs__mixer_add_stereo false
stereo_add_fn agb_rs__mixer_add_stereo_first true stereo_add_fn agb_rs__mixer_add_stereo_first true
@ TODO(GI): Might bring this back later
@ stereo_add_fn agb_rs__mixer_add_stereo_first true
agb_arm_func agb_rs__mixer_collapse agb_arm_func agb_rs__mixer_collapse
@ Arguments: @ Arguments:
@ r0 = target buffer (i8) @ r0 = target buffer (i8)

View file

@ -226,11 +226,12 @@ pub struct SoundChannel {
data: &'static [u8], data: &'static [u8],
pos: Num<u32, 8>, pos: Num<u32, 8>,
should_loop: bool, should_loop: bool,
restart_point: Num<u32, 8>,
playback_speed: Num<u32, 8>, playback_speed: Num<u32, 8>,
volume: Num<i16, 4>, // between 0 and 1 volume: Num<i16, 8>, // between 0 and 1
panning: Num<i16, 4>, // between -1 and 1 panning: Num<i16, 8>, // between -1 and 1
is_done: bool, is_done: bool,
is_stereo: bool, is_stereo: bool,
@ -276,6 +277,7 @@ impl SoundChannel {
priority: SoundPriority::Low, priority: SoundPriority::Low,
volume: 1.into(), volume: 1.into(),
is_stereo: false, is_stereo: false,
restart_point: 0.into(),
} }
} }
@ -319,6 +321,7 @@ impl SoundChannel {
priority: SoundPriority::High, priority: SoundPriority::High,
volume: 1.into(), volume: 1.into(),
is_stereo: false, is_stereo: false,
restart_point: 0.into(),
} }
} }
@ -330,6 +333,20 @@ impl SoundChannel {
self self
} }
/// Sets the point at which the sample should restart once it loops. Does nothing
/// unless you also call [`should_loop()`](SoundChannel::should_loop()).
///
/// Useful if your song has an introduction or similar.
#[inline(always)]
pub fn restart_point(&mut self, restart_point: impl Into<Num<u32, 8>>) -> &mut Self {
self.restart_point = restart_point.into();
assert!(
self.restart_point.floor() as usize <= self.data.len(),
"restart point must be shorter than the length of the sample"
);
self
}
/// Sets the speed at which this should channel should be played. Defaults /// Sets the speed at which this should channel should be played. Defaults
/// to 1 with values between 0 and 1 being slower above 1 being faster. /// to 1 with values between 0 and 1 being slower above 1 being faster.
/// ///
@ -349,7 +366,7 @@ impl SoundChannel {
/// Defaults to 0 (meaning equal on left and right) and doesn't affect stereo /// Defaults to 0 (meaning equal on left and right) and doesn't affect stereo
/// sounds. /// sounds.
#[inline(always)] #[inline(always)]
pub fn panning(&mut self, panning: impl Into<Num<i16, 4>>) -> &mut Self { pub fn panning(&mut self, panning: impl Into<Num<i16, 8>>) -> &mut Self {
let panning = panning.into(); let panning = panning.into();
debug_assert!(panning >= Num::new(-1), "panning value must be >= -1"); debug_assert!(panning >= Num::new(-1), "panning value must be >= -1");
@ -364,7 +381,7 @@ impl SoundChannel {
/// ///
/// Must be a value >= 0 and defaults to 1. /// Must be a value >= 0 and defaults to 1.
#[inline(always)] #[inline(always)]
pub fn volume(&mut self, volume: impl Into<Num<i16, 4>>) -> &mut Self { pub fn volume(&mut self, volume: impl Into<Num<i16, 8>>) -> &mut Self {
let volume = volume.into(); let volume = volume.into();
assert!(volume >= Num::new(0), "volume must be >= 0"); assert!(volume >= Num::new(0), "volume must be >= 0");

View file

@ -19,34 +19,35 @@ use crate::{
timer::Timer, timer::Timer,
}; };
macro_rules! add_mono_fn {
($name:ident) => {
fn $name(
sample_data: *const u8,
sample_buffer: *mut i32,
buffer_size: usize,
restart_amount: Num<u32, 8>,
channel_length: usize,
current_pos: Num<u32, 8>,
playback_speed: Num<u32, 8>,
mul_amount: i32,
) -> Num<u32, 8>;
};
}
// Defined in mixer.s // Defined in mixer.s
extern "C" { extern "C" {
fn agb_rs__mixer_add(
sound_data: *const u8,
sound_buffer: *mut Num<i16, 4>,
playback_speed: Num<u32, 8>,
left_amount: Num<i16, 4>,
right_amount: Num<i16, 4>,
);
fn agb_rs__mixer_add_first(
sound_data: *const u8,
sound_buffer: *mut Num<i16, 4>,
playback_speed: Num<u32, 8>,
left_amount: Num<i16, 4>,
right_amount: Num<i16, 4>,
);
fn agb_rs__mixer_add_stereo( fn agb_rs__mixer_add_stereo(
sound_data: *const u8, sound_data: *const u8,
sound_buffer: *mut Num<i16, 4>, sound_buffer: *mut Num<i16, 4>,
volume: Num<i16, 4>, volume: Num<i16, 4>,
buffer_size: usize,
); );
fn agb_rs__mixer_add_stereo_first( fn agb_rs__mixer_add_stereo_first(
sound_data: *const u8, sound_data: *const u8,
sound_buffer: *mut Num<i16, 4>, sound_buffer: *mut Num<i16, 4>,
volume: Num<i16, 4>, volume: Num<i16, 4>,
buffer_size: usize,
); );
fn agb_rs__mixer_collapse( fn agb_rs__mixer_collapse(
@ -54,6 +55,11 @@ extern "C" {
input_buffer: *const Num<i16, 4>, input_buffer: *const Num<i16, 4>,
num_samples: usize, num_samples: usize,
); );
add_mono_fn!(agb_rs__mixer_add_mono_loop_first);
add_mono_fn!(agb_rs__mixer_add_mono_loop);
add_mono_fn!(agb_rs__mixer_add_mono_first);
add_mono_fn!(agb_rs__mixer_add_mono);
} }
/// The main software mixer struct. /// The main software mixer struct.
@ -164,8 +170,6 @@ impl Mixer<'_> {
}) })
}; };
set_asm_buffer_size(frequency);
let mut working_buffer = let mut working_buffer =
Vec::with_capacity_in(frequency.buffer_size() * 2, InternalAllocator); Vec::with_capacity_in(frequency.buffer_size() * 2, InternalAllocator);
working_buffer.resize(frequency.buffer_size() * 2, 0.into()); working_buffer.resize(frequency.buffer_size() * 2, 0.into());
@ -322,16 +326,6 @@ impl Mixer<'_> {
} }
} }
fn set_asm_buffer_size(frequency: Frequency) {
extern "C" {
static mut agb_rs__buffer_size: usize;
}
unsafe {
agb_rs__buffer_size = frequency.buffer_size();
}
}
struct SoundBuffer(Box<[i8], InternalAllocator>); struct SoundBuffer(Box<[i8], InternalAllocator>);
impl SoundBuffer { impl SoundBuffer {
@ -420,91 +414,26 @@ impl MixerBuffer {
working_buffer: &mut [Num<i16, 4>], working_buffer: &mut [Num<i16, 4>],
channels: impl Iterator<Item = &'a mut SoundChannel>, channels: impl Iterator<Item = &'a mut SoundChannel>,
) { ) {
let mut channels = channels let mut channels =
.filter(|channel| !channel.is_done) channels.filter(|channel| !channel.is_done && channel.volume != 0.into());
.filter_map(|channel| {
let playback_speed = if channel.is_stereo {
2.into()
} else {
channel.playback_speed
};
if (channel.pos + playback_speed * self.frequency.buffer_size() as u32).floor() if let Some(channel) = channels.next() {
>= channel.data.len() as u32
{
// TODO: This should probably play what's left rather than skip the last bit
if channel.should_loop {
channel.pos = 0.into();
} else {
channel.is_done = true;
return None;
}
}
Some((channel, playback_speed))
});
if let Some((channel, playback_speed)) = channels.next() {
if channel.volume != 0.into() {
if channel.is_stereo { if channel.is_stereo {
unsafe { self.write_stereo(channel, working_buffer, true);
agb_rs__mixer_add_stereo_first(
channel.data.as_ptr().add(channel.pos.floor() as usize),
working_buffer.as_mut_ptr(),
channel.volume,
);
}
} else { } else {
let right_amount = ((channel.panning + 1) / 2) * channel.volume; self.write_mono(channel, working_buffer, true);
let left_amount = ((-channel.panning + 1) / 2) * channel.volume;
unsafe {
agb_rs__mixer_add_first(
channel.data.as_ptr().add(channel.pos.floor() as usize),
working_buffer.as_mut_ptr(),
playback_speed,
left_amount,
right_amount,
);
}
} }
} else { } else {
working_buffer.fill(0.into()); working_buffer.fill(0.into());
} }
channel.pos += playback_speed * self.frequency.buffer_size() as u32; for channel in channels {
} else {
working_buffer.fill(0.into());
}
for (channel, playback_speed) in channels {
if channel.volume != 0.into() {
if channel.is_stereo { if channel.is_stereo {
unsafe { self.write_stereo(channel, working_buffer, false);
agb_rs__mixer_add_stereo(
channel.data.as_ptr().add(channel.pos.floor() as usize),
working_buffer.as_mut_ptr(),
channel.volume,
);
}
} else { } else {
let right_amount = ((channel.panning + 1) / 2) * channel.volume; self.write_mono(channel, working_buffer, false);
let left_amount = ((-channel.panning + 1) / 2) * channel.volume;
unsafe {
agb_rs__mixer_add(
channel.data.as_ptr().add(channel.pos.floor() as usize),
working_buffer.as_mut_ptr(),
playback_speed,
left_amount,
right_amount,
);
} }
} }
}
channel.pos += playback_speed * self.frequency.buffer_size() as u32;
}
let write_buffer = free(|cs| self.state.borrow(cs).borrow_mut().active_advanced()); let write_buffer = free(|cs| self.state.borrow(cs).borrow_mut().active_advanced());
@ -516,6 +445,107 @@ impl MixerBuffer {
); );
} }
} }
fn write_stereo(
&self,
channel: &mut SoundChannel,
working_buffer: &mut [Num<i16, 4>],
is_first: bool,
) {
if (channel.pos + 2 * self.frequency.buffer_size() as u32).floor()
>= channel.data.len() as u32
{
if channel.should_loop {
channel.pos = channel.restart_point * 2;
} else {
channel.is_done = true;
if is_first {
working_buffer.fill(0.into());
}
return;
}
}
unsafe {
if is_first {
agb_rs__mixer_add_stereo_first(
channel.data.as_ptr().add(channel.pos.floor() as usize),
working_buffer.as_mut_ptr(),
channel.volume.change_base(),
self.frequency.buffer_size(),
);
} else {
agb_rs__mixer_add_stereo(
channel.data.as_ptr().add(channel.pos.floor() as usize),
working_buffer.as_mut_ptr(),
channel.volume.change_base(),
self.frequency.buffer_size(),
);
}
}
channel.pos += 2 * self.frequency.buffer_size() as u32;
}
fn write_mono(
&self,
channel: &mut SoundChannel,
working_buffer: &mut [Num<i16, 4>],
is_first: bool,
) {
let right_amount = ((channel.panning + 1) / 2) * channel.volume;
let left_amount = ((-channel.panning + 1) / 2) * channel.volume;
let right_amount: Num<i16, 4> = right_amount.change_base();
let left_amount: Num<i16, 4> = left_amount.change_base();
let channel_len = Num::<u32, 8>::new(channel.data.len() as u32);
let mut playback_speed = channel.playback_speed;
while playback_speed >= channel_len - channel.restart_point {
playback_speed -= channel_len;
}
// SAFETY: always aligned correctly by construction
let working_buffer_i32: &mut [i32] = unsafe {
core::slice::from_raw_parts_mut(
working_buffer.as_mut_ptr().cast(),
working_buffer.len() / 2,
)
};
let mul_amount =
((left_amount.to_raw() as i32) << 16) | (right_amount.to_raw() as i32 & 0x0000ffff);
macro_rules! call_mono_fn {
($fn_name:ident) => {
channel.pos = unsafe {
$fn_name(
channel.data.as_ptr(),
working_buffer_i32.as_mut_ptr(),
working_buffer_i32.len(),
channel_len - channel.restart_point,
channel.data.len(),
channel.pos,
channel.playback_speed,
mul_amount,
)
}
};
}
match (is_first, channel.should_loop) {
(true, true) => call_mono_fn!(agb_rs__mixer_add_mono_loop_first),
(false, true) => call_mono_fn!(agb_rs__mixer_add_mono_loop),
(true, false) => {
call_mono_fn!(agb_rs__mixer_add_mono_first);
channel.is_done = channel.pos > channel_len;
}
(false, false) => {
call_mono_fn!(agb_rs__mixer_add_mono);
channel.is_done = channel.pos > channel_len;
}
}
}
} }
#[cfg(test)] #[cfg(test)]
@ -590,4 +620,34 @@ mod test {
] ]
); );
} }
#[test_case]
fn mono_add_loop_first_should_work(_: &mut crate::Gba) {
let mut buffer = vec![0i32; 16];
let sample_data: [i8; 9] = [5, 10, 0, 100, -18, 55, 8, -120, 19];
let restart_amount = num!(9.0);
let current_pos = num!(0.0);
let playback_speed = num!(1.0);
let mul_amount = 10;
let result = unsafe {
agb_rs__mixer_add_mono_loop_first(
sample_data.as_ptr().cast(),
buffer.as_mut_ptr(),
buffer.len(),
restart_amount,
sample_data.len(),
current_pos,
playback_speed,
mul_amount,
)
};
assert_eq!(
buffer,
&[50, 100, 0, 1000, -180, 550, 80, -1200, 190, 50, 100, 0, 1000, -180, 550, 80]
);
assert_eq!(result, num!(7.0));
}
} }

View file

@ -78,7 +78,7 @@ unsafe fn transfer_align4_thumb<T: Copy>(mut dst: *mut T, mut src: *const T) {
} }
} }
#[cfg_attr(not(doc), instruction_set(arm::a32))] #[instruction_set(arm::a32)]
#[allow(unused_assignments)] #[allow(unused_assignments)]
unsafe fn transfer_align4_arm<T: Copy>(mut dst: *mut T, mut src: *const T) { unsafe fn transfer_align4_arm<T: Copy>(mut dst: *mut T, mut src: *const T) {
let size = mem::size_of::<T>(); let size = mem::size_of::<T>();
@ -168,14 +168,14 @@ unsafe fn exchange<T>(dst: *mut T, src: *const T) -> T {
} }
} }
#[cfg_attr(not(doc), instruction_set(arm::a32))] #[instruction_set(arm::a32)]
unsafe fn exchange_align4_arm<T>(dst: *mut T, i: u32) -> u32 { unsafe fn exchange_align4_arm<T>(dst: *mut T, i: u32) -> u32 {
let out; let out;
asm!("swp {2}, {1}, [{0}]", in(reg) dst, in(reg) i, lateout(reg) out); asm!("swp {2}, {1}, [{0}]", in(reg) dst, in(reg) i, lateout(reg) out);
out out
} }
#[cfg_attr(not(doc), instruction_set(arm::a32))] #[instruction_set(arm::a32)]
unsafe fn exchange_align1_arm<T>(dst: *mut T, i: u8) -> u8 { unsafe fn exchange_align1_arm<T>(dst: *mut T, i: u8) -> u8 {
let out; let out;
asm!("swpb {2}, {1}, [{0}]", in(reg) dst, in(reg) i, lateout(reg) out); asm!("swpb {2}, {1}, [{0}]", in(reg) dst, in(reg) i, lateout(reg) out);

View file

@ -1,4 +1,4 @@
use agb::fixnum::Num; use agb::fixnum::num;
use agb::rng; use agb::rng;
use agb::sound::mixer::{ChannelId, Mixer, SoundChannel}; use agb::sound::mixer::{ChannelId, Mixer, SoundChannel};
@ -98,8 +98,7 @@ impl<'a> Sfx<'a> {
pub fn slime_boing(&mut self) { pub fn slime_boing(&mut self) {
let mut channel = SoundChannel::new(SLIME_BOING); let mut channel = SoundChannel::new(SLIME_BOING);
let one: Num<i16, 4> = 1.into(); channel.volume(num!(0.25));
channel.volume(one / 4);
self.mixer.play_sound(channel); self.mixer.play_sound(channel);
} }

View file

@ -5,8 +5,10 @@ build: build-roms
build-debug: build-debug:
just _build-debug agb just _build-debug agb
just _build-debug tracker/agb-tracker
build-release: build-release:
just _build-release agb just _build-release agb
just _build-release tracker/agb-tracker
clippy: clippy:
just _all-crates _clippy just _all-crates _clippy
just _clippy tools just _clippy tools
@ -15,19 +17,22 @@ test:
just _test-debug agb just _test-debug agb
just _test-debug agb-fixnum just _test-debug agb-fixnum
just _test-debug agb-hashmap just _test-debug agb-hashmap
just _test-debug tracker/agb-tracker
just _test-debug-arm agb just _test-debug-arm agb
just _test-debug tools just _test-debug tools
test-release: test-release:
just _test-release agb just _test-release agb
just _test-release agb-fixnum just _test-release agb-fixnum
just _test-release tracker/agb-tracker
just _test-release-arm agb just _test-release-arm agb
doctest-agb: doctest-agb:
(cd agb && cargo test --doc -Z doctest-xcompile) (cd agb && cargo test --doc -Z doctest-xcompile)
check-docs: check-docs:
(cd agb && cargo doc --target=thumbv6m-none-eabi --no-deps) (cd agb && cargo doc --target=thumbv4t-none-eabi --no-deps)
(cd tracker/agb-tracker && cargo doc --target=thumbv4t-none-eabi --no-deps)
just _build_docs agb-fixnum just _build_docs agb-fixnum
just _build_docs agb-hashmap just _build_docs agb-hashmap
@ -120,7 +125,7 @@ gbafix *args:
(cd agb-gbafix && cargo run --release -- {{args}}) (cd agb-gbafix && cargo run --release -- {{args}})
_all-crates target: _all-crates target:
for CARGO_PROJECT_FILE in agb-*/Cargo.toml agb/Cargo.toml; do \ for CARGO_PROJECT_FILE in agb-*/Cargo.toml agb/Cargo.toml tracker/agb-*/Cargo.toml; do \
PROJECT_DIR=$(dirname "$CARGO_PROJECT_FILE"); \ PROJECT_DIR=$(dirname "$CARGO_PROJECT_FILE"); \
just "{{target}}" "$PROJECT_DIR" || exit $?; \ just "{{target}}" "$PROJECT_DIR" || exit $?; \
done done

View file

@ -3,7 +3,7 @@ use dependency_graph::DependencyGraph;
use std::cell::RefCell; use std::cell::RefCell;
use std::collections::HashMap; use std::collections::HashMap;
use std::fs; use std::fs;
use std::path::Path; use std::path::{Path, PathBuf};
use std::process::Command; use std::process::Command;
use toml_edit::Document; use toml_edit::Document;
@ -22,6 +22,7 @@ pub enum Error {
struct Package { struct Package {
name: String, name: String,
dependencies: Vec<String>, dependencies: Vec<String>,
directory: PathBuf,
} }
impl dependency_graph::Node for Package { impl dependency_graph::Node for Package {
@ -55,7 +56,11 @@ pub fn publish(matches: &ArgMatches) -> Result<(), Error> {
let mut in_progress: HashMap<_, RefCell<std::process::Child>> = HashMap::new(); let mut in_progress: HashMap<_, RefCell<std::process::Child>> = HashMap::new();
let dependencies = build_dependency_graph(&root_directory)?; let mut dependencies = build_dependency_graph(&root_directory)?;
let mut tracker_dependencies = build_dependency_graph(&root_directory.join("tracker"))?;
dependencies.append(&mut tracker_dependencies);
let graph = DependencyGraph::from(&dependencies[..]); let graph = DependencyGraph::from(&dependencies[..]);
for package in graph { for package in graph {
@ -76,7 +81,7 @@ pub fn publish(matches: &ArgMatches) -> Result<(), Error> {
let publish_cmd = Command::new("cargo") let publish_cmd = Command::new("cargo")
.arg("publish") .arg("publish")
.args(&dry_run) .args(&dry_run)
.current_dir(root_directory.join(&package.name)) .current_dir(&package.directory)
.spawn() .spawn()
.map_err(|_| Error::PublishCrate)?; .map_err(|_| Error::PublishCrate)?;
@ -117,6 +122,7 @@ fn build_dependency_graph(root: &Path) -> Result<Vec<Package>, Error> {
packages.push(Package { packages.push(Package {
name: dir.file_name().to_string_lossy().to_string(), name: dir.file_name().to_string_lossy().to_string(),
dependencies: get_agb_dependencies(&crate_path)?, dependencies: get_agb_dependencies(&crate_path)?,
directory: crate_path,
}); });
} }

View file

@ -108,6 +108,7 @@ fn update_to_version(
&[ &[
"agb-*/Cargo.toml", "agb-*/Cargo.toml",
"agb/Cargo.toml", "agb/Cargo.toml",
"tracker/agb-*/Cargo.toml",
"examples/*/Cargo.toml", "examples/*/Cargo.toml",
"book/games/*/Cargo.toml", "book/games/*/Cargo.toml",
"template/Cargo.toml", "template/Cargo.toml",

1
tracker/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
agb-*/Cargo.lock

View file

@ -0,0 +1,18 @@
[package]
name = "agb_tracker_interop"
version = "0.16.0"
authors = ["Gwilym Inzani <gw@ilym.me>"]
edition = "2021"
license = "MPL-2.0"
description = "Library for interop between tracker plugins and agb itself. Designed for use with the agb library for the Game Boy Advance."
repository = "https://github.com/agbrs/agb"
[features]
default = ["quote"]
quote = ["dep:quote", "dep:proc-macro2", "std"]
std = []
[dependencies]
quote = { version = "1", optional = true }
proc-macro2 = { version = "1", optional = true }
agb_fixnum = { version = "0.16.0", path = "../../agb-fixnum" }

View file

@ -0,0 +1,229 @@
#![cfg_attr(not(feature = "std"), no_std)]
use agb_fixnum::Num;
#[derive(Debug)]
pub struct Track<'a> {
pub samples: &'a [Sample<'a>],
pub pattern_data: &'a [PatternSlot],
pub patterns: &'a [Pattern],
pub patterns_to_play: &'a [usize],
pub num_channels: usize,
pub frames_per_tick: Num<u32, 8>,
pub ticks_per_step: u32,
}
#[derive(Debug)]
pub struct Sample<'a> {
pub data: &'a [u8],
pub should_loop: bool,
pub restart_point: u32,
pub volume: Num<i16, 8>,
}
#[derive(Debug)]
pub struct Pattern {
pub length: usize,
pub start_position: usize,
}
#[derive(Debug)]
pub struct PatternSlot {
pub speed: Num<u16, 8>,
pub sample: u16,
pub effect1: PatternEffect,
pub effect2: PatternEffect,
}
#[derive(Debug, Default)]
pub enum PatternEffect {
/// Don't play an effect
#[default]
None,
/// Stops playing the current note
Stop,
/// Plays an arpeggiation of three notes in one row, cycling betwen the current note, current note + first speed, current note + second speed
Arpeggio(Num<u16, 8>, Num<u16, 8>),
Panning(Num<i16, 4>),
Volume(Num<i16, 8>),
VolumeSlide(Num<i16, 8>),
FineVolumeSlide(Num<i16, 8>),
NoteCut(u32),
Portamento(Num<u16, 8>),
/// Slide each tick the first amount to at most the second amount
TonePortamento(Num<u16, 8>, Num<u16, 8>),
}
#[cfg(feature = "quote")]
impl<'a> quote::ToTokens for Track<'a> {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
use quote::{quote, TokenStreamExt};
let Track {
samples,
pattern_data,
patterns,
frames_per_tick,
num_channels,
patterns_to_play,
ticks_per_step,
} = self;
let frames_per_tick = frames_per_tick.to_raw();
tokens.append_all(quote! {
{
const SAMPLES: &[agb_tracker::__private::agb_tracker_interop::Sample<'static>] = &[#(#samples),*];
const PATTERN_DATA: &[agb_tracker::__private::agb_tracker_interop::PatternSlot] = &[#(#pattern_data),*];
const PATTERNS: &[agb_tracker::__private::agb_tracker_interop::Pattern] = &[#(#patterns),*];
const PATTERNS_TO_PLAY: &[usize] = &[#(#patterns_to_play),*];
agb_tracker::Track {
samples: SAMPLES,
pattern_data: PATTERN_DATA,
patterns: PATTERNS,
patterns_to_play: PATTERNS_TO_PLAY,
frames_per_tick: agb_tracker::__private::Num::from_raw(#frames_per_tick),
num_channels: #num_channels,
ticks_per_step: #ticks_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 Sample {
data,
should_loop,
restart_point,
volume,
} = self;
let samples = ByteString(data);
let volume = volume.to_raw();
tokens.append_all(quote! {
{
#[repr(align(4))]
struct AlignmentWrapper<const N: usize>([u8; N]);
const SAMPLE_DATA: &[u8] = &AlignmentWrapper(*#samples).0;
agb_tracker::__private::agb_tracker_interop::Sample {
data: SAMPLE_DATA,
should_loop: #should_loop,
restart_point: #restart_point,
volume: agb_tracker::__private::Num::from_raw(#volume),
}
}
});
}
}
#[cfg(feature = "quote")]
impl quote::ToTokens for PatternSlot {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
use quote::{quote, TokenStreamExt};
let PatternSlot {
speed,
sample,
effect1,
effect2,
} = &self;
let speed = speed.to_raw();
tokens.append_all(quote! {
agb_tracker::__private::agb_tracker_interop::PatternSlot {
speed: agb_tracker::__private::Num::from_raw(#speed),
sample: #sample,
effect1: #effect1,
effect2: #effect2,
}
});
}
}
#[cfg(feature = "quote")]
impl quote::ToTokens for Pattern {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
use quote::{quote, TokenStreamExt};
let Pattern {
length,
start_position,
} = self;
tokens.append_all(quote! {
agb_tracker::__private::agb_tracker_interop::Pattern {
length: #length,
start_position: #start_position,
}
})
}
}
#[cfg(feature = "quote")]
impl quote::ToTokens for PatternEffect {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
use quote::{quote, TokenStreamExt};
let type_bit = match self {
PatternEffect::None => quote! { None },
PatternEffect::Stop => quote! { Stop },
PatternEffect::Arpeggio(first, second) => {
let first = first.to_raw();
let second = second.to_raw();
quote! { Arpeggio(agb_tracker::__private::Num::from_raw(#first), agb_tracker::__private::Num::from_raw(#second)) }
}
PatternEffect::Panning(panning) => {
let panning = panning.to_raw();
quote! { Panning(agb_tracker::__private::Num::from_raw(#panning))}
}
PatternEffect::Volume(volume) => {
let volume = volume.to_raw();
quote! { Volume(agb_tracker::__private::Num::from_raw(#volume))}
}
PatternEffect::VolumeSlide(amount) => {
let amount = amount.to_raw();
quote! { VolumeSlide(agb_tracker::__private::Num::from_raw(#amount))}
}
PatternEffect::FineVolumeSlide(amount) => {
let amount = amount.to_raw();
quote! { FineVolumeSlide(agb_tracker::__private::Num::from_raw(#amount))}
}
PatternEffect::NoteCut(wait) => quote! { NoteCut(#wait) },
PatternEffect::Portamento(amount) => {
let amount = amount.to_raw();
quote! { Portamento(agb_tracker::__private::Num::from_raw(#amount))}
}
PatternEffect::TonePortamento(amount, target) => {
let amount = amount.to_raw();
let target = target.to_raw();
quote! { TonePortamento(agb_tracker::__private::Num::from_raw(#amount), agb_tracker::__private::Num::from_raw(#target))}
}
};
tokens.append_all(quote! {
agb_tracker::__private::agb_tracker_interop::PatternEffect::#type_bit
});
}
}

View file

@ -0,0 +1,14 @@
[unstable]
build-std = ["core", "alloc"]
build-std-features = ["compiler-builtins-mem"]
[build]
target = "thumbv4t-none-eabi"
[target.thumbv4t-none-eabi]
rustflags = ["-Clink-arg=-Tgba.ld", "-Ctarget-cpu=arm7tdmi"]
runner = "mgba-test-runner"
[target.armv4t-none-eabi]
rustflags = ["-Clink-arg=-Tgba.ld", "-Ctarget-cpu=arm7tdmi"]
runner = "mgba-test-runner"

View file

@ -0,0 +1,29 @@
[package]
name = "agb_tracker"
version = "0.16.0"
authors = ["Gwilym Inzani <gw@ilym.me>"]
edition = "2021"
license = "MPL-2.0"
description = "Library for playing tracker music. Designed for use with the agb library for the Game Boy Advance."
repository = "https://github.com/agbrs/agb"
[features]
default = ["xm"]
xm = ["dep:agb_xm"]
[dependencies]
agb_xm = { version = "0.16.0", path = "../agb-xm", optional = true }
agb = { version = "0.16.0", path = "../../agb" }
agb_tracker_interop = { version = "0.16.0", path = "../agb-tracker-interop", default-features = false }
[profile.dev]
opt-level = 3
debug = true
[profile.release]
opt-level = 3
lto = "fat"
debug = true
[package.metadata.docs.rs]
default-target = "thumbv4t-none-eabi"

Binary file not shown.

View file

@ -0,0 +1,26 @@
#![no_std]
#![no_main]
use agb::sound::mixer::Frequency;
use agb::Gba;
use agb_tracker::{include_xm, Track, Tracker};
// Found on: https://modarchive.org/index.php?request=view_by_moduleid&query=36662
const 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::Hz18157);
mixer.enable();
let mut tracker = Tracker::new(&DB_TOFFE);
loop {
tracker.step(&mut mixer);
mixer.frame();
vblank_provider.wait_for_vblank();
}
}

Binary file not shown.

View file

@ -0,0 +1,56 @@
#![no_std]
#![no_main]
use agb::sound::mixer::Frequency;
use agb::Gba;
use agb_tracker::{include_xm, Track, Tracker};
// Found on: https://modarchive.org/index.php?request=view_by_moduleid&query=36662
const DB_TOFFE: Track = include_xm!("examples/db_toffe.xm");
#[agb::entry]
fn main(mut gba: Gba) -> ! {
let vblank_provider = agb::interrupt::VBlank::get();
let timer_controller = gba.timers.timers();
let mut timer = timer_controller.timer2;
let mut timer2 = timer_controller.timer3;
timer.set_enabled(true);
timer2.set_cascade(true).set_enabled(true);
let mut mixer = gba.mixer.mixer(Frequency::Hz18157);
mixer.enable();
let mut tracker = Tracker::new(&DB_TOFFE);
loop {
let before_mixing_cycles_high = timer2.value();
let before_mixing_cycles_low = timer.value();
tracker.step(&mut mixer);
let after_step_cycles_high = timer2.value();
let after_step_cycles_low = timer.value();
mixer.frame();
let after_mixing_cycles_low = timer.value();
let after_mixing_cycles_high = timer2.value();
vblank_provider.wait_for_vblank();
let before_mixing_cycles =
((before_mixing_cycles_high as u32) << 16) + before_mixing_cycles_low as u32;
let after_mixing_cycles =
((after_mixing_cycles_high as u32) << 16) + after_mixing_cycles_low as u32;
let after_step_cycles =
((after_step_cycles_high as u32) << 16) + after_step_cycles_low as u32;
let step_cycles = after_step_cycles - before_mixing_cycles;
let mixing_cycles = after_mixing_cycles - before_mixing_cycles;
let total_cycles = after_mixing_cycles.wrapping_sub(before_mixing_cycles);
agb::println!(
"step = {step_cycles}, mixing = {mixing_cycles}, total = {total_cycles} cycles"
);
}
}

115
tracker/agb-tracker/gba.ld Normal file
View file

@ -0,0 +1,115 @@
OUTPUT_FORMAT("elf32-littlearm", "elf32-bigarm", "elf32-littlearm")
OUTPUT_ARCH(arm)
ENTRY(__start)
EXTERN(__RUST_INTERRUPT_HANDLER)
EXTERN(__agbabi_memset)
EXTERN(__agbabi_memcpy)
MEMORY {
ewram (w!x) : ORIGIN = 0x02000000, LENGTH = 256K
iwram (w!x) : ORIGIN = 0x03000000, LENGTH = 32K
rom (rx) : ORIGIN = 0x08000000, LENGTH = 32M
}
__text_start = ORIGIN(rom);
SECTIONS {
. = __text_start;
.text : {
KEEP(*(.crt0));
*(.crt0 .crt0*);
*(.text .text*);
. = ALIGN(4);
} > rom
__text_end = .;
.rodata : {
*(.rodata .rodata.*);
. = ALIGN(4);
} > rom
__iwram_rom_start = .;
.iwram : {
__iwram_data_start = ABSOLUTE(.);
*(.iwram .iwram.*);
. = ALIGN(4);
*(.text_iwram .text_iwram.*);
. = ALIGN(4);
__iwram_data_end = ABSOLUTE(.);
} > iwram AT>rom
. = __iwram_rom_start + (__iwram_data_end - __iwram_data_start);
__ewram_rom_start = .;
.ewram : {
__ewram_data_start = ABSOLUTE(.);
*(.ewram .ewram.*);
. = ALIGN(4);
*(.data .data.*);
. = ALIGN(4);
__ewram_data_end = ABSOLUTE(.);
} > ewram AT>rom
.bss : {
*(.bss .bss.*);
. = ALIGN(4);
__iwram_end = ABSOLUTE(.);
} > iwram
__iwram_rom_length_bytes = __iwram_data_end - __iwram_data_start;
__iwram_rom_length_halfwords = (__iwram_rom_length_bytes + 1) / 2;
__ewram_rom_length_bytes = __ewram_data_end - __ewram_data_start;
__ewram_rom_length_halfwords = (__ewram_rom_length_bytes + 1) / 2;
.shstrtab : {
*(.shstrtab)
}
/* debugging sections */
/* Stabs */
.stab 0 : { *(.stab) }
.stabstr 0 : { *(.stabstr) }
.stab.excl 0 : { *(.stab.excl) }
.stab.exclstr 0 : { *(.stab.exclstr) }
.stab.index 0 : { *(.stab.index) }
.stab.indexstr 0 : { *(.stab.indexstr) }
.comment 0 : { *(.comment) }
/* DWARF 1 */
.debug 0 : { *(.debug) }
.line 0 : { *(.line) }
/* GNU DWARF 1 extensions */
.debug_srcinfo 0 : { *(.debug_srcinfo) }
.debug_sfnames 0 : { *(.debug_sfnames) }
/* DWARF 1.1 and DWARF 2 */
.debug_aranges 0 : { *(.debug_aranges) }
.debug_pubnames 0 : { *(.debug_pubnames) }
/* DWARF 2 */
.debug_info 0 : { *(.debug_info) }
.debug_abbrev 0 : { *(.debug_abbrev) }
.debug_line 0 : { *(.debug_line) }
.debug_frame 0 : { *(.debug_frame) }
.debug_str 0 : { *(.debug_str) }
.debug_loc 0 : { *(.debug_loc) }
.debug_macinfo 0 : { *(.debug_macinfo) }
/* SGI/MIPS DWARF 2 extensions */
.debug_weaknames 0 : { *(.debug_weaknames) }
.debug_funcnames 0 : { *(.debug_funcnames) }
.debug_typenames 0 : { *(.debug_typenames) }
.debug_varnames 0 : { *(.debug_varnames) }
.debug_ranges 0 : { *(.debug_ranges) }
/* discard anything not already mentioned */
/DISCARD/ : { *(*) }
}

View file

@ -0,0 +1,3 @@
[toolchain]
channel = "nightly"
components = ["rust-src", "clippy", "rustfmt"]

View file

@ -0,0 +1,307 @@
#![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};
//!
//! const 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::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.
extern crate alloc;
use agb_tracker_interop::{PatternEffect, Sample};
use alloc::vec::Vec;
use agb::{
fixnum::Num,
sound::mixer::{ChannelId, Mixer, SoundChannel},
};
/// 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;
#[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 Tracker {
track: &'static Track<'static>,
channels: Vec<TrackerChannel>,
frame: Num<u32, 8>,
tick: u32,
first: bool,
current_row: usize,
current_pattern: usize,
}
struct TrackerChannel {
channel_id: Option<ChannelId>,
base_speed: Num<u32, 8>,
volume: Num<i32, 8>,
}
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(),
});
Self {
track,
channels,
frame: 0.into(),
first: true,
tick: 0,
current_row: 0,
current_pattern: 0,
}
}
/// 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 Mixer) {
if !self.increment_frame() {
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 (channel, pattern_slot) in self.channels.iter_mut().zip(pattern_slots) {
if pattern_slot.sample != 0 && self.tick == 0 {
let sample = &self.track.samples[pattern_slot.sample as usize - 1];
channel.play_sound(mixer, sample);
}
if self.tick == 0 {
channel.set_speed(mixer, pattern_slot.speed.change_base());
}
channel.apply_effect(mixer, &pattern_slot.effect1, self.tick);
channel.apply_effect(mixer, &pattern_slot.effect2, self.tick);
}
self.increment_step();
}
fn increment_frame(&mut self) -> bool {
if self.first {
self.first = false;
return true;
}
self.frame += 1;
if self.frame >= self.track.frames_per_tick {
self.tick += 1;
self.frame -= self.track.frames_per_tick;
if self.tick == self.track.ticks_per_step {
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 = 0;
}
}
self.tick = 0;
}
true
} else {
false
}
}
fn increment_step(&mut self) {}
}
impl TrackerChannel {
fn play_sound(&mut self, mixer: &mut Mixer<'_>, sample: &Sample<'static>) {
if let Some(channel) = self
.channel_id
.take()
.and_then(|channel_id| mixer.channel(&channel_id))
{
channel.stop();
}
let mut new_channel = SoundChannel::new(sample.data);
new_channel.volume(sample.volume.change_base());
if sample.should_loop {
new_channel
.should_loop()
.restart_point(sample.restart_point);
}
self.channel_id = mixer.play_sound(new_channel);
self.volume = sample.volume.change_base();
}
fn set_speed(&mut self, mixer: &mut Mixer<'_>, speed: Num<u32, 8>) {
if let Some(channel) = self
.channel_id
.as_ref()
.and_then(|channel_id| mixer.channel(channel_id))
{
if speed != 0.into() {
self.base_speed = speed;
}
channel.playback(self.base_speed);
}
}
fn apply_effect(&mut self, mixer: &mut Mixer<'_>, effect: &PatternEffect, tick: u32) {
if let Some(channel) = self
.channel_id
.as_ref()
.and_then(|channel_id| mixer.channel(channel_id))
{
match effect {
PatternEffect::None => {}
PatternEffect::Stop => {
channel.stop();
}
PatternEffect::Arpeggio(first, second) => {
match tick % 3 {
0 => channel.playback(self.base_speed),
1 => channel.playback(first.change_base()),
2 => channel.playback(second.change_base()),
_ => unreachable!(),
};
}
PatternEffect::Panning(panning) => {
channel.panning(panning.change_base());
}
PatternEffect::Volume(volume) => {
channel.volume(volume.change_base());
self.volume = volume.change_base();
}
PatternEffect::VolumeSlide(amount) => {
if tick != 0 {
self.volume = (self.volume + amount.change_base()).max(0.into());
channel.volume(self.volume.try_change_base().unwrap());
}
}
PatternEffect::FineVolumeSlide(amount) => {
if tick == 0 {
self.volume = (self.volume + amount.change_base()).max(0.into());
channel.volume(self.volume.try_change_base().unwrap());
}
}
PatternEffect::NoteCut(wait) => {
if tick == *wait {
channel.volume(0);
self.volume = 0.into();
}
}
PatternEffect::Portamento(amount) => {
if tick != 0 {
self.base_speed *= amount.change_base();
channel.playback(self.base_speed);
}
}
PatternEffect::TonePortamento(amount, target) => {
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());
}
}
}
}
}
}
#[cfg(test)]
#[agb::entry]
fn main(gba: agb::Gba) -> ! {
loop {}
}

View file

@ -0,0 +1,19 @@
[package]
name = "agb_xm_core"
version = "0.16.0"
authors = ["Gwilym Inzani <gw@ilym.me>"]
edition = "2021"
license = "MPL-2.0"
description = "Library for converting XM tracker files for use with agb-tracker on the Game Boy Advance. You shouldn't use this package directly"
repository = "https://github.com/agbrs/agb"
[dependencies]
proc-macro-error = "1"
proc-macro2 = "1"
quote = "1"
syn = "2"
agb_tracker_interop = { version = "0.16.0", path = "../agb-tracker-interop" }
agb_fixnum = { version = "0.16.0", path = "../../agb-fixnum" }
xmrs = "0.3"

View file

@ -0,0 +1,437 @@
use std::{collections::HashMap, error::Error, fs, path::Path};
use agb_tracker_interop::PatternEffect;
use proc_macro2::TokenStream;
use proc_macro_error::abort;
use quote::quote;
use syn::LitStr;
use agb_fixnum::Num;
use xmrs::{prelude::*, xm::xmmodule::XmModule};
pub fn agb_xm_core(args: TokenStream) -> TokenStream {
let input = match syn::parse::<LitStr>(args.into()) {
Ok(input) => input,
Err(err) => return err.to_compile_error(),
};
let filename = input.value();
let root = std::env::var("CARGO_MANIFEST_DIR").expect("Failed to get cargo manifest dir");
let path = Path::new(&root).join(&*filename);
let include_path = path.to_string_lossy();
let module = match load_module_from_file(&path) {
Ok(track) => track,
Err(e) => abort!(input, e),
};
let parsed = parse_module(&module);
quote! {
{
const _: &[u8] = include_bytes!(#include_path);
#parsed
}
}
}
pub fn load_module_from_file(xm_path: &Path) -> Result<Module, Box<dyn Error>> {
let file_content = fs::read(xm_path)?;
Ok(XmModule::load(&file_content)?.to_module())
}
pub fn parse_module(module: &Module) -> TokenStream {
let instruments = &module.instrument;
let mut instruments_map = HashMap::new();
struct SampleData {
data: Vec<u8>,
should_loop: bool,
fine_tune: f64,
relative_note: i8,
restart_point: u32,
volume: Num<i16, 8>,
}
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 should_loop = !matches!(sample.flags, LoopType::No);
let fine_tune = sample.finetune as f64;
let relative_note = sample.relative_note;
let restart_point = sample.loop_start;
let sample_len = if sample.loop_length > 0 {
(sample.loop_length + sample.loop_start) as usize
} else {
usize::MAX
};
let volume = Num::from_raw((sample.volume * (1 << 8) as f32) as i16);
let sample = match &sample.data {
SampleDataType::Depth8(depth8) => depth8
.iter()
.map(|value| *value as u8)
.take(sample_len)
.collect::<Vec<_>>(),
SampleDataType::Depth16(depth16) => depth16
.iter()
.map(|sample| (sample >> 8) as i8 as u8)
.take(sample_len)
.collect::<Vec<_>>(),
};
instruments_map.insert((instrument_index, sample_index), samples.len());
samples.push(SampleData {
data: sample,
should_loop,
fine_tune,
relative_note,
restart_point,
volume,
});
}
}
let mut patterns = vec![];
let mut pattern_data = vec![];
for pattern in &module.pattern {
let start_pos = pattern_data.len();
let mut effect_parameters: [u8; 255] = [0; u8::MAX as usize];
let mut tone_portamento_directions = vec![0.0; module.get_num_channels()];
let mut note_and_sample = vec![None; module.get_num_channels()];
for row in pattern.iter() {
for (i, slot) in row.iter().enumerate() {
let channel_number = i % module.get_num_channels();
let sample = if slot.instrument == 0 {
0
} else {
let instrument_index = (slot.instrument - 1) as usize;
if let InstrumentType::Default(ref instrument) =
module.instrument[instrument_index].instr_type
{
let sample_slot = instrument.sample_for_note[slot.note as usize] as usize;
instruments_map
.get(&(instrument_index, sample_slot))
.map(|sample_idx| sample_idx + 1)
.unwrap_or(0)
} else {
0
}
};
let mut effect1 = PatternEffect::None;
let previous_note_and_sample = note_and_sample[channel_number];
let maybe_note_and_sample = if matches!(slot.note, Note::KeyOff) {
effect1 = PatternEffect::Stop;
note_and_sample[channel_number] = None;
&None
} else if !matches!(slot.note, Note::None) {
if sample != 0 {
note_and_sample[channel_number] = Some((slot.note, &samples[sample - 1]));
} else if let Some((note, _)) = &mut note_and_sample[channel_number] {
*note = slot.note;
}
&note_and_sample[channel_number]
} else {
&note_and_sample[channel_number]
};
if matches!(effect1, PatternEffect::None) {
effect1 = match slot.volume {
0x10..=0x50 => PatternEffect::Volume(
(Num::new((slot.volume - 0x10) as i16) / 64)
* maybe_note_and_sample
.map(|note_and_sample| note_and_sample.1.volume)
.unwrap_or(1.into()),
),
0x60..=0x6F => {
PatternEffect::VolumeSlide(-Num::new((slot.volume - 0x60) as i16) / 64)
}
0x70..=0x7F => {
PatternEffect::VolumeSlide(Num::new((slot.volume - 0x70) as i16) / 64)
}
0x80..=0x8F => PatternEffect::FineVolumeSlide(
-Num::new((slot.volume - 0x80) as i16) / 64,
),
0x90..=0x9F => PatternEffect::FineVolumeSlide(
Num::new((slot.volume - 0x90) as i16) / 64,
),
0xC0..=0xCF => PatternEffect::Panning(
Num::new(slot.volume as i16 - (0xC0 + (0xCF - 0xC0) / 2)) / 8,
),
_ => PatternEffect::None,
};
}
let effect_parameter = if slot.effect_parameter != 0 {
effect_parameters[slot.effect_type as usize] = slot.effect_parameter;
slot.effect_parameter
} else {
effect_parameters[slot.effect_type as usize]
};
let effect2 = match slot.effect_type {
0x0 => {
if slot.effect_parameter == 0 {
PatternEffect::None
} else if let Some((note, sample)) = maybe_note_and_sample {
let first_arpeggio = slot.effect_parameter >> 4;
let second_arpeggio = slot.effect_parameter & 0xF;
let first_arpeggio_speed = note_to_speed(
*note,
sample.fine_tune,
sample.relative_note + first_arpeggio as i8,
module.frequency_type,
);
let second_arpeggio_speed = note_to_speed(
*note,
sample.fine_tune,
sample.relative_note + second_arpeggio as i8,
module.frequency_type,
);
PatternEffect::Arpeggio(
first_arpeggio_speed
.try_change_base()
.expect("Arpeggio size too large"),
second_arpeggio_speed
.try_change_base()
.expect("Arpeggio size too large"),
)
} else {
PatternEffect::None
}
}
0x1 => {
let c4_speed = note_to_speed(Note::C4, 0.0, 0, module.frequency_type);
let speed = note_to_speed(
Note::C4,
effect_parameter as f64,
0,
module.frequency_type,
);
let portamento_amount = speed / c4_speed;
PatternEffect::Portamento(portamento_amount.try_change_base().unwrap())
}
0x2 => {
let c4_speed = note_to_speed(Note::C4, 0.0, 0, module.frequency_type);
let speed = note_to_speed(
Note::C4,
-(effect_parameter as f64),
0,
module.frequency_type,
);
let portamento_amount = speed / c4_speed;
PatternEffect::Portamento(portamento_amount.try_change_base().unwrap())
}
0x3 => {
if let (Some((note, sample)), Some((prev_note, _))) =
(maybe_note_and_sample, previous_note_and_sample)
{
let target_speed = note_to_speed(
*note,
sample.fine_tune,
sample.relative_note,
module.frequency_type,
);
let direction = match (prev_note as usize).cmp(&(*note as usize)) {
std::cmp::Ordering::Less => 1.0,
std::cmp::Ordering::Equal => {
tone_portamento_directions[channel_number]
}
std::cmp::Ordering::Greater => -1.0,
};
tone_portamento_directions[channel_number] = direction;
let c4_speed = note_to_speed(Note::C4, 0.0, 0, module.frequency_type);
let speed = note_to_speed(
Note::C4,
effect_parameter as f64 * direction,
0,
module.frequency_type,
);
let portamento_amount = speed / c4_speed;
PatternEffect::TonePortamento(
portamento_amount.try_change_base().unwrap(),
target_speed.try_change_base().unwrap(),
)
} else {
PatternEffect::None
}
}
0x8 => {
PatternEffect::Panning(Num::new(slot.effect_parameter as i16 - 128) / 128)
}
0xA => {
let first = effect_parameter >> 4;
let second = effect_parameter & 0xF;
if first == 0 {
PatternEffect::VolumeSlide(-Num::new(second as i16) / 16)
} else {
PatternEffect::VolumeSlide(Num::new(first as i16) / 16)
}
}
0xC => {
if let Some((_, sample)) = maybe_note_and_sample {
PatternEffect::Volume(
(Num::new(slot.effect_parameter as i16) / 255) * sample.volume,
)
} else {
PatternEffect::None
}
}
0xE => match slot.effect_parameter >> 4 {
0xA => PatternEffect::FineVolumeSlide(
Num::new((slot.effect_parameter & 0xf) as i16) / 64,
),
0xB => PatternEffect::FineVolumeSlide(
-Num::new((slot.effect_parameter & 0xf) as i16) / 64,
),
0xC => PatternEffect::NoteCut((slot.effect_parameter & 0xf).into()),
_ => PatternEffect::None,
},
_ => PatternEffect::None,
};
if sample == 0 {
pattern_data.push(agb_tracker_interop::PatternSlot {
speed: 0.into(),
sample: 0,
effect1,
effect2,
});
} else {
let sample_played = &samples[sample - 1];
let speed = note_to_speed(
slot.note,
sample_played.fine_tune,
sample_played.relative_note,
module.frequency_type,
);
pattern_data.push(agb_tracker_interop::PatternSlot {
speed: speed.try_change_base().unwrap(),
sample: sample as u16,
effect1,
effect2,
});
}
}
}
patterns.push(agb_tracker_interop::Pattern {
length: pattern.len(),
start_position: start_pos,
});
}
let samples: Vec<_> = samples
.iter()
.map(|sample| agb_tracker_interop::Sample {
data: &sample.data,
should_loop: sample.should_loop,
restart_point: sample.restart_point,
volume: sample.volume,
})
.collect();
let patterns_to_play = module
.pattern_order
.iter()
.map(|order| *order as usize)
.collect::<Vec<_>>();
// Number 150 here deduced experimentally
let frames_per_tick = Num::<u32, 8>::new(150) / module.default_bpm as u32;
let ticks_per_step = module.default_tempo;
let interop = agb_tracker_interop::Track {
samples: &samples,
pattern_data: &pattern_data,
patterns: &patterns,
num_channels: module.get_num_channels(),
patterns_to_play: &patterns_to_play,
frames_per_tick,
ticks_per_step: ticks_per_step.into(),
};
quote!(#interop)
}
fn note_to_speed(
note: Note,
fine_tune: f64,
relative_note: i8,
frequency_type: FrequencyType,
) -> Num<u32, 8> {
let frequency = match frequency_type {
FrequencyType::LinearFrequencies => {
note_to_frequency_linear(note, fine_tune, relative_note)
}
FrequencyType::AmigaFrequencies => note_to_frequency_amega(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)
}
fn note_to_frequency_linear(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 * 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_frequency_amega(note: Note, fine_tune: f64, relative_note: i8) -> f64 {
let note = (note as usize)
.checked_add_signed(relative_note as isize)
.expect("Note gone negative");
let pos = (note % 12) * 8 + (fine_tune / 16.0) as usize;
let frac = (fine_tune / 16.0) - (fine_tune / 16.0).floor();
let period = ((AMEGA_FREQUENCIES[pos] as f64 * (1.0 - frac))
+ AMEGA_FREQUENCIES[pos + 1] as f64 * frac)
* 32.0 // docs say 16 here, but for some reason I need 32 :/
/ (1 << ((note as i64) / 12)) as f64;
8363.0 * 1712.0 / period
}
const AMEGA_FREQUENCIES: &[u32] = &[
907, 900, 894, 887, 881, 875, 868, 862, 856, 850, 844, 838, 832, 826, 820, 814, 808, 802, 796,
791, 785, 779, 774, 768, 762, 757, 752, 746, 741, 736, 730, 725, 720, 715, 709, 704, 699, 694,
689, 684, 678, 675, 670, 665, 660, 655, 651, 646, 640, 636, 632, 628, 623, 619, 614, 610, 604,
601, 597, 592, 588, 584, 580, 575, 570, 567, 563, 559, 555, 551, 547, 543, 538, 535, 532, 528,
524, 520, 516, 513, 508, 505, 502, 498, 494, 491, 487, 484, 480, 477, 474, 470, 467, 463, 460,
457,
];

16
tracker/agb-xm/Cargo.toml Normal file
View file

@ -0,0 +1,16 @@
[package]
name = "agb_xm"
version = "0.16.0"
authors = ["Gwilym Kuiper <gw@ilym.me>"]
edition = "2021"
license = "MPL-2.0"
description = "Library for converting XM tracker files for use with agb-tracker on the Game Boy Advance. You shouldn't use this package directly"
repository = "https://github.com/agbrs/agb"
[lib]
proc-macro = true
[dependencies]
agb_xm_core = { version = "0.16.0", path = "../agb-xm-core" }
proc-macro-error = "1"
proc-macro2 = "1"

View file

@ -0,0 +1,8 @@
use proc_macro::TokenStream;
use proc_macro_error::proc_macro_error;
#[proc_macro_error]
#[proc_macro]
pub fn include_xm(args: TokenStream) -> TokenStream {
agb_xm_core::agb_xm_core(args.into()).into()
}