From 9311e825382260bffb29e2c98cf4c48ed0c37373 Mon Sep 17 00:00:00 2001 From: b1rdmania <102524336+b1rdmania@users.noreply.github.com> Date: Tue, 20 Jan 2026 19:50:55 +0000 Subject: [PATCH] Fix bass channel - warmer, less synthetic sound - Use pure triangle wave for bass preset (warmest option) - Reduce harmonics from 64 to 16 with gentle rolloff - Add 2kHz low-pass filter to wave channel - Route all wave sounds through filter for warmth --- src-v2/audio/apu/WaveChannel.ts | 15 +++++++++--- src-v2/audio/synthesis/WaveTable.ts | 36 ++++++++++++++--------------- 2 files changed, 29 insertions(+), 22 deletions(-) diff --git a/src-v2/audio/apu/WaveChannel.ts b/src-v2/audio/apu/WaveChannel.ts index 38b6d45..27f5cf2 100644 --- a/src-v2/audio/apu/WaveChannel.ts +++ b/src-v2/audio/apu/WaveChannel.ts @@ -34,6 +34,7 @@ export class WaveChannel { private volume: WaveVolume = 1; // Default to 100% private outputNode: GainNode; private currentPreset: WavePreset; + private lowpassFilter: BiquadFilterNode; constructor( audioContext: AudioContext, @@ -44,6 +45,14 @@ export class WaveChannel { this.outputNode = outputNode; this.currentPreset = preset; + // Create a low-pass filter to warm up the sound + // GB wave channel had natural rolloff from the DAC + this.lowpassFilter = audioContext.createBiquadFilter(); + this.lowpassFilter.type = 'lowpass'; + this.lowpassFilter.frequency.value = 2000; // Cut harsh highs + this.lowpassFilter.Q.value = 0.7; + this.lowpassFilter.connect(outputNode); + // Initialize wavetable with preset this.waveTable = new WaveTable(); this.loadPreset(preset); @@ -149,9 +158,9 @@ export class WaveChannel { const stopTime = now + duration + releaseTime; gain.gain.linearRampToValueAtTime(0.001, stopTime); - // Connect nodes + // Connect nodes through low-pass filter for warmer sound oscillator.connect(gain); - gain.connect(this.outputNode); + gain.connect(this.lowpassFilter); // Schedule playback oscillator.start(now); @@ -211,7 +220,7 @@ export class WaveChannel { gain.gain.linearRampToValueAtTime(0.001, stopTime); oscillator.connect(gain); - gain.connect(this.outputNode); + gain.connect(this.lowpassFilter); oscillator.start(now); oscillator.stop(stopTime + 0.01); diff --git a/src-v2/audio/synthesis/WaveTable.ts b/src-v2/audio/synthesis/WaveTable.ts index 1547fa4..a758ef3 100644 --- a/src-v2/audio/synthesis/WaveTable.ts +++ b/src-v2/audio/synthesis/WaveTable.ts @@ -193,21 +193,12 @@ export function generateSquareWave(): Uint8Array { /** * Generate a bass-optimized waveform. - * Combination of triangle with slight harmonics. + * Pure triangle wave - warm, round, and perfect for bass. + * This is the classic Game Boy bass sound. */ export function generateBassWave(): Uint8Array { - const wave = new Uint8Array(WAVE_TABLE_SIZE); - - for (let i = 0; i < WAVE_TABLE_SIZE; i++) { - const position = i / WAVE_TABLE_SIZE; - const angle = position * Math.PI * 2; - - // Fundamental + slight 2nd harmonic for warmth - const value = (Math.sin(angle) * 0.8 + Math.sin(angle * 2) * 0.2 + 1) / 2; - wave[i] = Math.floor(value * MAX_SAMPLE_VALUE); - } - - return wave; + // Use triangle wave for bass - warmest and most bass-friendly + return generateTriangleWave(); } /** @@ -259,10 +250,15 @@ export type WavePreset = keyof typeof WAVE_PRESETS; /** * Create a PeriodicWave from a wavetable for use with OscillatorNode. * This is more accurate than using AudioBufferSourceNode with playback rate. + * + * @param samples - The wavetable samples (0-15 values) + * @param audioContext - The audio context + * @param maxHarmonics - Maximum harmonics (lower = warmer, higher = brighter). Default 16 for GB authenticity. */ export function createPeriodicWaveFromTable( samples: Uint8Array | number[], - audioContext: BaseAudioContext + audioContext: BaseAudioContext, + maxHarmonics: number = 16 ): PeriodicWave { const n = samples.length; @@ -273,8 +269,9 @@ export function createPeriodicWaveFromTable( normalized.push((sample / MAX_SAMPLE_VALUE) * 2 - 1); } - // Number of harmonics - more harmonics = more accurate representation - const numHarmonics = 64; + // Limit harmonics for warmer, more GB-authentic sound + // The real GB had limited bandwidth due to its DAC + const numHarmonics = Math.min(maxHarmonics, 32); // Calculate Fourier coefficients const real = new Float32Array(numHarmonics); @@ -295,9 +292,10 @@ export function createPeriodicWaveFromTable( imagSum -= normalized[i] * Math.sin(angle); } - // Scale by 2/n for proper amplitude - real[k] = (2 * realSum) / n; - imag[k] = (2 * imagSum) / n; + // Scale by 2/n for proper amplitude, with harmonic rolloff for warmth + const rolloff = 1 / (1 + k * 0.1); // Gentle high-frequency rolloff + real[k] = (2 * realSum) / n * rolloff; + imag[k] = (2 * imagSum) / n * rolloff; } return audioContext.createPeriodicWave(real, imag, {