feat: Hermite resampling, sprite shift registers, controller open bus
Some checks failed
CI / rust (push) Has been cancelled
Some checks failed
CI / rust (push) Has been cancelled
#3 audio.rs: replace linear interpolation with Catmull-Rom Hermite cubic. Stores prev_sample as p0 control point; m1=(p2-p0)/2, m2=(p2-p1)/2 tangents give continuous first derivative across batch boundaries. #4 ppu: add per-slot sprite shift registers (spr_shift_lo/hi, spr_x_counter, spr_attr_latch). load_sprite_shifters fetches pattern bytes with h-flip at dot 1 of each visible scanline. sprite_pixel_from_shifters replaces the per-pixel OAM scan; sprite-0 hit detection integrated into the shifter path. #5 joypad.rs: format_controller_read now preserves bits 1-5,7 as open bus (!0x41 mask) instead of zeroing bits 1-4, matching NES hardware behaviour.
This commit is contained in:
@@ -7,6 +7,13 @@ pub struct AudioMixer {
|
||||
samples_per_cpu_cycle: f64,
|
||||
sample_accumulator: f64,
|
||||
last_output_sample: f32,
|
||||
// Previous output sample (two batches ago) used as the p0 control point
|
||||
// for Catmull-Rom Hermite interpolation. Storing p0 allows the tangent at
|
||||
// the start of each interpolation interval to be computed as
|
||||
// m1 = (p2 - p0) / 2
|
||||
// which produces a smooth, continuous first derivative across batch
|
||||
// boundaries rather than the kink introduced by linear ramps.
|
||||
prev_sample: f32,
|
||||
// One-pole IIR low-pass filter state (approximates NES ~14 kHz RC filter).
|
||||
// Coefficient: a = exp(-2π * fc / fs). At fc=14000, fs=48000: a ≈ 0.160
|
||||
lp_coeff: f32,
|
||||
@@ -31,6 +38,7 @@ impl AudioMixer {
|
||||
samples_per_cpu_cycle: sample_rate as f64 / cpu_hz,
|
||||
sample_accumulator: 0.0,
|
||||
last_output_sample: 0.0,
|
||||
prev_sample: 0.0,
|
||||
lp_coeff,
|
||||
lp_state: 0.0,
|
||||
hp_coeff,
|
||||
@@ -46,6 +54,7 @@ impl AudioMixer {
|
||||
pub fn reset(&mut self) {
|
||||
self.sample_accumulator = 0.0;
|
||||
self.last_output_sample = 0.0;
|
||||
self.prev_sample = 0.0;
|
||||
self.lp_state = 0.0;
|
||||
self.hp_prev_x = 0.0;
|
||||
self.hp_prev_y = 0.0;
|
||||
@@ -82,29 +91,49 @@ impl AudioMixer {
|
||||
return;
|
||||
}
|
||||
|
||||
let start = self.last_output_sample;
|
||||
// Catmull-Rom Hermite interpolation between the previous batch sample
|
||||
// (p1 = last_output_sample) and the current batch sample (p2 = sample).
|
||||
//
|
||||
// The tangent at p1 uses the two-point central difference:
|
||||
// m1 = (p2 - p0) / 2, where p0 = prev_sample (two batches ago).
|
||||
// The tangent at p2 uses the forward difference (p3 approximated as p2,
|
||||
// i.e. the signal stays flat beyond the current batch):
|
||||
// m2 = (p2 - p1) / 2.
|
||||
//
|
||||
// Hermite basis:
|
||||
// h00(t) = 2t³ - 3t² + 1
|
||||
// h10(t) = t³ - 2t² + t
|
||||
// h01(t) = -2t³ + 3t²
|
||||
// h11(t) = t³ - t²
|
||||
// f(t) = h00·p1 + h10·m1 + h01·p2 + h11·m2
|
||||
//
|
||||
// For t = 1 this collapses to p2, so the last output of each batch
|
||||
// always lands exactly on the current APU sample value.
|
||||
let p0 = self.prev_sample;
|
||||
let p1 = self.last_output_sample;
|
||||
let p2 = sample;
|
||||
let m1 = (p2 - p0) * 0.5;
|
||||
let m2 = (p2 - p1) * 0.5;
|
||||
|
||||
let denom = samples as f32;
|
||||
let a = self.lp_coeff;
|
||||
let b = 1.0 - a;
|
||||
if samples == 1 {
|
||||
let lp = a * self.lp_state + b * sample;
|
||||
for idx in 0..samples {
|
||||
let t = (idx + 1) as f32 / denom;
|
||||
let t2 = t * t;
|
||||
let t3 = t2 * t;
|
||||
let interp = (2.0 * t3 - 3.0 * t2 + 1.0) * p1
|
||||
+ (t3 - 2.0 * t2 + t) * m1
|
||||
+ (-2.0 * t3 + 3.0 * t2) * p2
|
||||
+ (t3 - t2) * m2;
|
||||
let lp = a * self.lp_state + b * interp;
|
||||
self.lp_state = lp;
|
||||
let hp = self.hp_coeff * self.hp_prev_y + lp - self.hp_prev_x;
|
||||
self.hp_prev_x = lp;
|
||||
self.hp_prev_y = hp;
|
||||
out.push(hp);
|
||||
} else {
|
||||
let denom = samples as f32;
|
||||
for idx in 0..samples {
|
||||
let t = (idx + 1) as f32 / denom;
|
||||
let interp = start + (sample - start) * t;
|
||||
let lp = a * self.lp_state + b * interp;
|
||||
self.lp_state = lp;
|
||||
let hp = self.hp_coeff * self.hp_prev_y + lp - self.hp_prev_x;
|
||||
self.hp_prev_x = lp;
|
||||
self.hp_prev_y = hp;
|
||||
out.push(hp);
|
||||
}
|
||||
}
|
||||
self.prev_sample = p1;
|
||||
self.last_output_sample = sample;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user