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
|
- name: Set CARGO_TARGET_DIR
|
||||||
run: echo "CARGO_TARGET_DIR=$HOME/target" >> $GITHUB_ENV
|
run: echo "CARGO_TARGET_DIR=$HOME/target" >> $GITHUB_ENV
|
||||||
- name: Install build tools
|
- 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
|
- name: Install Miri
|
||||||
run: |
|
run: |
|
||||||
rustup component add miri clippy rustfmt
|
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]
|
## [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
|
### Fixed
|
||||||
- There are no longer gaps between tiles in affine graphics modes.
|
- There are no longer gaps between tiles in affine graphics modes.
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,7 @@ members = [
|
||||||
# "tracker/agb-tracker",
|
# "tracker/agb-tracker",
|
||||||
"tracker/agb-xm",
|
"tracker/agb-xm",
|
||||||
"tracker/agb-xm-core",
|
"tracker/agb-xm-core",
|
||||||
|
"tracker/desktop-player",
|
||||||
|
|
||||||
"tools",
|
"tools",
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,8 @@ edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
agb = { version = "0.20.5", path = "../../agb" }
|
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 }
|
slotmap = { version = "1", default-features = false }
|
||||||
|
|
||||||
[profile.dev]
|
[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"
|
repository = "https://github.com/agbrs/agb"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
proc-macro-error = "1"
|
|
||||||
proc-macro2 = "1"
|
|
||||||
quote = "1"
|
|
||||||
syn = "2"
|
|
||||||
rustysynth = "1.3"
|
rustysynth = "1.3"
|
||||||
midly = { version = "0.5", default-features = false, features = [
|
midly = { version = "0.5", default-features = false, features = [
|
||||||
"alloc",
|
"alloc",
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use std::{
|
use std::{
|
||||||
|
borrow::Cow,
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
error::Error,
|
error::Error,
|
||||||
fs::{self, File},
|
fs::{self, File},
|
||||||
|
@ -9,63 +10,7 @@ use std::{
|
||||||
use agb_fixnum::Num;
|
use agb_fixnum::Num;
|
||||||
use agb_tracker_interop::{Envelope, Pattern, PatternEffect, PatternSlot, Sample, Track};
|
use agb_tracker_interop::{Envelope, Pattern, PatternEffect, PatternSlot, Sample, Track};
|
||||||
use midly::{Format, MetaMessage, Smf, Timing, TrackEventKind};
|
use midly::{Format, MetaMessage, Smf, Timing, TrackEventKind};
|
||||||
use proc_macro2::TokenStream;
|
|
||||||
use proc_macro_error::abort;
|
|
||||||
use quote::quote;
|
|
||||||
use rustysynth::SoundFont;
|
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 {
|
pub struct MidiInfo {
|
||||||
sound_font: SoundFont,
|
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 mut samples = vec![];
|
||||||
let sf2 = &midi_info.sound_font;
|
let sf2 = &midi_info.sound_font;
|
||||||
let sf2_data = sf2.get_wave_data();
|
let sf2_data = sf2.get_wave_data();
|
||||||
|
@ -386,7 +331,7 @@ pub fn parse_midi(midi_info: &MidiInfo) -> TokenStream {
|
||||||
let samples: Vec<_> = samples
|
let samples: Vec<_> = samples
|
||||||
.iter()
|
.iter()
|
||||||
.map(|sample| Sample {
|
.map(|sample| Sample {
|
||||||
data: &sample.data,
|
data: sample.data.clone().into(),
|
||||||
should_loop: sample.restart_point.is_some(),
|
should_loop: sample.restart_point.is_some(),
|
||||||
restart_point: sample.restart_point.unwrap_or(0),
|
restart_point: sample.restart_point.unwrap_or(0),
|
||||||
volume: 256.into(),
|
volume: 256.into(),
|
||||||
|
@ -409,29 +354,31 @@ pub fn parse_midi(midi_info: &MidiInfo) -> TokenStream {
|
||||||
let envelopes: Vec<_> = envelopes
|
let envelopes: Vec<_> = envelopes
|
||||||
.iter()
|
.iter()
|
||||||
.map(|envelope| Envelope {
|
.map(|envelope| Envelope {
|
||||||
amount: &envelope.amounts,
|
amount: envelope.amounts.clone().into(),
|
||||||
sustain: Some(envelope.amounts.len() - 1),
|
sustain: Some(envelope.amounts.len() - 1),
|
||||||
loop_start: None,
|
loop_start: None,
|
||||||
loop_end: None,
|
loop_end: None,
|
||||||
|
|
||||||
|
vib_waveform: Default::default(),
|
||||||
|
vib_amount: Default::default(),
|
||||||
|
vib_speed: Default::default(),
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let track = Track {
|
Track {
|
||||||
samples: &samples,
|
samples: samples.into(),
|
||||||
envelopes: &envelopes,
|
envelopes: envelopes.into(),
|
||||||
pattern_data: &pattern,
|
patterns: Cow::from(vec![Pattern {
|
||||||
patterns: &[Pattern {
|
|
||||||
length: pattern.len() / resulting_num_channels,
|
length: pattern.len() / resulting_num_channels,
|
||||||
start_position: 0,
|
start_position: 0,
|
||||||
}],
|
}]),
|
||||||
patterns_to_play: &[0],
|
pattern_data: pattern.into(),
|
||||||
|
patterns_to_play: Cow::from(vec![0]),
|
||||||
num_channels: resulting_num_channels,
|
num_channels: resulting_num_channels,
|
||||||
frames_per_tick: Num::from_f64(frames_per_tick),
|
frames_per_tick: Num::from_f64(frames_per_tick),
|
||||||
ticks_per_step: 1,
|
ticks_per_step: 1,
|
||||||
repeat: 0,
|
repeat: 0,
|
||||||
};
|
}
|
||||||
|
|
||||||
quote!(#track)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Default)]
|
#[derive(Clone, Default)]
|
||||||
|
|
|
@ -11,6 +11,16 @@ repository = "https://github.com/agbrs/agb"
|
||||||
proc-macro = true
|
proc-macro = true
|
||||||
|
|
||||||
[dependencies]
|
[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" }
|
agb_midi_core = { version = "0.20.5", path = "../agb-midi-core" }
|
||||||
|
|
||||||
proc-macro-error = "1"
|
proc-macro-error = "1"
|
||||||
proc-macro2 = "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::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_error]
|
||||||
#[proc_macro]
|
#[proc_macro]
|
||||||
pub fn include_midi(args: TokenStream) -> TokenStream {
|
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)]
|
#![cfg_attr(not(feature = "std"), no_std)]
|
||||||
|
|
||||||
|
extern crate alloc;
|
||||||
|
|
||||||
use agb_fixnum::Num;
|
use agb_fixnum::Num;
|
||||||
|
use alloc::borrow::Cow;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Track<'a> {
|
pub struct Track {
|
||||||
pub samples: &'a [Sample<'a>],
|
pub samples: Cow<'static, [Sample]>,
|
||||||
pub envelopes: &'a [Envelope<'a>],
|
pub envelopes: Cow<'static, [Envelope]>,
|
||||||
pub pattern_data: &'a [PatternSlot],
|
pub pattern_data: Cow<'static, [PatternSlot]>,
|
||||||
pub patterns: &'a [Pattern],
|
pub patterns: Cow<'static, [Pattern]>,
|
||||||
pub patterns_to_play: &'a [usize],
|
pub patterns_to_play: Cow<'static, [usize]>,
|
||||||
|
|
||||||
pub num_channels: usize,
|
pub num_channels: usize,
|
||||||
pub frames_per_tick: Num<u32, 8>,
|
pub frames_per_tick: Num<u32, 8>,
|
||||||
|
@ -16,9 +19,9 @@ pub struct Track<'a> {
|
||||||
pub repeat: usize,
|
pub repeat: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Sample<'a> {
|
pub struct Sample {
|
||||||
pub data: &'a [u8],
|
pub data: Cow<'static, [u8]>,
|
||||||
pub should_loop: bool,
|
pub should_loop: bool,
|
||||||
pub restart_point: u32,
|
pub restart_point: u32,
|
||||||
pub volume: Num<i16, 8>,
|
pub volume: Num<i16, 8>,
|
||||||
|
@ -26,7 +29,7 @@ pub struct Sample<'a> {
|
||||||
pub fadeout: Num<i32, 8>,
|
pub fadeout: Num<i32, 8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Pattern {
|
pub struct Pattern {
|
||||||
pub length: usize,
|
pub length: usize,
|
||||||
pub start_position: usize,
|
pub start_position: usize,
|
||||||
|
@ -40,12 +43,16 @@ pub struct PatternSlot {
|
||||||
pub effect2: PatternEffect,
|
pub effect2: PatternEffect,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Envelope<'a> {
|
pub struct Envelope {
|
||||||
pub amount: &'a [Num<i16, 8>],
|
pub amount: Cow<'static, [Num<i16, 8>]>,
|
||||||
pub sustain: Option<usize>,
|
pub sustain: Option<usize>,
|
||||||
pub loop_start: Option<usize>,
|
pub loop_start: Option<usize>,
|
||||||
pub loop_end: 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)]
|
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||||
|
@ -59,13 +66,16 @@ pub enum PatternEffect {
|
||||||
Arpeggio(Num<u16, 8>, Num<u16, 8>),
|
Arpeggio(Num<u16, 8>, Num<u16, 8>),
|
||||||
Panning(Num<i16, 4>),
|
Panning(Num<i16, 4>),
|
||||||
Volume(Num<i16, 8>),
|
Volume(Num<i16, 8>),
|
||||||
VolumeSlide(Num<i16, 8>),
|
// bool = maintain vibrato?
|
||||||
|
VolumeSlide(Num<i16, 8>, bool),
|
||||||
FineVolumeSlide(Num<i16, 8>),
|
FineVolumeSlide(Num<i16, 8>),
|
||||||
NoteCut(u32),
|
NoteCut(u32),
|
||||||
NoteDelay(u32),
|
NoteDelay(u32),
|
||||||
Portamento(Num<u16, 12>),
|
Portamento(Num<u16, 12>),
|
||||||
|
FinePortamento(Num<u16, 12>),
|
||||||
/// Slide each tick the first amount to at most the second amount
|
/// Slide each tick the first amount to at most the second amount
|
||||||
TonePortamento(Num<u16, 12>, Num<u16, 12>),
|
TonePortamento(Num<u16, 12>, Num<u16, 12>),
|
||||||
|
Vibrato(Waveform, Num<u16, 12>, u8),
|
||||||
SetTicksPerStep(u32),
|
SetTicksPerStep(u32),
|
||||||
SetFramesPerTick(Num<u32, 8>),
|
SetFramesPerTick(Num<u32, 8>),
|
||||||
SetGlobalVolume(Num<i32, 8>),
|
SetGlobalVolume(Num<i32, 8>),
|
||||||
|
@ -74,8 +84,16 @@ pub enum PatternEffect {
|
||||||
PitchBend(Num<u32, 8>),
|
PitchBend(Num<u32, 8>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub enum Waveform {
|
||||||
|
#[default]
|
||||||
|
Sine,
|
||||||
|
Saw,
|
||||||
|
Square,
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "quote")]
|
#[cfg(feature = "quote")]
|
||||||
impl<'a> quote::ToTokens for Track<'a> {
|
impl quote::ToTokens for Track {
|
||||||
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
|
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
|
||||||
use quote::{quote, TokenStreamExt};
|
use quote::{quote, TokenStreamExt};
|
||||||
|
|
||||||
|
@ -95,20 +113,24 @@ impl<'a> quote::ToTokens for Track<'a> {
|
||||||
|
|
||||||
tokens.append_all(quote! {
|
tokens.append_all(quote! {
|
||||||
{
|
{
|
||||||
static SAMPLES: &[agb_tracker::__private::agb_tracker_interop::Sample<'static>] = &[#(#samples),*];
|
use alloc::borrow::Cow;
|
||||||
static PATTERN_DATA: &[agb_tracker::__private::agb_tracker_interop::PatternSlot] = &[#(#pattern_data),*];
|
use agb_tracker::__private::agb_tracker_interop::*;
|
||||||
static PATTERNS: &[agb_tracker::__private::agb_tracker_interop::Pattern] = &[#(#patterns),*];
|
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 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 {
|
agb_tracker::Track {
|
||||||
samples: SAMPLES,
|
samples: Cow::Borrowed(SAMPLES),
|
||||||
envelopes: ENVELOPES,
|
envelopes: Cow::Borrowed(ENVELOPES),
|
||||||
pattern_data: PATTERN_DATA,
|
pattern_data: Cow::Borrowed(PATTERN_DATA),
|
||||||
patterns: PATTERNS,
|
patterns: Cow::Borrowed(PATTERNS),
|
||||||
patterns_to_play: PATTERNS_TO_PLAY,
|
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,
|
num_channels: #num_channels,
|
||||||
ticks_per_step: #ticks_per_step,
|
ticks_per_step: #ticks_per_step,
|
||||||
repeat: #repeat,
|
repeat: #repeat,
|
||||||
|
@ -119,7 +141,7 @@ impl<'a> quote::ToTokens for Track<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "quote")]
|
#[cfg(feature = "quote")]
|
||||||
impl quote::ToTokens for Envelope<'_> {
|
impl quote::ToTokens for Envelope {
|
||||||
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
|
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
|
||||||
use quote::{quote, TokenStreamExt};
|
use quote::{quote, TokenStreamExt};
|
||||||
|
|
||||||
|
@ -128,6 +150,9 @@ impl quote::ToTokens for Envelope<'_> {
|
||||||
sustain,
|
sustain,
|
||||||
loop_start,
|
loop_start,
|
||||||
loop_end,
|
loop_end,
|
||||||
|
vib_amount,
|
||||||
|
vib_speed,
|
||||||
|
vib_waveform,
|
||||||
} = self;
|
} = self;
|
||||||
|
|
||||||
let amount = amount.iter().map(|value| {
|
let amount = amount.iter().map(|value| {
|
||||||
|
@ -135,6 +160,11 @@ impl quote::ToTokens for Envelope<'_> {
|
||||||
quote! { agb_tracker::__private::Num::from_raw(#value) }
|
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 {
|
let sustain = match sustain {
|
||||||
Some(value) => quote!(Some(#value)),
|
Some(value) => quote!(Some(#value)),
|
||||||
None => quote!(None),
|
None => quote!(None),
|
||||||
|
@ -153,10 +183,14 @@ impl quote::ToTokens for Envelope<'_> {
|
||||||
static AMOUNTS: &[agb_tracker::__private::Num<i16, 8>] = &[#(#amount),*];
|
static AMOUNTS: &[agb_tracker::__private::Num<i16, 8>] = &[#(#amount),*];
|
||||||
|
|
||||||
agb_tracker::__private::agb_tracker_interop::Envelope {
|
agb_tracker::__private::agb_tracker_interop::Envelope {
|
||||||
amount: AMOUNTS,
|
amount: Cow::Borrowed(AMOUNTS),
|
||||||
sustain: #sustain,
|
sustain: #sustain,
|
||||||
loop_start: #loop_start,
|
loop_start: #loop_start,
|
||||||
loop_end: #loop_end,
|
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")]
|
#[cfg(feature = "quote")]
|
||||||
impl<'a> quote::ToTokens for Sample<'a> {
|
impl quote::ToTokens for Sample {
|
||||||
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
|
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
|
||||||
use quote::{quote, TokenStreamExt};
|
use quote::{quote, TokenStreamExt};
|
||||||
|
|
||||||
|
@ -204,7 +238,7 @@ impl<'a> quote::ToTokens for Sample<'a> {
|
||||||
|
|
||||||
static SAMPLE_DATA: &[u8] = &AlignmentWrapper(*#samples).0;
|
static SAMPLE_DATA: &[u8] = &AlignmentWrapper(*#samples).0;
|
||||||
agb_tracker::__private::agb_tracker_interop::Sample {
|
agb_tracker::__private::agb_tracker_interop::Sample {
|
||||||
data: SAMPLE_DATA,
|
data: Cow::Borrowed(SAMPLE_DATA),
|
||||||
should_loop: #should_loop,
|
should_loop: #should_loop,
|
||||||
restart_point: #restart_point,
|
restart_point: #restart_point,
|
||||||
volume: agb_tracker::__private::Num::from_raw(#volume),
|
volume: agb_tracker::__private::Num::from_raw(#volume),
|
||||||
|
@ -281,9 +315,9 @@ impl quote::ToTokens for PatternEffect {
|
||||||
let volume = volume.to_raw();
|
let volume = volume.to_raw();
|
||||||
quote! { Volume(agb_tracker::__private::Num::from_raw(#volume))}
|
quote! { Volume(agb_tracker::__private::Num::from_raw(#volume))}
|
||||||
}
|
}
|
||||||
PatternEffect::VolumeSlide(amount) => {
|
PatternEffect::VolumeSlide(amount, vibrato) => {
|
||||||
let amount = amount.to_raw();
|
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) => {
|
PatternEffect::FineVolumeSlide(amount) => {
|
||||||
let amount = amount.to_raw();
|
let amount = amount.to_raw();
|
||||||
|
@ -295,6 +329,10 @@ impl quote::ToTokens for PatternEffect {
|
||||||
let amount = amount.to_raw();
|
let amount = amount.to_raw();
|
||||||
quote! { Portamento(agb_tracker::__private::Num::from_raw(#amount))}
|
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) => {
|
PatternEffect::TonePortamento(amount, target) => {
|
||||||
let amount = amount.to_raw();
|
let amount = amount.to_raw();
|
||||||
let target = target.to_raw();
|
let target = target.to_raw();
|
||||||
|
@ -319,6 +357,10 @@ impl quote::ToTokens for PatternEffect {
|
||||||
let amount = amount.to_raw();
|
let amount = amount.to_raw();
|
||||||
quote! { PitchBend(agb_tracker::__private::Num::from_raw(#amount)) }
|
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! {
|
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"]
|
exclude = ["/examples"]
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["xm", "midi"]
|
default = ["agb", "midi", "xm"]
|
||||||
|
|
||||||
|
agb = ["dep:agb"]
|
||||||
xm = ["dep:agb_xm"]
|
xm = ["dep:agb_xm"]
|
||||||
midi = ["dep:agb_midi"]
|
midi = ["dep:agb_midi"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
agb_midi = { version = "0.20.5", path = "../agb-midi", optional = true }
|
agb_midi = { version = "0.20.5", path = "../agb-midi", optional = true }
|
||||||
agb_xm = { version = "0.20.5", path = "../agb-xm", 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 }
|
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]
|
[profile.dev]
|
||||||
opt-level = 3
|
opt-level = 3
|
||||||
debug = true
|
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_std]
|
||||||
#![no_main]
|
#![no_main]
|
||||||
|
|
||||||
|
extern crate alloc;
|
||||||
|
|
||||||
use agb::sound::mixer::Frequency;
|
use agb::sound::mixer::Frequency;
|
||||||
use agb::Gba;
|
use agb::Gba;
|
||||||
use agb_tracker::{include_xm, Track, Tracker};
|
use agb_tracker::{include_xm, Track, Tracker};
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
#![no_std]
|
#![no_std]
|
||||||
#![no_main]
|
#![no_main]
|
||||||
|
|
||||||
|
extern crate alloc;
|
||||||
|
|
||||||
use agb::sound::mixer::Frequency;
|
use agb::sound::mixer::Frequency;
|
||||||
use agb::Gba;
|
use agb::Gba;
|
||||||
use agb_tracker::{include_xm, Track, Tracker};
|
use agb_tracker::{include_xm, Track, Tracker};
|
||||||
|
|
|
@ -65,13 +65,15 @@
|
||||||
|
|
||||||
extern crate alloc;
|
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 alloc::vec::Vec;
|
||||||
|
|
||||||
use agb::{
|
pub use mixer::{Mixer, SoundChannel};
|
||||||
fixnum::Num,
|
|
||||||
sound::mixer::{ChannelId, Mixer, SoundChannel},
|
use agb_fixnum::Num;
|
||||||
};
|
|
||||||
|
|
||||||
/// Import an XM file. Only available if you have the `xm` feature enabled (enabled by default).
|
/// Import an XM file. Only available if you have the `xm` feature enabled (enabled by default).
|
||||||
#[cfg(feature = "xm")]
|
#[cfg(feature = "xm")]
|
||||||
|
@ -86,7 +88,7 @@ pub use agb_midi::include_midi;
|
||||||
|
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub mod __private {
|
pub mod __private {
|
||||||
pub use agb::fixnum::Num;
|
pub use agb_fixnum::Num;
|
||||||
pub use agb_tracker_interop;
|
pub use agb_tracker_interop;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -94,11 +96,13 @@ pub mod __private {
|
||||||
pub use agb_tracker_interop::Track;
|
pub use agb_tracker_interop::Track;
|
||||||
|
|
||||||
/// Stores the required state in order to play tracker music.
|
/// Stores the required state in order to play tracker music.
|
||||||
pub struct Tracker {
|
pub struct TrackerInner<'track, TChannelId> {
|
||||||
track: &'static Track<'static>,
|
track: &'track Track,
|
||||||
channels: Vec<TrackerChannel>,
|
channels: Vec<TrackerChannel>,
|
||||||
envelopes: Vec<Option<EnvelopeState>>,
|
envelopes: Vec<Option<EnvelopeState>>,
|
||||||
|
|
||||||
|
mixer_channels: Vec<Option<TChannelId>>,
|
||||||
|
|
||||||
frame: Num<u32, 8>,
|
frame: Num<u32, 8>,
|
||||||
tick: u32,
|
tick: u32,
|
||||||
first: bool,
|
first: bool,
|
||||||
|
@ -111,10 +115,44 @@ pub struct Tracker {
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct TrackerChannel {
|
struct TrackerChannel {
|
||||||
channel_id: Option<ChannelId>,
|
|
||||||
original_speed: Num<u32, 16>,
|
original_speed: Num<u32, 16>,
|
||||||
base_speed: Num<u32, 16>,
|
base_speed: Num<u32, 16>,
|
||||||
volume: Num<i32, 8>,
|
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 {
|
struct EnvelopeState {
|
||||||
|
@ -122,6 +160,8 @@ struct EnvelopeState {
|
||||||
envelope_id: usize,
|
envelope_id: usize,
|
||||||
finished: bool,
|
finished: bool,
|
||||||
fadeout: Num<i32, 8>,
|
fadeout: Num<i32, 8>,
|
||||||
|
|
||||||
|
vibrato_pos: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
@ -132,15 +172,18 @@ struct GlobalSettings {
|
||||||
volume: Num<i32, 8>,
|
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.
|
/// 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();
|
let mut channels = Vec::new();
|
||||||
channels.resize_with(track.num_channels, Default::default);
|
channels.resize_with(track.num_channels, Default::default);
|
||||||
|
|
||||||
let mut envelopes = Vec::new();
|
let mut envelopes = Vec::new();
|
||||||
envelopes.resize_with(track.num_channels, || None);
|
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 {
|
let global_settings = GlobalSettings {
|
||||||
ticks_per_step: track.ticks_per_step,
|
ticks_per_step: track.ticks_per_step,
|
||||||
frames_per_tick: track.frames_per_tick,
|
frames_per_tick: track.frames_per_tick,
|
||||||
|
@ -149,6 +192,7 @@ impl Tracker {
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
track,
|
track,
|
||||||
|
mixer_channels,
|
||||||
channels,
|
channels,
|
||||||
envelopes,
|
envelopes,
|
||||||
|
|
||||||
|
@ -165,9 +209,11 @@ impl Tracker {
|
||||||
|
|
||||||
/// Call this once per frame before calling [`mixer.frame`](agb::sound::mixer::Mixer::frame()).
|
/// 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.
|
/// 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() {
|
if !self.increment_frame() {
|
||||||
self.update_envelopes(mixer);
|
self.update_envelopes();
|
||||||
|
|
||||||
|
self.realise(mixer);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -183,28 +229,48 @@ impl Tracker {
|
||||||
{
|
{
|
||||||
if pattern_slot.sample != 0 && self.tick == 0 {
|
if pattern_slot.sample != 0 && self.tick == 0 {
|
||||||
let sample = &self.track.samples[pattern_slot.sample as usize - 1];
|
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 {
|
self.envelopes[i] = sample.volume_envelope.map(|envelope_id| EnvelopeState {
|
||||||
frame: 0,
|
frame: 0,
|
||||||
envelope_id,
|
envelope_id,
|
||||||
finished: false,
|
finished: false,
|
||||||
fadeout: sample.fadeout,
|
fadeout: sample.fadeout,
|
||||||
|
|
||||||
|
vibrato_pos: 0,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.tick == 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(
|
channel.apply_effect(
|
||||||
mixer,
|
|
||||||
&pattern_slot.effect1,
|
&pattern_slot.effect1,
|
||||||
self.tick,
|
self.tick,
|
||||||
&mut self.global_settings,
|
&mut self.global_settings,
|
||||||
&mut self.envelopes[i],
|
&mut self.envelopes[i],
|
||||||
);
|
);
|
||||||
channel.apply_effect(
|
channel.apply_effect(
|
||||||
mixer,
|
|
||||||
&pattern_slot.effect2,
|
&pattern_slot.effect2,
|
||||||
self.tick,
|
self.tick,
|
||||||
&mut self.global_settings,
|
&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) {
|
for (channel, envelope_state_option) in self.channels.iter_mut().zip(&mut self.envelopes) {
|
||||||
if let Some(envelope_state) = envelope_state_option {
|
if let Some(envelope_state) = envelope_state_option {
|
||||||
let envelope = &self.track.envelopes[envelope_state.envelope_id];
|
let envelope = &self.track.envelopes[envelope_state.envelope_id];
|
||||||
|
|
||||||
if !channel.update_volume_envelope(
|
if !channel.update_volume_envelope(envelope_state, envelope, &self.global_settings)
|
||||||
mixer,
|
{
|
||||||
envelope_state,
|
|
||||||
envelope,
|
|
||||||
&self.global_settings,
|
|
||||||
) {
|
|
||||||
envelope_state_option.take();
|
envelope_state_option.take();
|
||||||
} else {
|
} else {
|
||||||
envelope_state.frame += 1;
|
envelope_state.frame += 1;
|
||||||
|
@ -289,177 +397,131 @@ impl Tracker {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TrackerChannel {
|
impl TrackerChannel {
|
||||||
fn play_sound(
|
fn reset(&mut self, sample: &Sample) {
|
||||||
&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);
|
|
||||||
self.volume = sample.volume.change_base();
|
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>) {
|
fn set_speed(&mut self, speed: Num<u32, 8>) {
|
||||||
if let Some(channel) = self
|
if speed != 0.into() {
|
||||||
.channel_id
|
self.base_speed = speed.change_base();
|
||||||
.as_ref()
|
self.original_speed = self.base_speed;
|
||||||
.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());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.current_speed = self.base_speed;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_effect(
|
fn apply_effect(
|
||||||
&mut self,
|
&mut self,
|
||||||
mixer: &mut Mixer<'_>,
|
|
||||||
effect: &PatternEffect,
|
effect: &PatternEffect,
|
||||||
tick: u32,
|
tick: u32,
|
||||||
global_settings: &mut GlobalSettings,
|
global_settings: &mut GlobalSettings,
|
||||||
envelope_state: &mut Option<EnvelopeState>,
|
envelope_state: &mut Option<EnvelopeState>,
|
||||||
) {
|
) {
|
||||||
if let Some(channel) = self
|
match effect {
|
||||||
.channel_id
|
PatternEffect::None => {}
|
||||||
.as_ref()
|
PatternEffect::Stop => {
|
||||||
.and_then(|channel_id| mixer.channel(channel_id))
|
self.current_volume = 0.into();
|
||||||
{
|
|
||||||
match effect {
|
if let Some(envelope_state) = envelope_state {
|
||||||
PatternEffect::None => {}
|
envelope_state.finished = true;
|
||||||
PatternEffect::Stop => {
|
}
|
||||||
channel.volume(0);
|
}
|
||||||
|
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 {
|
if let Some(envelope_state) = envelope_state {
|
||||||
envelope_state.finished = true;
|
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) => {
|
PatternEffect::SetTicksPerStep(amount) => {
|
||||||
global_settings.ticks_per_step = *amount;
|
global_settings.ticks_per_step = *amount;
|
||||||
}
|
}
|
||||||
|
@ -473,44 +535,110 @@ impl TrackerChannel {
|
||||||
global_settings.volume =
|
global_settings.volume =
|
||||||
(global_settings.volume + *volume_delta).clamp(0.into(), 1.into());
|
(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]
|
#[must_use]
|
||||||
fn update_volume_envelope(
|
fn update_volume_envelope(
|
||||||
&mut self,
|
&mut self,
|
||||||
mixer: &mut Mixer<'_>,
|
|
||||||
envelope_state: &EnvelopeState,
|
envelope_state: &EnvelopeState,
|
||||||
envelope: &agb_tracker_interop::Envelope<'_>,
|
envelope: &agb_tracker_interop::Envelope,
|
||||||
global_settings: &GlobalSettings,
|
global_settings: &GlobalSettings,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
if let Some(channel) = self
|
let amount = envelope.amount[envelope_state.frame];
|
||||||
.channel_id
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|channel_id| mixer.channel(channel_id))
|
|
||||||
{
|
|
||||||
let amount = envelope.amount[envelope_state.frame];
|
|
||||||
|
|
||||||
if envelope_state.finished {
|
if envelope_state.finished {
|
||||||
self.volume = (self.volume - envelope_state.fadeout).max(0.into());
|
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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]
|
#[agb::entry]
|
||||||
fn main(gba: agb::Gba) -> ! {
|
fn main(gba: agb::Gba) -> ! {
|
||||||
loop {}
|
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"
|
quote = "1"
|
||||||
syn = "2"
|
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" }
|
agb_fixnum = { version = "0.20.5", path = "../../agb-fixnum" }
|
||||||
|
|
||||||
xmrs = { version = "0.6.1", features = ["std"] }
|
xmrs = { version = "0.6.1", features = ["std"] }
|
||||||
|
|
|
@ -1,51 +1,11 @@
|
||||||
use std::{collections::HashMap, error::Error, fs, path::Path};
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use agb_tracker_interop::PatternEffect;
|
|
||||||
use proc_macro2::TokenStream;
|
|
||||||
use proc_macro_error::abort;
|
|
||||||
|
|
||||||
use quote::quote;
|
|
||||||
use syn::LitStr;
|
|
||||||
|
|
||||||
use agb_fixnum::Num;
|
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 {
|
pub fn parse_module(module: &Module) -> agb_tracker_interop::Track {
|
||||||
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 {
|
|
||||||
let instruments = &module.instrument;
|
let instruments = &module.instrument;
|
||||||
let mut instruments_map = HashMap::new();
|
let mut instruments_map = HashMap::new();
|
||||||
|
|
||||||
|
@ -71,7 +31,12 @@ pub fn parse_module(module: &Module) -> TokenStream {
|
||||||
|
|
||||||
let envelope = &instrument.volume_envelope;
|
let envelope = &instrument.volume_envelope;
|
||||||
let envelope_id = if envelope.enabled {
|
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
|
let id = existing_envelopes
|
||||||
.entry(envelope)
|
.entry(envelope)
|
||||||
.or_insert_with_key(|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)
|
.map(|note_and_sample| note_and_sample.1.volume)
|
||||||
.unwrap_or(1.into()),
|
.unwrap_or(1.into()),
|
||||||
),
|
),
|
||||||
0x60..=0x6F => {
|
0x60..=0x6F => PatternEffect::VolumeSlide(
|
||||||
PatternEffect::VolumeSlide(-Num::new((slot.volume - 0x60) as i16) / 64)
|
-Num::new((slot.volume - 0x60) as i16) / 64,
|
||||||
}
|
false,
|
||||||
0x70..=0x7F => {
|
),
|
||||||
PatternEffect::VolumeSlide(Num::new((slot.volume - 0x70) as i16) / 64)
|
0x70..=0x7F => PatternEffect::VolumeSlide(
|
||||||
}
|
Num::new((slot.volume - 0x70) as i16) / 64,
|
||||||
|
false,
|
||||||
|
),
|
||||||
0x80..=0x8F => PatternEffect::FineVolumeSlide(
|
0x80..=0x8F => PatternEffect::FineVolumeSlide(
|
||||||
-Num::new((slot.volume - 0x80) as i16) / 128,
|
-Num::new((slot.volume - 0x80) as i16) / 128,
|
||||||
),
|
),
|
||||||
|
@ -317,6 +284,22 @@ pub fn parse_module(module: &Module) -> TokenStream {
|
||||||
PatternEffect::None
|
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 => {
|
0x8 => {
|
||||||
PatternEffect::Panning(Num::new(slot.effect_parameter as i16 - 128) / 128)
|
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;
|
let second = effect_parameter & 0xF;
|
||||||
|
|
||||||
if first == 0 {
|
if first == 0 {
|
||||||
PatternEffect::VolumeSlide(-Num::new(second as i16) / 64)
|
PatternEffect::VolumeSlide(
|
||||||
|
-Num::new(second as i16) / 64,
|
||||||
|
slot.effect_type == 0x6,
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
PatternEffect::VolumeSlide(Num::new(first as i16) / 64)
|
PatternEffect::VolumeSlide(
|
||||||
|
Num::new(first as i16) / 64,
|
||||||
|
slot.effect_type == 0x6,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
0xC => {
|
0xC => {
|
||||||
|
@ -340,6 +329,40 @@ pub fn parse_module(module: &Module) -> TokenStream {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
0xE => match slot.effect_parameter >> 4 {
|
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(
|
0xA => PatternEffect::FineVolumeSlide(
|
||||||
Num::new((slot.effect_parameter & 0xf) as i16) / 128,
|
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()),
|
0xC => PatternEffect::NoteCut((slot.effect_parameter & 0xf).into()),
|
||||||
0xD => PatternEffect::NoteDelay((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 {
|
0xF => match slot.effect_parameter {
|
||||||
0 => PatternEffect::SetTicksPerStep(u32::MAX),
|
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::GlobalVolumeSlide(Num::new(first as i32) / 0x40)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => PatternEffect::None,
|
e => {
|
||||||
|
eprintln!("Unsupported effect {e:X}xy");
|
||||||
|
|
||||||
|
PatternEffect::None
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if sample == 0
|
if sample == 0
|
||||||
|
@ -414,7 +444,7 @@ pub fn parse_module(module: &Module) -> TokenStream {
|
||||||
let samples: Vec<_> = samples
|
let samples: Vec<_> = samples
|
||||||
.iter()
|
.iter()
|
||||||
.map(|sample| agb_tracker_interop::Sample {
|
.map(|sample| agb_tracker_interop::Sample {
|
||||||
data: &sample.data,
|
data: sample.data.clone().into(),
|
||||||
should_loop: sample.should_loop,
|
should_loop: sample.should_loop,
|
||||||
restart_point: sample.restart_point,
|
restart_point: sample.restart_point,
|
||||||
volume: sample.volume,
|
volume: sample.volume,
|
||||||
|
@ -432,30 +462,32 @@ pub fn parse_module(module: &Module) -> TokenStream {
|
||||||
let envelopes = envelopes
|
let envelopes = envelopes
|
||||||
.iter()
|
.iter()
|
||||||
.map(|envelope| agb_tracker_interop::Envelope {
|
.map(|envelope| agb_tracker_interop::Envelope {
|
||||||
amount: &envelope.amounts,
|
amount: envelope.amounts.clone().into(),
|
||||||
sustain: envelope.sustain,
|
sustain: envelope.sustain,
|
||||||
loop_start: envelope.loop_start,
|
loop_start: envelope.loop_start,
|
||||||
loop_end: envelope.loop_end,
|
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<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
let frames_per_tick = bpm_to_frames_per_tick(module.default_bpm as u32);
|
let frames_per_tick = bpm_to_frames_per_tick(module.default_bpm as u32);
|
||||||
let ticks_per_step = module.default_tempo;
|
let ticks_per_step = module.default_tempo;
|
||||||
|
|
||||||
let interop = agb_tracker_interop::Track {
|
agb_tracker_interop::Track {
|
||||||
samples: &samples,
|
samples: samples.into(),
|
||||||
pattern_data: &pattern_data,
|
pattern_data: pattern_data.into(),
|
||||||
patterns: &patterns,
|
patterns: patterns.into(),
|
||||||
num_channels: module.get_num_channels(),
|
num_channels: module.get_num_channels(),
|
||||||
patterns_to_play: &patterns_to_play,
|
patterns_to_play: patterns_to_play.into(),
|
||||||
envelopes: &envelopes,
|
envelopes: envelopes.into(),
|
||||||
|
|
||||||
frames_per_tick,
|
frames_per_tick,
|
||||||
ticks_per_step: ticks_per_step.into(),
|
ticks_per_step: ticks_per_step.into(),
|
||||||
repeat: module.restart_position as usize,
|
repeat: module.restart_position as usize,
|
||||||
};
|
}
|
||||||
|
|
||||||
quote!(#interop)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn bpm_to_frames_per_tick(bpm: u32) -> Num<u32, 8> {
|
fn bpm_to_frames_per_tick(bpm: u32) -> Num<u32, 8> {
|
||||||
|
@ -518,10 +550,19 @@ struct EnvelopeData {
|
||||||
sustain: Option<usize>,
|
sustain: Option<usize>,
|
||||||
loop_start: Option<usize>,
|
loop_start: Option<usize>,
|
||||||
loop_end: Option<usize>,
|
loop_end: Option<usize>,
|
||||||
|
|
||||||
|
vib_waveform: Waveform,
|
||||||
|
vib_speed: u8,
|
||||||
|
vib_amount: Num<i32, 12>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EnvelopeData {
|
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![];
|
let mut amounts = vec![];
|
||||||
|
|
||||||
for frame in 0..=(Self::envelope_frame_to_gba_frame(e.point.last().unwrap().frame, bpm)) {
|
for frame in 0..=(Self::envelope_frame_to_gba_frame(e.point.last().unwrap().frame, bpm)) {
|
||||||
|
@ -564,11 +605,38 @@ impl EnvelopeData {
|
||||||
(None, None)
|
(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 {
|
EnvelopeData {
|
||||||
amounts,
|
amounts,
|
||||||
sustain,
|
sustain,
|
||||||
loop_start,
|
loop_start,
|
||||||
loop_end,
|
loop_end,
|
||||||
|
|
||||||
|
vib_waveform,
|
||||||
|
vib_speed,
|
||||||
|
vib_amount,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,5 +12,11 @@ proc-macro = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
agb_xm_core = { version = "0.20.5", path = "../agb-xm-core" }
|
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-macro-error = "1"
|
||||||
proc-macro2 = "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::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_error]
|
||||||
#[proc_macro]
|
#[proc_macro]
|
||||||
pub fn include_xm(args: TokenStream) -> TokenStream {
|
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