diff --git a/CHANGELOG.md b/CHANGELOG.md index f89c6d3e..224ebb0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/agb-fixnum/src/lib.rs b/agb-fixnum/src/lib.rs index f058b1b5..3ba3a47f 100644 --- a/agb-fixnum/src/lib.rs +++ b/agb-fixnum/src/lib.rs @@ -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); @@ -376,6 +376,20 @@ impl Num { 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 Debug for Num { } /// 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 { /// The x coordinate pub x: T, @@ -880,7 +894,7 @@ impl From> 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 { /// The position of the rectangle diff --git a/examples/the-dungeon-puzzlers-lament/Cargo.lock b/examples/the-dungeon-puzzlers-lament/Cargo.lock index 2bdb4516..7ecf3010 100644 --- a/examples/the-dungeon-puzzlers-lament/Cargo.lock +++ b/examples/the-dungeon-puzzlers-lament/Cargo.lock @@ -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", +] diff --git a/examples/the-dungeon-puzzlers-lament/Cargo.toml b/examples/the-dungeon-puzzlers-lament/Cargo.toml index 746ab6c6..0d24fba5 100644 --- a/examples/the-dungeon-puzzlers-lament/Cargo.toml +++ b/examples/the-dungeon-puzzlers-lament/Cargo.toml @@ -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" \ No newline at end of file +proc-macro2 = "1" diff --git a/examples/the-dungeon-puzzlers-lament/sfx/bad.wav b/examples/the-dungeon-puzzlers-lament/sfx/bad.wav index a86daef4..d5e8d43d 100644 Binary files a/examples/the-dungeon-puzzlers-lament/sfx/bad.wav and b/examples/the-dungeon-puzzlers-lament/sfx/bad.wav differ diff --git a/examples/the-dungeon-puzzlers-lament/sfx/bgm.wav b/examples/the-dungeon-puzzlers-lament/sfx/bgm.wav deleted file mode 100644 index 9489590f..00000000 Binary files a/examples/the-dungeon-puzzlers-lament/sfx/bgm.wav and /dev/null differ diff --git a/examples/the-dungeon-puzzlers-lament/sfx/door_open.wav b/examples/the-dungeon-puzzlers-lament/sfx/door_open.wav index f23048ae..e2c25d29 100644 Binary files a/examples/the-dungeon-puzzlers-lament/sfx/door_open.wav and b/examples/the-dungeon-puzzlers-lament/sfx/door_open.wav differ diff --git a/examples/the-dungeon-puzzlers-lament/sfx/gwilym-theme2.xm b/examples/the-dungeon-puzzlers-lament/sfx/gwilym-theme2.xm new file mode 100644 index 00000000..cb4d4959 Binary files /dev/null and b/examples/the-dungeon-puzzlers-lament/sfx/gwilym-theme2.xm differ diff --git a/examples/the-dungeon-puzzlers-lament/sfx/place.wav b/examples/the-dungeon-puzzlers-lament/sfx/place.wav index ce5b5639..a5b5d5e3 100644 Binary files a/examples/the-dungeon-puzzlers-lament/sfx/place.wav and b/examples/the-dungeon-puzzlers-lament/sfx/place.wav differ diff --git a/examples/the-dungeon-puzzlers-lament/sfx/select.wav b/examples/the-dungeon-puzzlers-lament/sfx/select.wav index e2324c64..05119d2d 100644 Binary files a/examples/the-dungeon-puzzlers-lament/sfx/select.wav and b/examples/the-dungeon-puzzlers-lament/sfx/select.wav differ diff --git a/examples/the-dungeon-puzzlers-lament/sfx/slime_death.wav b/examples/the-dungeon-puzzlers-lament/sfx/slime_death.wav index dbe60e09..6ba88621 100644 Binary files a/examples/the-dungeon-puzzlers-lament/sfx/slime_death.wav and b/examples/the-dungeon-puzzlers-lament/sfx/slime_death.wav differ diff --git a/examples/the-dungeon-puzzlers-lament/sfx/switch_toggle1.wav b/examples/the-dungeon-puzzlers-lament/sfx/switch_toggle1.wav index 6c629c5b..47a92b1e 100644 Binary files a/examples/the-dungeon-puzzlers-lament/sfx/switch_toggle1.wav and b/examples/the-dungeon-puzzlers-lament/sfx/switch_toggle1.wav differ diff --git a/examples/the-dungeon-puzzlers-lament/sfx/sword_pickup.wav b/examples/the-dungeon-puzzlers-lament/sfx/sword_pickup.wav index 84f8b330..07135239 100644 Binary files a/examples/the-dungeon-puzzlers-lament/sfx/sword_pickup.wav and b/examples/the-dungeon-puzzlers-lament/sfx/sword_pickup.wav differ diff --git a/examples/the-dungeon-puzzlers-lament/sfx/theme.xm b/examples/the-dungeon-puzzlers-lament/sfx/theme.xm new file mode 100644 index 00000000..f2b1d121 Binary files /dev/null and b/examples/the-dungeon-puzzlers-lament/sfx/theme.xm differ diff --git a/examples/the-dungeon-puzzlers-lament/sfx/wall_hit.wav b/examples/the-dungeon-puzzlers-lament/sfx/wall_hit.wav index 6ea52376..6179f765 100644 Binary files a/examples/the-dungeon-puzzlers-lament/sfx/wall_hit.wav and b/examples/the-dungeon-puzzlers-lament/sfx/wall_hit.wav differ diff --git a/examples/the-dungeon-puzzlers-lament/src/sfx.rs b/examples/the-dungeon-puzzlers-lament/src/sfx.rs index bc336e13..b4b9cd18 100644 --- a/examples/the-dungeon-puzzlers-lament/src/sfx.rs +++ b/examples/the-dungeon-puzzlers-lament/src/sfx.rs @@ -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(); } diff --git a/tracker/agb-tracker-interop/src/lib.rs b/tracker/agb-tracker-interop/src/lib.rs index 26026d9a..7261d683 100644 --- a/tracker/agb-tracker-interop/src/lib.rs +++ b/tracker/agb-tracker-interop/src/lib.rs @@ -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, 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, + pub volume_envelope: Option, + pub fadeout: Num, } #[derive(Debug)] @@ -36,6 +40,14 @@ pub struct PatternSlot { pub effect2: PatternEffect, } +#[derive(Debug)] +pub struct Envelope<'a> { + pub amount: &'a [Num], + pub sustain: Option, + pub loop_start: Option, + pub loop_end: Option, +} + #[derive(Debug, Default)] pub enum PatternEffect { /// Don't play an effect @@ -50,9 +62,13 @@ pub enum PatternEffect { VolumeSlide(Num), FineVolumeSlide(Num), NoteCut(u32), - Portamento(Num), + Portamento(Num), /// Slide each tick the first amount to at most the second amount - TonePortamento(Num, Num), + TonePortamento(Num, Num), + SetTicksPerStep(u32), + SetFramesPerTick(Num), + SetGlobalVolume(Num), + GlobalVolumeSlide(Num), } #[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] = &[#(#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! { diff --git a/tracker/agb-tracker/src/lib.rs b/tracker/agb-tracker/src/lib.rs index a80aed1b..f0593618 100644 --- a/tracker/agb-tracker/src/lib.rs +++ b/tracker/agb-tracker/src/lib.rs @@ -90,18 +90,37 @@ pub use agb_tracker_interop::Track; pub struct Tracker { track: &'static Track<'static>, channels: Vec, + envelopes: Vec>, frame: Num, tick: u32, first: bool, + global_settings: GlobalSettings, + current_row: usize, current_pattern: usize, } +#[derive(Default)] struct TrackerChannel { channel_id: Option, - base_speed: Num, + base_speed: Num, + volume: Num, +} + +struct EnvelopeState { + frame: usize, + envelope_id: usize, + finished: bool, + fadeout: Num, +} + +#[derive(Clone)] +struct GlobalSettings { + ticks_per_step: u32, + + frames_per_tick: Num, volume: Num, } @@ -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, + ) { 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 + } } } diff --git a/tracker/agb-xm-core/src/lib.rs b/tracker/agb-xm-core/src/lib.rs index 537aeb68..dd7c0636 100644 --- a/tracker/agb-xm-core/src/lib.rs +++ b/tracker/agb-xm-core/src/lib.rs @@ -56,18 +56,37 @@ pub fn parse_module(module: &Module) -> TokenStream { relative_note: i8, restart_point: u32, volume: Num, + envelope_id: Option, + fadeout: Num, } let mut samples = vec![]; + let mut envelopes: Vec = vec![]; + let mut existing_envelopes: HashMap = 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::>(), }; + 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 = + note_to_speed(Note::C4, 0.0, 0, module.frequency_type).change_base(); + let speed: Num = 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::>(); - // Number 150 here deduced experimentally - let frames_per_tick = Num::::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::>(); + + 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 { + // Number 150 here deduced experimentally + Num::::new(150) / bpm +} + fn note_to_speed( note: Note, fine_tune: f64, relative_note: i8, frequency_type: FrequencyType, -) -> Num { +) -> Num { 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>, + sustain: Option, + loop_start: Option, + loop_end: Option, +} + +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, + } + } +}