From 580aa4b96fa87e4ca4caf108c6885f2923273a41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Forment?= Date: Mon, 13 Oct 2025 13:09:59 +0200 Subject: [PATCH] Spectral additive --- src/lib/audio/engines/FormantFM.ts | 404 +++++++++++++++++++++++ src/lib/audio/engines/MassiveAdditive.ts | 313 ++++++++++++++++++ src/lib/audio/engines/registry.ts | 4 + 3 files changed, 721 insertions(+) create mode 100644 src/lib/audio/engines/FormantFM.ts create mode 100644 src/lib/audio/engines/MassiveAdditive.ts diff --git a/src/lib/audio/engines/FormantFM.ts b/src/lib/audio/engines/FormantFM.ts new file mode 100644 index 0000000..bae0901 --- /dev/null +++ b/src/lib/audio/engines/FormantFM.ts @@ -0,0 +1,404 @@ +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; + } + + 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), + }; + } +} diff --git a/src/lib/audio/engines/MassiveAdditive.ts b/src/lib/audio/engines/MassiveAdditive.ts new file mode 100644 index 0000000..0e0acb3 --- /dev/null +++ b/src/lib/audio/engines/MassiveAdditive.ts @@ -0,0 +1,313 @@ +import { CsoundEngine, type CsoundParameter } from './base/CsoundEngine'; +import type { PitchLock } from './base/SynthEngine'; + +interface MassiveAdditiveParams { + baseFreq: number; + numPartials: number; + spectrumShape: number; + harmonicSpread: number; + inharmonicity: number; + attack: number; + decay: number; + sustain: number; + release: number; + ampLFORate: number; + ampLFODepth: number; + ampLFOPhaseSpread: number; + freqLFORate: number; + freqLFODepth: number; + partialDecayRate: number; + partialAttackSpread: number; + oddEvenBalance: number; + shimmer: number; + shimmerRate: number; + stereoSpread: number; + brightness: number; + chaos: number; +} + +export class MassiveAdditive extends CsoundEngine { + getName(): string { + return 'Spectral Add'; + } + + getDescription(): string { + return 'Spectral additive synthesis with octave doubling, micro-detuned beating, and evolving harmonic content'; + } + + getType() { + return 'generative' as const; + } + + protected getOrchestra(): string { + return ` +; Function tables for sine wave +gisine ftgen 0, 0, 16384, 10, 1 + +instr 1 + iBaseFreq chnget "baseFreq" + iNumPartials chnget "numPartials" + iSpectrumShape chnget "spectrumShape" + iHarmonicSpread chnget "harmonicSpread" + iInharmonicity chnget "inharmonicity" + iAttack chnget "attack" + iDecay chnget "decay" + iSustain chnget "sustain" + iRelease chnget "release" + iAmpLFORate chnget "ampLFORate" + iAmpLFODepth chnget "ampLFODepth" + iAmpLFOPhaseSpread chnget "ampLFOPhaseSpread" + iFreqLFORate chnget "freqLFORate" + iFreqLFODepth chnget "freqLFODepth" + iPartialDecayRate chnget "partialDecayRate" + iPartialAttackSpread chnget "partialAttackSpread" + iOddEvenBalance chnget "oddEvenBalance" + iShimmer chnget "shimmer" + iShimmerRate chnget "shimmerRate" + iStereoSpread chnget "stereoSpread" + iBrightness chnget "brightness" + iChaos chnget "chaos" + + idur = p3 + iAttackTime = iAttack * idur + iDecayTime = iDecay * idur + iReleaseTime = iRelease * idur + + ; Main envelope + kEnv madsr iAttackTime, iDecayTime, iSustain, iReleaseTime + + ; Global modulation LFOs + kGlobalAmpLFO oscili 1, iAmpLFORate * 0.5 + kShimmerLFO oscili 1, iShimmerRate + kFreqLFO oscili iFreqLFODepth * 0.02, iFreqLFORate + + ; Chaos LFOs + kChaosLFO1 oscili 1, iAmpLFORate * 1.618 + kChaosLFO2 oscili 1, iAmpLFORate * 2.414 + + ; Create frequency and amplitude tables dynamically + iPartialCount = min(iNumPartials, 64) + + ; Generate tables for partial frequencies and amplitudes + gifreq ftgen 0, 0, 128, -2, 0 + giamp ftgen 0, 0, 128, -2, 0 + gifreq2 ftgen 0, 0, 128, -2, 0 + giamp2 ftgen 0, 0, 128, -2, 0 + + iPartialIndex = 0 + loop_setup: + iN = iPartialIndex + 1 + + ; Calculate frequency ratio with harmonicity and inharmonicity + iHarmonic = iN * iHarmonicSpread + iInharmonicShift = iInharmonicity * iN * iN * 0.001 + iFreqRatio = iHarmonic * (1 + iInharmonicShift) + + ; Calculate amplitude based on spectrum shape + iAmpFalloff = 1 / pow(iN, 1 + iSpectrumShape * 2) + + ; Brightness boost for higher partials + iBrightnessBoost = 1 + (iBrightness * (iN / iPartialCount) * 2) + + ; Odd/even balance + iOddEvenFactor = 1 + if (iN % 2) == 0 then + iOddEvenFactor = iOddEvenBalance + else + iOddEvenFactor = 2 - iOddEvenBalance + endif + + iAmp = iAmpFalloff * iBrightnessBoost * iOddEvenFactor + + ; Create alternate partial distribution for morphing + iFreqRatio2 = iFreqRatio * (1 + (iChaos * 0.05 * sin(iN * 0.7))) + iAmp2 = iAmp * (1 - (iN / iPartialCount) * 0.3) + + ; Write to tables + tableiw iFreqRatio, iPartialIndex, gifreq + tableiw iAmp, iPartialIndex, giamp + tableiw iFreqRatio2, iPartialIndex, gifreq2 + tableiw iAmp2, iPartialIndex, giamp2 + + iPartialIndex = iPartialIndex + 1 + if iPartialIndex < iPartialCount goto loop_setup + + ; Generate additive synthesis with heavy modulation and octave doubling + + ; Slow modulation LFOs for spectral evolution + kLFO1 oscili 1, iAmpLFORate * 0.53 + kLFO2 oscili 1, iAmpLFORate * 0.89 + kLFO3 oscili 1, iAmpLFORate * 1.37 + kShimmerLFO1 oscili 1, iShimmerRate * 0.67 + kShimmerLFO2 oscili 1, iShimmerRate * 1.13 + + ; Very slow spectral morphing + kSpectralMorph oscili 1, iAmpLFORate * 0.19 + kMorphAmount = (kSpectralMorph + 1) * 0.5 + + ; Minimal frequency wobble for organic feel + kFreqWobble = kFreqLFO * 0.5 + + ; Micro-detuning for beating (0.1 to 0.5 Hz beating) + iMicroDetune = 0.0003 + (iChaos * 0.0005) + + ; === FUNDAMENTAL OCTAVE === + ; Main voice at fundamental frequency + kcps = iBaseFreq * (1 + kFreqWobble) + kAmpMain = kEnv * (0.6 + kLFO1 * iAmpLFODepth * 0.2) + aMainVoice adsynt2 kAmpMain, kcps, gisine, gifreq, giamp, iPartialCount + + ; Slightly detuned fundamental for beating + kcpsDetune = iBaseFreq * (1 + iMicroDetune) * (1 + kFreqWobble * 0.93) + kAmpDetune = kEnv * (0.5 + kLFO2 * iAmpLFODepth * 0.25) + aDetuneVoice adsynt2 kAmpDetune, kcpsDetune, gisine, gifreq, giamp, iPartialCount, 0.25 + + ; Morphing spectral variant + kAmpMorph = kEnv * kMorphAmount * (0.4 + kShimmerLFO1 * iShimmer * 0.3) + aMorphVoice adsynt2 kAmpMorph, kcps, gisine, gifreq2, giamp2, iPartialCount, 0.5 + + ; === OCTAVE UP === + ; One octave higher with micro-detune for beating + kcpsOctUp = (iBaseFreq * 2) * (1 + kFreqWobble * 1.07) + kAmpOctUp = kEnv * (0.35 + kLFO3 * iAmpLFODepth * 0.2 + kShimmerLFO2 * iShimmer * 0.25) + aOctaveUp adsynt2 kAmpOctUp, kcpsOctUp, gisine, gifreq, giamp, iPartialCount, 0.33 + + ; Detuned octave up for complex beating + kcpsOctUpDetune = (iBaseFreq * 2) * (1 - iMicroDetune * 0.8) * (1 + kFreqWobble * 1.11) + kAmpOctUpDetune = kEnv * (0.3 + kLFO1 * iAmpLFODepth * 0.25) + aOctaveUpDetune adsynt2 kAmpOctUpDetune, kcpsOctUpDetune, gisine, gifreq, giamp, iPartialCount, 0.67 + + ; === OCTAVE DOWN === + ; One octave lower with micro-detune + kcpsOctDown = (iBaseFreq * 0.5) * (1 + kFreqWobble * 0.97) + kAmpOctDown = kEnv * (0.4 + kShimmerLFO1 * iShimmer * 0.3) + aOctaveDown adsynt2 kAmpOctDown, kcpsOctDown, gisine, gifreq, giamp, iPartialCount, 0.17 + + ; Detuned octave down for sub-harmonic beating + kcpsOctDownDetune = (iBaseFreq * 0.5) * (1 + iMicroDetune * 1.2) * (1 + kFreqWobble * 1.03) + kAmpOctDownDetune = kEnv * (0.35 + kLFO2 * iAmpLFODepth * 0.2) + aOctaveDownDetune adsynt2 kAmpOctDownDetune, kcpsOctDownDetune, gisine, gifreq2, giamp, iPartialCount, 0.83 + + ; === STEREO MIXING === + ; Left channel: emphasize fundamental and octave down + aOutL = aMainVoice * 0.7 + aDetuneVoice * 0.6 + aMorphVoice * 0.5 + aOutL = aOutL + aOctaveUp * 0.4 + aOctaveUpDetune * 0.35 + aOutL = aOutL + aOctaveDown * 0.55 + aOctaveDownDetune * 0.45 + + ; Right channel: emphasize octaves with different balance + aOutR = aMainVoice * 0.65 + aDetuneVoice * 0.55 + aMorphVoice * 0.6 + aOutR = aOutR + aOctaveUp * 0.5 + aOctaveUpDetune * 0.4 + aOutR = aOutR + aOctaveDown * 0.45 + aOctaveDownDetune * 0.5 + + ; Subtle stereo width from chaos + kStereoMod = kChaosLFO1 * iChaos * 0.1 + aOutL = aOutL * (1 - kStereoMod * iStereoSpread) + aOutR = aOutR * (1 + kStereoMod * iStereoSpread) + + ; Normalize to prevent clipping + aOutL = aOutL * 0.28 + aOutR = aOutR * 0.28 + + outs aOutL, aOutR +endin +`; + } + + protected getParametersForCsound(params: MassiveAdditiveParams): CsoundParameter[] { + return [ + { channelName: 'baseFreq', value: params.baseFreq }, + { channelName: 'numPartials', value: params.numPartials }, + { channelName: 'spectrumShape', value: params.spectrumShape }, + { channelName: 'harmonicSpread', value: params.harmonicSpread }, + { channelName: 'inharmonicity', value: params.inharmonicity }, + { channelName: 'attack', value: params.attack }, + { channelName: 'decay', value: params.decay }, + { channelName: 'sustain', value: params.sustain }, + { channelName: 'release', value: params.release }, + { channelName: 'ampLFORate', value: params.ampLFORate }, + { channelName: 'ampLFODepth', value: params.ampLFODepth }, + { channelName: 'ampLFOPhaseSpread', value: params.ampLFOPhaseSpread }, + { channelName: 'freqLFORate', value: params.freqLFORate }, + { channelName: 'freqLFODepth', value: params.freqLFODepth }, + { channelName: 'partialDecayRate', value: params.partialDecayRate }, + { channelName: 'partialAttackSpread', value: params.partialAttackSpread }, + { channelName: 'oddEvenBalance', value: params.oddEvenBalance }, + { channelName: 'shimmer', value: params.shimmer }, + { channelName: 'shimmerRate', value: params.shimmerRate }, + { channelName: 'stereoSpread', value: params.stereoSpread }, + { channelName: 'brightness', value: params.brightness }, + { channelName: 'chaos', value: params.chaos }, + ]; + } + + randomParams(pitchLock?: PitchLock): MassiveAdditiveParams { + const baseFreqChoices = [55, 82.4, 110, 146.8, 220, 293.7, 440]; + const baseFreq = pitchLock?.enabled + ? pitchLock.frequency + : this.randomChoice(baseFreqChoices) * this.randomRange(0.98, 1.02); + + return { + baseFreq, + numPartials: this.randomInt(32, 64), + spectrumShape: this.randomRange(0.3, 0.7), + harmonicSpread: this.randomChoice([1, 1.5, 2, 3]), + inharmonicity: this.randomRange(0, 0.5), + attack: this.randomRange(0.05, 0.3), + decay: this.randomRange(0.15, 0.5), + sustain: this.randomRange(0.5, 0.9), + release: this.randomRange(0.2, 0.6), + ampLFORate: this.randomRange(0.2, 2), + ampLFODepth: this.randomRange(0.3, 0.8), + ampLFOPhaseSpread: this.randomRange(0.5, 1), + freqLFORate: this.randomRange(0.1, 1.5), + freqLFODepth: this.randomRange(0.05, 0.3), + partialDecayRate: this.randomRange(0.3, 0.8), + partialAttackSpread: this.randomRange(0.1, 0.5), + oddEvenBalance: this.randomRange(0.5, 1.5), + shimmer: this.randomRange(0.3, 0.9), + shimmerRate: this.randomRange(0.05, 0.8), + stereoSpread: this.randomRange(0.4, 1), + brightness: this.randomRange(0.3, 0.9), + chaos: this.randomRange(0.1, 0.7), + }; + } + + mutateParams( + params: MassiveAdditiveParams, + mutationAmount: number = 0.15, + pitchLock?: PitchLock + ): MassiveAdditiveParams { + const baseFreq = pitchLock?.enabled ? pitchLock.frequency : params.baseFreq; + + return { + baseFreq, + numPartials: + Math.random() < 0.15 + ? this.randomInt(16, 64) + : Math.round(this.mutateValue(params.numPartials, mutationAmount, 16, 64)), + spectrumShape: this.mutateValue(params.spectrumShape, mutationAmount, 0, 1), + harmonicSpread: + Math.random() < 0.1 + ? this.randomChoice([0.5, 1, 1.5, 2]) + : params.harmonicSpread, + inharmonicity: this.mutateValue(params.inharmonicity, mutationAmount, 0, 0.5), + attack: this.mutateValue(params.attack, mutationAmount, 0.001, 0.4), + decay: this.mutateValue(params.decay, mutationAmount, 0.05, 0.6), + sustain: this.mutateValue(params.sustain, mutationAmount, 0.2, 0.9), + release: this.mutateValue(params.release, mutationAmount, 0.05, 0.8), + ampLFORate: this.mutateValue(params.ampLFORate, mutationAmount, 0.1, 6), + ampLFODepth: this.mutateValue(params.ampLFODepth, mutationAmount, 0, 1), + ampLFOPhaseSpread: this.mutateValue(params.ampLFOPhaseSpread, mutationAmount, 0, 1), + freqLFORate: this.mutateValue(params.freqLFORate, mutationAmount, 0.1, 5), + freqLFODepth: this.mutateValue(params.freqLFODepth, mutationAmount, 0, 0.8), + partialDecayRate: this.mutateValue(params.partialDecayRate, mutationAmount, 0, 1), + partialAttackSpread: this.mutateValue(params.partialAttackSpread, mutationAmount, 0, 0.8), + oddEvenBalance: this.mutateValue(params.oddEvenBalance, mutationAmount, 0.2, 1.8), + shimmer: this.mutateValue(params.shimmer, mutationAmount, 0, 1), + shimmerRate: this.mutateValue(params.shimmerRate, mutationAmount, 0.05, 2), + stereoSpread: this.mutateValue(params.stereoSpread, mutationAmount, 0, 1), + brightness: this.mutateValue(params.brightness, mutationAmount, 0, 1), + chaos: this.mutateValue(params.chaos, mutationAmount, 0, 0.8), + }; + } +} diff --git a/src/lib/audio/engines/registry.ts b/src/lib/audio/engines/registry.ts index d59a3fb..6e97070 100644 --- a/src/lib/audio/engines/registry.ts +++ b/src/lib/audio/engines/registry.ts @@ -2,6 +2,7 @@ import type { SynthEngine } from './base/SynthEngine'; import { FourOpFM } from './FourOpFM'; import { TwoOpFM } from './TwoOpFM'; import { PhaseDistortionFM } from './PhaseDistortionFM'; +import { FormantFM } from './FormantFM'; import { DubSiren } from './DubSiren'; import { Benjolin } from './Benjolin'; import { ZzfxEngine } from './ZzfxEngine'; @@ -17,6 +18,7 @@ import { HiHat } from './HiHat'; import { ParticleNoise } from './ParticleNoise'; import { DustNoise } from './DustNoise'; import { SubtractiveThreeOsc } from './SubtractiveThreeOsc'; +import { MassiveAdditive } from './MassiveAdditive'; export const engines: SynthEngine[] = [ new Sample(), @@ -24,6 +26,7 @@ export const engines: SynthEngine[] = [ new FourOpFM(), new TwoOpFM(), new PhaseDistortionFM(), + new FormantFM(), new DubSiren(), new Benjolin(), new ZzfxEngine(), @@ -37,4 +40,5 @@ export const engines: SynthEngine[] = [ new ParticleNoise(), new DustNoise(), new SubtractiveThreeOsc(), + new MassiveAdditive(), ];