409 lines
12 KiB
TypeScript
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),
|
|
};
|
|
}
|
|
}
|