From b700c68b4d5014fd95ff4fe744e6d7f41f8c2c24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Forment?= Date: Mon, 13 Oct 2025 17:35:47 +0200 Subject: [PATCH] Quite a big update --- src/lib/audio/engines/Dripwater.ts | 334 +++++++++++++++++++++ src/lib/audio/engines/Form1.ts | 296 ++++++++++++++++++ src/lib/audio/engines/LaserSweep.ts | 244 +++++++++++++++ src/lib/audio/engines/Squine1.ts | 149 +++++++++ src/lib/audio/engines/Sub8.ts | 135 +++++++++ src/lib/audio/engines/base/CsoundEngine.ts | 89 +++++- src/lib/audio/engines/registry.ts | 10 + src/lib/audio/processors/ExpFadeIn.ts | 27 ++ src/lib/audio/processors/ExpFadeOut.ts | 27 ++ src/lib/audio/processors/LinearFadeIn.ts | 26 ++ src/lib/audio/processors/LinearFadeOut.ts | 26 ++ src/lib/audio/processors/registry.ts | 8 + 12 files changed, 1366 insertions(+), 5 deletions(-) create mode 100644 src/lib/audio/engines/Dripwater.ts create mode 100644 src/lib/audio/engines/Form1.ts create mode 100644 src/lib/audio/engines/LaserSweep.ts create mode 100644 src/lib/audio/engines/Squine1.ts create mode 100644 src/lib/audio/engines/Sub8.ts create mode 100644 src/lib/audio/processors/ExpFadeIn.ts create mode 100644 src/lib/audio/processors/ExpFadeOut.ts create mode 100644 src/lib/audio/processors/LinearFadeIn.ts create mode 100644 src/lib/audio/processors/LinearFadeOut.ts diff --git a/src/lib/audio/engines/Dripwater.ts b/src/lib/audio/engines/Dripwater.ts new file mode 100644 index 0000000..cdf7bb8 --- /dev/null +++ b/src/lib/audio/engines/Dripwater.ts @@ -0,0 +1,334 @@ +import { CsoundEngine, type CsoundParameter } from './base/CsoundEngine'; +import type { PitchLock } from './base/SynthEngine'; + +interface DripwaterParams { + baseFreq: number; + dropRate: number; + numElements: number; + damping: number; + shakeIntensity: number; + freqRatio1: number; + freqRatio2: number; + freqRatio3: number; + attack: number; + release: number; + amplitude: number; + delayTime1: number; + delayTime2: number; + delayTime3: number; + delayTime4: number; + delayGain1: number; + delayGain2: number; + delayGain3: number; + delayGain4: number; + feedbackDelayTime: number; + feedbackAmount: number; + modDepth: number; + modRate: number; + stereoSpread: number; + combDelayTime: number; + combFeedback: number; + reverseDelayTime: number; + reverseGain: number; + cascadeDelay1: number; + cascadeDelay2: number; + cascadeFeedback: number; +} + +export class Dripwater extends CsoundEngine { + getName(): string { + return 'Weird Waters'; + } + + getDescription(): string { + return 'Physical model of water droplets through cascading delays, comb filters, and reverse echoes'; + } + + getType() { + return 'generative' as const; + } + + getCategory() { + return 'Noise' as const; + } + + protected getOrchestra(): string { + return ` +instr 1 + iBaseFreq chnget "baseFreq" + iDropRate chnget "dropRate" + iNumElements chnget "numElements" + iDamping chnget "damping" + iShakeIntensity chnget "shakeIntensity" + iFreqRatio1 chnget "freqRatio1" + iFreqRatio2 chnget "freqRatio2" + iFreqRatio3 chnget "freqRatio3" + iAttack chnget "attack" + iRelease chnget "release" + iAmplitude chnget "amplitude" + iDelayTime1 chnget "delayTime1" + iDelayTime2 chnget "delayTime2" + iDelayTime3 chnget "delayTime3" + iDelayTime4 chnget "delayTime4" + iDelayGain1 chnget "delayGain1" + iDelayGain2 chnget "delayGain2" + iDelayGain3 chnget "delayGain3" + iDelayGain4 chnget "delayGain4" + iFeedbackDelayTime chnget "feedbackDelayTime" + iFeedbackAmount chnget "feedbackAmount" + iModDepth chnget "modDepth" + iModRate chnget "modRate" + iStereoSpread chnget "stereoSpread" + iCombDelayTime chnget "combDelayTime" + iCombFeedback chnget "combFeedback" + iReverseDelayTime chnget "reverseDelayTime" + iReverseGain chnget "reverseGain" + iCascadeDelay1 chnget "cascadeDelay1" + iCascadeDelay2 chnget "cascadeDelay2" + iCascadeFeedback chnget "cascadeFeedback" + + idur = p3 + iAttackTime = iAttack * idur + iReleaseTime = iRelease * idur + + ; Scale delay times by duration so they fill the entire sound + iScaledDelayTime1 = iDelayTime1 * idur + iScaledDelayTime2 = iDelayTime2 * idur + iScaledDelayTime3 = iDelayTime3 * idur + iScaledDelayTime4 = iDelayTime4 * idur + iScaledFeedbackDelay = iFeedbackDelayTime * idur + iModDepthScaled = iModDepth * idur * 0.1 + iScaledCombDelay = iCombDelayTime * idur + iScaledReverseDelay = iReverseDelayTime * idur + iScaledCascade1 = iCascadeDelay1 * idur + iScaledCascade2 = iCascadeDelay2 * idur + + ; Calculate resonant frequencies from base frequency and ratios + iFreq1 = iBaseFreq * iFreqRatio1 + iFreq2 = iBaseFreq * iFreqRatio2 + iFreq3 = iBaseFreq * iFreqRatio3 + + ; Generate dripwater sound + ; Signature: ares dripwater kamp, idettack, inum, idamp, imaxshake, ifreq, ifreq1, ifreq2 + ; Reduce amplitude to prevent clipping with all the delays + kAmp = iAmplitude * 0.08 + aDrip dripwater kAmp, iDropRate, iNumElements, iDamping, iShakeIntensity, iFreq1, iFreq2, iFreq3 + + ; Apply AR envelope + kEnv linsegr 0, iAttackTime, 1, iReleaseTime, 0 + + aDry = aDrip * kEnv + + ; Apply multitap delay with duration-scaled times + ; Signature: a1 [, a2, a3, ...] multitap asource [, itime1, igain1, itime2, igain2, ...] + aMultitap multitap aDry, iScaledDelayTime1, iDelayGain1, iScaledDelayTime2, iDelayGain2, iScaledDelayTime3, iDelayGain3, iScaledDelayTime4, iDelayGain4 + + ; Add modulated feedback delay + ; LFO modulates delay time + kLFO oscili iModDepthScaled, iModRate + kDelayTime = iScaledFeedbackDelay + kLFO + kDelayTime = max(kDelayTime, 0.001) + + ; Variable delay with feedback + aBuf delayr idur + aTapL deltapi kDelayTime + delayw aDry + (aTapL * iFeedbackAmount) + + ; Comb filter delay (creates resonance) + aComb comb aDry, iScaledCombDelay, iCombFeedback + + ; Reverse delay using delayr/delayw with interpolation + aBufRev delayr iScaledReverseDelay + aReverseTap deltapi iScaledReverseDelay * (1 - (line:a(0, idur, 1))) + delayw aDry + aReverse = aReverseTap * iReverseGain + + ; Cascaded delays (two delays feeding into each other) + aBuf1 delayr idur + aCascade1 deltapi iScaledCascade1 + aBuf2 delayr idur + aCascade2 deltapi iScaledCascade2 + delayw aCascade1 * iCascadeFeedback + delayw aDry + (aCascade2 * iCascadeFeedback) + + aOut = aDry + aMultitap + aTapL + aComb + aReverse + aCascade1 + aCascade2 + + ; Create stereo width with slightly different parameters for right channel + ; Slightly detune the resonant frequencies for right channel + iFreq1_R = iFreq1 * 1.003 + iFreq2_R = iFreq2 * 0.998 + iFreq3_R = iFreq3 * 1.002 + + ; Use slightly different initial values for variation + iDropRate_R = iDropRate * 1.02 + iShakeIntensity_R = iShakeIntensity * 0.98 + + kAmp_R = iAmplitude * 0.08 + aDrip_R dripwater kAmp_R, iDropRate_R, iNumElements, iDamping, iShakeIntensity_R, iFreq1_R, iFreq2_R, iFreq3_R + + aDry_R = aDrip_R * kEnv + + ; Apply multitap delay to right channel with slightly different times for stereo width + iScaledDelayTime1_R = iScaledDelayTime1 * 1.03 + iScaledDelayTime2_R = iScaledDelayTime2 * 0.97 + iScaledDelayTime3_R = iScaledDelayTime3 * 1.05 + iScaledDelayTime4_R = iScaledDelayTime4 * 0.95 + + aMultitap_R multitap aDry_R, iScaledDelayTime1_R, iDelayGain1, iScaledDelayTime2_R, iDelayGain2, iScaledDelayTime3_R, iDelayGain3, iScaledDelayTime4_R, iDelayGain4 + + ; Right channel modulated delay with stereo spread + kLFO_R oscili iModDepthScaled, iModRate * 1.07 + kDelayTime_R = (iScaledFeedbackDelay * iStereoSpread) + kLFO_R + kDelayTime_R = max(kDelayTime_R, 0.001) + + aBuf_R delayr idur + aTapR deltapi kDelayTime_R + delayw aDry_R + (aTapR * iFeedbackAmount) + + ; Comb filter for right channel + iScaledCombDelay_R = iScaledCombDelay * 1.07 + aComb_R comb aDry_R, iScaledCombDelay_R, iCombFeedback + + ; Reverse delay for right channel + iScaledReverseDelay_R = iScaledReverseDelay * 0.93 + aBufRev_R delayr iScaledReverseDelay_R + aReverseTap_R deltapi iScaledReverseDelay_R * (1 - (line:a(0, idur, 1))) + delayw aDry_R + aReverse_R = aReverseTap_R * iReverseGain + + ; Cascaded delays for right channel + iScaledCascade1_R = iScaledCascade1 * 1.05 + iScaledCascade2_R = iScaledCascade2 * 0.95 + aBuf1_R delayr idur + aCascade1_R deltapi iScaledCascade1_R + aBuf2_R delayr idur + aCascade2_R deltapi iScaledCascade2_R + delayw aCascade1_R * iCascadeFeedback + delayw aDry_R + (aCascade2_R * iCascadeFeedback) + + aOut_R = aDry_R + aMultitap_R + aTapR + aComb_R + aReverse_R + aCascade1_R + aCascade2_R + + outs aOut, aOut_R +endin +`; + } + + protected getParametersForCsound(params: DripwaterParams): CsoundParameter[] { + return [ + { channelName: 'baseFreq', value: params.baseFreq }, + { channelName: 'dropRate', value: params.dropRate }, + { channelName: 'numElements', value: params.numElements }, + { channelName: 'damping', value: params.damping }, + { channelName: 'shakeIntensity', value: params.shakeIntensity }, + { channelName: 'freqRatio1', value: params.freqRatio1 }, + { channelName: 'freqRatio2', value: params.freqRatio2 }, + { channelName: 'freqRatio3', value: params.freqRatio3 }, + { channelName: 'attack', value: params.attack }, + { channelName: 'release', value: params.release }, + { channelName: 'amplitude', value: params.amplitude }, + { channelName: 'delayTime1', value: params.delayTime1 }, + { channelName: 'delayTime2', value: params.delayTime2 }, + { channelName: 'delayTime3', value: params.delayTime3 }, + { channelName: 'delayTime4', value: params.delayTime4 }, + { channelName: 'delayGain1', value: params.delayGain1 }, + { channelName: 'delayGain2', value: params.delayGain2 }, + { channelName: 'delayGain3', value: params.delayGain3 }, + { channelName: 'delayGain4', value: params.delayGain4 }, + { channelName: 'feedbackDelayTime', value: params.feedbackDelayTime }, + { channelName: 'feedbackAmount', value: params.feedbackAmount }, + { channelName: 'modDepth', value: params.modDepth }, + { channelName: 'modRate', value: params.modRate }, + { channelName: 'stereoSpread', value: params.stereoSpread }, + { channelName: 'combDelayTime', value: params.combDelayTime }, + { channelName: 'combFeedback', value: params.combFeedback }, + { channelName: 'reverseDelayTime', value: params.reverseDelayTime }, + { channelName: 'reverseGain', value: params.reverseGain }, + { channelName: 'cascadeDelay1', value: params.cascadeDelay1 }, + { channelName: 'cascadeDelay2', value: params.cascadeDelay2 }, + { channelName: 'cascadeFeedback', value: params.cascadeFeedback }, + ]; + } + + randomParams(pitchLock?: PitchLock): DripwaterParams { + const baseFreqChoices = [55, 82.4, 110, 146.8, 220, 293.7, 440, 587.3, 880]; + const baseFreq = pitchLock?.enabled + ? pitchLock.frequency + : this.randomChoice(baseFreqChoices) * this.randomRange(0.95, 1.05); + + return { + baseFreq, + dropRate: this.randomRange(2, 40), + numElements: this.randomInt(4, 48), + damping: this.randomRange(0.1, 0.85), + shakeIntensity: this.randomRange(0.2, 0.9), + freqRatio1: this.randomRange(1.0, 3.5), + freqRatio2: this.randomRange(2.5, 7.0), + freqRatio3: this.randomRange(5.0, 14.0), + attack: this.randomRange(0.001, 0.04), + release: this.randomRange(0.15, 0.9), + amplitude: this.randomRange(0.6, 0.95), + delayTime1: this.randomRange(0.1, 0.3), + delayTime2: this.randomRange(0.25, 0.5), + delayTime3: this.randomRange(0.45, 0.7), + delayTime4: this.randomRange(0.65, 0.95), + delayGain1: this.randomRange(0.4, 0.7), + delayGain2: this.randomRange(0.3, 0.55), + delayGain3: this.randomRange(0.2, 0.4), + delayGain4: this.randomRange(0.1, 0.25), + feedbackDelayTime: this.randomRange(0.15, 0.5), + feedbackAmount: this.randomRange(0.3, 0.7), + modDepth: this.randomRange(0.02, 0.12), + modRate: this.randomRange(0.1, 3.0), + stereoSpread: this.randomRange(0.85, 1.15), + combDelayTime: this.randomRange(0.05, 0.25), + combFeedback: this.randomRange(0.4, 0.8), + reverseDelayTime: this.randomRange(0.3, 0.8), + reverseGain: this.randomRange(0.15, 0.4), + cascadeDelay1: this.randomRange(0.2, 0.45), + cascadeDelay2: this.randomRange(0.35, 0.65), + cascadeFeedback: this.randomRange(0.3, 0.65), + }; + } + + mutateParams( + params: DripwaterParams, + mutationAmount: number = 0.15, + pitchLock?: PitchLock + ): DripwaterParams { + const baseFreq = pitchLock?.enabled ? pitchLock.frequency : params.baseFreq; + + return { + baseFreq, + dropRate: this.mutateValue(params.dropRate, mutationAmount, 1, 50), + numElements: Math.round(this.mutateValue(params.numElements, mutationAmount, 2, 64)), + damping: this.mutateValue(params.damping, mutationAmount, 0.0, 0.95), + shakeIntensity: this.mutateValue(params.shakeIntensity, mutationAmount, 0.1, 1.0), + freqRatio1: this.mutateValue(params.freqRatio1, mutationAmount, 1.0, 4.0), + freqRatio2: this.mutateValue(params.freqRatio2, mutationAmount, 2.0, 8.0), + freqRatio3: this.mutateValue(params.freqRatio3, mutationAmount, 4.0, 16.0), + attack: this.mutateValue(params.attack, mutationAmount, 0.001, 0.05), + release: this.mutateValue(params.release, mutationAmount, 0.1, 0.95), + amplitude: this.mutateValue(params.amplitude, mutationAmount, 0.5, 1.0), + delayTime1: this.mutateValue(params.delayTime1, mutationAmount, 0.05, 0.35), + delayTime2: this.mutateValue(params.delayTime2, mutationAmount, 0.2, 0.6), + delayTime3: this.mutateValue(params.delayTime3, mutationAmount, 0.4, 0.8), + delayTime4: this.mutateValue(params.delayTime4, mutationAmount, 0.6, 0.98), + delayGain1: this.mutateValue(params.delayGain1, mutationAmount, 0.3, 0.8), + delayGain2: this.mutateValue(params.delayGain2, mutationAmount, 0.2, 0.65), + delayGain3: this.mutateValue(params.delayGain3, mutationAmount, 0.1, 0.5), + delayGain4: this.mutateValue(params.delayGain4, mutationAmount, 0.05, 0.35), + feedbackDelayTime: this.mutateValue(params.feedbackDelayTime, mutationAmount, 0.1, 0.6), + feedbackAmount: this.mutateValue(params.feedbackAmount, mutationAmount, 0.2, 0.8), + modDepth: this.mutateValue(params.modDepth, mutationAmount, 0.01, 0.15), + modRate: this.mutateValue(params.modRate, mutationAmount, 0.05, 4.0), + stereoSpread: this.mutateValue(params.stereoSpread, mutationAmount, 0.7, 1.3), + combDelayTime: this.mutateValue(params.combDelayTime, mutationAmount, 0.03, 0.3), + combFeedback: this.mutateValue(params.combFeedback, mutationAmount, 0.3, 0.85), + reverseDelayTime: this.mutateValue(params.reverseDelayTime, mutationAmount, 0.2, 0.9), + reverseGain: this.mutateValue(params.reverseGain, mutationAmount, 0.1, 0.5), + cascadeDelay1: this.mutateValue(params.cascadeDelay1, mutationAmount, 0.15, 0.5), + cascadeDelay2: this.mutateValue(params.cascadeDelay2, mutationAmount, 0.3, 0.7), + cascadeFeedback: this.mutateValue(params.cascadeFeedback, mutationAmount, 0.2, 0.75), + }; + } +} diff --git a/src/lib/audio/engines/Form1.ts b/src/lib/audio/engines/Form1.ts new file mode 100644 index 0000000..c95ac12 --- /dev/null +++ b/src/lib/audio/engines/Form1.ts @@ -0,0 +1,296 @@ +import { CsoundEngine, type CsoundParameter } from './base/CsoundEngine'; +import type { PitchLock } from './base/SynthEngine'; + +interface Form1Params { + frequency: number; + vibratoRate: number; + vibratoDepth: number; + formant1Freq: number; + formant1BW: number; + formant1Amp: number; + formant2Freq: number; + formant2BW: number; + formant2Amp: number; + formant3Freq: number; + formant3BW: number; + formant3Amp: number; + formant4Freq: number; + formant4BW: number; + formant4Amp: number; + formant5Freq: number; + formant5BW: number; + formant5Amp: number; + attack: number; + decay: number; + sustain: number; + release: number; +} + +export class Form1 extends CsoundEngine { + getName(): string { + return 'Throat'; + } + + getDescription(): string { + return 'Multi-voice formant vocal synthesizer with deep sub-bass and wide stereo field'; + } + + getType(): 'generative' | 'sample' | 'input' { + return 'generative'; + } + + getCategory(): 'Additive' | 'Subtractive' | 'FM' | 'Percussion' | 'Noise' | 'Physical' | 'Modulation' | 'Experimental' | 'Utility' { + return 'Physical'; + } + + getOrchestra(): string { + return ` +instr 1 + ifreq = chnget("frequency") + iamp = 0.3 + ivibratoRate = chnget("vibratoRate") + ivibratoDepth = chnget("vibratoDepth") + + if1freq = chnget("formant1Freq") + if1bw = chnget("formant1BW") + if1amp = chnget("formant1Amp") + if2freq = chnget("formant2Freq") + if2bw = chnget("formant2BW") + if2amp = chnget("formant2Amp") + if3freq = chnget("formant3Freq") + if3bw = chnget("formant3BW") + if3amp = chnget("formant3Amp") + if4freq = chnget("formant4Freq") + if4bw = chnget("formant4BW") + if4amp = chnget("formant4Amp") + if5freq = chnget("formant5Freq") + if5bw = chnget("formant5BW") + if5amp = chnget("formant5Amp") + + iatt = chnget("attack") * p3 + idec = chnget("decay") * p3 + isus = chnget("sustain") + irel = chnget("release") * p3 + + ; Amplitude envelope + aenv = madsr(iatt, idec, isus, irel) + + ; Dual vibrato LFOs for stereo width + kvibL = lfo(ivibratoDepth, ivibratoRate) + kvibR = lfo(ivibratoDepth, ivibratoRate * 1.03) + + ; === LEFT CHANNEL VOICES === + ; Voice 1L: Main voice + kfreq1L = ifreq * (1 + kvibL) + inumharm1L = int(sr / (2 * ifreq)) + asig1L = buzz(1, kfreq1L, inumharm1L, -1) + + ; Voice 2L: Slightly detuned (0.987x for beating) + kfreq2L = ifreq * 0.987 * (1 + kvibL * 1.02) + inumharm2L = int(sr / (2 * ifreq * 0.987)) + asig2L = buzz(0.85, kfreq2L, inumharm2L, -1) + + ; Voice 3L: Slightly detuned up (1.013x for beating) + kfreq3L = ifreq * 1.013 * (1 + kvibL * 0.98) + inumharm3L = int(sr / (2 * ifreq * 1.013)) + asig3L = buzz(0.8, kfreq3L, inumharm3L, -1) + + ; Voice 4L: Octave down + kfreq4L = ifreq * 0.5 * (1 + kvibL * 1.04) + inumharm4L = int(sr / (2 * ifreq * 0.5)) + asig4L = buzz(0.65, kfreq4L, inumharm4L, -1) + + ; Voice 5L: Octave up + kfreq5L = ifreq * 2 * (1 + kvibL * 0.96) + inumharm5L = int(sr / (2 * ifreq * 2)) + asig5L = buzz(0.45, kfreq5L, inumharm5L, -1) + + ; === RIGHT CHANNEL VOICES (different detuning) === + ; Voice 1R: Main voice slightly different + kfreq1R = ifreq * 1.002 * (1 + kvibR) + inumharm1R = int(sr / (2 * ifreq * 1.002)) + asig1R = buzz(1, kfreq1R, inumharm1R, -1) + + ; Voice 2R: Different detuning (0.993x) + kfreq2R = ifreq * 0.993 * (1 + kvibR * 1.01) + inumharm2R = int(sr / (2 * ifreq * 0.993)) + asig2R = buzz(0.85, kfreq2R, inumharm2R, -1) + + ; Voice 3R: Different detuning up (1.007x) + kfreq3R = ifreq * 1.007 * (1 + kvibR * 0.99) + inumharm3R = int(sr / (2 * ifreq * 1.007)) + asig3R = buzz(0.8, kfreq3R, inumharm3R, -1) + + ; Voice 4R: Octave down different phase + kfreq4R = ifreq * 0.501 * (1 + kvibR * 1.06) + inumharm4R = int(sr / (2 * ifreq * 0.501)) + asig4R = buzz(0.6, kfreq4R, inumharm4R, -1) + + ; Voice 5R: Octave up different phase + kfreq5R = ifreq * 1.998 * (1 + kvibR * 0.94) + inumharm5R = int(sr / (2 * ifreq * 1.998)) + asig5R = buzz(0.4, kfreq5R, inumharm5R, -1) + + ; Sub-bass: Buzz oscillator one octave below with harmonic content + kfreqSubL = ifreq * 0.5 * (1 + kvibL * 0.5) + kfreqSubR = ifreq * 0.502 * (1 + kvibR * 0.5) + inumharmSubL = int(sr / (2 * ifreq * 0.5)) + inumharmSubR = int(sr / (2 * ifreq * 0.502)) + aSubRawL = buzz(1, kfreqSubL, inumharmSubL, -1) + aSubRawR = buzz(1, kfreqSubR, inumharmSubR, -1) + + ; Apply very wide formants for vocal character with depth + aSubF1L = butterbp(aSubRawL, if1freq * 0.6, if1bw * 8) + aSubF2L = butterbp(aSubRawL, if2freq * 0.7, if2bw * 10) + aSubF3L = butterbp(aSubRawL, if3freq * 0.65, if3bw * 9) + aSubF1R = butterbp(aSubRawR, if1freq * 0.62, if1bw * 8.5) + aSubF2R = butterbp(aSubRawR, if2freq * 0.68, if2bw * 10.5) + aSubF3R = butterbp(aSubRawR, if3freq * 0.67, if3bw * 9.5) + + ; Mix sub with formants for depth and complexity + aSubMixL = (aSubRawL * 0.3) + (aSubF1L * 0.3) + (aSubF2L * 0.25) + (aSubF3L * 0.15) + aSubMixR = (aSubRawR * 0.3) + (aSubF1R * 0.3) + (aSubF2R * 0.25) + (aSubF3R * 0.15) + + ; Add gentle low-pass for warmth and smooth out harsh frequencies + aSubL = butterlp(aSubMixL, 1200) + aSubR = butterlp(aSubMixR, 1250) + + ; Scale sub-bass + aSubL = aSubL * 0.8 + aSubR = aSubR * 0.8 + + ; Mix voices per channel (sub added separately later) + asigMixL = asig1L + asig2L + asig3L + asig4L + asig5L + asigMixR = asig1R + asig2R + asig3R + asig4R + asig5R + + ; === LEFT CHANNEL FORMANTS === + ; Main formants + a1L = butterbp(asigMixL * if1amp, if1freq, if1bw) + a2L = butterbp(asigMixL * if2amp, if2freq, if2bw) + a3L = butterbp(asigMixL * if3amp, if3freq, if3bw) + a4L = butterbp(asigMixL * if4amp, if4freq, if4bw) + a5L = butterbp(asigMixL * if5amp, if5freq, if5bw) + + ; Additional formant layers + a1bL = butterbp(asigMixL * if1amp * 0.35, if1freq * 1.025, if1bw * 1.12) + a2bL = butterbp(asigMixL * if2amp * 0.3, if2freq * 0.975, if2bw * 1.18) + a3bL = butterbp(asigMixL * if3amp * 0.25, if3freq * 1.035, if3bw * 1.1) + + ; === RIGHT CHANNEL FORMANTS (different shifts) === + ; Main formants + a1R = butterbp(asigMixR * if1amp, if1freq * 1.005, if1bw * 1.02) + a2R = butterbp(asigMixR * if2amp, if2freq * 0.995, if2bw * 1.03) + a3R = butterbp(asigMixR * if3amp, if3freq * 1.008, if3bw * 1.01) + a4R = butterbp(asigMixR * if4amp, if4freq * 0.997, if4bw * 1.04) + a5R = butterbp(asigMixR * if5amp, if5freq * 1.003, if5bw * 1.02) + + ; Additional formant layers + a1bR = butterbp(asigMixR * if1amp * 0.3, if1freq * 0.98, if1bw * 1.15) + a2bR = butterbp(asigMixR * if2amp * 0.28, if2freq * 1.022, if2bw * 1.2) + a3bR = butterbp(asigMixR * if3amp * 0.22, if3freq * 0.97, if3bw * 1.12) + + ; Combine formants per channel + asigOutL = a1L + a2L + a3L + a4L + a5L + a1bL + a2bL + a3bL + asigOutR = a1R + a2R + a3R + a4R + a5R + a1bR + a2bR + a3bR + + ; Apply envelope and level + asigFormantL = asigOutL * aenv * iamp * 4 + asigFormantR = asigOutR * aenv * iamp * 4 + + ; Add sub-bass directly (bypassing formants) + aL = asigFormantL + (aSubL * aenv) + aR = asigFormantR + (aSubR * aenv) + + outs aL, aR +endin +`; + } + + getParametersForCsound(params: Form1Params): CsoundParameter[] { + return [ + { channelName: 'frequency', value: params.frequency }, + { channelName: 'vibratoRate', value: params.vibratoRate }, + { channelName: 'vibratoDepth', value: params.vibratoDepth }, + { channelName: 'formant1Freq', value: params.formant1Freq }, + { channelName: 'formant1BW', value: params.formant1BW }, + { channelName: 'formant1Amp', value: params.formant1Amp }, + { channelName: 'formant2Freq', value: params.formant2Freq }, + { channelName: 'formant2BW', value: params.formant2BW }, + { channelName: 'formant2Amp', value: params.formant2Amp }, + { channelName: 'formant3Freq', value: params.formant3Freq }, + { channelName: 'formant3BW', value: params.formant3BW }, + { channelName: 'formant3Amp', value: params.formant3Amp }, + { channelName: 'formant4Freq', value: params.formant4Freq }, + { channelName: 'formant4BW', value: params.formant4BW }, + { channelName: 'formant4Amp', value: params.formant4Amp }, + { channelName: 'formant5Freq', value: params.formant5Freq }, + { channelName: 'formant5BW', value: params.formant5BW }, + { channelName: 'formant5Amp', value: params.formant5Amp }, + { channelName: 'attack', value: params.attack }, + { channelName: 'decay', value: params.decay }, + { channelName: 'sustain', value: params.sustain }, + { channelName: 'release', value: params.release } + ]; + } + + randomParams(pitchLock?: PitchLock): Form1Params { + const frequency = pitchLock?.enabled ? pitchLock.frequency : 55 * Math.pow(2, Math.random() * 4); + + return { + frequency, + vibratoRate: 2 + Math.random() * 6, + vibratoDepth: 0.001 + Math.random() * 0.008, + formant1Freq: 400 + Math.random() * 800, + formant1BW: 40 + Math.random() * 100, + formant1Amp: 0.8 + Math.random() * 0.2, + formant2Freq: 800 + Math.random() * 600, + formant2BW: 50 + Math.random() * 100, + formant2Amp: 0.4 + Math.random() * 0.4, + formant3Freq: 2000 + Math.random() * 1500, + formant3BW: 80 + Math.random() * 120, + formant3Amp: 0.05 + Math.random() * 0.15, + formant4Freq: 3000 + Math.random() * 1500, + formant4BW: 100 + Math.random() * 100, + formant4Amp: 0.1 + Math.random() * 0.2, + formant5Freq: 4000 + Math.random() * 1500, + formant5BW: 100 + Math.random() * 150, + formant5Amp: 0.01 + Math.random() * 0.1, + attack: Math.random() * 0.15, + decay: 0.05 + Math.random() * 0.3, + sustain: 0.3 + Math.random() * 0.5, + release: 0.05 + Math.random() * 0.4 + }; + } + + mutateParams(params: Form1Params, mutationAmount = 0.2, pitchLock?: PitchLock): Form1Params { + const mutate = (value: number, min: number, max: number) => { + const change = (Math.random() - 0.5) * 2 * mutationAmount * (max - min); + return Math.max(min, Math.min(max, value + change)); + }; + + return { + frequency: pitchLock?.enabled ? pitchLock.frequency : mutate(params.frequency, 55, 55 * Math.pow(2, 4)), + vibratoRate: mutate(params.vibratoRate, 2, 8), + vibratoDepth: mutate(params.vibratoDepth, 0.001, 0.009), + formant1Freq: mutate(params.formant1Freq, 400, 1200), + formant1BW: mutate(params.formant1BW, 40, 140), + formant1Amp: mutate(params.formant1Amp, 0.8, 1.0), + formant2Freq: mutate(params.formant2Freq, 800, 1400), + formant2BW: mutate(params.formant2BW, 50, 150), + formant2Amp: mutate(params.formant2Amp, 0.4, 0.8), + formant3Freq: mutate(params.formant3Freq, 2000, 3500), + formant3BW: mutate(params.formant3BW, 80, 200), + formant3Amp: mutate(params.formant3Amp, 0.05, 0.2), + formant4Freq: mutate(params.formant4Freq, 3000, 4500), + formant4BW: mutate(params.formant4BW, 100, 200), + formant4Amp: mutate(params.formant4Amp, 0.1, 0.3), + formant5Freq: mutate(params.formant5Freq, 4000, 5500), + formant5BW: mutate(params.formant5BW, 100, 250), + formant5Amp: mutate(params.formant5Amp, 0.01, 0.11), + attack: mutate(params.attack, 0, 0.15), + decay: mutate(params.decay, 0.05, 0.35), + sustain: mutate(params.sustain, 0.3, 0.8), + release: mutate(params.release, 0.05, 0.45) + }; + } +} diff --git a/src/lib/audio/engines/LaserSweep.ts b/src/lib/audio/engines/LaserSweep.ts new file mode 100644 index 0000000..3acd341 --- /dev/null +++ b/src/lib/audio/engines/LaserSweep.ts @@ -0,0 +1,244 @@ +import { CsoundEngine, type CsoundParameter } from './base/CsoundEngine'; + +interface LaserSweepParams { + startFreq: number; + endFreq: number; + sweepCurve: number; + resonance: number; + brightness: number; + delayTime: number; + delayFeedback: number; + reverbMix: number; + reverbSize: number; + noiseAmount: number; + decayShape: number; + waveformMix: number; + mainWaveType: number; + secondWaveType: number; + pulseWidth: number; + filterTracking: number; + subOctave: number; + subOctaveLevel: number; +} + +export class LaserSweep extends CsoundEngine { + getName(): string { + return 'Laser Sweep'; + } + + getDescription(): string { + return 'Sweeping laser beam with delay and reverb'; + } + + getType(): 'generative' | 'sample' | 'input' { + return 'generative'; + } + + getCategory() { + return 'Experimental' as const; + } + + getOrchestra(): string { + return ` +sr = 44100 +ksmps = 64 +nchnls = 2 +0dbfs = 1 + +instr 1 + iDur = p3 + + iStartFreq chnget "startFreq" + iEndFreq chnget "endFreq" + iSweepCurve chnget "sweepCurve" + iResonance chnget "resonance" + iBrightness chnget "brightness" + iDelayTime chnget "delayTime" + iDelayFeedback chnget "delayFeedback" + iReverbMix chnget "reverbMix" + iReverbSize chnget "reverbSize" + iNoiseAmount chnget "noiseAmount" + iDecayShape chnget "decayShape" + iWaveformMix chnget "waveformMix" + iMainWaveType chnget "mainWaveType" + iSecondWaveType chnget "secondWaveType" + iPulseWidth chnget "pulseWidth" + iFilterTracking chnget "filterTracking" + iSubOctave chnget "subOctave" + iSubOctaveLevel chnget "subOctaveLevel" + + if iDecayShape < 0.33 then + kEnv expseg 1, iDur * 0.85, 1, iDur * 0.15, 0.001 + elseif iDecayShape < 0.67 then + kEnv linseg 1, iDur * 0.85, 1, iDur * 0.15, 0 + else + kEnv expseg 1, iDur * 0.8, 1, iDur * 0.18, 0.001 + endif + + kFreq expseg iStartFreq, iDur * iSweepCurve, iEndFreq + kFreqLimited limit kFreq, 40, 16000 + + if iMainWaveType < 0.2 then + aMain vco2 0.6, kFreqLimited, 10 + elseif iMainWaveType < 0.4 then + aMain vco2 0.6, kFreqLimited, 0 + elseif iMainWaveType < 0.6 then + aMain vco2 0.6, kFreqLimited, 2, iPulseWidth + elseif iMainWaveType < 0.8 then + aMain vco2 0.6, kFreqLimited, 12 + else + aMain oscili 0.6, kFreqLimited + endif + + if iSecondWaveType < 0.2 then + aSecond vco2 0.5, kFreqLimited, 2, iPulseWidth + elseif iSecondWaveType < 0.4 then + aSecond vco2 0.5, kFreqLimited, 0 + elseif iSecondWaveType < 0.6 then + aSecond vco2 0.5, kFreqLimited, 12 + elseif iSecondWaveType < 0.8 then + aSecond vco2 0.5, kFreqLimited, 10 + else + aSecond oscili 0.5, kFreqLimited * 1.5 + endif + + aNoise rand 0.3 + kNoiseFiltFreq limit kFreqLimited * 0.5, 50, 12000 + aNoise butterbp aNoise, kNoiseFiltFreq, kNoiseFiltFreq * 0.5 + + if iSubOctaveLevel > 0.01 then + if iSubOctave < 0.5 then + kSubFreq = kFreqLimited * 0.5 + else + kSubFreq = kFreqLimited * 0.25 + endif + aSub oscili iSubOctaveLevel * 0.7, kSubFreq + else + aSub = 0 + endif + + aOsc = aMain + aSecond * iWaveformMix + aSecond * iBrightness * 0.3 + aNoise * iNoiseAmount + aSub + + kFilterFreq = kFreqLimited * (1 + (1 - iFilterTracking) * 2) + kFilterFreq limit kFilterFreq, 50, 15000 + + kBandwidth = kFilterFreq * (0.1 + iResonance * 0.3) + aFilt butterbp aOsc, kFilterFreq, kBandwidth + + aFilt = aFilt * kEnv * 0.8 + + if iDelayTime > 0.01 then + aDlyL vdelay3 aFilt, iDelayTime * 1000, 2000 + aDlyR vdelay3 aFilt, iDelayTime * 1000 * 1.1, 2000 + + aDlyL = aDlyL * iDelayFeedback * kEnv + aDlyR = aDlyR * iDelayFeedback * kEnv + else + aDlyL = 0 + aDlyR = 0 + endif + + if iReverbMix > 0.01 then + aWetL, aWetR reverbsc aFilt + aDlyL * 0.5, aFilt + aDlyR * 0.5, iReverbSize, 8000 + else + aWetL = 0 + aWetR = 0 + endif + + aOutL = aFilt + aDlyL * 0.4 + aWetL * iReverbMix + aOutR = aFilt + aDlyR * 0.4 + aWetR * iReverbMix + + outs aOutL, aOutR +endin +`; + } + + getParametersForCsound(params: LaserSweepParams): CsoundParameter[] { + return [ + { channelName: 'startFreq', value: params.startFreq }, + { channelName: 'endFreq', value: params.endFreq }, + { channelName: 'sweepCurve', value: params.sweepCurve }, + { channelName: 'resonance', value: params.resonance }, + { channelName: 'brightness', value: params.brightness }, + { channelName: 'delayTime', value: params.delayTime }, + { channelName: 'delayFeedback', value: params.delayFeedback }, + { channelName: 'reverbMix', value: params.reverbMix }, + { channelName: 'reverbSize', value: params.reverbSize }, + { channelName: 'noiseAmount', value: params.noiseAmount }, + { channelName: 'decayShape', value: params.decayShape }, + { channelName: 'waveformMix', value: params.waveformMix }, + { channelName: 'mainWaveType', value: params.mainWaveType }, + { channelName: 'secondWaveType', value: params.secondWaveType }, + { channelName: 'pulseWidth', value: params.pulseWidth }, + { channelName: 'filterTracking', value: params.filterTracking }, + { channelName: 'subOctave', value: params.subOctave }, + { channelName: 'subOctaveLevel', value: params.subOctaveLevel } + ]; + } + + randomParams(pitchLock?: number): LaserSweepParams { + const isUpward = Math.random() > 0.3; + + const startFreq = isUpward + ? 60 + Math.random() * 200 + : 1000 + Math.random() * 10000; + + const endFreq = isUpward + ? 1000 + Math.random() * 10000 + : 60 + Math.random() * 200; + + return { + startFreq, + endFreq, + sweepCurve: 0.2 + Math.random() * 0.7, + resonance: 0.2 + Math.random() * 0.7, + brightness: 0.1 + Math.random() * 0.8, + delayTime: Math.random() < 0.3 ? 0 : 0.05 + Math.random() * 0.5, + delayFeedback: 0.1 + Math.random() * 0.7, + reverbMix: Math.random() < 0.2 ? 0 : 0.1 + Math.random() * 0.5, + reverbSize: 0.4 + Math.random() * 0.5, + noiseAmount: 0.05 + Math.random() * 0.5, + decayShape: Math.random(), + waveformMix: 0.3 + Math.random() * 0.7, + mainWaveType: Math.random(), + secondWaveType: Math.random(), + pulseWidth: 0.1 + Math.random() * 0.8, + filterTracking: 0.3 + Math.random() * 0.7, + subOctave: Math.random(), + subOctaveLevel: Math.random() < 0.4 ? 0 : 0.3 + Math.random() * 0.7 + }; + } + + mutateParams( + params: LaserSweepParams, + mutationAmount: number = 0.3, + pitchLock?: number + ): LaserSweepParams { + const mutate = (value: number, min: number, max: number): number => { + const range = max - min; + const change = (Math.random() - 0.5) * range * mutationAmount; + return Math.max(min, Math.min(max, value + change)); + }; + + return { + startFreq: mutate(params.startFreq, 40, 12000), + endFreq: mutate(params.endFreq, 40, 12000), + sweepCurve: mutate(params.sweepCurve, 0.1, 0.95), + resonance: mutate(params.resonance, 0.1, 1.0), + brightness: mutate(params.brightness, 0.0, 1.0), + delayTime: mutate(params.delayTime, 0.0, 0.6), + delayFeedback: mutate(params.delayFeedback, 0.0, 0.85), + reverbMix: mutate(params.reverbMix, 0.0, 0.7), + reverbSize: mutate(params.reverbSize, 0.3, 0.95), + noiseAmount: mutate(params.noiseAmount, 0.0, 0.7), + decayShape: mutate(params.decayShape, 0.0, 1.0), + waveformMix: mutate(params.waveformMix, 0.0, 1.0), + mainWaveType: mutate(params.mainWaveType, 0.0, 1.0), + secondWaveType: mutate(params.secondWaveType, 0.0, 1.0), + pulseWidth: mutate(params.pulseWidth, 0.05, 0.95), + filterTracking: mutate(params.filterTracking, 0.0, 1.0), + subOctave: mutate(params.subOctave, 0.0, 1.0), + subOctaveLevel: mutate(params.subOctaveLevel, 0.0, 1.0) + }; + } +} diff --git a/src/lib/audio/engines/Squine1.ts b/src/lib/audio/engines/Squine1.ts new file mode 100644 index 0000000..7d190a4 --- /dev/null +++ b/src/lib/audio/engines/Squine1.ts @@ -0,0 +1,149 @@ +import { CsoundEngine, type CsoundParameter } from './base/CsoundEngine'; +import type { PitchLock } from './base/SynthEngine'; + +interface Squine1Params { + frequency: number; + shapeAmount1: number; + shapeAmount2: number; + modulationDepth1: number; + modulationDepth2: number; + osc2Detune: number; + osc2Level: number; + hpfCutoff: number; + attack: number; + decay: number; + sustain: number; + release: number; +} + +export class Squine1 extends CsoundEngine { + getName(): string { + return 'Squine1'; + } + + getDescription(): string { + return 'Squinewave synthesizer with waveshaping and dual oscillators'; + } + + getType(): 'generative' | 'sample' | 'input' { + return 'generative'; + } + + getCategory(): 'Additive' | 'Subtractive' | 'FM' | 'Percussion' | 'Noise' | 'Physical' | 'Modulation' | 'Experimental' | 'Utility' { + return 'Subtractive'; + } + + getOrchestra(): string { + return ` +instr 1 + ifreq = chnget("frequency") + iamp = 0.5 + ishape1 = chnget("shapeAmount1") + ishape2 = chnget("shapeAmount2") + imod1 = chnget("modulationDepth1") + imod2 = chnget("modulationDepth2") + idetune = chnget("osc2Detune") + iosc2Level = chnget("osc2Level") + ihpf = chnget("hpfCutoff") + iatt = chnget("attack") * p3 + idec = chnget("decay") * p3 + isus = chnget("sustain") + irel = chnget("release") * p3 + + ; First oscillator with waveshaping (squine approximation) + aosc1 = poscil(1, ifreq) + + ; Create dynamic shape envelope for osc1 + kshape1 = expseg(ishape1, p3, ishape1 * imod1) + + ; Waveshaping: blend between sine and square-like + asig1 = tanh(aosc1 * kshape1 * 5) / tanh(kshape1 * 5) + + ; Second oscillator slightly detuned + aosc2 = poscil(1, ifreq * (1 + idetune)) + + ; Create dynamic shape envelope for osc2 + kshape2 = expseg(ishape2, p3, ishape2 * imod2) + + ; Waveshaping for second oscillator + asig2 = tanh(aosc2 * kshape2 * 5) / tanh(kshape2 * 5) + + ; Mix oscillators + asig = asig1 + (asig2 * iosc2Level) + + ; High-pass filter to remove DC and rumble + kcutoff = max(ihpf, 20) + asig = butterhp(asig, kcutoff) + + ; Amplitude envelope + aenv = madsr(iatt, idec, isus, irel) + asig = asig * aenv * iamp + + ; DC blocker + asig = dcblock2(asig) + + outs asig, asig +endin +`; + } + + getParametersForCsound(params: Squine1Params): CsoundParameter[] { + return [ + { channelName: 'frequency', value: params.frequency }, + { channelName: 'shapeAmount1', value: params.shapeAmount1 }, + { channelName: 'shapeAmount2', value: params.shapeAmount2 }, + { channelName: 'modulationDepth1', value: params.modulationDepth1 }, + { channelName: 'modulationDepth2', value: params.modulationDepth2 }, + { channelName: 'osc2Detune', value: params.osc2Detune }, + { channelName: 'osc2Level', value: params.osc2Level }, + { channelName: 'hpfCutoff', value: params.hpfCutoff }, + { channelName: 'attack', value: params.attack }, + { channelName: 'decay', value: params.decay }, + { channelName: 'sustain', value: params.sustain }, + { channelName: 'release', value: params.release } + ]; + } + + randomParams(pitchLock?: PitchLock): Squine1Params { + const frequency = pitchLock?.enabled ? pitchLock.frequency : 55 * Math.pow(2, Math.random() * 5); + + return { + frequency, + shapeAmount1: 0.2 + Math.random() * 0.8, + shapeAmount2: 0.2 + Math.random() * 0.8, + modulationDepth1: 0.05 + Math.random() * 0.7, + modulationDepth2: 0.05 + Math.random() * 0.7, + osc2Detune: 0.0001 + Math.random() * 0.005, + osc2Level: 0.01 + Math.random() * 0.15, + hpfCutoff: frequency * 0.5 + Math.random() * frequency, + attack: 0.001 + Math.random() * 0.05, + decay: 0.05 + Math.random() * 0.3, + sustain: 0.2 + Math.random() * 0.6, + release: 0.01 + Math.random() * 0.3 + }; + } + + mutateParams(params: Squine1Params, mutationAmount = 0.2, pitchLock?: PitchLock): Squine1Params { + const mutate = (value: number, min: number, max: number) => { + const change = (Math.random() - 0.5) * 2 * mutationAmount * (max - min); + return Math.max(min, Math.min(max, value + change)); + }; + + const newFreq = pitchLock?.enabled ? pitchLock.frequency : mutate(params.frequency, 55, 55 * Math.pow(2, 5)); + + return { + frequency: newFreq, + shapeAmount1: mutate(params.shapeAmount1, 0.2, 1.0), + shapeAmount2: mutate(params.shapeAmount2, 0.2, 1.0), + modulationDepth1: mutate(params.modulationDepth1, 0.05, 0.75), + modulationDepth2: mutate(params.modulationDepth2, 0.05, 0.75), + osc2Detune: mutate(params.osc2Detune, 0.0001, 0.0051), + osc2Level: mutate(params.osc2Level, 0.01, 0.16), + hpfCutoff: mutate(params.hpfCutoff, newFreq * 0.5, newFreq * 1.5), + attack: mutate(params.attack, 0.001, 0.051), + decay: mutate(params.decay, 0.05, 0.35), + sustain: mutate(params.sustain, 0.2, 0.8), + release: mutate(params.release, 0.01, 0.31) + }; + } +} diff --git a/src/lib/audio/engines/Sub8.ts b/src/lib/audio/engines/Sub8.ts new file mode 100644 index 0000000..ccdb972 --- /dev/null +++ b/src/lib/audio/engines/Sub8.ts @@ -0,0 +1,135 @@ +import { CsoundEngine, type CsoundParameter } from './base/CsoundEngine'; +import type { PitchLock } from './base/SynthEngine'; + +interface Sub8Params { + frequency: number; + filterCutoff: number; + filterEnvAmount: number; + resonance: number; + saturation: number; + osc1Level: number; + osc2Level: number; + osc3Level: number; + attack: number; + decay: number; + sustain: number; + release: number; +} + +export class Sub8 extends CsoundEngine { + getName(): string { + return 'Sub8'; + } + + getDescription(): string { + return 'Multi-oscillator subtractive synth with diode ladder filter and saturation'; + } + + getType(): 'generative' | 'sample' | 'input' { + return 'generative'; + } + + getCategory(): 'Additive' | 'Subtractive' | 'FM' | 'Percussion' | 'Noise' | 'Physical' | 'Modulation' | 'Experimental' | 'Utility' { + return 'Subtractive'; + } + + getOrchestra(): string { + return ` +instr 1 + ifreq = chnget("frequency") + iamp = 0.5 + ifilterCutoff = chnget("filterCutoff") + ifilterEnvAmount = chnget("filterEnvAmount") + ireso = chnget("resonance") + isat = chnget("saturation") + iosc1Level = chnget("osc1Level") + iosc2Level = chnget("osc2Level") + iosc3Level = chnget("osc3Level") + iatt = chnget("attack") * p3 + idec = chnget("decay") * p3 + isus = chnget("sustain") + irel = chnget("release") * p3 + + ; Three detuned oscillators + asig1 = vco2(iamp * iosc1Level, ifreq, 10) + asig2 = vco2(iamp * iosc2Level, ifreq * 2, 10) + asig3 = vco2(iamp * iosc3Level, ifreq * 3.5, 12) + asig = asig1 + asig2 + asig3 + + ; Saturation + asig = tanh(asig * isat) / tanh(isat) + + ; Filter envelope + aenv = madsr(iatt, idec, isus, irel) + kcutoff = ifilterCutoff + (aenv * ifilterEnvAmount) + kcutoff = limit(kcutoff, 20, 18000) + + ; Diode ladder filter + asig = diode_ladder(asig, kcutoff, ireso) + + ; Final amplitude envelope + asig = asig * aenv * 0.7 + + outs asig, asig +endin +`; + } + + getParametersForCsound(params: Sub8Params): CsoundParameter[] { + return [ + { channelName: 'frequency', value: params.frequency }, + { channelName: 'filterCutoff', value: params.filterCutoff }, + { channelName: 'filterEnvAmount', value: params.filterEnvAmount }, + { channelName: 'resonance', value: params.resonance }, + { channelName: 'saturation', value: params.saturation }, + { channelName: 'osc1Level', value: params.osc1Level }, + { channelName: 'osc2Level', value: params.osc2Level }, + { channelName: 'osc3Level', value: params.osc3Level }, + { channelName: 'attack', value: params.attack }, + { channelName: 'decay', value: params.decay }, + { channelName: 'sustain', value: params.sustain }, + { channelName: 'release', value: params.release } + ]; + } + + randomParams(pitchLock?: PitchLock): Sub8Params { + const frequency = pitchLock?.enabled ? pitchLock.frequency : 55 * Math.pow(2, Math.random() * 5); + + return { + frequency, + filterCutoff: 200 + Math.random() * 3800, + filterEnvAmount: Math.random() * 6000, + resonance: 0.5 + Math.random() * 14.5, + saturation: 1 + Math.random() * 9, + osc1Level: 0.5 + Math.random() * 0.5, + osc2Level: Math.random() * 0.5, + osc3Level: Math.random() * 0.3, + attack: Math.random() * 0.1, + decay: 0.05 + Math.random() * 0.3, + sustain: 0.2 + Math.random() * 0.6, + release: 0.05 + Math.random() * 0.4 + }; + } + + mutateParams(params: Sub8Params, mutationAmount = 0.2, pitchLock?: PitchLock): Sub8Params { + const mutate = (value: number, min: number, max: number) => { + const change = (Math.random() - 0.5) * 2 * mutationAmount * (max - min); + return Math.max(min, Math.min(max, value + change)); + }; + + return { + frequency: pitchLock?.enabled ? pitchLock.frequency : mutate(params.frequency, 55, 55 * Math.pow(2, 5)), + filterCutoff: mutate(params.filterCutoff, 200, 4000), + filterEnvAmount: mutate(params.filterEnvAmount, 0, 6000), + resonance: mutate(params.resonance, 0.5, 15), + saturation: mutate(params.saturation, 1, 10), + osc1Level: mutate(params.osc1Level, 0.5, 1), + osc2Level: mutate(params.osc2Level, 0, 0.5), + osc3Level: mutate(params.osc3Level, 0, 0.3), + attack: mutate(params.attack, 0, 0.1), + decay: mutate(params.decay, 0.05, 0.35), + sustain: mutate(params.sustain, 0.2, 0.8), + release: mutate(params.release, 0.05, 0.45) + }; + } +} diff --git a/src/lib/audio/engines/base/CsoundEngine.ts b/src/lib/audio/engines/base/CsoundEngine.ts index 274d1a0..db1c275 100644 --- a/src/lib/audio/engines/base/CsoundEngine.ts +++ b/src/lib/audio/engines/base/CsoundEngine.ts @@ -45,11 +45,17 @@ export abstract class CsoundEngine implements SynthEngine { await csound.terminateInstance(); - const leftChannel = new Float32Array(audioBuffer.leftChannel); - const rightChannel = new Float32Array(audioBuffer.rightChannel); + let leftChannel = new Float32Array(audioBuffer.leftChannel); + let rightChannel = new Float32Array(audioBuffer.rightChannel); + + this.removeDCOffset(leftChannel, rightChannel); + + const trimmed = this.trimToZeroCrossing(leftChannel, rightChannel, sampleRate); + leftChannel = trimmed.left; + rightChannel = trimmed.right; - // Apply short fade-in to prevent click at start this.applyFadeIn(leftChannel, rightChannel, sampleRate); + this.applyFadeOut(leftChannel, rightChannel, sampleRate); const peak = this.findPeak(leftChannel, rightChannel); if (peak > 0.001) { @@ -218,22 +224,95 @@ e } } + private removeDCOffset(leftChannel: Float32Array, rightChannel: Float32Array): void { + let leftSum = 0; + let rightSum = 0; + const length = leftChannel.length; + + for (let i = 0; i < length; i++) { + leftSum += leftChannel[i]; + rightSum += rightChannel[i]; + } + + const leftDC = leftSum / length; + const rightDC = rightSum / length; + + for (let i = 0; i < length; i++) { + leftChannel[i] -= leftDC; + rightChannel[i] -= rightDC; + } + } + + private trimToZeroCrossing( + leftChannel: Float32Array, + rightChannel: Float32Array, + sampleRate: number + ): { left: Float32Array; right: Float32Array } { + const maxSearchSamples = Math.min(Math.floor(sampleRate * 0.01), leftChannel.length); + let trimIndex = 0; + + for (let i = 1; i < maxSearchSamples; i++) { + const prevL = leftChannel[i - 1]; + const currL = leftChannel[i]; + const prevR = rightChannel[i - 1]; + const currR = rightChannel[i]; + + if ( + (prevL <= 0 && currL >= 0) || (prevL >= 0 && currL <= 0) || + (prevR <= 0 && currR >= 0) || (prevR >= 0 && currR <= 0) + ) { + trimIndex = i; + break; + } + } + + if (trimIndex > 0) { + const newLeft = new Float32Array(leftChannel.length - trimIndex); + const newRight = new Float32Array(rightChannel.length - trimIndex); + newLeft.set(leftChannel.subarray(trimIndex)); + newRight.set(rightChannel.subarray(trimIndex)); + return { left: newLeft, right: newRight }; + } + + return { left: leftChannel, right: rightChannel }; + } + private applyFadeIn( leftChannel: Float32Array, rightChannel: Float32Array, sampleRate: number ): void { - const fadeInMs = 5; // 5ms fade-in to prevent clicks + const fadeInMs = 5; const fadeSamples = Math.floor((fadeInMs / 1000) * sampleRate); const actualFadeSamples = Math.min(fadeSamples, leftChannel.length); for (let i = 0; i < actualFadeSamples; i++) { - const gain = i / actualFadeSamples; + const phase = i / actualFadeSamples; + const gain = 0.5 - 0.5 * Math.cos(phase * Math.PI); leftChannel[i] *= gain; rightChannel[i] *= gain; } } + private applyFadeOut( + leftChannel: Float32Array, + rightChannel: Float32Array, + sampleRate: number + ): void { + const fadeOutMs = 5; + const fadeSamples = Math.floor((fadeOutMs / 1000) * sampleRate); + const actualFadeSamples = Math.min(fadeSamples, leftChannel.length); + const startSample = leftChannel.length - actualFadeSamples; + + for (let i = 0; i < actualFadeSamples; i++) { + const sampleIndex = startSample + i; + const phase = i / actualFadeSamples; + const gain = 0.5 + 0.5 * Math.cos(phase * Math.PI); + leftChannel[sampleIndex] *= gain; + rightChannel[sampleIndex] *= gain; + } + } + protected randomRange(min: number, max: number): number { return min + Math.random() * (max - min); } diff --git a/src/lib/audio/engines/registry.ts b/src/lib/audio/engines/registry.ts index 84d3bb5..76e63e7 100644 --- a/src/lib/audio/engines/registry.ts +++ b/src/lib/audio/engines/registry.ts @@ -26,6 +26,11 @@ import { RingCymbal } from './RingCymbal'; import { AdditiveBass } from './AdditiveBass'; import { FeedbackSnare } from './FeedbackSnare'; import { CombResonator } from './CombResonator'; +import { LaserSweep } from './LaserSweep'; +import { Dripwater } from './Dripwater'; +import { Sub8 } from './Sub8'; +import { Form1 } from './Form1'; +import { Squine1 } from './Squine1'; export const engines: SynthEngine[] = [ new Sample(), @@ -55,4 +60,9 @@ export const engines: SynthEngine[] = [ new SubtractiveThreeOsc(), new CombResonator(), new MassiveAdditive(), + new LaserSweep(), + new Dripwater(), + new Sub8(), + new Form1(), + new Squine1(), ]; diff --git a/src/lib/audio/processors/ExpFadeIn.ts b/src/lib/audio/processors/ExpFadeIn.ts new file mode 100644 index 0000000..a7221d3 --- /dev/null +++ b/src/lib/audio/processors/ExpFadeIn.ts @@ -0,0 +1,27 @@ +import type { AudioProcessor } from './AudioProcessor'; + +export class ExpFadeIn implements AudioProcessor { + getName(): string { + return 'Fade In (Exp)'; + } + + getDescription(): string { + return 'Applies an exponential fade from silence to current level'; + } + + process(leftIn: Float32Array, rightIn: Float32Array): [Float32Array, Float32Array] { + const length = leftIn.length; + + const leftOut = new Float32Array(length); + const rightOut = new Float32Array(length); + + for (let i = 0; i < length; i++) { + const t = i / length; + const gain = 1.0 - Math.exp(-5.0 * t); + leftOut[i] = leftIn[i] * gain; + rightOut[i] = rightIn[i] * gain; + } + + return [leftOut, rightOut]; + } +} diff --git a/src/lib/audio/processors/ExpFadeOut.ts b/src/lib/audio/processors/ExpFadeOut.ts new file mode 100644 index 0000000..e9fb0d4 --- /dev/null +++ b/src/lib/audio/processors/ExpFadeOut.ts @@ -0,0 +1,27 @@ +import type { AudioProcessor } from './AudioProcessor'; + +export class ExpFadeOut implements AudioProcessor { + getName(): string { + return 'Fade Out (Exp)'; + } + + getDescription(): string { + return 'Applies an exponential fade from current level to silence'; + } + + process(leftIn: Float32Array, rightIn: Float32Array): [Float32Array, Float32Array] { + const length = leftIn.length; + + const leftOut = new Float32Array(length); + const rightOut = new Float32Array(length); + + for (let i = 0; i < length; i++) { + const t = i / length; + const gain = Math.exp(-5.0 * t); + leftOut[i] = leftIn[i] * gain; + rightOut[i] = rightIn[i] * gain; + } + + return [leftOut, rightOut]; + } +} diff --git a/src/lib/audio/processors/LinearFadeIn.ts b/src/lib/audio/processors/LinearFadeIn.ts new file mode 100644 index 0000000..c7c1c91 --- /dev/null +++ b/src/lib/audio/processors/LinearFadeIn.ts @@ -0,0 +1,26 @@ +import type { AudioProcessor } from './AudioProcessor'; + +export class LinearFadeIn implements AudioProcessor { + getName(): string { + return 'Fade In'; + } + + getDescription(): string { + return 'Applies a linear fade from silence to current level'; + } + + process(leftIn: Float32Array, rightIn: Float32Array): [Float32Array, Float32Array] { + const length = leftIn.length; + + const leftOut = new Float32Array(length); + const rightOut = new Float32Array(length); + + for (let i = 0; i < length; i++) { + const gain = i / length; + leftOut[i] = leftIn[i] * gain; + rightOut[i] = rightIn[i] * gain; + } + + return [leftOut, rightOut]; + } +} diff --git a/src/lib/audio/processors/LinearFadeOut.ts b/src/lib/audio/processors/LinearFadeOut.ts new file mode 100644 index 0000000..aa62262 --- /dev/null +++ b/src/lib/audio/processors/LinearFadeOut.ts @@ -0,0 +1,26 @@ +import type { AudioProcessor } from './AudioProcessor'; + +export class LinearFadeOut implements AudioProcessor { + getName(): string { + return 'Fade Out'; + } + + getDescription(): string { + return 'Applies a linear fade from current level to silence'; + } + + process(leftIn: Float32Array, rightIn: Float32Array): [Float32Array, Float32Array] { + const length = leftIn.length; + + const leftOut = new Float32Array(length); + const rightOut = new Float32Array(length); + + for (let i = 0; i < length; i++) { + const gain = 1.0 - i / length; + leftOut[i] = leftIn[i] * gain; + rightOut[i] = rightIn[i] * gain; + } + + return [leftOut, rightOut]; + } +} diff --git a/src/lib/audio/processors/registry.ts b/src/lib/audio/processors/registry.ts index 3e9f1ee..1d118b1 100644 --- a/src/lib/audio/processors/registry.ts +++ b/src/lib/audio/processors/registry.ts @@ -25,6 +25,10 @@ import { Waveshaper } from './Waveshaper'; import { DCOffsetRemover } from './DCOffsetRemover'; import { TrimSilence } from './TrimSilence'; import { Resonator } from './Resonator'; +import { LinearFadeOut } from './LinearFadeOut'; +import { ExpFadeOut } from './ExpFadeOut'; +import { LinearFadeIn } from './LinearFadeIn'; +import { ExpFadeIn } from './ExpFadeIn'; const processors: AudioProcessor[] = [ new SegmentShuffler(), @@ -53,6 +57,10 @@ const processors: AudioProcessor[] = [ new DCOffsetRemover(), new TrimSilence(), new Resonator(), + new LinearFadeOut(), + new ExpFadeOut(), + new LinearFadeIn(), + new ExpFadeIn(), ]; export function getRandomProcessor(): AudioProcessor {