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/)
162 lines
4.7 KiB
TypeScript
162 lines
4.7 KiB
TypeScript
/**
|
|
* Game Boy Frequency Calculations
|
|
*
|
|
* The GB uses specific frequency formulas based on 11-bit period registers.
|
|
* This creates slightly "off" tuning compared to standard A440 tuning,
|
|
* which is part of the characteristic GB sound.
|
|
*
|
|
* Reference: https://gbdev.io/pandocs/Audio_details.html
|
|
*/
|
|
|
|
/**
|
|
* GB CPU clock rate used for audio timing
|
|
*/
|
|
const GB_CLOCK = 4194304; // 4.194304 MHz
|
|
|
|
/**
|
|
* Pulse channel base frequency divider
|
|
* Formula: freq = 131072 / (2048 - period)
|
|
*/
|
|
const PULSE_FREQ_BASE = 131072;
|
|
|
|
/**
|
|
* Wave channel base frequency divider
|
|
* Formula: freq = 65536 / (2048 - period)
|
|
* (Half the pulse frequency, so wave plays one octave lower for same period)
|
|
*/
|
|
const WAVE_FREQ_BASE = 65536;
|
|
|
|
/**
|
|
* Maximum period register value (11-bit)
|
|
*/
|
|
const MAX_PERIOD = 2047;
|
|
|
|
/**
|
|
* Noise channel divisor lookup table
|
|
* Used with divisor code (r) in noise frequency calculation
|
|
*/
|
|
const NOISE_DIVISORS = [0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4] as const;
|
|
|
|
/**
|
|
* Convert MIDI note number to standard frequency (A4 = 440Hz)
|
|
*/
|
|
export function midiToStandardFrequency(midiNote: number): number {
|
|
return 440 * Math.pow(2, (midiNote - 69) / 12);
|
|
}
|
|
|
|
/**
|
|
* Convert standard frequency to GB pulse period register value.
|
|
* Returns clamped 11-bit value (0-2047).
|
|
*/
|
|
export function frequencyToPulsePeriod(frequency: number): number {
|
|
// freq = 131072 / (2048 - period)
|
|
// period = 2048 - (131072 / freq)
|
|
const period = Math.round(2048 - (PULSE_FREQ_BASE / frequency));
|
|
return Math.max(0, Math.min(MAX_PERIOD, period));
|
|
}
|
|
|
|
/**
|
|
* Convert GB pulse period register to actual output frequency.
|
|
*/
|
|
export function pulsePeriodToFrequency(period: number): number {
|
|
if (period >= 2048) return 0;
|
|
return PULSE_FREQ_BASE / (2048 - period);
|
|
}
|
|
|
|
/**
|
|
* Calculate the actual GB frequency for a pulse channel from MIDI note.
|
|
*
|
|
* This goes: MIDI → standard freq → period register → GB freq
|
|
* The register quantization creates the characteristic slight detuning.
|
|
*/
|
|
export function calculatePulseFrequency(midiNote: number): number {
|
|
const standardFreq = midiToStandardFrequency(midiNote);
|
|
const period = frequencyToPulsePeriod(standardFreq);
|
|
return pulsePeriodToFrequency(period);
|
|
}
|
|
|
|
/**
|
|
* Convert standard frequency to GB wave period register value.
|
|
*/
|
|
export function frequencyToWavePeriod(frequency: number): number {
|
|
// freq = 65536 / (2048 - period)
|
|
// period = 2048 - (65536 / freq)
|
|
const period = Math.round(2048 - (WAVE_FREQ_BASE / frequency));
|
|
return Math.max(0, Math.min(MAX_PERIOD, period));
|
|
}
|
|
|
|
/**
|
|
* Convert GB wave period register to actual output frequency.
|
|
*/
|
|
export function wavePeriodToFrequency(period: number): number {
|
|
if (period >= 2048) return 0;
|
|
return WAVE_FREQ_BASE / (2048 - period);
|
|
}
|
|
|
|
/**
|
|
* Calculate the actual GB frequency for a wave channel from MIDI note.
|
|
*/
|
|
export function calculateWaveFrequency(midiNote: number): number {
|
|
const standardFreq = midiToStandardFrequency(midiNote);
|
|
const period = frequencyToWavePeriod(standardFreq);
|
|
return wavePeriodToFrequency(period);
|
|
}
|
|
|
|
/**
|
|
* Calculate noise channel frequency.
|
|
*
|
|
* @param divisorCode - Divisor code (0-7), selects from NOISE_DIVISORS
|
|
* @param clockShift - Clock shift (0-14), higher = lower frequency
|
|
* @returns Frequency in Hz
|
|
*
|
|
* Formula: freq = 524288 / divisor / 2^(shift+1)
|
|
*/
|
|
export function calculateNoiseFrequency(
|
|
divisorCode: number,
|
|
clockShift: number
|
|
): number {
|
|
const divisor = NOISE_DIVISORS[divisorCode % 8];
|
|
const shift = Math.max(0, Math.min(14, clockShift));
|
|
return 524288 / divisor / Math.pow(2, shift + 1);
|
|
}
|
|
|
|
/**
|
|
* Map a MIDI note to noise parameters.
|
|
* Lower notes = lower noise frequency (more "boomy")
|
|
* Higher notes = higher noise frequency (more "hissy")
|
|
*
|
|
* This is an approximation since noise isn't truly pitched.
|
|
*/
|
|
export function midiToNoiseParams(midiNote: number): {
|
|
divisorCode: number;
|
|
clockShift: number;
|
|
} {
|
|
// Map MIDI notes 24-96 to noise parameters
|
|
// Lower notes get higher shift (lower freq)
|
|
// Higher notes get lower shift (higher freq)
|
|
|
|
const normalized = Math.max(0, Math.min(72, midiNote - 24));
|
|
|
|
// Map to shift (0-14): high notes = low shift, low notes = high shift
|
|
const clockShift = Math.floor(14 - (normalized / 72) * 14);
|
|
|
|
// Divisor code affects timbre - use middle values for most natural sound
|
|
const divisorCode = Math.floor((normalized % 8));
|
|
|
|
return { divisorCode, clockShift };
|
|
}
|
|
|
|
/**
|
|
* Calculate the frequency deviation from standard tuning.
|
|
* Useful for testing/verification.
|
|
*
|
|
* @returns Deviation in cents (100 cents = 1 semitone)
|
|
*/
|
|
export function getFrequencyDeviation(midiNote: number): number {
|
|
const standard = midiToStandardFrequency(midiNote);
|
|
const gbFreq = calculatePulseFrequency(midiNote);
|
|
|
|
// Cents = 1200 * log2(f2/f1)
|
|
return 1200 * Math.log2(gbFreq / standard);
|
|
}
|