Skip to content

Commit

Permalink
Fix random crackles in N163 playback and WAV export
Browse files Browse the repository at this point in the history
### Background

N163 playback, like other chips, uses `Blip_Synth` and `Blip_Buffer` for
resampling. Each sound chip has its own Blip_Synth, with an independent
volume control and treble filter (a set of impulse responses).

Chip emulators create audio by calling `Blip_Synth::update
(time_clocks, amplitude)`. Blip_Synth calculates `delta = amplitude -
previous amplitude`, then inserts an impulse of size `delta` into a
`Blip_Buffer`. `Blip_Synth` performs resampling by converting
`time_clocks` into fractional samples, then adding a fractionally
delayed impulse to Blip_Buffer's difference array starting at `int
(sample timestamp)`. (This introduces a few samples of delay, from the
impulse start to center point.)

Each frame lasts an integer number of clocks, but a fractional number of
samples. At the end of a frame, `Blip_Buffer::read_samples()` finds the
sample where the next frame begins, generates audio up to that sample
using a running sum of Blip_Buffer's difference array (with optional
highpass DC removal), then drops the returned samples (keeping the
fractional part of completed samples, so the next frame's impulses
begin midway through the new first sample of the buffer).

### Bug

Dn-FT outputs audio from a global Blip_Buffer, `CMixer::BlipBuffer`. In
order to apply (approximately emulated) audio filtering, `CN163` has
its own Blip_Buffer, `CN163::m_BlipN163`.

When we begin playback, we call `CMixer::ClearBuffer()` and
`CN163::Reset()`, which both reset `Blip_Buffer::offset_` to 0. Due to
how FamiTracker is designed, this can happen at random points
mid-frame (rather than on boundaries).

The bug in this case was that when FamiTracker called `CN163::Reset
()` without ending the previous frame normally, `CN163` failed to clear
its "time since frame begin" field (`m_iTime`). Normally this would
result in steps being delayed slightly for the next frame (or at
extremely low tick rates, writing out of bounds of Blip_Buffer's
difference array). But CN163 has its own Blip_Buffer, so each
subsequent frame starts and stops at a different fractional sample than
the global Blip_Buffer, resulting in different numbers of completed
samples being generated each frame.

These frames of incorrect length get mixed into the global Blip_Buffer
when `CN163::EndFrame()` calls `Output.mix_samples_raw()`
(where `Output` points to `CMixer::BlipBuffer`). Whenever a N163 frame
doesn't match the length of its corresponding global frame, it can
produce 1-sample overlaps or gaps at frame borders, resulting in audio
crackling.

### Fix

Make `CN163::Reset()` set `m_iTime = 0`, so the first N163 frame adds
samples at the correct time, has the correct length, and all subsequent
frames have the correct phase relative to the global Blip_Buffer.

Does CN163 need to have a separate Blip_Buffer? It's complicated. It's
used to perform a first-order lowpass filter at 12 kHz, but this is
close enough to Nyquist (at the usual sampling rate of 44-48 kHz) to
have noticeable frequency warping. I don't know if this is
hardware-accurate or an attempt to filter out switching noise.
  • Loading branch information
nyanpasu64 committed Sep 16, 2024
1 parent d838080 commit 1caa313
Showing 1 changed file with 1 addition and 0 deletions.
1 change: 1 addition & 0 deletions Source/APU/N163.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ void CN163::Reset()
m_N163.Reset();
m_N163.SetMixing(m_bUseLinearMixing);

m_iTime = 0;
m_SynthN163.clear();
m_BlipN163.clear();
}
Expand Down

0 comments on commit 1caa313

Please sign in to comment.