Update tracker to support PatternBreak and PositionJump (#748)

This pull request adds support for pattern breaks and position jumps to
the music tracker module. This PR only adds functionality for XM input
files for now. However, these changes would also be applicable to other
types of music modules, such as Amiga modules.

In `agb-xm-core`: 
* handle effects Bxx, Dxx, and their combination

In `agb-tracker-interop`: 
* represent jump using a `PatternEffect`

In `agb-tracker`:
* keep track of any applicable jump in `TrackerInner`, and handle it in
`increment_frame()` when needed
This commit is contained in:
Gwilym Inzani 2024-08-24 23:23:21 +01:00 committed by GitHub
commit a79829068c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 137 additions and 10 deletions

View file

@ -29,6 +29,16 @@ pub struct Sample {
pub fadeout: Num<i32, 8>, pub fadeout: Num<i32, 8>,
} }
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Jump {
/// Jump to the given pattern position `pattern` at row index 0
Position { pattern: u8 },
/// Jump to the next pattern position, at row index `row`
PatternBreak { row: u8 },
/// Jump to the pattern position `pattern` at row index `row`
Combined { pattern: u8, row: u8 },
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Pattern { pub struct Pattern {
pub length: usize, pub length: usize,
@ -82,6 +92,7 @@ pub enum PatternEffect {
GlobalVolumeSlide(Num<i32, 8>), GlobalVolumeSlide(Num<i32, 8>),
/// Increase / decrease the pitch by the specified amount immediately /// Increase / decrease the pitch by the specified amount immediately
PitchBend(Num<u32, 8>), PitchBend(Num<u32, 8>),
Jump(Jump),
} }
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
@ -92,6 +103,31 @@ pub enum Waveform {
Square, Square,
} }
#[cfg(feature = "quote")]
impl quote::ToTokens for Jump {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
use quote::{quote, TokenStreamExt};
let type_bit = match self {
Jump::Position { pattern } => {
quote! {Position{pattern: #pattern} }
}
Jump::PatternBreak { row } => {
quote! { PatternBreak{row: #row} }
}
Jump::Combined { pattern, row } => {
quote! {
Combined{pattern: #pattern, row: #row }
}
}
};
tokens.append_all(quote! {
agb_tracker::__private::agb_tracker_interop::Jump::#type_bit
});
}
}
#[cfg(feature = "quote")] #[cfg(feature = "quote")]
impl quote::ToTokens for Track { impl quote::ToTokens for Track {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
@ -286,6 +322,7 @@ impl quote::ToTokens for Pattern {
} = self; } = self;
tokens.append_all(quote! { tokens.append_all(quote! {
agb_tracker::__private::agb_tracker_interop::Pattern { agb_tracker::__private::agb_tracker_interop::Pattern {
length: #length, length: #length,
start_position: #start_position, start_position: #start_position,
@ -361,6 +398,9 @@ impl quote::ToTokens for PatternEffect {
let amount = amount.to_raw(); let amount = amount.to_raw();
quote! { Vibrato(#waveform, agb_tracker::__private::Num::from_raw(#amount), #speed) } quote! { Vibrato(#waveform, agb_tracker::__private::Num::from_raw(#amount), #speed) }
} }
PatternEffect::Jump(jump) => {
quote! { Jump(#jump) }
}
}; };
tokens.append_all(quote! { tokens.append_all(quote! {

View file

@ -68,7 +68,7 @@ extern crate alloc;
mod lookups; mod lookups;
mod mixer; mod mixer;
use agb_tracker_interop::{PatternEffect, Sample, Waveform}; use agb_tracker_interop::{Jump, PatternEffect, Sample, Waveform};
use alloc::vec::Vec; use alloc::vec::Vec;
pub use mixer::{Mixer, SoundChannel}; pub use mixer::{Mixer, SoundChannel};
@ -111,6 +111,7 @@ pub struct TrackerInner<'track, TChannelId> {
current_row: usize, current_row: usize,
current_pattern: usize, current_pattern: usize,
current_jump: Option<Jump>,
} }
#[derive(Default)] #[derive(Default)]
@ -204,6 +205,7 @@ impl<'track, TChannelId> TrackerInner<'track, TChannelId> {
current_pattern: 0, current_pattern: 0,
current_row: 0, current_row: 0,
current_jump: None,
} }
} }
@ -269,12 +271,14 @@ impl<'track, TChannelId> TrackerInner<'track, TChannelId> {
self.tick, self.tick,
&mut self.global_settings, &mut self.global_settings,
&mut self.envelopes[i], &mut self.envelopes[i],
&mut self.current_jump,
); );
channel.apply_effect( channel.apply_effect(
&pattern_slot.effect2, &pattern_slot.effect2,
self.tick, self.tick,
&mut self.global_settings, &mut self.global_settings,
&mut self.envelopes[i], &mut self.envelopes[i],
&mut self.current_jump,
); );
} }
@ -373,16 +377,21 @@ impl<'track, TChannelId> TrackerInner<'track, TChannelId> {
self.frame -= self.global_settings.frames_per_tick; self.frame -= self.global_settings.frames_per_tick;
if self.tick >= self.global_settings.ticks_per_step { if self.tick >= self.global_settings.ticks_per_step {
self.current_row += 1; if let Some(jump) = self.current_jump.take() {
self.handle_jump(jump);
} else {
self.current_row += 1;
if self.current_row if self.current_row
>= self.track.patterns[self.track.patterns_to_play[self.current_pattern]].length >= self.track.patterns[self.track.patterns_to_play[self.current_pattern]]
{ .length
self.current_pattern += 1; {
self.current_row = 0; self.current_pattern += 1;
self.current_row = 0;
if self.current_pattern >= self.track.patterns_to_play.len() { if self.current_pattern >= self.track.patterns_to_play.len() {
self.current_pattern = self.track.repeat; self.current_pattern = self.track.repeat;
}
} }
} }
@ -394,6 +403,32 @@ impl<'track, TChannelId> TrackerInner<'track, TChannelId> {
false false
} }
} }
fn handle_jump(&mut self, jump: Jump) {
match jump {
Jump::Position { pattern } => {
self.current_pattern = pattern as usize;
self.current_row = 0;
}
Jump::PatternBreak { row } => {
self.current_pattern += 1;
self.current_row = row as usize;
}
Jump::Combined { pattern, row } => {
self.current_pattern = pattern as usize;
self.current_row = row as usize;
}
};
if self.current_pattern >= self.track.patterns_to_play.len() {
self.current_pattern = self.track.repeat;
}
if self.current_row
>= self.track.patterns[self.track.patterns_to_play[self.current_pattern]].length
{
// TODO: reconsider this default
self.current_row = 0;
}
}
} }
impl TrackerChannel { impl TrackerChannel {
@ -419,6 +454,7 @@ impl TrackerChannel {
tick: u32, tick: u32,
global_settings: &mut GlobalSettings, global_settings: &mut GlobalSettings,
envelope_state: &mut Option<EnvelopeState>, envelope_state: &mut Option<EnvelopeState>,
current_jump: &mut Option<Jump>,
) { ) {
match effect { match effect {
PatternEffect::None => {} PatternEffect::None => {}
@ -547,6 +583,9 @@ impl TrackerChannel {
self.vibrato.waveform = *waveform; self.vibrato.waveform = *waveform;
self.vibrato.enable = true; self.vibrato.enable = true;
} }
PatternEffect::Jump(jump) => {
*current_jump = Some(jump.clone());
}
} }
} }

View file

@ -1,7 +1,7 @@
use std::collections::HashMap; use std::collections::HashMap;
use agb_fixnum::Num; use agb_fixnum::Num;
use agb_tracker_interop::{PatternEffect, Waveform}; use agb_tracker_interop::{Jump, PatternEffect, Waveform};
use xmrs::prelude::*; use xmrs::prelude::*;
@ -101,6 +101,9 @@ pub fn parse_module(module: &Module) -> agb_tracker_interop::Track {
let mut note_and_sample = vec![None; module.get_num_channels()]; let mut note_and_sample = vec![None; module.get_num_channels()];
for row in pattern.iter() { for row in pattern.iter() {
// the combined jump for each row
let mut jump = None;
for (i, slot) in row.iter().enumerate() { for (i, slot) in row.iter().enumerate() {
let channel_number = i % module.get_num_channels(); let channel_number = i % module.get_num_channels();
@ -319,6 +322,18 @@ pub fn parse_module(module: &Module) -> agb_tracker_interop::Track {
) )
} }
} }
0xB => {
let pattern_idx = slot.effect_parameter;
jump = Some((
channel_number,
Jump::Position {
pattern: pattern_idx,
},
));
PatternEffect::None
}
0xC => { 0xC => {
if let Some((_, sample)) = maybe_note_and_sample { if let Some((_, sample)) = maybe_note_and_sample {
PatternEffect::Volume( PatternEffect::Volume(
@ -328,6 +343,29 @@ pub fn parse_module(module: &Module) -> agb_tracker_interop::Track {
PatternEffect::None PatternEffect::None
} }
} }
0xD => {
// NOTE: this field is generally interpreted as decimal.
let first = slot.effect_parameter >> 4;
let second = slot.effect_parameter & 0xF;
let row_idx = first * 10 + second;
let pattern_break = Jump::PatternBreak { row: row_idx };
// if to the *right* of 0xD effect, make combined
if let Some((idx, Jump::Position { pattern })) = jump {
jump = Some((
idx,
Jump::Combined {
pattern,
row: row_idx,
},
))
} else {
jump = Some((channel_number, pattern_break));
}
PatternEffect::None
}
0xE => match slot.effect_parameter >> 4 { 0xE => match slot.effect_parameter >> 4 {
0x1 => { 0x1 => {
let c4_speed: Num<u32, 12> = let c4_speed: Num<u32, 12> =
@ -433,6 +471,16 @@ pub fn parse_module(module: &Module) -> agb_tracker_interop::Track {
}); });
} }
} }
// At the last channel, evaluate the combined jump,
// and place at the first jump effect channel index
if let Some((jump_channel, jump)) = jump.take() {
let jump_effect = PatternEffect::Jump(jump);
let pattern_data_idx =
pattern_data.len() - module.get_num_channels() + jump_channel;
if let Some(data) = pattern_data.get_mut(pattern_data_idx) {
data.effect2 = jump_effect;
}
}
} }
patterns.push(agb_tracker_interop::Pattern { patterns.push(agb_tracker_interop::Pattern {