diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 42f55f27..9ff2910b 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -22,7 +22,7 @@ jobs: - name: Set CARGO_TARGET_DIR run: echo "CARGO_TARGET_DIR=$HOME/target" >> $GITHUB_ENV - name: Install build tools - run: sudo apt-get update && sudo apt-get install build-essential libelf-dev zip -y + run: sudo apt-get update && sudo apt-get install build-essential libelf-dev zip libasound-dev -y - name: Install Miri run: | rustup component add miri clippy rustfmt diff --git a/CHANGELOG.md b/CHANGELOG.md index 90ec1ca1..f10db823 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Added support for vibrato in `agb-tracker`'s XM format + +### Changed + +- `agb-tracker` now has an `agb` feature which you must enable to use it with `agb`. You won't notice + unless you've been using `default-features = false` in which case we recommend removing that from your `Cargo.toml`. + ### Fixed - There are no longer gaps between tiles in affine graphics modes. diff --git a/Cargo.toml b/Cargo.toml index af29369a..b74ea1ee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ members = [ # "tracker/agb-tracker", "tracker/agb-xm", "tracker/agb-xm-core", + "tracker/desktop-player", "tools", diff --git a/examples/the-dungeon-puzzlers-lament/Cargo.toml b/examples/the-dungeon-puzzlers-lament/Cargo.toml index 6abf2884..e6710192 100644 --- a/examples/the-dungeon-puzzlers-lament/Cargo.toml +++ b/examples/the-dungeon-puzzlers-lament/Cargo.toml @@ -8,7 +8,8 @@ edition = "2021" [dependencies] agb = { version = "0.20.5", path = "../../agb" } -agb_tracker = { version = "0.20.5", path = "../../tracker/agb-tracker", default-features = false, features = ["xm"] } +agb_tracker = { version = "0.20.5", path = "../../tracker/agb-tracker" } + slotmap = { version = "1", default-features = false } [profile.dev] diff --git a/tracker/agb-midi-core/Cargo.toml b/tracker/agb-midi-core/Cargo.toml index 092e1b3f..9a8f156e 100644 --- a/tracker/agb-midi-core/Cargo.toml +++ b/tracker/agb-midi-core/Cargo.toml @@ -8,10 +8,6 @@ description = "Library for converting MIDI files for use with agb-tracker on the repository = "https://github.com/agbrs/agb" [dependencies] -proc-macro-error = "1" -proc-macro2 = "1" -quote = "1" -syn = "2" rustysynth = "1.3" midly = { version = "0.5", default-features = false, features = [ "alloc", diff --git a/tracker/agb-midi-core/src/lib.rs b/tracker/agb-midi-core/src/lib.rs index e9f03446..2b1cefac 100644 --- a/tracker/agb-midi-core/src/lib.rs +++ b/tracker/agb-midi-core/src/lib.rs @@ -1,4 +1,5 @@ use std::{ + borrow::Cow, collections::HashMap, error::Error, fs::{self, File}, @@ -9,63 +10,7 @@ use std::{ use agb_fixnum::Num; use agb_tracker_interop::{Envelope, Pattern, PatternEffect, PatternSlot, Sample, Track}; use midly::{Format, MetaMessage, Smf, Timing, TrackEventKind}; -use proc_macro2::TokenStream; -use proc_macro_error::abort; -use quote::quote; use rustysynth::SoundFont; -use syn::{ - parse::{Parse, ParseStream}, - LitStr, Token, -}; - -struct MidiCoreInput { - sf2_file: LitStr, - _comma: Token![,], - midi_file: LitStr, -} - -impl Parse for MidiCoreInput { - fn parse(input: ParseStream) -> syn::Result { - Ok(Self { - sf2_file: input.parse()?, - _comma: input.parse()?, - midi_file: input.parse()?, - }) - } -} - -pub fn agb_midi_core(args: TokenStream) -> TokenStream { - let input: MidiCoreInput = match syn::parse2(args.clone()) { - Ok(input) => input, - Err(e) => abort!(args, e), - }; - - let sf2_file = input.sf2_file.value(); - let midi_file = input.midi_file.value(); - - let root = std::env::var("CARGO_MANIFEST_DIR").expect("Failed to get cargo manifest dir"); - let sf2_file = Path::new(&root).join(&*sf2_file); - let midi_file = Path::new(&root).join(&*midi_file); - - let sf2_include_path = sf2_file.to_string_lossy(); - let midi_file_include_path = midi_file.to_string_lossy(); - - let midi_info = match MidiInfo::load_from_file(&sf2_file, &midi_file) { - Ok(track) => track, - Err(e) => abort!(args, e), - }; - - let parsed = parse_midi(&midi_info); - - quote! { - { - const _: &[u8] = include_bytes!(#sf2_include_path); - const _: &[u8] = include_bytes!(#midi_file_include_path); - - #parsed - } - } -} pub struct MidiInfo { sound_font: SoundFont, @@ -87,7 +32,7 @@ impl MidiInfo { } } -pub fn parse_midi(midi_info: &MidiInfo) -> TokenStream { +pub fn parse_midi(midi_info: &MidiInfo) -> Track { let mut samples = vec![]; let sf2 = &midi_info.sound_font; let sf2_data = sf2.get_wave_data(); @@ -386,7 +331,7 @@ pub fn parse_midi(midi_info: &MidiInfo) -> TokenStream { let samples: Vec<_> = samples .iter() .map(|sample| Sample { - data: &sample.data, + data: sample.data.clone().into(), should_loop: sample.restart_point.is_some(), restart_point: sample.restart_point.unwrap_or(0), volume: 256.into(), @@ -409,29 +354,31 @@ pub fn parse_midi(midi_info: &MidiInfo) -> TokenStream { let envelopes: Vec<_> = envelopes .iter() .map(|envelope| Envelope { - amount: &envelope.amounts, + amount: envelope.amounts.clone().into(), sustain: Some(envelope.amounts.len() - 1), loop_start: None, loop_end: None, + + vib_waveform: Default::default(), + vib_amount: Default::default(), + vib_speed: Default::default(), }) .collect(); - let track = Track { - samples: &samples, - envelopes: &envelopes, - pattern_data: &pattern, - patterns: &[Pattern { + Track { + samples: samples.into(), + envelopes: envelopes.into(), + patterns: Cow::from(vec![Pattern { length: pattern.len() / resulting_num_channels, start_position: 0, - }], - patterns_to_play: &[0], + }]), + pattern_data: pattern.into(), + patterns_to_play: Cow::from(vec![0]), num_channels: resulting_num_channels, frames_per_tick: Num::from_f64(frames_per_tick), ticks_per_step: 1, repeat: 0, - }; - - quote!(#track) + } } #[derive(Clone, Default)] diff --git a/tracker/agb-midi/Cargo.toml b/tracker/agb-midi/Cargo.toml index 73d77ead..76f95a4b 100644 --- a/tracker/agb-midi/Cargo.toml +++ b/tracker/agb-midi/Cargo.toml @@ -11,6 +11,16 @@ repository = "https://github.com/agbrs/agb" proc-macro = true [dependencies] +agb_tracker_interop = { version = "0.20.5", path = "../agb-tracker-interop", features = ["quote"] } agb_midi_core = { version = "0.20.5", path = "../agb-midi-core" } + proc-macro-error = "1" proc-macro2 = "1" +quote = "1" +syn = "2" + +rustysynth = "1.3" +midly = { version = "0.5", default-features = false, features = [ + "alloc", + "std", +] } diff --git a/tracker/agb-midi/src/lib.rs b/tracker/agb-midi/src/lib.rs index 7918f3ca..1bf4ff78 100644 --- a/tracker/agb-midi/src/lib.rs +++ b/tracker/agb-midi/src/lib.rs @@ -1,8 +1,66 @@ +use std::path::Path; + +use agb_midi_core::{parse_midi, MidiInfo}; use proc_macro::TokenStream; -use proc_macro_error::proc_macro_error; +use proc_macro_error::{abort, proc_macro_error}; +use quote::quote; +use syn::{ + parse::{Parse, ParseStream}, + LitStr, Token, +}; #[proc_macro_error] #[proc_macro] pub fn include_midi(args: TokenStream) -> TokenStream { - agb_midi_core::agb_midi_core(args.into()).into() + agb_midi_core(args) +} + +struct MidiCoreInput { + sf2_file: LitStr, + _comma: Token![,], + midi_file: LitStr, +} + +impl Parse for MidiCoreInput { + fn parse(input: ParseStream) -> syn::Result { + Ok(Self { + sf2_file: input.parse()?, + _comma: input.parse()?, + midi_file: input.parse()?, + }) + } +} + +fn agb_midi_core(args: TokenStream) -> TokenStream { + let input: MidiCoreInput = match syn::parse(args.clone()) { + Ok(input) => input, + Err(e) => abort!(proc_macro2::TokenStream::from(args), e), + }; + + let sf2_file = input.sf2_file.value(); + let midi_file = input.midi_file.value(); + + let root = std::env::var("CARGO_MANIFEST_DIR").expect("Failed to get cargo manifest dir"); + let sf2_file = Path::new(&root).join(&*sf2_file); + let midi_file = Path::new(&root).join(&*midi_file); + + let sf2_include_path = sf2_file.to_string_lossy(); + let midi_file_include_path = midi_file.to_string_lossy(); + + let midi_info = match MidiInfo::load_from_file(&sf2_file, &midi_file) { + Ok(track) => track, + Err(e) => abort!(proc_macro2::TokenStream::from(args), e), + }; + + let parsed = parse_midi(&midi_info); + + quote! { + { + const _: &[u8] = include_bytes!(#sf2_include_path); + const _: &[u8] = include_bytes!(#midi_file_include_path); + + #parsed + } + } + .into() } diff --git a/tracker/agb-tracker-interop/src/lib.rs b/tracker/agb-tracker-interop/src/lib.rs index e9ccebc0..565c1d89 100644 --- a/tracker/agb-tracker-interop/src/lib.rs +++ b/tracker/agb-tracker-interop/src/lib.rs @@ -1,14 +1,17 @@ #![cfg_attr(not(feature = "std"), no_std)] +extern crate alloc; + use agb_fixnum::Num; +use alloc::borrow::Cow; #[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], +pub struct Track { + pub samples: Cow<'static, [Sample]>, + pub envelopes: Cow<'static, [Envelope]>, + pub pattern_data: Cow<'static, [PatternSlot]>, + pub patterns: Cow<'static, [Pattern]>, + pub patterns_to_play: Cow<'static, [usize]>, pub num_channels: usize, pub frames_per_tick: Num, @@ -16,9 +19,9 @@ pub struct Track<'a> { pub repeat: usize, } -#[derive(Debug)] -pub struct Sample<'a> { - pub data: &'a [u8], +#[derive(Debug, Clone)] +pub struct Sample { + pub data: Cow<'static, [u8]>, pub should_loop: bool, pub restart_point: u32, pub volume: Num, @@ -26,7 +29,7 @@ pub struct Sample<'a> { pub fadeout: Num, } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Pattern { pub length: usize, pub start_position: usize, @@ -40,12 +43,16 @@ pub struct PatternSlot { pub effect2: PatternEffect, } -#[derive(Debug)] -pub struct Envelope<'a> { - pub amount: &'a [Num], +#[derive(Debug, Clone)] +pub struct Envelope { + pub amount: Cow<'static, [Num]>, pub sustain: Option, pub loop_start: Option, pub loop_end: Option, + + pub vib_waveform: Waveform, + pub vib_amount: Num, + pub vib_speed: u8, } #[derive(Debug, Default, Clone, PartialEq, Eq)] @@ -59,13 +66,16 @@ pub enum PatternEffect { Arpeggio(Num, Num), Panning(Num), Volume(Num), - VolumeSlide(Num), + // bool = maintain vibrato? + VolumeSlide(Num, bool), FineVolumeSlide(Num), NoteCut(u32), NoteDelay(u32), Portamento(Num), + FinePortamento(Num), /// Slide each tick the first amount to at most the second amount TonePortamento(Num, Num), + Vibrato(Waveform, Num, u8), SetTicksPerStep(u32), SetFramesPerTick(Num), SetGlobalVolume(Num), @@ -74,8 +84,16 @@ pub enum PatternEffect { PitchBend(Num), } +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Waveform { + #[default] + Sine, + Saw, + Square, +} + #[cfg(feature = "quote")] -impl<'a> quote::ToTokens for Track<'a> { +impl quote::ToTokens for Track { fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { use quote::{quote, TokenStreamExt}; @@ -95,20 +113,24 @@ impl<'a> quote::ToTokens for Track<'a> { tokens.append_all(quote! { { - static SAMPLES: &[agb_tracker::__private::agb_tracker_interop::Sample<'static>] = &[#(#samples),*]; - static PATTERN_DATA: &[agb_tracker::__private::agb_tracker_interop::PatternSlot] = &[#(#pattern_data),*]; - static PATTERNS: &[agb_tracker::__private::agb_tracker_interop::Pattern] = &[#(#patterns),*]; + use alloc::borrow::Cow; + use agb_tracker::__private::agb_tracker_interop::*; + use agb_tracker::__private::Num; + + static SAMPLES: &[Sample] = &[#(#samples),*]; + static PATTERN_DATA: &[PatternSlot] = &[#(#pattern_data),*]; + static PATTERNS: &[Pattern] = &[#(#patterns),*]; static PATTERNS_TO_PLAY: &[usize] = &[#(#patterns_to_play),*]; - static ENVELOPES: &[agb_tracker::__private::agb_tracker_interop::Envelope<'static>] = &[#(#envelopes),*]; + static ENVELOPES: &[Envelope] = &[#(#envelopes),*]; agb_tracker::Track { - samples: SAMPLES, - envelopes: ENVELOPES, - pattern_data: PATTERN_DATA, - patterns: PATTERNS, - patterns_to_play: PATTERNS_TO_PLAY, + samples: Cow::Borrowed(SAMPLES), + envelopes: Cow::Borrowed(ENVELOPES), + pattern_data: Cow::Borrowed(PATTERN_DATA), + patterns: Cow::Borrowed(PATTERNS), + patterns_to_play: Cow::Borrowed(PATTERNS_TO_PLAY), - frames_per_tick: agb_tracker::__private::Num::from_raw(#frames_per_tick), + frames_per_tick: Num::from_raw(#frames_per_tick), num_channels: #num_channels, ticks_per_step: #ticks_per_step, repeat: #repeat, @@ -119,7 +141,7 @@ impl<'a> quote::ToTokens for Track<'a> { } #[cfg(feature = "quote")] -impl quote::ToTokens for Envelope<'_> { +impl quote::ToTokens for Envelope { fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { use quote::{quote, TokenStreamExt}; @@ -128,6 +150,9 @@ impl quote::ToTokens for Envelope<'_> { sustain, loop_start, loop_end, + vib_amount, + vib_speed, + vib_waveform, } = self; let amount = amount.iter().map(|value| { @@ -135,6 +160,11 @@ impl quote::ToTokens for Envelope<'_> { quote! { agb_tracker::__private::Num::from_raw(#value) } }); + let vib_amount = { + let value = vib_amount.to_raw(); + quote! { agb_tracker::__private::Num::from_raw(#value) } + }; + let sustain = match sustain { Some(value) => quote!(Some(#value)), None => quote!(None), @@ -153,10 +183,14 @@ impl quote::ToTokens for Envelope<'_> { static AMOUNTS: &[agb_tracker::__private::Num] = &[#(#amount),*]; agb_tracker::__private::agb_tracker_interop::Envelope { - amount: AMOUNTS, + amount: Cow::Borrowed(AMOUNTS), sustain: #sustain, loop_start: #loop_start, loop_end: #loop_end, + + vib_waveform: #vib_waveform, + vib_amount: #vib_amount, + vib_speed: #vib_speed, } } }); @@ -175,7 +209,7 @@ impl quote::ToTokens for ByteString<'_> { } #[cfg(feature = "quote")] -impl<'a> quote::ToTokens for Sample<'a> { +impl quote::ToTokens for Sample { fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { use quote::{quote, TokenStreamExt}; @@ -204,7 +238,7 @@ impl<'a> quote::ToTokens for Sample<'a> { static SAMPLE_DATA: &[u8] = &AlignmentWrapper(*#samples).0; agb_tracker::__private::agb_tracker_interop::Sample { - data: SAMPLE_DATA, + data: Cow::Borrowed(SAMPLE_DATA), should_loop: #should_loop, restart_point: #restart_point, volume: agb_tracker::__private::Num::from_raw(#volume), @@ -281,9 +315,9 @@ impl quote::ToTokens for PatternEffect { let volume = volume.to_raw(); quote! { Volume(agb_tracker::__private::Num::from_raw(#volume))} } - PatternEffect::VolumeSlide(amount) => { + PatternEffect::VolumeSlide(amount, vibrato) => { let amount = amount.to_raw(); - quote! { VolumeSlide(agb_tracker::__private::Num::from_raw(#amount))} + quote! { VolumeSlide(agb_tracker::__private::Num::from_raw(#amount), #vibrato)} } PatternEffect::FineVolumeSlide(amount) => { let amount = amount.to_raw(); @@ -295,6 +329,10 @@ impl quote::ToTokens for PatternEffect { let amount = amount.to_raw(); quote! { Portamento(agb_tracker::__private::Num::from_raw(#amount))} } + PatternEffect::FinePortamento(amount) => { + let amount = amount.to_raw(); + quote! { FinePortamento(agb_tracker::__private::Num::from_raw(#amount))} + } PatternEffect::TonePortamento(amount, target) => { let amount = amount.to_raw(); let target = target.to_raw(); @@ -319,6 +357,10 @@ impl quote::ToTokens for PatternEffect { let amount = amount.to_raw(); quote! { PitchBend(agb_tracker::__private::Num::from_raw(#amount)) } } + PatternEffect::Vibrato(waveform, amount, speed) => { + let amount = amount.to_raw(); + quote! { Vibrato(#waveform, agb_tracker::__private::Num::from_raw(#amount), #speed) } + } }; tokens.append_all(quote! { @@ -326,3 +368,20 @@ impl quote::ToTokens for PatternEffect { }); } } + +#[cfg(feature = "quote")] +impl quote::ToTokens for Waveform { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + use quote::{quote, TokenStreamExt}; + + let name = match self { + Waveform::Sine => quote!(Sine), + Waveform::Saw => quote!(Saw), + Waveform::Square => quote!(Square), + }; + + tokens.append_all(quote! { + agb_tracker::__private::agb_tracker_interop::Waveform::#name + }); + } +} diff --git a/tracker/agb-tracker/Cargo.toml b/tracker/agb-tracker/Cargo.toml index 396cc477..70c241c6 100644 --- a/tracker/agb-tracker/Cargo.toml +++ b/tracker/agb-tracker/Cargo.toml @@ -9,16 +9,23 @@ repository = "https://github.com/agbrs/agb" exclude = ["/examples"] [features] -default = ["xm", "midi"] +default = ["agb", "midi", "xm"] + +agb = ["dep:agb"] xm = ["dep:agb_xm"] midi = ["dep:agb_midi"] [dependencies] agb_midi = { version = "0.20.5", path = "../agb-midi", optional = true } agb_xm = { version = "0.20.5", path = "../agb-xm", optional = true } -agb = { version = "0.20.5", path = "../../agb" } +agb = { version = "0.20.5", path = "../../agb", optional = true } + +agb_fixnum = { version = "0.20.5", path = "../../agb-fixnum" } agb_tracker_interop = { version = "0.20.5", path = "../agb-tracker-interop", default-features = false } +[build-dependencies] +agb_fixnum = { version = "0.20.5", path = "../../agb-fixnum" } + [profile.dev] opt-level = 3 debug = true diff --git a/tracker/agb-tracker/build.rs b/tracker/agb-tracker/build.rs new file mode 100644 index 00000000..5236b62e --- /dev/null +++ b/tracker/agb-tracker/build.rs @@ -0,0 +1,48 @@ +use agb_fixnum::Num; + +use std::env; +use std::fs; +use std::path::Path; + +fn main() { + let sine = (0..64).map(|i| (Num::::new(i) / 64).sin()); + + let square = (0..64).map(|i| { + if i < 32 { + Num::::new(-1) + } else { + Num::::new(1) + } + }); + + let saw = (0..64).map(|i| (Num::::new(i) - 32) / 32); + + let out_dir = env::var_os("OUT_DIR").unwrap(); + let dest_path = Path::new(&out_dir).join("lookups.rs"); + + fs::write( + &dest_path, + format!( + " + pub(crate) static SINE_LOOKUP: [agb_fixnum::Num; 64] = [{sine_lookup}]; + pub(crate) static SQUARE_LOOKUP: [agb_fixnum::Num; 64] = [{square_lookup}]; + pub(crate) static SAW_LOOKUP: [agb_fixnum::Num; 64] = [{saw_lookup}]; + ", + sine_lookup = gen_lookup(sine), + square_lookup = gen_lookup(square), + saw_lookup = gen_lookup(saw), + ), + ) + .unwrap(); + + println!("cargo::rerun-if-changed=build.rs"); +} + +fn gen_lookup(input: impl IntoIterator>) -> String { + let output: Vec<_> = input + .into_iter() + .map(|v| format!("agb_fixnum::Num::from_raw({})", v.to_raw())) + .collect(); + + output.join(", ") +} diff --git a/tracker/agb-tracker/examples/basic.rs b/tracker/agb-tracker/examples/basic.rs index fc9b827e..53740009 100644 --- a/tracker/agb-tracker/examples/basic.rs +++ b/tracker/agb-tracker/examples/basic.rs @@ -1,6 +1,8 @@ #![no_std] #![no_main] +extern crate alloc; + use agb::sound::mixer::Frequency; use agb::Gba; use agb_tracker::{include_xm, Track, Tracker}; diff --git a/tracker/agb-tracker/examples/timing.rs b/tracker/agb-tracker/examples/timing.rs index 7ee7d034..81addb6a 100644 --- a/tracker/agb-tracker/examples/timing.rs +++ b/tracker/agb-tracker/examples/timing.rs @@ -1,6 +1,8 @@ #![no_std] #![no_main] +extern crate alloc; + use agb::sound::mixer::Frequency; use agb::Gba; use agb_tracker::{include_xm, Track, Tracker}; diff --git a/tracker/agb-tracker/src/lib.rs b/tracker/agb-tracker/src/lib.rs index 8312717f..c575bb98 100644 --- a/tracker/agb-tracker/src/lib.rs +++ b/tracker/agb-tracker/src/lib.rs @@ -65,13 +65,15 @@ extern crate alloc; -use agb_tracker_interop::{PatternEffect, Sample}; +mod lookups; +mod mixer; + +use agb_tracker_interop::{PatternEffect, Sample, Waveform}; use alloc::vec::Vec; -use agb::{ - fixnum::Num, - sound::mixer::{ChannelId, Mixer, SoundChannel}, -}; +pub use mixer::{Mixer, SoundChannel}; + +use agb_fixnum::Num; /// Import an XM file. Only available if you have the `xm` feature enabled (enabled by default). #[cfg(feature = "xm")] @@ -86,7 +88,7 @@ pub use agb_midi::include_midi; #[doc(hidden)] pub mod __private { - pub use agb::fixnum::Num; + pub use agb_fixnum::Num; pub use agb_tracker_interop; } @@ -94,11 +96,13 @@ pub mod __private { pub use agb_tracker_interop::Track; /// Stores the required state in order to play tracker music. -pub struct Tracker { - track: &'static Track<'static>, +pub struct TrackerInner<'track, TChannelId> { + track: &'track Track, channels: Vec, envelopes: Vec>, + mixer_channels: Vec>, + frame: Num, tick: u32, first: bool, @@ -111,10 +115,44 @@ pub struct Tracker { #[derive(Default)] struct TrackerChannel { - channel_id: Option, original_speed: Num, base_speed: Num, volume: Num, + + vibrato: Waves, + + current_volume: Num, + current_speed: Num, + current_panning: Num, + is_playing: bool, +} + +#[derive(Default)] +struct Waves { + waveform: Waveform, + frame: usize, + speed: usize, + amount: Num, + + enable: bool, +} + +impl Waves { + fn value(&self) -> Num { + assert!(self.amount.abs() <= 1.into()); + + calculate_wave(self.waveform, self.amount, self.frame) + } +} + +fn calculate_wave(waveform: Waveform, amount: Num, frame: usize) -> Num { + let lookup = match waveform { + Waveform::Sine => lookups::SINE_LOOKUP, + Waveform::Saw => lookups::SAW_LOOKUP, + Waveform::Square => lookups::SQUARE_LOOKUP, + }; + + (amount * lookup[frame] + 1).try_change_base().unwrap() } struct EnvelopeState { @@ -122,6 +160,8 @@ struct EnvelopeState { envelope_id: usize, finished: bool, fadeout: Num, + + vibrato_pos: usize, } #[derive(Clone)] @@ -132,15 +172,18 @@ struct GlobalSettings { volume: Num, } -impl Tracker { +impl<'track, TChannelId> TrackerInner<'track, TChannelId> { /// 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 { + pub fn new(track: &'track Track) -> Self { let mut channels = Vec::new(); channels.resize_with(track.num_channels, Default::default); let mut envelopes = Vec::new(); envelopes.resize_with(track.num_channels, || None); + let mut mixer_channels = Vec::new(); + mixer_channels.resize_with(track.num_channels, || None); + let global_settings = GlobalSettings { ticks_per_step: track.ticks_per_step, frames_per_tick: track.frames_per_tick, @@ -149,6 +192,7 @@ impl Tracker { Self { track, + mixer_channels, channels, envelopes, @@ -165,9 +209,11 @@ impl Tracker { /// Call this once per frame before calling [`mixer.frame`](agb::sound::mixer::Mixer::frame()). /// See the [example](crate#example) for how to use the tracker. - pub fn step(&mut self, mixer: &mut Mixer) { + pub fn step>(&mut self, mixer: &mut M) { if !self.increment_frame() { - self.update_envelopes(mixer); + self.update_envelopes(); + + self.realise(mixer); return; } @@ -183,28 +229,48 @@ impl Tracker { { if pattern_slot.sample != 0 && self.tick == 0 { let sample = &self.track.samples[pattern_slot.sample as usize - 1]; - channel.play_sound(mixer, sample, &self.global_settings); + + if let Some(channel) = self.mixer_channels[i] + .take() + .and_then(|channel_id| mixer.channel(&channel_id)) + { + channel.stop(); + } + + let mut new_channel = M::SoundChannel::new(&sample.data); + if sample.should_loop { + new_channel + .should_loop() + .restart_point(sample.restart_point); + } + + self.mixer_channels[i] = mixer.play_sound(new_channel); + + channel.reset(sample); + self.envelopes[i] = sample.volume_envelope.map(|envelope_id| EnvelopeState { frame: 0, envelope_id, finished: false, fadeout: sample.fadeout, + + vibrato_pos: 0, }); } if self.tick == 0 { - channel.set_speed(mixer, pattern_slot.speed.change_base()); + channel.set_speed(pattern_slot.speed.change_base()); } + channel.vibrato.enable = false; + 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, @@ -212,20 +278,62 @@ impl Tracker { ); } - self.update_envelopes(mixer); + self.update_envelopes(); + self.realise(mixer); } - fn update_envelopes(&mut self, mixer: &mut Mixer) { + fn realise>(&mut self, mixer: &mut M) { + for (i, (mixer_channel, tracker_channel)) in self + .mixer_channels + .iter() + .zip(&mut self.channels) + .enumerate() + { + tracker_channel.tick(); + + if let Some(channel) = mixer_channel + .as_ref() + .and_then(|channel_id| mixer.channel(channel_id)) + { + let mut current_speed = tracker_channel.current_speed; + + if tracker_channel.vibrato.speed != 0 && tracker_channel.vibrato.enable { + current_speed *= tracker_channel.vibrato.value().change_base(); + } else if let Some(envelope) = &mut self.envelopes[i] { + let track_envelope = &self.track.envelopes[envelope.envelope_id]; + + if track_envelope.vib_speed != 0 { + current_speed *= calculate_wave( + track_envelope.vib_waveform, + track_envelope.vib_amount.change_base(), + envelope.vibrato_pos, + ) + .change_base(); + envelope.vibrato_pos = + (envelope.vibrato_pos + track_envelope.vib_speed as usize) % 64; + } + } + + channel.playback(current_speed.change_base()); + channel.volume(tracker_channel.current_volume.try_change_base().unwrap()); + channel.panning(tracker_channel.current_panning.try_change_base().unwrap()); + + if tracker_channel.is_playing { + channel.resume(); + } else { + channel.pause(); + } + } + } + } + + fn update_envelopes(&mut self) { 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, - ) { + if !channel.update_volume_envelope(envelope_state, envelope, &self.global_settings) + { envelope_state_option.take(); } else { envelope_state.frame += 1; @@ -289,177 +397,131 @@ impl Tracker { } impl TrackerChannel { - fn play_sound( - &mut self, - mixer: &mut Mixer<'_>, - sample: &Sample<'static>, - global_settings: &GlobalSettings, - ) { - if let Some(channel) = self - .channel_id - .take() - .and_then(|channel_id| mixer.channel(&channel_id)) - { - channel.stop(); - } - - let mut new_channel = SoundChannel::new(sample.data); - - new_channel.volume( - (sample.volume.change_base() * global_settings.volume) - .try_change_base() - .unwrap(), - ); - - if sample.should_loop { - new_channel - .should_loop() - .restart_point(sample.restart_point); - } - - self.channel_id = mixer.play_sound(new_channel); + fn reset(&mut self, sample: &Sample) { self.volume = sample.volume.change_base(); + self.current_volume = self.volume; + self.current_panning = 0.into(); + self.is_playing = true; } - fn set_speed(&mut self, mixer: &mut Mixer<'_>, speed: Num) { - if let Some(channel) = self - .channel_id - .as_ref() - .and_then(|channel_id| mixer.channel(channel_id)) - { - if speed != 0.into() { - self.base_speed = speed.change_base(); - self.original_speed = self.base_speed; - } - - channel.playback(self.base_speed.change_base()); + fn set_speed(&mut self, speed: Num) { + if speed != 0.into() { + self.base_speed = speed.change_base(); + self.original_speed = self.base_speed; } + + self.current_speed = self.base_speed; } 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() - .and_then(|channel_id| mixer.channel(channel_id)) - { - match effect { - PatternEffect::None => {} - PatternEffect::Stop => { - channel.volume(0); + match effect { + PatternEffect::None => {} + PatternEffect::Stop => { + self.current_volume = 0.into(); + + if let Some(envelope_state) = envelope_state { + envelope_state.finished = true; + } + } + PatternEffect::Arpeggio(first, second) => { + match tick % 3 { + 0 => self.current_speed = self.base_speed.change_base(), + 1 => self.current_speed = first.change_base(), + 2 => self.current_speed = second.change_base(), + _ => unreachable!(), + }; + } + PatternEffect::Panning(panning) => { + self.current_panning = panning.change_base(); + } + PatternEffect::Volume(volume) => { + self.current_volume = (volume.change_base() * global_settings.volume) + .try_change_base() + .unwrap(); + + self.volume = volume.change_base(); + } + PatternEffect::VolumeSlide(amount, keep_vibrato) => { + if tick != 0 { + self.volume = (self.volume + amount.change_base()).max(0.into()); + self.current_volume = (self.volume * global_settings.volume) + .try_change_base() + .unwrap(); + } + + self.vibrato.enable = *keep_vibrato; + } + PatternEffect::FineVolumeSlide(amount) => { + if tick == 0 { + self.volume = (self.volume + amount.change_base()).max(0.into()); + self.current_volume = (self.volume * global_settings.volume) + .try_change_base() + .unwrap(); + } + } + PatternEffect::NoteCut(wait) => { + if tick == *wait { + self.current_volume = 0.into(); + if let Some(envelope_state) = envelope_state { envelope_state.finished = true; } } - PatternEffect::Arpeggio(first, second) => { - match tick % 3 { - 0 => channel.playback(self.base_speed.change_base()), - 1 => channel.playback(first.change_base()), - 2 => channel.playback(second.change_base()), - _ => unreachable!(), - }; - } - PatternEffect::Panning(panning) => { - channel.panning(panning.change_base()); - } - PatternEffect::Volume(volume) => { - channel.volume( - (volume.change_base() * 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 * 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 * global_settings.volume) - .try_change_base() - .unwrap(), - ); - } - } - PatternEffect::NoteCut(wait) => { - if tick == *wait { - channel.volume(0); - - if let Some(envelope_state) = envelope_state { - envelope_state.finished = true; - } - } - } - PatternEffect::NoteDelay(wait) => { - if tick < *wait { - channel.pause(); - } - - if tick == *wait { - channel.resume(); - channel.volume( - (self.volume * global_settings.volume) - .try_change_base() - .unwrap(), - ); - } - } - PatternEffect::Portamento(amount) => { - if tick != 0 { - self.base_speed *= amount.change_base(); - channel.playback(self.base_speed.change_base()); - } - } - PatternEffect::TonePortamento(amount, target) => { - 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()); - } - PatternEffect::PitchBend(amount) => { - if tick == 0 { - self.base_speed = self.original_speed * amount.change_base(); - channel.playback(self.base_speed.change_base()); - } - } - // These are global effects handled below - PatternEffect::SetTicksPerStep(_) - | PatternEffect::SetFramesPerTick(_) - | PatternEffect::SetGlobalVolume(_) - | PatternEffect::GlobalVolumeSlide(_) => {} } - } + PatternEffect::NoteDelay(wait) => { + if tick < *wait { + self.is_playing = false; + } + + if tick == *wait { + self.is_playing = true; + self.current_volume = (self.volume * global_settings.volume) + .try_change_base() + .unwrap(); + } + } + PatternEffect::Portamento(amount) => { + if tick != 0 { + self.base_speed *= amount.change_base(); + self.current_speed = self.base_speed.change_base(); + } + } + PatternEffect::FinePortamento(amount) => { + if tick == 1 { + self.base_speed *= amount.change_base(); + self.current_speed = self.base_speed.change_base(); + } + } + PatternEffect::TonePortamento(amount, target) => { + self.current_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()); + } + } + + self.current_speed = self.base_speed.change_base(); + } + PatternEffect::PitchBend(amount) => { + if tick == 0 { + self.base_speed = self.original_speed * amount.change_base(); + self.current_speed = self.base_speed.change_base(); + } + } - // Some effects have to happen regardless of if we're actually playing anything - match effect { PatternEffect::SetTicksPerStep(amount) => { global_settings.ticks_per_step = *amount; } @@ -473,44 +535,110 @@ impl TrackerChannel { global_settings.volume = (global_settings.volume + *volume_delta).clamp(0.into(), 1.into()); } - _ => {} + PatternEffect::Vibrato(waveform, amount, speed) => { + if *amount != 0.into() { + self.vibrato.amount = amount.change_base(); + } + + if *speed != 0 { + self.vibrato.speed = *speed as usize; + } + + self.vibrato.waveform = *waveform; + self.vibrato.enable = true; + } } } #[must_use] fn update_volume_envelope( &mut self, - mixer: &mut Mixer<'_>, envelope_state: &EnvelopeState, - envelope: &agb_tracker_interop::Envelope<'_>, + 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]; + 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 + if envelope_state.finished { + self.volume = (self.volume - envelope_state.fadeout).max(0.into()); } + + self.current_volume = (self.volume * amount.change_base() * global_settings.volume) + .try_change_base() + .unwrap(); + + self.volume != 0.into() + } + + fn tick(&mut self) { + self.vibrato.frame = (self.vibrato.frame + self.vibrato.speed) % 64; } } -#[cfg(test)] +#[cfg(all(test, feature = "agb"))] #[agb::entry] fn main(gba: agb::Gba) -> ! { loop {} } + +#[cfg(feature = "agb")] +impl SoundChannel for agb::sound::mixer::SoundChannel { + fn new(data: &alloc::borrow::Cow<'static, [u8]>) -> Self { + Self::new(match data { + alloc::borrow::Cow::Borrowed(data) => data, + alloc::borrow::Cow::Owned(_) => { + unimplemented!("Must use borrowed COW data for tracker") + } + }) + } + + fn stop(&mut self) { + self.stop(); + } + + fn pause(&mut self) -> &mut Self { + self.pause() + } + + fn resume(&mut self) -> &mut Self { + self.resume() + } + + fn should_loop(&mut self) -> &mut Self { + self.should_loop() + } + + fn volume(&mut self, value: impl Into>) -> &mut Self { + self.volume(value) + } + + fn restart_point(&mut self, value: impl Into>) -> &mut Self { + self.restart_point(value) + } + + fn playback(&mut self, playback_speed: impl Into>) -> &mut Self { + self.playback(playback_speed) + } + + fn panning(&mut self, panning: impl Into>) -> &mut Self { + self.panning(panning) + } +} + +#[cfg(feature = "agb")] +impl<'gba> Mixer for agb::sound::mixer::Mixer<'gba> { + type ChannelId = agb::sound::mixer::ChannelId; + type SoundChannel = agb::sound::mixer::SoundChannel; + + fn channel(&mut self, channel_id: &Self::ChannelId) -> Option<&mut Self::SoundChannel> { + self.channel(channel_id) + } + + fn play_sound(&mut self, channel: Self::SoundChannel) -> Option { + self.play_sound(channel) + } +} + +#[cfg(feature = "agb")] +/// The type to use if you're using agb-tracker with agb +pub type Tracker = TrackerInner<'static, agb::sound::mixer::ChannelId>; diff --git a/tracker/agb-tracker/src/lookups.rs b/tracker/agb-tracker/src/lookups.rs new file mode 100644 index 00000000..0f20e646 --- /dev/null +++ b/tracker/agb-tracker/src/lookups.rs @@ -0,0 +1 @@ +include!(concat!(env!("OUT_DIR"), "/lookups.rs")); diff --git a/tracker/agb-tracker/src/mixer.rs b/tracker/agb-tracker/src/mixer.rs new file mode 100644 index 00000000..110627e9 --- /dev/null +++ b/tracker/agb-tracker/src/mixer.rs @@ -0,0 +1,28 @@ +#![allow(missing_docs)] + +use agb_fixnum::Num; +use alloc::borrow::Cow; + +pub trait SoundChannel { + // I need a reference to a cow here to support the static data correctly + #[allow(clippy::ptr_arg)] + fn new(data: &Cow<'static, [u8]>) -> Self; + + fn stop(&mut self); + fn pause(&mut self) -> &mut Self; + fn resume(&mut self) -> &mut Self; + + fn should_loop(&mut self) -> &mut Self; + fn volume(&mut self, value: impl Into>) -> &mut Self; + fn restart_point(&mut self, value: impl Into>) -> &mut Self; + fn playback(&mut self, playback_speed: impl Into>) -> &mut Self; + fn panning(&mut self, panning: impl Into>) -> &mut Self; +} + +pub trait Mixer { + type ChannelId; + type SoundChannel: SoundChannel; + + fn channel(&mut self, channel_id: &Self::ChannelId) -> Option<&mut Self::SoundChannel>; + fn play_sound(&mut self, channel: Self::SoundChannel) -> Option; +} diff --git a/tracker/agb-xm-core/Cargo.toml b/tracker/agb-xm-core/Cargo.toml index daa83a32..abb0bde3 100644 --- a/tracker/agb-xm-core/Cargo.toml +++ b/tracker/agb-xm-core/Cargo.toml @@ -13,7 +13,7 @@ proc-macro2 = "1" quote = "1" syn = "2" -agb_tracker_interop = { version = "0.20.5", path = "../agb-tracker-interop" } +agb_tracker_interop = { version = "0.20.5", path = "../agb-tracker-interop", default-features = false } agb_fixnum = { version = "0.20.5", path = "../../agb-fixnum" } xmrs = { version = "0.6.1", features = ["std"] } diff --git a/tracker/agb-xm-core/src/lib.rs b/tracker/agb-xm-core/src/lib.rs index 5181ca80..6ad3e88c 100644 --- a/tracker/agb-xm-core/src/lib.rs +++ b/tracker/agb-xm-core/src/lib.rs @@ -1,51 +1,11 @@ -use std::{collections::HashMap, error::Error, fs, path::Path}; - -use agb_tracker_interop::PatternEffect; -use proc_macro2::TokenStream; -use proc_macro_error::abort; - -use quote::quote; -use syn::LitStr; +use std::collections::HashMap; use agb_fixnum::Num; +use agb_tracker_interop::{PatternEffect, Waveform}; -use xmrs::{prelude::*, xm::xmmodule::XmModule}; +use xmrs::prelude::*; -pub fn agb_xm_core(args: TokenStream) -> TokenStream { - let input = match syn::parse::(args.into()) { - Ok(input) => input, - Err(err) => return err.to_compile_error(), - }; - - let filename = input.value(); - - let root = std::env::var("CARGO_MANIFEST_DIR").expect("Failed to get cargo manifest dir"); - let path = Path::new(&root).join(&*filename); - - let include_path = path.to_string_lossy(); - - let module = match load_module_from_file(&path) { - Ok(track) => track, - Err(e) => abort!(input, e), - }; - - let parsed = parse_module(&module); - - quote! { - { - const _: &[u8] = include_bytes!(#include_path); - - #parsed - } - } -} - -pub fn load_module_from_file(xm_path: &Path) -> Result> { - let file_content = fs::read(xm_path)?; - Ok(XmModule::load(&file_content)?.to_module()) -} - -pub fn parse_module(module: &Module) -> TokenStream { +pub fn parse_module(module: &Module) -> agb_tracker_interop::Track { let instruments = &module.instrument; let mut instruments_map = HashMap::new(); @@ -71,7 +31,12 @@ pub fn parse_module(module: &Module) -> TokenStream { let envelope = &instrument.volume_envelope; let envelope_id = if envelope.enabled { - let envelope = EnvelopeData::new(envelope, module.default_bpm as u32); + let envelope = EnvelopeData::new( + envelope, + instrument, + module.frequency_type, + module.default_bpm as u32, + ); let id = existing_envelopes .entry(envelope) .or_insert_with_key(|envelope| { @@ -187,12 +152,14 @@ pub fn parse_module(module: &Module) -> TokenStream { .map(|note_and_sample| note_and_sample.1.volume) .unwrap_or(1.into()), ), - 0x60..=0x6F => { - PatternEffect::VolumeSlide(-Num::new((slot.volume - 0x60) as i16) / 64) - } - 0x70..=0x7F => { - PatternEffect::VolumeSlide(Num::new((slot.volume - 0x70) as i16) / 64) - } + 0x60..=0x6F => PatternEffect::VolumeSlide( + -Num::new((slot.volume - 0x60) as i16) / 64, + false, + ), + 0x70..=0x7F => PatternEffect::VolumeSlide( + Num::new((slot.volume - 0x70) as i16) / 64, + false, + ), 0x80..=0x8F => PatternEffect::FineVolumeSlide( -Num::new((slot.volume - 0x80) as i16) / 128, ), @@ -317,6 +284,22 @@ pub fn parse_module(module: &Module) -> TokenStream { PatternEffect::None } } + 0x4 => { + let vibrato_speed = effect_parameter >> 4; + let depth = effect_parameter & 0xF; + + let c4_speed = note_to_speed(Note::C4, 0.0, 0, module.frequency_type); + let speed = + note_to_speed(Note::C4, depth as f64 * 16.0, 0, module.frequency_type); + + let amount = speed / c4_speed - 1; + + PatternEffect::Vibrato( + Waveform::Sine, + amount.try_change_base().unwrap(), + vibrato_speed, + ) + } 0x8 => { PatternEffect::Panning(Num::new(slot.effect_parameter as i16 - 128) / 128) } @@ -325,9 +308,15 @@ pub fn parse_module(module: &Module) -> TokenStream { let second = effect_parameter & 0xF; if first == 0 { - PatternEffect::VolumeSlide(-Num::new(second as i16) / 64) + PatternEffect::VolumeSlide( + -Num::new(second as i16) / 64, + slot.effect_type == 0x6, + ) } else { - PatternEffect::VolumeSlide(Num::new(first as i16) / 64) + PatternEffect::VolumeSlide( + Num::new(first as i16) / 64, + slot.effect_type == 0x6, + ) } } 0xC => { @@ -340,6 +329,40 @@ pub fn parse_module(module: &Module) -> TokenStream { } } 0xE => match slot.effect_parameter >> 4 { + 0x1 => { + 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 * 8.0, + 0, + module.frequency_type, + ) + .change_base(); + + let portamento_amount = speed / c4_speed; + + PatternEffect::FinePortamento( + portamento_amount.try_change_base().unwrap(), + ) + } + 0x2 => { + let c4_speed = note_to_speed(Note::C4, 0.0, 0, module.frequency_type); + let speed = note_to_speed( + Note::C4, + effect_parameter as f64 * 8.0, + 0, + module.frequency_type, + ); + + let portamento_amount = c4_speed / speed; + + PatternEffect::FinePortamento( + portamento_amount.try_change_base().unwrap(), + ) + } + 0xA => PatternEffect::FineVolumeSlide( Num::new((slot.effect_parameter & 0xf) as i16) / 128, ), @@ -348,7 +371,10 @@ pub fn parse_module(module: &Module) -> TokenStream { ), 0xC => PatternEffect::NoteCut((slot.effect_parameter & 0xf).into()), 0xD => PatternEffect::NoteDelay((slot.effect_parameter & 0xf).into()), - _ => PatternEffect::None, + u => { + eprintln!("Unsupported extended effect E{u:X}y"); + PatternEffect::None + } }, 0xF => match slot.effect_parameter { 0 => PatternEffect::SetTicksPerStep(u32::MAX), @@ -372,7 +398,11 @@ pub fn parse_module(module: &Module) -> TokenStream { PatternEffect::GlobalVolumeSlide(Num::new(first as i32) / 0x40) } } - _ => PatternEffect::None, + e => { + eprintln!("Unsupported effect {e:X}xy"); + + PatternEffect::None + } }; if sample == 0 @@ -414,7 +444,7 @@ pub fn parse_module(module: &Module) -> TokenStream { let samples: Vec<_> = samples .iter() .map(|sample| agb_tracker_interop::Sample { - data: &sample.data, + data: sample.data.clone().into(), should_loop: sample.should_loop, restart_point: sample.restart_point, volume: sample.volume, @@ -432,30 +462,32 @@ pub fn parse_module(module: &Module) -> TokenStream { let envelopes = envelopes .iter() .map(|envelope| agb_tracker_interop::Envelope { - amount: &envelope.amounts, + amount: envelope.amounts.clone().into(), sustain: envelope.sustain, loop_start: envelope.loop_start, loop_end: envelope.loop_end, + + vib_amount: envelope.vib_amount.try_change_base().unwrap(), + vib_waveform: envelope.vib_waveform, + vib_speed: envelope.vib_speed, }) .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 { - samples: &samples, - pattern_data: &pattern_data, - patterns: &patterns, + agb_tracker_interop::Track { + samples: samples.into(), + pattern_data: pattern_data.into(), + patterns: patterns.into(), num_channels: module.get_num_channels(), - patterns_to_play: &patterns_to_play, - envelopes: &envelopes, + patterns_to_play: patterns_to_play.into(), + envelopes: envelopes.into(), 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 { @@ -518,10 +550,19 @@ struct EnvelopeData { sustain: Option, loop_start: Option, loop_end: Option, + + vib_waveform: Waveform, + vib_speed: u8, + vib_amount: Num, } impl EnvelopeData { - fn new(e: &xmrs::envelope::Envelope, bpm: u32) -> Self { + fn new( + e: &xmrs::envelope::Envelope, + instrument: &xmrs::instr_default::InstrDefault, + frequency_type: FrequencyType, + bpm: u32, + ) -> Self { let mut amounts = vec![]; for frame in 0..=(Self::envelope_frame_to_gba_frame(e.point.last().unwrap().frame, bpm)) { @@ -564,11 +605,38 @@ impl EnvelopeData { (None, None) }; + let vib_waveform = match instrument.vibrato.waveform { + xmrs::instr_vibrato::Waveform::Sine => Waveform::Sine, + xmrs::instr_vibrato::Waveform::Square => Waveform::Square, + xmrs::instr_vibrato::Waveform::RampUp => Waveform::Saw, + xmrs::instr_vibrato::Waveform::RampDown => Waveform::Saw, + }; + + let vib_speed = (instrument.vibrato.speed * 64.0) as u8; + let vib_depth = instrument.vibrato.depth * 8.0; + + let c4_speed = note_to_speed(Note::C4, 0.0, 0, frequency_type); + let mut vib_amount = + (note_to_speed(Note::C4, vib_depth.into(), 0, frequency_type) / c4_speed - 1) + .try_change_base() + .unwrap(); + + if matches!( + instrument.vibrato.waveform, + xmrs::instr_vibrato::Waveform::RampDown + ) { + vib_amount = -vib_amount; + } + EnvelopeData { amounts, sustain, loop_start, loop_end, + + vib_waveform, + vib_speed, + vib_amount, } } diff --git a/tracker/agb-xm/Cargo.toml b/tracker/agb-xm/Cargo.toml index cc35322b..5a03edbb 100644 --- a/tracker/agb-xm/Cargo.toml +++ b/tracker/agb-xm/Cargo.toml @@ -12,5 +12,11 @@ proc-macro = true [dependencies] agb_xm_core = { version = "0.20.5", path = "../agb-xm-core" } +agb_tracker_interop = { version = "0.20.2", path = "../agb-tracker-interop", features = ["quote"] } proc-macro-error = "1" proc-macro2 = "1" + +quote = "1" +syn = "2" + +xmrs = "0.6" diff --git a/tracker/agb-xm/src/lib.rs b/tracker/agb-xm/src/lib.rs index bea1dcb4..749d3c7e 100644 --- a/tracker/agb-xm/src/lib.rs +++ b/tracker/agb-xm/src/lib.rs @@ -1,8 +1,49 @@ +use std::{error::Error, fs, path::Path}; + +use agb_xm_core::parse_module; use proc_macro::TokenStream; -use proc_macro_error::proc_macro_error; +use proc_macro_error::{abort, proc_macro_error}; +use quote::quote; +use syn::LitStr; +use xmrs::{module::Module, xm::xmmodule::XmModule}; #[proc_macro_error] #[proc_macro] pub fn include_xm(args: TokenStream) -> TokenStream { - agb_xm_core::agb_xm_core(args.into()).into() + agb_xm_core(args) +} + +fn agb_xm_core(args: TokenStream) -> TokenStream { + let input = match syn::parse::(args) { + Ok(input) => input, + Err(err) => return err.to_compile_error().into(), + }; + + let filename = input.value(); + + let root = std::env::var("CARGO_MANIFEST_DIR").expect("Failed to get cargo manifest dir"); + let path = Path::new(&root).join(&*filename); + + let include_path = path.to_string_lossy(); + + let module = match load_module_from_file(&path) { + Ok(track) => track, + Err(e) => abort!(input, e), + }; + + let parsed = parse_module(&module); + + quote! { + { + const _: &[u8] = include_bytes!(#include_path); + + #parsed + } + } + .into() +} + +fn load_module_from_file(xm_path: &Path) -> Result> { + let file_content = fs::read(xm_path)?; + Ok(XmModule::load(&file_content)?.to_module()) } diff --git a/tracker/desktop-player/Cargo.toml b/tracker/desktop-player/Cargo.toml new file mode 100644 index 00000000..cd9f94e0 --- /dev/null +++ b/tracker/desktop-player/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "desktop-player" +version = "0.1.0" +edition = "2021" +authors = ["Gwilym Inzani "] +license = "MPL-2.0" +description = "A way to play XM files on desktop as they would on the gba without needing a test ROM" +repository = "https://github.com/agbrs/agb" + +[dependencies] +agb_xm_core = { version = "0.20.2", path = "../agb-xm-core" } +agb_tracker = { version = "0.20.2", path = "../agb-tracker", default-features = false } +agb_fixnum = { version = "0.20.2", path = "../../agb-fixnum" } + +xmrs = "0.6" + +cpal = "0.15" diff --git a/tracker/desktop-player/src/main.rs b/tracker/desktop-player/src/main.rs new file mode 100644 index 00000000..5f8e6092 --- /dev/null +++ b/tracker/desktop-player/src/main.rs @@ -0,0 +1,66 @@ +use std::{env, error::Error, fs, path::Path, sync::mpsc}; + +use cpal::{ + traits::{DeviceTrait, HostTrait, StreamTrait}, + SampleFormat, SampleRate, +}; +use mixer::Mixer; +use xmrs::{module::Module, xm::xmmodule::XmModule}; + +mod mixer; + +fn main() -> Result<(), Box> { + let args: Vec = env::args().collect(); + + let file_path = &args[1]; + let module = load_module_from_file(Path::new(file_path))?; + + let track = agb_xm_core::parse_module(&module); + + let mut mixer = Mixer::new(); + let mut tracker = agb_tracker::TrackerInner::new(&track); + + let host = cpal::default_host(); + let device = host + .default_output_device() + .expect("Failed to open output device"); + + let mut supported_configs = device.supported_output_configs()?; + let config = supported_configs + .find_map(|config| { + if config.channels() == 2 && config.sample_format() == SampleFormat::F32 { + return config.try_with_sample_rate(SampleRate(32768)); + } + + None + }) + .expect("Could not produce valid config"); + + let (tx, rx) = mpsc::sync_channel(32768 * 3); + + let stream = device.build_output_stream( + &config.into(), + move |data: &mut [f32], _| { + for val in data.iter_mut() { + *val = rx.try_recv().unwrap_or(0.0); + } + }, + |err| eprintln!("Error on audio stream {err}"), + None, + )?; + + stream.play()?; + + loop { + tracker.step(&mut mixer); + for (l, r) in mixer.frame() { + tx.send((l as f32) / 128.0)?; + tx.send((r as f32) / 128.0)?; + } + } +} + +fn load_module_from_file(xm_path: &Path) -> Result> { + let file_content = fs::read(xm_path)?; + Ok(XmModule::load(&file_content)?.to_module()) +} diff --git a/tracker/desktop-player/src/mixer.rs b/tracker/desktop-player/src/mixer.rs new file mode 100644 index 00000000..990e56e9 --- /dev/null +++ b/tracker/desktop-player/src/mixer.rs @@ -0,0 +1,206 @@ +use agb_fixnum::Num; +use std::{borrow::Cow, num::Wrapping}; + +const BUFFER_SIZE: usize = 560; +const NUM_CHANNELS: usize = 8; + +#[derive(Default)] +pub struct Mixer { + channels: [Option; NUM_CHANNELS], + indices: [Wrapping; NUM_CHANNELS], +} + +impl Mixer { + pub fn new() -> Self { + Self { + channels: Default::default(), + indices: Default::default(), + } + } + + pub fn frame(&mut self) -> Vec<(i8, i8)> { + let channels = + self.channels.iter_mut().flatten().filter(|channel| { + !channel.is_done && channel.volume != 0.into() && channel.is_playing + }); + + let mut buffer = vec![Num::new(0); BUFFER_SIZE * 2]; + + for channel in channels { + let right_amount = ((channel.panning + 1) / 2) * channel.volume; + let left_amount = ((-channel.panning + 1) / 2) * channel.volume; + + let right_amount: Num = right_amount.change_base(); + let left_amount: Num = left_amount.change_base(); + + let channel_len = Num::::new(channel.data.len() as u32); + let mut playback_speed = channel.playback_speed; + + while playback_speed >= channel_len - channel.restart_point { + playback_speed -= channel_len; + } + + let restart_subtract = channel_len - channel.restart_point; + + let mut current_pos = channel.pos; + + for i in 0..BUFFER_SIZE { + let val = channel.data[current_pos.floor() as usize] as i8 as i16; + + buffer[2 * i] += left_amount * val; + buffer[2 * i + 1] += right_amount * val; + + current_pos += playback_speed; + + if current_pos >= channel_len { + if channel.should_loop { + current_pos -= restart_subtract; + } else { + channel.is_done = true; + break; + } + } + } + + channel.pos = current_pos; + } + + let mut ret = Vec::with_capacity(BUFFER_SIZE); + for i in 0..BUFFER_SIZE { + let l = buffer[2 * i].floor(); + let r = buffer[2 * i + 1].floor(); + + ret.push(( + l.clamp(i8::MIN as i16, i8::MAX as i16) as i8, + r.clamp(i8::MIN as i16, i8::MAX as i16) as i8, + )); + } + + ret + } +} + +pub struct SoundChannel { + data: Cow<'static, [u8]>, + pos: Num, + should_loop: bool, + restart_point: Num, + + is_playing: bool, + playback_speed: Num, + volume: Num, + + panning: Num, // between -1 and 1 + is_done: bool, +} + +impl std::fmt::Debug for SoundChannel { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("SoundChannel") + .field("pos", &self.pos) + .field("should_loop", &self.should_loop) + .field("restart_point", &self.restart_point) + .field("is_playing", &self.is_playing) + .field("playback_speed", &self.playback_speed) + .field("volume", &self.volume) + .field("panning", &self.panning) + .field("is_done", &self.is_done) + .finish() + } +} + +impl SoundChannel { + fn new(data: Cow<'static, [u8]>) -> Self { + Self { + data: data.clone(), + + pos: 0.into(), + should_loop: false, + playback_speed: 1.into(), + is_playing: true, + panning: 0.into(), + is_done: false, + volume: 1.into(), + restart_point: 0.into(), + } + } +} + +pub struct SoundChannelId(usize, Wrapping); + +impl agb_tracker::SoundChannel for SoundChannel { + fn new(data: &Cow<'static, [u8]>) -> Self { + Self::new(data.clone()) + } + + fn stop(&mut self) { + self.is_done = true; + } + + fn pause(&mut self) -> &mut Self { + self.is_playing = false; + self + } + + fn resume(&mut self) -> &mut Self { + self.is_playing = true; + self + } + + fn should_loop(&mut self) -> &mut Self { + self.should_loop = true; + self + } + + fn volume(&mut self, value: impl Into>) -> &mut Self { + self.volume = value.into(); + self + } + + fn restart_point(&mut self, value: impl Into>) -> &mut Self { + self.restart_point = value.into(); + self + } + + fn playback(&mut self, playback_speed: impl Into>) -> &mut Self { + self.playback_speed = playback_speed.into(); + self + } + + fn panning(&mut self, panning: impl Into>) -> &mut Self { + self.panning = panning.into(); + self + } +} + +impl agb_tracker::Mixer for Mixer { + type ChannelId = SoundChannelId; + + type SoundChannel = SoundChannel; + + fn channel(&mut self, channel_id: &Self::ChannelId) -> Option<&mut Self::SoundChannel> { + if let Some(channel) = &mut self.channels[channel_id.0] { + if self.indices[channel_id.0] == channel_id.1 && !channel.is_done { + return Some(channel); + } + } + + None + } + + fn play_sound(&mut self, new_channel: Self::SoundChannel) -> Option { + for (i, channel) in self.channels.iter_mut().enumerate() { + if let Some(channel) = channel { + if !channel.is_done { + continue; + } + } + + channel.replace(new_channel); + self.indices[i] += 1; + return Some(SoundChannelId(i, self.indices[i])); + } + + None + } +} diff --git a/tracker/desktop-player/tests/vibrato.xm b/tracker/desktop-player/tests/vibrato.xm new file mode 100644 index 00000000..f167543c Binary files /dev/null and b/tracker/desktop-player/tests/vibrato.xm differ