Files
motif/src-v2/audio/synthesis/LFSR.ts
T
b1rdmania 5e127c3a3e Add v2 Game Boy sound engine (isolated from v1)
- 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/)
2026-01-20 19:36:13 +00:00

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;
}