5e127c3a3e
- Authentic DMG-CPU sound chip implementation: - 4 Pulse channels with duty cycle control (12.5%, 25%, 50%, 75%) - 2 Wave channels with 4-bit wavetables - 2 Noise channels with LFSR (7-bit and 15-bit modes) - GameBoy Colorizer effect chain: - Low-pass filter (natural GB rolloff) - Bit-crushing (4-bit DAC simulation) - Sample rate reduction - Saturation and high-pass filter - Presets: DMG, GBC, GBA, Clean - Intelligent MIDI processing: - Track analysis and role detection (bass, lead, drums, etc.) - Automatic channel mapping to GB channels - Chord arpeggiator for polyphony handling - GameBoy Arranger for fuller sound - BitMidi search integration - Completely isolated from v1 (no changes to src/)
268 lines
7.1 KiB
TypeScript
268 lines
7.1 KiB
TypeScript
/**
|
|
* Sound Test for Wario Synth v2
|
|
*
|
|
* Tests all individual sound generators to verify they work
|
|
* and sound authentically Game Boy-like.
|
|
*/
|
|
|
|
import { PulseChannel } from '../apu/PulseChannel';
|
|
import { WaveChannel } from '../apu/WaveChannel';
|
|
import { NoiseChannel } from '../apu/NoiseChannel';
|
|
import { verifyLFSR } from '../synthesis/LFSR';
|
|
import { getFrequencyDeviation } from '../synthesis/FrequencyCalc';
|
|
import type { DutyIndex } from '../synthesis/DutyCycle';
|
|
|
|
export interface TestResult {
|
|
name: string;
|
|
passed: boolean;
|
|
message: string;
|
|
}
|
|
|
|
/**
|
|
* Run all verification tests (non-audio).
|
|
*/
|
|
export function runVerificationTests(): TestResult[] {
|
|
const results: TestResult[] = [];
|
|
|
|
// Test LFSR implementation
|
|
const lfsrOk = verifyLFSR();
|
|
results.push({
|
|
name: 'LFSR Sequence',
|
|
passed: lfsrOk,
|
|
message: lfsrOk ? 'LFSR matches expected GB sequence' : 'LFSR sequence mismatch!'
|
|
});
|
|
|
|
// Test frequency deviation (should be non-zero but small)
|
|
const devA4 = getFrequencyDeviation(69); // A4
|
|
const devOk = Math.abs(devA4) > 0.01 && Math.abs(devA4) < 10;
|
|
results.push({
|
|
name: 'Frequency Deviation',
|
|
passed: devOk,
|
|
message: `A4 deviation: ${devA4.toFixed(2)} cents (expected small non-zero value)`
|
|
});
|
|
|
|
return results;
|
|
}
|
|
|
|
/**
|
|
* Create test channels for audio testing.
|
|
*/
|
|
export function createTestChannels(audioContext: AudioContext) {
|
|
// Create master gain
|
|
const masterGain = audioContext.createGain();
|
|
masterGain.gain.value = 0.5;
|
|
masterGain.connect(audioContext.destination);
|
|
|
|
// Create individual channel gains
|
|
const pulseGain = audioContext.createGain();
|
|
pulseGain.gain.value = 0.4;
|
|
pulseGain.connect(masterGain);
|
|
|
|
const waveGain = audioContext.createGain();
|
|
waveGain.gain.value = 0.5;
|
|
waveGain.connect(masterGain);
|
|
|
|
const noiseGain = audioContext.createGain();
|
|
noiseGain.gain.value = 0.4;
|
|
noiseGain.connect(masterGain);
|
|
|
|
return {
|
|
pulse: new PulseChannel(audioContext, pulseGain, true),
|
|
wave: new WaveChannel(audioContext, waveGain, 'bass'),
|
|
noise: new NoiseChannel(audioContext, noiseGain, '15bit'),
|
|
masterGain
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Test all 4 duty cycles on the pulse channel.
|
|
*/
|
|
export async function testDutyCycles(
|
|
pulse: PulseChannel,
|
|
audioContext: AudioContext
|
|
): Promise<void> {
|
|
console.log('Testing duty cycles...');
|
|
|
|
const duties: DutyIndex[] = [0, 1, 2, 3];
|
|
const dutyNames = ['12.5%', '25%', '50%', '75%'];
|
|
|
|
for (let i = 0; i < duties.length; i++) {
|
|
console.log(` Playing duty cycle ${dutyNames[i]}`);
|
|
pulse.setDutyCycle(duties[i]);
|
|
|
|
// Play a short melody
|
|
const notes = [60, 64, 67, 72]; // C major arpeggio
|
|
const now = audioContext.currentTime;
|
|
|
|
notes.forEach((note, idx) => {
|
|
pulse.playNote(note, 0.15, 100, now + idx * 0.2);
|
|
});
|
|
|
|
// Wait for notes to finish
|
|
await sleep(1000);
|
|
}
|
|
|
|
console.log('Duty cycle test complete!');
|
|
}
|
|
|
|
/**
|
|
* Test the wave channel with different presets.
|
|
*/
|
|
export async function testWaveChannel(
|
|
wave: WaveChannel,
|
|
audioContext: AudioContext
|
|
): Promise<void> {
|
|
console.log('Testing wave channel...');
|
|
|
|
const presets = ['bass', 'pad', 'lead', 'triangle', 'sawtooth'] as const;
|
|
|
|
for (const preset of presets) {
|
|
console.log(` Playing preset: ${preset}`);
|
|
wave.loadPreset(preset);
|
|
|
|
// Play a bass line
|
|
const notes = [36, 36, 43, 41]; // Low C, C, G, F
|
|
const now = audioContext.currentTime;
|
|
|
|
notes.forEach((note, idx) => {
|
|
wave.playNote(note, 0.4, 100, now + idx * 0.5);
|
|
});
|
|
|
|
await sleep(2200);
|
|
}
|
|
|
|
console.log('Wave channel test complete!');
|
|
}
|
|
|
|
/**
|
|
* Test the noise channel with different modes.
|
|
*/
|
|
export async function testNoiseChannel(
|
|
noise: NoiseChannel,
|
|
audioContext: AudioContext
|
|
): Promise<void> {
|
|
console.log('Testing noise channel...');
|
|
|
|
// Test 15-bit mode (fuller noise)
|
|
console.log(' Testing 15-bit mode (full noise)');
|
|
noise.setMode('15bit');
|
|
|
|
let now = audioContext.currentTime;
|
|
noise.playHihat(80, false, now);
|
|
noise.playHihat(60, false, now + 0.25);
|
|
noise.playHihat(80, false, now + 0.5);
|
|
noise.playHihat(60, true, now + 0.75);
|
|
|
|
await sleep(1500);
|
|
|
|
// Test 7-bit mode (metallic)
|
|
console.log(' Testing 7-bit mode (metallic)');
|
|
noise.setMode('7bit');
|
|
|
|
now = audioContext.currentTime;
|
|
noise.playKick(100, now);
|
|
noise.playSnare(90, now + 0.5);
|
|
noise.playKick(100, now + 1.0);
|
|
noise.playSnare(90, now + 1.5);
|
|
|
|
await sleep(2200);
|
|
|
|
// Test different frequencies
|
|
console.log(' Testing frequency range');
|
|
now = audioContext.currentTime;
|
|
|
|
for (let i = 0; i < 8; i++) {
|
|
noise.playNote(36 + i * 6, 0.2, 80, now + i * 0.25);
|
|
}
|
|
|
|
await sleep(2500);
|
|
|
|
console.log('Noise channel test complete!');
|
|
}
|
|
|
|
/**
|
|
* Play a simple test melody using all channels.
|
|
*/
|
|
export async function testCombined(
|
|
pulse: PulseChannel,
|
|
wave: WaveChannel,
|
|
noise: NoiseChannel,
|
|
audioContext: AudioContext
|
|
): Promise<void> {
|
|
console.log('Testing combined playback...');
|
|
|
|
const bpm = 120;
|
|
const beatDuration = 60 / bpm;
|
|
const now = audioContext.currentTime;
|
|
|
|
// Set up channels
|
|
pulse.setDutyCycle(2); // 50%
|
|
wave.loadPreset('bass');
|
|
noise.setMode('7bit');
|
|
|
|
// 4-bar phrase
|
|
for (let bar = 0; bar < 4; bar++) {
|
|
const barStart = now + bar * 4 * beatDuration;
|
|
|
|
// Bass line (wave channel) - root notes
|
|
const bassNotes = [36, 36, 43, 41]; // C, C, G, F
|
|
wave.playNote(bassNotes[bar], beatDuration * 3.5, 90, barStart);
|
|
|
|
// Melody (pulse channel)
|
|
const melodyNotes = [
|
|
[60, 64, 67], // Bar 1: C E G
|
|
[64, 67, 72], // Bar 2: E G C
|
|
[67, 71, 74], // Bar 3: G B D
|
|
[65, 69, 72], // Bar 4: F A C
|
|
];
|
|
|
|
melodyNotes[bar].forEach((note, i) => {
|
|
pulse.playNote(note, beatDuration * 0.9, 80, barStart + i * beatDuration);
|
|
});
|
|
|
|
// Drums (noise channel)
|
|
noise.playKick(100, barStart);
|
|
noise.playHihat(60, false, barStart + beatDuration * 0.5);
|
|
noise.playSnare(90, barStart + beatDuration);
|
|
noise.playHihat(60, false, barStart + beatDuration * 1.5);
|
|
noise.playKick(80, barStart + beatDuration * 2);
|
|
noise.playHihat(60, false, barStart + beatDuration * 2.5);
|
|
noise.playSnare(90, barStart + beatDuration * 3);
|
|
noise.playHihat(60, true, barStart + beatDuration * 3.5);
|
|
}
|
|
|
|
// Wait for playback to complete
|
|
await sleep(4 * 4 * beatDuration * 1000 + 500);
|
|
|
|
console.log('Combined test complete!');
|
|
}
|
|
|
|
/**
|
|
* Run all audio tests sequentially.
|
|
*/
|
|
export async function runAllAudioTests(audioContext: AudioContext): Promise<void> {
|
|
const { pulse, wave, noise } = createTestChannels(audioContext);
|
|
|
|
console.log('=== Wario Synth v2 Audio Tests ===\n');
|
|
|
|
await testDutyCycles(pulse, audioContext);
|
|
await sleep(500);
|
|
|
|
await testWaveChannel(wave, audioContext);
|
|
await sleep(500);
|
|
|
|
await testNoiseChannel(noise, audioContext);
|
|
await sleep(500);
|
|
|
|
await testCombined(pulse, wave, noise, audioContext);
|
|
|
|
console.log('\n=== All Audio Tests Complete ===');
|
|
}
|
|
|
|
/**
|
|
* Helper: sleep for a given duration.
|
|
*/
|
|
function sleep(ms: number): Promise<void> {
|
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
}
|