Skip to content

Fix DC-offset audio glitches in browser hosted emulator#6

Merged
lanceewing merged 1 commit into
lanceewing:masterfrom
lxpollitt:fix/gwt-psg-dc-offset
May 17, 2026
Merged

Fix DC-offset audio glitches in browser hosted emulator#6
lanceewing merged 1 commit into
lanceewing:masterfrom
lxpollitt:fix/gwt-psg-dc-offset

Conversation

@lxpollitt

Copy link
Copy Markdown
Contributor

Fix for audio glitches in the browser-hosted emulator

1. DC offset on the AudioWorklet output

The emulated AY-3-8912 produces a unipolar signal: each of the three channels contributes a non-negative value, with a DC offset that varies over time based on overall volume. So silence is 0, and a full volume signal will average over time around the 16384 mid-point. To convert to the -1 to +1 range the AudioWorkletProcessor the original conversion subtracted a fixed value 16384.0f to centre the signal to 0, which would correct the DC offset when the a signal is being played at max volume but meant that silence was effectively being converted to -1. This leads to loud clicks when the audio signal is switched on/off (e.g. emulator paused or restarted, or sound on/off icon clicked on) and could possibly in some cases lead to excess speaker coil load depending on the physical output chain.

This proposed fix replaces the static subtraction with a one-pole DC blocker (y[n] = x[n] - x[n-1] + R·y[n-1], R = 0.995). At the 22050 Hz sample rate used this gives a -3 dB corner around 17.5 Hz which should be below any audible content the Oric would normally produce, but high enough to effectively remove the DC offset drift this bug is about.

2. Wrap-on-overflow in the channel sum

I'm not totally sure if this condition would ever be hit (I don't know the emulator well enough), but the three output channels were summed and then masked with & 0x7FFF. If the sum ever exceeded 15 bits the mask would effectively wrap the value, producing a sharp discontinuity in the sample stream and an audible click. Replaced with Math.min(..., 0x7FFF) so loud sums clamp at the rail instead of wrapping.

Implementation notes

  • The output clamp to ±1.0 is folded into the same expression as the DC blocker, so a rail-hit (e.g. silence-to-full-output in one sample) feeds the clamped value back into the filter state. This is non-linear, but bounds the filter state, and I think probably more closely matches the saturating behaviour of a real AC coupling cap, and recovers from clipping faster than feeding the raw overshoot back into the filter would.
  • Filter state is reset alongside the existing reset path.
  • No changes outside GwtAYPSG; desktop / Android paths are unaffected.

Testing

Tested locally in Safari and Chrome and the previously very audible clicks/thumps on transitions are gone. No audible regressions on content that I could hear in the programs I tested with, but it would be great to get a quick double check by whoever reviews this PR with programs they are familiar with.

@lanceewing

Copy link
Copy Markdown
Owner

Thanks for finding this one and proposing a fix. I will try running it hopefully later today and will merge if I confirm it also works for me. A very thorough description of the issue. I really appreciate it.

@lanceewing

Copy link
Copy Markdown
Owner

I have just tried this out and I'm happy that it works and is a big improvement. I can't hear those clicks anymore. I will merge it in a few minutes. It should be live maybe 10 minutes after that.

@lanceewing lanceewing merged commit 14ae102 into lanceewing:master May 17, 2026
@lxpollitt

Copy link
Copy Markdown
Contributor Author

Great, thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants