Files
rsgp/src/lib/audio/engines/FormantFM.ts

409 lines
12 KiB
TypeScript

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<FormantFMParams> {
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),
};
}
}