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:
Gwilym Inzani 2023-08-06 20:48:35 +01:00 committed by GitHub
commit 18a161269e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 749 additions and 84 deletions

View file

@ -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

View file

@ -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

View file

@ -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",
]

View file

@ -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

Binary file not shown.

View file

@ -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();
}

View file

@ -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! {

View file

@ -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
}
}
}

View file

@ -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
&note_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,
}
}
}