import { CsoundEngine, type CsoundParameter } from './base/CsoundEngine'; import type { PitchLock } from './base/SynthEngine'; enum VowelType { A, E, I, O, U, AE, OE, UE, } enum ModulationType { SimpleFM, DoubleFM, RingMod, CrossFM, } interface FormantBand { frequency: number; bandwidth: number; amplitude: number; } interface FormantFMParams { baseFreq: number; vowel: VowelType; vowelMorph: number; modulationType: ModulationType; modIndex: number; modRatio: number; attack: number; decay: number; sustain: number; release: number; brightness: number; vibrato: number; vibratoRate: number; detune: number; noise: number; formantLFORate: number; formantLFODepth: number; modIndexLFORate: number; modIndexLFODepth: number; chaos: number; } export class FormantFM extends CsoundEngine { getName(): string { return 'Formant FM'; } getDescription(): string { return 'FM synthesis with formant filters creating vowel-like sounds and vocal textures'; } getType() { return 'generative' as const; } getCategory() { return 'FM' as const; } protected getOrchestra(): string { return ` instr 1 iBaseFreq chnget "baseFreq" iVowel chnget "vowel" iVowelMorph chnget "vowelMorph" iModType chnget "modulationType" iModIndex chnget "modIndex" iModRatio chnget "modRatio" iAttack chnget "attack" iDecay chnget "decay" iSustain chnget "sustain" iRelease chnget "release" iBrightness chnget "brightness" iVibrato chnget "vibrato" iVibratoRate chnget "vibratoRate" iDetune chnget "detune" iNoise chnget "noise" iFormantLFORate chnget "formantLFORate" iFormantLFODepth chnget "formantLFODepth" iModIndexLFORate chnget "modIndexLFORate" iModIndexLFODepth chnget "modIndexLFODepth" iChaos chnget "chaos" idur = p3 iAttackTime = iAttack * idur iDecayTime = iDecay * idur iReleaseTime = iRelease * idur ; Envelope kEnv madsr iAttackTime, iDecayTime, iSustain, iReleaseTime ; Vibrato LFO with chaos kVib oscili iVibrato * iBaseFreq * 0.02, iVibratoRate kChaosLFO1 oscili iChaos * iBaseFreq * 0.01, iVibratoRate * 1.618 kChaosLFO2 oscili iChaos * iBaseFreq * 0.005, iVibratoRate * 2.414 kFreq = iBaseFreq + kVib + kChaosLFO1 + kChaosLFO2 ; Get formant parameters based on vowel type if iVowel == 0 then ; A (as in "father") iF1 = 730 iF2 = 1090 iF3 = 2440 iA1 = 1.0 iA2 = 0.5 iA3 = 0.25 iBW1 = 80 iBW2 = 90 iBW3 = 120 elseif iVowel == 1 then ; E (as in "bet") iF1 = 530 iF2 = 1840 iF3 = 2480 iA1 = 1.0 iA2 = 0.6 iA3 = 0.3 iBW1 = 60 iBW2 = 100 iBW3 = 120 elseif iVowel == 2 then ; I (as in "bit") iF1 = 390 iF2 = 1990 iF3 = 2550 iA1 = 1.0 iA2 = 0.7 iA3 = 0.2 iBW1 = 50 iBW2 = 100 iBW3 = 120 elseif iVowel == 3 then ; O (as in "boat") iF1 = 570 iF2 = 840 iF3 = 2410 iA1 = 1.0 iA2 = 0.45 iA3 = 0.28 iBW1 = 70 iBW2 = 80 iBW3 = 100 elseif iVowel == 4 then ; U (as in "boot") iF1 = 440 iF2 = 1020 iF3 = 2240 iA1 = 1.0 iA2 = 0.4 iA3 = 0.2 iBW1 = 70 iBW2 = 80 iBW3 = 100 elseif iVowel == 5 then ; AE (as in "bat") iF1 = 660 iF2 = 1720 iF3 = 2410 iA1 = 1.0 iA2 = 0.55 iA3 = 0.3 iBW1 = 80 iBW2 = 90 iBW3 = 120 elseif iVowel == 6 then ; OE (as in "bird") iF1 = 490 iF2 = 1350 iF3 = 1690 iA1 = 1.0 iA2 = 0.5 iA3 = 0.4 iBW1 = 70 iBW2 = 80 iBW3 = 100 else ; UE (as in "about") iF1 = 520 iF2 = 1190 iF3 = 2390 iA1 = 1.0 iA2 = 0.45 iA3 = 0.25 iBW1 = 70 iBW2 = 80 iBW3 = 110 endif ; Modulate formant frequencies with multiple LFOs kFormantShift = 1 + (iVowelMorph - 0.5) * 0.4 kFormantLFO oscili iFormantLFODepth * 0.3, iFormantLFORate kFormantShiftModulated = kFormantShift + kFormantLFO kF1 = iF1 * kFormantShiftModulated kF2 = iF2 * kFormantShiftModulated kF3 = iF3 * kFormantShiftModulated ; Modulation index LFO kModIndexLFO oscili iModIndexLFODepth * iModIndex, iModIndexLFORate kModIndexDynamic = iModIndex + kModIndexLFO ; Generate carrier and modulator based on modulation type kModFreq = kFreq * iModRatio if iModType == 0 then ; Simple FM with dynamic modulation aMod oscili kModIndexDynamic * kModFreq, kModFreq aCarrier oscili 0.5, kFreq + aMod elseif iModType == 1 then ; Double FM (modulator modulates itself) with chaos kChaosModDepth = 1 + (iChaos * 0.5) aMod1 oscili kModIndexDynamic * 0.3 * kModFreq * kChaosModDepth, kModFreq * 0.5 aMod2 oscili kModIndexDynamic * kModFreq, kModFreq + aMod1 aCarrier oscili 0.5, kFreq + aMod2 elseif iModType == 2 then ; Ring modulation with frequency wobble kRingModWobble oscili iChaos * 0.2, iFormantLFORate * 0.7 aMod oscili 0.5, kModFreq * (1 + kRingModWobble) aCarrierTemp oscili 0.5, kFreq aCarrier = aCarrierTemp * aMod else ; Cross FM with multiple carriers aMod oscili kModIndexDynamic * kModFreq, kModFreq aCarrier1 oscili 0.4, kFreq + aMod aCarrier2 oscili 0.3, kFreq * 0.5 + aMod * 0.5 kThirdCarrierFreq = kFreq * (1.5 + iChaos * 0.3) aCarrier3 oscili 0.2 * iChaos, kThirdCarrierFreq + aMod * 0.3 aCarrier = aCarrier1 + aCarrier2 + aCarrier3 endif ; Add brightness via high-frequency content aCarrierBright oscili 0.15 * iBrightness, kFreq * 2 aCarrierMix = aCarrier + aCarrierBright ; Add subtle noise for breathiness aNoise noise 0.08 * iNoise, 0 aCarrierFinal = aCarrierMix + aNoise ; Apply formant filters (bandpass filters at formant frequencies) aFormant1 butterbp aCarrierFinal, kF1, iBW1 aFormant1Scaled = aFormant1 * iA1 aFormant2 butterbp aCarrierFinal, kF2, iBW2 aFormant2Scaled = aFormant2 * iA2 aFormant3 butterbp aCarrierFinal, kF3, iBW3 aFormant3Scaled = aFormant3 * iA3 ; Mix formants aMix = (aFormant1Scaled + aFormant2Scaled + aFormant3Scaled) * 0.6 ; Apply envelope aOut = aMix * kEnv ; Stereo - slightly different phase for right channel iDetuneFactor = 1 + (iDetune * 0.5) kFreqR = iBaseFreq * iDetuneFactor + kVib ; Regenerate right channel with detuned frequency kModFreqR = kFreqR * iModRatio if iModType == 0 then aModR oscili iModIndex * kModFreqR, kModFreqR aCarrierR oscili 0.5, kFreqR + aModR elseif iModType == 1 then aMod1R oscili iModIndex * 0.3 * kModFreqR, kModFreqR * 0.5 aMod2R oscili iModIndex * kModFreqR, kModFreqR + aMod1R aCarrierR oscili 0.5, kFreqR + aMod2R elseif iModType == 2 then aModR oscili 0.5, kModFreqR aCarrierTempR oscili 0.5, kFreqR aCarrierR = aCarrierTempR * aModR else aModR oscili iModIndex * kModFreqR, kModFreqR aCarrier1R oscili 0.4, kFreqR + aModR aCarrier2R oscili 0.3, kFreqR * 0.5 + aModR * 0.5 aCarrierR = aCarrier1R + aCarrier2R endif aCarrierBrightR oscili 0.15 * iBrightness, kFreqR * 2 aCarrierMixR = aCarrierR + aCarrierBrightR aNoiseR noise 0.08 * iNoise, 0 aCarrierFinalR = aCarrierMixR + aNoiseR kF1R = iF1 * kFormantShift kF2R = iF2 * kFormantShift kF3R = iF3 * kFormantShift aFormant1R butterbp aCarrierFinalR, kF1R, iBW1 aFormant1ScaledR = aFormant1R * iA1 aFormant2R butterbp aCarrierFinalR, kF2R, iBW2 aFormant2ScaledR = aFormant2R * iA2 aFormant3R butterbp aCarrierFinalR, kF3R, iBW3 aFormant3ScaledR = aFormant3R * iA3 aMixR = (aFormant1ScaledR + aFormant2ScaledR + aFormant3ScaledR) * 0.6 aOutR = aMixR * kEnv outs aOut, aOutR endin `; } protected getParametersForCsound(params: FormantFMParams): CsoundParameter[] { return [ { channelName: 'baseFreq', value: params.baseFreq }, { channelName: 'vowel', value: params.vowel }, { channelName: 'vowelMorph', value: params.vowelMorph }, { channelName: 'modulationType', value: params.modulationType }, { channelName: 'modIndex', value: params.modIndex }, { channelName: 'modRatio', value: params.modRatio }, { channelName: 'attack', value: params.attack }, { channelName: 'decay', value: params.decay }, { channelName: 'sustain', value: params.sustain }, { channelName: 'release', value: params.release }, { channelName: 'brightness', value: params.brightness }, { channelName: 'vibrato', value: params.vibrato }, { channelName: 'vibratoRate', value: params.vibratoRate }, { channelName: 'detune', value: params.detune }, { channelName: 'noise', value: params.noise }, { channelName: 'formantLFORate', value: params.formantLFORate }, { channelName: 'formantLFODepth', value: params.formantLFODepth }, { channelName: 'modIndexLFORate', value: params.modIndexLFORate }, { channelName: 'modIndexLFODepth', value: params.modIndexLFODepth }, { channelName: 'chaos', value: params.chaos }, ]; } randomParams(pitchLock?: PitchLock): FormantFMParams { const baseFreqChoices = [82.4, 110, 146.8, 220, 293.7, 440]; const baseFreq = pitchLock?.enabled ? pitchLock.frequency : this.randomChoice(baseFreqChoices) * this.randomRange(0.95, 1.05); const modulationRatios = [0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 5, 7, 9]; return { baseFreq, vowel: this.randomInt(0, 7) as VowelType, vowelMorph: this.randomRange(0.3, 0.7), modulationType: this.randomInt(0, 3) as ModulationType, modIndex: this.randomRange(1, 10), modRatio: this.randomChoice(modulationRatios), attack: this.randomRange(0.001, 0.15), decay: this.randomRange(0.05, 0.25), sustain: this.randomRange(0.3, 0.8), release: this.randomRange(0.1, 0.4), brightness: this.randomRange(0, 0.6), vibrato: this.randomRange(0, 0.5), vibratoRate: this.randomRange(3, 8), detune: this.randomRange(0.001, 0.01), noise: this.randomRange(0, 0.3), formantLFORate: this.randomRange(0.2, 6), formantLFODepth: this.randomRange(0, 0.8), modIndexLFORate: this.randomRange(0.5, 12), modIndexLFODepth: this.randomRange(0, 0.7), chaos: this.randomRange(0, 0.8), }; } mutateParams( params: FormantFMParams, mutationAmount: number = 0.15, pitchLock?: PitchLock ): FormantFMParams { const baseFreq = pitchLock?.enabled ? pitchLock.frequency : params.baseFreq; const modulationRatios = [0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 5, 7, 9]; return { baseFreq, vowel: Math.random() < 0.1 ? (this.randomInt(0, 7) as VowelType) : params.vowel, vowelMorph: this.mutateValue(params.vowelMorph, mutationAmount, 0, 1), modulationType: Math.random() < 0.08 ? (this.randomInt(0, 3) as ModulationType) : params.modulationType, modIndex: this.mutateValue(params.modIndex, mutationAmount, 0.5, 15), modRatio: Math.random() < 0.12 ? this.randomChoice(modulationRatios) : params.modRatio, attack: this.mutateValue(params.attack, mutationAmount, 0.001, 0.25), decay: this.mutateValue(params.decay, mutationAmount, 0.02, 0.4), sustain: this.mutateValue(params.sustain, mutationAmount, 0.1, 0.9), release: this.mutateValue(params.release, mutationAmount, 0.05, 0.6), brightness: this.mutateValue(params.brightness, mutationAmount, 0, 1), vibrato: this.mutateValue(params.vibrato, mutationAmount, 0, 0.6), vibratoRate: this.mutateValue(params.vibratoRate, mutationAmount, 2, 10), detune: this.mutateValue(params.detune, mutationAmount, 0.0005, 0.02), noise: this.mutateValue(params.noise, mutationAmount, 0, 0.5), formantLFORate: this.mutateValue(params.formantLFORate, mutationAmount, 0.1, 8), formantLFODepth: this.mutateValue(params.formantLFODepth, mutationAmount, 0, 1), modIndexLFORate: this.mutateValue(params.modIndexLFORate, mutationAmount, 0.3, 15), modIndexLFODepth: this.mutateValue(params.modIndexLFODepth, mutationAmount, 0, 1), chaos: this.mutateValue(params.chaos, mutationAmount, 0, 1), }; } }