mirror of
https://github.com/italicsjenga/agb.git
synced 2024-12-23 08:11:33 +11:00
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:
commit
db9fdb77e3
2
.github/workflows/build-and-test.yml
vendored
2
.github/workflows/build-and-test.yml
vendored
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ members = [
|
|||
# "tracker/agb-tracker",
|
||||
"tracker/agb-xm",
|
||||
"tracker/agb-xm-core",
|
||||
"tracker/desktop-player",
|
||||
|
||||
"tools",
|
||||
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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",
|
||||
] }
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
48
tracker/agb-tracker/build.rs
Normal file
48
tracker/agb-tracker/build.rs
Normal 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(", ")
|
||||
}
|
|
@ -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};
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -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>;
|
||||
|
|
1
tracker/agb-tracker/src/lookups.rs
Normal file
1
tracker/agb-tracker/src/lookups.rs
Normal file
|
@ -0,0 +1 @@
|
|||
include!(concat!(env!("OUT_DIR"), "/lookups.rs"));
|
28
tracker/agb-tracker/src/mixer.rs
Normal file
28
tracker/agb-tracker/src/mixer.rs
Normal 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>;
|
||||
}
|
|
@ -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"] }
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
|
17
tracker/desktop-player/Cargo.toml
Normal file
17
tracker/desktop-player/Cargo.toml
Normal 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"
|
66
tracker/desktop-player/src/main.rs
Normal file
66
tracker/desktop-player/src/main.rs
Normal 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())
|
||||
}
|
206
tracker/desktop-player/src/mixer.rs
Normal file
206
tracker/desktop-player/src/mixer.rs
Normal 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
|
||||
}
|
||||
}
|
BIN
tracker/desktop-player/tests/vibrato.xm
Normal file
BIN
tracker/desktop-player/tests/vibrato.xm
Normal file
Binary file not shown.
Loading…
Reference in a new issue