From dd4e1cfebd4fa0b6d2e977ca1d305e6fa244726b Mon Sep 17 00:00:00 2001 From: b1rdmania <102524336+b1rdmania@users.noreply.github.com> Date: Tue, 20 Jan 2026 19:55:03 +0000 Subject: [PATCH] Fix clicky distortion - proper gain staging and reduced saturation - Reduced channel gains: pulse 0.12, wave 0.18, noise 0.08 - Total max gain now ~1.0 instead of 2.7 (was clipping) - More aggressive limiter: -12dB threshold, ratio 20 - Reduced saturation from 0.2-0.3 to 0.08-0.1 in all presets - Should eliminate clicky distortion during playback --- src-v2/audio/apu/APU.ts | 16 ++++++++++------ src-v2/audio/effects/GameBoyColorizer.ts | 18 +++++++++--------- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src-v2/audio/apu/APU.ts b/src-v2/audio/apu/APU.ts index 89980ac..6878055 100644 --- a/src-v2/audio/apu/APU.ts +++ b/src-v2/audio/apu/APU.ts @@ -97,33 +97,37 @@ export class GameBoyAPU { /** * Initialize all 8 channels with their gain nodes. + * + * Gain staging: Keep total under 1.0 to avoid clipping + * With 8 channels potentially active: each should be ~0.1-0.15 */ private initializeChannels(): void { - // Create pulse channels + // Create pulse channels (4 × 0.12 = 0.48 max) for (const config of CHANNEL_CONFIG.pulse) { - const gain = this.createChannelGain(config.id, 0.25); + const gain = this.createChannelGain(config.id, 0.12); const channel = new PulseChannel(this.audioContext, gain, config.hasSweep); channel.setDutyCycle(config.defaultDuty); this.pulseChannels.set(config.id, channel); this.initChannelState(config.id); } - // Create wave channels (higher gain for bass) + // Create wave channels - slightly higher for bass presence (2 × 0.18 = 0.36 max) for (const config of CHANNEL_CONFIG.wave) { - const gain = this.createChannelGain(config.id, 0.55); // Boosted for bass + const gain = this.createChannelGain(config.id, 0.18); const channel = new WaveChannel(this.audioContext, gain, config.preset); this.waveChannels.set(config.id, channel); this.initChannelState(config.id); } - // Create noise channels + // Create noise channels (2 × 0.08 = 0.16 max) for (const config of CHANNEL_CONFIG.noise) { - const gain = this.createChannelGain(config.id, 0.3); + const gain = this.createChannelGain(config.id, 0.08); const channel = new NoiseChannel(this.audioContext, gain, config.mode); this.noiseChannels.set(config.id, channel); this.initChannelState(config.id); } } + // Total max: 0.48 + 0.36 + 0.16 = 1.0 /** * Create a gain node for a channel and connect to master. diff --git a/src-v2/audio/effects/GameBoyColorizer.ts b/src-v2/audio/effects/GameBoyColorizer.ts index 506a903..0db802f 100644 --- a/src-v2/audio/effects/GameBoyColorizer.ts +++ b/src-v2/audio/effects/GameBoyColorizer.ts @@ -34,7 +34,7 @@ const DEFAULT_CONFIG: ColorizerConfig = { lowpassFreq: 10000, // GB natural rolloff (slightly higher) bitDepth: 8, // Less aggressive bit crushing (4 was too harsh) sampleRateReduction: 1, // No sample rate reduction (was causing artifacts) - saturation: 0.2, // Subtle warmth + saturation: 0.08, // Very subtle - avoid clipping artifacts highpassFreq: 20, // LOW - allow bass through! }; @@ -78,13 +78,13 @@ export class GameBoyColorizer { this.waveshaper.curve = this.createSaturationCurve(this.config.saturation); this.waveshaper.oversample = '2x'; - // Limiter to prevent clipping + // Limiter to prevent clipping - more aggressive settings this.limiter = audioContext.createDynamicsCompressor(); - this.limiter.threshold.value = -6; - this.limiter.knee.value = 6; - this.limiter.ratio.value = 12; + this.limiter.threshold.value = -12; // Catch peaks earlier + this.limiter.knee.value = 3; // Harder knee + this.limiter.ratio.value = 20; // More aggressive limiting this.limiter.attack.value = 0.001; - this.limiter.release.value = 0.1; + this.limiter.release.value = 0.05; // Faster release // Initialize chain (without bit crusher for now) this.initializeBasicChain(); @@ -227,13 +227,13 @@ export class GameBoyColorizer { static createPreset(preset: 'dmg' | 'gbc' | 'gba' | 'clean'): Partial { switch (preset) { case 'dmg': - // Original Game Boy - lo-fi but with bass + // Original Game Boy - warm but clean return { enabled: true, lowpassFreq: 8000, bitDepth: 8, // Less harsh than 4-bit sampleRateReduction: 1, // No SR reduction (causes artifacts) - saturation: 0.3, + saturation: 0.1, // Minimal saturation to avoid clicks highpassFreq: 30, // Let bass through! }; @@ -244,7 +244,7 @@ export class GameBoyColorizer { lowpassFreq: 10000, bitDepth: 8, sampleRateReduction: 1, - saturation: 0.2, + saturation: 0.08, // Minimal saturation highpassFreq: 25, };