Compare commits

..

2 Commits

Author SHA1 Message Date
38479f0253 Adding more CSound models 2025-10-13 13:45:33 +02:00
580aa4b96f Spectral additive 2025-10-13 13:09:59 +02:00
31 changed files with 2179 additions and 23 deletions

View File

@ -55,6 +55,69 @@ Opens on http://localhost:8080
4. Keep all DSP code, helpers, and types in the same file 4. Keep all DSP code, helpers, and types in the same file
5. Register in `src/lib/audio/engines/registry.ts` 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<ParamsType>` 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<MyParams> {
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 ### Adding Audio Processors
1. Create a single file in `src/lib/audio/processors/` implementing the `AudioProcessor` interface 1. Create a single file in `src/lib/audio/processors/` implementing the `AudioProcessor` interface

View File

@ -9,7 +9,7 @@
import type { EngineType } from "./lib/audio/engines/base/SynthEngine"; import type { EngineType } from "./lib/audio/engines/base/SynthEngine";
import { AudioService } from "./lib/audio/services/AudioService"; import { AudioService } from "./lib/audio/services/AudioService";
import { downloadWAV } from "./lib/audio/utils/WAVEncoder"; 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 { cropAudio, cutAudio, processSelection } from "./lib/audio/utils/AudioEdit";
import { generateRandomColor } from "./lib/utils/colors"; import { generateRandomColor } from "./lib/utils/colors";
import { getRandomProcessor } from "./lib/audio/processors/registry"; import { getRandomProcessor } from "./lib/audio/processors/registry";
@ -19,6 +19,7 @@
import { createKeyboardHandler } from "./lib/utils/keyboard"; import { createKeyboardHandler } from "./lib/utils/keyboard";
import { parseFrequencyInput, formatFrequency } from "./lib/utils/pitch"; import { parseFrequencyInput, formatFrequency } from "./lib/utils/pitch";
import { UndoManager, type AudioState } from "./lib/utils/UndoManager"; import { UndoManager, type AudioState } from "./lib/utils/UndoManager";
import type { EngineCategory } from "./lib/audio/engines/base/SynthEngine";
let currentEngineIndex = $state(0); let currentEngineIndex = $state(0);
const engine = $derived(engines[currentEngineIndex]); const engine = $derived(engines[currentEngineIndex]);
@ -46,6 +47,7 @@
let selectionEnd = $state<number | null>(null); let selectionEnd = $state<number | null>(null);
let canUndo = $state(false); let canUndo = $state(false);
let sidebarOpen = $state(false); let sidebarOpen = $state(false);
let expandedCategories = $state<Set<string>>(loadExpandedCategories());
const showDuration = $derived(engineType !== 'sample'); const showDuration = $derived(engineType !== 'sample');
const showRandomButton = $derived(engineType === 'generative'); const showRandomButton = $derived(engineType === 'generative');
@ -74,6 +76,33 @@
savePitchLockFrequency(pitchLockFrequency); savePitchLockFrequency(pitchLockFrequency);
}); });
$effect(() => {
saveExpandedCategories(expandedCategories);
});
// Group engines by category
const enginesByCategory = $derived.by(() => {
const grouped = new Map<EngineCategory, typeof engines>();
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(() => { onMount(() => {
audioService.setPlaybackUpdateCallback((position) => { audioService.setPlaybackUpdateCallback((position) => {
playbackPosition = position; playbackPosition = position;
@ -485,15 +514,32 @@
<div class="content-wrapper"> <div class="content-wrapper">
<div class="sidebar" class:open={sidebarOpen}> <div class="sidebar" class:open={sidebarOpen}>
<div class="sidebar-content"> <div class="sidebar-content">
{#each engines as currentEngine, index} {#each Array.from(enginesByCategory.entries()) as [category, categoryEngines]}
<button <div class="category-section">
class="engine-button" <button
class:active={currentEngineIndex === index} class="category-header"
data-description={currentEngine.getDescription()} class:collapsed={!expandedCategories.has(category)}
onclick={() => switchEngine(index)} onclick={() => toggleCategory(category)}
> >
{currentEngine.getName()} <svg class="category-arrow" width="12" height="12" viewBox="0 0 12 12" fill="none">
</button> <path d="M3 4.5L6 7.5L9 4.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="square"/>
</svg>
<span>{category}</span>
</button>
{#if expandedCategories.has(category)}
{#each categoryEngines as currentEngine}
{@const index = engines.indexOf(currentEngine)}
<button
class="engine-button"
class:active={currentEngineIndex === index}
data-description={currentEngine.getDescription()}
onclick={() => switchEngine(index)}
>
{currentEngine.getName()}
</button>
{/each}
{/if}
</div>
{/each} {/each}
</div> </div>
</div> </div>
@ -693,12 +739,50 @@
border-radius: 0; 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 { .engine-button {
opacity: 0.7; opacity: 0.7;
position: relative; position: relative;
flex-shrink: 0; flex-shrink: 0;
font-size: 0.75rem; font-size: 0.75rem;
padding: 0.5rem 0.5rem; padding: 0.5rem 0.5rem 0.5rem 1rem;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;

View File

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

View File

@ -80,7 +80,7 @@ export interface AdditiveParams {
export class AdditiveEngine implements SynthEngine<AdditiveParams> { export class AdditiveEngine implements SynthEngine<AdditiveParams> {
getName(): string { getName(): string {
return 'Prism'; return 'Glass Prism';
} }
getDescription(): string { getDescription(): string {
@ -91,6 +91,10 @@ export class AdditiveEngine implements SynthEngine<AdditiveParams> {
return 'generative' as const; return 'generative' as const;
} }
getCategory() {
return 'Additive' as const;
}
generate(params: AdditiveParams, sampleRate: number, duration: number): [Float32Array, Float32Array] { generate(params: AdditiveParams, sampleRate: number, duration: number): [Float32Array, Float32Array] {
const numSamples = Math.floor(sampleRate * duration); const numSamples = Math.floor(sampleRate * duration);
const leftBuffer = new Float32Array(numSamples); const leftBuffer = new Float32Array(numSamples);

View File

@ -57,7 +57,7 @@ interface BassDrumParams {
export class BassDrum implements SynthEngine { export class BassDrum implements SynthEngine {
getName(): string { getName(): string {
return 'Kick'; return 'Dark Kick';
} }
getDescription(): string { getDescription(): string {
@ -68,6 +68,10 @@ export class BassDrum implements SynthEngine {
return 'generative' as const; return 'generative' as const;
} }
getCategory() {
return 'Percussion' as const;
}
randomParams(pitchLock?: PitchLock): BassDrumParams { randomParams(pitchLock?: PitchLock): BassDrumParams {
// Choose a kick character/style // Choose a kick character/style
const styleRoll = Math.random(); const styleRoll = Math.random();

View File

@ -73,6 +73,10 @@ export class Benjolin implements SynthEngine<BenjolinParams> {
return 'generative' as const; return 'generative' as const;
} }
getCategory() {
return 'Experimental' as const;
}
generate(params: BenjolinParams, sampleRate: number, duration: number): [Float32Array, Float32Array] { generate(params: BenjolinParams, sampleRate: number, duration: number): [Float32Array, Float32Array] {
const numSamples = Math.floor(duration * sampleRate); const numSamples = Math.floor(duration * sampleRate);
const left = new Float32Array(numSamples); const left = new Float32Array(numSamples);

View File

@ -67,6 +67,10 @@ export class DubSiren implements SynthEngine<DubSirenParams> {
return 'generative' as const; return 'generative' as const;
} }
getCategory() {
return 'Experimental' as const;
}
generate(params: DubSirenParams, sampleRate: number, duration: number): [Float32Array, Float32Array] { generate(params: DubSirenParams, sampleRate: number, duration: number): [Float32Array, Float32Array] {
const numSamples = Math.floor(sampleRate * duration); const numSamples = Math.floor(sampleRate * duration);
const leftBuffer = new Float32Array(numSamples); const leftBuffer = new Float32Array(numSamples);

View File

@ -46,6 +46,10 @@ export class DustNoise implements SynthEngine {
return 'generative' as const; return 'generative' as const;
} }
getCategory() {
return 'Noise' as const;
}
randomParams(pitchLock?: PitchLock): DustNoiseParams { randomParams(pitchLock?: PitchLock): DustNoiseParams {
const characterBias = Math.random(); const characterBias = Math.random();

View File

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

View File

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

View File

@ -0,0 +1,408 @@
import { CsoundEngine, type CsoundParameter } from './base/CsoundEngine';
import type { PitchLock } from './base/SynthEngine';
enum VowelType {
A,
E,
I,
O,
U,
AE,
OE,
UE,
}
enum ModulationType {
SimpleFM,
DoubleFM,
RingMod,
CrossFM,
}
interface FormantBand {
frequency: number;
bandwidth: number;
amplitude: number;
}
interface FormantFMParams {
baseFreq: number;
vowel: VowelType;
vowelMorph: number;
modulationType: ModulationType;
modIndex: number;
modRatio: number;
attack: number;
decay: number;
sustain: number;
release: number;
brightness: number;
vibrato: number;
vibratoRate: number;
detune: number;
noise: number;
formantLFORate: number;
formantLFODepth: number;
modIndexLFORate: number;
modIndexLFODepth: number;
chaos: number;
}
export class FormantFM extends CsoundEngine<FormantFMParams> {
getName(): string {
return 'Formant FM';
}
getDescription(): string {
return 'FM synthesis with formant filters creating vowel-like sounds and vocal textures';
}
getType() {
return 'generative' as const;
}
getCategory() {
return 'FM' as const;
}
protected getOrchestra(): string {
return `
instr 1
iBaseFreq chnget "baseFreq"
iVowel chnget "vowel"
iVowelMorph chnget "vowelMorph"
iModType chnget "modulationType"
iModIndex chnget "modIndex"
iModRatio chnget "modRatio"
iAttack chnget "attack"
iDecay chnget "decay"
iSustain chnget "sustain"
iRelease chnget "release"
iBrightness chnget "brightness"
iVibrato chnget "vibrato"
iVibratoRate chnget "vibratoRate"
iDetune chnget "detune"
iNoise chnget "noise"
iFormantLFORate chnget "formantLFORate"
iFormantLFODepth chnget "formantLFODepth"
iModIndexLFORate chnget "modIndexLFORate"
iModIndexLFODepth chnget "modIndexLFODepth"
iChaos chnget "chaos"
idur = p3
iAttackTime = iAttack * idur
iDecayTime = iDecay * idur
iReleaseTime = iRelease * idur
; Envelope
kEnv madsr iAttackTime, iDecayTime, iSustain, iReleaseTime
; Vibrato LFO with chaos
kVib oscili iVibrato * iBaseFreq * 0.02, iVibratoRate
kChaosLFO1 oscili iChaos * iBaseFreq * 0.01, iVibratoRate * 1.618
kChaosLFO2 oscili iChaos * iBaseFreq * 0.005, iVibratoRate * 2.414
kFreq = iBaseFreq + kVib + kChaosLFO1 + kChaosLFO2
; Get formant parameters based on vowel type
if iVowel == 0 then
; A (as in "father")
iF1 = 730
iF2 = 1090
iF3 = 2440
iA1 = 1.0
iA2 = 0.5
iA3 = 0.25
iBW1 = 80
iBW2 = 90
iBW3 = 120
elseif iVowel == 1 then
; E (as in "bet")
iF1 = 530
iF2 = 1840
iF3 = 2480
iA1 = 1.0
iA2 = 0.6
iA3 = 0.3
iBW1 = 60
iBW2 = 100
iBW3 = 120
elseif iVowel == 2 then
; I (as in "bit")
iF1 = 390
iF2 = 1990
iF3 = 2550
iA1 = 1.0
iA2 = 0.7
iA3 = 0.2
iBW1 = 50
iBW2 = 100
iBW3 = 120
elseif iVowel == 3 then
; O (as in "boat")
iF1 = 570
iF2 = 840
iF3 = 2410
iA1 = 1.0
iA2 = 0.45
iA3 = 0.28
iBW1 = 70
iBW2 = 80
iBW3 = 100
elseif iVowel == 4 then
; U (as in "boot")
iF1 = 440
iF2 = 1020
iF3 = 2240
iA1 = 1.0
iA2 = 0.4
iA3 = 0.2
iBW1 = 70
iBW2 = 80
iBW3 = 100
elseif iVowel == 5 then
; AE (as in "bat")
iF1 = 660
iF2 = 1720
iF3 = 2410
iA1 = 1.0
iA2 = 0.55
iA3 = 0.3
iBW1 = 80
iBW2 = 90
iBW3 = 120
elseif iVowel == 6 then
; OE (as in "bird")
iF1 = 490
iF2 = 1350
iF3 = 1690
iA1 = 1.0
iA2 = 0.5
iA3 = 0.4
iBW1 = 70
iBW2 = 80
iBW3 = 100
else
; UE (as in "about")
iF1 = 520
iF2 = 1190
iF3 = 2390
iA1 = 1.0
iA2 = 0.45
iA3 = 0.25
iBW1 = 70
iBW2 = 80
iBW3 = 110
endif
; Modulate formant frequencies with multiple LFOs
kFormantShift = 1 + (iVowelMorph - 0.5) * 0.4
kFormantLFO oscili iFormantLFODepth * 0.3, iFormantLFORate
kFormantShiftModulated = kFormantShift + kFormantLFO
kF1 = iF1 * kFormantShiftModulated
kF2 = iF2 * kFormantShiftModulated
kF3 = iF3 * kFormantShiftModulated
; Modulation index LFO
kModIndexLFO oscili iModIndexLFODepth * iModIndex, iModIndexLFORate
kModIndexDynamic = iModIndex + kModIndexLFO
; Generate carrier and modulator based on modulation type
kModFreq = kFreq * iModRatio
if iModType == 0 then
; Simple FM with dynamic modulation
aMod oscili kModIndexDynamic * kModFreq, kModFreq
aCarrier oscili 0.5, kFreq + aMod
elseif iModType == 1 then
; Double FM (modulator modulates itself) with chaos
kChaosModDepth = 1 + (iChaos * 0.5)
aMod1 oscili kModIndexDynamic * 0.3 * kModFreq * kChaosModDepth, kModFreq * 0.5
aMod2 oscili kModIndexDynamic * kModFreq, kModFreq + aMod1
aCarrier oscili 0.5, kFreq + aMod2
elseif iModType == 2 then
; Ring modulation with frequency wobble
kRingModWobble oscili iChaos * 0.2, iFormantLFORate * 0.7
aMod oscili 0.5, kModFreq * (1 + kRingModWobble)
aCarrierTemp oscili 0.5, kFreq
aCarrier = aCarrierTemp * aMod
else
; Cross FM with multiple carriers
aMod oscili kModIndexDynamic * kModFreq, kModFreq
aCarrier1 oscili 0.4, kFreq + aMod
aCarrier2 oscili 0.3, kFreq * 0.5 + aMod * 0.5
kThirdCarrierFreq = kFreq * (1.5 + iChaos * 0.3)
aCarrier3 oscili 0.2 * iChaos, kThirdCarrierFreq + aMod * 0.3
aCarrier = aCarrier1 + aCarrier2 + aCarrier3
endif
; Add brightness via high-frequency content
aCarrierBright oscili 0.15 * iBrightness, kFreq * 2
aCarrierMix = aCarrier + aCarrierBright
; Add subtle noise for breathiness
aNoise noise 0.08 * iNoise, 0
aCarrierFinal = aCarrierMix + aNoise
; Apply formant filters (bandpass filters at formant frequencies)
aFormant1 butterbp aCarrierFinal, kF1, iBW1
aFormant1Scaled = aFormant1 * iA1
aFormant2 butterbp aCarrierFinal, kF2, iBW2
aFormant2Scaled = aFormant2 * iA2
aFormant3 butterbp aCarrierFinal, kF3, iBW3
aFormant3Scaled = aFormant3 * iA3
; Mix formants
aMix = (aFormant1Scaled + aFormant2Scaled + aFormant3Scaled) * 0.6
; Apply envelope
aOut = aMix * kEnv
; Stereo - slightly different phase for right channel
iDetuneFactor = 1 + (iDetune * 0.5)
kFreqR = iBaseFreq * iDetuneFactor + kVib
; Regenerate right channel with detuned frequency
kModFreqR = kFreqR * iModRatio
if iModType == 0 then
aModR oscili iModIndex * kModFreqR, kModFreqR
aCarrierR oscili 0.5, kFreqR + aModR
elseif iModType == 1 then
aMod1R oscili iModIndex * 0.3 * kModFreqR, kModFreqR * 0.5
aMod2R oscili iModIndex * kModFreqR, kModFreqR + aMod1R
aCarrierR oscili 0.5, kFreqR + aMod2R
elseif iModType == 2 then
aModR oscili 0.5, kModFreqR
aCarrierTempR oscili 0.5, kFreqR
aCarrierR = aCarrierTempR * aModR
else
aModR oscili iModIndex * kModFreqR, kModFreqR
aCarrier1R oscili 0.4, kFreqR + aModR
aCarrier2R oscili 0.3, kFreqR * 0.5 + aModR * 0.5
aCarrierR = aCarrier1R + aCarrier2R
endif
aCarrierBrightR oscili 0.15 * iBrightness, kFreqR * 2
aCarrierMixR = aCarrierR + aCarrierBrightR
aNoiseR noise 0.08 * iNoise, 0
aCarrierFinalR = aCarrierMixR + aNoiseR
kF1R = iF1 * kFormantShift
kF2R = iF2 * kFormantShift
kF3R = iF3 * kFormantShift
aFormant1R butterbp aCarrierFinalR, kF1R, iBW1
aFormant1ScaledR = aFormant1R * iA1
aFormant2R butterbp aCarrierFinalR, kF2R, iBW2
aFormant2ScaledR = aFormant2R * iA2
aFormant3R butterbp aCarrierFinalR, kF3R, iBW3
aFormant3ScaledR = aFormant3R * iA3
aMixR = (aFormant1ScaledR + aFormant2ScaledR + aFormant3ScaledR) * 0.6
aOutR = aMixR * kEnv
outs aOut, aOutR
endin
`;
}
protected getParametersForCsound(params: FormantFMParams): CsoundParameter[] {
return [
{ channelName: 'baseFreq', value: params.baseFreq },
{ channelName: 'vowel', value: params.vowel },
{ channelName: 'vowelMorph', value: params.vowelMorph },
{ channelName: 'modulationType', value: params.modulationType },
{ channelName: 'modIndex', value: params.modIndex },
{ channelName: 'modRatio', value: params.modRatio },
{ channelName: 'attack', value: params.attack },
{ channelName: 'decay', value: params.decay },
{ channelName: 'sustain', value: params.sustain },
{ channelName: 'release', value: params.release },
{ channelName: 'brightness', value: params.brightness },
{ channelName: 'vibrato', value: params.vibrato },
{ channelName: 'vibratoRate', value: params.vibratoRate },
{ channelName: 'detune', value: params.detune },
{ channelName: 'noise', value: params.noise },
{ channelName: 'formantLFORate', value: params.formantLFORate },
{ channelName: 'formantLFODepth', value: params.formantLFODepth },
{ channelName: 'modIndexLFORate', value: params.modIndexLFORate },
{ channelName: 'modIndexLFODepth', value: params.modIndexLFODepth },
{ channelName: 'chaos', value: params.chaos },
];
}
randomParams(pitchLock?: PitchLock): FormantFMParams {
const baseFreqChoices = [82.4, 110, 146.8, 220, 293.7, 440];
const baseFreq = pitchLock?.enabled
? pitchLock.frequency
: this.randomChoice(baseFreqChoices) * this.randomRange(0.95, 1.05);
const modulationRatios = [0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 5, 7, 9];
return {
baseFreq,
vowel: this.randomInt(0, 7) as VowelType,
vowelMorph: this.randomRange(0.3, 0.7),
modulationType: this.randomInt(0, 3) as ModulationType,
modIndex: this.randomRange(1, 10),
modRatio: this.randomChoice(modulationRatios),
attack: this.randomRange(0.001, 0.15),
decay: this.randomRange(0.05, 0.25),
sustain: this.randomRange(0.3, 0.8),
release: this.randomRange(0.1, 0.4),
brightness: this.randomRange(0, 0.6),
vibrato: this.randomRange(0, 0.5),
vibratoRate: this.randomRange(3, 8),
detune: this.randomRange(0.001, 0.01),
noise: this.randomRange(0, 0.3),
formantLFORate: this.randomRange(0.2, 6),
formantLFODepth: this.randomRange(0, 0.8),
modIndexLFORate: this.randomRange(0.5, 12),
modIndexLFODepth: this.randomRange(0, 0.7),
chaos: this.randomRange(0, 0.8),
};
}
mutateParams(
params: FormantFMParams,
mutationAmount: number = 0.15,
pitchLock?: PitchLock
): FormantFMParams {
const baseFreq = pitchLock?.enabled ? pitchLock.frequency : params.baseFreq;
const modulationRatios = [0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 5, 7, 9];
return {
baseFreq,
vowel: Math.random() < 0.1 ? (this.randomInt(0, 7) as VowelType) : params.vowel,
vowelMorph: this.mutateValue(params.vowelMorph, mutationAmount, 0, 1),
modulationType:
Math.random() < 0.08
? (this.randomInt(0, 3) as ModulationType)
: params.modulationType,
modIndex: this.mutateValue(params.modIndex, mutationAmount, 0.5, 15),
modRatio:
Math.random() < 0.12
? this.randomChoice(modulationRatios)
: params.modRatio,
attack: this.mutateValue(params.attack, mutationAmount, 0.001, 0.25),
decay: this.mutateValue(params.decay, mutationAmount, 0.02, 0.4),
sustain: this.mutateValue(params.sustain, mutationAmount, 0.1, 0.9),
release: this.mutateValue(params.release, mutationAmount, 0.05, 0.6),
brightness: this.mutateValue(params.brightness, mutationAmount, 0, 1),
vibrato: this.mutateValue(params.vibrato, mutationAmount, 0, 0.6),
vibratoRate: this.mutateValue(params.vibratoRate, mutationAmount, 2, 10),
detune: this.mutateValue(params.detune, mutationAmount, 0.0005, 0.02),
noise: this.mutateValue(params.noise, mutationAmount, 0, 0.5),
formantLFORate: this.mutateValue(params.formantLFORate, mutationAmount, 0.1, 8),
formantLFODepth: this.mutateValue(params.formantLFODepth, mutationAmount, 0, 1),
modIndexLFORate: this.mutateValue(params.modIndexLFORate, mutationAmount, 0.3, 15),
modIndexLFODepth: this.mutateValue(params.modIndexLFODepth, mutationAmount, 0, 1),
chaos: this.mutateValue(params.chaos, mutationAmount, 0, 1),
};
}
}

View File

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

View File

@ -75,6 +75,10 @@ export class FourOpFM implements SynthEngine<FourOpFMParams> {
return 'generative' as const; return 'generative' as const;
} }
getCategory() {
return 'FM' as const;
}
generate(params: FourOpFMParams, sampleRate: number, duration: number): [Float32Array, Float32Array] { generate(params: FourOpFMParams, sampleRate: number, duration: number): [Float32Array, Float32Array] {
const numSamples = Math.floor(sampleRate * duration); const numSamples = Math.floor(sampleRate * duration);
const leftBuffer = new Float32Array(numSamples); const leftBuffer = new Float32Array(numSamples);

View File

@ -19,7 +19,7 @@ interface HiHatParams {
export class HiHat implements SynthEngine { export class HiHat implements SynthEngine {
getName(): string { getName(): string {
return 'Hi-Hat'; return 'Noise Hi-Hat';
} }
getDescription(): string { getDescription(): string {
@ -30,6 +30,10 @@ export class HiHat implements SynthEngine {
return 'generative' as const; return 'generative' as const;
} }
getCategory() {
return 'Percussion' as const;
}
randomParams(pitchLock?: PitchLock): HiHatParams { randomParams(pitchLock?: PitchLock): HiHatParams {
return { return {
decay: Math.random(), decay: Math.random(),

View File

@ -21,6 +21,10 @@ export class Input implements SynthEngine<InputParams> {
return 'input' as const; return 'input' as const;
} }
getCategory() {
return 'Utility' as const;
}
async record(duration: number): Promise<void> { async record(duration: number): Promise<void> {
const stream = await navigator.mediaDevices.getUserMedia({ const stream = await navigator.mediaDevices.getUserMedia({
audio: { audio: {

View File

@ -39,6 +39,10 @@ export class KarplusStrong implements SynthEngine<KarplusStrongParams> {
return 'generative' as const; return 'generative' as const;
} }
getCategory() {
return 'Physical' as const;
}
generate(params: KarplusStrongParams, sampleRate: number, duration: number): [Float32Array, Float32Array] { generate(params: KarplusStrongParams, sampleRate: number, duration: number): [Float32Array, Float32Array] {
const numSamples = Math.floor(sampleRate * duration); const numSamples = Math.floor(sampleRate * duration);
const leftBuffer = new Float32Array(numSamples); const leftBuffer = new Float32Array(numSamples);

View File

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

View File

@ -39,7 +39,7 @@ interface NoiseDrumParams {
export class NoiseDrum implements SynthEngine { export class NoiseDrum implements SynthEngine {
getName(): string { getName(): string {
return 'NPerc'; return 'Noise Perc';
} }
getDescription(): string { getDescription(): string {
@ -50,6 +50,10 @@ export class NoiseDrum implements SynthEngine {
return 'generative' as const; return 'generative' as const;
} }
getCategory() {
return 'Percussion' as const;
}
randomParams(): NoiseDrumParams { randomParams(): NoiseDrumParams {
// Intelligent parameter generation based on correlated characteristics // Intelligent parameter generation based on correlated characteristics
@ -93,16 +97,16 @@ export class NoiseDrum implements SynthEngine {
const filterType = filterFreq < 0.3 ? const filterType = filterFreq < 0.3 ?
Math.random() * 0.35 : // Low freq prefers lowpass Math.random() * 0.35 : // Low freq prefers lowpass
filterFreq > 0.7 ? filterFreq > 0.7 ?
0.5 + Math.random() * 0.5 : // High freq prefers highpass/bandpass 0.5 + Math.random() * 0.5 : // High freq prefers highpass/bandpass
Math.random(); // Mid freq - any type Math.random(); // Mid freq - any type
// Decay time inversely correlates with frequency // Decay time inversely correlates with frequency
const decayBias = Math.random(); const decayBias = Math.random();
const ampDecay = filterFreq < 0.3 ? const ampDecay = filterFreq < 0.3 ?
0.25 + decayBias * 0.4 : // Low freq can be longer 0.25 + decayBias * 0.4 : // Low freq can be longer
filterFreq > 0.6 ? filterFreq > 0.6 ?
0.08 + decayBias * 0.35 : // High freq shorter 0.08 + decayBias * 0.35 : // High freq shorter
0.2 + decayBias * 0.45; // Mid range 0.2 + decayBias * 0.45; // Mid range
// Attack is generally very short for percussion // Attack is generally very short for percussion
const ampAttack = Math.random() < 0.85 ? const ampAttack = Math.random() < 0.85 ?
@ -372,7 +376,7 @@ export class NoiseDrum implements SynthEngine {
// Blend body resonance - SUBTLE // Blend body resonance - SUBTLE
sample = sample * (1 - params.bodyAmount * 0.4) + sample = sample * (1 - params.bodyAmount * 0.4) +
bodyFiltered.output * params.bodyAmount * 0.6 * bodyEnv; bodyFiltered.output * params.bodyAmount * 0.6 * bodyEnv;
} }
// Apply amplitude envelope // Apply amplitude envelope
@ -434,7 +438,7 @@ export class NoiseDrum implements SynthEngine {
pinkState[5] = -0.7616 * pinkState[5] - whiteNoise * 0.0168980; pinkState[5] = -0.7616 * pinkState[5] - whiteNoise * 0.0168980;
const pink = pinkState[0] + pinkState[1] + pinkState[2] + pinkState[3] + 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; pinkState[6] = whiteNoise * 0.115926;
return pink * 0.11; return pink * 0.11;

View File

@ -38,6 +38,10 @@ export class ParticleNoise implements SynthEngine {
return 'generative' as const; return 'generative' as const;
} }
getCategory() {
return 'Noise' as const;
}
randomParams(pitchLock?: PitchLock): ParticleNoiseParams { randomParams(pitchLock?: PitchLock): ParticleNoiseParams {
const densityBias = Math.random(); const densityBias = Math.random();

View File

@ -65,7 +65,7 @@ export class PhaseDistortionFM implements SynthEngine<PhaseDistortionFMParams> {
private static workletURL: string | null = null; private static workletURL: string | null = null;
getName(): string { getName(): string {
return 'PD'; return 'Phase Dist';
} }
getDescription(): string { getDescription(): string {
@ -76,6 +76,10 @@ export class PhaseDistortionFM implements SynthEngine<PhaseDistortionFMParams> {
return 'generative' as const; return 'generative' as const;
} }
getCategory() {
return 'FM' as const;
}
generate(params: PhaseDistortionFMParams, sampleRate: number, duration: number): [Float32Array, Float32Array] { generate(params: PhaseDistortionFMParams, sampleRate: number, duration: number): [Float32Array, Float32Array] {
const numSamples = Math.floor(sampleRate * duration); const numSamples = Math.floor(sampleRate * duration);
const leftBuffer = new Float32Array(numSamples); const leftBuffer = new Float32Array(numSamples);

View File

@ -82,6 +82,10 @@ export class Ring implements SynthEngine<RingParams> {
return 'generative' as const; return 'generative' as const;
} }
getCategory() {
return 'Modulation' as const;
}
generate(params: RingParams, sampleRate: number, duration: number): [Float32Array, Float32Array] { generate(params: RingParams, sampleRate: number, duration: number): [Float32Array, Float32Array] {
const numSamples = Math.floor(sampleRate * duration); const numSamples = Math.floor(sampleRate * duration);
const leftBuffer = new Float32Array(numSamples); const leftBuffer = new Float32Array(numSamples);

View File

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

View File

@ -21,6 +21,10 @@ export class Sample implements SynthEngine<SampleParams> {
return 'sample' as const; return 'sample' as const;
} }
getCategory() {
return 'Utility' as const;
}
async loadFile(file: File): Promise<void> { async loadFile(file: File): Promise<void> {
const arrayBuffer = await file.arrayBuffer(); const arrayBuffer = await file.arrayBuffer();
const audioContext = new AudioContext(); const audioContext = new AudioContext();

View File

@ -23,7 +23,7 @@ interface SnareParams {
export class Snare implements SynthEngine { export class Snare implements SynthEngine {
getName(): string { getName(): string {
return 'Snare'; return 'Noise Snare';
} }
getDescription(): string { getDescription(): string {
@ -34,6 +34,10 @@ export class Snare implements SynthEngine {
return 'generative' as const; return 'generative' as const;
} }
getCategory() {
return 'Percussion' as const;
}
randomParams(pitchLock?: PitchLock): SnareParams { randomParams(pitchLock?: PitchLock): SnareParams {
return { return {
baseFreq: pitchLock ? this.freqToParam(pitchLock.frequency) : 0.3 + Math.random() * 0.4, baseFreq: pitchLock ? this.freqToParam(pitchLock.frequency) : 0.3 + Math.random() * 0.4,

View File

@ -50,6 +50,10 @@ export class SubtractiveThreeOsc extends CsoundEngine<SubtractiveThreeOscParams>
return 'generative' as const; return 'generative' as const;
} }
getCategory() {
return 'Subtractive' as const;
}
protected getOrchestra(): string { protected getOrchestra(): string {
return ` return `
instr 1 instr 1

View File

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

View File

@ -73,6 +73,10 @@ export class TwoOpFM implements SynthEngine<TwoOpFMParams> {
return 'generative' as const; return 'generative' as const;
} }
getCategory() {
return 'FM' as const;
}
generate(params: TwoOpFMParams, sampleRate: number, duration: number): [Float32Array, Float32Array] { generate(params: TwoOpFMParams, sampleRate: number, duration: number): [Float32Array, Float32Array] {
const numSamples = Math.floor(sampleRate * duration); const numSamples = Math.floor(sampleRate * duration);
const leftBuffer = new Float32Array(numSamples); const leftBuffer = new Float32Array(numSamples);

View File

@ -38,6 +38,10 @@ export class ZzfxEngine implements SynthEngine<ZzfxParams> {
return 'generative' as const; return 'generative' as const;
} }
getCategory() {
return 'Experimental' as const;
}
generate(params: ZzfxParams, sampleRate: number, duration: number): [Float32Array, Float32Array] { generate(params: ZzfxParams, sampleRate: number, duration: number): [Float32Array, Float32Array] {
// ZZFX uses 44100 sample rate internally // ZZFX uses 44100 sample rate internally
const zzfxSampleRate = 44100; const zzfxSampleRate = 44100;

View File

@ -6,6 +6,17 @@
export type EngineType = 'generative' | 'sample' | 'input'; export type EngineType = 'generative' | 'sample' | 'input';
export type EngineCategory =
| 'Additive'
| 'Subtractive'
| 'FM'
| 'Percussion'
| 'Noise'
| 'Physical'
| 'Modulation'
| 'Experimental'
| 'Utility';
export interface PitchLock { export interface PitchLock {
enabled: boolean; enabled: boolean;
frequency: number; // Frequency in Hz frequency: number; // Frequency in Hz
@ -15,6 +26,7 @@ export interface SynthEngine<T = any> {
getName(): string; getName(): string;
getDescription(): string; getDescription(): string;
getType(): EngineType; getType(): EngineType;
getCategory(): EngineCategory;
generate(params: T, sampleRate: number, duration: number, pitchLock?: PitchLock): [Float32Array, Float32Array] | Promise<[Float32Array, Float32Array]>; generate(params: T, sampleRate: number, duration: number, pitchLock?: PitchLock): [Float32Array, Float32Array] | Promise<[Float32Array, Float32Array]>;
randomParams(pitchLock?: PitchLock): T; randomParams(pitchLock?: PitchLock): T;
mutateParams(params: T, mutationAmount?: number, pitchLock?: PitchLock): T; mutateParams(params: T, mutationAmount?: number, pitchLock?: PitchLock): T;

View File

@ -2,6 +2,7 @@ import type { SynthEngine } from './base/SynthEngine';
import { FourOpFM } from './FourOpFM'; import { FourOpFM } from './FourOpFM';
import { TwoOpFM } from './TwoOpFM'; import { TwoOpFM } from './TwoOpFM';
import { PhaseDistortionFM } from './PhaseDistortionFM'; import { PhaseDistortionFM } from './PhaseDistortionFM';
import { FormantFM } from './FormantFM';
import { DubSiren } from './DubSiren'; import { DubSiren } from './DubSiren';
import { Benjolin } from './Benjolin'; import { Benjolin } from './Benjolin';
import { ZzfxEngine } from './ZzfxEngine'; import { ZzfxEngine } from './ZzfxEngine';
@ -17,6 +18,13 @@ import { HiHat } from './HiHat';
import { ParticleNoise } from './ParticleNoise'; import { ParticleNoise } from './ParticleNoise';
import { DustNoise } from './DustNoise'; import { DustNoise } from './DustNoise';
import { SubtractiveThreeOsc } from './SubtractiveThreeOsc'; 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[] = [ export const engines: SynthEngine[] = [
new Sample(), new Sample(),
@ -24,6 +32,7 @@ export const engines: SynthEngine[] = [
new FourOpFM(), new FourOpFM(),
new TwoOpFM(), new TwoOpFM(),
new PhaseDistortionFM(), new PhaseDistortionFM(),
new FormantFM(),
new DubSiren(), new DubSiren(),
new Benjolin(), new Benjolin(),
new ZzfxEngine(), new ZzfxEngine(),
@ -31,10 +40,17 @@ export const engines: SynthEngine[] = [
new Snare(), new Snare(),
new BassDrum(), new BassDrum(),
new HiHat(), new HiHat(),
new FormantPopDrum(),
new TechnoKick(),
new FMTomTom(),
new RingCymbal(),
new AdditiveBass(),
new FeedbackSnare(),
new Ring(), new Ring(),
new KarplusStrong(), new KarplusStrong(),
new AdditiveEngine(), new AdditiveEngine(),
new ParticleNoise(), new ParticleNoise(),
new DustNoise(), new DustNoise(),
new SubtractiveThreeOsc(), new SubtractiveThreeOsc(),
new MassiveAdditive(),
]; ];

View File

@ -8,6 +8,7 @@ const STORAGE_KEYS = {
DURATION: 'duration', DURATION: 'duration',
PITCH_LOCK_ENABLED: 'pitchLockEnabled', PITCH_LOCK_ENABLED: 'pitchLockEnabled',
PITCH_LOCK_FREQUENCY: 'pitchLockFrequency', PITCH_LOCK_FREQUENCY: 'pitchLockFrequency',
EXPANDED_CATEGORIES: 'expandedCategories',
} as const; } as const;
export function loadVolume(): number { export function loadVolume(): number {
@ -45,3 +46,20 @@ export function loadPitchLockFrequency(): number {
export function savePitchLockFrequency(frequency: number): void { export function savePitchLockFrequency(frequency: number): void {
localStorage.setItem(STORAGE_KEYS.PITCH_LOCK_FREQUENCY, frequency.toString()); localStorage.setItem(STORAGE_KEYS.PITCH_LOCK_FREQUENCY, frequency.toString());
} }
export function loadExpandedCategories(): Set<string> {
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<string>): void {
localStorage.setItem(STORAGE_KEYS.EXPANDED_CATEGORIES, JSON.stringify(Array.from(categories)));
}