From 0d79fc7a049a11498c6f0bd4c95b977d8c3401f6 Mon Sep 17 00:00:00 2001 From: Nemo55aa Date: Sun, 2 Aug 2026 12:52:09 +0900 Subject: [PATCH 1/4] : Blip Buffer error feedback for N163 telescoping --- Source/Blip_Buffer/Blip_Buffer.h | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/Source/Blip_Buffer/Blip_Buffer.h b/Source/Blip_Buffer/Blip_Buffer.h index 0e3c89e5..17223889 100644 --- a/Source/Blip_Buffer/Blip_Buffer.h +++ b/Source/Blip_Buffer/Blip_Buffer.h @@ -172,6 +172,10 @@ class Blip_Buffer { public: int last_amp = 0; double delta_factor; + // Rounding error carried over from the previous delta. Without it, quantizing + // each delta independently breaks the telescoping property (sum of deltas == + // amplitude) and lets a DC offset accumulate without bound. + mutable double delta_residual = 0.0; void volume_unit( double ); Blip_Synth_Fast_(); @@ -182,6 +186,8 @@ class Blip_Buffer { public: int last_amp = 0; double delta_factor; + // See Blip_Synth_Fast_::delta_residual. + mutable double delta_residual = 0.0; void volume_unit( double ); Blip_Synth_( short* impulses, int width ); @@ -213,7 +219,7 @@ class Blip_Synth { // Configure low-pass filter (see blip_buffer.txt) void treble_eq( blip_eq_t const& eq ) { impl.treble_eq( eq ); } - void clear() { impl.last_amp = 0; } + void clear() { impl.last_amp = 0; impl.delta_residual = 0.0; } /// Set the last-seen amplitude to `dc_amp` without outputting a step. /// If Blip_Buffer currently has output level 0, @@ -372,7 +378,19 @@ inline void Blip_Synth::offset_resampled( blip_resampled_time_t time, // Fails if time is beyond end of Blip_Buffer, due to a bug in caller code or the // need for a longer buffer as set by set_sample_rate(). assert( (blip_long) (time >> BLIP_BUFFER_ACCURACY) < blip_buf->buffer_size_ ); + +#if 0 // Nemo55aa 260802 delta = (int)((double)delta * impl.delta_factor); +#else + // Quantize with error feedback. Truncating each delta toward zero on its own + // biases the error against the sign of the delta, which on an asymmetric waveform + // integrates into an unbounded DC offset. Carrying the residual keeps the total + // error below one LSB forever. + double const scaled = (double)delta * impl.delta_factor + impl.delta_residual; + delta = (int) (scaled < 0 ? scaled - 0.5 : scaled + 0.5); + impl.delta_residual = scaled - (double)delta; +#endif // end Nemo55aa 260802 + blip_long* BLIP_RESTRICT buf = blip_buf->buffer_ + (time >> BLIP_BUFFER_ACCURACY); int phase = (int) (time >> (BLIP_BUFFER_ACCURACY - BLIP_PHASE_BITS) & (blip_res - 1)); From 5103050d4303ccea3ef93f39aa47bc4ba1764cf0 Mon Sep 17 00:00:00 2001 From: Nemo55aa Date: Sun, 2 Aug 2026 12:56:03 +0900 Subject: [PATCH 2/4] : set N163 DC filtering --- Source/APU/N163.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Source/APU/N163.cpp b/Source/APU/N163.cpp index 49972388..4ebcab7d 100644 --- a/Source/APU/N163.cpp +++ b/Source/APU/N163.cpp @@ -55,7 +55,11 @@ void CN163::UpdateFilter(blip_eq_t eq) { m_BlipN163.set_sample_rate(eq.sample_rate); m_SynthN163.treble_eq(eq); - m_BlipN163.bass_freq(0); + // Not 0: this buffer is read back into the master buffer as absolute samples, + // so any DC that leaks in here would accumulate forever and eventually saturate + // the 16-bit clamp in read_samples(), silencing the chip. 1 Hz is inaudible but + // bounds the offset. + m_BlipN163.bass_freq(1); m_CutoffHz = 12000; RecomputeN163Filter(); } From 65ca320c3284e5489fdd1dadf4401a17942423bc Mon Sep 17 00:00:00 2001 From: Nemo55aa Date: Sun, 2 Aug 2026 14:59:59 +0900 Subject: [PATCH 3/4] : chagne log update (fix N163 DC drift) --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 65756777..12d738d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ Last updated: 2026-06-03 --- -## Unreleased - 2026-06-03 +## Unreleased - 2026-08-02 ### Breaking changes @@ -30,6 +30,7 @@ Last updated: 2026-06-03 - MMC5 also now has proper nonlinear mixing. - Port VRC6 to new CSoundChip and NSFPlay emulation core (@Gumball2415 @eulyderg #325 #417) - VRC6 sawtooth volume meter reads the register value rather than the actual output, the only practical difference being that the meter now displays volume level 1 correctly. +- N163 DC drifting fixed (@Nemo55aa #423) ### Bug fixes From 8c55c00d2f3ed25ea08592f6e5f276c683551d51 Mon Sep 17 00:00:00 2001 From: Nemo55aa Date: Sun, 2 Aug 2026 20:48:43 +0900 Subject: [PATCH 4/4] Revert ": set N163 DC filtering" This reverts commit 5103050d4303ccea3ef93f39aa47bc4ba1764cf0. --- Source/APU/N163.cpp | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/Source/APU/N163.cpp b/Source/APU/N163.cpp index 4ebcab7d..49972388 100644 --- a/Source/APU/N163.cpp +++ b/Source/APU/N163.cpp @@ -55,11 +55,7 @@ void CN163::UpdateFilter(blip_eq_t eq) { m_BlipN163.set_sample_rate(eq.sample_rate); m_SynthN163.treble_eq(eq); - // Not 0: this buffer is read back into the master buffer as absolute samples, - // so any DC that leaks in here would accumulate forever and eventually saturate - // the 16-bit clamp in read_samples(), silencing the chip. 1 Hz is inaudible but - // bounds the offset. - m_BlipN163.bass_freq(1); + m_BlipN163.bass_freq(0); m_CutoffHz = 12000; RecomputeN163Filter(); }