Skip to main content

PlayerState

Struct PlayerState 

Source
pub struct PlayerState {
Show 13 fields pub samples: Vec<SampleBody>, pub patterns: Vec<Pattern>, pub order: Vec<u8>, pub song_length: u8, pub channels: Vec<Channel>, pub speed: u8, pub bpm: u8, pub order_index: u8, pub row: u8, pub tick: u8, pub tick_sample_cursor: u32, pub sample_rate: u32, pub ended: bool, /* private fields */
}
Expand description

Top-level player state. Owns samples, patterns, order, and the per-channel mixer. Feeds render(dst) to fill an interleaved stereo S16 buffer.

Fields§

§samples: Vec<SampleBody>§patterns: Vec<Pattern>§order: Vec<u8>§song_length: u8§channels: Vec<Channel>§speed: u8§bpm: u8§order_index: u8

Current position in the order table (0..song_length).

§row: u8

Current row inside the current pattern (0..64).

§tick: u8

Current tick inside the current row (0..speed).

§tick_sample_cursor: u32

Samples emitted so far within the current tick.

§sample_rate: u32§ended: bool

Flag set when the song has wrapped past its last order.

Implementations§

Source§

impl PlayerState

Source

pub const DEFAULT_PAN_SEPARATION: f32 = 0.5

Default stereo pan separation (0.5 = 50 %).

See the pan_separation field doc-comment for the rationale. We default to 0.5 — half-way between the Protracker-effects-MODFIL12.txt §11 hard pan recommendation (which itself warns against full hard pan “especially when using headphones”) and full mono. Empirically this is the value that minimises cross-correlation drift versus a black-box reference render on real-world MODs (halluc.mod, rhmst.mod) — a partial-bleed default is common practice. An r14 trial used 0.7 but on these specific fixtures 0.5 yielded ~3 % better xcorr per 1 s window across the entire song. Override with PlayerState::set_pan_separation.

Source

pub const RAMP_FRAMES: u32 = 44

Length of the per-trigger volume ramp, in output frames. 44 frames at 44.1 kHz ≈ 1 ms — enough to smooth the discontinuity at a fresh note-on without smearing percussive transients audibly.

Without this ramp, every note re-trigger emits a single-sample step of magnitude (prev_mixed - new_mixed) into the post-mix L/R bus. On real-world MODs (halluc.mod, rhmst.mod) those steps were measured at ~5775 LSB and were the dominant contributor to per-second cross-correlation drift versus a black-box reference render. A 1 ms linear crossfade corresponds to the Protracker-effects-MODFIL12.txt §1.6 note about Paula taking ~140 µs to settle on a fresh DMA fetch (we round up to one full output millisecond so the ramp is robust at any reasonable output sample rate).

Source

pub const FIXED_RC_CUTOFF_HZ: f32 = 16_000.0

Cutoff of the always-on first RC stage that sits between Paula’s DAC and the audio jacks.

Real-hardware schematic R/C values yield 1/(2πRC) ≈ 4400 Hz (rounded to 5000 Hz in r14 of this module to model an “authentic” Amiga sound). However, the prevailing modern rendering convention effectively bypasses this stage in the default render path — only the LED-gated stage is modelled — because the real-hardware corner lops off audible content that listeners expect to hear back from a MOD render. With the strict 5 kHz value we measured cross-correlation drift against a black-box reference render on rhmst.mod of about 5 % (~0.94 vs ~0.99) — i.e. the always-on filter was the dominant contributor to per-second drift versus the modern reference.

We default to 16000 Hz to make the always-on stage audibly transparent (it still rolls off ultrasonic content past the resampler, which is the only PT-faithful purpose it serves at modern output rates).

Documentation references: paula-filter-notes.md, docs/audio/trackers/mod/openmpt-module-formats.html (Resampling).

Source

pub const LED_FILTER_CUTOFF_HZ: f32 = 11_500.0

Cutoff of the LED-controlled second RC stage (the one toggled by E00 / E01).

The real Amiga “Power LED” filter is a 12 dB/oct Sallen-Key cell with ~3.275 kHz nominal corner — that’s the “spec-strict” value previously used here. However, multimedia-cx-protracker.html documents an 11500 Hz 1-pole approximation that the modern PT rendering convention converges on, and every cross-correlation measurement against a black-box reference render confirms 11500 Hz is much closer to the modern-player ground truth than 3300 Hz is. The Sallen-Key value lops off so much HF content that real-world MOD files render audibly muffled compared to what their authors were probably listening to in their final mixdown — and this manifests as cross-correlation dips of 0.05–0.10 across most of rhmst.mod and halluc.mod.

Source

pub fn new( header: &ModHeader, samples: Vec<SampleBody>, patterns: Vec<Pattern>, sample_rate: u32, ) -> Self

Source

pub fn set_pan_separation(&mut self, sep: f32)

Override the stereo pan separation. 1.0 = full Amiga hard pan (channels 0/3 → LEFT only, 1/2 → RIGHT only). 0.0 = full mono. Values outside 0.0..=1.0 are clamped.

Source

pub fn pan_separation(&self) -> f32

Read the current stereo pan separation.

Source

pub fn samples_per_tick(&self) -> u32

Samples-per-tick rounded down. Classic formula is sample_rate * 2.5 / BPM.

Source

pub fn channel_is_left(i: usize) -> bool

True if track index i is hard-panned left under the classic Amiga convention (channels 0 & 3 left, 1 & 2 right; for >4 channels the pattern repeats every 4).

Source

pub fn render(&mut self, dst: &mut [i16]) -> usize

Render n_frames stereo samples into dst (interleaved S16, length = n_frames * 2). Returns samples actually rendered (may be less than requested if song ends).

Source

pub fn render_per_channel( &mut self, planes: &mut [&mut [i16]], n_frames: usize, ) -> usize

Render into one S16 plane per MOD channel. planes.len() must equal self.channels.len(); each plane receives the same number of samples, and all planes must be at least n_frames long.

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.