Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 35 additions & 7 deletions lwjgl3/src/main/java/emu/joric/lwjgl3/AY38912PSG.java
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,15 @@ public class AY38912PSG implements AYPSG {
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;

// Diagnostic logging in writeSample (drop events, top-up events, periodic
// stats) is gated on this flag. Disabled by default so the released build
// is silent. Flip to true when investigating audio drift / glitches on a
Expand Down Expand Up @@ -169,8 +178,11 @@ public void init(Via via, Keyboard keyboard, Snapshot snapshot) {
} catch (LineUnavailableException lue) {
audioLine = null;
}

cyclesToNextSample = CYCLES_PER_SAMPLE;

dcBlockerX1 = 0f;
dcBlockerY1 = 0f;
}

/**
Expand Down Expand Up @@ -495,12 +507,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, flush (write) it out to the audio line
// with drift management: drop the flush if the audio line buffer is
Expand Down
Loading