Refactor tracker core (#719)

I thought it would be fun to try and write the tracker in such a way
that it could be used locally too.

- [x] changelog updated
This commit is contained in:
Gwilym Inzani 2024-07-16 20:11:04 +01:00 committed by GitHub
commit db9fdb77e3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 1081 additions and 380 deletions

View file

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

View file

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

View file

@ -20,6 +20,7 @@ members = [
# "tracker/agb-tracker",
"tracker/agb-xm",
"tracker/agb-xm-core",
"tracker/desktop-player",
"tools",

View file

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

View file

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

View file

@ -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<Self> {
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)]

View file

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

View file

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

View file

@ -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<u32, 8>,
@ -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<i16, 8>,
@ -26,7 +29,7 @@ pub struct Sample<'a> {
pub fadeout: Num<i32, 8>,
}
#[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<i16, 8>],
#[derive(Debug, Clone)]
pub struct Envelope {
pub amount: Cow<'static, [Num<i16, 8>]>,
pub sustain: Option<usize>,
pub loop_start: Option<usize>,
pub loop_end: Option<usize>,
pub vib_waveform: Waveform,
pub vib_amount: Num<i16, 12>,
pub vib_speed: u8,
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
@ -59,13 +66,16 @@ pub enum PatternEffect {
Arpeggio(Num<u16, 8>, Num<u16, 8>),
Panning(Num<i16, 4>),
Volume(Num<i16, 8>),
VolumeSlide(Num<i16, 8>),
// bool = maintain vibrato?
VolumeSlide(Num<i16, 8>, bool),
FineVolumeSlide(Num<i16, 8>),
NoteCut(u32),
NoteDelay(u32),
Portamento(Num<u16, 12>),
FinePortamento(Num<u16, 12>),
/// Slide each tick the first amount to at most the second amount
TonePortamento(Num<u16, 12>, Num<u16, 12>),
Vibrato(Waveform, Num<u16, 12>, u8),
SetTicksPerStep(u32),
SetFramesPerTick(Num<u32, 8>),
SetGlobalVolume(Num<i32, 8>),
@ -74,8 +84,16 @@ pub enum PatternEffect {
PitchBend(Num<u32, 8>),
}
#[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<i16, 8>] = &[#(#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
});
}
}

View file

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

View file

@ -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::<i32, 12>::new(i) / 64).sin());
let square = (0..64).map(|i| {
if i < 32 {
Num::<i32, 12>::new(-1)
} else {
Num::<i32, 12>::new(1)
}
});
let saw = (0..64).map(|i| (Num::<i32, 12>::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<i32, 12>; 64] = [{sine_lookup}];
pub(crate) static SQUARE_LOOKUP: [agb_fixnum::Num<i32, 12>; 64] = [{square_lookup}];
pub(crate) static SAW_LOOKUP: [agb_fixnum::Num<i32, 12>; 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<Item = Num<i32, 12>>) -> String {
let output: Vec<_> = input
.into_iter()
.map(|v| format!("agb_fixnum::Num::from_raw({})", v.to_raw()))
.collect();
output.join(", ")
}

View file

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

View file

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

View file

@ -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<TrackerChannel>,
envelopes: Vec<Option<EnvelopeState>>,
mixer_channels: Vec<Option<TChannelId>>,
frame: Num<u32, 8>,
tick: u32,
first: bool,
@ -111,10 +115,44 @@ pub struct Tracker {
#[derive(Default)]
struct TrackerChannel {
channel_id: Option<ChannelId>,
original_speed: Num<u32, 16>,
base_speed: Num<u32, 16>,
volume: Num<i32, 8>,
vibrato: Waves,
current_volume: Num<i32, 8>,
current_speed: Num<u32, 16>,
current_panning: Num<i32, 8>,
is_playing: bool,
}
#[derive(Default)]
struct Waves {
waveform: Waveform,
frame: usize,
speed: usize,
amount: Num<i32, 12>,
enable: bool,
}
impl Waves {
fn value(&self) -> Num<u32, 8> {
assert!(self.amount.abs() <= 1.into());
calculate_wave(self.waveform, self.amount, self.frame)
}
}
fn calculate_wave(waveform: Waveform, amount: Num<i32, 12>, frame: usize) -> Num<u32, 8> {
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<i32, 8>,
vibrato_pos: usize,
}
#[derive(Clone)]
@ -132,15 +172,18 @@ struct GlobalSettings {
volume: Num<i32, 8>,
}
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<M: Mixer<ChannelId = TChannelId>>(&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<M: Mixer<ChannelId = TChannelId>>(&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<u32, 8>) {
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<u32, 8>) {
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<EnvelopeState>,
) {
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<Num<i16, 8>>) -> &mut Self {
self.volume(value)
}
fn restart_point(&mut self, value: impl Into<Num<u32, 8>>) -> &mut Self {
self.restart_point(value)
}
fn playback(&mut self, playback_speed: impl Into<Num<u32, 8>>) -> &mut Self {
self.playback(playback_speed)
}
fn panning(&mut self, panning: impl Into<Num<i16, 8>>) -> &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::ChannelId> {
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>;

View file

@ -0,0 +1 @@
include!(concat!(env!("OUT_DIR"), "/lookups.rs"));

View file

@ -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<Num<i16, 8>>) -> &mut Self;
fn restart_point(&mut self, value: impl Into<Num<u32, 8>>) -> &mut Self;
fn playback(&mut self, playback_speed: impl Into<Num<u32, 8>>) -> &mut Self;
fn panning(&mut self, panning: impl Into<Num<i16, 8>>) -> &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<Self::ChannelId>;
}

View file

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

View file

@ -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::<LitStr>(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<Module, Box<dyn Error>> {
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<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 * 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::<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 {
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<u32, 8> {
@ -518,10 +550,19 @@ struct EnvelopeData {
sustain: Option<usize>,
loop_start: Option<usize>,
loop_end: Option<usize>,
vib_waveform: Waveform,
vib_speed: u8,
vib_amount: Num<i32, 12>,
}
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,
}
}

View file

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

View file

@ -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::<LitStr>(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<Module, Box<dyn Error>> {
let file_content = fs::read(xm_path)?;
Ok(XmModule::load(&file_content)?.to_module())
}

View file

@ -0,0 +1,17 @@
[package]
name = "desktop-player"
version = "0.1.0"
edition = "2021"
authors = ["Gwilym Inzani <gw@ilym.me>"]
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"

View file

@ -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<dyn Error>> {
let args: Vec<String> = 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<Module, Box<dyn Error>> {
let file_content = fs::read(xm_path)?;
Ok(XmModule::load(&file_content)?.to_module())
}

View file

@ -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<SoundChannel>; NUM_CHANNELS],
indices: [Wrapping<usize>; 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<i16, 4> = right_amount.change_base();
let left_amount: Num<i16, 4> = left_amount.change_base();
let channel_len = Num::<u32, 8>::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<u32, 8>,
should_loop: bool,
restart_point: Num<u32, 8>,
is_playing: bool,
playback_speed: Num<u32, 8>,
volume: Num<i16, 8>,
panning: Num<i16, 8>, // 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<usize>);
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<Num<i16, 8>>) -> &mut Self {
self.volume = value.into();
self
}
fn restart_point(&mut self, value: impl Into<Num<u32, 8>>) -> &mut Self {
self.restart_point = value.into();
self
}
fn playback(&mut self, playback_speed: impl Into<Num<u32, 8>>) -> &mut Self {
self.playback_speed = playback_speed.into();
self
}
fn panning(&mut self, panning: impl Into<Num<i16, 8>>) -> &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<Self::ChannelId> {
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
}
}

Binary file not shown.