mirror of
https://github.com/italicsjenga/agb.git
synced 2024-12-23 08:11:33 +11:00
Really basic playing
This commit is contained in:
parent
a77b536e69
commit
308cb3a19c
|
@ -7,16 +7,21 @@ pub struct Track<'a> {
|
||||||
pub samples: &'a [Sample<'a>],
|
pub samples: &'a [Sample<'a>],
|
||||||
pub pattern_data: &'a [PatternSlot],
|
pub pattern_data: &'a [PatternSlot],
|
||||||
pub patterns: &'a [Pattern],
|
pub patterns: &'a [Pattern],
|
||||||
|
|
||||||
|
pub frames_per_step: u16,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Sample<'a> {
|
pub struct Sample<'a> {
|
||||||
pub data: &'a [u8],
|
pub data: &'a [u8],
|
||||||
|
pub should_loop: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Pattern {
|
pub struct Pattern {
|
||||||
pub num_channels: usize,
|
pub num_channels: usize,
|
||||||
|
pub length: usize,
|
||||||
|
pub start_position: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -32,9 +37,12 @@ impl<'a> quote::ToTokens for Track<'a> {
|
||||||
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};
|
||||||
|
|
||||||
let samples = self.samples;
|
let Track {
|
||||||
let pattern_data = self.pattern_data;
|
samples,
|
||||||
let patterns = self.patterns;
|
pattern_data,
|
||||||
|
patterns,
|
||||||
|
frames_per_step,
|
||||||
|
} = self;
|
||||||
|
|
||||||
tokens.append_all(quote! {
|
tokens.append_all(quote! {
|
||||||
{
|
{
|
||||||
|
@ -48,25 +56,43 @@ impl<'a> quote::ToTokens for Track<'a> {
|
||||||
samples: SAMPLES,
|
samples: SAMPLES,
|
||||||
pattern_data: PATTERN_DATA,
|
pattern_data: PATTERN_DATA,
|
||||||
patterns: PATTERNS,
|
patterns: PATTERNS,
|
||||||
|
|
||||||
|
frames_per_step: #frames_per_step,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "quote")]
|
||||||
|
struct ByteString<'a>(&'a [u8]);
|
||||||
|
#[cfg(feature = "quote")]
|
||||||
|
impl quote::ToTokens for ByteString<'_> {
|
||||||
|
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
|
||||||
|
use quote::TokenStreamExt;
|
||||||
|
|
||||||
|
tokens.append(proc_macro2::Literal::byte_string(self.0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "quote")]
|
#[cfg(feature = "quote")]
|
||||||
impl<'a> quote::ToTokens for Sample<'a> {
|
impl<'a> quote::ToTokens for Sample<'a> {
|
||||||
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};
|
||||||
|
|
||||||
let self_as_u8s = self.data.iter().map(|i| *i as u8);
|
let self_as_u8s: Vec<_> = self.data.iter().map(|i| *i as u8).collect();
|
||||||
|
let samples = ByteString(&self_as_u8s);
|
||||||
|
let should_loop = self.should_loop;
|
||||||
|
|
||||||
tokens.append_all(quote! {
|
tokens.append_all(quote! {
|
||||||
{
|
{
|
||||||
use agb_tracker_interop::*;
|
use agb_tracker_interop::*;
|
||||||
|
|
||||||
const SAMPLE_DATA: &[u8] = &[#(#self_as_u8s),*];
|
#[repr(align(4))]
|
||||||
agb_tracker_interop::Sample { data: SAMPLE_DATA }
|
struct AlignmentWrapper<const N: usize>([u8; N]);
|
||||||
|
|
||||||
|
const SAMPLE_DATA: &[u8] = &AlignmentWrapper(*#samples).0;
|
||||||
|
agb_tracker_interop::Sample { data: SAMPLE_DATA, should_loop: #should_loop }
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -109,7 +135,11 @@ impl quote::ToTokens for Pattern {
|
||||||
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};
|
||||||
|
|
||||||
let num_channels = self.num_channels;
|
let Pattern {
|
||||||
|
num_channels,
|
||||||
|
length,
|
||||||
|
start_position,
|
||||||
|
} = self;
|
||||||
|
|
||||||
tokens.append_all(quote! {
|
tokens.append_all(quote! {
|
||||||
{
|
{
|
||||||
|
@ -117,6 +147,8 @@ impl quote::ToTokens for Pattern {
|
||||||
|
|
||||||
Pattern {
|
Pattern {
|
||||||
num_channels: #num_channels,
|
num_channels: #num_channels,
|
||||||
|
length: #length,
|
||||||
|
start_position: #start_position,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
use agb::sound::mixer::Frequency;
|
use agb::sound::mixer::Frequency;
|
||||||
use agb::Gba;
|
use agb::Gba;
|
||||||
use agb_tracker::{import_xm, Track};
|
use agb_tracker::{import_xm, Track, Tracker};
|
||||||
|
|
||||||
const AJOJ: Track = import_xm!("examples/ajoj.xm");
|
const AJOJ: Track = import_xm!("examples/ajoj.xm");
|
||||||
|
|
||||||
|
@ -14,7 +14,10 @@ fn main(mut gba: Gba) -> ! {
|
||||||
let mut mixer = gba.mixer.mixer(Frequency::Hz18157);
|
let mut mixer = gba.mixer.mixer(Frequency::Hz18157);
|
||||||
mixer.enable();
|
mixer.enable();
|
||||||
|
|
||||||
|
let mut tracker = Tracker::new(&AJOJ);
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
|
tracker.step(&mut mixer);
|
||||||
mixer.frame();
|
mixer.frame();
|
||||||
vblank_provider.wait_for_vblank();
|
vblank_provider.wait_for_vblank();
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,12 @@
|
||||||
#![cfg_attr(test, reexport_test_harness_main = "test_main")]
|
#![cfg_attr(test, reexport_test_harness_main = "test_main")]
|
||||||
#![cfg_attr(test, test_runner(agb::test_runner::test_runner))]
|
#![cfg_attr(test, test_runner(agb::test_runner::test_runner))]
|
||||||
|
|
||||||
|
extern crate alloc;
|
||||||
|
|
||||||
|
use alloc::{vec, vec::Vec};
|
||||||
|
|
||||||
|
use agb::sound::mixer::{ChannelId, Mixer, SoundChannel};
|
||||||
|
|
||||||
#[cfg(feature = "xm")]
|
#[cfg(feature = "xm")]
|
||||||
pub use agb_xm::import_xm;
|
pub use agb_xm::import_xm;
|
||||||
|
|
||||||
|
@ -12,11 +18,86 @@ pub use agb_tracker_interop as __private;
|
||||||
|
|
||||||
pub use __private::Track;
|
pub use __private::Track;
|
||||||
|
|
||||||
#[cfg(test)]
|
pub struct Tracker {
|
||||||
mod tests {
|
track: &'static Track<'static>,
|
||||||
#[test_case]
|
channels: Vec<Option<ChannelId>>,
|
||||||
fn it_works(_gba: &mut agb::Gba) {
|
|
||||||
assert_eq!(1, 1);
|
step: u16,
|
||||||
|
current_row: usize,
|
||||||
|
current_pattern: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Tracker {
|
||||||
|
pub fn new(track: &'static Track<'static>) -> Self {
|
||||||
|
agb::println!("{}", track.frames_per_step);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
track,
|
||||||
|
channels: vec![],
|
||||||
|
|
||||||
|
step: 0,
|
||||||
|
current_row: 0,
|
||||||
|
current_pattern: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn step(&mut self, mixer: &mut Mixer) {
|
||||||
|
if self.step != 0 {
|
||||||
|
self.increment_step();
|
||||||
|
return; // TODO: volume / pitch slides
|
||||||
|
}
|
||||||
|
|
||||||
|
let current_pattern = &self.track.patterns[self.current_pattern];
|
||||||
|
|
||||||
|
let channels_to_play = current_pattern.num_channels;
|
||||||
|
self.channels.resize_with(channels_to_play, || None);
|
||||||
|
|
||||||
|
let pattern_data_pos = current_pattern.start_position + self.current_row * channels_to_play;
|
||||||
|
let pattern_slots =
|
||||||
|
&self.track.pattern_data[pattern_data_pos..pattern_data_pos + channels_to_play];
|
||||||
|
|
||||||
|
for (channel_id, pattern_slot) in self.channels.iter_mut().zip(pattern_slots) {
|
||||||
|
if pattern_slot.sample == 0 {
|
||||||
|
// do nothing
|
||||||
|
} else {
|
||||||
|
if let Some(channel) = channel_id
|
||||||
|
.take()
|
||||||
|
.and_then(|channel_id| mixer.channel(&channel_id))
|
||||||
|
{
|
||||||
|
channel.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
let sample = &self.track.samples[pattern_slot.sample - 1];
|
||||||
|
let mut new_channel = SoundChannel::new(sample.data);
|
||||||
|
new_channel
|
||||||
|
.panning(pattern_slot.panning)
|
||||||
|
.volume(pattern_slot.volume)
|
||||||
|
.playback(pattern_slot.speed);
|
||||||
|
|
||||||
|
if sample.should_loop {
|
||||||
|
new_channel.should_loop();
|
||||||
|
}
|
||||||
|
|
||||||
|
*channel_id = mixer.play_sound(new_channel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.increment_step();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn increment_step(&mut self) {
|
||||||
|
self.step += 1;
|
||||||
|
|
||||||
|
if self.step == self.track.frames_per_step * 2 {
|
||||||
|
self.current_row += 1;
|
||||||
|
|
||||||
|
if self.current_row > self.track.patterns[self.current_pattern].length {
|
||||||
|
self.current_pattern += 1;
|
||||||
|
self.current_row = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.step = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -48,26 +48,50 @@ 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();
|
||||||
|
|
||||||
|
struct SampleData {
|
||||||
|
data: Vec<u8>,
|
||||||
|
should_loop: bool,
|
||||||
|
fine_tune: f64,
|
||||||
|
relative_note: i8,
|
||||||
|
}
|
||||||
|
|
||||||
let mut samples = vec![];
|
let mut samples = vec![];
|
||||||
|
|
||||||
for (instrument_index, instrument) in instruments.iter().enumerate() {
|
for (instrument_index, instrument) in instruments.iter().enumerate() {
|
||||||
let InstrumentType::Default(ref instrument) = instrument.instr_type else { continue; };
|
let InstrumentType::Default(ref instrument) = instrument.instr_type else { continue; };
|
||||||
|
|
||||||
for (sample_index, sample) in instrument.sample.iter().enumerate() {
|
for (sample_index, sample) in instrument.sample.iter().enumerate() {
|
||||||
let sample = match &sample.data {
|
let should_loop = !matches!(sample.flags, LoopType::No);
|
||||||
SampleDataType::Depth8(depth8) => depth8
|
let fine_tune = sample.finetune as f64;
|
||||||
.iter()
|
let relative_note = sample.relative_note;
|
||||||
.map(|value| *value as u8)
|
|
||||||
.collect::<Vec<_>>()
|
let mut sample = match &sample.data {
|
||||||
.clone(),
|
SampleDataType::Depth8(depth8) => {
|
||||||
|
depth8.iter().map(|value| *value as u8).collect::<Vec<_>>()
|
||||||
|
}
|
||||||
SampleDataType::Depth16(depth16) => depth16
|
SampleDataType::Depth16(depth16) => depth16
|
||||||
.iter()
|
.iter()
|
||||||
.map(|sample| (sample >> 8) as i8 as u8)
|
.map(|sample| (sample >> 8) as i8 as u8)
|
||||||
.collect::<Vec<_>>(),
|
.collect::<Vec<_>>(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if should_loop {
|
||||||
|
sample.append(&mut sample.clone());
|
||||||
|
sample.append(&mut sample.clone());
|
||||||
|
sample.append(&mut sample.clone());
|
||||||
|
sample.append(&mut sample.clone());
|
||||||
|
sample.append(&mut sample.clone());
|
||||||
|
sample.append(&mut sample.clone());
|
||||||
|
sample.append(&mut sample.clone());
|
||||||
|
}
|
||||||
|
|
||||||
instruments_map.insert((instrument_index, sample_index), samples.len());
|
instruments_map.insert((instrument_index, sample_index), samples.len());
|
||||||
samples.push(sample);
|
samples.push(SampleData {
|
||||||
|
data: sample,
|
||||||
|
should_loop,
|
||||||
|
fine_tune,
|
||||||
|
relative_note,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -76,6 +100,7 @@ pub fn parse_module(module: &Module) -> TokenStream {
|
||||||
|
|
||||||
for pattern in &module.pattern {
|
for pattern in &module.pattern {
|
||||||
let mut num_channels = 0;
|
let mut num_channels = 0;
|
||||||
|
let start_pos = pattern_data.len();
|
||||||
|
|
||||||
for row in pattern.iter() {
|
for row in pattern.iter() {
|
||||||
for slot in row {
|
for slot in row {
|
||||||
|
@ -90,7 +115,7 @@ pub fn parse_module(module: &Module) -> TokenStream {
|
||||||
let sample_slot = instrument.sample_for_note[slot.note as usize] as usize;
|
let sample_slot = instrument.sample_for_note[slot.note as usize] as usize;
|
||||||
instruments_map
|
instruments_map
|
||||||
.get(&(instrument_index, sample_slot))
|
.get(&(instrument_index, sample_slot))
|
||||||
.cloned()
|
.map(|sample_idx| sample_idx + 1)
|
||||||
.unwrap_or(0)
|
.unwrap_or(0)
|
||||||
} else {
|
} else {
|
||||||
0
|
0
|
||||||
|
@ -104,7 +129,23 @@ pub fn parse_module(module: &Module) -> TokenStream {
|
||||||
slot.volume as i16
|
slot.volume as i16
|
||||||
} / 64,
|
} / 64,
|
||||||
);
|
);
|
||||||
let speed = Num::new(1); // TODO: Calculate speed for the correct note here
|
|
||||||
|
if sample == 0 {
|
||||||
|
// TODO should take into account previous sample played on this channel
|
||||||
|
pattern_data.push(agb_tracker_interop::PatternSlot {
|
||||||
|
volume: Num::new(0),
|
||||||
|
speed: Num::new(0),
|
||||||
|
panning: Num::new(0),
|
||||||
|
sample: 0,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
let sample_played = &samples[sample - 1];
|
||||||
|
|
||||||
|
let speed = note_to_speed(
|
||||||
|
slot.note,
|
||||||
|
sample_played.fine_tune,
|
||||||
|
sample_played.relative_note,
|
||||||
|
);
|
||||||
let panning = Num::new(0);
|
let panning = Num::new(0);
|
||||||
|
|
||||||
pattern_data.push(agb_tracker_interop::PatternSlot {
|
pattern_data.push(agb_tracker_interop::PatternSlot {
|
||||||
|
@ -114,23 +155,51 @@ pub fn parse_module(module: &Module) -> TokenStream {
|
||||||
sample,
|
sample,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
num_channels = row.len();
|
num_channels = row.len();
|
||||||
}
|
}
|
||||||
|
|
||||||
patterns.push(agb_tracker_interop::Pattern { num_channels });
|
patterns.push(agb_tracker_interop::Pattern {
|
||||||
|
num_channels,
|
||||||
|
length: pattern.len(),
|
||||||
|
start_position: start_pos,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let samples: Vec<_> = samples
|
let samples: Vec<_> = samples
|
||||||
.iter()
|
.iter()
|
||||||
.map(|sample| agb_tracker_interop::Sample { data: &sample })
|
.map(|sample| agb_tracker_interop::Sample {
|
||||||
|
data: &sample.data,
|
||||||
|
should_loop: sample.should_loop,
|
||||||
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
let frames_per_step =
|
||||||
|
((60.0 * 60.0) / module.default_bpm as f64 / module.default_tempo as f64) as u16;
|
||||||
|
|
||||||
let interop = agb_tracker_interop::Track {
|
let interop = agb_tracker_interop::Track {
|
||||||
samples: &samples,
|
samples: &samples,
|
||||||
pattern_data: &pattern_data,
|
pattern_data: &pattern_data,
|
||||||
patterns: &patterns,
|
patterns: &patterns,
|
||||||
|
|
||||||
|
frames_per_step,
|
||||||
};
|
};
|
||||||
|
|
||||||
quote!(#interop)
|
quote!(#interop)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn note_to_frequency(note: Note, fine_tune: f64, relative_note: i8) -> f64 {
|
||||||
|
let real_note = (note as usize as f64) + (relative_note as f64);
|
||||||
|
let period = 10.0 * 12.0 * 16.0 * 4.0 - (real_note as f64) * 16.0 * 4.0 - fine_tune / 2.0;
|
||||||
|
8363.0 * 2.0f64.powf((6.0 * 12.0 * 16.0 * 4.0 - period) / (12.0 * 16.0 * 4.0))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn note_to_speed(note: Note, fine_tune: f64, relative_note: i8) -> Num<u32, 8> {
|
||||||
|
let frequency = note_to_frequency(note, fine_tune, relative_note);
|
||||||
|
|
||||||
|
let gba_audio_frequency = 18157f64;
|
||||||
|
|
||||||
|
let speed: f64 = frequency / gba_audio_frequency;
|
||||||
|
Num::from_raw((speed * (1 << 8) as f64) as u32)
|
||||||
|
}
|
||||||
|
|
8
tracker/agb-xm-core/src/main.rs
Normal file
8
tracker/agb-xm-core/src/main.rs
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let module = agb_xm_core::load_module_from_file(&std::path::Path::new(
|
||||||
|
"../agb-tracker/examples/ajoj.xm",
|
||||||
|
))?;
|
||||||
|
let output = agb_xm_core::parse_module(&module);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
Loading…
Reference in a new issue