Fix distortion with limiter and reduced gains
- Added DynamicsCompressorNode limiter at master output - Reduced channel gains (pulse: 0.08, wave: 0.12, noise: 0.05) - Disabled arranger by default (was adding overlapping notes) - Total max gain now 0.66 with headroom for overlap
This commit is contained in:
+22
-11
@@ -54,6 +54,7 @@ export class GameBoyAPU {
|
|||||||
|
|
||||||
// Master output chain
|
// Master output chain
|
||||||
private masterGain: GainNode;
|
private masterGain: GainNode;
|
||||||
|
private limiter: DynamicsCompressorNode;
|
||||||
private colorizer: GameBoyColorizer;
|
private colorizer: GameBoyColorizer;
|
||||||
|
|
||||||
// Individual channel instances
|
// Individual channel instances
|
||||||
@@ -87,8 +88,17 @@ export class GameBoyAPU {
|
|||||||
this.masterGain = this.audioContext.createGain();
|
this.masterGain = this.audioContext.createGain();
|
||||||
this.masterGain.gain.value = this.config.masterVolume;
|
this.masterGain.gain.value = this.config.masterVolume;
|
||||||
|
|
||||||
// SIMPLIFIED: master -> destination (bypass colorizer to avoid distortion)
|
// Create limiter to prevent clipping when many notes overlap
|
||||||
this.masterGain.connect(this.audioContext.destination);
|
this.limiter = this.audioContext.createDynamicsCompressor();
|
||||||
|
this.limiter.threshold.value = -6; // Start compressing at -6dB
|
||||||
|
this.limiter.knee.value = 3; // Soft knee
|
||||||
|
this.limiter.ratio.value = 20; // Heavy compression (limiter)
|
||||||
|
this.limiter.attack.value = 0.001; // Fast attack
|
||||||
|
this.limiter.release.value = 0.1; // Medium release
|
||||||
|
|
||||||
|
// Signal chain: master -> limiter -> destination
|
||||||
|
this.masterGain.connect(this.limiter);
|
||||||
|
this.limiter.connect(this.audioContext.destination);
|
||||||
|
|
||||||
// Initialize all channels
|
// Initialize all channels
|
||||||
this.initializeChannels();
|
this.initializeChannels();
|
||||||
@@ -97,36 +107,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
|
* Gain staging: Keep total well under 1.0 to leave headroom
|
||||||
* With 8 channels potentially active: each should be ~0.1-0.15
|
* Notes stack when overlapping, so we use conservative values
|
||||||
|
* The limiter will catch peaks, but we want to minimize its work
|
||||||
*/
|
*/
|
||||||
private initializeChannels(): void {
|
private initializeChannels(): void {
|
||||||
// Create pulse channels (4 × 0.12 = 0.48 max)
|
// Create pulse channels (4 × 0.08 = 0.32 max)
|
||||||
for (const config of CHANNEL_CONFIG.pulse) {
|
for (const config of CHANNEL_CONFIG.pulse) {
|
||||||
const gain = this.createChannelGain(config.id, 0.12);
|
const gain = this.createChannelGain(config.id, 0.08);
|
||||||
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 - slightly higher for bass presence (2 × 0.18 = 0.36 max)
|
// Create wave channels - slightly higher for bass presence (2 × 0.12 = 0.24 max)
|
||||||
for (const config of CHANNEL_CONFIG.wave) {
|
for (const config of CHANNEL_CONFIG.wave) {
|
||||||
const gain = this.createChannelGain(config.id, 0.18);
|
const gain = this.createChannelGain(config.id, 0.12);
|
||||||
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 (2 × 0.08 = 0.16 max)
|
// Create noise channels (2 × 0.05 = 0.10 max)
|
||||||
for (const config of CHANNEL_CONFIG.noise) {
|
for (const config of CHANNEL_CONFIG.noise) {
|
||||||
const gain = this.createChannelGain(config.id, 0.08);
|
const gain = this.createChannelGain(config.id, 0.05);
|
||||||
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
|
// Total max: 0.32 + 0.24 + 0.10 = 0.66 (headroom for note overlap)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a gain node for a channel and connect to master.
|
* Create a gain node for a channel and connect to master.
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ const DEFAULT_PLAYER_CONFIG: GameBoyPlayerConfig = {
|
|||||||
autoResume: true,
|
autoResume: true,
|
||||||
defaultBPM: 120,
|
defaultBPM: 120,
|
||||||
masterVolume: 0.7,
|
masterVolume: 0.7,
|
||||||
enableArranger: true, // ON by default for fuller sound
|
enableArranger: false, // OFF by default - too many overlapping notes causes clipping
|
||||||
arrangerConfig: {},
|
arrangerConfig: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user