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/)
182 lines
4.4 KiB
TypeScript
182 lines
4.4 KiB
TypeScript
/**
|
|
* Linear Feedback Shift Register (LFSR) Noise Generator
|
|
*
|
|
* The Game Boy's noise channel uses a 15-bit LFSR to generate
|
|
* pseudo-random noise. It can also operate in 7-bit mode for
|
|
* a more tonal, metallic sound.
|
|
*
|
|
* This is what gives GB noise its characteristic "crunchy" quality
|
|
* compared to smooth white noise.
|
|
*
|
|
* Reference: https://gbdev.io/pandocs/Audio_details.html#noise-channel
|
|
*/
|
|
|
|
export type LFSRMode = '7bit' | '15bit';
|
|
|
|
/**
|
|
* Initial LFSR seed value (all 1s for 15-bit register)
|
|
*/
|
|
const INITIAL_SEED = 0x7FFF;
|
|
|
|
/**
|
|
* LFSR noise generator that matches Game Boy hardware behavior.
|
|
*/
|
|
export class LFSR {
|
|
private lfsr: number;
|
|
private mode: LFSRMode;
|
|
|
|
constructor(mode: LFSRMode = '15bit') {
|
|
this.mode = mode;
|
|
this.lfsr = INITIAL_SEED;
|
|
}
|
|
|
|
/**
|
|
* Clock the LFSR once and return the output bit.
|
|
*
|
|
* Algorithm:
|
|
* 1. XOR bits 0 and 1 to get new bit
|
|
* 2. Output is current bit 0 (before shift)
|
|
* 3. Shift register right by 1
|
|
* 4. Put XOR result into bit 14
|
|
* 5. If 7-bit mode, also put XOR result into bit 6
|
|
*
|
|
* @returns 0 or 1
|
|
*/
|
|
clock(): number {
|
|
// Output is bit 0 before we modify anything
|
|
const output = this.lfsr & 1;
|
|
|
|
// XOR bits 0 and 1
|
|
const bit0 = this.lfsr & 1;
|
|
const bit1 = (this.lfsr >> 1) & 1;
|
|
const xorResult = bit0 ^ bit1;
|
|
|
|
// Shift right by 1
|
|
this.lfsr >>= 1;
|
|
|
|
// Set bit 14 to XOR result
|
|
this.lfsr |= (xorResult << 14);
|
|
|
|
// In 7-bit mode, also set bit 6
|
|
if (this.mode === '7bit') {
|
|
// Clear bit 6 first, then set if needed
|
|
this.lfsr &= ~(1 << 6);
|
|
this.lfsr |= (xorResult << 6);
|
|
}
|
|
|
|
return output;
|
|
}
|
|
|
|
/**
|
|
* Reset LFSR to initial state.
|
|
*/
|
|
reset(): void {
|
|
this.lfsr = INITIAL_SEED;
|
|
}
|
|
|
|
/**
|
|
* Set the LFSR mode.
|
|
* 7-bit mode produces more tonal, metallic sounds.
|
|
* 15-bit mode produces fuller noise.
|
|
*/
|
|
setMode(mode: LFSRMode): void {
|
|
this.mode = mode;
|
|
}
|
|
|
|
/**
|
|
* Get current mode.
|
|
*/
|
|
getMode(): LFSRMode {
|
|
return this.mode;
|
|
}
|
|
|
|
/**
|
|
* Get current register value (for debugging/visualization).
|
|
*/
|
|
getValue(): number {
|
|
return this.lfsr;
|
|
}
|
|
|
|
/**
|
|
* Generate a sequence of n output bits.
|
|
* Useful for verification against known GB sequences.
|
|
*/
|
|
generateSequence(length: number): number[] {
|
|
const sequence: number[] = [];
|
|
for (let i = 0; i < length; i++) {
|
|
sequence.push(this.clock());
|
|
}
|
|
return sequence;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Known first 20 values of 15-bit LFSR starting from 0x7FFF (all 1s).
|
|
* The first outputs are just the low bits shifting out.
|
|
* Used for verification that our implementation matches GB hardware.
|
|
*/
|
|
export const LFSR_15BIT_EXPECTED = [
|
|
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
|
0, 0, 0, 0, 0
|
|
];
|
|
|
|
/**
|
|
* Verify that our LFSR implementation produces correct output.
|
|
*/
|
|
export function verifyLFSR(): boolean {
|
|
const lfsr = new LFSR('15bit');
|
|
const sequence = lfsr.generateSequence(20);
|
|
|
|
for (let i = 0; i < LFSR_15BIT_EXPECTED.length; i++) {
|
|
if (sequence[i] !== LFSR_15BIT_EXPECTED[i]) {
|
|
console.error(`LFSR mismatch at index ${i}: got ${sequence[i]}, expected ${LFSR_15BIT_EXPECTED[i]}`);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Generate an audio buffer filled with LFSR noise.
|
|
*
|
|
* @param audioContext - Web Audio context
|
|
* @param duration - Duration in seconds
|
|
* @param frequency - Clock frequency of the LFSR
|
|
* @param mode - LFSR mode (7bit or 15bit)
|
|
* @returns AudioBuffer filled with noise
|
|
*/
|
|
export function generateNoiseBuffer(
|
|
audioContext: BaseAudioContext,
|
|
duration: number,
|
|
frequency: number,
|
|
mode: LFSRMode = '15bit'
|
|
): AudioBuffer {
|
|
const sampleRate = audioContext.sampleRate;
|
|
const bufferLength = Math.ceil(duration * sampleRate);
|
|
const buffer = audioContext.createBuffer(1, bufferLength, sampleRate);
|
|
const data = buffer.getChannelData(0);
|
|
|
|
const lfsr = new LFSR(mode);
|
|
|
|
// How many samples between LFSR clocks
|
|
const samplesPerClock = sampleRate / frequency;
|
|
|
|
let clockAccumulator = 0;
|
|
let currentOutput = 0;
|
|
|
|
for (let i = 0; i < bufferLength; i++) {
|
|
// Clock LFSR when accumulator reaches threshold
|
|
clockAccumulator += 1;
|
|
if (clockAccumulator >= samplesPerClock) {
|
|
currentOutput = lfsr.clock();
|
|
clockAccumulator -= samplesPerClock;
|
|
}
|
|
|
|
// Convert 0/1 to -1/+1 for audio
|
|
data[i] = currentOutput * 2 - 1;
|
|
}
|
|
|
|
return buffer;
|
|
}
|