Adding more CSound models

This commit is contained in:
2025-10-13 13:45:33 +02:00
parent 580aa4b96f
commit 38479f0253
31 changed files with 1458 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
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
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 { 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<number | null>(null);
let canUndo = $state(false);
let sidebarOpen = $state(false);
let expandedCategories = $state<Set<string>>(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<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(() => {
audioService.setPlaybackUpdateCallback((position) => {
playbackPosition = position;
@ -485,15 +514,32 @@
<div class="content-wrapper">
<div class="sidebar" class:open={sidebarOpen}>
<div class="sidebar-content">
{#each engines as currentEngine, index}
<button
class="engine-button"
class:active={currentEngineIndex === index}
data-description={currentEngine.getDescription()}
onclick={() => switchEngine(index)}
>
{currentEngine.getName()}
</button>
{#each Array.from(enginesByCategory.entries()) as [category, categoryEngines]}
<div class="category-section">
<button
class="category-header"
class:collapsed={!expandedCategories.has(category)}
onclick={() => toggleCategory(category)}
>
<svg class="category-arrow" width="12" height="12" viewBox="0 0 12 12" fill="none">
<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}
</div>
</div>
@ -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;

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> {
getName(): string {
return 'Prism';
return 'Glass Prism';
}
getDescription(): string {
@ -91,6 +91,10 @@ export class AdditiveEngine implements SynthEngine<AdditiveParams> {
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);

View File

@ -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();

View File

@ -73,6 +73,10 @@ export class Benjolin implements SynthEngine<BenjolinParams> {
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);

View File

@ -67,6 +67,10 @@ export class DubSiren implements SynthEngine<DubSirenParams> {
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);

View File

@ -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();

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

@ -61,6 +61,10 @@ export class FormantFM extends CsoundEngine<FormantFMParams> {
return 'generative' as const;
}
getCategory() {
return 'FM' as const;
}
protected getOrchestra(): string {
return `
instr 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;
}
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);

View File

@ -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(),

View File

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

View File

@ -39,6 +39,10 @@ export class KarplusStrong implements SynthEngine<KarplusStrongParams> {
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);

View File

@ -39,6 +39,10 @@ export class MassiveAdditive extends CsoundEngine<MassiveAdditiveParams> {
return 'generative' as const;
}
getCategory() {
return 'Additive' as const;
}
protected getOrchestra(): string {
return `
; Function tables for sine wave

View File

@ -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;

View File

@ -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();

View File

@ -65,7 +65,7 @@ export class PhaseDistortionFM implements SynthEngine<PhaseDistortionFMParams> {
private static workletURL: string | null = null;
getName(): string {
return 'PD';
return 'Phase Dist';
}
getDescription(): string {
@ -76,6 +76,10 @@ export class PhaseDistortionFM implements SynthEngine<PhaseDistortionFMParams> {
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);

View File

@ -82,6 +82,10 @@ export class Ring implements SynthEngine<RingParams> {
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);

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;
}
getCategory() {
return 'Utility' as const;
}
async loadFile(file: File): Promise<void> {
const arrayBuffer = await file.arrayBuffer();
const audioContext = new AudioContext();

View File

@ -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,

View File

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

View File

@ -38,6 +38,10 @@ export class ZzfxEngine implements SynthEngine<ZzfxParams> {
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;

View File

@ -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<T = any> {
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;

View File

@ -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(),

View File

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