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
This commit is contained in:
+10
-6
@@ -97,33 +97,37 @@ export class GameBoyAPU {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize all 8 channels with their gain nodes.
|
* 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 {
|
private initializeChannels(): void {
|
||||||
// Create pulse channels
|
// Create pulse channels (4 × 0.12 = 0.48 max)
|
||||||
for (const config of CHANNEL_CONFIG.pulse) {
|
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);
|
const channel = new PulseChannel(this.audioContext, gain, config.hasSweep);
|
||||||
channel.setDutyCycle(config.defaultDuty);
|
channel.setDutyCycle(config.defaultDuty);
|
||||||
this.pulseChannels.set(config.id, channel);
|
this.pulseChannels.set(config.id, channel);
|
||||||
this.initChannelState(config.id);
|
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) {
|
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);
|
const channel = new WaveChannel(this.audioContext, gain, config.preset);
|
||||||
this.waveChannels.set(config.id, channel);
|
this.waveChannels.set(config.id, channel);
|
||||||
this.initChannelState(config.id);
|
this.initChannelState(config.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create noise channels
|
// Create noise channels (2 × 0.08 = 0.16 max)
|
||||||
for (const config of CHANNEL_CONFIG.noise) {
|
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);
|
const channel = new NoiseChannel(this.audioContext, gain, config.mode);
|
||||||
this.noiseChannels.set(config.id, channel);
|
this.noiseChannels.set(config.id, channel);
|
||||||
this.initChannelState(config.id);
|
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.
|
* Create a gain node for a channel and connect to master.
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ const DEFAULT_CONFIG: ColorizerConfig = {
|
|||||||
lowpassFreq: 10000, // GB natural rolloff (slightly higher)
|
lowpassFreq: 10000, // GB natural rolloff (slightly higher)
|
||||||
bitDepth: 8, // Less aggressive bit crushing (4 was too harsh)
|
bitDepth: 8, // Less aggressive bit crushing (4 was too harsh)
|
||||||
sampleRateReduction: 1, // No sample rate reduction (was causing artifacts)
|
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!
|
highpassFreq: 20, // LOW - allow bass through!
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -78,13 +78,13 @@ export class GameBoyColorizer {
|
|||||||
this.waveshaper.curve = this.createSaturationCurve(this.config.saturation);
|
this.waveshaper.curve = this.createSaturationCurve(this.config.saturation);
|
||||||
this.waveshaper.oversample = '2x';
|
this.waveshaper.oversample = '2x';
|
||||||
|
|
||||||
// Limiter to prevent clipping
|
// Limiter to prevent clipping - more aggressive settings
|
||||||
this.limiter = audioContext.createDynamicsCompressor();
|
this.limiter = audioContext.createDynamicsCompressor();
|
||||||
this.limiter.threshold.value = -6;
|
this.limiter.threshold.value = -12; // Catch peaks earlier
|
||||||
this.limiter.knee.value = 6;
|
this.limiter.knee.value = 3; // Harder knee
|
||||||
this.limiter.ratio.value = 12;
|
this.limiter.ratio.value = 20; // More aggressive limiting
|
||||||
this.limiter.attack.value = 0.001;
|
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)
|
// Initialize chain (without bit crusher for now)
|
||||||
this.initializeBasicChain();
|
this.initializeBasicChain();
|
||||||
@@ -227,13 +227,13 @@ export class GameBoyColorizer {
|
|||||||
static createPreset(preset: 'dmg' | 'gbc' | 'gba' | 'clean'): Partial<ColorizerConfig> {
|
static createPreset(preset: 'dmg' | 'gbc' | 'gba' | 'clean'): Partial<ColorizerConfig> {
|
||||||
switch (preset) {
|
switch (preset) {
|
||||||
case 'dmg':
|
case 'dmg':
|
||||||
// Original Game Boy - lo-fi but with bass
|
// Original Game Boy - warm but clean
|
||||||
return {
|
return {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
lowpassFreq: 8000,
|
lowpassFreq: 8000,
|
||||||
bitDepth: 8, // Less harsh than 4-bit
|
bitDepth: 8, // Less harsh than 4-bit
|
||||||
sampleRateReduction: 1, // No SR reduction (causes artifacts)
|
sampleRateReduction: 1, // No SR reduction (causes artifacts)
|
||||||
saturation: 0.3,
|
saturation: 0.1, // Minimal saturation to avoid clicks
|
||||||
highpassFreq: 30, // Let bass through!
|
highpassFreq: 30, // Let bass through!
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -244,7 +244,7 @@ export class GameBoyColorizer {
|
|||||||
lowpassFreq: 10000,
|
lowpassFreq: 10000,
|
||||||
bitDepth: 8,
|
bitDepth: 8,
|
||||||
sampleRateReduction: 1,
|
sampleRateReduction: 1,
|
||||||
saturation: 0.2,
|
saturation: 0.08, // Minimal saturation
|
||||||
highpassFreq: 25,
|
highpassFreq: 25,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user