From daa80b5a180e0d26e425959e43100eaf7bb1ed44 Mon Sep 17 00:00:00 2001 From: lxpollitt <630494+lxpollitt@users.noreply.github.com> Date: Mon, 8 Jun 2026 16:57:03 +0100 Subject: [PATCH] Add DC offset correction to desktop audio --- .../java/emu/joric/lwjgl3/AY38912PSG.java | 42 +++++++++++++++---- 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/lwjgl3/src/main/java/emu/joric/lwjgl3/AY38912PSG.java b/lwjgl3/src/main/java/emu/joric/lwjgl3/AY38912PSG.java index 6b03d6f..1d18970 100644 --- a/lwjgl3/src/main/java/emu/joric/lwjgl3/AY38912PSG.java +++ b/lwjgl3/src/main/java/emu/joric/lwjgl3/AY38912PSG.java @@ -81,6 +81,15 @@ public class AY38912PSG implements AYPSG { private int sampleBufferOffset = 0; private float cyclesToNextSample; private SourceDataLine audioLine; + + // One-pole DC-blocker state and coefficient used to model the AC coupling capacitor + // on Oric audio output to remove the DC offset that the audio chip's emulated unipolar + // signal would otherwise carry into the audio line output. + // R = 0.995 gives a -3 dB corner at ~17.5 Hz @ 22050 Hz sample rate, which should be + // below any expected normally audible Oric content. + private static final float DC_BLOCKER_R = 0.995f; + private float dcBlockerX1; + private float dcBlockerY1; /** * The AY-3-8912 in the Oric gets its data from the 6522 VIA chip. @@ -147,8 +156,11 @@ public void init(Via via, Keyboard keyboard, Snapshot snapshot) { } catch (LineUnavailableException lue) { audioLine = null; } - + cyclesToNextSample = CYCLES_PER_SAMPLE; + + dcBlockerX1 = 0f; + dcBlockerY1 = 0f; } /** @@ -473,12 +485,28 @@ public void writeSample() { } } - int sample = (((((VOLUME_LEVELS[volumeA] * cnt[A]) >> 13) + - ((VOLUME_LEVELS[volumeB] * cnt[B]) >> 13) + - ((VOLUME_LEVELS[volumeC] * cnt[C]) >> 13)) & 0x7FFF)); - - sampleBuffer[sampleBufferOffset + 0] = (byte)(sample & 0x00FF); - sampleBuffer[sampleBufferOffset + 1] = (byte)((sample & 0xFF00) >> 8); + int sample = Math.min(((VOLUME_LEVELS[volumeA] * cnt[A]) >> 13) + + ((VOLUME_LEVELS[volumeB] * cnt[B]) >> 13) + + ((VOLUME_LEVELS[volumeC] * cnt[C]) >> 13), 0x7FFF); + + // Use a simple DC blocker to convert to -1.0 to 1.0, then scale to signed + // 16-bit PCM, which is what the audio line needs. The output clamp is folded + // into the same expression as the filter, so on the rare transient that hits + // the rail (e.g. a register write flipping the chip from full silence to full + // output in one sample), the clamped value is what gets fed back into the + // filter state. (Clamping the filter state is arguably less mathematically + // accurate, because the clamping behaviour is non-linear. But this approach + // brings us out of the clipping state and into more normal behaviour faster, + // and is likely a closer approximation of the original hardware circuit + // behaviour.) + float x = sample / 16384.0f; + float y = Math.max(-1f, Math.min(1f, + x - dcBlockerX1 + DC_BLOCKER_R * dcBlockerY1)); + dcBlockerX1 = x; + dcBlockerY1 = y; + int int16Sample = (int)(y * 32767f); + sampleBuffer[sampleBufferOffset + 0] = (byte)(int16Sample & 0x00FF); + sampleBuffer[sampleBufferOffset + 1] = (byte)((int16Sample & 0xFF00) >> 8); // If the sample buffer is full, write it out to the audio line. if ((sampleBufferOffset += 2) == sampleBuffer.length) {