mirror of
https://github.com/italicsjenga/agb.git
synced 2024-12-23 08:11:33 +11:00
Envelopes (#469)
A combination of the previous PR (#468) and support for volume envelopes in the tracker - [x] Changelog updated / no changelog update needed
This commit is contained in:
commit
18a161269e
|
@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
- New tracker for playing XM files (see the `agb-tracker` crate).
|
||||
- You can now declare where looping sound channels should restart.
|
||||
- Fixnums now have constructors from_f32 and from_f64. This is mainly useful if using agb-fixnum outside of the Game Boy Advance e.g. in build scripts or macros.
|
||||
|
||||
### Changed
|
||||
|
||||
|
|
|
@ -166,7 +166,7 @@ fixed_width_signed_integer_impl!(i16);
|
|||
fixed_width_signed_integer_impl!(i32);
|
||||
|
||||
/// A fixed point number represented using `I` with `N` bits of fractional precision
|
||||
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
#[repr(transparent)]
|
||||
pub struct Num<I: FixedWidthUnsignedInteger, const N: usize>(I);
|
||||
|
||||
|
@ -376,6 +376,20 @@ impl<I: FixedWidthUnsignedInteger, const N: usize> Num<I, N> {
|
|||
self.0
|
||||
}
|
||||
|
||||
/// Lossily transforms an f32 into a fixed point representation. This is not const
|
||||
/// because you cannot currently do floating point operations in const contexts, so
|
||||
/// you should use the `num!` macro from agb-macros if you want a const from_f32/f64
|
||||
pub fn from_f32(input: f32) -> Self {
|
||||
Self::from_raw(I::from_as_i32((input * (1 << N) as f32) as i32))
|
||||
}
|
||||
|
||||
/// Lossily transforms an f64 into a fixed point representation. This is not const
|
||||
/// because you cannot currently do floating point operations in const contexts, so
|
||||
/// you should use the `num!` macro from agb-macros if you want a const from_f32/f64
|
||||
pub fn from_f64(input: f64) -> Self {
|
||||
Self::from_raw(I::from_as_i32((input * (1 << N) as f64) as i32))
|
||||
}
|
||||
|
||||
/// Truncates the fixed point number returning the integral part
|
||||
/// ```rust
|
||||
/// # use agb_fixnum::*;
|
||||
|
@ -631,7 +645,7 @@ impl<I: FixedWidthUnsignedInteger, const N: usize> Debug for Num<I, N> {
|
|||
}
|
||||
|
||||
/// A vector of two points: (x, y) represented by integers or fixed point numbers
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, Hash)]
|
||||
pub struct Vector2D<T: Number> {
|
||||
/// The x coordinate
|
||||
pub x: T,
|
||||
|
@ -880,7 +894,7 @@ impl<I: FixedWidthUnsignedInteger, const N: usize> From<Vector2D<I>> for Vector2
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
|
||||
/// A rectangle with a position in 2d space and a 2d size
|
||||
pub struct Rect<T: Number> {
|
||||
/// The position of the rectangle
|
||||
|
|
258
examples/the-dungeon-puzzlers-lament/Cargo.lock
generated
258
examples/the-dungeon-puzzlers-lament/Cargo.lock
generated
|
@ -52,7 +52,7 @@ dependencies = [
|
|||
"image",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.27",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -61,7 +61,7 @@ version = "0.16.0"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.27",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -71,7 +71,47 @@ dependencies = [
|
|||
"hound",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.27",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "agb_tracker"
|
||||
version = "0.16.0"
|
||||
dependencies = [
|
||||
"agb",
|
||||
"agb_tracker_interop",
|
||||
"agb_xm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "agb_tracker_interop"
|
||||
version = "0.16.0"
|
||||
dependencies = [
|
||||
"agb_fixnum",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "agb_xm"
|
||||
version = "0.16.0"
|
||||
dependencies = [
|
||||
"agb_xm_core",
|
||||
"proc-macro-error",
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "agb_xm_core"
|
||||
version = "0.16.0"
|
||||
dependencies = [
|
||||
"agb_fixnum",
|
||||
"agb_tracker_interop",
|
||||
"proc-macro-error",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.27",
|
||||
"xmrs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -142,7 +182,16 @@ dependencies = [
|
|||
"proc-macro-error",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.27",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bincode"
|
||||
version = "1.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -200,6 +249,12 @@ dependencies = [
|
|||
"byteorder",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.0.26"
|
||||
|
@ -216,10 +271,21 @@ version = "0.7.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0793f5137567643cf65ea42043a538804ff0fbf288649e2141442b602d81f9bc"
|
||||
dependencies = [
|
||||
"hashbrown",
|
||||
"hashbrown 0.13.2",
|
||||
"ttf-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.13.2"
|
||||
|
@ -229,6 +295,12 @@ dependencies = [
|
|||
"ahash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a"
|
||||
|
||||
[[package]]
|
||||
name = "hound"
|
||||
version = "3.5.0"
|
||||
|
@ -250,6 +322,22 @@ dependencies = [
|
|||
"png",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.14.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.147"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
|
||||
|
||||
[[package]]
|
||||
name = "libflate"
|
||||
version = "1.4.0"
|
||||
|
@ -276,6 +364,12 @@ version = "0.4.19"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.3.7"
|
||||
|
@ -341,6 +435,27 @@ dependencies = [
|
|||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num_enum"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a015b430d3c108a207fd776d2e2196aaf8b1cf8cf93253e3a097ff3085076a1"
|
||||
dependencies = [
|
||||
"num_enum_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num_enum_derive"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96667db765a921f7b295ffee8b60472b686a51d4f21c2ee4ffdb94c7013b65a6"
|
||||
dependencies = [
|
||||
"proc-macro-crate",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.27",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.18.0"
|
||||
|
@ -359,6 +474,22 @@ dependencies = [
|
|||
"miniz_oxide 0.3.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-crate"
|
||||
version = "1.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"toml_edit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error"
|
||||
version = "1.0.4"
|
||||
|
@ -368,6 +499,7 @@ dependencies = [
|
|||
"proc-macro-error-attr",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
|
@ -400,6 +532,36 @@ dependencies = [
|
|||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rle-decode-fast"
|
||||
version = "1.0.3"
|
||||
|
@ -412,6 +574,35 @@ version = "1.1.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.179"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0a5bf42b8d227d4abf38a1ddb08602e229108a517cd4e5bb28f9c7eaafdce5c0"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde-big-array"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "11fc7cc2c76d73e0f27ee52abbd64eec84d46f370c88371120433196934e4b7f"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.179"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "741e124f5485c7e60c03b043f79f320bff3527f4bbf12cf3831750dc46a0ec2c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.27",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "slotmap"
|
||||
version = "1.0.6"
|
||||
|
@ -421,6 +612,16 @@ dependencies = [
|
|||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.109"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.27"
|
||||
|
@ -437,6 +638,7 @@ name = "the-dungeon-puzzlers-lament"
|
|||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"agb",
|
||||
"agb_tracker",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"slotmap",
|
||||
|
@ -454,6 +656,23 @@ dependencies = [
|
|||
"xml-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b"
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.19.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8123f27e969974a3dfba720fdb560be359f57b44302d280ba72e76a74480e8a"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"toml_datetime",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ttf-parser"
|
||||
version = "0.15.2"
|
||||
|
@ -472,8 +691,37 @@ version = "0.9.4"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.0+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8bd122eb777186e60c3fdf765a58ac76e41c582f1f535fbf3314434c6b58f3f7"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xml-rs"
|
||||
version = "0.8.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47430998a7b5d499ccee752b41567bc3afc57e1327dc855b1a2aa44ce29b5fa1"
|
||||
|
||||
[[package]]
|
||||
name = "xmrs"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6fa1ec7c01e6bb4c716f84a418f4ced5f4a735b2ae6364f4bb5850da61321d16"
|
||||
dependencies = [
|
||||
"bincode",
|
||||
"libflate",
|
||||
"num_enum",
|
||||
"rand",
|
||||
"serde",
|
||||
"serde-big-array",
|
||||
]
|
||||
|
|
|
@ -9,6 +9,7 @@ edition = "2021"
|
|||
[dependencies]
|
||||
agb = { version = "0.16.0", path = "../../agb" }
|
||||
slotmap = { version = "1", default-features = false }
|
||||
agb_tracker = { version = "0.16.0", path = "../../tracker/agb-tracker" }
|
||||
|
||||
[profile.dev]
|
||||
opt-level = 3
|
||||
|
@ -22,4 +23,4 @@ debug = true
|
|||
[build-dependencies]
|
||||
tiled = { version = "0.11", default-features = false }
|
||||
quote = "1"
|
||||
proc-macro2 = "1"
|
||||
proc-macro2 = "1"
|
||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
examples/the-dungeon-puzzlers-lament/sfx/gwilym-theme2.xm
Normal file
BIN
examples/the-dungeon-puzzlers-lament/sfx/gwilym-theme2.xm
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
examples/the-dungeon-puzzlers-lament/sfx/theme.xm
Normal file
BIN
examples/the-dungeon-puzzlers-lament/sfx/theme.xm
Normal file
Binary file not shown.
Binary file not shown.
|
@ -2,8 +2,10 @@ use agb::{
|
|||
include_wav,
|
||||
sound::mixer::{Mixer, SoundChannel},
|
||||
};
|
||||
use agb_tracker::{include_xm, Track, Tracker};
|
||||
|
||||
const MUSIC: Track = include_xm!("sfx/gwilym-theme2.xm");
|
||||
|
||||
const BGM: &[u8] = include_wav!("sfx/bgm.wav");
|
||||
const BAD_SELECTION: &[u8] = include_wav!("sfx/bad.wav");
|
||||
const SELECT: &[u8] = include_wav!("sfx/select.wav");
|
||||
const PLACE: &[u8] = include_wav!("sfx/place.wav");
|
||||
|
@ -17,20 +19,20 @@ const SWICTH_TOGGLES: &[&[u8]] = &[include_wav!("sfx/switch_toggle1.wav")];
|
|||
|
||||
pub struct Sfx<'a> {
|
||||
mixer: &'a mut Mixer<'a>,
|
||||
tracker: Tracker,
|
||||
}
|
||||
|
||||
impl<'a> Sfx<'a> {
|
||||
pub fn new(mixer: &'a mut Mixer<'a>) -> Self {
|
||||
let mut bgm_channel = SoundChannel::new_high_priority(BGM);
|
||||
bgm_channel.stereo().should_loop();
|
||||
|
||||
mixer.play_sound(bgm_channel);
|
||||
mixer.enable();
|
||||
|
||||
Self { mixer }
|
||||
let tracker = Tracker::new(&MUSIC);
|
||||
|
||||
Self { mixer, tracker }
|
||||
}
|
||||
|
||||
pub fn frame(&mut self) {
|
||||
self.tracker.step(self.mixer);
|
||||
self.mixer.frame();
|
||||
}
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ use agb_fixnum::Num;
|
|||
#[derive(Debug)]
|
||||
pub struct Track<'a> {
|
||||
pub samples: &'a [Sample<'a>],
|
||||
pub envelopes: &'a [Envelope<'a>],
|
||||
pub pattern_data: &'a [PatternSlot],
|
||||
pub patterns: &'a [Pattern],
|
||||
pub patterns_to_play: &'a [usize],
|
||||
|
@ -12,6 +13,7 @@ pub struct Track<'a> {
|
|||
pub num_channels: usize,
|
||||
pub frames_per_tick: Num<u32, 8>,
|
||||
pub ticks_per_step: u32,
|
||||
pub repeat: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -20,6 +22,8 @@ pub struct Sample<'a> {
|
|||
pub should_loop: bool,
|
||||
pub restart_point: u32,
|
||||
pub volume: Num<i16, 8>,
|
||||
pub volume_envelope: Option<usize>,
|
||||
pub fadeout: Num<i32, 8>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -36,6 +40,14 @@ pub struct PatternSlot {
|
|||
pub effect2: PatternEffect,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Envelope<'a> {
|
||||
pub amount: &'a [Num<i16, 8>],
|
||||
pub sustain: Option<usize>,
|
||||
pub loop_start: Option<usize>,
|
||||
pub loop_end: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub enum PatternEffect {
|
||||
/// Don't play an effect
|
||||
|
@ -50,9 +62,13 @@ pub enum PatternEffect {
|
|||
VolumeSlide(Num<i16, 8>),
|
||||
FineVolumeSlide(Num<i16, 8>),
|
||||
NoteCut(u32),
|
||||
Portamento(Num<u16, 8>),
|
||||
Portamento(Num<u16, 12>),
|
||||
/// Slide each tick the first amount to at most the second amount
|
||||
TonePortamento(Num<u16, 8>, Num<u16, 8>),
|
||||
TonePortamento(Num<u16, 12>, Num<u16, 12>),
|
||||
SetTicksPerStep(u32),
|
||||
SetFramesPerTick(Num<u32, 8>),
|
||||
SetGlobalVolume(Num<i32, 8>),
|
||||
GlobalVolumeSlide(Num<i32, 8>),
|
||||
}
|
||||
|
||||
#[cfg(feature = "quote")]
|
||||
|
@ -62,12 +78,14 @@ impl<'a> quote::ToTokens for Track<'a> {
|
|||
|
||||
let Track {
|
||||
samples,
|
||||
envelopes,
|
||||
pattern_data,
|
||||
patterns,
|
||||
frames_per_tick,
|
||||
num_channels,
|
||||
patterns_to_play,
|
||||
ticks_per_step,
|
||||
repeat,
|
||||
} = self;
|
||||
|
||||
let frames_per_tick = frames_per_tick.to_raw();
|
||||
|
@ -78,9 +96,11 @@ impl<'a> quote::ToTokens for Track<'a> {
|
|||
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),*];
|
||||
const ENVELOPES: &[agb_tracker::__private::agb_tracker_interop::Envelope<'static>] = &[#(#envelopes),*];
|
||||
|
||||
agb_tracker::Track {
|
||||
samples: SAMPLES,
|
||||
envelopes: ENVELOPES,
|
||||
pattern_data: PATTERN_DATA,
|
||||
patterns: PATTERNS,
|
||||
patterns_to_play: PATTERNS_TO_PLAY,
|
||||
|
@ -88,12 +108,58 @@ impl<'a> quote::ToTokens for Track<'a> {
|
|||
frames_per_tick: agb_tracker::__private::Num::from_raw(#frames_per_tick),
|
||||
num_channels: #num_channels,
|
||||
ticks_per_step: #ticks_per_step,
|
||||
repeat: #repeat,
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "quote")]
|
||||
impl quote::ToTokens for Envelope<'_> {
|
||||
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
|
||||
use quote::{quote, TokenStreamExt};
|
||||
|
||||
let Envelope {
|
||||
amount,
|
||||
sustain,
|
||||
loop_start,
|
||||
loop_end,
|
||||
} = self;
|
||||
|
||||
let amount = amount.iter().map(|value| {
|
||||
let value = value.to_raw();
|
||||
quote! { agb_tracker::__private::Num::from_raw(#value) }
|
||||
});
|
||||
|
||||
let sustain = match sustain {
|
||||
Some(value) => quote!(Some(#value)),
|
||||
None => quote!(None),
|
||||
};
|
||||
let loop_start = match loop_start {
|
||||
Some(value) => quote!(Some(#value)),
|
||||
None => quote!(None),
|
||||
};
|
||||
let loop_end = match loop_end {
|
||||
Some(value) => quote!(Some(#value)),
|
||||
None => quote!(None),
|
||||
};
|
||||
|
||||
tokens.append_all(quote! {
|
||||
{
|
||||
const AMOUNTS: &[agb_tracker::__private::Num<i16, 8>] = &[#(#amount),*];
|
||||
|
||||
agb_tracker::__private::agb_tracker_interop::Envelope {
|
||||
amount: AMOUNTS,
|
||||
sustain: #sustain,
|
||||
loop_start: #loop_start,
|
||||
loop_end: #loop_end,
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "quote")]
|
||||
struct ByteString<'a>(&'a [u8]);
|
||||
#[cfg(feature = "quote")]
|
||||
|
@ -115,8 +181,16 @@ impl<'a> quote::ToTokens for Sample<'a> {
|
|||
should_loop,
|
||||
restart_point,
|
||||
volume,
|
||||
volume_envelope,
|
||||
fadeout,
|
||||
} = self;
|
||||
|
||||
let volume_envelope = match volume_envelope {
|
||||
Some(index) => quote!(Some(#index)),
|
||||
None => quote!(None),
|
||||
};
|
||||
let fadeout = fadeout.to_raw();
|
||||
|
||||
let samples = ByteString(data);
|
||||
let volume = volume.to_raw();
|
||||
|
||||
|
@ -131,6 +205,8 @@ impl<'a> quote::ToTokens for Sample<'a> {
|
|||
should_loop: #should_loop,
|
||||
restart_point: #restart_point,
|
||||
volume: agb_tracker::__private::Num::from_raw(#volume),
|
||||
volume_envelope: #volume_envelope,
|
||||
fadeout: agb_tracker::__private::Num::from_raw(#fadeout),
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -220,6 +296,21 @@ impl quote::ToTokens for PatternEffect {
|
|||
let target = target.to_raw();
|
||||
quote! { TonePortamento(agb_tracker::__private::Num::from_raw(#amount), agb_tracker::__private::Num::from_raw(#target))}
|
||||
}
|
||||
PatternEffect::SetTicksPerStep(new_ticks) => {
|
||||
quote! { SetTicksPerStep(#new_ticks) }
|
||||
}
|
||||
PatternEffect::SetFramesPerTick(new_frames_per_tick) => {
|
||||
let amount = new_frames_per_tick.to_raw();
|
||||
quote! { SetFramesPerTick(agb_tracker::__private::Num::from_raw(#amount)) }
|
||||
}
|
||||
PatternEffect::SetGlobalVolume(amount) => {
|
||||
let amount = amount.to_raw();
|
||||
quote! { SetGlobalVolume(agb_tracker::__private::Num::from_raw(#amount)) }
|
||||
}
|
||||
PatternEffect::GlobalVolumeSlide(amount) => {
|
||||
let amount = amount.to_raw();
|
||||
quote! { GlobalVolumeSlide(agb_tracker::__private::Num::from_raw(#amount)) }
|
||||
}
|
||||
};
|
||||
|
||||
tokens.append_all(quote! {
|
||||
|
|
|
@ -90,18 +90,37 @@ pub use agb_tracker_interop::Track;
|
|||
pub struct Tracker {
|
||||
track: &'static Track<'static>,
|
||||
channels: Vec<TrackerChannel>,
|
||||
envelopes: Vec<Option<EnvelopeState>>,
|
||||
|
||||
frame: Num<u32, 8>,
|
||||
tick: u32,
|
||||
first: bool,
|
||||
|
||||
global_settings: GlobalSettings,
|
||||
|
||||
current_row: usize,
|
||||
current_pattern: usize,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct TrackerChannel {
|
||||
channel_id: Option<ChannelId>,
|
||||
base_speed: Num<u32, 8>,
|
||||
base_speed: Num<u32, 16>,
|
||||
volume: Num<i32, 8>,
|
||||
}
|
||||
|
||||
struct EnvelopeState {
|
||||
frame: usize,
|
||||
envelope_id: usize,
|
||||
finished: bool,
|
||||
fadeout: Num<i32, 8>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct GlobalSettings {
|
||||
ticks_per_step: u32,
|
||||
|
||||
frames_per_tick: Num<u32, 8>,
|
||||
volume: Num<i32, 8>,
|
||||
}
|
||||
|
||||
|
@ -109,22 +128,30 @@ impl Tracker {
|
|||
/// Create a new tracker playing a specified track. See the [example](crate#example) for how to use the tracker.
|
||||
pub fn new(track: &'static Track<'static>) -> Self {
|
||||
let mut channels = Vec::new();
|
||||
channels.resize_with(track.num_channels, || TrackerChannel {
|
||||
channel_id: None,
|
||||
base_speed: 0.into(),
|
||||
volume: 0.into(),
|
||||
});
|
||||
channels.resize_with(track.num_channels, Default::default);
|
||||
|
||||
let mut envelopes = Vec::new();
|
||||
envelopes.resize_with(track.num_channels, || None);
|
||||
|
||||
let global_settings = GlobalSettings {
|
||||
ticks_per_step: track.ticks_per_step,
|
||||
frames_per_tick: track.frames_per_tick,
|
||||
volume: 1.into(),
|
||||
};
|
||||
|
||||
Self {
|
||||
track,
|
||||
channels,
|
||||
envelopes,
|
||||
|
||||
frame: 0.into(),
|
||||
first: true,
|
||||
tick: 0,
|
||||
|
||||
current_row: 0,
|
||||
global_settings,
|
||||
|
||||
current_pattern: 0,
|
||||
current_row: 0,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -132,6 +159,7 @@ impl Tracker {
|
|||
/// See the [example](crate#example) for how to use the tracker.
|
||||
pub fn step(&mut self, mixer: &mut Mixer) {
|
||||
if !self.increment_frame() {
|
||||
self.update_envelopes(mixer);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -143,21 +171,77 @@ 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 (i, (channel, pattern_slot)) in self.channels.iter_mut().zip(pattern_slots).enumerate()
|
||||
{
|
||||
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.play_sound(mixer, sample, &self.global_settings);
|
||||
self.envelopes[i] = sample.volume_envelope.map(|envelope_id| EnvelopeState {
|
||||
frame: 0,
|
||||
envelope_id,
|
||||
finished: false,
|
||||
fadeout: sample.fadeout,
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
channel.apply_effect(
|
||||
mixer,
|
||||
&pattern_slot.effect1,
|
||||
self.tick,
|
||||
&mut self.global_settings,
|
||||
&mut self.envelopes[i],
|
||||
);
|
||||
channel.apply_effect(
|
||||
mixer,
|
||||
&pattern_slot.effect2,
|
||||
self.tick,
|
||||
&mut self.global_settings,
|
||||
&mut self.envelopes[i],
|
||||
);
|
||||
}
|
||||
|
||||
self.increment_step();
|
||||
self.update_envelopes(mixer);
|
||||
}
|
||||
|
||||
fn update_envelopes(&mut self, mixer: &mut Mixer) {
|
||||
for (channel, envelope_state_option) in self.channels.iter_mut().zip(&mut self.envelopes) {
|
||||
if let Some(envelope_state) = envelope_state_option {
|
||||
let envelope = &self.track.envelopes[envelope_state.envelope_id];
|
||||
|
||||
if !channel.update_volume_envelope(
|
||||
mixer,
|
||||
envelope_state,
|
||||
envelope,
|
||||
&self.global_settings,
|
||||
) {
|
||||
envelope_state_option.take();
|
||||
} else {
|
||||
envelope_state.frame += 1;
|
||||
|
||||
if !envelope_state.finished {
|
||||
if let Some(sustain) = envelope.sustain {
|
||||
if envelope_state.frame >= sustain {
|
||||
envelope_state.frame = sustain;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(loop_end) = envelope.loop_end {
|
||||
if envelope_state.frame >= loop_end {
|
||||
envelope_state.frame = envelope.loop_start.unwrap_or(0);
|
||||
}
|
||||
}
|
||||
|
||||
if envelope_state.frame >= envelope.amount.len() {
|
||||
envelope_state.frame = envelope.amount.len() - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn increment_frame(&mut self) -> bool {
|
||||
|
@ -168,11 +252,11 @@ impl Tracker {
|
|||
|
||||
self.frame += 1;
|
||||
|
||||
if self.frame >= self.track.frames_per_tick {
|
||||
if self.frame >= self.global_settings.frames_per_tick {
|
||||
self.tick += 1;
|
||||
self.frame -= self.track.frames_per_tick;
|
||||
self.frame -= self.global_settings.frames_per_tick;
|
||||
|
||||
if self.tick == self.track.ticks_per_step {
|
||||
if self.tick >= self.global_settings.ticks_per_step {
|
||||
self.current_row += 1;
|
||||
|
||||
if self.current_row
|
||||
|
@ -182,7 +266,7 @@ impl Tracker {
|
|||
self.current_row = 0;
|
||||
|
||||
if self.current_pattern >= self.track.patterns_to_play.len() {
|
||||
self.current_pattern = 0;
|
||||
self.current_pattern = self.track.repeat;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -194,12 +278,15 @@ impl Tracker {
|
|||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn increment_step(&mut self) {}
|
||||
}
|
||||
|
||||
impl TrackerChannel {
|
||||
fn play_sound(&mut self, mixer: &mut Mixer<'_>, sample: &Sample<'static>) {
|
||||
fn play_sound(
|
||||
&mut self,
|
||||
mixer: &mut Mixer<'_>,
|
||||
sample: &Sample<'static>,
|
||||
global_settings: &GlobalSettings,
|
||||
) {
|
||||
if let Some(channel) = self
|
||||
.channel_id
|
||||
.take()
|
||||
|
@ -210,7 +297,11 @@ impl TrackerChannel {
|
|||
|
||||
let mut new_channel = SoundChannel::new(sample.data);
|
||||
|
||||
new_channel.volume(sample.volume.change_base());
|
||||
new_channel.volume(
|
||||
(sample.volume.change_base() * global_settings.volume)
|
||||
.try_change_base()
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
if sample.should_loop {
|
||||
new_channel
|
||||
|
@ -229,14 +320,21 @@ impl TrackerChannel {
|
|||
.and_then(|channel_id| mixer.channel(channel_id))
|
||||
{
|
||||
if speed != 0.into() {
|
||||
self.base_speed = speed;
|
||||
self.base_speed = speed.change_base();
|
||||
}
|
||||
|
||||
channel.playback(self.base_speed);
|
||||
channel.playback(self.base_speed.change_base());
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_effect(&mut self, mixer: &mut Mixer<'_>, effect: &PatternEffect, tick: u32) {
|
||||
fn apply_effect(
|
||||
&mut self,
|
||||
mixer: &mut Mixer<'_>,
|
||||
effect: &PatternEffect,
|
||||
tick: u32,
|
||||
global_settings: &mut GlobalSettings,
|
||||
envelope_state: &mut Option<EnvelopeState>,
|
||||
) {
|
||||
if let Some(channel) = self
|
||||
.channel_id
|
||||
.as_ref()
|
||||
|
@ -245,11 +343,14 @@ impl TrackerChannel {
|
|||
match effect {
|
||||
PatternEffect::None => {}
|
||||
PatternEffect::Stop => {
|
||||
channel.stop();
|
||||
channel.volume(0);
|
||||
if let Some(envelope_state) = envelope_state {
|
||||
envelope_state.finished = true;
|
||||
}
|
||||
}
|
||||
PatternEffect::Arpeggio(first, second) => {
|
||||
match tick % 3 {
|
||||
0 => channel.playback(self.base_speed),
|
||||
0 => channel.playback(self.base_speed.change_base()),
|
||||
1 => channel.playback(first.change_base()),
|
||||
2 => channel.playback(second.change_base()),
|
||||
_ => unreachable!(),
|
||||
|
@ -259,44 +360,123 @@ impl TrackerChannel {
|
|||
channel.panning(panning.change_base());
|
||||
}
|
||||
PatternEffect::Volume(volume) => {
|
||||
channel.volume(volume.change_base());
|
||||
channel.volume(
|
||||
(volume.change_base() * global_settings.volume)
|
||||
.try_change_base()
|
||||
.unwrap(),
|
||||
);
|
||||
self.volume = volume.change_base();
|
||||
}
|
||||
PatternEffect::VolumeSlide(amount) => {
|
||||
if tick != 0 {
|
||||
self.volume = (self.volume + amount.change_base()).max(0.into());
|
||||
channel.volume(self.volume.try_change_base().unwrap());
|
||||
channel.volume(
|
||||
(self.volume * global_settings.volume)
|
||||
.try_change_base()
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
}
|
||||
PatternEffect::FineVolumeSlide(amount) => {
|
||||
if tick == 0 {
|
||||
self.volume = (self.volume + amount.change_base()).max(0.into());
|
||||
channel.volume(self.volume.try_change_base().unwrap());
|
||||
channel.volume(
|
||||
(self.volume * global_settings.volume)
|
||||
.try_change_base()
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
}
|
||||
PatternEffect::NoteCut(wait) => {
|
||||
if tick == *wait {
|
||||
channel.volume(0);
|
||||
self.volume = 0.into();
|
||||
|
||||
if let Some(envelope_state) = envelope_state {
|
||||
envelope_state.finished = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
PatternEffect::Portamento(amount) => {
|
||||
if tick != 0 {
|
||||
self.base_speed *= amount.change_base();
|
||||
channel.playback(self.base_speed);
|
||||
channel.playback(self.base_speed.change_base());
|
||||
}
|
||||
}
|
||||
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.volume(
|
||||
(self.volume * global_settings.volume)
|
||||
.try_change_base()
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
if tick != 0 {
|
||||
if *amount < 1.into() {
|
||||
self.base_speed =
|
||||
(self.base_speed * amount.change_base()).max(target.change_base());
|
||||
} else {
|
||||
self.base_speed =
|
||||
(self.base_speed * amount.change_base()).min(target.change_base());
|
||||
}
|
||||
}
|
||||
|
||||
channel.playback(self.base_speed.change_base());
|
||||
}
|
||||
// These are global effects handled below
|
||||
PatternEffect::SetTicksPerStep(_)
|
||||
| PatternEffect::SetFramesPerTick(_)
|
||||
| PatternEffect::SetGlobalVolume(_)
|
||||
| PatternEffect::GlobalVolumeSlide(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
// Some effects have to happen regardless of if we're actually playing anything
|
||||
match effect {
|
||||
PatternEffect::SetTicksPerStep(amount) => {
|
||||
global_settings.ticks_per_step = *amount;
|
||||
}
|
||||
PatternEffect::SetFramesPerTick(new_frames_per_tick) => {
|
||||
global_settings.frames_per_tick = *new_frames_per_tick;
|
||||
}
|
||||
PatternEffect::SetGlobalVolume(volume) => {
|
||||
global_settings.volume = *volume;
|
||||
}
|
||||
PatternEffect::GlobalVolumeSlide(volume_delta) => {
|
||||
global_settings.volume =
|
||||
(global_settings.volume + *volume_delta).clamp(0.into(), 1.into());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
fn update_volume_envelope(
|
||||
&mut self,
|
||||
mixer: &mut Mixer<'_>,
|
||||
envelope_state: &EnvelopeState,
|
||||
envelope: &agb_tracker_interop::Envelope<'_>,
|
||||
global_settings: &GlobalSettings,
|
||||
) -> bool {
|
||||
if let Some(channel) = self
|
||||
.channel_id
|
||||
.as_ref()
|
||||
.and_then(|channel_id| mixer.channel(channel_id))
|
||||
{
|
||||
let amount = envelope.amount[envelope_state.frame];
|
||||
|
||||
if envelope_state.finished {
|
||||
self.volume = (self.volume - envelope_state.fadeout).max(0.into());
|
||||
}
|
||||
|
||||
channel.volume(
|
||||
(self.volume * amount.change_base() * global_settings.volume)
|
||||
.try_change_base()
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
self.volume != 0.into()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -56,18 +56,37 @@ pub fn parse_module(module: &Module) -> TokenStream {
|
|||
relative_note: i8,
|
||||
restart_point: u32,
|
||||
volume: Num<i16, 8>,
|
||||
envelope_id: Option<usize>,
|
||||
fadeout: Num<i32, 8>,
|
||||
}
|
||||
|
||||
let mut samples = vec![];
|
||||
let mut envelopes: Vec<EnvelopeData> = vec![];
|
||||
let mut existing_envelopes: HashMap<EnvelopeData, usize> = Default::default();
|
||||
|
||||
for (instrument_index, instrument) in instruments.iter().enumerate() {
|
||||
let InstrumentType::Default(ref instrument) = instrument.instr_type else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let envelope = &instrument.volume_envelope;
|
||||
let envelope_id = if envelope.enabled {
|
||||
let envelope: EnvelopeData = envelope.as_ref().into();
|
||||
let id = existing_envelopes
|
||||
.entry(envelope)
|
||||
.or_insert_with_key(|envelope| {
|
||||
envelopes.push(envelope.clone());
|
||||
envelopes.len() - 1
|
||||
});
|
||||
|
||||
Some(*id)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
for (sample_index, sample) in instrument.sample.iter().enumerate() {
|
||||
let should_loop = !matches!(sample.flags, LoopType::No);
|
||||
let fine_tune = sample.finetune as f64;
|
||||
let fine_tune = sample.finetune as f64 * 128.0;
|
||||
let relative_note = sample.relative_note;
|
||||
let restart_point = sample.loop_start;
|
||||
let sample_len = if sample.loop_length > 0 {
|
||||
|
@ -76,7 +95,7 @@ pub fn parse_module(module: &Module) -> TokenStream {
|
|||
usize::MAX
|
||||
};
|
||||
|
||||
let volume = Num::from_raw((sample.volume * (1 << 8) as f32) as i16);
|
||||
let volume = Num::from_f32(sample.volume);
|
||||
|
||||
let sample = match &sample.data {
|
||||
SampleDataType::Depth8(depth8) => depth8
|
||||
|
@ -91,6 +110,8 @@ pub fn parse_module(module: &Module) -> TokenStream {
|
|||
.collect::<Vec<_>>(),
|
||||
};
|
||||
|
||||
let fadeout = Num::from_f32(instrument.volume_fadeout);
|
||||
|
||||
instruments_map.insert((instrument_index, sample_index), samples.len());
|
||||
samples.push(SampleData {
|
||||
data: sample,
|
||||
|
@ -99,6 +120,8 @@ pub fn parse_module(module: &Module) -> TokenStream {
|
|||
relative_note,
|
||||
restart_point,
|
||||
volume,
|
||||
envelope_id,
|
||||
fadeout,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -109,7 +132,7 @@ pub fn parse_module(module: &Module) -> TokenStream {
|
|||
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 tone_portamento_directions = vec![0; module.get_num_channels()];
|
||||
let mut note_and_sample = vec![None; module.get_num_channels()];
|
||||
|
||||
for row in pattern.iter() {
|
||||
|
@ -124,7 +147,10 @@ pub fn parse_module(module: &Module) -> TokenStream {
|
|||
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;
|
||||
let sample_slot = *instrument
|
||||
.sample_for_note
|
||||
.get(slot.note as usize)
|
||||
.unwrap_or(&0) as usize;
|
||||
instruments_map
|
||||
.get(&(instrument_index, sample_slot))
|
||||
.map(|sample_idx| sample_idx + 1)
|
||||
|
@ -139,8 +165,8 @@ pub fn parse_module(module: &Module) -> TokenStream {
|
|||
let previous_note_and_sample = note_and_sample[channel_number];
|
||||
let maybe_note_and_sample = if matches!(slot.note, Note::KeyOff) {
|
||||
effect1 = PatternEffect::Stop;
|
||||
note_and_sample[channel_number] = None;
|
||||
&None
|
||||
|
||||
¬e_and_sample[channel_number]
|
||||
} else if !matches!(slot.note, Note::None) {
|
||||
if sample != 0 {
|
||||
note_and_sample[channel_number] = Some((slot.note, &samples[sample - 1]));
|
||||
|
@ -168,10 +194,10 @@ pub fn parse_module(module: &Module) -> TokenStream {
|
|||
PatternEffect::VolumeSlide(Num::new((slot.volume - 0x70) as i16) / 64)
|
||||
}
|
||||
0x80..=0x8F => PatternEffect::FineVolumeSlide(
|
||||
-Num::new((slot.volume - 0x80) as i16) / 64,
|
||||
-Num::new((slot.volume - 0x80) as i16) / 128,
|
||||
),
|
||||
0x90..=0x9F => PatternEffect::FineVolumeSlide(
|
||||
Num::new((slot.volume - 0x90) as i16) / 64,
|
||||
Num::new((slot.volume - 0x90) as i16) / 128,
|
||||
),
|
||||
0xC0..=0xCF => PatternEffect::Panning(
|
||||
Num::new(slot.volume as i16 - (0xC0 + (0xCF - 0xC0) / 2)) / 8,
|
||||
|
@ -221,13 +247,15 @@ pub fn parse_module(module: &Module) -> TokenStream {
|
|||
}
|
||||
}
|
||||
0x1 => {
|
||||
let c4_speed = note_to_speed(Note::C4, 0.0, 0, module.frequency_type);
|
||||
let speed = note_to_speed(
|
||||
let c4_speed: Num<u32, 12> =
|
||||
note_to_speed(Note::C4, 0.0, 0, module.frequency_type).change_base();
|
||||
let speed: Num<u32, 12> = note_to_speed(
|
||||
Note::C4,
|
||||
effect_parameter as f64,
|
||||
effect_parameter as f64 * 8.0,
|
||||
0,
|
||||
module.frequency_type,
|
||||
);
|
||||
)
|
||||
.change_base();
|
||||
|
||||
let portamento_amount = speed / c4_speed;
|
||||
|
||||
|
@ -237,12 +265,12 @@ 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,
|
||||
-(effect_parameter as f64),
|
||||
effect_parameter as f64 * 8.0,
|
||||
0,
|
||||
module.frequency_type,
|
||||
);
|
||||
|
||||
let portamento_amount = speed / c4_speed;
|
||||
let portamento_amount = c4_speed / speed;
|
||||
|
||||
PatternEffect::Portamento(portamento_amount.try_change_base().unwrap())
|
||||
}
|
||||
|
@ -258,11 +286,11 @@ pub fn parse_module(module: &Module) -> TokenStream {
|
|||
);
|
||||
|
||||
let direction = match (prev_note as usize).cmp(&(*note as usize)) {
|
||||
std::cmp::Ordering::Less => 1.0,
|
||||
std::cmp::Ordering::Less => 1,
|
||||
std::cmp::Ordering::Equal => {
|
||||
tone_portamento_directions[channel_number]
|
||||
}
|
||||
std::cmp::Ordering::Greater => -1.0,
|
||||
std::cmp::Ordering::Greater => -1,
|
||||
};
|
||||
|
||||
tone_portamento_directions[channel_number] = direction;
|
||||
|
@ -270,12 +298,16 @@ 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,
|
||||
effect_parameter as f64 * direction,
|
||||
effect_parameter as f64 * 8.0,
|
||||
0,
|
||||
module.frequency_type,
|
||||
);
|
||||
|
||||
let portamento_amount = speed / c4_speed;
|
||||
let portamento_amount = if direction > 0 {
|
||||
speed / c4_speed
|
||||
} else {
|
||||
c4_speed / speed
|
||||
};
|
||||
|
||||
PatternEffect::TonePortamento(
|
||||
portamento_amount.try_change_base().unwrap(),
|
||||
|
@ -288,20 +320,20 @@ pub fn parse_module(module: &Module) -> TokenStream {
|
|||
0x8 => {
|
||||
PatternEffect::Panning(Num::new(slot.effect_parameter as i16 - 128) / 128)
|
||||
}
|
||||
0xA => {
|
||||
0x5 | 0x6 | 0xA => {
|
||||
let first = effect_parameter >> 4;
|
||||
let second = effect_parameter & 0xF;
|
||||
|
||||
if first == 0 {
|
||||
PatternEffect::VolumeSlide(-Num::new(second as i16) / 16)
|
||||
PatternEffect::VolumeSlide(-Num::new(second as i16) / 64)
|
||||
} else {
|
||||
PatternEffect::VolumeSlide(Num::new(first as i16) / 16)
|
||||
PatternEffect::VolumeSlide(Num::new(first as i16) / 64)
|
||||
}
|
||||
}
|
||||
0xC => {
|
||||
if let Some((_, sample)) = maybe_note_and_sample {
|
||||
PatternEffect::Volume(
|
||||
(Num::new(slot.effect_parameter as i16) / 255) * sample.volume,
|
||||
(Num::new(slot.effect_parameter as i16) / 64) * sample.volume,
|
||||
)
|
||||
} else {
|
||||
PatternEffect::None
|
||||
|
@ -309,18 +341,43 @@ pub fn parse_module(module: &Module) -> TokenStream {
|
|||
}
|
||||
0xE => match slot.effect_parameter >> 4 {
|
||||
0xA => PatternEffect::FineVolumeSlide(
|
||||
Num::new((slot.effect_parameter & 0xf) as i16) / 64,
|
||||
Num::new((slot.effect_parameter & 0xf) as i16) / 128,
|
||||
),
|
||||
0xB => PatternEffect::FineVolumeSlide(
|
||||
-Num::new((slot.effect_parameter & 0xf) as i16) / 64,
|
||||
-Num::new((slot.effect_parameter & 0xf) as i16) / 128,
|
||||
),
|
||||
0xC => PatternEffect::NoteCut((slot.effect_parameter & 0xf).into()),
|
||||
_ => PatternEffect::None,
|
||||
},
|
||||
0xF => match slot.effect_parameter {
|
||||
0 => PatternEffect::SetTicksPerStep(u32::MAX),
|
||||
1..=0x20 => PatternEffect::SetTicksPerStep(slot.effect_parameter as u32),
|
||||
0x21.. => PatternEffect::SetFramesPerTick(bpm_to_frames_per_tick(
|
||||
slot.effect_parameter as u32,
|
||||
)),
|
||||
},
|
||||
// G
|
||||
0x10 => PatternEffect::SetGlobalVolume(
|
||||
Num::new(slot.effect_parameter as i32) / 0x40,
|
||||
),
|
||||
// H
|
||||
0x11 => {
|
||||
let first = effect_parameter >> 4;
|
||||
let second = effect_parameter & 0xF;
|
||||
|
||||
if first == 0 {
|
||||
PatternEffect::GlobalVolumeSlide(-Num::new(second as i32) / 0x40)
|
||||
} else {
|
||||
PatternEffect::GlobalVolumeSlide(Num::new(first as i32) / 0x40)
|
||||
}
|
||||
}
|
||||
_ => PatternEffect::None,
|
||||
};
|
||||
|
||||
if sample == 0 {
|
||||
if sample == 0
|
||||
|| matches!(effect2, PatternEffect::TonePortamento(_, _))
|
||||
|| matches!(effect1, PatternEffect::Stop)
|
||||
{
|
||||
pattern_data.push(agb_tracker_interop::PatternSlot {
|
||||
speed: 0.into(),
|
||||
sample: 0,
|
||||
|
@ -360,6 +417,8 @@ pub fn parse_module(module: &Module) -> TokenStream {
|
|||
should_loop: sample.should_loop,
|
||||
restart_point: sample.restart_point,
|
||||
volume: sample.volume,
|
||||
volume_envelope: sample.envelope_id,
|
||||
fadeout: sample.fadeout,
|
||||
})
|
||||
.collect();
|
||||
|
||||
|
@ -369,8 +428,17 @@ pub fn parse_module(module: &Module) -> TokenStream {
|
|||
.map(|order| *order as usize)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Number 150 here deduced experimentally
|
||||
let frames_per_tick = Num::<u32, 8>::new(150) / module.default_bpm as u32;
|
||||
let envelopes = envelopes
|
||||
.iter()
|
||||
.map(|envelope| agb_tracker_interop::Envelope {
|
||||
amount: &envelope.amounts,
|
||||
sustain: envelope.sustain,
|
||||
loop_start: envelope.loop_start,
|
||||
loop_end: envelope.loop_end,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let frames_per_tick = bpm_to_frames_per_tick(module.default_bpm as u32);
|
||||
let ticks_per_step = module.default_tempo;
|
||||
|
||||
let interop = agb_tracker_interop::Track {
|
||||
|
@ -379,20 +447,27 @@ pub fn parse_module(module: &Module) -> TokenStream {
|
|||
patterns: &patterns,
|
||||
num_channels: module.get_num_channels(),
|
||||
patterns_to_play: &patterns_to_play,
|
||||
envelopes: &envelopes,
|
||||
|
||||
frames_per_tick,
|
||||
ticks_per_step: ticks_per_step.into(),
|
||||
repeat: module.restart_position as usize,
|
||||
};
|
||||
|
||||
quote!(#interop)
|
||||
}
|
||||
|
||||
fn bpm_to_frames_per_tick(bpm: u32) -> Num<u32, 8> {
|
||||
// Number 150 here deduced experimentally
|
||||
Num::<u32, 8>::new(150) / bpm
|
||||
}
|
||||
|
||||
fn note_to_speed(
|
||||
note: Note,
|
||||
fine_tune: f64,
|
||||
relative_note: i8,
|
||||
frequency_type: FrequencyType,
|
||||
) -> Num<u32, 8> {
|
||||
) -> Num<u32, 12> {
|
||||
let frequency = match frequency_type {
|
||||
FrequencyType::LinearFrequencies => {
|
||||
note_to_frequency_linear(note, fine_tune, relative_note)
|
||||
|
@ -402,8 +477,8 @@ fn note_to_speed(
|
|||
|
||||
let gba_audio_frequency = 18157f64;
|
||||
|
||||
let speed: f64 = frequency / gba_audio_frequency;
|
||||
Num::from_raw((speed * (1 << 8) as f64) as u32)
|
||||
let speed = frequency / gba_audio_frequency;
|
||||
Num::from_f64(speed)
|
||||
}
|
||||
|
||||
fn note_to_frequency_linear(note: Note, fine_tune: f64, relative_note: i8) -> f64 {
|
||||
|
@ -416,7 +491,7 @@ fn note_to_frequency_amega(note: Note, fine_tune: f64, relative_note: i8) -> f64
|
|||
let note = (note as usize)
|
||||
.checked_add_signed(relative_note as isize)
|
||||
.expect("Note gone negative");
|
||||
let pos = (note % 12) * 8 + (fine_tune / 16.0) as usize;
|
||||
let pos = ((note % 12) * 8 + (fine_tune / 16.0) as usize).min(AMEGA_FREQUENCIES.len() - 2);
|
||||
let frac = (fine_tune / 16.0) - (fine_tune / 16.0).floor();
|
||||
|
||||
let period = ((AMEGA_FREQUENCIES[pos] as f64 * (1.0 - frac))
|
||||
|
@ -435,3 +510,56 @@ const AMEGA_FREQUENCIES: &[u32] = &[
|
|||
524, 520, 516, 513, 508, 505, 502, 498, 494, 491, 487, 484, 480, 477, 474, 470, 467, 463, 460,
|
||||
457,
|
||||
];
|
||||
|
||||
#[derive(PartialEq, Eq, Hash, Clone)]
|
||||
struct EnvelopeData {
|
||||
amounts: Vec<Num<i16, 8>>,
|
||||
sustain: Option<usize>,
|
||||
loop_start: Option<usize>,
|
||||
loop_end: Option<usize>,
|
||||
}
|
||||
|
||||
impl From<&xmrs::envelope::Envelope> for EnvelopeData {
|
||||
fn from(e: &xmrs::envelope::Envelope) -> Self {
|
||||
let mut amounts = vec![];
|
||||
|
||||
// it should be sampled at 50fps, but we're sampling at 60fps, so need to do a bit of cheating here.
|
||||
for frame in 0..(e.point.last().unwrap().frame * 60 / 50) {
|
||||
let xm_frame = frame * 50 / 60;
|
||||
let index = e
|
||||
.point
|
||||
.iter()
|
||||
.rposition(|point| point.frame < xm_frame)
|
||||
.unwrap_or(0);
|
||||
|
||||
let first_point = &e.point[index];
|
||||
let second_point = &e.point[index + 1];
|
||||
|
||||
let amount = EnvelopePoint::lerp(first_point, second_point, xm_frame) / 64.0;
|
||||
let amount = Num::from_f32(amount);
|
||||
|
||||
amounts.push(amount);
|
||||
}
|
||||
|
||||
let sustain = if e.sustain_enabled {
|
||||
Some(e.point[e.sustain_point as usize].frame as usize * 60 / 50)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let (loop_start, loop_end) = if e.loop_enabled {
|
||||
(
|
||||
Some(e.point[e.loop_start_point as usize].frame as usize * 60 / 50),
|
||||
Some(e.point[e.loop_end_point as usize].frame as usize * 60 / 50),
|
||||
)
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
|
||||
EnvelopeData {
|
||||
amounts,
|
||||
sustain,
|
||||
loop_start,
|
||||
loop_end,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue