diff --git a/README.md b/README.md index e88516b..73b5d33 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,69 @@ Opens on http://localhost:8080 4. Keep all DSP code, helpers, and types in the same file 5. Register in `src/lib/audio/engines/registry.ts` +### Adding CSound-Based Synthesis Engines + +For complex DSP algorithms, you can leverage CSound's powerful audio language: + +1. Create a single file in `src/lib/audio/engines/` extending the `CsoundEngine` abstract class +2. Define a TypeScript interface for your parameters +3. Implement required methods: + - `getName()`: Engine display name + - `getDescription()`: Brief description + - `getType()`: Return `'generative'`, `'sample'`, or `'input'` + - `getOrchestra()`: Return CSound orchestra code as a string + - `getParametersForCsound(params)`: Map TypeScript params to CSound channel parameters + - `randomParams(pitchLock?)`: Generate random parameter values + - `mutateParams(params, mutationAmount?, pitchLock?)`: Mutate existing parameters +4. Keep all enums, interfaces, and helper logic in the same file +5. Register in `src/lib/audio/engines/registry.ts` + +**CSound Orchestra Guidelines:** +- Use `instr 1` as your main instrument +- Read parameters via `chnget "paramName"` +- Duration is available as `p3` +- Time-based parameters (attack, decay, release) should be ratios (0-1) scaled by `p3` +- Output stereo audio with `outs aLeft, aRight` +- The base class handles WAV parsing, normalization, and fade-in + +**Example:** +```typescript +import { CsoundEngine, type CsoundParameter } from './base/CsoundEngine'; + +interface MyParams { + frequency: number; + resonance: number; +} + +export class MyEngine extends CsoundEngine { + getName() { return 'My Engine'; } + getDescription() { return 'Description'; } + getType() { return 'generative' as const; } + + protected getOrchestra(): string { + return ` +instr 1 + iFreq chnget "frequency" + iRes chnget "resonance" + aNoise noise 1, 0 + aOut butterbp aNoise, iFreq, iRes + outs aOut, aOut +endin +`; + } + + protected getParametersForCsound(params: MyParams): CsoundParameter[] { + return [ + { channelName: 'frequency', value: params.frequency }, + { channelName: 'resonance', value: params.resonance } + ]; + } + + randomParams() { /* ... */ } + mutateParams(params, amount = 0.15) { /* ... */ } +} +``` + ### Adding Audio Processors 1. Create a single file in `src/lib/audio/processors/` implementing the `AudioProcessor` interface diff --git a/src/App.svelte b/src/App.svelte index 5d1668c..14cde93 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -9,7 +9,7 @@ import type { EngineType } from "./lib/audio/engines/base/SynthEngine"; import { AudioService } from "./lib/audio/services/AudioService"; import { downloadWAV } from "./lib/audio/utils/WAVEncoder"; - import { loadVolume, saveVolume, loadDuration, saveDuration, loadPitchLockEnabled, savePitchLockEnabled, loadPitchLockFrequency, savePitchLockFrequency } from "./lib/utils/settings"; + import { loadVolume, saveVolume, loadDuration, saveDuration, loadPitchLockEnabled, savePitchLockEnabled, loadPitchLockFrequency, savePitchLockFrequency, loadExpandedCategories, saveExpandedCategories } from "./lib/utils/settings"; import { cropAudio, cutAudio, processSelection } from "./lib/audio/utils/AudioEdit"; import { generateRandomColor } from "./lib/utils/colors"; import { getRandomProcessor } from "./lib/audio/processors/registry"; @@ -19,6 +19,7 @@ import { createKeyboardHandler } from "./lib/utils/keyboard"; import { parseFrequencyInput, formatFrequency } from "./lib/utils/pitch"; import { UndoManager, type AudioState } from "./lib/utils/UndoManager"; + import type { EngineCategory } from "./lib/audio/engines/base/SynthEngine"; let currentEngineIndex = $state(0); const engine = $derived(engines[currentEngineIndex]); @@ -46,6 +47,7 @@ let selectionEnd = $state(null); let canUndo = $state(false); let sidebarOpen = $state(false); + let expandedCategories = $state>(loadExpandedCategories()); const showDuration = $derived(engineType !== 'sample'); const showRandomButton = $derived(engineType === 'generative'); @@ -74,6 +76,33 @@ savePitchLockFrequency(pitchLockFrequency); }); + $effect(() => { + saveExpandedCategories(expandedCategories); + }); + + // Group engines by category + const enginesByCategory = $derived.by(() => { + const grouped = new Map(); + for (const engine of engines) { + const category = engine.getCategory(); + if (!grouped.has(category)) { + grouped.set(category, []); + } + grouped.get(category)!.push(engine); + } + return grouped; + }); + + function toggleCategory(category: string) { + const newSet = new Set(expandedCategories); + if (newSet.has(category)) { + newSet.delete(category); + } else { + newSet.add(category); + } + expandedCategories = newSet; + } + onMount(() => { audioService.setPlaybackUpdateCallback((position) => { playbackPosition = position; @@ -485,15 +514,32 @@
@@ -693,12 +739,50 @@ border-radius: 0; } + .category-section { + display: flex; + flex-direction: column; + } + + .category-header { + display: flex; + align-items: center; + gap: 0.35rem; + font-size: 0.65rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + padding: 0.5rem 0.5rem; + background-color: #1a1a1a; + border: none; + border-bottom: 1px solid #333; + color: #999; + text-align: left; + cursor: pointer; + transition: color 0.2s, background-color 0.2s; + flex-shrink: 0; + } + + .category-header:hover { + color: #ccc; + background-color: #222; + } + + .category-arrow { + transition: transform 0.2s; + flex-shrink: 0; + } + + .category-header.collapsed .category-arrow { + transform: rotate(-90deg); + } + .engine-button { opacity: 0.7; position: relative; flex-shrink: 0; font-size: 0.75rem; - padding: 0.5rem 0.5rem; + padding: 0.5rem 0.5rem 0.5rem 1rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; diff --git a/src/lib/audio/engines/AdditiveBass.ts b/src/lib/audio/engines/AdditiveBass.ts new file mode 100644 index 0000000..a90585e --- /dev/null +++ b/src/lib/audio/engines/AdditiveBass.ts @@ -0,0 +1,228 @@ +import { CsoundEngine, type CsoundParameter } from './base/CsoundEngine'; +import type { PitchLock } from './base/SynthEngine'; + +interface AdditiveBassParams { + baseFreq: number; + pitchSweep: number; + pitchDecay: number; + overtoneAmp: number; + overtoneFreqMult: number; + noiseAmp: number; + noiseDecay: number; + filterResonance: number; + filterCutoff: number; + attack: number; + decay: number; + waveshape: number; + bodyResonance: number; + click: number; + harmonicSpread: number; +} + +export class AdditiveBass extends CsoundEngine { + getName(): string { + return 'Additive Bass'; + } + + getDescription(): string { + return 'Deep bass drum using additive synthesis with pink noise and waveshaping'; + } + + getType() { + return 'generative' as const; + } + + getCategory() { + return 'Percussion' as const; + } + + protected getOrchestra(): string { + return ` +instr 1 + iBaseFreq chnget "baseFreq" + iPitchSweep chnget "pitchSweep" + iPitchDecay chnget "pitchDecay" + iOvertoneAmp chnget "overtoneAmp" + iOvertoneFreqMult chnget "overtoneFreqMult" + iNoiseAmp chnget "noiseAmp" + iNoiseDecay chnget "noiseDecay" + iFilterResonance chnget "filterResonance" + iFilterCutoff chnget "filterCutoff" + iAttack chnget "attack" + iDecay chnget "decay" + iWaveshape chnget "waveshape" + iBodyResonance chnget "bodyResonance" + iClick chnget "click" + iHarmonicSpread chnget "harmonicSpread" + + idur = p3 + iAttackTime = iAttack * idur + iDecayTime = iDecay * idur + iPitchDecayTime = iPitchDecay * idur + iNoiseDecayTime = iNoiseDecay * idur + + ; Pitch envelope: exponential sweep from high to low + kPitchEnv expseg iBaseFreq * (1 + iPitchSweep * 3), iPitchDecayTime, iBaseFreq, idur - iPitchDecayTime, iBaseFreq * 0.95 + + ; Main amplitude envelope with attack and decay + kAmpEnv linseg 0, iAttackTime, 1, iDecayTime, 0.001, 0.001, 0 + kAmpEnv = kAmpEnv * kAmpEnv + + ; Generate fundamental sine wave + aFund oscili 0.7, kPitchEnv + + ; Generate overtone at multiple of fundamental + aOvertone oscili iOvertoneAmp, kPitchEnv * iOvertoneFreqMult + + ; Add harmonic spread (additional harmonics) + aHarm2 oscili iHarmonicSpread * 0.3, kPitchEnv * 3 + aHarm3 oscili iHarmonicSpread * 0.2, kPitchEnv * 5 + + ; Mix oscillators + aMix = aFund + aOvertone + aHarm2 + aHarm3 + + ; Apply waveshaping (hyperbolic tangent style) + if iWaveshape > 0.1 then + aMix = tanh(aMix * (1 + iWaveshape * 3)) + endif + + ; Generate pink noise + aPink pinkish 1 + + ; Noise envelope (fast decay) + kNoiseEnv expseg 1, iNoiseDecayTime, 0.001, idur - iNoiseDecayTime, 0.001 + aPinkScaled = aPink * iNoiseAmp * kNoiseEnv + + ; Add noise to mix + aMix = aMix + aPinkScaled + + ; Click transient (high frequency burst at start) + if iClick > 0.1 then + kClickEnv linseg 1, 0.005, 0, idur - 0.005, 0 + aClick oscili iClick * 0.4, kPitchEnv * 8 + aMix = aMix + aClick * kClickEnv + endif + + ; Apply resonant low-pass filter + kFilterFreq = iFilterCutoff * (1 + kPitchEnv / iBaseFreq * 0.5) + aFiltered rezzy aMix, kFilterFreq, iFilterResonance + + ; Body resonance (second resonant filter at fundamental) + if iBodyResonance > 0.1 then + aBodyFilt butterbp aFiltered, kPitchEnv * 0.5, 20 + aFiltered = aFiltered + aBodyFilt * iBodyResonance + endif + + ; Apply main envelope + aOut = aFiltered * kAmpEnv * 0.5 + + ; Stereo - slightly different phase and detune for right channel + kPitchEnvR expseg iBaseFreq * 1.002 * (1 + iPitchSweep * 3), iPitchDecayTime, iBaseFreq * 1.002, idur - iPitchDecayTime, iBaseFreq * 0.952 + + aFundR oscili 0.7, kPitchEnvR + aOvertoneR oscili iOvertoneAmp, kPitchEnvR * iOvertoneFreqMult + aHarm2R oscili iHarmonicSpread * 0.3, kPitchEnvR * 3 + aHarm3R oscili iHarmonicSpread * 0.2, kPitchEnvR * 5 + + aMixR = aFundR + aOvertoneR + aHarm2R + aHarm3R + + if iWaveshape > 0.1 then + aMixR = tanh(aMixR * (1 + iWaveshape * 3)) + endif + + aPinkR pinkish 1 + aPinkScaledR = aPinkR * iNoiseAmp * kNoiseEnv + aMixR = aMixR + aPinkScaledR + + if iClick > 0.1 then + aClickR oscili iClick * 0.4, kPitchEnvR * 8 + aMixR = aMixR + aClickR * kClickEnv + endif + + kFilterFreqR = iFilterCutoff * (1 + kPitchEnvR / iBaseFreq * 0.5) + aFilteredR rezzy aMixR, kFilterFreqR, iFilterResonance + + if iBodyResonance > 0.1 then + aBodyFiltR butterbp aFilteredR, kPitchEnvR * 0.5, 20 + aFilteredR = aFilteredR + aBodyFiltR * iBodyResonance + endif + + aOutR = aFilteredR * kAmpEnv * 0.5 + + outs aOut, aOutR +endin +`; + } + + protected getParametersForCsound(params: AdditiveBassParams): CsoundParameter[] { + return [ + { channelName: 'baseFreq', value: params.baseFreq }, + { channelName: 'pitchSweep', value: params.pitchSweep }, + { channelName: 'pitchDecay', value: params.pitchDecay }, + { channelName: 'overtoneAmp', value: params.overtoneAmp }, + { channelName: 'overtoneFreqMult', value: params.overtoneFreqMult }, + { channelName: 'noiseAmp', value: params.noiseAmp }, + { channelName: 'noiseDecay', value: params.noiseDecay }, + { channelName: 'filterResonance', value: params.filterResonance }, + { channelName: 'filterCutoff', value: params.filterCutoff }, + { channelName: 'attack', value: params.attack }, + { channelName: 'decay', value: params.decay }, + { channelName: 'waveshape', value: params.waveshape }, + { channelName: 'bodyResonance', value: params.bodyResonance }, + { channelName: 'click', value: params.click }, + { channelName: 'harmonicSpread', value: params.harmonicSpread }, + ]; + } + + randomParams(pitchLock?: PitchLock): AdditiveBassParams { + const baseFreq = pitchLock?.enabled ? pitchLock.frequency : this.randomRange(35, 80); + + const overtoneMultChoices = [1.5, 2.0, 2.5, 3.0, 4.0]; + + return { + baseFreq, + pitchSweep: this.randomRange(0.3, 1.0), + pitchDecay: this.randomRange(0.02, 0.15), + overtoneAmp: this.randomRange(0.2, 0.7), + overtoneFreqMult: this.randomChoice(overtoneMultChoices), + noiseAmp: this.randomRange(0.05, 0.3), + noiseDecay: this.randomRange(0.01, 0.08), + filterResonance: this.randomRange(5, 25), + filterCutoff: this.randomRange(100, 800), + attack: this.randomRange(0.001, 0.02), + decay: this.randomRange(0.3, 0.8), + waveshape: this.randomRange(0, 0.7), + bodyResonance: this.randomRange(0, 0.5), + click: this.randomRange(0, 0.6), + harmonicSpread: this.randomRange(0, 0.5), + }; + } + + mutateParams( + params: AdditiveBassParams, + mutationAmount: number = 0.15, + pitchLock?: PitchLock + ): AdditiveBassParams { + const baseFreq = pitchLock?.enabled ? pitchLock.frequency : params.baseFreq; + const overtoneMultChoices = [1.5, 2.0, 2.5, 3.0, 4.0]; + + return { + baseFreq, + pitchSweep: this.mutateValue(params.pitchSweep, mutationAmount, 0.1, 1.5), + pitchDecay: this.mutateValue(params.pitchDecay, mutationAmount, 0.01, 0.25), + overtoneAmp: this.mutateValue(params.overtoneAmp, mutationAmount, 0, 1.0), + overtoneFreqMult: + Math.random() < 0.1 ? this.randomChoice(overtoneMultChoices) : params.overtoneFreqMult, + noiseAmp: this.mutateValue(params.noiseAmp, mutationAmount, 0, 0.5), + noiseDecay: this.mutateValue(params.noiseDecay, mutationAmount, 0.005, 0.15), + filterResonance: this.mutateValue(params.filterResonance, mutationAmount, 2, 40), + filterCutoff: this.mutateValue(params.filterCutoff, mutationAmount, 80, 1200), + attack: this.mutateValue(params.attack, mutationAmount, 0.001, 0.05), + decay: this.mutateValue(params.decay, mutationAmount, 0.15, 0.95), + waveshape: this.mutateValue(params.waveshape, mutationAmount, 0, 1), + bodyResonance: this.mutateValue(params.bodyResonance, mutationAmount, 0, 0.8), + click: this.mutateValue(params.click, mutationAmount, 0, 0.8), + harmonicSpread: this.mutateValue(params.harmonicSpread, mutationAmount, 0, 0.7), + }; + } +} diff --git a/src/lib/audio/engines/AdditiveEngine.ts b/src/lib/audio/engines/AdditiveEngine.ts index 3f6b485..a3206a5 100644 --- a/src/lib/audio/engines/AdditiveEngine.ts +++ b/src/lib/audio/engines/AdditiveEngine.ts @@ -80,7 +80,7 @@ export interface AdditiveParams { export class AdditiveEngine implements SynthEngine { getName(): string { - return 'Prism'; + return 'Glass Prism'; } getDescription(): string { @@ -91,6 +91,10 @@ export class AdditiveEngine implements SynthEngine { return 'generative' as const; } + getCategory() { + return 'Additive' as const; + } + generate(params: AdditiveParams, sampleRate: number, duration: number): [Float32Array, Float32Array] { const numSamples = Math.floor(sampleRate * duration); const leftBuffer = new Float32Array(numSamples); diff --git a/src/lib/audio/engines/BassDrum.ts b/src/lib/audio/engines/BassDrum.ts index fc15f53..d062f94 100644 --- a/src/lib/audio/engines/BassDrum.ts +++ b/src/lib/audio/engines/BassDrum.ts @@ -57,7 +57,7 @@ interface BassDrumParams { export class BassDrum implements SynthEngine { getName(): string { - return 'Kick'; + return 'Dark Kick'; } getDescription(): string { @@ -68,6 +68,10 @@ export class BassDrum implements SynthEngine { return 'generative' as const; } + getCategory() { + return 'Percussion' as const; + } + randomParams(pitchLock?: PitchLock): BassDrumParams { // Choose a kick character/style const styleRoll = Math.random(); diff --git a/src/lib/audio/engines/Benjolin.ts b/src/lib/audio/engines/Benjolin.ts index 5115af3..53d69a8 100644 --- a/src/lib/audio/engines/Benjolin.ts +++ b/src/lib/audio/engines/Benjolin.ts @@ -73,6 +73,10 @@ export class Benjolin implements SynthEngine { return 'generative' as const; } + getCategory() { + return 'Experimental' as const; + } + generate(params: BenjolinParams, sampleRate: number, duration: number): [Float32Array, Float32Array] { const numSamples = Math.floor(duration * sampleRate); const left = new Float32Array(numSamples); diff --git a/src/lib/audio/engines/DubSiren.ts b/src/lib/audio/engines/DubSiren.ts index 9242702..3e87fb7 100644 --- a/src/lib/audio/engines/DubSiren.ts +++ b/src/lib/audio/engines/DubSiren.ts @@ -67,6 +67,10 @@ export class DubSiren implements SynthEngine { return 'generative' as const; } + getCategory() { + return 'Experimental' as const; + } + generate(params: DubSirenParams, sampleRate: number, duration: number): [Float32Array, Float32Array] { const numSamples = Math.floor(sampleRate * duration); const leftBuffer = new Float32Array(numSamples); diff --git a/src/lib/audio/engines/DustNoise.ts b/src/lib/audio/engines/DustNoise.ts index 4c24c42..71cefd6 100644 --- a/src/lib/audio/engines/DustNoise.ts +++ b/src/lib/audio/engines/DustNoise.ts @@ -46,6 +46,10 @@ export class DustNoise implements SynthEngine { return 'generative' as const; } + getCategory() { + return 'Noise' as const; + } + randomParams(pitchLock?: PitchLock): DustNoiseParams { const characterBias = Math.random(); diff --git a/src/lib/audio/engines/FMTomTom.ts b/src/lib/audio/engines/FMTomTom.ts new file mode 100644 index 0000000..7ce748b --- /dev/null +++ b/src/lib/audio/engines/FMTomTom.ts @@ -0,0 +1,184 @@ +import { CsoundEngine, type CsoundParameter } from './base/CsoundEngine'; +import type { PitchLock } from './base/SynthEngine'; + +interface FMTomTomParams { + baseFreq: number; + pitchBendAmount: number; + pitchBendDecay: number; + modIndex: number; + modRatio: number; + noiseHPFreq: number; + noiseResonance: number; + noiseMix: number; + ampAttack: number; + ampDecay: number; + sustain: number; + release: number; + tonality: number; + stereoDetune: number; +} + +export class FMTomTom extends CsoundEngine { + getName(): string { + return 'FM Tom-Tom'; + } + + getDescription(): string { + return 'High-pass filtered noise modulating a sine oscillator with pitch bend envelope simulating tom-tom membrane'; + } + + getType() { + return 'generative' as const; + } + + getCategory() { + return 'Percussion' as const; + } + + protected getOrchestra(): string { + return ` +instr 1 + iBaseFreq chnget "baseFreq" + iPitchBendAmount chnget "pitchBendAmount" + iPitchBendDecay chnget "pitchBendDecay" + iModIndex chnget "modIndex" + iModRatio chnget "modRatio" + iNoiseHPFreq chnget "noiseHPFreq" + iNoiseResonance chnget "noiseResonance" + iNoiseMix chnget "noiseMix" + iAmpAttack chnget "ampAttack" + iAmpDecay chnget "ampDecay" + iSustain chnget "sustain" + iRelease chnget "release" + iTonality chnget "tonality" + iStereoDetune chnget "stereoDetune" + + idur = p3 + iPitchBendTime = iPitchBendDecay * idur + iAmpAttackTime = iAmpAttack * idur + iAmpDecayTime = iAmpDecay * idur + iReleaseTime = iRelease * idur + + ; Pitch bend envelope (simulates drum membrane tightening) + ; Starts at higher pitch and decays to base pitch + iPitchStart = iBaseFreq * (1 + iPitchBendAmount) + kPitchEnv expseg iPitchStart, iPitchBendTime, iBaseFreq, idur - iPitchBendTime, iBaseFreq + + ; Generate high-pass filtered noise for modulation + aNoise noise 1, 0 + aNoiseHP butterhp aNoise, iNoiseHPFreq + aNoiseFiltered butterbp aNoiseHP, iNoiseHPFreq * 2, iNoiseResonance + + ; Scale noise for FM modulation + aNoiseScaled = aNoiseFiltered * iModIndex * kPitchEnv * iTonality + + ; FM synthesis: noise modulates sine oscillator + aModulator oscili iModIndex * kPitchEnv, kPitchEnv * iModRatio + aCarrier oscili 0.5, kPitchEnv + aModulator + aNoiseScaled + + ; Add direct noise component for more realistic tom sound + aNoiseDirect = aNoiseFiltered * iNoiseMix * 0.3 + + ; Mix carrier and noise + aMix = aCarrier * (1 - iNoiseMix * 0.5) + aNoiseDirect + + ; Amplitude envelope (ADSR-like with fast attack and decay) + kAmpEnv expseg 0.001, iAmpAttackTime, 1, iAmpDecayTime, iSustain, idur - iAmpAttackTime - iAmpDecayTime - iReleaseTime, iSustain, iReleaseTime, 0.001 + + ; Apply amplitude envelope + aOut = aMix * kAmpEnv + + ; Right channel with stereo detune + iBaseFreqR = iBaseFreq * (1 + iStereoDetune * 0.02) + iPitchStartR = iBaseFreqR * (1 + iPitchBendAmount) + kPitchEnvR expseg iPitchStartR, iPitchBendTime, iBaseFreqR, idur - iPitchBendTime, iBaseFreqR + + aNoiseR noise 1, 0 + aNoiseHPR butterhp aNoiseR, iNoiseHPFreq * (1 + iStereoDetune * 0.01) + aNoiseFilteredR butterbp aNoiseHPR, iNoiseHPFreq * 2 * (1 + iStereoDetune * 0.01), iNoiseResonance + + aNoiseScaledR = aNoiseFilteredR * iModIndex * kPitchEnvR * iTonality + aModulatorR oscili iModIndex * kPitchEnvR, kPitchEnvR * iModRatio + aCarrierR oscili 0.5, kPitchEnvR + aModulatorR + aNoiseScaledR + + aNoiseDirectR = aNoiseFilteredR * iNoiseMix * 0.3 + aMixR = aCarrierR * (1 - iNoiseMix * 0.5) + aNoiseDirectR + aOutR = aMixR * kAmpEnv + + outs aOut, aOutR +endin +`; + } + + protected getParametersForCsound(params: FMTomTomParams): CsoundParameter[] { + return [ + { channelName: 'baseFreq', value: params.baseFreq }, + { channelName: 'pitchBendAmount', value: params.pitchBendAmount }, + { channelName: 'pitchBendDecay', value: params.pitchBendDecay }, + { channelName: 'modIndex', value: params.modIndex }, + { channelName: 'modRatio', value: params.modRatio }, + { channelName: 'noiseHPFreq', value: params.noiseHPFreq }, + { channelName: 'noiseResonance', value: params.noiseResonance }, + { channelName: 'noiseMix', value: params.noiseMix }, + { channelName: 'ampAttack', value: params.ampAttack }, + { channelName: 'ampDecay', value: params.ampDecay }, + { channelName: 'sustain', value: params.sustain }, + { channelName: 'release', value: params.release }, + { channelName: 'tonality', value: params.tonality }, + { channelName: 'stereoDetune', value: params.stereoDetune }, + ]; + } + + randomParams(pitchLock?: PitchLock): FMTomTomParams { + const baseFreqChoices = [80, 100, 120, 150, 180, 220, 260, 300]; + const baseFreq = pitchLock?.enabled + ? pitchLock.frequency + : this.randomChoice(baseFreqChoices) * this.randomRange(0.9, 1.1); + + const modRatios = [0.5, 1, 1.5, 2, 2.5, 3]; + + return { + baseFreq, + pitchBendAmount: this.randomRange(0.2, 0.8), + pitchBendDecay: this.randomRange(0.05, 0.2), + modIndex: this.randomRange(1, 8), + modRatio: this.randomChoice(modRatios), + noiseHPFreq: this.randomRange(200, 800), + noiseResonance: this.randomRange(20, 100), + noiseMix: this.randomRange(0.1, 0.6), + ampAttack: this.randomRange(0.001, 0.01), + ampDecay: this.randomRange(0.1, 0.3), + sustain: this.randomRange(0.2, 0.6), + release: this.randomRange(0.2, 0.5), + tonality: this.randomRange(0.3, 0.9), + stereoDetune: this.randomRange(0, 0.5), + }; + } + + mutateParams( + params: FMTomTomParams, + mutationAmount: number = 0.15, + pitchLock?: PitchLock + ): FMTomTomParams { + const baseFreq = pitchLock?.enabled ? pitchLock.frequency : params.baseFreq; + const modRatios = [0.5, 1, 1.5, 2, 2.5, 3]; + + return { + baseFreq, + pitchBendAmount: this.mutateValue(params.pitchBendAmount, mutationAmount, 0.1, 1), + pitchBendDecay: this.mutateValue(params.pitchBendDecay, mutationAmount, 0.02, 0.4), + modIndex: this.mutateValue(params.modIndex, mutationAmount, 0.5, 12), + modRatio: + Math.random() < 0.15 ? this.randomChoice(modRatios) : params.modRatio, + noiseHPFreq: this.mutateValue(params.noiseHPFreq, mutationAmount, 100, 1200), + noiseResonance: this.mutateValue(params.noiseResonance, mutationAmount, 15, 150), + noiseMix: this.mutateValue(params.noiseMix, mutationAmount, 0, 0.8), + ampAttack: this.mutateValue(params.ampAttack, mutationAmount, 0.001, 0.02), + ampDecay: this.mutateValue(params.ampDecay, mutationAmount, 0.05, 0.5), + sustain: this.mutateValue(params.sustain, mutationAmount, 0.1, 0.8), + release: this.mutateValue(params.release, mutationAmount, 0.1, 0.7), + tonality: this.mutateValue(params.tonality, mutationAmount, 0.1, 1), + stereoDetune: this.mutateValue(params.stereoDetune, mutationAmount, 0, 1), + }; + } +} diff --git a/src/lib/audio/engines/FeedbackSnare.ts b/src/lib/audio/engines/FeedbackSnare.ts new file mode 100644 index 0000000..230f7ed --- /dev/null +++ b/src/lib/audio/engines/FeedbackSnare.ts @@ -0,0 +1,247 @@ +import { CsoundEngine, type CsoundParameter } from './base/CsoundEngine'; +import type { PitchLock } from './base/SynthEngine'; + +interface FeedbackSnareParams { + baseFreq: number; + tonalDecay: number; + noiseDecay: number; + toneResonance: number; + springDecay: number; + springTone: number; + pitchBend: number; + pitchBendSpeed: number; + pulseRate: number; + feedbackAmount: number; + delayTime: number; + crossFeedMix: number; + snap: number; + brightness: number; +} + +export class FeedbackSnare extends CsoundEngine { + getName(): string { + return 'Feedback Snare'; + } + + getDescription(): string { + return 'Complex snare using cross-feedback delay network with pulsed noise modulation'; + } + + getType() { + return 'generative' as const; + } + + getCategory() { + return 'Percussion' as const; + } + + protected getOrchestra(): string { + return ` +instr 1 + iBaseFreq chnget "baseFreq" + iTonalDecay chnget "tonalDecay" + iNoiseDecay chnget "noiseDecay" + iToneResonance chnget "toneResonance" + iSpringDecay chnget "springDecay" + iSpringTone chnget "springTone" + iPitchBend chnget "pitchBend" + iPitchBendSpeed chnget "pitchBendSpeed" + iPulseRate chnget "pulseRate" + iFeedbackAmount chnget "feedbackAmount" + iDelayTime chnget "delayTime" + iCrossFeedMix chnget "crossFeedMix" + iSnap chnget "snap" + iBrightness chnget "brightness" + + idur = p3 + iTonalDecayTime = iTonalDecay * idur + iNoiseDecayTime = iNoiseDecay * idur + iSpringDecayTime = iSpringDecay * idur + iPitchBendTime = iPitchBendSpeed * idur + + ; Pitch envelope with bend + kPitchEnv expseg iBaseFreq * (1 + iPitchBend * 2), iPitchBendTime, iBaseFreq, idur - iPitchBendTime, iBaseFreq * 0.95 + + ; Generate square wave pulse for tonal component + aPulse vco2 0.5, kPitchEnv, 2, 0.5 + + ; Tonal envelope + kToneEnv expseg 1, iTonalDecayTime, 0.001, idur - iTonalDecayTime, 0.001 + aTonal = aPulse * kToneEnv + + ; Apply drum tone resonant filter + aDrumTone rezzy aTonal, kPitchEnv, iToneResonance + + ; Generate white noise + aNoise noise 1, 0 + + ; Pulse modulation of noise (creates rhythmic texture) + kPulseMod oscili 1, iPulseRate + kPulseMod = (kPulseMod + 1) * 0.5 + aNoiseModulated = aNoise * kPulseMod + + ; Noise envelope + kNoiseEnv expseg 1, iNoiseDecayTime, 0.001, idur - iNoiseDecayTime, 0.001 + + ; Parallel filters on noise + ; Bandpass filter 1 (body) + aNoiseBody butterbp aNoiseModulated, iBaseFreq * 1.5, 100 + ; Bandpass filter 2 (mid) + aNoiseMid butterbp aNoiseModulated, iBaseFreq * 3, 200 + ; Highpass filter (crispness) + aNoiseHigh butterhp aNoiseModulated, 3000 + + ; Mix noise components + aNoiseMix = (aNoiseBody * 0.4 + aNoiseMid * 0.3 + aNoiseHigh * 0.3 * iBrightness) * kNoiseEnv + + ; Mix tonal and noise + aMix = aDrumTone * 0.5 + aNoiseMix * 0.5 + + ; Cross-feedback delay network (simulates spring/snare wires) + ; Create two delay lines that feed back into each other + aDelay1Init init 0 + aDelay2Init init 0 + + ; Spring tone filter (for the delayed signal) + iSpringFreq = 800 + iSpringTone * 4000 + + ; Delay line 1 + aDelayIn1 = aMix + aDelay2Init * iFeedbackAmount * iCrossFeedMix + aDelay1 vdelay aDelayIn1, iDelayTime * 1000, 50 + aDelay1Filt butterbp aDelay1, iSpringFreq, 100 + aDelay1Out = aDelay1Filt * exp(-p3 / iSpringDecayTime) + + ; Delay line 2 + aDelayIn2 = aMix + aDelay1Out * iFeedbackAmount + aDelay2 vdelay aDelayIn2, iDelayTime * 1.3 * 1000, 50 + aDelay2Filt butterbp aDelay2, iSpringFreq * 1.2, 120 + aDelay2Out = aDelay2Filt * exp(-p3 / iSpringDecayTime) + + ; Update feedback + aDelay1Init = aDelay1Out + aDelay2Init = aDelay2Out + + ; Mix dry and delay + aOut = aMix * 0.6 + aDelay1Out * 0.2 + aDelay2Out * 0.2 + + ; Add snap transient + if iSnap > 0.1 then + kSnapEnv linseg 1, 0.003, 0, idur - 0.003, 0 + aSnap noise iSnap * 0.5, 0 + aSnapFilt butterhp aSnap, 8000 + aOut = aOut + aSnapFilt * kSnapEnv + endif + + ; Final output scaling + aOut = aOut * 0.4 + + ; Right channel with slightly different parameters + aPulseR vco2 0.5, kPitchEnv * 1.002, 2, 0.5 + aTonalR = aPulseR * kToneEnv + aDrumToneR rezzy aTonalR, kPitchEnv * 1.002, iToneResonance + + aNoiseR noise 1, 0 + aNoiseModulatedR = aNoiseR * kPulseMod + aNoiseBodyR butterbp aNoiseModulatedR, iBaseFreq * 1.52, 100 + aNoiseMidR butterbp aNoiseModulatedR, iBaseFreq * 3.03, 200 + aNoiseHighR butterhp aNoiseModulatedR, 3100 + + aNoiseMixR = (aNoiseBodyR * 0.4 + aNoiseMidR * 0.3 + aNoiseHighR * 0.3 * iBrightness) * kNoiseEnv + aMixR = aDrumToneR * 0.5 + aNoiseMixR * 0.5 + + aDelay1InitR init 0 + aDelay2InitR init 0 + + aDelayIn1R = aMixR + aDelay2InitR * iFeedbackAmount * iCrossFeedMix + aDelay1R vdelay aDelayIn1R, iDelayTime * 1.05 * 1000, 50 + aDelay1FiltR butterbp aDelay1R, iSpringFreq * 1.01, 100 + aDelay1OutR = aDelay1FiltR * exp(-p3 / iSpringDecayTime) + + aDelayIn2R = aMixR + aDelay1OutR * iFeedbackAmount + aDelay2R vdelay aDelayIn2R, iDelayTime * 1.35 * 1000, 50 + aDelay2FiltR butterbp aDelay2R, iSpringFreq * 1.22, 120 + aDelay2OutR = aDelay2FiltR * exp(-p3 / iSpringDecayTime) + + aDelay1InitR = aDelay1OutR + aDelay2InitR = aDelay2OutR + + aOutR = aMixR * 0.6 + aDelay1OutR * 0.2 + aDelay2OutR * 0.2 + + if iSnap > 0.1 then + aSnapR noise iSnap * 0.5, 0 + aSnapFiltR butterhp aSnapR, 8100 + aOutR = aOutR + aSnapFiltR * kSnapEnv + endif + + aOutR = aOutR * 0.4 + + outs aOut, aOutR +endin +`; + } + + protected getParametersForCsound(params: FeedbackSnareParams): CsoundParameter[] { + return [ + { channelName: 'baseFreq', value: params.baseFreq }, + { channelName: 'tonalDecay', value: params.tonalDecay }, + { channelName: 'noiseDecay', value: params.noiseDecay }, + { channelName: 'toneResonance', value: params.toneResonance }, + { channelName: 'springDecay', value: params.springDecay }, + { channelName: 'springTone', value: params.springTone }, + { channelName: 'pitchBend', value: params.pitchBend }, + { channelName: 'pitchBendSpeed', value: params.pitchBendSpeed }, + { channelName: 'pulseRate', value: params.pulseRate }, + { channelName: 'feedbackAmount', value: params.feedbackAmount }, + { channelName: 'delayTime', value: params.delayTime }, + { channelName: 'crossFeedMix', value: params.crossFeedMix }, + { channelName: 'snap', value: params.snap }, + { channelName: 'brightness', value: params.brightness }, + ]; + } + + randomParams(pitchLock?: PitchLock): FeedbackSnareParams { + const baseFreq = pitchLock?.enabled ? pitchLock.frequency : this.randomRange(150, 350); + + return { + baseFreq, + tonalDecay: this.randomRange(0.1, 0.3), + noiseDecay: this.randomRange(0.3, 0.7), + toneResonance: this.randomRange(5, 25), + springDecay: this.randomRange(0.2, 0.6), + springTone: this.randomRange(0.2, 0.8), + pitchBend: this.randomRange(0.3, 0.9), + pitchBendSpeed: this.randomRange(0.01, 0.05), + pulseRate: this.randomRange(50, 300), + feedbackAmount: this.randomRange(0.3, 0.7), + delayTime: this.randomRange(0.005, 0.025), + crossFeedMix: this.randomRange(0.4, 0.9), + snap: this.randomRange(0, 0.6), + brightness: this.randomRange(0.3, 0.9), + }; + } + + mutateParams( + params: FeedbackSnareParams, + mutationAmount: number = 0.15, + pitchLock?: PitchLock + ): FeedbackSnareParams { + const baseFreq = pitchLock?.enabled ? pitchLock.frequency : params.baseFreq; + + return { + baseFreq, + tonalDecay: this.mutateValue(params.tonalDecay, mutationAmount, 0.05, 0.5), + noiseDecay: this.mutateValue(params.noiseDecay, mutationAmount, 0.2, 0.9), + toneResonance: this.mutateValue(params.toneResonance, mutationAmount, 2, 40), + springDecay: this.mutateValue(params.springDecay, mutationAmount, 0.1, 0.8), + springTone: this.mutateValue(params.springTone, mutationAmount, 0, 1), + pitchBend: this.mutateValue(params.pitchBend, mutationAmount, 0.1, 1.2), + pitchBendSpeed: this.mutateValue(params.pitchBendSpeed, mutationAmount, 0.005, 0.1), + pulseRate: this.mutateValue(params.pulseRate, mutationAmount, 20, 500), + feedbackAmount: this.mutateValue(params.feedbackAmount, mutationAmount, 0.1, 0.85), + delayTime: this.mutateValue(params.delayTime, mutationAmount, 0.003, 0.04), + crossFeedMix: this.mutateValue(params.crossFeedMix, mutationAmount, 0.2, 1), + snap: this.mutateValue(params.snap, mutationAmount, 0, 0.8), + brightness: this.mutateValue(params.brightness, mutationAmount, 0, 1), + }; + } +} diff --git a/src/lib/audio/engines/FormantFM.ts b/src/lib/audio/engines/FormantFM.ts index bae0901..a89d1e3 100644 --- a/src/lib/audio/engines/FormantFM.ts +++ b/src/lib/audio/engines/FormantFM.ts @@ -61,6 +61,10 @@ export class FormantFM extends CsoundEngine { return 'generative' as const; } + getCategory() { + return 'FM' as const; + } + protected getOrchestra(): string { return ` instr 1 diff --git a/src/lib/audio/engines/FormantPopDrum.ts b/src/lib/audio/engines/FormantPopDrum.ts new file mode 100644 index 0000000..3b60f2e --- /dev/null +++ b/src/lib/audio/engines/FormantPopDrum.ts @@ -0,0 +1,142 @@ +import { CsoundEngine, type CsoundParameter } from './base/CsoundEngine'; +import type { PitchLock } from './base/SynthEngine'; + +interface FormantPopDrumParams { + formant1Freq: number; + formant1Width: number; + formant2Freq: number; + formant2Width: number; + noiseDecay: number; + ampAttack: number; + ampDecay: number; + brightness: number; + stereoSpread: number; +} + +export class FormantPopDrum extends CsoundEngine { + getName(): string { + return 'Formant Pop Drum'; + } + + getDescription(): string { + return 'Short noise burst through dual bandpass filters creating marimba-like or wooden drum tones'; + } + + getType() { + return 'generative' as const; + } + + getCategory() { + return 'Percussion' as const; + } + + protected getOrchestra(): string { + return ` +instr 1 + iF1Freq chnget "formant1Freq" + iF1Width chnget "formant1Width" + iF2Freq chnget "formant2Freq" + iF2Width chnget "formant2Width" + iNoiseDecay chnget "noiseDecay" + iAmpAttack chnget "ampAttack" + iAmpDecay chnget "ampDecay" + iBrightness chnget "brightness" + iStereoSpread chnget "stereoSpread" + + idur = p3 + iNoiseDecayTime = iNoiseDecay * idur + iAmpAttackTime = iAmpAttack * idur + iAmpDecayTime = iAmpDecay * idur + + ; Declick envelope for noise (very short to avoid clicks) + kDeclickEnv linseg 0, 0.001, 1, iNoiseDecayTime, 0, idur - iNoiseDecayTime - 0.001, 0 + + ; Generate random noise + aNoise noise 1, 0 + + ; Apply declick envelope to noise + aNoiseEnv = aNoise * kDeclickEnv + + ; First bandpass filter (formant 1) + aFormant1 butterbp aNoiseEnv, iF1Freq, iF1Width + + ; Second bandpass filter (formant 2) + aFormant2 butterbp aNoiseEnv, iF2Freq, iF2Width + + ; Mix formants with brightness control + aMix = aFormant1 * (1 - iBrightness * 0.5) + aFormant2 * iBrightness + + ; Amplitude envelope (exponential decay) + kAmpEnv expseg 0.001, iAmpAttackTime, 1, iAmpDecayTime, 0.001, idur - iAmpAttackTime - iAmpDecayTime, 0.001 + + ; Apply amplitude envelope + aOut = aMix * kAmpEnv + + ; Stereo output with slight frequency offset for right channel + iF1FreqR = iF1Freq * (1 + iStereoSpread * 0.02) + iF2FreqR = iF2Freq * (1 + iStereoSpread * 0.02) + + aFormant1R butterbp aNoiseEnv, iF1FreqR, iF1Width + aFormant2R butterbp aNoiseEnv, iF2FreqR, iF2Width + + aMixR = aFormant1R * (1 - iBrightness * 0.5) + aFormant2R * iBrightness + aOutR = aMixR * kAmpEnv + + outs aOut, aOutR +endin +`; + } + + protected getParametersForCsound(params: FormantPopDrumParams): CsoundParameter[] { + return [ + { channelName: 'formant1Freq', value: params.formant1Freq }, + { channelName: 'formant1Width', value: params.formant1Width }, + { channelName: 'formant2Freq', value: params.formant2Freq }, + { channelName: 'formant2Width', value: params.formant2Width }, + { channelName: 'noiseDecay', value: params.noiseDecay }, + { channelName: 'ampAttack', value: params.ampAttack }, + { channelName: 'ampDecay', value: params.ampDecay }, + { channelName: 'brightness', value: params.brightness }, + { channelName: 'stereoSpread', value: params.stereoSpread }, + ]; + } + + randomParams(pitchLock?: PitchLock): FormantPopDrumParams { + const formant1FreqChoices = [200, 250, 300, 400, 500, 600, 800, 1000]; + const formant1Freq = pitchLock?.enabled + ? pitchLock.frequency + : this.randomChoice(formant1FreqChoices) * this.randomRange(0.9, 1.1); + + return { + formant1Freq, + formant1Width: this.randomRange(30, 120), + formant2Freq: formant1Freq * this.randomRange(1.5, 3.5), + formant2Width: this.randomRange(40, 150), + noiseDecay: this.randomRange(0.05, 0.3), + ampAttack: this.randomRange(0.001, 0.02), + ampDecay: this.randomRange(0.1, 0.6), + brightness: this.randomRange(0.2, 0.8), + stereoSpread: this.randomRange(0, 0.5), + }; + } + + mutateParams( + params: FormantPopDrumParams, + mutationAmount: number = 0.15, + pitchLock?: PitchLock + ): FormantPopDrumParams { + const formant1Freq = pitchLock?.enabled ? pitchLock.frequency : params.formant1Freq; + + return { + formant1Freq, + formant1Width: this.mutateValue(params.formant1Width, mutationAmount, 20, 200), + formant2Freq: this.mutateValue(params.formant2Freq, mutationAmount, 300, 4000), + formant2Width: this.mutateValue(params.formant2Width, mutationAmount, 30, 250), + noiseDecay: this.mutateValue(params.noiseDecay, mutationAmount, 0.02, 0.5), + ampAttack: this.mutateValue(params.ampAttack, mutationAmount, 0.001, 0.05), + ampDecay: this.mutateValue(params.ampDecay, mutationAmount, 0.05, 0.8), + brightness: this.mutateValue(params.brightness, mutationAmount, 0, 1), + stereoSpread: this.mutateValue(params.stereoSpread, mutationAmount, 0, 1), + }; + } +} diff --git a/src/lib/audio/engines/FourOpFM.ts b/src/lib/audio/engines/FourOpFM.ts index 865099e..e52894d 100644 --- a/src/lib/audio/engines/FourOpFM.ts +++ b/src/lib/audio/engines/FourOpFM.ts @@ -75,6 +75,10 @@ export class FourOpFM implements SynthEngine { return 'generative' as const; } + getCategory() { + return 'FM' as const; + } + generate(params: FourOpFMParams, sampleRate: number, duration: number): [Float32Array, Float32Array] { const numSamples = Math.floor(sampleRate * duration); const leftBuffer = new Float32Array(numSamples); diff --git a/src/lib/audio/engines/HiHat.ts b/src/lib/audio/engines/HiHat.ts index 54808b4..c49c470 100644 --- a/src/lib/audio/engines/HiHat.ts +++ b/src/lib/audio/engines/HiHat.ts @@ -19,7 +19,7 @@ interface HiHatParams { export class HiHat implements SynthEngine { getName(): string { - return 'Hi-Hat'; + return 'Noise Hi-Hat'; } getDescription(): string { @@ -30,6 +30,10 @@ export class HiHat implements SynthEngine { return 'generative' as const; } + getCategory() { + return 'Percussion' as const; + } + randomParams(pitchLock?: PitchLock): HiHatParams { return { decay: Math.random(), diff --git a/src/lib/audio/engines/Input.ts b/src/lib/audio/engines/Input.ts index 9f179d9..3b97dfa 100644 --- a/src/lib/audio/engines/Input.ts +++ b/src/lib/audio/engines/Input.ts @@ -21,6 +21,10 @@ export class Input implements SynthEngine { return 'input' as const; } + getCategory() { + return 'Utility' as const; + } + async record(duration: number): Promise { const stream = await navigator.mediaDevices.getUserMedia({ audio: { diff --git a/src/lib/audio/engines/KarplusStrong.ts b/src/lib/audio/engines/KarplusStrong.ts index d461213..b8aed2c 100644 --- a/src/lib/audio/engines/KarplusStrong.ts +++ b/src/lib/audio/engines/KarplusStrong.ts @@ -39,6 +39,10 @@ export class KarplusStrong implements SynthEngine { return 'generative' as const; } + getCategory() { + return 'Physical' as const; + } + generate(params: KarplusStrongParams, sampleRate: number, duration: number): [Float32Array, Float32Array] { const numSamples = Math.floor(sampleRate * duration); const leftBuffer = new Float32Array(numSamples); diff --git a/src/lib/audio/engines/MassiveAdditive.ts b/src/lib/audio/engines/MassiveAdditive.ts index 0e0acb3..6f925e1 100644 --- a/src/lib/audio/engines/MassiveAdditive.ts +++ b/src/lib/audio/engines/MassiveAdditive.ts @@ -39,6 +39,10 @@ export class MassiveAdditive extends CsoundEngine { return 'generative' as const; } + getCategory() { + return 'Additive' as const; + } + protected getOrchestra(): string { return ` ; Function tables for sine wave diff --git a/src/lib/audio/engines/NoiseDrum.ts b/src/lib/audio/engines/NoiseDrum.ts index 06c71b9..e97fd6a 100644 --- a/src/lib/audio/engines/NoiseDrum.ts +++ b/src/lib/audio/engines/NoiseDrum.ts @@ -39,7 +39,7 @@ interface NoiseDrumParams { export class NoiseDrum implements SynthEngine { getName(): string { - return 'NPerc'; + return 'Noise Perc'; } getDescription(): string { @@ -50,6 +50,10 @@ export class NoiseDrum implements SynthEngine { return 'generative' as const; } + getCategory() { + return 'Percussion' as const; + } + randomParams(): NoiseDrumParams { // Intelligent parameter generation based on correlated characteristics @@ -93,16 +97,16 @@ export class NoiseDrum implements SynthEngine { const filterType = filterFreq < 0.3 ? Math.random() * 0.35 : // Low freq prefers lowpass filterFreq > 0.7 ? - 0.5 + Math.random() * 0.5 : // High freq prefers highpass/bandpass - Math.random(); // Mid freq - any type + 0.5 + Math.random() * 0.5 : // High freq prefers highpass/bandpass + Math.random(); // Mid freq - any type // Decay time inversely correlates with frequency const decayBias = Math.random(); const ampDecay = filterFreq < 0.3 ? 0.25 + decayBias * 0.4 : // Low freq can be longer filterFreq > 0.6 ? - 0.08 + decayBias * 0.35 : // High freq shorter - 0.2 + decayBias * 0.45; // Mid range + 0.08 + decayBias * 0.35 : // High freq shorter + 0.2 + decayBias * 0.45; // Mid range // Attack is generally very short for percussion const ampAttack = Math.random() < 0.85 ? @@ -372,7 +376,7 @@ export class NoiseDrum implements SynthEngine { // Blend body resonance - SUBTLE sample = sample * (1 - params.bodyAmount * 0.4) + - bodyFiltered.output * params.bodyAmount * 0.6 * bodyEnv; + bodyFiltered.output * params.bodyAmount * 0.6 * bodyEnv; } // Apply amplitude envelope @@ -434,7 +438,7 @@ export class NoiseDrum implements SynthEngine { pinkState[5] = -0.7616 * pinkState[5] - whiteNoise * 0.0168980; const pink = pinkState[0] + pinkState[1] + pinkState[2] + pinkState[3] + - pinkState[4] + pinkState[5] + pinkState[6] + whiteNoise * 0.5362; + pinkState[4] + pinkState[5] + pinkState[6] + whiteNoise * 0.5362; pinkState[6] = whiteNoise * 0.115926; return pink * 0.11; diff --git a/src/lib/audio/engines/ParticleNoise.ts b/src/lib/audio/engines/ParticleNoise.ts index 1c2894c..e6be5d0 100644 --- a/src/lib/audio/engines/ParticleNoise.ts +++ b/src/lib/audio/engines/ParticleNoise.ts @@ -38,6 +38,10 @@ export class ParticleNoise implements SynthEngine { return 'generative' as const; } + getCategory() { + return 'Noise' as const; + } + randomParams(pitchLock?: PitchLock): ParticleNoiseParams { const densityBias = Math.random(); diff --git a/src/lib/audio/engines/PhaseDistortionFM.ts b/src/lib/audio/engines/PhaseDistortionFM.ts index 786c80e..1200482 100644 --- a/src/lib/audio/engines/PhaseDistortionFM.ts +++ b/src/lib/audio/engines/PhaseDistortionFM.ts @@ -65,7 +65,7 @@ export class PhaseDistortionFM implements SynthEngine { private static workletURL: string | null = null; getName(): string { - return 'PD'; + return 'Phase Dist'; } getDescription(): string { @@ -76,6 +76,10 @@ export class PhaseDistortionFM implements SynthEngine { return 'generative' as const; } + getCategory() { + return 'FM' as const; + } + generate(params: PhaseDistortionFMParams, sampleRate: number, duration: number): [Float32Array, Float32Array] { const numSamples = Math.floor(sampleRate * duration); const leftBuffer = new Float32Array(numSamples); diff --git a/src/lib/audio/engines/Ring.ts b/src/lib/audio/engines/Ring.ts index 236b79b..00c3a29 100644 --- a/src/lib/audio/engines/Ring.ts +++ b/src/lib/audio/engines/Ring.ts @@ -82,6 +82,10 @@ export class Ring implements SynthEngine { return 'generative' as const; } + getCategory() { + return 'Modulation' as const; + } + generate(params: RingParams, sampleRate: number, duration: number): [Float32Array, Float32Array] { const numSamples = Math.floor(sampleRate * duration); const leftBuffer = new Float32Array(numSamples); diff --git a/src/lib/audio/engines/RingCymbal.ts b/src/lib/audio/engines/RingCymbal.ts new file mode 100644 index 0000000..a4a1ddf --- /dev/null +++ b/src/lib/audio/engines/RingCymbal.ts @@ -0,0 +1,178 @@ +import { CsoundEngine, type CsoundParameter } from './base/CsoundEngine'; +import type { PitchLock } from './base/SynthEngine'; + +interface RingCymbalParams { + baseFreq: number; + overtone1Freq: number; + overtone2Freq: number; + overtone3Freq: number; + overtone1Vol: number; + overtone2Vol: number; + overtone3Vol: number; + filterCutoff: number; + resonance: number; + decay: number; + attack: number; + noise: number; + brightness: number; + spread: number; +} + +export class RingCymbal extends CsoundEngine { + getName(): string { + return 'Ring Cymbal'; + } + + getDescription(): string { + return 'Metallic cymbal using ring modulation with noise and multiple oscillators'; + } + + getType() { + return 'generative' as const; + } + + getCategory() { + return 'Percussion' as const; + } + + protected getOrchestra(): string { + return ` +instr 1 + iBaseFreq chnget "baseFreq" + iOvertone1Freq chnget "overtone1Freq" + iOvertone2Freq chnget "overtone2Freq" + iOvertone3Freq chnget "overtone3Freq" + iOvertone1Vol chnget "overtone1Vol" + iOvertone2Vol chnget "overtone2Vol" + iOvertone3Vol chnget "overtone3Vol" + iFilterCutoff chnget "filterCutoff" + iResonance chnget "resonance" + iDecay chnget "decay" + iAttack chnget "attack" + iNoise chnget "noise" + iBrightness chnget "brightness" + iSpread chnget "spread" + + idur = p3 + iDecayTime = iDecay * idur + iAttackTime = iAttack * idur + + ; Exponential decay envelope with attack + kEnv linseg 0, iAttackTime, 1, iDecayTime - iAttackTime, 0.001, 0.001, 0 + kEnv = kEnv * kEnv + + ; Generate white noise source + aNoise noise 1, 0 + + ; Generate impulse oscillators at different frequencies + aOsc1 oscili 1, iBaseFreq + aOsc2 oscili iOvertone1Vol, iOvertone1Freq + aOsc3 oscili iOvertone2Vol, iOvertone2Freq + aOsc4 oscili iOvertone3Vol, iOvertone3Freq + + ; Ring modulation: multiply noise with oscillators + aRing1 = aNoise * aOsc1 + aRing2 = aNoise * aOsc2 + aRing3 = aNoise * aOsc3 + aRing4 = aNoise * aOsc4 + + ; Mix ring modulated signals + aMix = (aRing1 + aRing2 + aRing3 + aRing4) * 0.25 + + ; Add raw noise for character + aMix = aMix * (1 - iNoise) + aNoise * iNoise * 0.3 + + ; Apply resonant high-pass filter for metallic character + aFiltered butterhp aMix, iFilterCutoff, iResonance + + ; Additional high-pass for brightness + if iBrightness > 0.3 then + aFiltered butterhp aFiltered, iFilterCutoff * (1 + iBrightness), iResonance * 0.5 + endif + + ; Apply envelope + aOut = aFiltered * kEnv * 0.4 + + ; Stereo spread using slightly different filter parameters + iFilterCutoffR = iFilterCutoff * (1 + iSpread * 0.1) + aFilteredR butterhp aMix, iFilterCutoffR, iResonance + + if iBrightness > 0.3 then + aFilteredR butterhp aFilteredR, iFilterCutoffR * (1 + iBrightness), iResonance * 0.5 + endif + + aOutR = aFilteredR * kEnv * 0.4 + + outs aOut, aOutR +endin +`; + } + + protected getParametersForCsound(params: RingCymbalParams): CsoundParameter[] { + return [ + { channelName: 'baseFreq', value: params.baseFreq }, + { channelName: 'overtone1Freq', value: params.overtone1Freq }, + { channelName: 'overtone2Freq', value: params.overtone2Freq }, + { channelName: 'overtone3Freq', value: params.overtone3Freq }, + { channelName: 'overtone1Vol', value: params.overtone1Vol }, + { channelName: 'overtone2Vol', value: params.overtone2Vol }, + { channelName: 'overtone3Vol', value: params.overtone3Vol }, + { channelName: 'filterCutoff', value: params.filterCutoff }, + { channelName: 'resonance', value: params.resonance }, + { channelName: 'decay', value: params.decay }, + { channelName: 'attack', value: params.attack }, + { channelName: 'noise', value: params.noise }, + { channelName: 'brightness', value: params.brightness }, + { channelName: 'spread', value: params.spread }, + ]; + } + + randomParams(pitchLock?: PitchLock): RingCymbalParams { + const baseFreq = pitchLock?.enabled ? pitchLock.frequency : this.randomRange(800, 1800); + + const inharmonicRatios = [1.4, 1.7, 2.1, 2.3, 2.9, 3.1, 3.7, 4.3]; + + return { + baseFreq, + overtone1Freq: baseFreq * this.randomChoice(inharmonicRatios), + overtone2Freq: baseFreq * this.randomChoice(inharmonicRatios), + overtone3Freq: baseFreq * this.randomChoice(inharmonicRatios), + overtone1Vol: this.randomRange(0.4, 1.0), + overtone2Vol: this.randomRange(0.3, 0.9), + overtone3Vol: this.randomRange(0.2, 0.7), + filterCutoff: this.randomRange(3000, 8000), + resonance: this.randomRange(2, 8), + decay: this.randomRange(0.3, 0.9), + attack: this.randomRange(0.001, 0.02), + noise: this.randomRange(0.1, 0.5), + brightness: this.randomRange(0, 0.8), + spread: this.randomRange(0.01, 0.15), + }; + } + + mutateParams( + params: RingCymbalParams, + mutationAmount: number = 0.15, + pitchLock?: PitchLock + ): RingCymbalParams { + const baseFreq = pitchLock?.enabled ? pitchLock.frequency : params.baseFreq; + const freqRatio = baseFreq / params.baseFreq; + + return { + baseFreq, + overtone1Freq: this.mutateValue(params.overtone1Freq * freqRatio, mutationAmount, baseFreq * 1.2, baseFreq * 5), + overtone2Freq: this.mutateValue(params.overtone2Freq * freqRatio, mutationAmount, baseFreq * 1.2, baseFreq * 5), + overtone3Freq: this.mutateValue(params.overtone3Freq * freqRatio, mutationAmount, baseFreq * 1.2, baseFreq * 5), + overtone1Vol: this.mutateValue(params.overtone1Vol, mutationAmount, 0.2, 1.0), + overtone2Vol: this.mutateValue(params.overtone2Vol, mutationAmount, 0.1, 1.0), + overtone3Vol: this.mutateValue(params.overtone3Vol, mutationAmount, 0.1, 0.9), + filterCutoff: this.mutateValue(params.filterCutoff, mutationAmount, 2000, 10000), + resonance: this.mutateValue(params.resonance, mutationAmount, 1, 12), + decay: this.mutateValue(params.decay, mutationAmount, 0.2, 1.0), + attack: this.mutateValue(params.attack, mutationAmount, 0.001, 0.05), + noise: this.mutateValue(params.noise, mutationAmount, 0, 0.7), + brightness: this.mutateValue(params.brightness, mutationAmount, 0, 1), + spread: this.mutateValue(params.spread, mutationAmount, 0.005, 0.25), + }; + } +} diff --git a/src/lib/audio/engines/Sample.ts b/src/lib/audio/engines/Sample.ts index 13160eb..a6ee8ac 100644 --- a/src/lib/audio/engines/Sample.ts +++ b/src/lib/audio/engines/Sample.ts @@ -21,6 +21,10 @@ export class Sample implements SynthEngine { return 'sample' as const; } + getCategory() { + return 'Utility' as const; + } + async loadFile(file: File): Promise { const arrayBuffer = await file.arrayBuffer(); const audioContext = new AudioContext(); diff --git a/src/lib/audio/engines/Snare.ts b/src/lib/audio/engines/Snare.ts index 6a98995..c33457a 100644 --- a/src/lib/audio/engines/Snare.ts +++ b/src/lib/audio/engines/Snare.ts @@ -23,7 +23,7 @@ interface SnareParams { export class Snare implements SynthEngine { getName(): string { - return 'Snare'; + return 'Noise Snare'; } getDescription(): string { @@ -34,6 +34,10 @@ export class Snare implements SynthEngine { return 'generative' as const; } + getCategory() { + return 'Percussion' as const; + } + randomParams(pitchLock?: PitchLock): SnareParams { return { baseFreq: pitchLock ? this.freqToParam(pitchLock.frequency) : 0.3 + Math.random() * 0.4, diff --git a/src/lib/audio/engines/SubtractiveThreeOsc.ts b/src/lib/audio/engines/SubtractiveThreeOsc.ts index 6db291c..772c86b 100644 --- a/src/lib/audio/engines/SubtractiveThreeOsc.ts +++ b/src/lib/audio/engines/SubtractiveThreeOsc.ts @@ -50,6 +50,10 @@ export class SubtractiveThreeOsc extends CsoundEngine return 'generative' as const; } + getCategory() { + return 'Subtractive' as const; + } + protected getOrchestra(): string { return ` instr 1 diff --git a/src/lib/audio/engines/TechnoKick.ts b/src/lib/audio/engines/TechnoKick.ts new file mode 100644 index 0000000..a576820 --- /dev/null +++ b/src/lib/audio/engines/TechnoKick.ts @@ -0,0 +1,187 @@ +import { CsoundEngine, type CsoundParameter } from './base/CsoundEngine'; +import type { PitchLock } from './base/SynthEngine'; + +interface TechnoKickParams { + startFreq: number; + endFreq: number; + freqDecay: number; + resonance: number; + cutoffStart: number; + cutoffEnd: number; + cutoffDecay: number; + ampAttack: number; + ampDecay: number; + noiseMix: number; + punch: number; + stereoWidth: number; +} + +export class TechnoKick extends CsoundEngine { + getName(): string { + return 'Techno Kick'; + } + + getDescription(): string { + return 'Noise through resonant low-pass filter with frequency sweep and RMS compression for punchy electronic kicks'; + } + + getType() { + return 'generative' as const; + } + + getCategory() { + return 'Percussion' as const; + } + + protected getOrchestra(): string { + return ` +instr 1 + iStartFreq chnget "startFreq" + iEndFreq chnget "endFreq" + iFreqDecay chnget "freqDecay" + iResonance chnget "resonance" + iCutoffStart chnget "cutoffStart" + iCutoffEnd chnget "cutoffEnd" + iCutoffDecay chnget "cutoffDecay" + iAmpAttack chnget "ampAttack" + iAmpDecay chnget "ampDecay" + iNoiseMix chnget "noiseMix" + iPunch chnget "punch" + iStereoWidth chnget "stereoWidth" + + idur = p3 + iFreqDecayTime = iFreqDecay * idur + iCutoffDecayTime = iCutoffDecay * idur + iAmpAttackTime = iAmpAttack * idur + iAmpDecayTime = iAmpDecay * idur + + ; Generate random noise + aNoise noise 1, 0 + + ; Frequency envelope for the filter cutoff (exponential sweep) + kFreqEnv expseg iStartFreq, iFreqDecayTime, iEndFreq, idur - iFreqDecayTime, iEndFreq + + ; Cutoff modulation envelope + kCutoffEnv expseg iCutoffStart, iCutoffDecayTime, iCutoffEnd, idur - iCutoffDecayTime, iCutoffEnd + + ; Apply resonant low-pass filter (rezzy) + aFiltered rezzy aNoise * (1 + iNoiseMix), kCutoffEnv, iResonance + + ; Add sine sub-bass component for more weight + kSubFreqEnv expseg iStartFreq * 0.5, iFreqDecayTime, iEndFreq * 0.5, idur - iFreqDecayTime, iEndFreq * 0.5 + aSubBass oscili 0.6, kSubFreqEnv + + ; Mix filtered noise and sub-bass + aMix = aFiltered * 0.5 + aSubBass + + ; Amplitude envelope (exponential) + kAmpEnv expseg 0.001, iAmpAttackTime, 1, iAmpDecayTime, 0.001, idur - iAmpAttackTime - iAmpDecayTime, 0.001 + + ; Apply amplitude envelope + aEnveloped = aMix * kAmpEnv + + ; RMS compression for punch + ; Calculate RMS of signal + kRMS rms aEnveloped + if kRMS < 0.01 then + kCompGain = 1 + else + kCompGain = 1 + iPunch * (0.3 / kRMS - 1) + endif + kCompGain limit kCompGain, 0.5, 3 + + aOut = aEnveloped * kCompGain + + ; Right channel with stereo width + iStartFreqR = iStartFreq * (1 + iStereoWidth * 0.01) + iEndFreqR = iEndFreq * (1 + iStereoWidth * 0.01) + + kFreqEnvR expseg iStartFreqR, iFreqDecayTime, iEndFreqR, idur - iFreqDecayTime, iEndFreqR + kCutoffEnvR expseg iCutoffStart * (1 + iStereoWidth * 0.02), iCutoffDecayTime, iCutoffEnd * (1 + iStereoWidth * 0.02), idur - iCutoffDecayTime, iCutoffEnd * (1 + iStereoWidth * 0.02) + + aNoiseR noise 1, 0 + aFilteredR rezzy aNoiseR * (1 + iNoiseMix), kCutoffEnvR, iResonance + + kSubFreqEnvR expseg iStartFreqR * 0.5, iFreqDecayTime, iEndFreqR * 0.5, idur - iFreqDecayTime, iEndFreqR * 0.5 + aSubBassR oscili 0.6, kSubFreqEnvR + + aMixR = aFilteredR * 0.5 + aSubBassR + aEnvelopedR = aMixR * kAmpEnv + + kRMSR rms aEnvelopedR + if kRMSR < 0.01 then + kCompGainR = 1 + else + kCompGainR = 1 + iPunch * (0.3 / kRMSR - 1) + endif + kCompGainR limit kCompGainR, 0.5, 3 + + aOutR = aEnvelopedR * kCompGainR + + outs aOut, aOutR +endin +`; + } + + protected getParametersForCsound(params: TechnoKickParams): CsoundParameter[] { + return [ + { channelName: 'startFreq', value: params.startFreq }, + { channelName: 'endFreq', value: params.endFreq }, + { channelName: 'freqDecay', value: params.freqDecay }, + { channelName: 'resonance', value: params.resonance }, + { channelName: 'cutoffStart', value: params.cutoffStart }, + { channelName: 'cutoffEnd', value: params.cutoffEnd }, + { channelName: 'cutoffDecay', value: params.cutoffDecay }, + { channelName: 'ampAttack', value: params.ampAttack }, + { channelName: 'ampDecay', value: params.ampDecay }, + { channelName: 'noiseMix', value: params.noiseMix }, + { channelName: 'punch', value: params.punch }, + { channelName: 'stereoWidth', value: params.stereoWidth }, + ]; + } + + randomParams(pitchLock?: PitchLock): TechnoKickParams { + const endFreqChoices = [40, 45, 50, 55, 60, 70, 80]; + const endFreq = pitchLock?.enabled + ? pitchLock.frequency + : this.randomChoice(endFreqChoices) * this.randomRange(0.95, 1.05); + + return { + startFreq: this.randomRange(800, 1200), + endFreq, + freqDecay: this.randomRange(0.05, 0.25), + resonance: this.randomRange(5, 40), + cutoffStart: this.randomRange(300, 800), + cutoffEnd: this.randomRange(80, 200), + cutoffDecay: this.randomRange(0.1, 0.4), + ampAttack: this.randomRange(0.001, 0.005), + ampDecay: this.randomRange(0.2, 0.6), + noiseMix: this.randomRange(0.1, 0.8), + punch: this.randomRange(0.3, 0.9), + stereoWidth: this.randomRange(0, 0.3), + }; + } + + mutateParams( + params: TechnoKickParams, + mutationAmount: number = 0.15, + pitchLock?: PitchLock + ): TechnoKickParams { + const endFreq = pitchLock?.enabled ? pitchLock.frequency : params.endFreq; + + return { + startFreq: this.mutateValue(params.startFreq, mutationAmount, 600, 1500), + endFreq, + freqDecay: this.mutateValue(params.freqDecay, mutationAmount, 0.02, 0.4), + resonance: this.mutateValue(params.resonance, mutationAmount, 3, 50), + cutoffStart: this.mutateValue(params.cutoffStart, mutationAmount, 200, 1000), + cutoffEnd: this.mutateValue(params.cutoffEnd, mutationAmount, 60, 300), + cutoffDecay: this.mutateValue(params.cutoffDecay, mutationAmount, 0.05, 0.6), + ampAttack: this.mutateValue(params.ampAttack, mutationAmount, 0.001, 0.01), + ampDecay: this.mutateValue(params.ampDecay, mutationAmount, 0.1, 0.8), + noiseMix: this.mutateValue(params.noiseMix, mutationAmount, 0, 1), + punch: this.mutateValue(params.punch, mutationAmount, 0.1, 1), + stereoWidth: this.mutateValue(params.stereoWidth, mutationAmount, 0, 0.5), + }; + } +} diff --git a/src/lib/audio/engines/TwoOpFM.ts b/src/lib/audio/engines/TwoOpFM.ts index f0a1aa6..9ed7891 100644 --- a/src/lib/audio/engines/TwoOpFM.ts +++ b/src/lib/audio/engines/TwoOpFM.ts @@ -73,6 +73,10 @@ export class TwoOpFM implements SynthEngine { return 'generative' as const; } + getCategory() { + return 'FM' as const; + } + generate(params: TwoOpFMParams, sampleRate: number, duration: number): [Float32Array, Float32Array] { const numSamples = Math.floor(sampleRate * duration); const leftBuffer = new Float32Array(numSamples); diff --git a/src/lib/audio/engines/ZzfxEngine.ts b/src/lib/audio/engines/ZzfxEngine.ts index b1a3022..2ca5b0a 100644 --- a/src/lib/audio/engines/ZzfxEngine.ts +++ b/src/lib/audio/engines/ZzfxEngine.ts @@ -38,6 +38,10 @@ export class ZzfxEngine implements SynthEngine { return 'generative' as const; } + getCategory() { + return 'Experimental' as const; + } + generate(params: ZzfxParams, sampleRate: number, duration: number): [Float32Array, Float32Array] { // ZZFX uses 44100 sample rate internally const zzfxSampleRate = 44100; diff --git a/src/lib/audio/engines/base/SynthEngine.ts b/src/lib/audio/engines/base/SynthEngine.ts index 94c8e1b..76ceb5a 100644 --- a/src/lib/audio/engines/base/SynthEngine.ts +++ b/src/lib/audio/engines/base/SynthEngine.ts @@ -6,6 +6,17 @@ export type EngineType = 'generative' | 'sample' | 'input'; +export type EngineCategory = + | 'Additive' + | 'Subtractive' + | 'FM' + | 'Percussion' + | 'Noise' + | 'Physical' + | 'Modulation' + | 'Experimental' + | 'Utility'; + export interface PitchLock { enabled: boolean; frequency: number; // Frequency in Hz @@ -15,6 +26,7 @@ export interface SynthEngine { getName(): string; getDescription(): string; getType(): EngineType; + getCategory(): EngineCategory; generate(params: T, sampleRate: number, duration: number, pitchLock?: PitchLock): [Float32Array, Float32Array] | Promise<[Float32Array, Float32Array]>; randomParams(pitchLock?: PitchLock): T; mutateParams(params: T, mutationAmount?: number, pitchLock?: PitchLock): T; diff --git a/src/lib/audio/engines/registry.ts b/src/lib/audio/engines/registry.ts index 6e97070..0bc39a0 100644 --- a/src/lib/audio/engines/registry.ts +++ b/src/lib/audio/engines/registry.ts @@ -19,6 +19,12 @@ import { ParticleNoise } from './ParticleNoise'; import { DustNoise } from './DustNoise'; import { SubtractiveThreeOsc } from './SubtractiveThreeOsc'; import { MassiveAdditive } from './MassiveAdditive'; +import { FormantPopDrum } from './FormantPopDrum'; +import { TechnoKick } from './TechnoKick'; +import { FMTomTom } from './FMTomTom'; +import { RingCymbal } from './RingCymbal'; +import { AdditiveBass } from './AdditiveBass'; +import { FeedbackSnare } from './FeedbackSnare'; export const engines: SynthEngine[] = [ new Sample(), @@ -34,6 +40,12 @@ export const engines: SynthEngine[] = [ new Snare(), new BassDrum(), new HiHat(), + new FormantPopDrum(), + new TechnoKick(), + new FMTomTom(), + new RingCymbal(), + new AdditiveBass(), + new FeedbackSnare(), new Ring(), new KarplusStrong(), new AdditiveEngine(), diff --git a/src/lib/utils/settings.ts b/src/lib/utils/settings.ts index e53f60b..e162a4b 100644 --- a/src/lib/utils/settings.ts +++ b/src/lib/utils/settings.ts @@ -8,6 +8,7 @@ const STORAGE_KEYS = { DURATION: 'duration', PITCH_LOCK_ENABLED: 'pitchLockEnabled', PITCH_LOCK_FREQUENCY: 'pitchLockFrequency', + EXPANDED_CATEGORIES: 'expandedCategories', } as const; export function loadVolume(): number { @@ -45,3 +46,20 @@ export function loadPitchLockFrequency(): number { export function savePitchLockFrequency(frequency: number): void { localStorage.setItem(STORAGE_KEYS.PITCH_LOCK_FREQUENCY, frequency.toString()); } + +export function loadExpandedCategories(): Set { + const stored = localStorage.getItem(STORAGE_KEYS.EXPANDED_CATEGORIES); + if (stored) { + try { + const parsed = JSON.parse(stored); + return new Set(Array.isArray(parsed) ? parsed : []); + } catch { + return new Set(); + } + } + return new Set(); +} + +export function saveExpandedCategories(categories: Set): void { + localStorage.setItem(STORAGE_KEYS.EXPANDED_CATEGORIES, JSON.stringify(Array.from(categories))); +}