From 7e4a2f2e2053ef0e5811050168564f8e5ef8a16d Mon Sep 17 00:00:00 2001 From: Gwilym Inzani Date: Wed, 12 Jul 2023 12:10:05 +0100 Subject: [PATCH 01/65] Create skeleton projects --- agb-tracker/.cargo/config.toml | 14 ++++ agb-tracker/Cargo.toml | 11 +++ agb-tracker/gba.ld | 115 ++++++++++++++++++++++++++++++++ agb-tracker/rust-toolchain.toml | 3 + agb-tracker/src/lib.rs | 20 ++++++ agb-xm/Cargo.toml | 13 ++++ agb-xm/src/lib.rs | 14 ++++ 7 files changed, 190 insertions(+) create mode 100644 agb-tracker/.cargo/config.toml create mode 100644 agb-tracker/Cargo.toml create mode 100644 agb-tracker/gba.ld create mode 100644 agb-tracker/rust-toolchain.toml create mode 100644 agb-tracker/src/lib.rs create mode 100644 agb-xm/Cargo.toml create mode 100644 agb-xm/src/lib.rs diff --git a/agb-tracker/.cargo/config.toml b/agb-tracker/.cargo/config.toml new file mode 100644 index 00000000..62ebedb7 --- /dev/null +++ b/agb-tracker/.cargo/config.toml @@ -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" diff --git a/agb-tracker/Cargo.toml b/agb-tracker/Cargo.toml new file mode 100644 index 00000000..e81ec1bd --- /dev/null +++ b/agb-tracker/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "agb_tracker" +version = "0.15.0" +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" + +[dependencies] +agb_xm = { version = "0.15.0", path = "../agb-xm" } +agb = { version = "0.15.0", path = "../agb" } \ No newline at end of file diff --git a/agb-tracker/gba.ld b/agb-tracker/gba.ld new file mode 100644 index 00000000..525260d9 --- /dev/null +++ b/agb-tracker/gba.ld @@ -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/ : { *(*) } +} \ No newline at end of file diff --git a/agb-tracker/rust-toolchain.toml b/agb-tracker/rust-toolchain.toml new file mode 100644 index 00000000..6e1f5327 --- /dev/null +++ b/agb-tracker/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "nightly" +components = ["rust-src", "clippy", "rustfmt"] diff --git a/agb-tracker/src/lib.rs b/agb-tracker/src/lib.rs new file mode 100644 index 00000000..e9471c40 --- /dev/null +++ b/agb-tracker/src/lib.rs @@ -0,0 +1,20 @@ +#![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))] + +#[cfg(test)] +mod tests { + #[test_case] + fn it_works(_gba: &mut agb::Gba) { + assert_eq!(1, 1); + } +} + +#[cfg(test)] +#[agb::entry] +fn main(gba: agb::Gba) -> ! { + loop {} +} diff --git a/agb-xm/Cargo.toml b/agb-xm/Cargo.toml new file mode 100644 index 00000000..cddb48f5 --- /dev/null +++ b/agb-xm/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "agb_xm" +version = "0.15.0" +authors = ["Gwilym Kuiper "] +edition = "2021" +license = "MPL-2.0" +description = "Library for converting XM tracker files for use with agb-tracker on the Game Boy Advance" +repository = "https://github.com/agbrs/agb" + +[lib] +proc-macro = true + +[dependencies] diff --git a/agb-xm/src/lib.rs b/agb-xm/src/lib.rs new file mode 100644 index 00000000..2ecdc793 --- /dev/null +++ b/agb-xm/src/lib.rs @@ -0,0 +1,14 @@ +fn add(left: usize, right: usize) -> usize { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +} From d903aa164b1a63a97b7ca7a49cac4e6e610b1115 Mon Sep 17 00:00:00 2001 From: Gwilym Inzani Date: Wed, 12 Jul 2023 12:16:00 +0100 Subject: [PATCH 02/65] Need an interop crate too --- agb-tracker-interop/Cargo.toml | 14 ++++++++++++++ agb-tracker-interop/src/lib.rs | 1 + agb-tracker/Cargo.toml | 3 ++- 3 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 agb-tracker-interop/Cargo.toml create mode 100644 agb-tracker-interop/src/lib.rs diff --git a/agb-tracker-interop/Cargo.toml b/agb-tracker-interop/Cargo.toml new file mode 100644 index 00000000..ddfb9ff4 --- /dev/null +++ b/agb-tracker-interop/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "agb_tracker_interop" +version = "0.15.0" +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"] + +[dependencies] +quote = "1" \ No newline at end of file diff --git a/agb-tracker-interop/src/lib.rs b/agb-tracker-interop/src/lib.rs new file mode 100644 index 00000000..2ac3961f --- /dev/null +++ b/agb-tracker-interop/src/lib.rs @@ -0,0 +1 @@ +#![cfg_attr(not(feature(quote)), no_std)] diff --git a/agb-tracker/Cargo.toml b/agb-tracker/Cargo.toml index e81ec1bd..bb7c9851 100644 --- a/agb-tracker/Cargo.toml +++ b/agb-tracker/Cargo.toml @@ -8,4 +8,5 @@ repository = "https://github.com/agbrs/agb" [dependencies] agb_xm = { version = "0.15.0", path = "../agb-xm" } -agb = { version = "0.15.0", path = "../agb" } \ No newline at end of file +agb = { version = "0.15.0", path = "../agb" } +agb_tracker_interop = { version = "0.15.0", path = "../agb-tracker-interop", default-features = false } \ No newline at end of file From aefa8426244f1bd5e1e174ccf771906e17229c84 Mon Sep 17 00:00:00 2001 From: Gwilym Inzani Date: Wed, 12 Jul 2023 12:18:02 +0100 Subject: [PATCH 03/65] Add a std feature instead --- agb-tracker-interop/Cargo.toml | 5 +++-- agb-tracker-interop/src/lib.rs | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/agb-tracker-interop/Cargo.toml b/agb-tracker-interop/Cargo.toml index ddfb9ff4..fe31639a 100644 --- a/agb-tracker-interop/Cargo.toml +++ b/agb-tracker-interop/Cargo.toml @@ -8,7 +8,8 @@ repository = "https://github.com/agbrs/agb" [features] default = ["quote"] -quote = ["dep:quote"] +quote = ["dep:quote", "std"] +std = [] [dependencies] -quote = "1" \ No newline at end of file +quote = { version = "1", optional = true } \ No newline at end of file diff --git a/agb-tracker-interop/src/lib.rs b/agb-tracker-interop/src/lib.rs index 2ac3961f..8731421b 100644 --- a/agb-tracker-interop/src/lib.rs +++ b/agb-tracker-interop/src/lib.rs @@ -1 +1 @@ -#![cfg_attr(not(feature(quote)), no_std)] +#![cfg_attr(not(feature = "std"), no_std)] From af0cf7170e239c3303178c9394283173068a1faf Mon Sep 17 00:00:00 2001 From: Gwilym Inzani Date: Wed, 12 Jul 2023 12:19:50 +0100 Subject: [PATCH 04/65] Move everything to a tracker directory --- tracker/.gitignore | 1 + {agb-tracker-interop => tracker/agb-tracker-interop}/Cargo.toml | 0 {agb-tracker-interop => tracker/agb-tracker-interop}/src/lib.rs | 0 {agb-tracker => tracker/agb-tracker}/.cargo/config.toml | 0 {agb-tracker => tracker/agb-tracker}/Cargo.toml | 2 +- {agb-tracker => tracker/agb-tracker}/gba.ld | 0 {agb-tracker => tracker/agb-tracker}/rust-toolchain.toml | 0 {agb-tracker => tracker/agb-tracker}/src/lib.rs | 0 {agb-xm => tracker/agb-xm}/Cargo.toml | 0 {agb-xm => tracker/agb-xm}/src/lib.rs | 0 10 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 tracker/.gitignore rename {agb-tracker-interop => tracker/agb-tracker-interop}/Cargo.toml (100%) rename {agb-tracker-interop => tracker/agb-tracker-interop}/src/lib.rs (100%) rename {agb-tracker => tracker/agb-tracker}/.cargo/config.toml (100%) rename {agb-tracker => tracker/agb-tracker}/Cargo.toml (89%) rename {agb-tracker => tracker/agb-tracker}/gba.ld (100%) rename {agb-tracker => tracker/agb-tracker}/rust-toolchain.toml (100%) rename {agb-tracker => tracker/agb-tracker}/src/lib.rs (100%) rename {agb-xm => tracker/agb-xm}/Cargo.toml (100%) rename {agb-xm => tracker/agb-xm}/src/lib.rs (100%) diff --git a/tracker/.gitignore b/tracker/.gitignore new file mode 100644 index 00000000..66f47f87 --- /dev/null +++ b/tracker/.gitignore @@ -0,0 +1 @@ +agb-*/Cargo.lock diff --git a/agb-tracker-interop/Cargo.toml b/tracker/agb-tracker-interop/Cargo.toml similarity index 100% rename from agb-tracker-interop/Cargo.toml rename to tracker/agb-tracker-interop/Cargo.toml diff --git a/agb-tracker-interop/src/lib.rs b/tracker/agb-tracker-interop/src/lib.rs similarity index 100% rename from agb-tracker-interop/src/lib.rs rename to tracker/agb-tracker-interop/src/lib.rs diff --git a/agb-tracker/.cargo/config.toml b/tracker/agb-tracker/.cargo/config.toml similarity index 100% rename from agb-tracker/.cargo/config.toml rename to tracker/agb-tracker/.cargo/config.toml diff --git a/agb-tracker/Cargo.toml b/tracker/agb-tracker/Cargo.toml similarity index 89% rename from agb-tracker/Cargo.toml rename to tracker/agb-tracker/Cargo.toml index bb7c9851..d1d9d7d5 100644 --- a/agb-tracker/Cargo.toml +++ b/tracker/agb-tracker/Cargo.toml @@ -8,5 +8,5 @@ repository = "https://github.com/agbrs/agb" [dependencies] agb_xm = { version = "0.15.0", path = "../agb-xm" } -agb = { version = "0.15.0", path = "../agb" } +agb = { version = "0.15.0", path = "../../agb" } agb_tracker_interop = { version = "0.15.0", path = "../agb-tracker-interop", default-features = false } \ No newline at end of file diff --git a/agb-tracker/gba.ld b/tracker/agb-tracker/gba.ld similarity index 100% rename from agb-tracker/gba.ld rename to tracker/agb-tracker/gba.ld diff --git a/agb-tracker/rust-toolchain.toml b/tracker/agb-tracker/rust-toolchain.toml similarity index 100% rename from agb-tracker/rust-toolchain.toml rename to tracker/agb-tracker/rust-toolchain.toml diff --git a/agb-tracker/src/lib.rs b/tracker/agb-tracker/src/lib.rs similarity index 100% rename from agb-tracker/src/lib.rs rename to tracker/agb-tracker/src/lib.rs diff --git a/agb-xm/Cargo.toml b/tracker/agb-xm/Cargo.toml similarity index 100% rename from agb-xm/Cargo.toml rename to tracker/agb-xm/Cargo.toml diff --git a/agb-xm/src/lib.rs b/tracker/agb-xm/src/lib.rs similarity index 100% rename from agb-xm/src/lib.rs rename to tracker/agb-xm/src/lib.rs From f3e3c243a4bf3aad9df6a4432e14d43c3b795ddf Mon Sep 17 00:00:00 2001 From: Gwilym Inzani Date: Wed, 12 Jul 2023 12:33:15 +0100 Subject: [PATCH 05/65] Use the recommended layout for proc macros --- tracker/agb-tracker/Cargo.toml | 6 +++++- tracker/agb-xm-core/Cargo.toml | 15 +++++++++++++++ tracker/agb-xm-core/src/lib.rs | 14 ++++++++++++++ tracker/agb-xm/Cargo.toml | 5 ++++- tracker/agb-xm/src/lib.rs | 18 ++++++------------ 5 files changed, 44 insertions(+), 14 deletions(-) create mode 100644 tracker/agb-xm-core/Cargo.toml create mode 100644 tracker/agb-xm-core/src/lib.rs diff --git a/tracker/agb-tracker/Cargo.toml b/tracker/agb-tracker/Cargo.toml index d1d9d7d5..7da1029d 100644 --- a/tracker/agb-tracker/Cargo.toml +++ b/tracker/agb-tracker/Cargo.toml @@ -6,7 +6,11 @@ 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.15.0", path = "../agb-xm" } +agb_xm = { version = "0.15.0", path = "../agb-xm", optional = true } agb = { version = "0.15.0", path = "../../agb" } agb_tracker_interop = { version = "0.15.0", path = "../agb-tracker-interop", default-features = false } \ No newline at end of file diff --git a/tracker/agb-xm-core/Cargo.toml b/tracker/agb-xm-core/Cargo.toml new file mode 100644 index 00000000..7439041b --- /dev/null +++ b/tracker/agb-xm-core/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "agb_xm_core" +version = "0.15.0" +authors = ["Gwilym Kuiper "] +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" + +agb_tracker_interop = { version = "0.15.0", path = "../agb-tracker-interop" } \ No newline at end of file diff --git a/tracker/agb-xm-core/src/lib.rs b/tracker/agb-xm-core/src/lib.rs new file mode 100644 index 00000000..fb0d6823 --- /dev/null +++ b/tracker/agb-xm-core/src/lib.rs @@ -0,0 +1,14 @@ +use proc_macro2::TokenStream; +use proc_macro_error::abort; + +use quote::quote; + +pub fn agb_xm_core(args: TokenStream) -> TokenStream { + if args.is_empty() { + abort!(args, "must pass a filename"); + } + + quote! { + fn hello_world() {} + } +} diff --git a/tracker/agb-xm/Cargo.toml b/tracker/agb-xm/Cargo.toml index cddb48f5..db3783f3 100644 --- a/tracker/agb-xm/Cargo.toml +++ b/tracker/agb-xm/Cargo.toml @@ -4,10 +4,13 @@ version = "0.15.0" authors = ["Gwilym Kuiper "] edition = "2021" license = "MPL-2.0" -description = "Library for converting XM tracker files for use with agb-tracker on the Game Boy Advance" +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.15.0", path = "../agb-xm-core" } +proc-macro-error = "1" +proc-macro2 = "1" diff --git a/tracker/agb-xm/src/lib.rs b/tracker/agb-xm/src/lib.rs index 2ecdc793..0ccc3918 100644 --- a/tracker/agb-xm/src/lib.rs +++ b/tracker/agb-xm/src/lib.rs @@ -1,14 +1,8 @@ -fn add(left: usize, right: usize) -> usize { - left + right -} +use proc_macro::TokenStream; +use proc_macro_error::proc_macro_error; -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); - } +#[proc_macro_error] +#[proc_macro] +pub fn import_xm(args: TokenStream) -> TokenStream { + agb_xm_core::agb_xm_core(args.into()).into() } From a77b536e69e92fa5e6c399d79eed4ae0d467258a Mon Sep 17 00:00:00 2001 From: Gwilym Inzani Date: Wed, 12 Jul 2023 15:38:09 +0100 Subject: [PATCH 06/65] Really basic export --- tracker/agb-tracker-interop/Cargo.toml | 6 +- tracker/agb-tracker-interop/src/lib.rs | 123 +++++++++++++++++++++++ tracker/agb-tracker/examples/ajoj.xm | Bin 0 -> 18757 bytes tracker/agb-tracker/examples/basic.rs | 21 ++++ tracker/agb-tracker/src/lib.rs | 7 ++ tracker/agb-xm-core/Cargo.toml | 6 +- tracker/agb-xm-core/src/lib.rs | 130 ++++++++++++++++++++++++- 7 files changed, 286 insertions(+), 7 deletions(-) create mode 100644 tracker/agb-tracker/examples/ajoj.xm create mode 100644 tracker/agb-tracker/examples/basic.rs diff --git a/tracker/agb-tracker-interop/Cargo.toml b/tracker/agb-tracker-interop/Cargo.toml index fe31639a..4b2c461e 100644 --- a/tracker/agb-tracker-interop/Cargo.toml +++ b/tracker/agb-tracker-interop/Cargo.toml @@ -8,8 +8,10 @@ repository = "https://github.com/agbrs/agb" [features] default = ["quote"] -quote = ["dep:quote", "std"] +quote = ["dep:quote", "dep:proc-macro2", "std"] std = [] [dependencies] -quote = { version = "1", optional = true } \ No newline at end of file +quote = { version = "1", optional = true } +proc-macro2 = { version = "1", optional = true } +agb_fixnum = { version = "0.15.0", path = "../../agb-fixnum" } \ No newline at end of file diff --git a/tracker/agb-tracker-interop/src/lib.rs b/tracker/agb-tracker-interop/src/lib.rs index 8731421b..f96086ae 100644 --- a/tracker/agb-tracker-interop/src/lib.rs +++ b/tracker/agb-tracker-interop/src/lib.rs @@ -1 +1,124 @@ #![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], +} + +#[derive(Debug)] +pub struct Sample<'a> { + pub data: &'a [u8], +} + +#[derive(Debug)] +pub struct Pattern { + pub num_channels: usize, +} + +#[derive(Debug)] +pub struct PatternSlot { + pub volume: Num, + pub speed: Num, + pub panning: Num, + pub sample: usize, +} + +#[cfg(feature = "quote")] +impl<'a> quote::ToTokens for Track<'a> { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + use quote::{quote, TokenStreamExt}; + + let samples = self.samples; + let pattern_data = self.pattern_data; + let patterns = self.patterns; + + tokens.append_all(quote! { + { + use agb_tracker_interop::*; + + const SAMPLES: &[Sample<'static>] = &[#(#samples),*]; + const PATTERN_DATA: &[PatternSlot] = &[#(#pattern_data),*]; + const PATTERNS: &[Pattern] = &[#(#patterns),*]; + + Track { + samples: SAMPLES, + pattern_data: PATTERN_DATA, + patterns: PATTERNS, + } + } + }) + } +} + +#[cfg(feature = "quote")] +impl<'a> quote::ToTokens for Sample<'a> { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + use quote::{quote, TokenStreamExt}; + + let self_as_u8s = self.data.iter().map(|i| *i as u8); + + tokens.append_all(quote! { + { + use agb_tracker_interop::*; + + const SAMPLE_DATA: &[u8] = &[#(#self_as_u8s),*]; + agb_tracker_interop::Sample { data: SAMPLE_DATA } + } + }); + } +} + +#[cfg(feature = "quote")] +impl quote::ToTokens for PatternSlot { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + use quote::{quote, TokenStreamExt}; + + let PatternSlot { + volume, + speed, + panning, + sample, + } = &self; + + let volume = volume.to_raw(); + let speed = speed.to_raw(); + let panning = panning.to_raw(); + + tokens.append_all(quote! { + { + use agb_tracker::__private::*; + use agb::fixnum::Num; + + PatternSlot { + volume: Num::from_raw(#volume), + speed: Num::from_raw(#speed), + panning: Num::from_raw(#panning), + sample: #sample, + } + } + }); + } +} + +#[cfg(feature = "quote")] +impl quote::ToTokens for Pattern { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + use quote::{quote, TokenStreamExt}; + + let num_channels = self.num_channels; + + tokens.append_all(quote! { + { + use agb_tracker_interop::*; + + Pattern { + num_channels: #num_channels, + } + } + }) + } +} diff --git a/tracker/agb-tracker/examples/ajoj.xm b/tracker/agb-tracker/examples/ajoj.xm new file mode 100644 index 0000000000000000000000000000000000000000..e47bd2f6e8a65497628959001bbcbc8576b586ce GIT binary patch literal 18757 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 literal 0 HcmV?d00001 diff --git a/tracker/agb-tracker/examples/basic.rs b/tracker/agb-tracker/examples/basic.rs new file mode 100644 index 00000000..2df214db --- /dev/null +++ b/tracker/agb-tracker/examples/basic.rs @@ -0,0 +1,21 @@ +#![no_std] +#![no_main] + +use agb::sound::mixer::Frequency; +use agb::Gba; +use agb_tracker::{import_xm, Track}; + +const AJOJ: Track = import_xm!("examples/ajoj.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(); + + loop { + mixer.frame(); + vblank_provider.wait_for_vblank(); + } +} diff --git a/tracker/agb-tracker/src/lib.rs b/tracker/agb-tracker/src/lib.rs index e9471c40..07723f2d 100644 --- a/tracker/agb-tracker/src/lib.rs +++ b/tracker/agb-tracker/src/lib.rs @@ -5,6 +5,13 @@ #![cfg_attr(test, reexport_test_harness_main = "test_main")] #![cfg_attr(test, test_runner(agb::test_runner::test_runner))] +#[cfg(feature = "xm")] +pub use agb_xm::import_xm; + +pub use agb_tracker_interop as __private; + +pub use __private::Track; + #[cfg(test)] mod tests { #[test_case] diff --git a/tracker/agb-xm-core/Cargo.toml b/tracker/agb-xm-core/Cargo.toml index 7439041b..c06b9287 100644 --- a/tracker/agb-xm-core/Cargo.toml +++ b/tracker/agb-xm-core/Cargo.toml @@ -11,5 +11,9 @@ repository = "https://github.com/agbrs/agb" proc-macro-error = "1" proc-macro2 = "1" quote = "1" +syn = "2" -agb_tracker_interop = { version = "0.15.0", path = "../agb-tracker-interop" } \ No newline at end of file +agb_tracker_interop = { version = "0.15.0", path = "../agb-tracker-interop" } +agb_fixnum = { version = "0.15.0", path = "../../agb-fixnum" } + +xmrs = "0.3" \ No newline at end of file diff --git a/tracker/agb-xm-core/src/lib.rs b/tracker/agb-xm-core/src/lib.rs index fb0d6823..c7887ff5 100644 --- a/tracker/agb-xm-core/src/lib.rs +++ b/tracker/agb-xm-core/src/lib.rs @@ -1,14 +1,136 @@ +use std::{collections::HashMap, error::Error, fs, path::Path}; + 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 { - if args.is_empty() { - abort!(args, "must pass a filename"); - } + let input = match syn::parse::(args.into()) { + Ok(input) => input, + Err(err) => return proc_macro2::TokenStream::from(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! { - fn hello_world() {} + { + const _: &[u8] = include_bytes!(#include_path); + + #parsed + } } } + +pub fn load_module_from_file(xm_path: &Path) -> Result> { + 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(); + + 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 sample = match &sample.data { + SampleDataType::Depth8(depth8) => depth8 + .iter() + .map(|value| *value as u8) + .collect::>() + .clone(), + SampleDataType::Depth16(depth16) => depth16 + .iter() + .map(|sample| (sample >> 8) as i8 as u8) + .collect::>(), + }; + + instruments_map.insert((instrument_index, sample_index), samples.len()); + samples.push(sample); + } + } + + let mut patterns = vec![]; + let mut pattern_data = vec![]; + + for pattern in &module.pattern { + let mut num_channels = 0; + + for row in pattern.iter() { + for slot in row { + 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)) + .cloned() + .unwrap_or(0) + } else { + 0 + } + }; + + let volume = Num::new( + if slot.volume == 0 { + 64 + } else { + slot.volume as i16 + } / 64, + ); + let speed = Num::new(1); // TODO: Calculate speed for the correct note here + let panning = Num::new(0); + + pattern_data.push(agb_tracker_interop::PatternSlot { + volume, + speed, + panning, + sample, + }); + } + + num_channels = row.len(); + } + + patterns.push(agb_tracker_interop::Pattern { num_channels }); + } + + let samples: Vec<_> = samples + .iter() + .map(|sample| agb_tracker_interop::Sample { data: &sample }) + .collect(); + + let interop = agb_tracker_interop::Track { + samples: &samples, + pattern_data: &pattern_data, + patterns: &patterns, + }; + + quote!(#interop) +} From 308cb3a19cb9f48d9c6e6a15ae822b4c62e98b4d Mon Sep 17 00:00:00 2001 From: Gwilym Inzani Date: Wed, 12 Jul 2023 17:36:41 +0100 Subject: [PATCH 07/65] Really basic playing --- tracker/agb-tracker-interop/src/lib.rs | 46 +++++++++-- tracker/agb-tracker/examples/basic.rs | 5 +- tracker/agb-tracker/src/lib.rs | 91 +++++++++++++++++++-- tracker/agb-xm-core/src/lib.rs | 105 ++++++++++++++++++++----- tracker/agb-xm-core/src/main.rs | 8 ++ 5 files changed, 224 insertions(+), 31 deletions(-) create mode 100644 tracker/agb-xm-core/src/main.rs diff --git a/tracker/agb-tracker-interop/src/lib.rs b/tracker/agb-tracker-interop/src/lib.rs index f96086ae..cf655e05 100644 --- a/tracker/agb-tracker-interop/src/lib.rs +++ b/tracker/agb-tracker-interop/src/lib.rs @@ -7,16 +7,21 @@ pub struct Track<'a> { pub samples: &'a [Sample<'a>], pub pattern_data: &'a [PatternSlot], pub patterns: &'a [Pattern], + + pub frames_per_step: u16, } #[derive(Debug)] pub struct Sample<'a> { pub data: &'a [u8], + pub should_loop: bool, } #[derive(Debug)] pub struct Pattern { pub num_channels: usize, + pub length: usize, + pub start_position: usize, } #[derive(Debug)] @@ -32,9 +37,12 @@ impl<'a> quote::ToTokens for Track<'a> { fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { use quote::{quote, TokenStreamExt}; - let samples = self.samples; - let pattern_data = self.pattern_data; - let patterns = self.patterns; + let Track { + samples, + pattern_data, + patterns, + frames_per_step, + } = self; tokens.append_all(quote! { { @@ -48,25 +56,43 @@ impl<'a> quote::ToTokens for Track<'a> { samples: SAMPLES, pattern_data: PATTERN_DATA, patterns: PATTERNS, + + frames_per_step: #frames_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 self_as_u8s = self.data.iter().map(|i| *i as u8); + let self_as_u8s: Vec<_> = self.data.iter().map(|i| *i as u8).collect(); + let samples = ByteString(&self_as_u8s); + let should_loop = self.should_loop; tokens.append_all(quote! { { use agb_tracker_interop::*; - const SAMPLE_DATA: &[u8] = &[#(#self_as_u8s),*]; - agb_tracker_interop::Sample { data: SAMPLE_DATA } + #[repr(align(4))] + struct AlignmentWrapper([u8; N]); + + const SAMPLE_DATA: &[u8] = &AlignmentWrapper(*#samples).0; + agb_tracker_interop::Sample { data: SAMPLE_DATA, should_loop: #should_loop } } }); } @@ -109,7 +135,11 @@ impl quote::ToTokens for Pattern { fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { use quote::{quote, TokenStreamExt}; - let num_channels = self.num_channels; + let Pattern { + num_channels, + length, + start_position, + } = self; tokens.append_all(quote! { { @@ -117,6 +147,8 @@ impl quote::ToTokens for Pattern { Pattern { num_channels: #num_channels, + length: #length, + start_position: #start_position, } } }) diff --git a/tracker/agb-tracker/examples/basic.rs b/tracker/agb-tracker/examples/basic.rs index 2df214db..2581f1e0 100644 --- a/tracker/agb-tracker/examples/basic.rs +++ b/tracker/agb-tracker/examples/basic.rs @@ -3,7 +3,7 @@ use agb::sound::mixer::Frequency; use agb::Gba; -use agb_tracker::{import_xm, Track}; +use agb_tracker::{import_xm, Track, Tracker}; const AJOJ: Track = import_xm!("examples/ajoj.xm"); @@ -14,7 +14,10 @@ fn main(mut gba: Gba) -> ! { let mut mixer = gba.mixer.mixer(Frequency::Hz18157); mixer.enable(); + let mut tracker = Tracker::new(&AJOJ); + loop { + tracker.step(&mut mixer); mixer.frame(); vblank_provider.wait_for_vblank(); } diff --git a/tracker/agb-tracker/src/lib.rs b/tracker/agb-tracker/src/lib.rs index 07723f2d..f7bbc700 100644 --- a/tracker/agb-tracker/src/lib.rs +++ b/tracker/agb-tracker/src/lib.rs @@ -5,6 +5,12 @@ #![cfg_attr(test, reexport_test_harness_main = "test_main")] #![cfg_attr(test, test_runner(agb::test_runner::test_runner))] +extern crate alloc; + +use alloc::{vec, vec::Vec}; + +use agb::sound::mixer::{ChannelId, Mixer, SoundChannel}; + #[cfg(feature = "xm")] pub use agb_xm::import_xm; @@ -12,11 +18,86 @@ pub use agb_tracker_interop as __private; pub use __private::Track; -#[cfg(test)] -mod tests { - #[test_case] - fn it_works(_gba: &mut agb::Gba) { - assert_eq!(1, 1); +pub struct Tracker { + track: &'static Track<'static>, + channels: Vec>, + + step: u16, + current_row: usize, + current_pattern: usize, +} + +impl Tracker { + pub fn new(track: &'static Track<'static>) -> Self { + agb::println!("{}", track.frames_per_step); + + Self { + track, + channels: vec![], + + step: 0, + current_row: 0, + current_pattern: 0, + } + } + + pub fn step(&mut self, mixer: &mut Mixer) { + if self.step != 0 { + self.increment_step(); + return; // TODO: volume / pitch slides + } + + let current_pattern = &self.track.patterns[self.current_pattern]; + + let channels_to_play = current_pattern.num_channels; + self.channels.resize_with(channels_to_play, || None); + + let pattern_data_pos = current_pattern.start_position + self.current_row * channels_to_play; + let pattern_slots = + &self.track.pattern_data[pattern_data_pos..pattern_data_pos + channels_to_play]; + + for (channel_id, pattern_slot) in self.channels.iter_mut().zip(pattern_slots) { + if pattern_slot.sample == 0 { + // do nothing + } else { + if let Some(channel) = channel_id + .take() + .and_then(|channel_id| mixer.channel(&channel_id)) + { + channel.stop(); + } + + let sample = &self.track.samples[pattern_slot.sample - 1]; + let mut new_channel = SoundChannel::new(sample.data); + new_channel + .panning(pattern_slot.panning) + .volume(pattern_slot.volume) + .playback(pattern_slot.speed); + + if sample.should_loop { + new_channel.should_loop(); + } + + *channel_id = mixer.play_sound(new_channel); + } + } + + self.increment_step(); + } + + fn increment_step(&mut self) { + self.step += 1; + + if self.step == self.track.frames_per_step * 2 { + self.current_row += 1; + + if self.current_row > self.track.patterns[self.current_pattern].length { + self.current_pattern += 1; + self.current_row = 0; + } + + self.step = 0; + } } } diff --git a/tracker/agb-xm-core/src/lib.rs b/tracker/agb-xm-core/src/lib.rs index c7887ff5..ca643010 100644 --- a/tracker/agb-xm-core/src/lib.rs +++ b/tracker/agb-xm-core/src/lib.rs @@ -48,26 +48,50 @@ pub fn parse_module(module: &Module) -> TokenStream { let instruments = &module.instrument; let mut instruments_map = HashMap::new(); + struct SampleData { + data: Vec, + should_loop: bool, + fine_tune: f64, + relative_note: i8, + } + 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 sample = match &sample.data { - SampleDataType::Depth8(depth8) => depth8 - .iter() - .map(|value| *value as u8) - .collect::>() - .clone(), + let should_loop = !matches!(sample.flags, LoopType::No); + let fine_tune = sample.finetune as f64; + let relative_note = sample.relative_note; + + let mut sample = match &sample.data { + SampleDataType::Depth8(depth8) => { + depth8.iter().map(|value| *value as u8).collect::>() + } SampleDataType::Depth16(depth16) => depth16 .iter() .map(|sample| (sample >> 8) as i8 as u8) .collect::>(), }; + if should_loop { + sample.append(&mut sample.clone()); + sample.append(&mut sample.clone()); + sample.append(&mut sample.clone()); + sample.append(&mut sample.clone()); + sample.append(&mut sample.clone()); + sample.append(&mut sample.clone()); + sample.append(&mut sample.clone()); + } + instruments_map.insert((instrument_index, sample_index), samples.len()); - samples.push(sample); + samples.push(SampleData { + data: sample, + should_loop, + fine_tune, + relative_note, + }); } } @@ -76,6 +100,7 @@ pub fn parse_module(module: &Module) -> TokenStream { for pattern in &module.pattern { let mut num_channels = 0; + let start_pos = pattern_data.len(); for row in pattern.iter() { for slot in row { @@ -90,7 +115,7 @@ pub fn parse_module(module: &Module) -> TokenStream { let sample_slot = instrument.sample_for_note[slot.note as usize] as usize; instruments_map .get(&(instrument_index, sample_slot)) - .cloned() + .map(|sample_idx| sample_idx + 1) .unwrap_or(0) } else { 0 @@ -104,33 +129,77 @@ pub fn parse_module(module: &Module) -> TokenStream { slot.volume as i16 } / 64, ); - let speed = Num::new(1); // TODO: Calculate speed for the correct note here - let panning = Num::new(0); - pattern_data.push(agb_tracker_interop::PatternSlot { - volume, - speed, - panning, - sample, - }); + if sample == 0 { + // TODO should take into account previous sample played on this channel + pattern_data.push(agb_tracker_interop::PatternSlot { + volume: Num::new(0), + speed: Num::new(0), + panning: Num::new(0), + sample: 0, + }) + } else { + let sample_played = &samples[sample - 1]; + + let speed = note_to_speed( + slot.note, + sample_played.fine_tune, + sample_played.relative_note, + ); + let panning = Num::new(0); + + pattern_data.push(agb_tracker_interop::PatternSlot { + volume, + speed, + panning, + sample, + }); + } } num_channels = row.len(); } - patterns.push(agb_tracker_interop::Pattern { num_channels }); + patterns.push(agb_tracker_interop::Pattern { + num_channels, + length: pattern.len(), + start_position: start_pos, + }); } let samples: Vec<_> = samples .iter() - .map(|sample| agb_tracker_interop::Sample { data: &sample }) + .map(|sample| agb_tracker_interop::Sample { + data: &sample.data, + should_loop: sample.should_loop, + }) .collect(); + let frames_per_step = + ((60.0 * 60.0) / module.default_bpm as f64 / module.default_tempo as f64) as u16; + let interop = agb_tracker_interop::Track { samples: &samples, pattern_data: &pattern_data, patterns: &patterns, + + frames_per_step, }; quote!(#interop) } + +fn note_to_frequency(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 as f64) * 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_speed(note: Note, fine_tune: f64, relative_note: i8) -> Num { + let frequency = note_to_frequency(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) +} diff --git a/tracker/agb-xm-core/src/main.rs b/tracker/agb-xm-core/src/main.rs new file mode 100644 index 00000000..2e2ba88f --- /dev/null +++ b/tracker/agb-xm-core/src/main.rs @@ -0,0 +1,8 @@ +fn main() -> Result<(), Box> { + let module = agb_xm_core::load_module_from_file(&std::path::Path::new( + "../agb-tracker/examples/ajoj.xm", + ))?; + let output = agb_xm_core::parse_module(&module); + + Ok(()) +} From 436b49c80df375870d9d72cf80017e67c02599a7 Mon Sep 17 00:00:00 2001 From: Gwilym Inzani Date: Wed, 12 Jul 2023 18:52:29 +0100 Subject: [PATCH 08/65] Get closer to some reasonable sounding --- tracker/agb-tracker-interop/src/lib.rs | 6 ++-- tracker/agb-tracker/examples/basic.rs | 2 +- tracker/agb-tracker/examples/db_toffe.xm | Bin 0 -> 83642 bytes .../agb-tracker/examples/final_countdown.xm | Bin 0 -> 69650 bytes tracker/agb-tracker/src/lib.rs | 24 +++++++++------ tracker/agb-xm-core/src/lib.rs | 28 ++++++++---------- tracker/agb-xm-core/src/main.rs | 2 +- 7 files changed, 32 insertions(+), 30 deletions(-) create mode 100644 tracker/agb-tracker/examples/db_toffe.xm create mode 100644 tracker/agb-tracker/examples/final_countdown.xm diff --git a/tracker/agb-tracker-interop/src/lib.rs b/tracker/agb-tracker-interop/src/lib.rs index cf655e05..54cc4758 100644 --- a/tracker/agb-tracker-interop/src/lib.rs +++ b/tracker/agb-tracker-interop/src/lib.rs @@ -8,6 +8,7 @@ pub struct Track<'a> { pub pattern_data: &'a [PatternSlot], pub patterns: &'a [Pattern], + pub num_channels: usize, pub frames_per_step: u16, } @@ -19,7 +20,6 @@ pub struct Sample<'a> { #[derive(Debug)] pub struct Pattern { - pub num_channels: usize, pub length: usize, pub start_position: usize, } @@ -42,6 +42,7 @@ impl<'a> quote::ToTokens for Track<'a> { pattern_data, patterns, frames_per_step, + num_channels, } = self; tokens.append_all(quote! { @@ -58,6 +59,7 @@ impl<'a> quote::ToTokens for Track<'a> { patterns: PATTERNS, frames_per_step: #frames_per_step, + num_channels: #num_channels, } } }) @@ -136,7 +138,6 @@ impl quote::ToTokens for Pattern { use quote::{quote, TokenStreamExt}; let Pattern { - num_channels, length, start_position, } = self; @@ -146,7 +147,6 @@ impl quote::ToTokens for Pattern { use agb_tracker_interop::*; Pattern { - num_channels: #num_channels, length: #length, start_position: #start_position, } diff --git a/tracker/agb-tracker/examples/basic.rs b/tracker/agb-tracker/examples/basic.rs index 2581f1e0..2913a4d0 100644 --- a/tracker/agb-tracker/examples/basic.rs +++ b/tracker/agb-tracker/examples/basic.rs @@ -5,7 +5,7 @@ use agb::sound::mixer::Frequency; use agb::Gba; use agb_tracker::{import_xm, Track, Tracker}; -const AJOJ: Track = import_xm!("examples/ajoj.xm"); +const AJOJ: Track = import_xm!("examples/db_toffe.xm"); #[agb::entry] fn main(mut gba: Gba) -> ! { diff --git a/tracker/agb-tracker/examples/db_toffe.xm b/tracker/agb-tracker/examples/db_toffe.xm new file mode 100644 index 0000000000000000000000000000000000000000..b2b22a104506fc5f764a36af5addd7f846faf4be GIT binary patch literal 83642 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 gD1YQJHDkt%X*77?HKY0sZ8U0F) -> Self { - agb::println!("{}", track.frames_per_step); + let mut channels = Vec::new(); + channels.resize_with(track.num_channels, || None); Self { track, - channels: vec![], + channels, step: 0, current_row: 0, @@ -49,16 +50,21 @@ impl Tracker { let current_pattern = &self.track.patterns[self.current_pattern]; - let channels_to_play = current_pattern.num_channels; - self.channels.resize_with(channels_to_play, || None); - - let pattern_data_pos = current_pattern.start_position + self.current_row * channels_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 + channels_to_play]; + &self.track.pattern_data[pattern_data_pos..pattern_data_pos + self.track.num_channels]; for (channel_id, pattern_slot) in self.channels.iter_mut().zip(pattern_slots) { if pattern_slot.sample == 0 { - // do nothing + if pattern_slot.speed == 0.into() { + if let Some(channel) = channel_id + .take() + .and_then(|channel_id| mixer.channel(&channel_id)) + { + channel.stop(); + } + } } else { if let Some(channel) = channel_id .take() diff --git a/tracker/agb-xm-core/src/lib.rs b/tracker/agb-xm-core/src/lib.rs index ca643010..b701080c 100644 --- a/tracker/agb-xm-core/src/lib.rs +++ b/tracker/agb-xm-core/src/lib.rs @@ -99,7 +99,6 @@ pub fn parse_module(module: &Module) -> TokenStream { let mut pattern_data = vec![]; for pattern in &module.pattern { - let mut num_channels = 0; let start_pos = pattern_data.len(); for row in pattern.iter() { @@ -122,19 +121,21 @@ pub fn parse_module(module: &Module) -> TokenStream { } }; - let volume = Num::new( - if slot.volume == 0 { - 64 - } else { - slot.volume as i16 - } / 64, - ); + let volume = Num::new(if slot.volume == 0 { + 64 + } else { + slot.volume as i16 + }) / 64; if sample == 0 { // TODO should take into account previous sample played on this channel pattern_data.push(agb_tracker_interop::PatternSlot { volume: Num::new(0), - speed: Num::new(0), + speed: if matches!(slot.note, Note::KeyOff) { + 0.into() + } else { + note_to_speed(slot.note, 0.0, 0) + }, panning: Num::new(0), sample: 0, }) @@ -156,12 +157,9 @@ pub fn parse_module(module: &Module) -> TokenStream { }); } } - - num_channels = row.len(); } patterns.push(agb_tracker_interop::Pattern { - num_channels, length: pattern.len(), start_position: start_pos, }); @@ -175,15 +173,13 @@ pub fn parse_module(module: &Module) -> TokenStream { }) .collect(); - let frames_per_step = - ((60.0 * 60.0) / module.default_bpm as f64 / module.default_tempo as f64) as u16; - let interop = agb_tracker_interop::Track { samples: &samples, pattern_data: &pattern_data, patterns: &patterns, + num_channels: module.get_num_channels(), - frames_per_step, + frames_per_step: 2, // TODO calculate this correctly }; quote!(#interop) diff --git a/tracker/agb-xm-core/src/main.rs b/tracker/agb-xm-core/src/main.rs index 2e2ba88f..026485eb 100644 --- a/tracker/agb-xm-core/src/main.rs +++ b/tracker/agb-xm-core/src/main.rs @@ -1,6 +1,6 @@ fn main() -> Result<(), Box> { let module = agb_xm_core::load_module_from_file(&std::path::Path::new( - "../agb-tracker/examples/ajoj.xm", + "../agb-tracker/examples/final_countdown.xm", ))?; let output = agb_xm_core::parse_module(&module); From cf1f3965d8bb3a7c5e8385bd1bebf8e17e7b25bd Mon Sep 17 00:00:00 2001 From: Gwilym Inzani Date: Wed, 12 Jul 2023 19:06:55 +0100 Subject: [PATCH 09/65] Improvements --- tracker/agb-tracker-interop/src/lib.rs | 4 ++++ tracker/agb-tracker/src/lib.rs | 13 ++++++++++--- tracker/agb-xm-core/src/lib.rs | 23 ++++++++++++++++++----- 3 files changed, 32 insertions(+), 8 deletions(-) diff --git a/tracker/agb-tracker-interop/src/lib.rs b/tracker/agb-tracker-interop/src/lib.rs index 54cc4758..73bdcbea 100644 --- a/tracker/agb-tracker-interop/src/lib.rs +++ b/tracker/agb-tracker-interop/src/lib.rs @@ -7,6 +7,7 @@ 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_step: u16, @@ -43,6 +44,7 @@ impl<'a> quote::ToTokens for Track<'a> { patterns, frames_per_step, num_channels, + patterns_to_play, } = self; tokens.append_all(quote! { @@ -52,11 +54,13 @@ impl<'a> quote::ToTokens for Track<'a> { const SAMPLES: &[Sample<'static>] = &[#(#samples),*]; const PATTERN_DATA: &[PatternSlot] = &[#(#pattern_data),*]; const PATTERNS: &[Pattern] = &[#(#patterns),*]; + const PATTERNS_TO_PLAY: &[usize] = &[#(#patterns_to_play),*]; Track { samples: SAMPLES, pattern_data: PATTERN_DATA, patterns: PATTERNS, + patterns_to_play: PATTERNS_TO_PLAY, frames_per_step: #frames_per_step, num_channels: #num_channels, diff --git a/tracker/agb-tracker/src/lib.rs b/tracker/agb-tracker/src/lib.rs index 066bd00d..d49963d8 100644 --- a/tracker/agb-tracker/src/lib.rs +++ b/tracker/agb-tracker/src/lib.rs @@ -48,7 +48,8 @@ impl Tracker { return; // TODO: volume / pitch slides } - let current_pattern = &self.track.patterns[self.current_pattern]; + 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; @@ -94,12 +95,18 @@ impl Tracker { fn increment_step(&mut self) { self.step += 1; - if self.step == self.track.frames_per_step * 2 { + if self.step == self.track.frames_per_step { self.current_row += 1; - if self.current_row > self.track.patterns[self.current_pattern].length { + 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.step = 0; diff --git a/tracker/agb-xm-core/src/lib.rs b/tracker/agb-xm-core/src/lib.rs index b701080c..3055a1d1 100644 --- a/tracker/agb-xm-core/src/lib.rs +++ b/tracker/agb-xm-core/src/lib.rs @@ -53,6 +53,7 @@ pub fn parse_module(module: &Module) -> TokenStream { should_loop: bool, fine_tune: f64, relative_note: i8, + volume: f64, } let mut samples = vec![]; @@ -64,6 +65,7 @@ pub fn parse_module(module: &Module) -> TokenStream { let should_loop = !matches!(sample.flags, LoopType::No); let fine_tune = sample.finetune as f64; let relative_note = sample.relative_note; + let volume = sample.volume as f64; let mut sample = match &sample.data { SampleDataType::Depth8(depth8) => { @@ -91,6 +93,7 @@ pub fn parse_module(module: &Module) -> TokenStream { should_loop, fine_tune, relative_note, + volume, }); } } @@ -121,11 +124,11 @@ pub fn parse_module(module: &Module) -> TokenStream { } }; - let volume = Num::new(if slot.volume == 0 { - 64 + let volume = if slot.volume == 0 { + 64.0 } else { - slot.volume as i16 - }) / 64; + slot.volume as f64 + } / 64.0; if sample == 0 { // TODO should take into account previous sample played on this channel @@ -149,6 +152,9 @@ pub fn parse_module(module: &Module) -> TokenStream { ); let panning = Num::new(0); + let overall_volume = volume * sample_played.volume; + let volume = Num::from_raw((overall_volume * (1 << 4) as f64) as i16); + pattern_data.push(agb_tracker_interop::PatternSlot { volume, speed, @@ -173,13 +179,20 @@ pub fn parse_module(module: &Module) -> TokenStream { }) .collect(); + let patterns_to_play = module + .pattern_order + .iter() + .map(|order| *order as usize) + .collect::>(); + 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_step: 2, // TODO calculate this correctly + frames_per_step: 4, // TODO calculate this correctly }; quote!(#interop) From 989d376056d1010268ba80d9f63b1dbac7efe330 Mon Sep 17 00:00:00 2001 From: Gwilym Inzani Date: Wed, 12 Jul 2023 23:41:30 +0100 Subject: [PATCH 10/65] Amega frequencies and effects --- tracker/agb-tracker-interop/src/lib.rs | 3 + tracker/agb-tracker/examples/basic.rs | 4 +- tracker/agb-tracker/src/lib.rs | 31 +++++-- tracker/agb-xm-core/src/lib.rs | 112 ++++++++++++++++++------- 4 files changed, 112 insertions(+), 38 deletions(-) diff --git a/tracker/agb-tracker-interop/src/lib.rs b/tracker/agb-tracker-interop/src/lib.rs index 73bdcbea..2e4a3606 100644 --- a/tracker/agb-tracker-interop/src/lib.rs +++ b/tracker/agb-tracker-interop/src/lib.rs @@ -33,6 +33,9 @@ pub struct PatternSlot { pub sample: usize, } +pub const SKIP_SLOT: usize = 277; +pub const STOP_CHANNEL: usize = 278; + #[cfg(feature = "quote")] impl<'a> quote::ToTokens for Track<'a> { fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { diff --git a/tracker/agb-tracker/examples/basic.rs b/tracker/agb-tracker/examples/basic.rs index 2913a4d0..2f7d7139 100644 --- a/tracker/agb-tracker/examples/basic.rs +++ b/tracker/agb-tracker/examples/basic.rs @@ -5,7 +5,7 @@ use agb::sound::mixer::Frequency; use agb::Gba; use agb_tracker::{import_xm, Track, Tracker}; -const AJOJ: Track = import_xm!("examples/db_toffe.xm"); +const DB_TOFFE: Track = import_xm!("examples/db_toffe.xm"); #[agb::entry] fn main(mut gba: Gba) -> ! { @@ -14,7 +14,7 @@ fn main(mut gba: Gba) -> ! { let mut mixer = gba.mixer.mixer(Frequency::Hz18157); mixer.enable(); - let mut tracker = Tracker::new(&AJOJ); + let mut tracker = Tracker::new(&DB_TOFFE); loop { tracker.step(&mut mixer); diff --git a/tracker/agb-tracker/src/lib.rs b/tracker/agb-tracker/src/lib.rs index d49963d8..e9d0473d 100644 --- a/tracker/agb-tracker/src/lib.rs +++ b/tracker/agb-tracker/src/lib.rs @@ -57,13 +57,30 @@ impl Tracker { &self.track.pattern_data[pattern_data_pos..pattern_data_pos + self.track.num_channels]; for (channel_id, pattern_slot) in self.channels.iter_mut().zip(pattern_slots) { - if pattern_slot.sample == 0 { - if pattern_slot.speed == 0.into() { - if let Some(channel) = channel_id - .take() - .and_then(|channel_id| mixer.channel(&channel_id)) - { - channel.stop(); + if pattern_slot.sample == agb_tracker_interop::SKIP_SLOT { + // completely skip + } else if pattern_slot.sample == agb_tracker_interop::STOP_CHANNEL { + if let Some(channel) = channel_id + .take() + .and_then(|channel_id| mixer.channel(&channel_id)) + { + channel.stop(); + } + } else if pattern_slot.sample == 0 { + if let Some(channel) = channel_id + .as_ref() + .and_then(|channel_id| mixer.channel(channel_id)) + { + if pattern_slot.volume != 0.into() { + channel.volume(pattern_slot.volume); + } + + if pattern_slot.panning != 0.into() { + channel.panning(pattern_slot.panning); + } + + if pattern_slot.speed != 0.into() { + channel.playback(pattern_slot.speed); } } } else { diff --git a/tracker/agb-xm-core/src/lib.rs b/tracker/agb-xm-core/src/lib.rs index 3055a1d1..7174899a 100644 --- a/tracker/agb-xm-core/src/lib.rs +++ b/tracker/agb-xm-core/src/lib.rs @@ -84,7 +84,6 @@ pub fn parse_module(module: &Module) -> TokenStream { sample.append(&mut sample.clone()); sample.append(&mut sample.clone()); sample.append(&mut sample.clone()); - sample.append(&mut sample.clone()); } instruments_map.insert((instrument_index, sample_index), samples.len()); @@ -124,24 +123,46 @@ pub fn parse_module(module: &Module) -> TokenStream { } }; - let volume = if slot.volume == 0 { - 64.0 - } else { - slot.volume as f64 - } / 64.0; + let (mut volume, panning) = match slot.volume { + 0x10..=0x50 => (Some((slot.volume - 0x10) as f64 / 64.0), None), + 0xC0..=0xCF => ( + None, + Some(Num::new(slot.volume as i16 - (0xC0 + (0xCF - 0xC0) / 2)) / 64), + ), + _ => (None, Some(0.into())), + }; + + if slot.effect_type == 0xC { + volume = Some(slot.effect_parameter as f64 / 255.0); + } if sample == 0 { - // TODO should take into account previous sample played on this channel - pattern_data.push(agb_tracker_interop::PatternSlot { - volume: Num::new(0), - speed: if matches!(slot.note, Note::KeyOff) { - 0.into() - } else { - note_to_speed(slot.note, 0.0, 0) - }, - panning: Num::new(0), - sample: 0, - }) + if slot.volume == 0 && slot.effect_type == 0 { + pattern_data.push(agb_tracker_interop::PatternSlot { + volume: 0.into(), + speed: 0.into(), + panning: 0.into(), + sample: agb_tracker_interop::SKIP_SLOT, + }); + } else if matches!(slot.note, Note::KeyOff) || volume == Some(0.0) { + pattern_data.push(agb_tracker_interop::PatternSlot { + volume: 0.into(), + speed: 0.into(), + panning: 0.into(), + sample: agb_tracker_interop::STOP_CHANNEL, + }); + } else { + let volume: Num = + Num::from_raw((volume.unwrap_or(0.into()) * (1 << 4) as f64) as i16); + let panning = panning.unwrap_or(0.into()); + + pattern_data.push(agb_tracker_interop::PatternSlot { + volume, + speed: 0.into(), + panning, + sample: 0, + }); + } } else { let sample_played = &samples[sample - 1]; @@ -149,16 +170,17 @@ pub fn parse_module(module: &Module) -> TokenStream { slot.note, sample_played.fine_tune, sample_played.relative_note, + module.frequency_type, ); - let panning = Num::new(0); - let overall_volume = volume * sample_played.volume; - let volume = Num::from_raw((overall_volume * (1 << 4) as f64) as i16); + let overall_volume = volume.unwrap_or(1.into()) * sample_played.volume; + let volume: Num = + Num::from_raw((overall_volume * (1 << 4) as f64) as i16); pattern_data.push(agb_tracker_interop::PatternSlot { volume, speed, - panning, + panning: panning.unwrap_or(0.into()), sample, }); } @@ -198,17 +220,49 @@ pub fn parse_module(module: &Module) -> TokenStream { quote!(#interop) } -fn note_to_frequency(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 as f64) * 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_speed(note: Note, fine_tune: f64, relative_note: i8) -> Num { - let frequency = note_to_frequency(note, fine_tune, relative_note); +fn note_to_speed( + note: Note, + fine_tune: f64, + relative_note: i8, + frequency_type: FrequencyType, +) -> Num { + 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 as f64) * 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) + relative_note as usize; + 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, +]; From a8d751a0ef140ad85cf4809b0da92de9fda165fa Mon Sep 17 00:00:00 2001 From: Gwilym Inzani Date: Wed, 12 Jul 2023 23:45:12 +0100 Subject: [PATCH 11/65] Panning effect --- tracker/agb-xm-core/src/lib.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tracker/agb-xm-core/src/lib.rs b/tracker/agb-xm-core/src/lib.rs index 7174899a..073988a7 100644 --- a/tracker/agb-xm-core/src/lib.rs +++ b/tracker/agb-xm-core/src/lib.rs @@ -123,7 +123,7 @@ pub fn parse_module(module: &Module) -> TokenStream { } }; - let (mut volume, panning) = match slot.volume { + let (mut volume, mut panning) = match slot.volume { 0x10..=0x50 => (Some((slot.volume - 0x10) as f64 / 64.0), None), 0xC0..=0xCF => ( None, @@ -136,6 +136,10 @@ pub fn parse_module(module: &Module) -> TokenStream { volume = Some(slot.effect_parameter as f64 / 255.0); } + if slot.effect_type == 0x8 { + panning = Some(Num::new(slot.effect_parameter as i16 - 128) / 128); + } + if sample == 0 { if slot.volume == 0 && slot.effect_type == 0 { pattern_data.push(agb_tracker_interop::PatternSlot { From 86db9d15bf83fa9d4560bd58e16779a2a28f56ce Mon Sep 17 00:00:00 2001 From: Gwilym Inzani Date: Thu, 13 Jul 2023 00:04:41 +0100 Subject: [PATCH 12/65] Don't assume agb-tracker-interop dependency --- tracker/agb-tracker-interop/src/lib.rs | 39 +++++++++----------------- tracker/agb-tracker/src/lib.rs | 7 +++-- 2 files changed, 18 insertions(+), 28 deletions(-) diff --git a/tracker/agb-tracker-interop/src/lib.rs b/tracker/agb-tracker-interop/src/lib.rs index 2e4a3606..47b5bd04 100644 --- a/tracker/agb-tracker-interop/src/lib.rs +++ b/tracker/agb-tracker-interop/src/lib.rs @@ -52,14 +52,12 @@ impl<'a> quote::ToTokens for Track<'a> { tokens.append_all(quote! { { - use agb_tracker_interop::*; - - const SAMPLES: &[Sample<'static>] = &[#(#samples),*]; - const PATTERN_DATA: &[PatternSlot] = &[#(#pattern_data),*]; - const PATTERNS: &[Pattern] = &[#(#patterns),*]; + 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),*]; - Track { + agb_tracker::Track { samples: SAMPLES, pattern_data: PATTERN_DATA, patterns: PATTERNS, @@ -95,13 +93,11 @@ impl<'a> quote::ToTokens for Sample<'a> { tokens.append_all(quote! { { - use agb_tracker_interop::*; - #[repr(align(4))] struct AlignmentWrapper([u8; N]); const SAMPLE_DATA: &[u8] = &AlignmentWrapper(*#samples).0; - agb_tracker_interop::Sample { data: SAMPLE_DATA, should_loop: #should_loop } + agb_tracker::__private::agb_tracker_interop::Sample { data: SAMPLE_DATA, should_loop: #should_loop } } }); } @@ -124,16 +120,11 @@ impl quote::ToTokens for PatternSlot { let panning = panning.to_raw(); tokens.append_all(quote! { - { - use agb_tracker::__private::*; - use agb::fixnum::Num; - - PatternSlot { - volume: Num::from_raw(#volume), - speed: Num::from_raw(#speed), - panning: Num::from_raw(#panning), - sample: #sample, - } + agb_tracker::__private::agb_tracker_interop::PatternSlot { + volume: agb_tracker::__private::Num::from_raw(#volume), + speed: agb_tracker::__private::Num::from_raw(#speed), + panning: agb_tracker::__private::Num::from_raw(#panning), + sample: #sample, } }); } @@ -150,13 +141,9 @@ impl quote::ToTokens for Pattern { } = self; tokens.append_all(quote! { - { - use agb_tracker_interop::*; - - Pattern { - length: #length, - start_position: #start_position, - } + agb_tracker::__private::agb_tracker_interop::Pattern { + length: #length, + start_position: #start_position, } }) } diff --git a/tracker/agb-tracker/src/lib.rs b/tracker/agb-tracker/src/lib.rs index e9d0473d..3b4c18d2 100644 --- a/tracker/agb-tracker/src/lib.rs +++ b/tracker/agb-tracker/src/lib.rs @@ -14,9 +14,12 @@ use agb::sound::mixer::{ChannelId, Mixer, SoundChannel}; #[cfg(feature = "xm")] pub use agb_xm::import_xm; -pub use agb_tracker_interop as __private; +pub mod __private { + pub use agb::fixnum::Num; + pub use agb_tracker_interop; +} -pub use __private::Track; +pub use agb_tracker_interop::Track; pub struct Tracker { track: &'static Track<'static>, From d38fea7f7a7d67519f6d954ed4e6e6b850e74ef3 Mon Sep 17 00:00:00 2001 From: Gwilym Inzani Date: Fri, 14 Jul 2023 22:50:11 +0100 Subject: [PATCH 13/65] Pass the buffer size rather than using the global variable for it --- agb/src/sound/mixer/mixer.s | 20 ++++++-------------- agb/src/sound/mixer/sw_mixer.rs | 20 ++++++++------------ 2 files changed, 14 insertions(+), 26 deletions(-) diff --git a/agb/src/sound/mixer/mixer.s b/agb/src/sound/mixer/mixer.s index 1f9db61b..9605e800 100644 --- a/agb/src/sound/mixer/mixer.s +++ b/agb/src/sound/mixer/mixer.s @@ -1,9 +1,3 @@ -.section .iwram.buffer_size - .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 @ Arguments @@ -12,8 +6,9 @@ agb_arm_func \fn_name @ r2 - playback speed (usize fixnum with 8 bits) @ r3 - amount to modify the left channel by (u16 fixnum with 4 bits) @ stack position 1 - amount to modify the right channel by (u16 fixnum with 4 bits) + @ stack position 2 - the buffer_size (usize) @ - @ The sound buffer must be SOUND_BUFFER_SIZE * 2 in size = 176 * 2 + @ The sound buffer must be buffer_size * 2 in size push {{r4-r8}} ldr r7, [sp, #20] @ load the right channel modification amount into r7 @@ -25,9 +20,7 @@ agb_arm_func \fn_name 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 r8, [sp, #24] 1: .rept 4 @@ -69,8 +62,7 @@ agb_arm_func \fn_name sub r3, r3, #1 mov r5, #0 @ current index we're reading from - ldr r8, =agb_rs__buffer_size @ the number of steps left - ldr r8, [r8] + ldr r8, [sp, #24] @ the number of steps we have left 1: .rept 4 @@ -110,14 +102,14 @@ agb_arm_func \fn_name @ 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) @ r2 - volume to play the sound at + @ r3 - the buffer size @ @ The sound buffer must be SOUND_BUFFER_SIZE * 2 in size = 176 * 2 push {{r4-r11}} ldr r5, =0x00000FFF - ldr r8, =agb_rs__buffer_size - ldr r8, [r8] + mov r8, r3 .macro add_stereo_sample sample_reg:req ldrsh r6, [r0], #2 @ load the current sound sample to r6 diff --git a/agb/src/sound/mixer/sw_mixer.rs b/agb/src/sound/mixer/sw_mixer.rs index 9ae8e3bc..cc9b2f15 100644 --- a/agb/src/sound/mixer/sw_mixer.rs +++ b/agb/src/sound/mixer/sw_mixer.rs @@ -27,6 +27,7 @@ extern "C" { playback_speed: Num, left_amount: Num, right_amount: Num, + buffer_size: usize, ); fn agb_rs__mixer_add_first( @@ -35,18 +36,21 @@ extern "C" { playback_speed: Num, left_amount: Num, right_amount: Num, + buffer_size: usize, ); fn agb_rs__mixer_add_stereo( sound_data: *const u8, sound_buffer: *mut Num, volume: Num, + buffer_size: usize, ); fn agb_rs__mixer_add_stereo_first( sound_data: *const u8, sound_buffer: *mut Num, volume: Num, + buffer_size: usize, ); fn agb_rs__mixer_collapse( @@ -164,8 +168,6 @@ impl Mixer<'_> { }) }; - set_asm_buffer_size(frequency); - let mut working_buffer = Vec::with_capacity_in(frequency.buffer_size() * 2, InternalAllocator); working_buffer.resize(frequency.buffer_size() * 2, 0.into()); @@ -322,16 +324,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>); impl SoundBuffer { @@ -452,6 +444,7 @@ impl MixerBuffer { channel.data.as_ptr().add(channel.pos.floor() as usize), working_buffer.as_mut_ptr(), channel.volume, + self.frequency.buffer_size(), ); } } else { @@ -465,6 +458,7 @@ impl MixerBuffer { playback_speed, left_amount, right_amount, + self.frequency.buffer_size(), ); } } @@ -485,6 +479,7 @@ impl MixerBuffer { channel.data.as_ptr().add(channel.pos.floor() as usize), working_buffer.as_mut_ptr(), channel.volume, + self.frequency.buffer_size(), ); } } else { @@ -498,6 +493,7 @@ impl MixerBuffer { playback_speed, left_amount, right_amount, + self.frequency.buffer_size(), ); } } From fc4632ca8a371cd9b4396a3240a4f1c60ab026a1 Mon Sep 17 00:00:00 2001 From: Gwilym Inzani Date: Fri, 14 Jul 2023 22:51:58 +0100 Subject: [PATCH 14/65] Only need to load the buffer size once --- agb/src/sound/mixer/mixer.s | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/agb/src/sound/mixer/mixer.s b/agb/src/sound/mixer/mixer.s index 9605e800..fb573f62 100644 --- a/agb/src/sound/mixer/mixer.s +++ b/agb/src/sound/mixer/mixer.s @@ -12,6 +12,7 @@ agb_arm_func \fn_name push {{r4-r8}} ldr r7, [sp, #20] @ load the right channel modification amount into r7 + ldr r8, [sp, #24] @ load the buffer size into r8 cmp r7, r3 @ check if left and right channel need the same modifications beq 3f @ same modification @@ -20,7 +21,6 @@ agb_arm_func \fn_name 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, [sp, #24] 1: .rept 4 @@ -62,7 +62,6 @@ agb_arm_func \fn_name sub r3, r3, #1 mov r5, #0 @ current index we're reading from - ldr r8, [sp, #24] @ the number of steps we have left 1: .rept 4 From eb8cb667c12070eae3dad0b7dc87c0216d597f3d Mon Sep 17 00:00:00 2001 From: Gwilym Inzani Date: Fri, 14 Jul 2023 23:02:41 +0100 Subject: [PATCH 15/65] Extract to macros --- agb/src/sound/mixer/mixer.s | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/agb/src/sound/mixer/mixer.s b/agb/src/sound/mixer/mixer.s index fb573f62..f9477b91 100644 --- a/agb/src/sound/mixer/mixer.s +++ b/agb/src/sound/mixer/mixer.s @@ -12,7 +12,14 @@ agb_arm_func \fn_name push {{r4-r8}} ldr r7, [sp, #20] @ load the right channel modification amount into r7 - ldr r8, [sp, #24] @ load the buffer size into r8 + ldr r8, [sp, #24] @ load the buffer size into r8 + + movs r8, r8 @ check that the buffer size isn't 0 + bne 1f + + pop {{r4-r8}} + bx lr +1: cmp r7, r3 @ check if left and right channel need the same modifications beq 3f @ same modification @@ -22,8 +29,7 @@ agb_arm_func \fn_name mov r5, #0 @ current index we're reading from -1: -.rept 4 +.macro add_one_sample add r4, r0, r5, asr #8 @ calculate the address of the next read from the sound buffer ldrsb r6, [r4] @ load the current sound sample to r6 add r5, r5, r2 @ calculate the position to read the next sample from @@ -36,8 +42,12 @@ agb_arm_func \fn_name .endif str r4, [r1], #4 @ store the new value, and increment the pointer +.endm +1: +.rept 4 + add_one_sample .endr - +.purgem add_one_sample subs r8, r8, #4 @ loop counter bne 1b @ jump back if we're done with the loop @@ -63,13 +73,11 @@ agb_arm_func \fn_name mov r5, #0 @ current index we're reading from -1: -.rept 4 +.macro add_one_sample_same_modification add r4, r0, r5, asr #8 @ calculate the address of the next read from the sound buffer ldrsb r6, [r4] @ load the current sound sample to r6 add r5, r5, r2 @ calculate the position to read the next sample from - lsl r6, r6, #16 orr r6, r6, lsr #16 @@ -81,7 +89,12 @@ agb_arm_func \fn_name .endif str r4, [r1], #4 @ store the new value, and increment the pointer +.endm +1: +.rept 4 + add_one_sample_same_modification .endr +.purgem add_one_sample_same_modification subs r8, r8, #4 @ loop counter bne 1b @ jump back if we're done with the loop From a61069fb60363293915ebc37fd65ca6263d2ec51 Mon Sep 17 00:00:00 2001 From: Gwilym Inzani Date: Fri, 14 Jul 2023 23:46:42 +0100 Subject: [PATCH 16/65] Handle non multiple of 4 buffer sizes for mono --- agb/src/sound/mixer/mixer.s | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/agb/src/sound/mixer/mixer.s b/agb/src/sound/mixer/mixer.s index f9477b91..8cc44ea0 100644 --- a/agb/src/sound/mixer/mixer.s +++ b/agb/src/sound/mixer/mixer.s @@ -43,6 +43,18 @@ agb_arm_func \fn_name str r4, [r1], #4 @ store the new value, and increment the pointer .endm + +@ handle the non-multiple of 4 buffer size case + and r3, r8, #3 +1: + subs r3, r3, #1 + bmi 1f + + add_one_sample + subs r8, r8, #1 + beq 2f + b 1b + 1: .rept 4 add_one_sample @@ -51,6 +63,7 @@ agb_arm_func \fn_name subs r8, r8, #4 @ loop counter bne 1b @ jump back if we're done with the loop +2: pop {{r4-r8}} bx lr @@ -90,6 +103,18 @@ agb_arm_func \fn_name str r4, [r1], #4 @ store the new value, and increment the pointer .endm + +@ handle the non-multiple of 4 buffer size case + and r7, r8, #3 +1: + subs r7, r7, #1 + bmi 1f + + add_one_sample_same_modification + subs r8, r8, #1 + beq 2f + b 1b + 1: .rept 4 add_one_sample_same_modification @@ -99,6 +124,7 @@ agb_arm_func \fn_name subs r8, r8, #4 @ loop counter bne 1b @ jump back if we're done with the loop +2: pop {{r4-r8}} bx lr From 8191de35460753ba294ddaa3eb5c1f74d137e95f Mon Sep 17 00:00:00 2001 From: Gwilym Inzani Date: Sat, 15 Jul 2023 21:07:55 +0100 Subject: [PATCH 17/65] Try a pure rust implementation to allow for looping correctly --- agb/src/sound/mixer/sw_mixer.rs | 87 ++++++++++++--------------------- tracker/agb-xm-core/src/lib.rs | 11 +---- 2 files changed, 32 insertions(+), 66 deletions(-) diff --git a/agb/src/sound/mixer/sw_mixer.rs b/agb/src/sound/mixer/sw_mixer.rs index cc9b2f15..ae411fb0 100644 --- a/agb/src/sound/mixer/sw_mixer.rs +++ b/agb/src/sound/mixer/sw_mixer.rs @@ -412,65 +412,31 @@ impl MixerBuffer { working_buffer: &mut [Num], channels: impl Iterator, ) { + working_buffer.fill(0.into()); + let mut channels = channels .filter(|channel| !channel.is_done) .filter_map(|channel| { let playback_speed = if channel.is_stereo { + if (channel.pos + 2 * self.frequency.buffer_size() as u32).floor() + >= channel.data.len() as u32 + { + if channel.should_loop { + channel.pos = 0.into(); + } else { + channel.is_done = true; + return None; + } + } + 2.into() } else { channel.playback_speed }; - if (channel.pos + playback_speed * self.frequency.buffer_size() as u32).floor() - >= 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 { - unsafe { - agb_rs__mixer_add_stereo_first( - channel.data.as_ptr().add(channel.pos.floor() as usize), - working_buffer.as_mut_ptr(), - channel.volume, - self.frequency.buffer_size(), - ); - } - } else { - let right_amount = ((channel.panning + 1) / 2) * channel.volume; - 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, - self.frequency.buffer_size(), - ); - } - } - } else { - working_buffer.fill(0.into()); - } - - channel.pos += playback_speed * self.frequency.buffer_size() as u32; - } else { - working_buffer.fill(0.into()); - } - for (channel, playback_speed) in channels { if channel.volume != 0.into() { if channel.is_stereo { @@ -486,15 +452,24 @@ impl MixerBuffer { let right_amount = ((channel.panning + 1) / 2) * channel.volume; 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, - self.frequency.buffer_size(), - ); + let channel_len = Num::::new(channel.data.len() as u32); + + 'outer: for i in 0..self.frequency.buffer_size() { + while channel.pos >= channel_len { + if channel.should_loop { + channel.pos -= channel_len; + } else { + channel.is_done = true; + break 'outer; + } + } + + let value = channel.data[channel.pos.floor() as usize] as i8 as i16; + + working_buffer[2 * i] += left_amount * value; + working_buffer[2 * i + 1] += right_amount * value; + + channel.pos += playback_speed; } } } diff --git a/tracker/agb-xm-core/src/lib.rs b/tracker/agb-xm-core/src/lib.rs index 073988a7..9e09c095 100644 --- a/tracker/agb-xm-core/src/lib.rs +++ b/tracker/agb-xm-core/src/lib.rs @@ -67,7 +67,7 @@ pub fn parse_module(module: &Module) -> TokenStream { let relative_note = sample.relative_note; let volume = sample.volume as f64; - let mut sample = match &sample.data { + let sample = match &sample.data { SampleDataType::Depth8(depth8) => { depth8.iter().map(|value| *value as u8).collect::>() } @@ -77,15 +77,6 @@ pub fn parse_module(module: &Module) -> TokenStream { .collect::>(), }; - if should_loop { - sample.append(&mut sample.clone()); - sample.append(&mut sample.clone()); - sample.append(&mut sample.clone()); - sample.append(&mut sample.clone()); - sample.append(&mut sample.clone()); - sample.append(&mut sample.clone()); - } - instruments_map.insert((instrument_index, sample_index), samples.len()); samples.push(SampleData { data: sample, From d4b2a2bc1a5f018a18a277d44cfaf162407f94c3 Mon Sep 17 00:00:00 2001 From: Gwilym Inzani Date: Sun, 16 Jul 2023 20:37:48 +0100 Subject: [PATCH 18/65] Fix crackly audio --- agb/src/sound/mixer/sw_mixer.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/agb/src/sound/mixer/sw_mixer.rs b/agb/src/sound/mixer/sw_mixer.rs index ae411fb0..bebd2129 100644 --- a/agb/src/sound/mixer/sw_mixer.rs +++ b/agb/src/sound/mixer/sw_mixer.rs @@ -414,7 +414,7 @@ impl MixerBuffer { ) { working_buffer.fill(0.into()); - let mut channels = channels + let channels = channels .filter(|channel| !channel.is_done) .filter_map(|channel| { let playback_speed = if channel.is_stereo { @@ -448,6 +448,8 @@ impl MixerBuffer { self.frequency.buffer_size(), ); } + + channel.pos += 2 * self.frequency.buffer_size() as u32; } else { let right_amount = ((channel.panning + 1) / 2) * channel.volume; let left_amount = ((-channel.panning + 1) / 2) * channel.volume; @@ -455,7 +457,7 @@ impl MixerBuffer { let channel_len = Num::::new(channel.data.len() as u32); 'outer: for i in 0..self.frequency.buffer_size() { - while channel.pos >= channel_len { + if channel.pos >= channel_len { if channel.should_loop { channel.pos -= channel_len; } else { @@ -466,15 +468,13 @@ impl MixerBuffer { let value = channel.data[channel.pos.floor() as usize] as i8 as i16; - working_buffer[2 * i] += left_amount * value; - working_buffer[2 * i + 1] += right_amount * value; + working_buffer[2 * i] += right_amount * value; + working_buffer[2 * i + 1] += left_amount * value; channel.pos += playback_speed; } } } - - channel.pos += playback_speed * self.frequency.buffer_size() as u32; } let write_buffer = free(|cs| self.state.borrow(cs).borrow_mut().active_advanced()); From 938f05f8d1fc9eb7623459077e6fddfce9c554dc Mon Sep 17 00:00:00 2001 From: Gwilym Inzani Date: Sun, 16 Jul 2023 20:52:29 +0100 Subject: [PATCH 19/65] Compile in release and make actually safe --- agb/src/sound/mixer/sw_mixer.rs | 46 +++++++++++++++++++-------------- tracker/agb-tracker/Cargo.toml | 11 +++++++- 2 files changed, 36 insertions(+), 21 deletions(-) diff --git a/agb/src/sound/mixer/sw_mixer.rs b/agb/src/sound/mixer/sw_mixer.rs index bebd2129..46951bb3 100644 --- a/agb/src/sound/mixer/sw_mixer.rs +++ b/agb/src/sound/mixer/sw_mixer.rs @@ -414,10 +414,9 @@ impl MixerBuffer { ) { working_buffer.fill(0.into()); - let channels = channels - .filter(|channel| !channel.is_done) - .filter_map(|channel| { - let playback_speed = if channel.is_stereo { + for channel in channels.filter(|channel| !channel.is_done) { + if channel.volume != 0.into() { + if channel.is_stereo { if (channel.pos + 2 * self.frequency.buffer_size() as u32).floor() >= channel.data.len() as u32 { @@ -425,21 +424,10 @@ impl MixerBuffer { channel.pos = 0.into(); } else { channel.is_done = true; - return None; + continue; } } - 2.into() - } else { - channel.playback_speed - }; - - Some((channel, playback_speed)) - }); - - for (channel, playback_speed) in channels { - if channel.volume != 0.into() { - if channel.is_stereo { unsafe { agb_rs__mixer_add_stereo( channel.data.as_ptr().add(channel.pos.floor() as usize), @@ -455,6 +443,22 @@ impl MixerBuffer { let left_amount = ((-channel.panning + 1) / 2) * channel.volume; let channel_len = Num::::new(channel.data.len() as u32); + let mut playback_speed = channel.playback_speed; + + while playback_speed >= channel_len { + 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); 'outer: for i in 0..self.frequency.buffer_size() { if channel.pos >= channel_len { @@ -466,11 +470,13 @@ impl MixerBuffer { } } - let value = channel.data[channel.pos.floor() as usize] as i8 as i16; - - working_buffer[2 * i] += right_amount * value; - working_buffer[2 * i + 1] += left_amount * value; + // SAFETY: channel.pos < channel_len by the above if statement and the fact we reduce the playback speed + let value = + unsafe { *channel.data.get_unchecked(channel.pos.floor() as usize) } + as i8 as i32; + // SAFETY: working buffer length = self.frequency.buffer_size() + unsafe { *working_buffer_i32.get_unchecked_mut(i) += value * mul_amount }; channel.pos += playback_speed; } } diff --git a/tracker/agb-tracker/Cargo.toml b/tracker/agb-tracker/Cargo.toml index 7da1029d..28b394ac 100644 --- a/tracker/agb-tracker/Cargo.toml +++ b/tracker/agb-tracker/Cargo.toml @@ -13,4 +13,13 @@ xm = ["dep:agb_xm"] [dependencies] agb_xm = { version = "0.15.0", path = "../agb-xm", optional = true } agb = { version = "0.15.0", path = "../../agb" } -agb_tracker_interop = { version = "0.15.0", path = "../agb-tracker-interop", default-features = false } \ No newline at end of file +agb_tracker_interop = { version = "0.15.0", path = "../agb-tracker-interop", default-features = false } + +[profile.dev] +opt-level = 3 +debug = true + +[profile.release] +opt-level = 3 +lto = "fat" +debug = true From d929a1689aa12b8d1374c2b0141859e15e328ac5 Mon Sep 17 00:00:00 2001 From: Gwilym Inzani Date: Sun, 16 Jul 2023 20:55:28 +0100 Subject: [PATCH 20/65] Extract methods --- agb/src/sound/mixer/sw_mixer.rs | 129 +++++++++++++++++--------------- 1 file changed, 68 insertions(+), 61 deletions(-) diff --git a/agb/src/sound/mixer/sw_mixer.rs b/agb/src/sound/mixer/sw_mixer.rs index 46951bb3..2551ab71 100644 --- a/agb/src/sound/mixer/sw_mixer.rs +++ b/agb/src/sound/mixer/sw_mixer.rs @@ -1,5 +1,6 @@ use core::cell::RefCell; use core::marker::PhantomData; +use core::ops::ControlFlow; use core::pin::Pin; use alloc::boxed::Box; @@ -417,68 +418,9 @@ impl MixerBuffer { for channel in channels.filter(|channel| !channel.is_done) { if channel.volume != 0.into() { if channel.is_stereo { - if (channel.pos + 2 * self.frequency.buffer_size() as u32).floor() - >= channel.data.len() as u32 - { - if channel.should_loop { - channel.pos = 0.into(); - } else { - channel.is_done = true; - continue; - } - } - - unsafe { - agb_rs__mixer_add_stereo( - channel.data.as_ptr().add(channel.pos.floor() as usize), - working_buffer.as_mut_ptr(), - channel.volume, - self.frequency.buffer_size(), - ); - } - - channel.pos += 2 * self.frequency.buffer_size() as u32; + self.write_stereo(channel, working_buffer); } else { - let right_amount = ((channel.panning + 1) / 2) * channel.volume; - let left_amount = ((-channel.panning + 1) / 2) * channel.volume; - - let channel_len = Num::::new(channel.data.len() as u32); - let mut playback_speed = channel.playback_speed; - - while playback_speed >= channel_len { - 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); - - 'outer: for i in 0..self.frequency.buffer_size() { - if channel.pos >= channel_len { - if channel.should_loop { - channel.pos -= channel_len; - } else { - channel.is_done = true; - break 'outer; - } - } - - // SAFETY: channel.pos < channel_len by the above if statement and the fact we reduce the playback speed - let value = - unsafe { *channel.data.get_unchecked(channel.pos.floor() as usize) } - as i8 as i32; - - // SAFETY: working buffer length = self.frequency.buffer_size() - unsafe { *working_buffer_i32.get_unchecked_mut(i) += value * mul_amount }; - channel.pos += playback_speed; - } + self.write_mono(channel, working_buffer); } } } @@ -493,6 +435,71 @@ impl MixerBuffer { ); } } + + fn write_stereo(&self, channel: &mut SoundChannel, working_buffer: &mut [Num]) { + if (channel.pos + 2 * self.frequency.buffer_size() as u32).floor() + >= channel.data.len() as u32 + { + if channel.should_loop { + channel.pos = 0.into(); + } else { + channel.is_done = true; + return; + } + } + unsafe { + agb_rs__mixer_add_stereo( + channel.data.as_ptr().add(channel.pos.floor() as usize), + working_buffer.as_mut_ptr(), + channel.volume, + self.frequency.buffer_size(), + ); + } + + channel.pos += 2 * self.frequency.buffer_size() as u32; + } + + fn write_mono(&self, channel: &mut SoundChannel, working_buffer: &mut [Num]) { + let right_amount = ((channel.panning + 1) / 2) * channel.volume; + let left_amount = ((-channel.panning + 1) / 2) * channel.volume; + + let channel_len = Num::::new(channel.data.len() as u32); + let mut playback_speed = channel.playback_speed; + + while playback_speed >= channel_len { + 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); + + 'outer: for i in 0..self.frequency.buffer_size() { + if channel.pos >= channel_len { + if channel.should_loop { + channel.pos -= channel_len; + } else { + channel.is_done = true; + break 'outer; + } + } + + // SAFETY: channel.pos < channel_len by the above if statement and the fact we reduce the playback speed + let value = + unsafe { *channel.data.get_unchecked(channel.pos.floor() as usize) } as i8 as i32; + + // SAFETY: working buffer length = self.frequency.buffer_size() + unsafe { *working_buffer_i32.get_unchecked_mut(i) += value * mul_amount }; + channel.pos += playback_speed; + } + } } #[cfg(test)] From c66f495cc763e0e84c090f7f09e903368708070a Mon Sep 17 00:00:00 2001 From: Gwilym Inzani Date: Sun, 16 Jul 2023 20:56:05 +0100 Subject: [PATCH 21/65] Don't need loop labels any more --- agb/src/sound/mixer/sw_mixer.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/agb/src/sound/mixer/sw_mixer.rs b/agb/src/sound/mixer/sw_mixer.rs index 2551ab71..6a14b5b3 100644 --- a/agb/src/sound/mixer/sw_mixer.rs +++ b/agb/src/sound/mixer/sw_mixer.rs @@ -481,13 +481,13 @@ impl MixerBuffer { let mul_amount = ((left_amount.to_raw() as i32) << 16) | (right_amount.to_raw() as i32 & 0x0000ffff); - 'outer: for i in 0..self.frequency.buffer_size() { + for i in 0..self.frequency.buffer_size() { if channel.pos >= channel_len { if channel.should_loop { channel.pos -= channel_len; } else { channel.is_done = true; - break 'outer; + break; } } From a0be2a333ea806b18b1f0d4e5aa0754f8500917c Mon Sep 17 00:00:00 2001 From: Gwilym Inzani Date: Sun, 16 Jul 2023 21:12:12 +0100 Subject: [PATCH 22/65] Attempt to calculate the speed to play more correctly --- tracker/agb-tracker-interop/src/lib.rs | 11 ++++++++--- tracker/agb-tracker/src/lib.rs | 26 +++++++++++++++++++------- tracker/agb-xm-core/src/lib.rs | 7 ++++++- 3 files changed, 33 insertions(+), 11 deletions(-) diff --git a/tracker/agb-tracker-interop/src/lib.rs b/tracker/agb-tracker-interop/src/lib.rs index 47b5bd04..23d82b5b 100644 --- a/tracker/agb-tracker-interop/src/lib.rs +++ b/tracker/agb-tracker-interop/src/lib.rs @@ -10,7 +10,8 @@ pub struct Track<'a> { pub patterns_to_play: &'a [usize], pub num_channels: usize, - pub frames_per_step: u16, + pub frames_per_tick: Num, + pub ticks_per_step: u16, } #[derive(Debug)] @@ -45,11 +46,14 @@ impl<'a> quote::ToTokens for Track<'a> { samples, pattern_data, patterns, - frames_per_step, + 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),*]; @@ -63,8 +67,9 @@ impl<'a> quote::ToTokens for Track<'a> { patterns: PATTERNS, patterns_to_play: PATTERNS_TO_PLAY, - frames_per_step: #frames_per_step, + frames_per_tick: agb_tracker::__private::Num::from_raw(#frames_per_tick), num_channels: #num_channels, + ticks_per_step: #ticks_per_step, } } }) diff --git a/tracker/agb-tracker/src/lib.rs b/tracker/agb-tracker/src/lib.rs index 3b4c18d2..b84ed05d 100644 --- a/tracker/agb-tracker/src/lib.rs +++ b/tracker/agb-tracker/src/lib.rs @@ -9,7 +9,10 @@ extern crate alloc; use alloc::vec::Vec; -use agb::sound::mixer::{ChannelId, Mixer, SoundChannel}; +use agb::{ + fixnum::Num, + sound::mixer::{ChannelId, Mixer, SoundChannel}, +}; #[cfg(feature = "xm")] pub use agb_xm::import_xm; @@ -25,7 +28,9 @@ pub struct Tracker { track: &'static Track<'static>, channels: Vec>, - step: u16, + frame: Num, + tick: u16, + current_row: usize, current_pattern: usize, } @@ -39,14 +44,16 @@ impl Tracker { track, channels, - step: 0, + frame: 0.into(), + tick: 0, + current_row: 0, current_pattern: 0, } } pub fn step(&mut self, mixer: &mut Mixer) { - if self.step != 0 { + if self.tick != 0 { self.increment_step(); return; // TODO: volume / pitch slides } @@ -113,9 +120,14 @@ impl Tracker { } fn increment_step(&mut self) { - self.step += 1; + self.frame += 1; - if self.step == self.track.frames_per_step { + 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 @@ -129,7 +141,7 @@ impl Tracker { } } - self.step = 0; + self.tick = 0; } } } diff --git a/tracker/agb-xm-core/src/lib.rs b/tracker/agb-xm-core/src/lib.rs index 9e09c095..5326e8ae 100644 --- a/tracker/agb-xm-core/src/lib.rs +++ b/tracker/agb-xm-core/src/lib.rs @@ -202,6 +202,10 @@ pub fn parse_module(module: &Module) -> TokenStream { .map(|order| *order as usize) .collect::>(); + // Number 150 here deduced experimentally + let frames_per_tick = Num::::new(150) / module.default_bpm; + let ticks_per_step = module.default_tempo; + let interop = agb_tracker_interop::Track { samples: &samples, pattern_data: &pattern_data, @@ -209,7 +213,8 @@ pub fn parse_module(module: &Module) -> TokenStream { num_channels: module.get_num_channels(), patterns_to_play: &patterns_to_play, - frames_per_step: 4, // TODO calculate this correctly + frames_per_tick, + ticks_per_step, }; quote!(#interop) From 1dd4c9fb83cbe7a3ea927220e13f7456d99fb8da Mon Sep 17 00:00:00 2001 From: Gwilym Inzani Date: Sun, 16 Jul 2023 22:57:12 +0100 Subject: [PATCH 23/65] Add a concept of a restart point --- agb/src/sound/mixer/mod.rs | 17 +++++++++++++++++ agb/src/sound/mixer/sw_mixer.rs | 6 +++--- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/agb/src/sound/mixer/mod.rs b/agb/src/sound/mixer/mod.rs index 0bff6c50..1a7fe1c4 100644 --- a/agb/src/sound/mixer/mod.rs +++ b/agb/src/sound/mixer/mod.rs @@ -226,6 +226,7 @@ pub struct SoundChannel { data: &'static [u8], pos: Num, should_loop: bool, + restart_point: Num, playback_speed: Num, volume: Num, // between 0 and 1 @@ -276,6 +277,7 @@ impl SoundChannel { priority: SoundPriority::Low, volume: 1.into(), is_stereo: false, + restart_point: 0.into(), } } @@ -319,6 +321,7 @@ impl SoundChannel { priority: SoundPriority::High, volume: 1.into(), is_stereo: false, + restart_point: 0.into(), } } @@ -330,6 +333,20 @@ impl SoundChannel { self } + /// Sets the point at which the sample should restart once it loops. Does nothing + /// unless you also call [`should_loop()`]. + /// + /// Useful if your song has an introduction or similar. + #[inline(always)] + pub fn restart_point(&mut self, restart_point: impl Into>) -> &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 /// to 1 with values between 0 and 1 being slower above 1 being faster. /// diff --git a/agb/src/sound/mixer/sw_mixer.rs b/agb/src/sound/mixer/sw_mixer.rs index 6a14b5b3..55617aac 100644 --- a/agb/src/sound/mixer/sw_mixer.rs +++ b/agb/src/sound/mixer/sw_mixer.rs @@ -441,7 +441,7 @@ impl MixerBuffer { >= channel.data.len() as u32 { if channel.should_loop { - channel.pos = 0.into(); + channel.pos = channel.restart_point * 2; } else { channel.is_done = true; return; @@ -466,7 +466,7 @@ impl MixerBuffer { let channel_len = Num::::new(channel.data.len() as u32); let mut playback_speed = channel.playback_speed; - while playback_speed >= channel_len { + while playback_speed >= channel_len - channel.restart_point { playback_speed -= channel_len; } @@ -484,7 +484,7 @@ impl MixerBuffer { for i in 0..self.frequency.buffer_size() { if channel.pos >= channel_len { if channel.should_loop { - channel.pos -= channel_len; + channel.pos -= channel_len + channel.restart_point; } else { channel.is_done = true; break; From 7861571a961479255d55df6b803db09ac3462f5b Mon Sep 17 00:00:00 2001 From: Gwilym Inzani Date: Sun, 16 Jul 2023 23:12:42 +0100 Subject: [PATCH 24/65] Correctly track restart point --- agb/src/sound/mixer/sw_mixer.rs | 2 +- tracker/agb-tracker-interop/src/lib.rs | 4 +++- tracker/agb-tracker/src/lib.rs | 3 ++- tracker/agb-xm-core/src/lib.rs | 18 +++++++++++++++--- 4 files changed, 21 insertions(+), 6 deletions(-) diff --git a/agb/src/sound/mixer/sw_mixer.rs b/agb/src/sound/mixer/sw_mixer.rs index 55617aac..0e71faeb 100644 --- a/agb/src/sound/mixer/sw_mixer.rs +++ b/agb/src/sound/mixer/sw_mixer.rs @@ -484,7 +484,7 @@ impl MixerBuffer { for i in 0..self.frequency.buffer_size() { if channel.pos >= channel_len { if channel.should_loop { - channel.pos -= channel_len + channel.restart_point; + channel.pos -= channel_len - channel.restart_point; } else { channel.is_done = true; break; diff --git a/tracker/agb-tracker-interop/src/lib.rs b/tracker/agb-tracker-interop/src/lib.rs index 23d82b5b..e8502a50 100644 --- a/tracker/agb-tracker-interop/src/lib.rs +++ b/tracker/agb-tracker-interop/src/lib.rs @@ -18,6 +18,7 @@ pub struct Track<'a> { pub struct Sample<'a> { pub data: &'a [u8], pub should_loop: bool, + pub restart_point: u32, } #[derive(Debug)] @@ -95,6 +96,7 @@ impl<'a> quote::ToTokens for Sample<'a> { let self_as_u8s: Vec<_> = self.data.iter().map(|i| *i as u8).collect(); let samples = ByteString(&self_as_u8s); let should_loop = self.should_loop; + let restart_point = self.restart_point; tokens.append_all(quote! { { @@ -102,7 +104,7 @@ impl<'a> quote::ToTokens for Sample<'a> { struct AlignmentWrapper([u8; N]); const SAMPLE_DATA: &[u8] = &AlignmentWrapper(*#samples).0; - agb_tracker::__private::agb_tracker_interop::Sample { data: SAMPLE_DATA, should_loop: #should_loop } + agb_tracker::__private::agb_tracker_interop::Sample { data: SAMPLE_DATA, should_loop: #should_loop, restart_point: #restart_point } } }); } diff --git a/tracker/agb-tracker/src/lib.rs b/tracker/agb-tracker/src/lib.rs index b84ed05d..ab88bd32 100644 --- a/tracker/agb-tracker/src/lib.rs +++ b/tracker/agb-tracker/src/lib.rs @@ -106,7 +106,8 @@ impl Tracker { new_channel .panning(pattern_slot.panning) .volume(pattern_slot.volume) - .playback(pattern_slot.speed); + .playback(pattern_slot.speed) + .restart_point(sample.restart_point); if sample.should_loop { new_channel.should_loop(); diff --git a/tracker/agb-xm-core/src/lib.rs b/tracker/agb-xm-core/src/lib.rs index 5326e8ae..217e431a 100644 --- a/tracker/agb-xm-core/src/lib.rs +++ b/tracker/agb-xm-core/src/lib.rs @@ -54,6 +54,7 @@ pub fn parse_module(module: &Module) -> TokenStream { fine_tune: f64, relative_note: i8, volume: f64, + restart_point: u32, } let mut samples = vec![]; @@ -66,14 +67,23 @@ pub fn parse_module(module: &Module) -> TokenStream { let fine_tune = sample.finetune as f64; let relative_note = sample.relative_note; let volume = sample.volume as f64; + 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 sample = match &sample.data { - SampleDataType::Depth8(depth8) => { - depth8.iter().map(|value| *value as u8).collect::>() - } + SampleDataType::Depth8(depth8) => depth8 + .iter() + .map(|value| *value as u8) + .take(sample_len) + .collect::>(), SampleDataType::Depth16(depth16) => depth16 .iter() .map(|sample| (sample >> 8) as i8 as u8) + .take(sample_len) .collect::>(), }; @@ -84,6 +94,7 @@ pub fn parse_module(module: &Module) -> TokenStream { fine_tune, relative_note, volume, + restart_point, }); } } @@ -193,6 +204,7 @@ pub fn parse_module(module: &Module) -> TokenStream { .map(|sample| agb_tracker_interop::Sample { data: &sample.data, should_loop: sample.should_loop, + restart_point: sample.restart_point, }) .collect(); From aa635e9aa6423872d0d6a3c82625bb80be999c38 Mon Sep 17 00:00:00 2001 From: Gwilym Inzani Date: Sun, 16 Jul 2023 23:57:11 +0100 Subject: [PATCH 25/65] Refactor to effects --- tracker/agb-tracker-interop/src/lib.rs | 57 +++++++-- tracker/agb-tracker/src/lib.rs | 161 +++++++++++++++++-------- tracker/agb-xm-core/src/lib.rs | 71 ++++------- 3 files changed, 184 insertions(+), 105 deletions(-) diff --git a/tracker/agb-tracker-interop/src/lib.rs b/tracker/agb-tracker-interop/src/lib.rs index e8502a50..365a4020 100644 --- a/tracker/agb-tracker-interop/src/lib.rs +++ b/tracker/agb-tracker-interop/src/lib.rs @@ -29,14 +29,24 @@ pub struct Pattern { #[derive(Debug)] pub struct PatternSlot { - pub volume: Num, pub speed: Num, - pub panning: Num, pub sample: usize, + pub effect1: PatternEffect, + pub effect2: PatternEffect, } -pub const SKIP_SLOT: usize = 277; -pub const STOP_CHANNEL: usize = 278; +#[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, Num), + Panning(Num), + Volume(Num), +} #[cfg(feature = "quote")] impl<'a> quote::ToTokens for Track<'a> { @@ -116,22 +126,20 @@ impl quote::ToTokens for PatternSlot { use quote::{quote, TokenStreamExt}; let PatternSlot { - volume, speed, - panning, sample, + effect1, + effect2, } = &self; - let volume = volume.to_raw(); let speed = speed.to_raw(); - let panning = panning.to_raw(); tokens.append_all(quote! { agb_tracker::__private::agb_tracker_interop::PatternSlot { - volume: agb_tracker::__private::Num::from_raw(#volume), speed: agb_tracker::__private::Num::from_raw(#speed), - panning: agb_tracker::__private::Num::from_raw(#panning), sample: #sample, + effect1: #effect1, + effect2: #effect2, } }); } @@ -155,3 +163,32 @@ impl quote::ToTokens for Pattern { }) } } + +#[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))} + } + }; + + tokens.append_all(quote! { + agb_tracker::__private::agb_tracker_interop::PatternEffect::#type_bit + }); + } +} diff --git a/tracker/agb-tracker/src/lib.rs b/tracker/agb-tracker/src/lib.rs index ab88bd32..a0fa3cd8 100644 --- a/tracker/agb-tracker/src/lib.rs +++ b/tracker/agb-tracker/src/lib.rs @@ -7,6 +7,7 @@ extern crate alloc; +use agb_tracker_interop::{PatternEffect, Sample}; use alloc::vec::Vec; use agb::{ @@ -26,7 +27,7 @@ pub use agb_tracker_interop::Track; pub struct Tracker { track: &'static Track<'static>, - channels: Vec>, + channels: Vec, frame: Num, tick: u16, @@ -35,10 +36,14 @@ pub struct Tracker { current_pattern: usize, } +struct TrackerChannel { + channel_id: Option, +} + impl Tracker { pub fn new(track: &'static Track<'static>) -> Self { let mut channels = Vec::new(); - channels.resize_with(track.num_channels, || None); + channels.resize_with(track.num_channels, || TrackerChannel { channel_id: None }); Self { track, @@ -66,55 +71,62 @@ impl Tracker { let pattern_slots = &self.track.pattern_data[pattern_data_pos..pattern_data_pos + self.track.num_channels]; - for (channel_id, pattern_slot) in self.channels.iter_mut().zip(pattern_slots) { - if pattern_slot.sample == agb_tracker_interop::SKIP_SLOT { - // completely skip - } else if pattern_slot.sample == agb_tracker_interop::STOP_CHANNEL { - if let Some(channel) = channel_id - .take() - .and_then(|channel_id| mixer.channel(&channel_id)) - { - channel.stop(); - } - } else if pattern_slot.sample == 0 { - if let Some(channel) = channel_id - .as_ref() - .and_then(|channel_id| mixer.channel(channel_id)) - { - if pattern_slot.volume != 0.into() { - channel.volume(pattern_slot.volume); - } - - if pattern_slot.panning != 0.into() { - channel.panning(pattern_slot.panning); - } - - if pattern_slot.speed != 0.into() { - channel.playback(pattern_slot.speed); - } - } - } else { - if let Some(channel) = channel_id - .take() - .and_then(|channel_id| mixer.channel(&channel_id)) - { - channel.stop(); - } - + for (channel, pattern_slot) in self.channels.iter_mut().zip(pattern_slots) { + if pattern_slot.sample != 0 { let sample = &self.track.samples[pattern_slot.sample - 1]; - let mut new_channel = SoundChannel::new(sample.data); - new_channel - .panning(pattern_slot.panning) - .volume(pattern_slot.volume) - .playback(pattern_slot.speed) - .restart_point(sample.restart_point); - - if sample.should_loop { - new_channel.should_loop(); - } - - *channel_id = mixer.play_sound(new_channel); + channel.play_sound(mixer, sample); } + + channel.apply_effect(mixer, &pattern_slot.effect1, self.tick, pattern_slot.speed); + channel.apply_effect(mixer, &pattern_slot.effect2, self.tick, pattern_slot.speed); + // if pattern_slot.sample == agb_tracker_interop::SKIP_SLOT { + // // completely skip + // } else if pattern_slot.sample == agb_tracker_interop::STOP_CHANNEL { + // if let Some(channel) = channel_id + // .take() + // .and_then(|channel_id| mixer.channel(&channel_id)) + // { + // channel.stop(); + // } + // } else if pattern_slot.sample == 0 { + // if let Some(channel) = channel_id + // .as_ref() + // .and_then(|channel_id| mixer.channel(channel_id)) + // { + // if pattern_slot.volume != 0.into() { + // channel.volume(pattern_slot.volume); + // } + + // if pattern_slot.panning != 0.into() { + // channel.panning(pattern_slot.panning); + // } + + // if pattern_slot.speed != 0.into() { + // channel.playback(pattern_slot.speed); + // } + // } + // } else { + // if let Some(channel) = channel_id + // .take() + // .and_then(|channel_id| mixer.channel(&channel_id)) + // { + // channel.stop(); + // } + + // let sample = &self.track.samples[pattern_slot.sample - 1]; + // let mut new_channel = SoundChannel::new(sample.data); + // new_channel + // .panning(pattern_slot.panning) + // .volume(pattern_slot.volume) + // .playback(pattern_slot.speed) + // .restart_point(sample.restart_point); + + // if sample.should_loop { + // new_channel.should_loop(); + // } + + // *channel_id = mixer.play_sound(new_channel); + // } } self.increment_step(); @@ -147,6 +159,57 @@ impl Tracker { } } +impl TrackerChannel { + fn play_sound(&mut self, mixer: &mut Mixer<'_>, sample: &Sample<'static>) { + self.channel_id + .take() + .and_then(|channel_id| mixer.channel(&channel_id)) + .map(|channel| channel.stop()); + + let mut new_channel = SoundChannel::new(sample.data); + + if sample.should_loop { + new_channel + .should_loop() + .restart_point(sample.restart_point); + } + + self.channel_id = mixer.play_sound(new_channel) + } + + fn apply_effect( + &mut self, + mixer: &mut Mixer<'_>, + effect: &PatternEffect, + tick: u16, + speed: Num, + ) { + if let Some(channel) = self + .channel_id + .as_ref() + .and_then(|channel_id| mixer.channel(&channel_id)) + { + if speed != 0.into() { + channel.playback(speed); + } + + match effect { + PatternEffect::None => {} + PatternEffect::Stop => { + channel.stop(); + } + PatternEffect::Arpeggio(_, _) => todo!(), + PatternEffect::Panning(panning) => { + channel.panning(*panning); + } + PatternEffect::Volume(volume) => { + channel.volume(*volume); + } + } + } + } +} + #[cfg(test)] #[agb::entry] fn main(gba: agb::Gba) -> ! { diff --git a/tracker/agb-xm-core/src/lib.rs b/tracker/agb-xm-core/src/lib.rs index 217e431a..f9bdf5b0 100644 --- a/tracker/agb-xm-core/src/lib.rs +++ b/tracker/agb-xm-core/src/lib.rs @@ -1,5 +1,6 @@ use std::{collections::HashMap, error::Error, fs, path::Path}; +use agb_tracker_interop::PatternEffect; use proc_macro2::TokenStream; use proc_macro_error::abort; @@ -53,7 +54,6 @@ pub fn parse_module(module: &Module) -> TokenStream { should_loop: bool, fine_tune: f64, relative_note: i8, - volume: f64, restart_point: u32, } @@ -66,7 +66,6 @@ pub fn parse_module(module: &Module) -> TokenStream { let should_loop = !matches!(sample.flags, LoopType::No); let fine_tune = sample.finetune as f64; let relative_note = sample.relative_note; - let volume = sample.volume as f64; let restart_point = sample.loop_start; let sample_len = if sample.loop_length > 0 { (sample.loop_length + sample.loop_start) as usize @@ -93,7 +92,6 @@ pub fn parse_module(module: &Module) -> TokenStream { should_loop, fine_tune, relative_note, - volume, restart_point, }); } @@ -125,50 +123,35 @@ pub fn parse_module(module: &Module) -> TokenStream { } }; - let (mut volume, mut panning) = match slot.volume { - 0x10..=0x50 => (Some((slot.volume - 0x10) as f64 / 64.0), None), - 0xC0..=0xCF => ( - None, - Some(Num::new(slot.volume as i16 - (0xC0 + (0xCF - 0xC0) / 2)) / 64), + let mut effect1 = match slot.volume { + 0x10..=0x50 => { + PatternEffect::Volume(Num::new((slot.volume - 0x10) as i16) / 64) + } + 0xC0..=0xCF => PatternEffect::Panning( + Num::new(slot.volume as i16 - (0xC0 + (0xCF - 0xC0) / 2)) / 64, ), - _ => (None, Some(0.into())), + _ => PatternEffect::None, }; - if slot.effect_type == 0xC { - volume = Some(slot.effect_parameter as f64 / 255.0); - } + let effect2 = match slot.effect_type { + 0x8 => { + PatternEffect::Panning(Num::new(slot.effect_parameter as i16 - 128) / 128) + } + 0xC => PatternEffect::Volume(Num::new(slot.effect_parameter as i16) / 255), + _ => PatternEffect::None, + }; - if slot.effect_type == 0x8 { - panning = Some(Num::new(slot.effect_parameter as i16 - 128) / 128); + if matches!(slot.note, Note::KeyOff) { + effect1 = PatternEffect::Stop; } if sample == 0 { - if slot.volume == 0 && slot.effect_type == 0 { - pattern_data.push(agb_tracker_interop::PatternSlot { - volume: 0.into(), - speed: 0.into(), - panning: 0.into(), - sample: agb_tracker_interop::SKIP_SLOT, - }); - } else if matches!(slot.note, Note::KeyOff) || volume == Some(0.0) { - pattern_data.push(agb_tracker_interop::PatternSlot { - volume: 0.into(), - speed: 0.into(), - panning: 0.into(), - sample: agb_tracker_interop::STOP_CHANNEL, - }); - } else { - let volume: Num = - Num::from_raw((volume.unwrap_or(0.into()) * (1 << 4) as f64) as i16); - let panning = panning.unwrap_or(0.into()); - - pattern_data.push(agb_tracker_interop::PatternSlot { - volume, - speed: 0.into(), - panning, - sample: 0, - }); - } + pattern_data.push(agb_tracker_interop::PatternSlot { + speed: 0.into(), + sample: 0, + effect1, + effect2, + }); } else { let sample_played = &samples[sample - 1]; @@ -179,15 +162,11 @@ pub fn parse_module(module: &Module) -> TokenStream { module.frequency_type, ); - let overall_volume = volume.unwrap_or(1.into()) * sample_played.volume; - let volume: Num = - Num::from_raw((overall_volume * (1 << 4) as f64) as i16); - pattern_data.push(agb_tracker_interop::PatternSlot { - volume, speed, - panning: panning.unwrap_or(0.into()), sample, + effect1, + effect2, }); } } From 9b94b2a2cb2d2341b246c71108a54523edce0521 Mon Sep 17 00:00:00 2001 From: Gwilym Inzani Date: Sun, 16 Jul 2023 23:57:23 +0100 Subject: [PATCH 26/65] Remove commented code --- tracker/agb-tracker/src/lib.rs | 48 ---------------------------------- 1 file changed, 48 deletions(-) diff --git a/tracker/agb-tracker/src/lib.rs b/tracker/agb-tracker/src/lib.rs index a0fa3cd8..03b62893 100644 --- a/tracker/agb-tracker/src/lib.rs +++ b/tracker/agb-tracker/src/lib.rs @@ -79,54 +79,6 @@ impl Tracker { channel.apply_effect(mixer, &pattern_slot.effect1, self.tick, pattern_slot.speed); channel.apply_effect(mixer, &pattern_slot.effect2, self.tick, pattern_slot.speed); - // if pattern_slot.sample == agb_tracker_interop::SKIP_SLOT { - // // completely skip - // } else if pattern_slot.sample == agb_tracker_interop::STOP_CHANNEL { - // if let Some(channel) = channel_id - // .take() - // .and_then(|channel_id| mixer.channel(&channel_id)) - // { - // channel.stop(); - // } - // } else if pattern_slot.sample == 0 { - // if let Some(channel) = channel_id - // .as_ref() - // .and_then(|channel_id| mixer.channel(channel_id)) - // { - // if pattern_slot.volume != 0.into() { - // channel.volume(pattern_slot.volume); - // } - - // if pattern_slot.panning != 0.into() { - // channel.panning(pattern_slot.panning); - // } - - // if pattern_slot.speed != 0.into() { - // channel.playback(pattern_slot.speed); - // } - // } - // } else { - // if let Some(channel) = channel_id - // .take() - // .and_then(|channel_id| mixer.channel(&channel_id)) - // { - // channel.stop(); - // } - - // let sample = &self.track.samples[pattern_slot.sample - 1]; - // let mut new_channel = SoundChannel::new(sample.data); - // new_channel - // .panning(pattern_slot.panning) - // .volume(pattern_slot.volume) - // .playback(pattern_slot.speed) - // .restart_point(sample.restart_point); - - // if sample.should_loop { - // new_channel.should_loop(); - // } - - // *channel_id = mixer.play_sound(new_channel); - // } } self.increment_step(); From 203b1a4026742e58bd33e5d60bd5fee7e16c9e70 Mon Sep 17 00:00:00 2001 From: Gwilym Inzani Date: Mon, 17 Jul 2023 00:27:20 +0100 Subject: [PATCH 27/65] Actually make arpeggios work --- tracker/agb-tracker/src/lib.rs | 63 ++++++++++++++++++++++++---------- tracker/agb-xm-core/src/lib.rs | 57 +++++++++++++++++++++++++++--- 2 files changed, 97 insertions(+), 23 deletions(-) diff --git a/tracker/agb-tracker/src/lib.rs b/tracker/agb-tracker/src/lib.rs index 03b62893..204a9f25 100644 --- a/tracker/agb-tracker/src/lib.rs +++ b/tracker/agb-tracker/src/lib.rs @@ -31,6 +31,7 @@ pub struct Tracker { frame: Num, tick: u16, + first: bool, current_row: usize, current_pattern: usize, @@ -38,18 +39,23 @@ pub struct Tracker { struct TrackerChannel { channel_id: Option, + base_speed: Num, } impl Tracker { pub fn new(track: &'static Track<'static>) -> Self { let mut channels = Vec::new(); - channels.resize_with(track.num_channels, || TrackerChannel { channel_id: None }); + channels.resize_with(track.num_channels, || TrackerChannel { + channel_id: None, + base_speed: 0.into(), + }); Self { track, channels, frame: 0.into(), + first: true, tick: 0, current_row: 0, @@ -58,9 +64,8 @@ impl Tracker { } pub fn step(&mut self, mixer: &mut Mixer) { - if self.tick != 0 { - self.increment_step(); - return; // TODO: volume / pitch slides + if !self.increment_frame() { + return; } let pattern_to_play = self.track.patterns_to_play[self.current_pattern]; @@ -72,7 +77,7 @@ impl Tracker { &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 { + if pattern_slot.sample != 0 && self.tick == 0 { let sample = &self.track.samples[pattern_slot.sample - 1]; channel.play_sound(mixer, sample); } @@ -84,31 +89,42 @@ impl Tracker { self.increment_step(); } - fn increment_step(&mut self) { + 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.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_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; + if self.current_pattern >= self.track.patterns_to_play.len() { + self.current_pattern = 0; + } } + + self.tick = 0; } - self.tick = 0; + true + } else { + false } } + + fn increment_step(&mut self) {} } impl TrackerChannel { @@ -143,6 +159,7 @@ impl TrackerChannel { { if speed != 0.into() { channel.playback(speed); + self.base_speed = speed; } match effect { @@ -150,7 +167,17 @@ impl TrackerChannel { PatternEffect::Stop => { channel.stop(); } - PatternEffect::Arpeggio(_, _) => todo!(), + PatternEffect::Arpeggio(first, second) => { + let first: Num = first.change_base(); + let second: Num = second.change_base(); + + match tick % 3 { + 0 => channel.playback(self.base_speed), + 1 => channel.playback(self.base_speed + first), + 2 => channel.playback(self.base_speed + second), + _ => unreachable!(), + }; + } PatternEffect::Panning(panning) => { channel.panning(*panning); } diff --git a/tracker/agb-xm-core/src/lib.rs b/tracker/agb-xm-core/src/lib.rs index f9bdf5b0..742cae47 100644 --- a/tracker/agb-xm-core/src/lib.rs +++ b/tracker/agb-xm-core/src/lib.rs @@ -104,7 +104,11 @@ pub fn parse_module(module: &Module) -> TokenStream { let start_pos = pattern_data.len(); for row in pattern.iter() { - for slot in row { + let mut notes = vec![None; module.get_num_channels()]; + + for (i, slot) in row.iter().enumerate() { + let channel_number = i % module.get_num_channels(); + let sample = if slot.instrument == 0 { 0 } else { @@ -133,7 +137,54 @@ pub fn parse_module(module: &Module) -> TokenStream { _ => PatternEffect::None, }; + if matches!(slot.note, Note::KeyOff) { + effect1 = PatternEffect::Stop; + notes[channel_number] = None; + } else { + notes[channel_number] = Some(slot.note); + } + let effect2 = match slot.effect_type { + 0x0 => { + if slot.effect_parameter == 0 { + PatternEffect::None + } else if let Some(note) = notes[channel_number] { + let first_arpeggio = slot.effect_parameter >> 4; + let second_arpeggio = slot.effect_parameter & 0xF; + + let note_speed = note_to_speed(note, 0.0, 0, module.frequency_type); + + let note = note as u8; + let first_arpeggio: Note = (note + first_arpeggio) + .try_into() + .expect("Note out of bounds"); + let second_arpeggio: Note = (note + second_arpeggio) + .try_into() + .expect("Note out of bounds"); + + let first_arpeggio_speed = + note_to_speed(first_arpeggio, 0.0, 0, module.frequency_type); + let second_arpeggio_speed = + note_to_speed(second_arpeggio, 0.0, 0, module.frequency_type); + + let first_arpeggio_difference = first_arpeggio_speed - note_speed; + let second_arpeggio_difference = second_arpeggio_speed - note_speed; + + let first_arpeggio_difference = first_arpeggio_difference + .try_change_base() + .expect("Arpeggio difference too large"); + let second_arpeggio_difference = second_arpeggio_difference + .try_change_base() + .expect("Arpeggio difference too large"); + + PatternEffect::Arpeggio( + first_arpeggio_difference, + second_arpeggio_difference, + ) + } else { + PatternEffect::None + } + } 0x8 => { PatternEffect::Panning(Num::new(slot.effect_parameter as i16 - 128) / 128) } @@ -141,10 +192,6 @@ pub fn parse_module(module: &Module) -> TokenStream { _ => PatternEffect::None, }; - if matches!(slot.note, Note::KeyOff) { - effect1 = PatternEffect::Stop; - } - if sample == 0 { pattern_data.push(agb_tracker_interop::PatternSlot { speed: 0.into(), From 4d699e900047ab13711afba53632f2274515be4a Mon Sep 17 00:00:00 2001 From: Gwilym Inzani Date: Mon, 17 Jul 2023 00:45:58 +0100 Subject: [PATCH 28/65] Volume slides - ish --- tracker/agb-tracker-interop/src/lib.rs | 5 +++++ tracker/agb-tracker/src/lib.rs | 10 ++++++++++ tracker/agb-xm-core/src/lib.rs | 10 ++++++++++ 3 files changed, 25 insertions(+) diff --git a/tracker/agb-tracker-interop/src/lib.rs b/tracker/agb-tracker-interop/src/lib.rs index 365a4020..a1a094b7 100644 --- a/tracker/agb-tracker-interop/src/lib.rs +++ b/tracker/agb-tracker-interop/src/lib.rs @@ -46,6 +46,7 @@ pub enum PatternEffect { Arpeggio(Num, Num), Panning(Num), Volume(Num), + VolumeSlide(Num), } #[cfg(feature = "quote")] @@ -185,6 +186,10 @@ impl quote::ToTokens for PatternEffect { 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))} + } }; tokens.append_all(quote! { diff --git a/tracker/agb-tracker/src/lib.rs b/tracker/agb-tracker/src/lib.rs index 204a9f25..b1dfa5e9 100644 --- a/tracker/agb-tracker/src/lib.rs +++ b/tracker/agb-tracker/src/lib.rs @@ -40,6 +40,7 @@ pub struct Tracker { struct TrackerChannel { channel_id: Option, base_speed: Num, + volume: Num, } impl Tracker { @@ -48,6 +49,7 @@ impl Tracker { channels.resize_with(track.num_channels, || TrackerChannel { channel_id: None, base_speed: 0.into(), + volume: 0.into(), }); Self { @@ -183,6 +185,14 @@ impl TrackerChannel { } PatternEffect::Volume(volume) => { channel.volume(*volume); + self.volume = *volume; + } + PatternEffect::VolumeSlide(amount) => { + self.volume += *amount; + if self.volume < 0.into() { + self.volume = 0.into(); + } + channel.volume(self.volume); } } } diff --git a/tracker/agb-xm-core/src/lib.rs b/tracker/agb-xm-core/src/lib.rs index 742cae47..ffc7882b 100644 --- a/tracker/agb-xm-core/src/lib.rs +++ b/tracker/agb-xm-core/src/lib.rs @@ -188,6 +188,16 @@ pub fn parse_module(module: &Module) -> TokenStream { 0x8 => { PatternEffect::Panning(Num::new(slot.effect_parameter as i16 - 128) / 128) } + 0xA => { + let first = slot.effect_parameter >> 4; + let second = slot.effect_parameter & 0xF; + + if first == 0 { + PatternEffect::VolumeSlide(-Num::new(second as i16) / 16) + } else { + PatternEffect::VolumeSlide(Num::new(first as i16) / 16) + } + } 0xC => PatternEffect::Volume(Num::new(slot.effect_parameter as i16) / 255), _ => PatternEffect::None, }; From 07b00f11e1dc92c6a822b2db180f6fc37ccf1a0b Mon Sep 17 00:00:00 2001 From: Gwilym Inzani Date: Mon, 17 Jul 2023 01:19:02 +0100 Subject: [PATCH 29/65] Use relative notes to make it sound almost correct --- tracker/agb-tracker/src/lib.rs | 7 ++---- tracker/agb-xm-core/src/lib.rs | 45 ++++++++++++++++++++-------------- 2 files changed, 29 insertions(+), 23 deletions(-) diff --git a/tracker/agb-tracker/src/lib.rs b/tracker/agb-tracker/src/lib.rs index b1dfa5e9..983889e2 100644 --- a/tracker/agb-tracker/src/lib.rs +++ b/tracker/agb-tracker/src/lib.rs @@ -170,13 +170,10 @@ impl TrackerChannel { channel.stop(); } PatternEffect::Arpeggio(first, second) => { - let first: Num = first.change_base(); - let second: Num = second.change_base(); - match tick % 3 { 0 => channel.playback(self.base_speed), - 1 => channel.playback(self.base_speed + first), - 2 => channel.playback(self.base_speed + second), + 1 => channel.playback(self.base_speed + first.change_base()), + 2 => channel.playback(self.base_speed + second.change_base()), _ => unreachable!(), }; } diff --git a/tracker/agb-xm-core/src/lib.rs b/tracker/agb-xm-core/src/lib.rs index ffc7882b..3722861f 100644 --- a/tracker/agb-xm-core/src/lib.rs +++ b/tracker/agb-xm-core/src/lib.rs @@ -104,7 +104,7 @@ pub fn parse_module(module: &Module) -> TokenStream { let start_pos = pattern_data.len(); for row in pattern.iter() { - let mut notes = vec![None; module.get_num_channels()]; + let mut note_and_sample = vec![None; module.get_num_channels()]; for (i, slot) in row.iter().enumerate() { let channel_number = i % module.get_num_channels(); @@ -139,33 +139,42 @@ pub fn parse_module(module: &Module) -> TokenStream { if matches!(slot.note, Note::KeyOff) { effect1 = PatternEffect::Stop; - notes[channel_number] = None; - } else { - notes[channel_number] = Some(slot.note); + note_and_sample[channel_number] = 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; + } } let effect2 = match slot.effect_type { 0x0 => { if slot.effect_parameter == 0 { PatternEffect::None - } else if let Some(note) = notes[channel_number] { + } else if let Some((note, sample)) = note_and_sample[channel_number] { let first_arpeggio = slot.effect_parameter >> 4; let second_arpeggio = slot.effect_parameter & 0xF; - let note_speed = note_to_speed(note, 0.0, 0, module.frequency_type); + let note_speed = note_to_speed( + note, + sample.fine_tune, + sample.relative_note, + module.frequency_type, + ); - let note = note as u8; - let first_arpeggio: Note = (note + first_arpeggio) - .try_into() - .expect("Note out of bounds"); - let second_arpeggio: Note = (note + second_arpeggio) - .try_into() - .expect("Note out of bounds"); - - let first_arpeggio_speed = - note_to_speed(first_arpeggio, 0.0, 0, module.frequency_type); - let second_arpeggio_speed = - note_to_speed(second_arpeggio, 0.0, 0, module.frequency_type); + 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, + ); let first_arpeggio_difference = first_arpeggio_speed - note_speed; let second_arpeggio_difference = second_arpeggio_speed - note_speed; From 515bcb13ca12aad81025fdf0c4608ce2eec71011 Mon Sep 17 00:00:00 2001 From: Gwilym Inzani Date: Mon, 17 Jul 2023 01:21:33 +0100 Subject: [PATCH 30/65] Reset the volume each time a new sound is played --- tracker/agb-tracker/src/lib.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tracker/agb-tracker/src/lib.rs b/tracker/agb-tracker/src/lib.rs index 983889e2..69bd6b37 100644 --- a/tracker/agb-tracker/src/lib.rs +++ b/tracker/agb-tracker/src/lib.rs @@ -144,7 +144,8 @@ impl TrackerChannel { .restart_point(sample.restart_point); } - self.channel_id = mixer.play_sound(new_channel) + self.channel_id = mixer.play_sound(new_channel); + self.volume = 1.into(); } fn apply_effect( From 652173c08e78ebbe1b264696a417f9b130f64236 Mon Sep 17 00:00:00 2001 From: Gwilym Inzani Date: Mon, 17 Jul 2023 01:27:22 +0100 Subject: [PATCH 31/65] Reduce size of pattern by 4 bytes per slot --- tracker/agb-tracker-interop/src/lib.rs | 4 ++-- tracker/agb-tracker/src/lib.rs | 16 +++++++++++++--- tracker/agb-xm-core/src/lib.rs | 4 ++-- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/tracker/agb-tracker-interop/src/lib.rs b/tracker/agb-tracker-interop/src/lib.rs index a1a094b7..cd015246 100644 --- a/tracker/agb-tracker-interop/src/lib.rs +++ b/tracker/agb-tracker-interop/src/lib.rs @@ -29,8 +29,8 @@ pub struct Pattern { #[derive(Debug)] pub struct PatternSlot { - pub speed: Num, - pub sample: usize, + pub speed: Num, + pub sample: u16, pub effect1: PatternEffect, pub effect2: PatternEffect, } diff --git a/tracker/agb-tracker/src/lib.rs b/tracker/agb-tracker/src/lib.rs index 69bd6b37..a96cd08c 100644 --- a/tracker/agb-tracker/src/lib.rs +++ b/tracker/agb-tracker/src/lib.rs @@ -80,12 +80,22 @@ impl Tracker { 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 - 1]; + let sample = &self.track.samples[pattern_slot.sample as usize - 1]; channel.play_sound(mixer, sample); } - channel.apply_effect(mixer, &pattern_slot.effect1, self.tick, pattern_slot.speed); - channel.apply_effect(mixer, &pattern_slot.effect2, self.tick, pattern_slot.speed); + channel.apply_effect( + mixer, + &pattern_slot.effect1, + self.tick, + pattern_slot.speed.change_base(), + ); + channel.apply_effect( + mixer, + &pattern_slot.effect2, + self.tick, + pattern_slot.speed.change_base(), + ); } self.increment_step(); diff --git a/tracker/agb-xm-core/src/lib.rs b/tracker/agb-xm-core/src/lib.rs index 3722861f..f02e9370 100644 --- a/tracker/agb-xm-core/src/lib.rs +++ b/tracker/agb-xm-core/src/lib.rs @@ -229,8 +229,8 @@ pub fn parse_module(module: &Module) -> TokenStream { ); pattern_data.push(agb_tracker_interop::PatternSlot { - speed, - sample, + speed: speed.try_change_base().unwrap(), + sample: sample as u16, effect1, effect2, }); From 8b296794feececbbe1a6fe858ff699a8c589fa62 Mon Sep 17 00:00:00 2001 From: Gwilym Inzani Date: Mon, 17 Jul 2023 08:47:20 +0100 Subject: [PATCH 32/65] Should only decrease the volume after the first tick --- tracker/agb-tracker/src/lib.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tracker/agb-tracker/src/lib.rs b/tracker/agb-tracker/src/lib.rs index a96cd08c..382db5d8 100644 --- a/tracker/agb-tracker/src/lib.rs +++ b/tracker/agb-tracker/src/lib.rs @@ -196,11 +196,13 @@ impl TrackerChannel { self.volume = *volume; } PatternEffect::VolumeSlide(amount) => { - self.volume += *amount; - if self.volume < 0.into() { - self.volume = 0.into(); + if tick != 0 { + self.volume += *amount; + if self.volume < 0.into() { + self.volume = 0.into(); + } + channel.volume(self.volume); } - channel.volume(self.volume); } } } From de085fc1ff3d0ac242002a225adb26fc11df98ab Mon Sep 17 00:00:00 2001 From: Gwilym Inzani Date: Tue, 18 Jul 2023 20:23:11 +0100 Subject: [PATCH 33/65] Use wrapping add and mul --- agb/src/sound/mixer/sw_mixer.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/agb/src/sound/mixer/sw_mixer.rs b/agb/src/sound/mixer/sw_mixer.rs index 0e71faeb..f7f64d39 100644 --- a/agb/src/sound/mixer/sw_mixer.rs +++ b/agb/src/sound/mixer/sw_mixer.rs @@ -496,7 +496,10 @@ impl MixerBuffer { unsafe { *channel.data.get_unchecked(channel.pos.floor() as usize) } as i8 as i32; // SAFETY: working buffer length = self.frequency.buffer_size() - unsafe { *working_buffer_i32.get_unchecked_mut(i) += value * mul_amount }; + unsafe { + let value_ref = working_buffer_i32.get_unchecked_mut(i); + *value_ref = value_ref.wrapping_add(value.wrapping_mul(mul_amount)); + }; channel.pos += playback_speed; } } From a91069eac24db7ddf70539c16fb1a3f1e96e9b77 Mon Sep 17 00:00:00 2001 From: Gwilym Inzani Date: Tue, 18 Jul 2023 21:36:37 +0100 Subject: [PATCH 34/65] Consider the global volume --- tracker/agb-tracker-interop/src/lib.rs | 20 ++++++++-- tracker/agb-tracker/src/lib.rs | 2 + tracker/agb-xm-core/src/lib.rs | 55 +++++++++++++++++++------- 3 files changed, 58 insertions(+), 19 deletions(-) diff --git a/tracker/agb-tracker-interop/src/lib.rs b/tracker/agb-tracker-interop/src/lib.rs index cd015246..c0bd85e4 100644 --- a/tracker/agb-tracker-interop/src/lib.rs +++ b/tracker/agb-tracker-interop/src/lib.rs @@ -19,6 +19,7 @@ pub struct Sample<'a> { pub data: &'a [u8], pub should_loop: bool, pub restart_point: u32, + pub volume: Num, } #[derive(Debug)] @@ -104,10 +105,16 @@ impl<'a> quote::ToTokens for Sample<'a> { fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { use quote::{quote, TokenStreamExt}; - let self_as_u8s: Vec<_> = self.data.iter().map(|i| *i as u8).collect(); + let Sample { + data, + should_loop, + restart_point, + volume, + } = self; + + let self_as_u8s: Vec<_> = data.iter().map(|i| *i as u8).collect(); let samples = ByteString(&self_as_u8s); - let should_loop = self.should_loop; - let restart_point = self.restart_point; + let volume = volume.to_raw(); tokens.append_all(quote! { { @@ -115,7 +122,12 @@ impl<'a> quote::ToTokens for Sample<'a> { struct AlignmentWrapper([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 } + 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), + } } }); } diff --git a/tracker/agb-tracker/src/lib.rs b/tracker/agb-tracker/src/lib.rs index 382db5d8..cdf5f10c 100644 --- a/tracker/agb-tracker/src/lib.rs +++ b/tracker/agb-tracker/src/lib.rs @@ -148,6 +148,8 @@ impl TrackerChannel { let mut new_channel = SoundChannel::new(sample.data); + new_channel.volume(sample.volume); + if sample.should_loop { new_channel .should_loop() diff --git a/tracker/agb-xm-core/src/lib.rs b/tracker/agb-xm-core/src/lib.rs index f02e9370..f6476114 100644 --- a/tracker/agb-xm-core/src/lib.rs +++ b/tracker/agb-xm-core/src/lib.rs @@ -55,6 +55,7 @@ pub fn parse_module(module: &Module) -> TokenStream { fine_tune: f64, relative_note: i8, restart_point: u32, + volume: Num, } let mut samples = vec![]; @@ -73,6 +74,8 @@ pub fn parse_module(module: &Module) -> TokenStream { usize::MAX }; + let volume = Num::from_raw((sample.volume * (1 << 4) as f32) as i16); + let sample = match &sample.data { SampleDataType::Depth8(depth8) => depth8 .iter() @@ -93,6 +96,7 @@ pub fn parse_module(module: &Module) -> TokenStream { fine_tune, relative_note, restart_point, + volume, }); } } @@ -127,50 +131,62 @@ pub fn parse_module(module: &Module) -> TokenStream { } }; - let mut effect1 = match slot.volume { - 0x10..=0x50 => { - PatternEffect::Volume(Num::new((slot.volume - 0x10) as i16) / 64) - } - 0xC0..=0xCF => PatternEffect::Panning( - Num::new(slot.volume as i16 - (0xC0 + (0xCF - 0xC0) / 2)) / 64, - ), - _ => PatternEffect::None, - }; + let mut effect1 = PatternEffect::None; - if matches!(slot.note, Note::KeyOff) { + 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()), + ), + 0xC0..=0xCF => PatternEffect::Panning( + Num::new(slot.volume as i16 - (0xC0 + (0xCF - 0xC0) / 2)) / 64, + ), + _ => PatternEffect::None, + }; } let effect2 = match slot.effect_type { 0x0 => { if slot.effect_parameter == 0 { PatternEffect::None - } else if let Some((note, sample)) = note_and_sample[channel_number] { + } 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 note_speed = note_to_speed( - note, + *note, sample.fine_tune, sample.relative_note, module.frequency_type, ); let first_arpeggio_speed = note_to_speed( - note, + *note, sample.fine_tune, sample.relative_note + first_arpeggio as i8, module.frequency_type, ); let second_arpeggio_speed = note_to_speed( - note, + *note, sample.fine_tune, sample.relative_note + second_arpeggio as i8, module.frequency_type, @@ -207,7 +223,15 @@ pub fn parse_module(module: &Module) -> TokenStream { PatternEffect::VolumeSlide(Num::new(first as i16) / 16) } } - 0xC => PatternEffect::Volume(Num::new(slot.effect_parameter as i16) / 255), + 0xC => { + if let Some((_, sample)) = maybe_note_and_sample { + PatternEffect::Volume( + (Num::new(slot.effect_parameter as i16) / 255) * sample.volume, + ) + } else { + PatternEffect::None + } + } _ => PatternEffect::None, }; @@ -250,6 +274,7 @@ pub fn parse_module(module: &Module) -> TokenStream { data: &sample.data, should_loop: sample.should_loop, restart_point: sample.restart_point, + volume: sample.volume, }) .collect(); From c2e7a3b9b46229d4b96b5e8db674cff60b2c963f Mon Sep 17 00:00:00 2001 From: Gwilym Inzani Date: Tue, 18 Jul 2023 21:51:31 +0100 Subject: [PATCH 35/65] Update to 0.16 --- tracker/agb-tracker-interop/Cargo.toml | 4 ++-- tracker/agb-tracker/Cargo.toml | 8 ++++---- tracker/agb-xm-core/Cargo.toml | 6 +++--- tracker/agb-xm/Cargo.toml | 4 ++-- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/tracker/agb-tracker-interop/Cargo.toml b/tracker/agb-tracker-interop/Cargo.toml index 4b2c461e..1ac38413 100644 --- a/tracker/agb-tracker-interop/Cargo.toml +++ b/tracker/agb-tracker-interop/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "agb_tracker_interop" -version = "0.15.0" +version = "0.16.0" 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." @@ -14,4 +14,4 @@ std = [] [dependencies] quote = { version = "1", optional = true } proc-macro2 = { version = "1", optional = true } -agb_fixnum = { version = "0.15.0", path = "../../agb-fixnum" } \ No newline at end of file +agb_fixnum = { version = "0.16.0", path = "../../agb-fixnum" } \ No newline at end of file diff --git a/tracker/agb-tracker/Cargo.toml b/tracker/agb-tracker/Cargo.toml index 28b394ac..a70ed1ae 100644 --- a/tracker/agb-tracker/Cargo.toml +++ b/tracker/agb-tracker/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "agb_tracker" -version = "0.15.0" +version = "0.16.0" edition = "2021" license = "MPL-2.0" description = "Library for playing tracker music. Designed for use with the agb library for the Game Boy Advance." @@ -11,9 +11,9 @@ default = ["xm"] xm = ["dep:agb_xm"] [dependencies] -agb_xm = { version = "0.15.0", path = "../agb-xm", optional = true } -agb = { version = "0.15.0", path = "../../agb" } -agb_tracker_interop = { version = "0.15.0", path = "../agb-tracker-interop", default-features = false } +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 diff --git a/tracker/agb-xm-core/Cargo.toml b/tracker/agb-xm-core/Cargo.toml index c06b9287..3020064c 100644 --- a/tracker/agb-xm-core/Cargo.toml +++ b/tracker/agb-xm-core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "agb_xm_core" -version = "0.15.0" +version = "0.16.0" authors = ["Gwilym Kuiper "] edition = "2021" license = "MPL-2.0" @@ -13,7 +13,7 @@ proc-macro2 = "1" quote = "1" syn = "2" -agb_tracker_interop = { version = "0.15.0", path = "../agb-tracker-interop" } -agb_fixnum = { version = "0.15.0", path = "../../agb-fixnum" } +agb_tracker_interop = { version = "0.16.0", path = "../agb-tracker-interop" } +agb_fixnum = { version = "0.16.0", path = "../../agb-fixnum" } xmrs = "0.3" \ No newline at end of file diff --git a/tracker/agb-xm/Cargo.toml b/tracker/agb-xm/Cargo.toml index db3783f3..a55e8ddc 100644 --- a/tracker/agb-xm/Cargo.toml +++ b/tracker/agb-xm/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "agb_xm" -version = "0.15.0" +version = "0.16.0" authors = ["Gwilym Kuiper "] edition = "2021" license = "MPL-2.0" @@ -11,6 +11,6 @@ repository = "https://github.com/agbrs/agb" proc-macro = true [dependencies] -agb_xm_core = { version = "0.15.0", path = "../agb-xm-core" } +agb_xm_core = { version = "0.16.0", path = "../agb-xm-core" } proc-macro-error = "1" proc-macro2 = "1" From 03b5f2fafd2356f471f560c22f7a1e9cc6cc33f5 Mon Sep 17 00:00:00 2001 From: Gwilym Inzani Date: Tue, 18 Jul 2023 21:51:38 +0100 Subject: [PATCH 36/65] Remove unused methods --- agb/src/sound/mixer/mixer.s | 140 +------------------------------- agb/src/sound/mixer/sw_mixer.rs | 26 ------ 2 files changed, 3 insertions(+), 163 deletions(-) diff --git a/agb/src/sound/mixer/mixer.s b/agb/src/sound/mixer/mixer.s index 8cc44ea0..0ba3e3e9 100644 --- a/agb/src/sound/mixer/mixer.s +++ b/agb/src/sound/mixer/mixer.s @@ -1,139 +1,3 @@ -.macro mixer_add fn_name:req is_first:req -agb_arm_func \fn_name - @ Arguments - @ 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) - @ r2 - playback speed (usize fixnum with 8 bits) - @ r3 - amount to modify the left channel by (u16 fixnum with 4 bits) - @ stack position 1 - amount to modify the right channel by (u16 fixnum with 4 bits) - @ stack position 2 - the buffer_size (usize) - @ - @ The sound buffer must be buffer_size * 2 in size - push {{r4-r8}} - - ldr r7, [sp, #20] @ load the right channel modification amount into r7 - ldr r8, [sp, #24] @ load the buffer size into r8 - - movs r8, r8 @ check that the buffer size isn't 0 - bne 1f - - pop {{r4-r8}} - bx lr -1: - - 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 - -.macro add_one_sample - add r4, r0, r5, asr #8 @ calculate the address of the next read from the sound buffer - 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 - - str r4, [r1], #4 @ store the new value, and increment the pointer -.endm - -@ handle the non-multiple of 4 buffer size case - and r3, r8, #3 -1: - subs r3, r3, #1 - bmi 1f - - add_one_sample - subs r8, r8, #1 - beq 2f - b 1b - -1: -.rept 4 - add_one_sample -.endr -.purgem add_one_sample - subs r8, r8, #4 @ loop counter - bne 1b @ jump back if we're done with the loop - -2: - pop {{r4-r8}} - 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 - - sub r3, r3, #1 - - mov r5, #0 @ current index we're reading from - -.macro add_one_sample_same_modification - add r4, r0, r5, asr #8 @ calculate the address of the next read from the sound buffer - ldrsb r6, [r4] @ load the current sound sample to r6 - add r5, r5, r2 @ calculate the position to read the next sample from - - lsl r6, r6, #16 - orr r6, r6, lsr #16 - -.ifc \is_first,true - mov r4, r6, lsl r3 @ r4 = r6 << r3 -.else - ldr r4, [r1] @ read the current value - add r4, r4, r6, lsl r3 @ r4 += r6 << r3 (calculating both the left and right samples together) -.endif - - str r4, [r1], #4 @ store the new value, and increment the pointer -.endm - -@ handle the non-multiple of 4 buffer size case - and r7, r8, #3 -1: - subs r7, r7, #1 - bmi 1f - - add_one_sample_same_modification - subs r8, r8, #1 - beq 2f - b 1b - -1: -.rept 4 - add_one_sample_same_modification -.endr -.purgem add_one_sample_same_modification - - subs r8, r8, #4 @ loop counter - bne 1b @ jump back if we're done with the loop - -2: - pop {{r4-r8}} - bx lr - -agb_arm_end \fn_name -.endm - -mixer_add agb_rs__mixer_add false -mixer_add agb_rs__mixer_add_first true - .macro stereo_add_fn fn_name:req is_first:req agb_arm_func \fn_name @ Arguments @@ -202,7 +66,9 @@ agb_arm_end \fn_name .endm stereo_add_fn agb_rs__mixer_add_stereo false -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 @ Arguments: diff --git a/agb/src/sound/mixer/sw_mixer.rs b/agb/src/sound/mixer/sw_mixer.rs index f7f64d39..c5d50bce 100644 --- a/agb/src/sound/mixer/sw_mixer.rs +++ b/agb/src/sound/mixer/sw_mixer.rs @@ -1,6 +1,5 @@ use core::cell::RefCell; use core::marker::PhantomData; -use core::ops::ControlFlow; use core::pin::Pin; use alloc::boxed::Box; @@ -22,24 +21,6 @@ use crate::{ // Defined in mixer.s extern "C" { - fn agb_rs__mixer_add( - sound_data: *const u8, - sound_buffer: *mut Num, - playback_speed: Num, - left_amount: Num, - right_amount: Num, - buffer_size: usize, - ); - - fn agb_rs__mixer_add_first( - sound_data: *const u8, - sound_buffer: *mut Num, - playback_speed: Num, - left_amount: Num, - right_amount: Num, - buffer_size: usize, - ); - fn agb_rs__mixer_add_stereo( sound_data: *const u8, sound_buffer: *mut Num, @@ -47,13 +28,6 @@ extern "C" { buffer_size: usize, ); - fn agb_rs__mixer_add_stereo_first( - sound_data: *const u8, - sound_buffer: *mut Num, - volume: Num, - buffer_size: usize, - ); - fn agb_rs__mixer_collapse( sound_buffer: *mut i8, input_buffer: *const Num, From b8adf083822d61772784bf0b7cdf174b2454fec3 Mon Sep 17 00:00:00 2001 From: Gwilym Inzani Date: Tue, 18 Jul 2023 22:17:17 +0100 Subject: [PATCH 37/65] Don't hold arpeggios --- tracker/agb-tracker/src/lib.rs | 6 ++++-- tracker/agb-xm-core/src/lib.rs | 25 ++++++------------------- 2 files changed, 10 insertions(+), 21 deletions(-) diff --git a/tracker/agb-tracker/src/lib.rs b/tracker/agb-tracker/src/lib.rs index cdf5f10c..903feebf 100644 --- a/tracker/agb-tracker/src/lib.rs +++ b/tracker/agb-tracker/src/lib.rs @@ -177,6 +177,8 @@ impl TrackerChannel { self.base_speed = speed; } + channel.playback(self.base_speed); + match effect { PatternEffect::None => {} PatternEffect::Stop => { @@ -185,8 +187,8 @@ impl TrackerChannel { PatternEffect::Arpeggio(first, second) => { match tick % 3 { 0 => channel.playback(self.base_speed), - 1 => channel.playback(self.base_speed + first.change_base()), - 2 => channel.playback(self.base_speed + second.change_base()), + 1 => channel.playback(first.change_base()), + 2 => channel.playback(second.change_base()), _ => unreachable!(), }; } diff --git a/tracker/agb-xm-core/src/lib.rs b/tracker/agb-xm-core/src/lib.rs index f6476114..f47d20bb 100644 --- a/tracker/agb-xm-core/src/lib.rs +++ b/tracker/agb-xm-core/src/lib.rs @@ -172,13 +172,6 @@ pub fn parse_module(module: &Module) -> TokenStream { let first_arpeggio = slot.effect_parameter >> 4; let second_arpeggio = slot.effect_parameter & 0xF; - let note_speed = note_to_speed( - *note, - sample.fine_tune, - sample.relative_note, - module.frequency_type, - ); - let first_arpeggio_speed = note_to_speed( *note, sample.fine_tune, @@ -192,19 +185,13 @@ pub fn parse_module(module: &Module) -> TokenStream { module.frequency_type, ); - let first_arpeggio_difference = first_arpeggio_speed - note_speed; - let second_arpeggio_difference = second_arpeggio_speed - note_speed; - - let first_arpeggio_difference = first_arpeggio_difference - .try_change_base() - .expect("Arpeggio difference too large"); - let second_arpeggio_difference = second_arpeggio_difference - .try_change_base() - .expect("Arpeggio difference too large"); - PatternEffect::Arpeggio( - first_arpeggio_difference, - second_arpeggio_difference, + 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 From 6f8633861d08f8c25bdcb979ae711259883a8760 Mon Sep 17 00:00:00 2001 From: Gwilym Inzani Date: Tue, 18 Jul 2023 22:49:56 +0100 Subject: [PATCH 38/65] Use 32-bit numbers more for more performance (and start tracking that) --- tracker/agb-tracker-interop/src/lib.rs | 4 ++-- tracker/agb-tracker/examples/basic.rs | 21 +++++++++++++++++++++ tracker/agb-tracker/src/lib.rs | 6 +++--- tracker/agb-xm-core/src/lib.rs | 4 ++-- 4 files changed, 28 insertions(+), 7 deletions(-) diff --git a/tracker/agb-tracker-interop/src/lib.rs b/tracker/agb-tracker-interop/src/lib.rs index c0bd85e4..d165d612 100644 --- a/tracker/agb-tracker-interop/src/lib.rs +++ b/tracker/agb-tracker-interop/src/lib.rs @@ -10,8 +10,8 @@ pub struct Track<'a> { pub patterns_to_play: &'a [usize], pub num_channels: usize, - pub frames_per_tick: Num, - pub ticks_per_step: u16, + pub frames_per_tick: Num, + pub ticks_per_step: u32, } #[derive(Debug)] diff --git a/tracker/agb-tracker/examples/basic.rs b/tracker/agb-tracker/examples/basic.rs index 2f7d7139..2835a2c0 100644 --- a/tracker/agb-tracker/examples/basic.rs +++ b/tracker/agb-tracker/examples/basic.rs @@ -11,14 +11,35 @@ const DB_TOFFE: Track = import_xm!("examples/db_toffe.xm"); 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); + 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 total_cycles = after_mixing_cycles.wrapping_sub(before_mixing_cycles); + + agb::println!("{total_cycles} cycles"); } } diff --git a/tracker/agb-tracker/src/lib.rs b/tracker/agb-tracker/src/lib.rs index 903feebf..f3c1b4c9 100644 --- a/tracker/agb-tracker/src/lib.rs +++ b/tracker/agb-tracker/src/lib.rs @@ -29,8 +29,8 @@ pub struct Tracker { track: &'static Track<'static>, channels: Vec, - frame: Num, - tick: u16, + frame: Num, + tick: u32, first: bool, current_row: usize, @@ -164,7 +164,7 @@ impl TrackerChannel { &mut self, mixer: &mut Mixer<'_>, effect: &PatternEffect, - tick: u16, + tick: u32, speed: Num, ) { if let Some(channel) = self diff --git a/tracker/agb-xm-core/src/lib.rs b/tracker/agb-xm-core/src/lib.rs index f47d20bb..da3fc4f8 100644 --- a/tracker/agb-xm-core/src/lib.rs +++ b/tracker/agb-xm-core/src/lib.rs @@ -272,7 +272,7 @@ pub fn parse_module(module: &Module) -> TokenStream { .collect::>(); // Number 150 here deduced experimentally - let frames_per_tick = Num::::new(150) / module.default_bpm; + let frames_per_tick = Num::::new(150) / module.default_bpm as u32; let ticks_per_step = module.default_tempo; let interop = agb_tracker_interop::Track { @@ -283,7 +283,7 @@ pub fn parse_module(module: &Module) -> TokenStream { patterns_to_play: &patterns_to_play, frames_per_tick, - ticks_per_step, + ticks_per_step: ticks_per_step.into(), }; quote!(#interop) From a9236531c7509bc455e5e45ea47cabf84a78cc57 Mon Sep 17 00:00:00 2001 From: Gwilym Inzani Date: Wed, 19 Jul 2023 12:31:59 +0100 Subject: [PATCH 39/65] Simplify the basic example and make a timing example --- tracker/agb-tracker/examples/basic.rs | 20 ---------- tracker/agb-tracker/examples/timing.rs | 55 ++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 20 deletions(-) create mode 100644 tracker/agb-tracker/examples/timing.rs diff --git a/tracker/agb-tracker/examples/basic.rs b/tracker/agb-tracker/examples/basic.rs index 2835a2c0..0c682767 100644 --- a/tracker/agb-tracker/examples/basic.rs +++ b/tracker/agb-tracker/examples/basic.rs @@ -11,35 +11,15 @@ const DB_TOFFE: Track = import_xm!("examples/db_toffe.xm"); 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); - 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 total_cycles = after_mixing_cycles.wrapping_sub(before_mixing_cycles); - - agb::println!("{total_cycles} cycles"); } } diff --git a/tracker/agb-tracker/examples/timing.rs b/tracker/agb-tracker/examples/timing.rs new file mode 100644 index 00000000..996484b7 --- /dev/null +++ b/tracker/agb-tracker/examples/timing.rs @@ -0,0 +1,55 @@ +#![no_std] +#![no_main] + +use agb::sound::mixer::Frequency; +use agb::Gba; +use agb_tracker::{import_xm, Track, Tracker}; + +const DB_TOFFE: Track = import_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" + ); + } +} From 1b8f4bbdc92d42cf332845bf816333f6bad37e6e Mon Sep 17 00:00:00 2001 From: Gwilym Inzani Date: Wed, 19 Jul 2023 13:22:26 +0100 Subject: [PATCH 40/65] NoteCut support --- tracker/agb-tracker-interop/src/lib.rs | 2 ++ tracker/agb-tracker/src/lib.rs | 6 ++++++ tracker/agb-xm-core/src/lib.rs | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/tracker/agb-tracker-interop/src/lib.rs b/tracker/agb-tracker-interop/src/lib.rs index d165d612..d2c92533 100644 --- a/tracker/agb-tracker-interop/src/lib.rs +++ b/tracker/agb-tracker-interop/src/lib.rs @@ -48,6 +48,7 @@ pub enum PatternEffect { Panning(Num), Volume(Num), VolumeSlide(Num), + NoteCut(u32), } #[cfg(feature = "quote")] @@ -202,6 +203,7 @@ impl quote::ToTokens for PatternEffect { let amount = amount.to_raw(); quote! { VolumeSlide(agb_tracker::__private::Num::from_raw(#amount))} } + PatternEffect::NoteCut(wait) => quote! { NoteCut(#wait) }, }; tokens.append_all(quote! { diff --git a/tracker/agb-tracker/src/lib.rs b/tracker/agb-tracker/src/lib.rs index f3c1b4c9..79875f2b 100644 --- a/tracker/agb-tracker/src/lib.rs +++ b/tracker/agb-tracker/src/lib.rs @@ -208,6 +208,12 @@ impl TrackerChannel { channel.volume(self.volume); } } + PatternEffect::NoteCut(wait) => { + if tick == *wait { + channel.volume(0); + self.volume = 0.into(); + } + } } } } diff --git a/tracker/agb-xm-core/src/lib.rs b/tracker/agb-xm-core/src/lib.rs index da3fc4f8..dcf343a0 100644 --- a/tracker/agb-xm-core/src/lib.rs +++ b/tracker/agb-xm-core/src/lib.rs @@ -219,6 +219,10 @@ pub fn parse_module(module: &Module) -> TokenStream { PatternEffect::None } } + 0xE => match slot.effect_parameter >> 4 { + 0xC => PatternEffect::NoteCut((slot.effect_parameter & 0xf).into()), + _ => PatternEffect::None, + }, _ => PatternEffect::None, }; From b6cccf3b5a0d7521f0c5be11c7a76676d59045f0 Mon Sep 17 00:00:00 2001 From: Gwilym Inzani Date: Wed, 19 Jul 2023 13:38:32 +0100 Subject: [PATCH 41/65] Add Portamento --- tracker/agb-tracker-interop/src/lib.rs | 5 +++++ tracker/agb-tracker/src/lib.rs | 10 ++++++++-- tracker/agb-xm-core/src/lib.rs | 26 ++++++++++++++++++++++++++ 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/tracker/agb-tracker-interop/src/lib.rs b/tracker/agb-tracker-interop/src/lib.rs index d2c92533..ba7aa5f5 100644 --- a/tracker/agb-tracker-interop/src/lib.rs +++ b/tracker/agb-tracker-interop/src/lib.rs @@ -49,6 +49,7 @@ pub enum PatternEffect { Volume(Num), VolumeSlide(Num), NoteCut(u32), + Portamento(Num), } #[cfg(feature = "quote")] @@ -204,6 +205,10 @@ impl quote::ToTokens for PatternEffect { quote! { VolumeSlide(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))} + } }; tokens.append_all(quote! { diff --git a/tracker/agb-tracker/src/lib.rs b/tracker/agb-tracker/src/lib.rs index 79875f2b..8a31fa69 100644 --- a/tracker/agb-tracker/src/lib.rs +++ b/tracker/agb-tracker/src/lib.rs @@ -61,7 +61,7 @@ impl Tracker { tick: 0, current_row: 0, - current_pattern: 0, + current_pattern: 31, } } @@ -78,7 +78,7 @@ impl Tracker { 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) { + for (channel, pattern_slot) in self.channels.iter_mut().zip(pattern_slots).skip(3) { if pattern_slot.sample != 0 && self.tick == 0 { let sample = &self.track.samples[pattern_slot.sample as usize - 1]; channel.play_sound(mixer, sample); @@ -214,6 +214,12 @@ impl TrackerChannel { self.volume = 0.into(); } } + PatternEffect::Portamento(amount) => { + if tick != 0 { + self.base_speed *= amount.change_base(); + channel.playback(self.base_speed); + } + } } } } diff --git a/tracker/agb-xm-core/src/lib.rs b/tracker/agb-xm-core/src/lib.rs index dcf343a0..f33d0f95 100644 --- a/tracker/agb-xm-core/src/lib.rs +++ b/tracker/agb-xm-core/src/lib.rs @@ -197,6 +197,32 @@ pub fn parse_module(module: &Module) -> TokenStream { PatternEffect::None } } + 0x1 => { + let c4_speed = note_to_speed(Note::C4, 0.0, 0, module.frequency_type); + let speed = note_to_speed( + Note::C4, + slot.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, + -(slot.effect_parameter as f64), + 0, + module.frequency_type, + ); + + let portamento_amount = speed / c4_speed; + + PatternEffect::Portamento(portamento_amount.try_change_base().unwrap()) + } 0x8 => { PatternEffect::Panning(Num::new(slot.effect_parameter as i16 - 128) / 128) } From ff0e8f659e5ca6d3f9d7c17c5ff0b67b49462705 Mon Sep 17 00:00:00 2001 From: Gwilym Inzani Date: Wed, 19 Jul 2023 13:53:46 +0100 Subject: [PATCH 42/65] Better tracking of speed --- tracker/agb-tracker/src/lib.rs | 48 ++++++++++++++++------------------ 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/tracker/agb-tracker/src/lib.rs b/tracker/agb-tracker/src/lib.rs index 8a31fa69..5a05f3ae 100644 --- a/tracker/agb-tracker/src/lib.rs +++ b/tracker/agb-tracker/src/lib.rs @@ -61,7 +61,7 @@ impl Tracker { tick: 0, current_row: 0, - current_pattern: 31, + current_pattern: 0, } } @@ -78,24 +78,18 @@ impl Tracker { 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).skip(3) { + 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); } - channel.apply_effect( - mixer, - &pattern_slot.effect1, - self.tick, - pattern_slot.speed.change_base(), - ); - channel.apply_effect( - mixer, - &pattern_slot.effect2, - self.tick, - pattern_slot.speed.change_base(), - ); + 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(); @@ -160,25 +154,26 @@ impl TrackerChannel { self.volume = 1.into(); } - fn apply_effect( - &mut self, - mixer: &mut Mixer<'_>, - effect: &PatternEffect, - tick: u32, - speed: Num, - ) { + fn set_speed(&mut self, mixer: &mut Mixer<'_>, speed: Num) { if let Some(channel) = self .channel_id .as_ref() .and_then(|channel_id| mixer.channel(&channel_id)) { if speed != 0.into() { - channel.playback(speed); 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 => { @@ -215,10 +210,13 @@ impl TrackerChannel { } } PatternEffect::Portamento(amount) => { - if tick != 0 { - self.base_speed *= amount.change_base(); - channel.playback(self.base_speed); + let mut new_speed = self.base_speed; + + for _ in 0..tick { + new_speed *= amount.change_base(); } + + channel.playback(new_speed); } } } From d6384a6886bfeaf837ff3f6fcef61325605e79b9 Mon Sep 17 00:00:00 2001 From: Gwilym Inzani Date: Sat, 22 Jul 2023 23:27:21 +0100 Subject: [PATCH 43/65] This improves mixing performance by about 20% --- agb/src/sound/mixer/sw_mixer.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/agb/src/sound/mixer/sw_mixer.rs b/agb/src/sound/mixer/sw_mixer.rs index c5d50bce..13cd0376 100644 --- a/agb/src/sound/mixer/sw_mixer.rs +++ b/agb/src/sound/mixer/sw_mixer.rs @@ -433,6 +433,8 @@ impl MixerBuffer { channel.pos += 2 * self.frequency.buffer_size() as u32; } + #[link_section = ".iwram.write_mono"] + #[inline(never)] fn write_mono(&self, channel: &mut SoundChannel, working_buffer: &mut [Num]) { let right_amount = ((channel.panning + 1) / 2) * channel.volume; let left_amount = ((-channel.panning + 1) / 2) * channel.volume; From 1aa8e5fd334d0efe337cdd22e68b2ccf45ac0e45 Mon Sep 17 00:00:00 2001 From: Gwilym Inzani Date: Sat, 22 Jul 2023 23:40:09 +0100 Subject: [PATCH 44/65] Reintroduce the first special casing --- agb/src/sound/mixer/mixer.s | 1 + agb/src/sound/mixer/sw_mixer.rs | 92 ++++++++++++++++++++++++++------- 2 files changed, 73 insertions(+), 20 deletions(-) diff --git a/agb/src/sound/mixer/mixer.s b/agb/src/sound/mixer/mixer.s index 0ba3e3e9..c1101dc2 100644 --- a/agb/src/sound/mixer/mixer.s +++ b/agb/src/sound/mixer/mixer.s @@ -66,6 +66,7 @@ agb_arm_end \fn_name .endm stereo_add_fn agb_rs__mixer_add_stereo false +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 diff --git a/agb/src/sound/mixer/sw_mixer.rs b/agb/src/sound/mixer/sw_mixer.rs index 13cd0376..4915e7a7 100644 --- a/agb/src/sound/mixer/sw_mixer.rs +++ b/agb/src/sound/mixer/sw_mixer.rs @@ -28,6 +28,13 @@ extern "C" { buffer_size: usize, ); + fn agb_rs__mixer_add_stereo_first( + sound_data: *const u8, + sound_buffer: *mut Num, + volume: Num, + buffer_size: usize, + ); + fn agb_rs__mixer_collapse( sound_buffer: *mut i8, input_buffer: *const Num, @@ -387,15 +394,24 @@ impl MixerBuffer { working_buffer: &mut [Num], channels: impl Iterator, ) { - working_buffer.fill(0.into()); + let mut channels = + channels.filter(|channel| !channel.is_done && channel.volume != 0.into()); - for channel in channels.filter(|channel| !channel.is_done) { - if channel.volume != 0.into() { - if channel.is_stereo { - self.write_stereo(channel, working_buffer); - } else { - self.write_mono(channel, working_buffer); - } + if let Some(channel) = channels.next() { + if channel.is_stereo { + self.write_stereo::(channel, working_buffer); + } else { + self.write_mono::(channel, working_buffer); + } + } else { + working_buffer.fill(0.into()); + } + + for channel in channels { + if channel.is_stereo { + self.write_stereo::(channel, working_buffer); + } else { + self.write_mono::(channel, working_buffer); } } @@ -410,7 +426,11 @@ impl MixerBuffer { } } - fn write_stereo(&self, channel: &mut SoundChannel, working_buffer: &mut [Num]) { + fn write_stereo( + &self, + channel: &mut SoundChannel, + working_buffer: &mut [Num], + ) { if (channel.pos + 2 * self.frequency.buffer_size() as u32).floor() >= channel.data.len() as u32 { @@ -418,16 +438,28 @@ impl MixerBuffer { channel.pos = channel.restart_point * 2; } else { channel.is_done = true; + if IS_FIRST { + working_buffer.fill(0.into()); + } return; } } unsafe { - agb_rs__mixer_add_stereo( - channel.data.as_ptr().add(channel.pos.floor() as usize), - working_buffer.as_mut_ptr(), - channel.volume, - self.frequency.buffer_size(), - ); + 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, + 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, + self.frequency.buffer_size(), + ); + } } channel.pos += 2 * self.frequency.buffer_size() as u32; @@ -435,7 +467,11 @@ impl MixerBuffer { #[link_section = ".iwram.write_mono"] #[inline(never)] - fn write_mono(&self, channel: &mut SoundChannel, working_buffer: &mut [Num]) { + fn write_mono( + &self, + channel: &mut SoundChannel, + working_buffer: &mut [Num], + ) { let right_amount = ((channel.panning + 1) / 2) * channel.volume; let left_amount = ((-channel.panning + 1) / 2) * channel.volume; @@ -463,6 +499,16 @@ impl MixerBuffer { channel.pos -= channel_len - channel.restart_point; } else { channel.is_done = true; + + if IS_FIRST { + for j in i..self.frequency.buffer_size() { + // SAFETY: working buffer length = self.frequency.buffer_size() + unsafe { + *working_buffer_i32.get_unchecked_mut(j) = 0.into(); + } + } + } + break; } } @@ -472,10 +518,16 @@ impl MixerBuffer { unsafe { *channel.data.get_unchecked(channel.pos.floor() as usize) } as i8 as i32; // SAFETY: working buffer length = self.frequency.buffer_size() - unsafe { - let value_ref = working_buffer_i32.get_unchecked_mut(i); - *value_ref = value_ref.wrapping_add(value.wrapping_mul(mul_amount)); - }; + if IS_FIRST { + unsafe { + *working_buffer_i32.get_unchecked_mut(i) = value.wrapping_mul(mul_amount); + } + } else { + unsafe { + let value_ref = working_buffer_i32.get_unchecked_mut(i); + *value_ref = value_ref.wrapping_add(value.wrapping_mul(mul_amount)); + }; + } channel.pos += playback_speed; } } From 992ce37464466ab672b92cdd65980722e713e72a Mon Sep 17 00:00:00 2001 From: Gwilym Inzani Date: Sun, 23 Jul 2023 00:31:12 +0100 Subject: [PATCH 45/65] Update justfile to support the tracker crates --- justfile | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/justfile b/justfile index 05a12dce..9aa2687e 100644 --- a/justfile +++ b/justfile @@ -15,12 +15,14 @@ test: just _test-debug agb just _test-debug agb-fixnum just _test-debug agb-hashmap + just _test-debug tracker/agb-tracker just _test-debug-arm agb just _test-debug tools test-release: just _test-release agb just _test-release agb-fixnum + just _test-debug tracker/agb-tracker just _test-release-arm agb doctest-agb: @@ -28,6 +30,7 @@ doctest-agb: check-docs: (cd agb && cargo doc --target=thumbv6m-none-eabi --no-deps) + (cd tracker/agb-tracker && cargo doc --target=thumbv6m-none-eabi --no-deps) just _build_docs agb-fixnum just _build_docs agb-hashmap @@ -120,7 +123,7 @@ gbafix *args: (cd agb-gbafix && cargo run --release -- {{args}}) _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"); \ just "{{target}}" "$PROJECT_DIR" || exit $?; \ done From 85561de1caa8fd10b76e96feea3b2553e7f24ee9 Mon Sep 17 00:00:00 2001 From: Gwilym Inzani Date: Sun, 23 Jul 2023 18:35:50 +0100 Subject: [PATCH 46/65] Attempt to update the tools to the new folder --- tools/src/publish.rs | 12 +++++++++--- tools/src/release.rs | 1 + 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/tools/src/publish.rs b/tools/src/publish.rs index 48d2d616..642017a0 100644 --- a/tools/src/publish.rs +++ b/tools/src/publish.rs @@ -3,7 +3,7 @@ use dependency_graph::DependencyGraph; use std::cell::RefCell; use std::collections::HashMap; use std::fs; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::process::Command; use toml_edit::Document; @@ -22,6 +22,7 @@ pub enum Error { struct Package { name: String, dependencies: Vec, + directory: PathBuf, } impl dependency_graph::Node for Package { @@ -55,7 +56,11 @@ pub fn publish(matches: &ArgMatches) -> Result<(), Error> { let mut in_progress: HashMap<_, RefCell> = 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[..]); for package in graph { @@ -76,7 +81,7 @@ pub fn publish(matches: &ArgMatches) -> Result<(), Error> { let publish_cmd = Command::new("cargo") .arg("publish") .args(&dry_run) - .current_dir(root_directory.join(&package.name)) + .current_dir(&package.directory) .spawn() .map_err(|_| Error::PublishCrate)?; @@ -117,6 +122,7 @@ fn build_dependency_graph(root: &Path) -> Result, Error> { packages.push(Package { name: dir.file_name().to_string_lossy().to_string(), dependencies: get_agb_dependencies(&crate_path)?, + directory: crate_path, }); } diff --git a/tools/src/release.rs b/tools/src/release.rs index dbe4bb9f..ffdfe241 100644 --- a/tools/src/release.rs +++ b/tools/src/release.rs @@ -108,6 +108,7 @@ fn update_to_version( &[ "agb-*/Cargo.toml", "agb/Cargo.toml", + "tracker/agb-*/Cargo.toml", "examples/*/Cargo.toml", "book/games/*/Cargo.toml", "template/Cargo.toml", From 352658f23facf6cf8aa9feb883ed68a42d8877c9 Mon Sep 17 00:00:00 2001 From: Gwilym Inzani Date: Sun, 23 Jul 2023 19:08:51 +0100 Subject: [PATCH 47/65] Fix clippy lints --- agb/src/sound/mixer/sw_mixer.rs | 2 +- tracker/agb-tracker-interop/src/lib.rs | 3 +-- tracker/agb-tracker/src/lib.rs | 11 +++++++---- tracker/agb-xm-core/src/lib.rs | 8 +++++--- tracker/agb-xm-core/src/main.rs | 8 -------- 5 files changed, 14 insertions(+), 18 deletions(-) delete mode 100644 tracker/agb-xm-core/src/main.rs diff --git a/agb/src/sound/mixer/sw_mixer.rs b/agb/src/sound/mixer/sw_mixer.rs index 4915e7a7..26556f97 100644 --- a/agb/src/sound/mixer/sw_mixer.rs +++ b/agb/src/sound/mixer/sw_mixer.rs @@ -504,7 +504,7 @@ impl MixerBuffer { for j in i..self.frequency.buffer_size() { // SAFETY: working buffer length = self.frequency.buffer_size() unsafe { - *working_buffer_i32.get_unchecked_mut(j) = 0.into(); + *working_buffer_i32.get_unchecked_mut(j) = 0; } } } diff --git a/tracker/agb-tracker-interop/src/lib.rs b/tracker/agb-tracker-interop/src/lib.rs index ba7aa5f5..38388b96 100644 --- a/tracker/agb-tracker-interop/src/lib.rs +++ b/tracker/agb-tracker-interop/src/lib.rs @@ -114,8 +114,7 @@ impl<'a> quote::ToTokens for Sample<'a> { volume, } = self; - let self_as_u8s: Vec<_> = data.iter().map(|i| *i as u8).collect(); - let samples = ByteString(&self_as_u8s); + let samples = ByteString(data); let volume = volume.to_raw(); tokens.append_all(quote! { diff --git a/tracker/agb-tracker/src/lib.rs b/tracker/agb-tracker/src/lib.rs index 5a05f3ae..35368977 100644 --- a/tracker/agb-tracker/src/lib.rs +++ b/tracker/agb-tracker/src/lib.rs @@ -135,10 +135,13 @@ impl Tracker { impl TrackerChannel { fn play_sound(&mut self, mixer: &mut Mixer<'_>, sample: &Sample<'static>) { - self.channel_id + if let Some(channel) = self + .channel_id .take() .and_then(|channel_id| mixer.channel(&channel_id)) - .map(|channel| channel.stop()); + { + channel.stop(); + } let mut new_channel = SoundChannel::new(sample.data); @@ -158,7 +161,7 @@ impl TrackerChannel { if let Some(channel) = self .channel_id .as_ref() - .and_then(|channel_id| mixer.channel(&channel_id)) + .and_then(|channel_id| mixer.channel(channel_id)) { if speed != 0.into() { self.base_speed = speed; @@ -172,7 +175,7 @@ impl TrackerChannel { if let Some(channel) = self .channel_id .as_ref() - .and_then(|channel_id| mixer.channel(&channel_id)) + .and_then(|channel_id| mixer.channel(channel_id)) { match effect { PatternEffect::None => {} diff --git a/tracker/agb-xm-core/src/lib.rs b/tracker/agb-xm-core/src/lib.rs index f33d0f95..c75d076e 100644 --- a/tracker/agb-xm-core/src/lib.rs +++ b/tracker/agb-xm-core/src/lib.rs @@ -14,7 +14,7 @@ use xmrs::{prelude::*, xm::xmmodule::XmModule}; pub fn agb_xm_core(args: TokenStream) -> TokenStream { let input = match syn::parse::(args.into()) { Ok(input) => input, - Err(err) => return proc_macro2::TokenStream::from(err.to_compile_error()), + Err(err) => return err.to_compile_error(), }; let filename = input.value(); @@ -61,7 +61,9 @@ pub fn parse_module(module: &Module) -> TokenStream { let mut samples = vec![]; for (instrument_index, instrument) in instruments.iter().enumerate() { - let InstrumentType::Default(ref instrument) = instrument.instr_type else { continue; }; + 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); @@ -340,7 +342,7 @@ fn note_to_speed( 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 as f64) * 16.0 * 4.0 - fine_tune / 2.0; + 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)) } diff --git a/tracker/agb-xm-core/src/main.rs b/tracker/agb-xm-core/src/main.rs deleted file mode 100644 index 026485eb..00000000 --- a/tracker/agb-xm-core/src/main.rs +++ /dev/null @@ -1,8 +0,0 @@ -fn main() -> Result<(), Box> { - let module = agb_xm_core::load_module_from_file(&std::path::Path::new( - "../agb-tracker/examples/final_countdown.xm", - ))?; - let output = agb_xm_core::parse_module(&module); - - Ok(()) -} From b593b90701d9c85311aeb9d31dce4afcf4696698 Mon Sep 17 00:00:00 2001 From: Gwilym Inzani Date: Sun, 23 Jul 2023 19:52:11 +0100 Subject: [PATCH 48/65] Get the docs building --- agb/Cargo.toml | 3 +-- agb/src/sound/mixer/mod.rs | 2 +- agb/src/sync/statics.rs | 6 +++--- justfile | 4 ++-- tracker/agb-tracker/Cargo.toml | 3 +++ 5 files changed, 10 insertions(+), 8 deletions(-) diff --git a/agb/Cargo.toml b/agb/Cargo.toml index e1bd60a2..390bd225 100644 --- a/agb/Cargo.toml +++ b/agb/Cargo.toml @@ -23,8 +23,7 @@ bilge = "0.1" rustc-hash = { version = "1", default-features = false } [package.metadata.docs.rs] -default-target = "thumbv6m-none-eabi" -targets = [] +default-target = "thumbv4t-none-eabi" [profile.dev] opt-level = 3 diff --git a/agb/src/sound/mixer/mod.rs b/agb/src/sound/mixer/mod.rs index 1a7fe1c4..7cb4c055 100644 --- a/agb/src/sound/mixer/mod.rs +++ b/agb/src/sound/mixer/mod.rs @@ -334,7 +334,7 @@ impl SoundChannel { } /// Sets the point at which the sample should restart once it loops. Does nothing - /// unless you also call [`should_loop()`]. + /// unless you also call [`should_loop()`](SoundChannel::should_loop()). /// /// Useful if your song has an introduction or similar. #[inline(always)] diff --git a/agb/src/sync/statics.rs b/agb/src/sync/statics.rs index 68eb5030..e7828c6c 100644 --- a/agb/src/sync/statics.rs +++ b/agb/src/sync/statics.rs @@ -78,7 +78,7 @@ unsafe fn transfer_align4_thumb(mut dst: *mut T, mut src: *const T) { } } -#[cfg_attr(not(doc), instruction_set(arm::a32))] +#[instruction_set(arm::a32)] #[allow(unused_assignments)] unsafe fn transfer_align4_arm(mut dst: *mut T, mut src: *const T) { let size = mem::size_of::(); @@ -168,14 +168,14 @@ unsafe fn exchange(dst: *mut T, src: *const T) -> T { } } -#[cfg_attr(not(doc), instruction_set(arm::a32))] +#[instruction_set(arm::a32)] unsafe fn exchange_align4_arm(dst: *mut T, i: u32) -> u32 { let out; asm!("swp {2}, {1}, [{0}]", in(reg) dst, in(reg) i, lateout(reg) out); out } -#[cfg_attr(not(doc), instruction_set(arm::a32))] +#[instruction_set(arm::a32)] unsafe fn exchange_align1_arm(dst: *mut T, i: u8) -> u8 { let out; asm!("swpb {2}, {1}, [{0}]", in(reg) dst, in(reg) i, lateout(reg) out); diff --git a/justfile b/justfile index 9aa2687e..216bc75f 100644 --- a/justfile +++ b/justfile @@ -29,8 +29,8 @@ doctest-agb: (cd agb && cargo test --doc -Z doctest-xcompile) check-docs: - (cd agb && cargo doc --target=thumbv6m-none-eabi --no-deps) - (cd tracker/agb-tracker && 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-hashmap diff --git a/tracker/agb-tracker/Cargo.toml b/tracker/agb-tracker/Cargo.toml index a70ed1ae..31b7b700 100644 --- a/tracker/agb-tracker/Cargo.toml +++ b/tracker/agb-tracker/Cargo.toml @@ -23,3 +23,6 @@ debug = true opt-level = 3 lto = "fat" debug = true + +[package.metadata.docs.rs] +default-target = "thumbv4t-none-eabi" From e2f14094fc5cbb31bc8af1e82960c8df006e01f5 Mon Sep 17 00:00:00 2001 From: Gwilym Inzani Date: Sun, 23 Jul 2023 20:00:31 +0100 Subject: [PATCH 49/65] Update my name --- tracker/agb-tracker-interop/Cargo.toml | 1 + tracker/agb-tracker/Cargo.toml | 1 + tracker/agb-xm-core/Cargo.toml | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/tracker/agb-tracker-interop/Cargo.toml b/tracker/agb-tracker-interop/Cargo.toml index 1ac38413..36257898 100644 --- a/tracker/agb-tracker-interop/Cargo.toml +++ b/tracker/agb-tracker-interop/Cargo.toml @@ -1,6 +1,7 @@ [package] name = "agb_tracker_interop" version = "0.16.0" +authors = ["Gwilym Inzani "] 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." diff --git a/tracker/agb-tracker/Cargo.toml b/tracker/agb-tracker/Cargo.toml index 31b7b700..c2a23d51 100644 --- a/tracker/agb-tracker/Cargo.toml +++ b/tracker/agb-tracker/Cargo.toml @@ -1,6 +1,7 @@ [package] name = "agb_tracker" version = "0.16.0" +authors = ["Gwilym Inzani "] edition = "2021" license = "MPL-2.0" description = "Library for playing tracker music. Designed for use with the agb library for the Game Boy Advance." diff --git a/tracker/agb-xm-core/Cargo.toml b/tracker/agb-xm-core/Cargo.toml index 3020064c..fb41bbca 100644 --- a/tracker/agb-xm-core/Cargo.toml +++ b/tracker/agb-xm-core/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "agb_xm_core" version = "0.16.0" -authors = ["Gwilym Kuiper "] +authors = ["Gwilym Inzani "] 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" From 6cb8f68104cf5584d973db4bdf97a480cce58dbd Mon Sep 17 00:00:00 2001 From: Gwilym Inzani Date: Sun, 23 Jul 2023 20:18:55 +0100 Subject: [PATCH 50/65] Write some simple docs for agb_tracker --- tracker/agb-tracker/src/lib.rs | 65 ++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/tracker/agb-tracker/src/lib.rs b/tracker/agb-tracker/src/lib.rs index 35368977..e88a1e93 100644 --- a/tracker/agb-tracker/src/lib.rs +++ b/tracker/agb-tracker/src/lib.rs @@ -4,6 +4,64 @@ #![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::{import_xm, Track, Tracker}; +//! +//! const DB_TOFFE: Track = import_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; @@ -15,16 +73,20 @@ use agb::{ 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::import_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 import 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, @@ -44,6 +106,7 @@ struct TrackerChannel { } 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 { @@ -65,6 +128,8 @@ impl Tracker { } } + /// 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; From 70d34f1fc845d360101cd3cb25398f83294db142 Mon Sep 17 00:00:00 2001 From: Gwilym Inzani Date: Sun, 23 Jul 2023 20:22:22 +0100 Subject: [PATCH 51/65] Rename to include_xm to match the rest of agb --- tracker/agb-tracker/examples/basic.rs | 4 ++-- tracker/agb-tracker/examples/timing.rs | 4 ++-- tracker/agb-tracker/src/lib.rs | 8 ++++---- tracker/agb-xm/src/lib.rs | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/tracker/agb-tracker/examples/basic.rs b/tracker/agb-tracker/examples/basic.rs index 0c682767..260e8735 100644 --- a/tracker/agb-tracker/examples/basic.rs +++ b/tracker/agb-tracker/examples/basic.rs @@ -3,9 +3,9 @@ use agb::sound::mixer::Frequency; use agb::Gba; -use agb_tracker::{import_xm, Track, Tracker}; +use agb_tracker::{include_xm, Track, Tracker}; -const DB_TOFFE: Track = import_xm!("examples/db_toffe.xm"); +const DB_TOFFE: Track = include_xm!("examples/db_toffe.xm"); #[agb::entry] fn main(mut gba: Gba) -> ! { diff --git a/tracker/agb-tracker/examples/timing.rs b/tracker/agb-tracker/examples/timing.rs index 996484b7..ff0a8e6e 100644 --- a/tracker/agb-tracker/examples/timing.rs +++ b/tracker/agb-tracker/examples/timing.rs @@ -3,9 +3,9 @@ use agb::sound::mixer::Frequency; use agb::Gba; -use agb_tracker::{import_xm, Track, Tracker}; +use agb_tracker::{include_xm, Track, Tracker}; -const DB_TOFFE: Track = import_xm!("examples/db_toffe.xm"); +const DB_TOFFE: Track = include_xm!("examples/db_toffe.xm"); #[agb::entry] fn main(mut gba: Gba) -> ! { diff --git a/tracker/agb-tracker/src/lib.rs b/tracker/agb-tracker/src/lib.rs index e88a1e93..eecf2a32 100644 --- a/tracker/agb-tracker/src/lib.rs +++ b/tracker/agb-tracker/src/lib.rs @@ -27,9 +27,9 @@ //! #![no_main] //! //! use agb::{Gba, sound::mixer::Frequency}; -//! use agb_tracker::{import_xm, Track, Tracker}; +//! use agb_tracker::{include_xm, Track, Tracker}; //! -//! const DB_TOFFE: Track = import_xm!("examples/db_toffe.xm"); +//! const DB_TOFFE: Track = include_xm!("examples/db_toffe.xm"); //! //! #[agb::entry] //! fn main(mut gba: Gba) -> ! { @@ -75,7 +75,7 @@ use agb::{ /// Import an XM file. Only available if you have the `xm` feature enabled (enabled by default). #[cfg(feature = "xm")] -pub use agb_xm::import_xm; +pub use agb_xm::include_xm; #[doc(hidden)] pub mod __private { @@ -83,7 +83,7 @@ pub mod __private { pub use agb_tracker_interop; } -/// A reference to a track. You should create this using one of the import macros. +/// 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. diff --git a/tracker/agb-xm/src/lib.rs b/tracker/agb-xm/src/lib.rs index 0ccc3918..bea1dcb4 100644 --- a/tracker/agb-xm/src/lib.rs +++ b/tracker/agb-xm/src/lib.rs @@ -3,6 +3,6 @@ use proc_macro_error::proc_macro_error; #[proc_macro_error] #[proc_macro] -pub fn import_xm(args: TokenStream) -> TokenStream { +pub fn include_xm(args: TokenStream) -> TokenStream { agb_xm_core::agb_xm_core(args.into()).into() } From 21d1d1385afefcbe2018771fdde58ed8da689018 Mon Sep 17 00:00:00 2001 From: Gwilym Inzani Date: Sun, 23 Jul 2023 20:31:46 +0100 Subject: [PATCH 52/65] Remove unused tracker files and add where I found db_toffe --- tracker/agb-tracker/examples/ajoj.xm | Bin 18757 -> 0 bytes tracker/agb-tracker/examples/basic.rs | 1 + tracker/agb-tracker/examples/final_countdown.xm | Bin 69650 -> 0 bytes tracker/agb-tracker/examples/timing.rs | 1 + 4 files changed, 2 insertions(+) delete mode 100644 tracker/agb-tracker/examples/ajoj.xm delete mode 100644 tracker/agb-tracker/examples/final_countdown.xm diff --git a/tracker/agb-tracker/examples/ajoj.xm b/tracker/agb-tracker/examples/ajoj.xm deleted file mode 100644 index e47bd2f6e8a65497628959001bbcbc8576b586ce..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18757 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 diff --git a/tracker/agb-tracker/examples/basic.rs b/tracker/agb-tracker/examples/basic.rs index 260e8735..bab1e138 100644 --- a/tracker/agb-tracker/examples/basic.rs +++ b/tracker/agb-tracker/examples/basic.rs @@ -5,6 +5,7 @@ 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] diff --git a/tracker/agb-tracker/examples/final_countdown.xm b/tracker/agb-tracker/examples/final_countdown.xm deleted file mode 100644 index 51031553b78969d15d4704e882449e71e02a242f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 69650 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 gD1YQJHDkt%X*77?HKY0sZ8U0F Date: Sun, 23 Jul 2023 20:36:54 +0100 Subject: [PATCH 53/65] Do the correct type of building for the tracker --- justfile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/justfile b/justfile index 216bc75f..3a7b0195 100644 --- a/justfile +++ b/justfile @@ -5,8 +5,10 @@ build: build-roms build-debug: just _build-debug agb + just _build-debug tracker/agb-tracker build-release: just _build-release agb + just _build-release tracker/agb-tracker clippy: just _all-crates _clippy just _clippy tools @@ -22,7 +24,7 @@ test: test-release: just _test-release agb just _test-release agb-fixnum - just _test-debug tracker/agb-tracker + just _test-release tracker/agb-tracker just _test-release-arm agb doctest-agb: From 0911ca44c59eb2320c2ce22bbbb26e7b3d00e684 Mon Sep 17 00:00:00 2001 From: Gwilym Inzani Date: Sun, 23 Jul 2023 20:54:24 +0100 Subject: [PATCH 54/65] Try a new song and add memory support --- .../examples/algar_-_ninja_on_speed.xm | Bin 0 -> 54255 bytes tracker/agb-tracker/examples/basic.rs | 2 +- tracker/agb-tracker/src/lib.rs | 2 +- tracker/agb-xm-core/src/lib.rs | 17 +++++++++++++---- 4 files changed, 15 insertions(+), 6 deletions(-) create mode 100644 tracker/agb-tracker/examples/algar_-_ninja_on_speed.xm diff --git a/tracker/agb-tracker/examples/algar_-_ninja_on_speed.xm b/tracker/agb-tracker/examples/algar_-_ninja_on_speed.xm new file mode 100644 index 0000000000000000000000000000000000000000..9f08454498aac9e5bc420d7cf37644a4c314f778 GIT binary patch literal 54255 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 literal 0 HcmV?d00001 diff --git a/tracker/agb-tracker/examples/basic.rs b/tracker/agb-tracker/examples/basic.rs index bab1e138..e09bc425 100644 --- a/tracker/agb-tracker/examples/basic.rs +++ b/tracker/agb-tracker/examples/basic.rs @@ -6,7 +6,7 @@ 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"); +const DB_TOFFE: Track = include_xm!("examples/algar_-_ninja_on_speed.xm"); #[agb::entry] fn main(mut gba: Gba) -> ! { diff --git a/tracker/agb-tracker/src/lib.rs b/tracker/agb-tracker/src/lib.rs index eecf2a32..4a349e3d 100644 --- a/tracker/agb-tracker/src/lib.rs +++ b/tracker/agb-tracker/src/lib.rs @@ -124,7 +124,7 @@ impl Tracker { tick: 0, current_row: 0, - current_pattern: 0, + current_pattern: 2, } } diff --git a/tracker/agb-xm-core/src/lib.rs b/tracker/agb-xm-core/src/lib.rs index c75d076e..68455a56 100644 --- a/tracker/agb-xm-core/src/lib.rs +++ b/tracker/agb-xm-core/src/lib.rs @@ -106,6 +106,8 @@ pub fn parse_module(module: &Module) -> TokenStream { let mut patterns = vec![]; let mut pattern_data = vec![]; + let mut effect_parameters = [0; u8::MAX as usize]; + for pattern in &module.pattern { let start_pos = pattern_data.len(); @@ -166,6 +168,13 @@ pub fn parse_module(module: &Module) -> TokenStream { }; } + 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 { @@ -203,7 +212,7 @@ pub fn parse_module(module: &Module) -> TokenStream { let c4_speed = note_to_speed(Note::C4, 0.0, 0, module.frequency_type); let speed = note_to_speed( Note::C4, - slot.effect_parameter as f64, + effect_parameter as f64, 0, module.frequency_type, ); @@ -216,7 +225,7 @@ pub fn parse_module(module: &Module) -> TokenStream { let c4_speed = note_to_speed(Note::C4, 0.0, 0, module.frequency_type); let speed = note_to_speed( Note::C4, - -(slot.effect_parameter as f64), + -(effect_parameter as f64), 0, module.frequency_type, ); @@ -229,8 +238,8 @@ pub fn parse_module(module: &Module) -> TokenStream { PatternEffect::Panning(Num::new(slot.effect_parameter as i16 - 128) / 128) } 0xA => { - let first = slot.effect_parameter >> 4; - let second = slot.effect_parameter & 0xF; + let first = effect_parameter >> 4; + let second = effect_parameter & 0xF; if first == 0 { PatternEffect::VolumeSlide(-Num::new(second as i16) / 16) From f6a05178db2b961371238b3b3b2a91d227b9e481 Mon Sep 17 00:00:00 2001 From: Gwilym Inzani Date: Sun, 23 Jul 2023 21:36:02 +0100 Subject: [PATCH 55/65] Fix slides and add the portamento one --- tracker/agb-tracker-interop/src/lib.rs | 7 +++++ tracker/agb-tracker/src/lib.rs | 20 ++++++++----- tracker/agb-xm-core/src/lib.rs | 39 +++++++++++++++++++++++++- 3 files changed, 58 insertions(+), 8 deletions(-) diff --git a/tracker/agb-tracker-interop/src/lib.rs b/tracker/agb-tracker-interop/src/lib.rs index 38388b96..4e787bdc 100644 --- a/tracker/agb-tracker-interop/src/lib.rs +++ b/tracker/agb-tracker-interop/src/lib.rs @@ -50,6 +50,8 @@ pub enum PatternEffect { VolumeSlide(Num), NoteCut(u32), Portamento(Num), + /// Slide each tick the first amount to at most the second amount + TonePortamento(Num, Num), } #[cfg(feature = "quote")] @@ -208,6 +210,11 @@ impl quote::ToTokens for PatternEffect { 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! { diff --git a/tracker/agb-tracker/src/lib.rs b/tracker/agb-tracker/src/lib.rs index 4a349e3d..1f4faca4 100644 --- a/tracker/agb-tracker/src/lib.rs +++ b/tracker/agb-tracker/src/lib.rs @@ -124,7 +124,7 @@ impl Tracker { tick: 0, current_row: 0, - current_pattern: 2, + current_pattern: 6, } } @@ -278,13 +278,19 @@ impl TrackerChannel { } } PatternEffect::Portamento(amount) => { - let mut new_speed = self.base_speed; - - for _ in 0..tick { - new_speed *= amount.change_base(); + 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()); } - - channel.playback(new_speed); } } } diff --git a/tracker/agb-xm-core/src/lib.rs b/tracker/agb-xm-core/src/lib.rs index 68455a56..553179f7 100644 --- a/tracker/agb-xm-core/src/lib.rs +++ b/tracker/agb-xm-core/src/lib.rs @@ -137,6 +137,7 @@ pub fn parse_module(module: &Module) -> TokenStream { 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; @@ -234,6 +235,40 @@ pub fn parse_module(module: &Module) -> TokenStream { PatternEffect::Portamento(portamento_amount.try_change_base().unwrap()) } + 0x3 => { + if let Some((previous_note, sample)) = previous_note_and_sample { + // we want to pitch slide to at most the current note by the parameter amount + let c4_speed = note_to_speed(Note::C4, 0.0, 0, module.frequency_type); + + let direction = if (previous_note as usize) < slot.note as usize { + -1.0 + } else { + 1.0 + }; + let speed = note_to_speed( + Note::C4, + effect_parameter as f64 * direction, + 0, + module.frequency_type, + ); + + let portamento_amount = speed / c4_speed; + + let target_speed = note_to_speed( + slot.note, + sample.fine_tune, + sample.relative_note, + module.frequency_type, + ); + + 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) } @@ -356,7 +391,9 @@ fn note_to_frequency_linear(note: Note, fine_tune: f64, relative_note: i8) -> f6 } fn note_to_frequency_amega(note: Note, fine_tune: f64, relative_note: i8) -> f64 { - let note = (note as usize) + relative_note as usize; + 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(); From 095723bbf898c05eedaee9c0c2bf58f3bf3887da Mon Sep 17 00:00:00 2001 From: Gwilym Inzani Date: Sun, 23 Jul 2023 22:03:32 +0100 Subject: [PATCH 56/65] FineVolumeSlide --- tracker/agb-tracker-interop/src/lib.rs | 5 +++++ tracker/agb-tracker/src/lib.rs | 17 ++++++++++------- tracker/agb-xm-core/src/lib.rs | 12 ++++++++++++ 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/tracker/agb-tracker-interop/src/lib.rs b/tracker/agb-tracker-interop/src/lib.rs index 4e787bdc..61b4deb9 100644 --- a/tracker/agb-tracker-interop/src/lib.rs +++ b/tracker/agb-tracker-interop/src/lib.rs @@ -48,6 +48,7 @@ pub enum PatternEffect { Panning(Num), Volume(Num), VolumeSlide(Num), + FineVolumeSlide(Num), NoteCut(u32), Portamento(Num), /// Slide each tick the first amount to at most the second amount @@ -205,6 +206,10 @@ impl quote::ToTokens for PatternEffect { 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(); diff --git a/tracker/agb-tracker/src/lib.rs b/tracker/agb-tracker/src/lib.rs index 1f4faca4..22ffb5d0 100644 --- a/tracker/agb-tracker/src/lib.rs +++ b/tracker/agb-tracker/src/lib.rs @@ -124,7 +124,7 @@ impl Tracker { tick: 0, current_row: 0, - current_pattern: 6, + current_pattern: 0x10, } } @@ -143,7 +143,7 @@ impl Tracker { 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) { + for (channel, pattern_slot) in self.channels.iter_mut().zip(pattern_slots).skip(3) { if pattern_slot.sample != 0 && self.tick == 0 { let sample = &self.track.samples[pattern_slot.sample as usize - 1]; channel.play_sound(mixer, sample); @@ -219,7 +219,7 @@ impl TrackerChannel { } self.channel_id = mixer.play_sound(new_channel); - self.volume = 1.into(); + self.volume = sample.volume; } fn set_speed(&mut self, mixer: &mut Mixer<'_>, speed: Num) { @@ -264,10 +264,13 @@ impl TrackerChannel { } PatternEffect::VolumeSlide(amount) => { if tick != 0 { - self.volume += *amount; - if self.volume < 0.into() { - self.volume = 0.into(); - } + self.volume = (self.volume + *amount).max(0.into()); + channel.volume(self.volume); + } + } + PatternEffect::FineVolumeSlide(amount) => { + if tick == 0 { + self.volume = (self.volume + *amount).max(0.into()); channel.volume(self.volume); } } diff --git a/tracker/agb-xm-core/src/lib.rs b/tracker/agb-xm-core/src/lib.rs index 553179f7..34fd935c 100644 --- a/tracker/agb-xm-core/src/lib.rs +++ b/tracker/agb-xm-core/src/lib.rs @@ -162,6 +162,12 @@ pub fn parse_module(module: &Module) -> TokenStream { .map(|note_and_sample| note_and_sample.1.volume) .unwrap_or(1.into()), ), + 0x80..=0x8F => PatternEffect::FineVolumeSlide( + -Num::new((slot.volume - 0x80) as i16) / 16, + ), + 0x90..=0x9F => PatternEffect::FineVolumeSlide( + Num::new((slot.volume - 0x90) as i16) / 16, + ), 0xC0..=0xCF => PatternEffect::Panning( Num::new(slot.volume as i16 - (0xC0 + (0xCF - 0xC0) / 2)) / 64, ), @@ -292,6 +298,12 @@ pub fn parse_module(module: &Module) -> TokenStream { } } 0xE => match slot.effect_parameter >> 4 { + 0xA => PatternEffect::FineVolumeSlide( + Num::new((slot.effect_parameter & 0xf) as i16) / 16, + ), + 0xB => PatternEffect::FineVolumeSlide( + -Num::new((slot.effect_parameter & 0xf) as i16) / 16, + ), 0xC => PatternEffect::NoteCut((slot.effect_parameter & 0xf).into()), _ => PatternEffect::None, }, From d233a2539c5d522316e53a957de8b1fdfa5cf44d Mon Sep 17 00:00:00 2001 From: Gwilym Inzani Date: Sun, 23 Jul 2023 22:10:25 +0100 Subject: [PATCH 57/65] Improve accuracy of the volume --- tracker/agb-tracker-interop/src/lib.rs | 8 ++++---- tracker/agb-tracker/src/lib.rs | 20 ++++++++++---------- tracker/agb-xm-core/src/lib.rs | 12 ++++++------ 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/tracker/agb-tracker-interop/src/lib.rs b/tracker/agb-tracker-interop/src/lib.rs index 61b4deb9..26026d9a 100644 --- a/tracker/agb-tracker-interop/src/lib.rs +++ b/tracker/agb-tracker-interop/src/lib.rs @@ -19,7 +19,7 @@ pub struct Sample<'a> { pub data: &'a [u8], pub should_loop: bool, pub restart_point: u32, - pub volume: Num, + pub volume: Num, } #[derive(Debug)] @@ -46,9 +46,9 @@ pub enum PatternEffect { /// Plays an arpeggiation of three notes in one row, cycling betwen the current note, current note + first speed, current note + second speed Arpeggio(Num, Num), Panning(Num), - Volume(Num), - VolumeSlide(Num), - FineVolumeSlide(Num), + Volume(Num), + VolumeSlide(Num), + FineVolumeSlide(Num), NoteCut(u32), Portamento(Num), /// Slide each tick the first amount to at most the second amount diff --git a/tracker/agb-tracker/src/lib.rs b/tracker/agb-tracker/src/lib.rs index 22ffb5d0..297b40bc 100644 --- a/tracker/agb-tracker/src/lib.rs +++ b/tracker/agb-tracker/src/lib.rs @@ -102,7 +102,7 @@ pub struct Tracker { struct TrackerChannel { channel_id: Option, base_speed: Num, - volume: Num, + volume: Num, } impl Tracker { @@ -143,7 +143,7 @@ impl Tracker { 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).skip(3) { + 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); @@ -210,7 +210,7 @@ impl TrackerChannel { let mut new_channel = SoundChannel::new(sample.data); - new_channel.volume(sample.volume); + new_channel.volume(sample.volume.change_base()); if sample.should_loop { new_channel @@ -219,7 +219,7 @@ impl TrackerChannel { } self.channel_id = mixer.play_sound(new_channel); - self.volume = sample.volume; + self.volume = sample.volume.change_base(); } fn set_speed(&mut self, mixer: &mut Mixer<'_>, speed: Num) { @@ -259,19 +259,19 @@ impl TrackerChannel { channel.panning(*panning); } PatternEffect::Volume(volume) => { - channel.volume(*volume); - self.volume = *volume; + channel.volume(volume.change_base()); + self.volume = volume.change_base(); } PatternEffect::VolumeSlide(amount) => { if tick != 0 { - self.volume = (self.volume + *amount).max(0.into()); - channel.volume(self.volume); + 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).max(0.into()); - channel.volume(self.volume); + self.volume = (self.volume + amount.change_base()).max(0.into()); + channel.volume(self.volume.try_change_base().unwrap()); } } PatternEffect::NoteCut(wait) => { diff --git a/tracker/agb-xm-core/src/lib.rs b/tracker/agb-xm-core/src/lib.rs index 34fd935c..1bf6803a 100644 --- a/tracker/agb-xm-core/src/lib.rs +++ b/tracker/agb-xm-core/src/lib.rs @@ -55,7 +55,7 @@ pub fn parse_module(module: &Module) -> TokenStream { fine_tune: f64, relative_note: i8, restart_point: u32, - volume: Num, + volume: Num, } let mut samples = vec![]; @@ -76,7 +76,7 @@ pub fn parse_module(module: &Module) -> TokenStream { usize::MAX }; - let volume = Num::from_raw((sample.volume * (1 << 4) as f32) as i16); + let volume = Num::from_raw((sample.volume * (1 << 8) as f32) as i16); let sample = match &sample.data { SampleDataType::Depth8(depth8) => depth8 @@ -163,10 +163,10 @@ pub fn parse_module(module: &Module) -> TokenStream { .unwrap_or(1.into()), ), 0x80..=0x8F => PatternEffect::FineVolumeSlide( - -Num::new((slot.volume - 0x80) as i16) / 16, + -Num::new((slot.volume - 0x80) as i16) / 64, ), 0x90..=0x9F => PatternEffect::FineVolumeSlide( - Num::new((slot.volume - 0x90) as i16) / 16, + Num::new((slot.volume - 0x90) as i16) / 64, ), 0xC0..=0xCF => PatternEffect::Panning( Num::new(slot.volume as i16 - (0xC0 + (0xCF - 0xC0) / 2)) / 64, @@ -299,10 +299,10 @@ pub fn parse_module(module: &Module) -> TokenStream { } 0xE => match slot.effect_parameter >> 4 { 0xA => PatternEffect::FineVolumeSlide( - Num::new((slot.effect_parameter & 0xf) as i16) / 16, + Num::new((slot.effect_parameter & 0xf) as i16) / 64, ), 0xB => PatternEffect::FineVolumeSlide( - -Num::new((slot.effect_parameter & 0xf) as i16) / 16, + -Num::new((slot.effect_parameter & 0xf) as i16) / 64, ), 0xC => PatternEffect::NoteCut((slot.effect_parameter & 0xf).into()), _ => PatternEffect::None, From a4df095031884b6bd3f3cbf1e484c5c8ad6873df Mon Sep 17 00:00:00 2001 From: Gwilym Inzani Date: Sun, 23 Jul 2023 22:15:30 +0100 Subject: [PATCH 58/65] Improve accuracy of the panning and volume until the last second --- agb/src/sound/mixer/mod.rs | 8 ++++---- agb/src/sound/mixer/sw_mixer.rs | 7 +++++-- tracker/agb-tracker/src/lib.rs | 2 +- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/agb/src/sound/mixer/mod.rs b/agb/src/sound/mixer/mod.rs index 7cb4c055..f3dc224c 100644 --- a/agb/src/sound/mixer/mod.rs +++ b/agb/src/sound/mixer/mod.rs @@ -229,9 +229,9 @@ pub struct SoundChannel { restart_point: Num, playback_speed: Num, - volume: Num, // between 0 and 1 + volume: Num, // between 0 and 1 - panning: Num, // between -1 and 1 + panning: Num, // between -1 and 1 is_done: bool, is_stereo: bool, @@ -366,7 +366,7 @@ impl SoundChannel { /// Defaults to 0 (meaning equal on left and right) and doesn't affect stereo /// sounds. #[inline(always)] - pub fn panning(&mut self, panning: impl Into>) -> &mut Self { + pub fn panning(&mut self, panning: impl Into>) -> &mut Self { let panning = panning.into(); debug_assert!(panning >= Num::new(-1), "panning value must be >= -1"); @@ -381,7 +381,7 @@ impl SoundChannel { /// /// Must be a value >= 0 and defaults to 1. #[inline(always)] - pub fn volume(&mut self, volume: impl Into>) -> &mut Self { + pub fn volume(&mut self, volume: impl Into>) -> &mut Self { let volume = volume.into(); assert!(volume >= Num::new(0), "volume must be >= 0"); diff --git a/agb/src/sound/mixer/sw_mixer.rs b/agb/src/sound/mixer/sw_mixer.rs index 26556f97..2fa951a1 100644 --- a/agb/src/sound/mixer/sw_mixer.rs +++ b/agb/src/sound/mixer/sw_mixer.rs @@ -449,14 +449,14 @@ impl MixerBuffer { agb_rs__mixer_add_stereo_first( channel.data.as_ptr().add(channel.pos.floor() as usize), working_buffer.as_mut_ptr(), - channel.volume, + 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, + channel.volume.change_base(), self.frequency.buffer_size(), ); } @@ -475,6 +475,9 @@ impl MixerBuffer { let right_amount = ((channel.panning + 1) / 2) * channel.volume; let left_amount = ((-channel.panning + 1) / 2) * channel.volume; + let right_amount: Num = right_amount.change_base(); + let left_amount: Num = left_amount.change_base(); + let channel_len = Num::::new(channel.data.len() as u32); let mut playback_speed = channel.playback_speed; diff --git a/tracker/agb-tracker/src/lib.rs b/tracker/agb-tracker/src/lib.rs index 297b40bc..6c100e6a 100644 --- a/tracker/agb-tracker/src/lib.rs +++ b/tracker/agb-tracker/src/lib.rs @@ -256,7 +256,7 @@ impl TrackerChannel { }; } PatternEffect::Panning(panning) => { - channel.panning(*panning); + channel.panning(panning.change_base()); } PatternEffect::Volume(volume) => { channel.volume(volume.change_base()); From 25ee8769324a5aa89cf004e024f8d6b4dcbff57e Mon Sep 17 00:00:00 2001 From: Gwilym Inzani Date: Sun, 23 Jul 2023 23:36:19 +0100 Subject: [PATCH 59/65] Fix a bunch of bugs after playing with different tracks --- tracker/agb-tracker/examples/basic.rs | 2 +- tracker/agb-tracker/src/lib.rs | 2 +- tracker/agb-xm-core/src/lib.rs | 48 ++++++++++++++++----------- 3 files changed, 31 insertions(+), 21 deletions(-) diff --git a/tracker/agb-tracker/examples/basic.rs b/tracker/agb-tracker/examples/basic.rs index e09bc425..bab1e138 100644 --- a/tracker/agb-tracker/examples/basic.rs +++ b/tracker/agb-tracker/examples/basic.rs @@ -6,7 +6,7 @@ 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/algar_-_ninja_on_speed.xm"); +const DB_TOFFE: Track = include_xm!("examples/db_toffe.xm"); #[agb::entry] fn main(mut gba: Gba) -> ! { diff --git a/tracker/agb-tracker/src/lib.rs b/tracker/agb-tracker/src/lib.rs index 6c100e6a..a80aed1b 100644 --- a/tracker/agb-tracker/src/lib.rs +++ b/tracker/agb-tracker/src/lib.rs @@ -124,7 +124,7 @@ impl Tracker { tick: 0, current_row: 0, - current_pattern: 0x10, + current_pattern: 0, } } diff --git a/tracker/agb-xm-core/src/lib.rs b/tracker/agb-xm-core/src/lib.rs index 1bf6803a..537aeb68 100644 --- a/tracker/agb-xm-core/src/lib.rs +++ b/tracker/agb-xm-core/src/lib.rs @@ -106,14 +106,13 @@ pub fn parse_module(module: &Module) -> TokenStream { let mut patterns = vec![]; let mut pattern_data = vec![]; - let mut effect_parameters = [0; u8::MAX as usize]; - 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() { - let mut note_and_sample = vec![None; module.get_num_channels()]; - for (i, slot) in row.iter().enumerate() { let channel_number = i % module.get_num_channels(); @@ -162,6 +161,12 @@ pub fn parse_module(module: &Module) -> TokenStream { .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, ), @@ -169,7 +174,7 @@ pub fn parse_module(module: &Module) -> TokenStream { Num::new((slot.volume - 0x90) as i16) / 64, ), 0xC0..=0xCF => PatternEffect::Panning( - Num::new(slot.volume as i16 - (0xC0 + (0xCF - 0xC0) / 2)) / 64, + Num::new(slot.volume as i16 - (0xC0 + (0xCF - 0xC0) / 2)) / 8, ), _ => PatternEffect::None, }; @@ -242,15 +247,27 @@ pub fn parse_module(module: &Module) -> TokenStream { PatternEffect::Portamento(portamento_amount.try_change_base().unwrap()) } 0x3 => { - if let Some((previous_note, sample)) = previous_note_and_sample { - // we want to pitch slide to at most the current note by the parameter amount - let c4_speed = note_to_speed(Note::C4, 0.0, 0, module.frequency_type); + 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 = if (previous_note as usize) < slot.note as usize { - -1.0 - } else { - 1.0 + 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, @@ -260,13 +277,6 @@ pub fn parse_module(module: &Module) -> TokenStream { let portamento_amount = speed / c4_speed; - let target_speed = note_to_speed( - slot.note, - sample.fine_tune, - sample.relative_note, - module.frequency_type, - ); - PatternEffect::TonePortamento( portamento_amount.try_change_base().unwrap(), target_speed.try_change_base().unwrap(), From df75d1ce8cc173805ae7e3d246339c30e711d33d Mon Sep 17 00:00:00 2001 From: Gwilym Inzani Date: Sun, 23 Jul 2023 23:39:52 +0100 Subject: [PATCH 60/65] Fix build errors --- agb/examples/mixer_basic.rs | 2 +- examples/the-purple-night/src/sfx.rs | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/agb/examples/mixer_basic.rs b/agb/examples/mixer_basic.rs index 97403e8b..ad29463c 100644 --- a/agb/examples/mixer_basic.rs +++ b/agb/examples/mixer_basic.rs @@ -25,7 +25,7 @@ fn main(mut gba: Gba) -> ! { { if let Some(channel) = mixer.channel(&channel_id) { - let half: Num = num!(0.5); + let half: Num = num!(0.5); let half_usize: Num = num!(0.5); match input.x_tri() { Tri::Negative => channel.panning(-half), diff --git a/examples/the-purple-night/src/sfx.rs b/examples/the-purple-night/src/sfx.rs index 445dd148..6c604f7b 100644 --- a/examples/the-purple-night/src/sfx.rs +++ b/examples/the-purple-night/src/sfx.rs @@ -1,4 +1,4 @@ -use agb::fixnum::Num; +use agb::fixnum::num; use agb::rng; use agb::sound::mixer::{ChannelId, Mixer, SoundChannel}; @@ -98,8 +98,7 @@ impl<'a> Sfx<'a> { pub fn slime_boing(&mut self) { let mut channel = SoundChannel::new(SLIME_BOING); - let one: Num = 1.into(); - channel.volume(one / 4); + channel.volume(num!(0.25)); self.mixer.play_sound(channel); } From de666a54f9da53d793548295f8764f463178b014 Mon Sep 17 00:00:00 2001 From: Gwilym Inzani Date: Mon, 24 Jul 2023 23:59:54 +0100 Subject: [PATCH 61/65] Core loop for the tracker in assembly --- agb/src/sound/mixer/mixer.s | 56 +++++++++++++++ agb/src/sound/mixer/sw_mixer.rs | 123 ++++++++++++++++++++++++-------- 2 files changed, 151 insertions(+), 28 deletions(-) diff --git a/agb/src/sound/mixer/mixer.s b/agb/src/sound/mixer/mixer.s index c1101dc2..e6cbe929 100644 --- a/agb/src/sound/mixer/mixer.s +++ b/agb/src/sound/mixer/mixer.s @@ -1,3 +1,59 @@ +.macro mono_add_fn_loop fn_name:req is_first:req +agb_arm_func \fn_name + @ Arguments + @ r0 - pointer to the sample data from the beginning + @ r1 - pointer to the target sample buffer &[i32; BUFFER_SIZE] + @ r2 - BUFFER_SIZE - the length of the array in r1. Must be a multiple of 4 + @ r3 - (length - restart point) (how much to rewind by) + @ 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 + @ + @ Returns the new channel position + push {{r4-r11,lr}} + + ldr r4, [sp, #(9*4)] @ load the channel length into r4 + ldr r5, [sp, #(10*4)] @ load the current channel position into r5 + ldr r6, [sp, #(11*4)] @ load the playback speed into r6 + ldr r12, [sp, #(12*4)] @ load the amount to multiply by into r12 + +@ The core loop +1: +.ifc \is_first,false + ldm r1, {{r7-r10}} +.endif + +.irp reg, r7,r8,r9,r10 + cmp r4, r5, lsr #8 @ check if we're overflowing + suble r5, r5, r3 @ if we are, subtract the overflow amount + + 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 + + stmia r1!, {{r7-r10}} + + subs r2, r2, #4 + bne 1b + + mov r0, r5 @ return the playback position + pop {{r4-r11,lr}} + + bx lr +agb_arm_end \fn_name +.endm + +mono_add_fn_loop agb_rs__mixer_add_mono_loop_first true +mono_add_fn_loop agb_rs__mixer_add_mono_loop false + .macro stereo_add_fn fn_name:req is_first:req agb_arm_func \fn_name @ Arguments diff --git a/agb/src/sound/mixer/sw_mixer.rs b/agb/src/sound/mixer/sw_mixer.rs index 2fa951a1..f572fb6b 100644 --- a/agb/src/sound/mixer/sw_mixer.rs +++ b/agb/src/sound/mixer/sw_mixer.rs @@ -40,6 +40,28 @@ extern "C" { input_buffer: *const Num, num_samples: usize, ); + + fn agb_rs__mixer_add_mono_loop_first( + sample_data: *const u8, + sample_buffer: *mut i32, + buffer_size: usize, + restart_amount: Num, + channel_length: usize, + current_pos: Num, + playback_speed: Num, + mul_amount: i32, + ) -> Num; + + fn agb_rs__mixer_add_mono_loop( + sample_data: *const u8, + sample_buffer: *mut i32, + buffer_size: usize, + restart_amount: Num, + channel_length: usize, + current_pos: Num, + playback_speed: Num, + mul_amount: i32, + ) -> Num; } /// The main software mixer struct. @@ -496,42 +518,57 @@ impl MixerBuffer { let mul_amount = ((left_amount.to_raw() as i32) << 16) | (right_amount.to_raw() as i32 & 0x0000ffff); - for i in 0..self.frequency.buffer_size() { - if channel.pos >= channel_len { - if channel.should_loop { - channel.pos -= channel_len - channel.restart_point; - } else { + if IS_FIRST && channel.should_loop { + channel.pos = unsafe { + agb_rs__mixer_add_mono_loop_first( + 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, + ) + }; + } else if !IS_FIRST && channel.should_loop { + channel.pos = unsafe { + agb_rs__mixer_add_mono_loop( + 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, + ) + }; + } else { + for i in 0..self.frequency.buffer_size() { + if channel.pos >= channel_len { channel.is_done = true; - if IS_FIRST { - for j in i..self.frequency.buffer_size() { - // SAFETY: working buffer length = self.frequency.buffer_size() - unsafe { - *working_buffer_i32.get_unchecked_mut(j) = 0; - } - } - } - break; } - } - // SAFETY: channel.pos < channel_len by the above if statement and the fact we reduce the playback speed - let value = - unsafe { *channel.data.get_unchecked(channel.pos.floor() as usize) } as i8 as i32; + // SAFETY: channel.pos < channel_len by the above if statement and the fact we reduce the playback speed + let value = unsafe { *channel.data.get_unchecked(channel.pos.floor() as usize) } + as i8 as i32; - // SAFETY: working buffer length = self.frequency.buffer_size() - if IS_FIRST { - unsafe { - *working_buffer_i32.get_unchecked_mut(i) = value.wrapping_mul(mul_amount); + // SAFETY: working buffer length = self.frequency.buffer_size() + if IS_FIRST { + unsafe { + *working_buffer_i32.get_unchecked_mut(i) = value.wrapping_mul(mul_amount); + } + } else { + unsafe { + let value_ref = working_buffer_i32.get_unchecked_mut(i); + *value_ref = value_ref.wrapping_add(value.wrapping_mul(mul_amount)); + }; } - } else { - unsafe { - let value_ref = working_buffer_i32.get_unchecked_mut(i); - *value_ref = value_ref.wrapping_add(value.wrapping_mul(mul_amount)); - }; + channel.pos += playback_speed; } - channel.pos += playback_speed; } } } @@ -608,4 +645,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)); + } } From 49b9a07a87ec2915fd0048c51186224e7d093904 Mon Sep 17 00:00:00 2001 From: Gwilym Inzani Date: Tue, 25 Jul 2023 00:18:48 +0100 Subject: [PATCH 62/65] Give the same assembly treatment to mono sounds --- agb/src/sound/mixer/mixer.s | 43 ++++++++++-- agb/src/sound/mixer/sw_mixer.rs | 113 +++++++++++++------------------- 2 files changed, 83 insertions(+), 73 deletions(-) diff --git a/agb/src/sound/mixer/mixer.s b/agb/src/sound/mixer/mixer.s index e6cbe929..71f3e87b 100644 --- a/agb/src/sound/mixer/mixer.s +++ b/agb/src/sound/mixer/mixer.s @@ -1,4 +1,4 @@ -.macro mono_add_fn_loop fn_name:req is_first:req +.macro mono_add_fn_loop fn_name:req is_first:req is_loop:req agb_arm_func \fn_name @ Arguments @ r0 - pointer to the sample data from the beginning @@ -26,11 +26,15 @@ agb_arm_func \fn_name .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 +.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 @@ -44,6 +48,35 @@ agb_arm_func \fn_name subs r2, r2, #4 bne 1b +.ifc \is_loop,false + b 3f + +2: +.ifc \is_first,true @ zero the rest of the buffer as this sample has ended + ands r7, r2, #3 + sub r2, r2, r7 + beq 5f + +4: + mov r8, #0 +4: + stmia r1!, {{r8}} + subs r7, r7, #1 + bne 4b + +5: +.irp reg, r7,r8,r9,r10 + mov \reg, #0 +.endr +5: + stmia r1!, {{r7-r10}} + subs r2, r2, #4 + bne 5b +.endif +3: +.endif + + mov r0, r5 @ return the playback position pop {{r4-r11,lr}} @@ -51,8 +84,10 @@ agb_arm_func \fn_name agb_arm_end \fn_name .endm -mono_add_fn_loop agb_rs__mixer_add_mono_loop_first true -mono_add_fn_loop agb_rs__mixer_add_mono_loop false +mono_add_fn_loop agb_rs__mixer_add_mono_loop_first true 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 agb_arm_func \fn_name diff --git a/agb/src/sound/mixer/sw_mixer.rs b/agb/src/sound/mixer/sw_mixer.rs index f572fb6b..9ad50bf0 100644 --- a/agb/src/sound/mixer/sw_mixer.rs +++ b/agb/src/sound/mixer/sw_mixer.rs @@ -19,6 +19,21 @@ use crate::{ 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, + channel_length: usize, + current_pos: Num, + playback_speed: Num, + mul_amount: i32, + ) -> Num; + }; +} + // Defined in mixer.s extern "C" { fn agb_rs__mixer_add_stereo( @@ -41,27 +56,10 @@ extern "C" { num_samples: usize, ); - fn agb_rs__mixer_add_mono_loop_first( - sample_data: *const u8, - sample_buffer: *mut i32, - buffer_size: usize, - restart_amount: Num, - channel_length: usize, - current_pos: Num, - playback_speed: Num, - mul_amount: i32, - ) -> Num; - - fn agb_rs__mixer_add_mono_loop( - sample_data: *const u8, - sample_buffer: *mut i32, - buffer_size: usize, - restart_amount: Num, - channel_length: usize, - current_pos: Num, - playback_speed: Num, - mul_amount: i32, - ) -> Num; + 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. @@ -518,56 +516,33 @@ impl MixerBuffer { let mul_amount = ((left_amount.to_raw() as i32) << 16) | (right_amount.to_raw() as i32 & 0x0000ffff); - if IS_FIRST && channel.should_loop { - channel.pos = unsafe { - agb_rs__mixer_add_mono_loop_first( - 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, - ) - }; - } else if !IS_FIRST && channel.should_loop { - channel.pos = unsafe { - agb_rs__mixer_add_mono_loop( - 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, - ) - }; - } else { - for i in 0..self.frequency.buffer_size() { - if channel.pos >= channel_len { - channel.is_done = true; - - break; + 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, + ) } + }; + } - // SAFETY: channel.pos < channel_len by the above if statement and the fact we reduce the playback speed - let value = unsafe { *channel.data.get_unchecked(channel.pos.floor() as usize) } - as i8 as i32; - - // SAFETY: working buffer length = self.frequency.buffer_size() - if IS_FIRST { - unsafe { - *working_buffer_i32.get_unchecked_mut(i) = value.wrapping_mul(mul_amount); - } - } else { - unsafe { - let value_ref = working_buffer_i32.get_unchecked_mut(i); - *value_ref = value_ref.wrapping_add(value.wrapping_mul(mul_amount)); - }; - } - channel.pos += playback_speed; + 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; } } } From f7b3aa8ecbdeeba186253134b0145bb020efa8a7 Mon Sep 17 00:00:00 2001 From: Gwilym Inzani Date: Tue, 25 Jul 2023 00:27:20 +0100 Subject: [PATCH 63/65] Don't need lr to be saved since we're not using it --- agb/src/sound/mixer/mixer.s | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/agb/src/sound/mixer/mixer.s b/agb/src/sound/mixer/mixer.s index 71f3e87b..fa59156f 100644 --- a/agb/src/sound/mixer/mixer.s +++ b/agb/src/sound/mixer/mixer.s @@ -11,12 +11,12 @@ agb_arm_func \fn_name @ Stack position 4 - the amount to multiply by @ @ Returns the new channel position - push {{r4-r11,lr}} + push {{r4-r11}} - ldr r4, [sp, #(9*4)] @ load the channel length into r4 - ldr r5, [sp, #(10*4)] @ load the current channel position into r5 - ldr r6, [sp, #(11*4)] @ load the playback speed into r6 - ldr r12, [sp, #(12*4)] @ load the amount to multiply by into r12 + 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: @@ -57,7 +57,6 @@ agb_arm_func \fn_name sub r2, r2, r7 beq 5f -4: mov r8, #0 4: stmia r1!, {{r8}} @@ -76,9 +75,8 @@ agb_arm_func \fn_name 3: .endif - mov r0, r5 @ return the playback position - pop {{r4-r11,lr}} + pop {{r4-r11}} bx lr agb_arm_end \fn_name From e8e5b31d422bfb31470eca11f68bad12d9803ee9 Mon Sep 17 00:00:00 2001 From: Gwilym Inzani Date: Tue, 25 Jul 2023 00:50:47 +0100 Subject: [PATCH 64/65] No need to use a generic here any more --- agb/src/sound/mixer/sw_mixer.rs | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/agb/src/sound/mixer/sw_mixer.rs b/agb/src/sound/mixer/sw_mixer.rs index 9ad50bf0..21bd0d0c 100644 --- a/agb/src/sound/mixer/sw_mixer.rs +++ b/agb/src/sound/mixer/sw_mixer.rs @@ -419,9 +419,9 @@ impl MixerBuffer { if let Some(channel) = channels.next() { if channel.is_stereo { - self.write_stereo::(channel, working_buffer); + self.write_stereo(channel, working_buffer, true); } else { - self.write_mono::(channel, working_buffer); + self.write_mono(channel, working_buffer, true); } } else { working_buffer.fill(0.into()); @@ -429,9 +429,9 @@ impl MixerBuffer { for channel in channels { if channel.is_stereo { - self.write_stereo::(channel, working_buffer); + self.write_stereo(channel, working_buffer, false); } else { - self.write_mono::(channel, working_buffer); + self.write_mono(channel, working_buffer, false); } } @@ -446,10 +446,11 @@ impl MixerBuffer { } } - fn write_stereo( + fn write_stereo( &self, channel: &mut SoundChannel, working_buffer: &mut [Num], + is_first: bool, ) { if (channel.pos + 2 * self.frequency.buffer_size() as u32).floor() >= channel.data.len() as u32 @@ -458,14 +459,14 @@ impl MixerBuffer { channel.pos = channel.restart_point * 2; } else { channel.is_done = true; - if IS_FIRST { + if is_first { working_buffer.fill(0.into()); } return; } } unsafe { - if IS_FIRST { + if is_first { agb_rs__mixer_add_stereo_first( channel.data.as_ptr().add(channel.pos.floor() as usize), working_buffer.as_mut_ptr(), @@ -485,12 +486,11 @@ impl MixerBuffer { channel.pos += 2 * self.frequency.buffer_size() as u32; } - #[link_section = ".iwram.write_mono"] - #[inline(never)] - fn write_mono( + fn write_mono( &self, channel: &mut SoundChannel, working_buffer: &mut [Num], + is_first: bool, ) { let right_amount = ((channel.panning + 1) / 2) * channel.volume; let left_amount = ((-channel.panning + 1) / 2) * channel.volume; @@ -533,7 +533,7 @@ impl MixerBuffer { }; } - match (IS_FIRST, channel.should_loop) { + 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) => { From a4a70892516df1aef7aaefcc4b06220044b4ca94 Mon Sep 17 00:00:00 2001 From: Gwilym Inzani Date: Tue, 25 Jul 2023 20:18:21 +0100 Subject: [PATCH 65/65] Add changelog entry and update readme --- CHANGELOG.md | 13 +++++++++++++ README.md | 52 +++++++++++++++++++++++++++------------------------- 2 files changed, 40 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41e6f9e6..4f8394bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [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` rather than `Num` 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 ### Added diff --git a/README.md b/README.md index f7636894..ff9403d4 100644 --- a/README.md +++ b/README.md @@ -16,11 +16,11 @@ without needing to have extensive knowledge of its low-level implementation. agb provides the following features: -* Simple build process with minimal dependencies -* Built in importing of sprites, backgrounds, music and sound effects -* High performance audio mixer -* Easy to use sprite and tiled background usage -* A global allocator allowing for use of both `core` and `alloc` +- Simple build process with minimal dependencies +- Built in importing of sprites, backgrounds, music and sound effects +- High performance audio mixer +- Easy to use sprite and tiled background usage +- A global allocator allowing for use of both `core` and `alloc` The documentation for the latest release can be found on [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! - ## 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 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. - * You can update rustup with `rustup update`, or using your package manager - if you obtained rustup in this way. -* libelf and cmake - * Debian and derivatives: `sudo apt install libelf-dev cmake` - * Arch Linux and derivatives: `pacman -S libelf cmake` -* mgba-test-runner - * Run `cargo install --path mgba-test-runner` inside this directory -* [The 'just' build tool](https://github.com/casey/just) - * Install with `cargo install just` -* [mdbook](https://rust-lang.github.io/mdBook/index.html) - * Install with `cargo install mdbook` -* [miri](https://github.com/rust-lang/miri) - * Some of the unsafe code is tested using miri, install with `rustup component add miri` + - You can update rustup with `rustup update`, or using your package manager + if you obtained rustup in this way. +- libelf and cmake + - Debian and derivatives: `sudo apt install libelf-dev cmake` + - Arch Linux and derivatives: `pacman -S libelf cmake` +- mgba-test-runner + - Run `cargo install --path mgba-test-runner` inside this directory +- [The 'just' build tool](https://github.com/casey/just) + - Install with `cargo install just` +- [mdbook](https://rust-lang.github.io/mdBook/index.html) + - Install with `cargo install mdbook` +- [miri](https://github.com/rust-lang/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 + ```sh 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 ` +`tracker` - crates that make up the `agb-tracker` library which allows playing of tracker files + `book` - the source for the tutorial and website `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: -* 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 -* [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 +- 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 +- [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 ## Licence @@ -128,4 +130,4 @@ changes you wish. The agb logo is released under [Creative Commons Attribution-ShareAlike 4.0](http://creativecommons.org/licenses/by-sa/4.0/) -The music used for the examples is by [Josh Woodward](https://www.joshwoodward.com) and released under [Creative Commons Attribution 4.0](https://creativecommons.org/licenses/by/4.0/) \ No newline at end of file +The music used for the examples is by [Josh Woodward](https://www.joshwoodward.com) and released under [Creative Commons Attribution 4.0](https://creativecommons.org/licenses/by/4.0/)