Files
motif/src-v2/audio/synthesis/FrequencyCalc.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

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