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 volume: WaveVolume = 1; // Default to 100%
|
||||||
private outputNode: GainNode;
|
private outputNode: GainNode;
|
||||||
private currentPreset: WavePreset;
|
private currentPreset: WavePreset;
|
||||||
|
private lowpassFilter: BiquadFilterNode;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
audioContext: AudioContext,
|
audioContext: AudioContext,
|
||||||
@@ -44,6 +45,14 @@ export class WaveChannel {
|
|||||||
this.outputNode = outputNode;
|
this.outputNode = outputNode;
|
||||||
this.currentPreset = preset;
|
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
|
// Initialize wavetable with preset
|
||||||
this.waveTable = new WaveTable();
|
this.waveTable = new WaveTable();
|
||||||
this.loadPreset(preset);
|
this.loadPreset(preset);
|
||||||
@@ -149,9 +158,9 @@ export class WaveChannel {
|
|||||||
const stopTime = now + duration + releaseTime;
|
const stopTime = now + duration + releaseTime;
|
||||||
gain.gain.linearRampToValueAtTime(0.001, stopTime);
|
gain.gain.linearRampToValueAtTime(0.001, stopTime);
|
||||||
|
|
||||||
// Connect nodes
|
// Connect nodes through low-pass filter for warmer sound
|
||||||
oscillator.connect(gain);
|
oscillator.connect(gain);
|
||||||
gain.connect(this.outputNode);
|
gain.connect(this.lowpassFilter);
|
||||||
|
|
||||||
// Schedule playback
|
// Schedule playback
|
||||||
oscillator.start(now);
|
oscillator.start(now);
|
||||||
@@ -211,7 +220,7 @@ export class WaveChannel {
|
|||||||
gain.gain.linearRampToValueAtTime(0.001, stopTime);
|
gain.gain.linearRampToValueAtTime(0.001, stopTime);
|
||||||
|
|
||||||
oscillator.connect(gain);
|
oscillator.connect(gain);
|
||||||
gain.connect(this.outputNode);
|
gain.connect(this.lowpassFilter);
|
||||||
|
|
||||||
oscillator.start(now);
|
oscillator.start(now);
|
||||||
oscillator.stop(stopTime + 0.01);
|
oscillator.stop(stopTime + 0.01);
|
||||||
|
|||||||
@@ -193,21 +193,12 @@ export function generateSquareWave(): Uint8Array {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a bass-optimized waveform.
|
* 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 {
|
export function generateBassWave(): Uint8Array {
|
||||||
const wave = new Uint8Array(WAVE_TABLE_SIZE);
|
// Use triangle wave for bass - warmest and most bass-friendly
|
||||||
|
return generateTriangleWave();
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -259,10 +250,15 @@ export type WavePreset = keyof typeof WAVE_PRESETS;
|
|||||||
/**
|
/**
|
||||||
* Create a PeriodicWave from a wavetable for use with OscillatorNode.
|
* Create a PeriodicWave from a wavetable for use with OscillatorNode.
|
||||||
* This is more accurate than using AudioBufferSourceNode with playback rate.
|
* 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(
|
export function createPeriodicWaveFromTable(
|
||||||
samples: Uint8Array | number[],
|
samples: Uint8Array | number[],
|
||||||
audioContext: BaseAudioContext
|
audioContext: BaseAudioContext,
|
||||||
|
maxHarmonics: number = 16
|
||||||
): PeriodicWave {
|
): PeriodicWave {
|
||||||
const n = samples.length;
|
const n = samples.length;
|
||||||
|
|
||||||
@@ -273,8 +269,9 @@ export function createPeriodicWaveFromTable(
|
|||||||
normalized.push((sample / MAX_SAMPLE_VALUE) * 2 - 1);
|
normalized.push((sample / MAX_SAMPLE_VALUE) * 2 - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Number of harmonics - more harmonics = more accurate representation
|
// Limit harmonics for warmer, more GB-authentic sound
|
||||||
const numHarmonics = 64;
|
// The real GB had limited bandwidth due to its DAC
|
||||||
|
const numHarmonics = Math.min(maxHarmonics, 32);
|
||||||
|
|
||||||
// Calculate Fourier coefficients
|
// Calculate Fourier coefficients
|
||||||
const real = new Float32Array(numHarmonics);
|
const real = new Float32Array(numHarmonics);
|
||||||
@@ -295,9 +292,10 @@ export function createPeriodicWaveFromTable(
|
|||||||
imagSum -= normalized[i] * Math.sin(angle);
|
imagSum -= normalized[i] * Math.sin(angle);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scale by 2/n for proper amplitude
|
// Scale by 2/n for proper amplitude, with harmonic rolloff for warmth
|
||||||
real[k] = (2 * realSum) / n;
|
const rolloff = 1 / (1 + k * 0.1); // Gentle high-frequency rolloff
|
||||||
imag[k] = (2 * imagSum) / n;
|
real[k] = (2 * realSum) / n * rolloff;
|
||||||
|
imag[k] = (2 * imagSum) / n * rolloff;
|
||||||
}
|
}
|
||||||
|
|
||||||
return audioContext.createPeriodicWave(real, imag, {
|
return audioContext.createPeriodicWave(real, imag, {
|
||||||
|
|||||||
Reference in New Issue
Block a user