mirror of
https://github.com/italicsjenga/agb.git
synced 2024-12-23 08:11:33 +11:00
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:
commit
bddb77f5c9
13
CHANGELOG.md
13
CHANGELOG.md
|
@ -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
|
||||||
|
|
48
README.md
48
README.md
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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");
|
||||||
|
|
|
@ -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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
9
justfile
9
justfile
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
1
tracker/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
agb-*/Cargo.lock
|
18
tracker/agb-tracker-interop/Cargo.toml
Normal file
18
tracker/agb-tracker-interop/Cargo.toml
Normal 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" }
|
229
tracker/agb-tracker-interop/src/lib.rs
Normal file
229
tracker/agb-tracker-interop/src/lib.rs
Normal 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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
14
tracker/agb-tracker/.cargo/config.toml
Normal file
14
tracker/agb-tracker/.cargo/config.toml
Normal 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"
|
29
tracker/agb-tracker/Cargo.toml
Normal file
29
tracker/agb-tracker/Cargo.toml
Normal 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"
|
BIN
tracker/agb-tracker/examples/algar_-_ninja_on_speed.xm
Normal file
BIN
tracker/agb-tracker/examples/algar_-_ninja_on_speed.xm
Normal file
Binary file not shown.
26
tracker/agb-tracker/examples/basic.rs
Normal file
26
tracker/agb-tracker/examples/basic.rs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
BIN
tracker/agb-tracker/examples/db_toffe.xm
Normal file
BIN
tracker/agb-tracker/examples/db_toffe.xm
Normal file
Binary file not shown.
56
tracker/agb-tracker/examples/timing.rs
Normal file
56
tracker/agb-tracker/examples/timing.rs
Normal 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
115
tracker/agb-tracker/gba.ld
Normal 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/ : { *(*) }
|
||||||
|
}
|
3
tracker/agb-tracker/rust-toolchain.toml
Normal file
3
tracker/agb-tracker/rust-toolchain.toml
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
[toolchain]
|
||||||
|
channel = "nightly"
|
||||||
|
components = ["rust-src", "clippy", "rustfmt"]
|
307
tracker/agb-tracker/src/lib.rs
Normal file
307
tracker/agb-tracker/src/lib.rs
Normal 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 {}
|
||||||
|
}
|
19
tracker/agb-xm-core/Cargo.toml
Normal file
19
tracker/agb-xm-core/Cargo.toml
Normal 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"
|
437
tracker/agb-xm-core/src/lib.rs
Normal file
437
tracker/agb-xm-core/src/lib.rs
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
¬e_and_sample[channel_number]
|
||||||
|
} else {
|
||||||
|
¬e_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
16
tracker/agb-xm/Cargo.toml
Normal 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"
|
8
tracker/agb-xm/src/lib.rs
Normal file
8
tracker/agb-xm/src/lib.rs
Normal 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()
|
||||||
|
}
|
Loading…
Reference in a new issue