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

83 lines
2.1 KiB
TypeScript

/**
* Game Boy Duty Cycle Implementation
*
* The GB pulse channels support 4 duty cycle patterns.
* These exact duty ratios give the Game Boy its distinctive sound.
*/
/**
* Duty cycle ratios for the 4 GB patterns
* 12.5% - Very thin, buzzy, laser-like sound
* 25% - Classic chiptune sound, bright and punchy
* 50% - Full square wave
* 75% - Same as 25% but inverted
*/
export const DUTY_RATIOS = [0.125, 0.25, 0.5, 0.75] as const;
export type DutyIndex = 0 | 1 | 2 | 3;
/**
* Creates a PeriodicWave for Web Audio from a duty cycle.
*
* Uses proper Fourier series for pulse wave:
* imag[n] = (2 / (π * n)) * sin(π * n * duty)
*
* This is the mathematically correct way to synthesize pulse waves.
*/
export function createDutyWave(
dutyIndex: DutyIndex,
audioContext: BaseAudioContext
): PeriodicWave {
const dutyRatio = DUTY_RATIOS[dutyIndex];
// More harmonics = sharper edges (but more CPU)
const numHarmonics = 64;
const real = new Float32Array(numHarmonics);
const imag = new Float32Array(numHarmonics);
// DC offset = 0 for centered waveform
real[0] = 0;
imag[0] = 0;
// Fourier series for pulse wave
// https://en.wikipedia.org/wiki/Pulse_wave
for (let n = 1; n < numHarmonics; n++) {
// Pulse wave Fourier coefficient
const coefficient = (2 / (Math.PI * n)) * Math.sin(Math.PI * n * dutyRatio);
imag[n] = coefficient;
real[n] = 0;
}
return audioContext.createPeriodicWave(real, imag, {
disableNormalization: false
});
}
/**
* Pre-creates all 4 duty cycle waveforms for efficient reuse.
*/
export function createAllDutyWaves(
audioContext: BaseAudioContext
): PeriodicWave[] {
return [
createDutyWave(0, audioContext),
createDutyWave(1, audioContext),
createDutyWave(2, audioContext),
createDutyWave(3, audioContext),
];
}
/**
* Returns a human-readable description of each duty cycle.
*/
export function getDutyDescription(dutyIndex: DutyIndex): string {
const descriptions = [
'12.5% - Thin, buzzy',
'25% - Classic chiptune',
'50% - Full square',
'75% - Bright, punchy',
];
return descriptions[dutyIndex];
}