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
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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, {
|
||||
|
||||
Reference in New Issue
Block a user