Adding more CSound models
This commit is contained in:
63
README.md
63
README.md
@ -55,6 +55,69 @@ Opens on http://localhost:8080
|
||||
4. Keep all DSP code, helpers, and types in the same file
|
||||
5. Register in `src/lib/audio/engines/registry.ts`
|
||||
|
||||
### Adding CSound-Based Synthesis Engines
|
||||
|
||||
For complex DSP algorithms, you can leverage CSound's powerful audio language:
|
||||
|
||||
1. Create a single file in `src/lib/audio/engines/` extending the `CsoundEngine<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
|
||||
|
||||
106
src/App.svelte
106
src/App.svelte
@ -9,7 +9,7 @@
|
||||
import type { EngineType } from "./lib/audio/engines/base/SynthEngine";
|
||||
import { AudioService } from "./lib/audio/services/AudioService";
|
||||
import { downloadWAV } from "./lib/audio/utils/WAVEncoder";
|
||||
import { loadVolume, saveVolume, loadDuration, saveDuration, loadPitchLockEnabled, savePitchLockEnabled, loadPitchLockFrequency, savePitchLockFrequency } from "./lib/utils/settings";
|
||||
import { loadVolume, saveVolume, loadDuration, saveDuration, loadPitchLockEnabled, savePitchLockEnabled, loadPitchLockFrequency, savePitchLockFrequency, loadExpandedCategories, saveExpandedCategories } from "./lib/utils/settings";
|
||||
import { cropAudio, cutAudio, processSelection } from "./lib/audio/utils/AudioEdit";
|
||||
import { generateRandomColor } from "./lib/utils/colors";
|
||||
import { getRandomProcessor } from "./lib/audio/processors/registry";
|
||||
@ -19,6 +19,7 @@
|
||||
import { createKeyboardHandler } from "./lib/utils/keyboard";
|
||||
import { parseFrequencyInput, formatFrequency } from "./lib/utils/pitch";
|
||||
import { UndoManager, type AudioState } from "./lib/utils/UndoManager";
|
||||
import type { EngineCategory } from "./lib/audio/engines/base/SynthEngine";
|
||||
|
||||
let currentEngineIndex = $state(0);
|
||||
const engine = $derived(engines[currentEngineIndex]);
|
||||
@ -46,6 +47,7 @@
|
||||
let selectionEnd = $state<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;
|
||||
|
||||
228
src/lib/audio/engines/AdditiveBass.ts
Normal file
228
src/lib/audio/engines/AdditiveBass.ts
Normal 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),
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
184
src/lib/audio/engines/FMTomTom.ts
Normal file
184
src/lib/audio/engines/FMTomTom.ts
Normal 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),
|
||||
};
|
||||
}
|
||||
}
|
||||
247
src/lib/audio/engines/FeedbackSnare.ts
Normal file
247
src/lib/audio/engines/FeedbackSnare.ts
Normal 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),
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
142
src/lib/audio/engines/FormantPopDrum.ts
Normal file
142
src/lib/audio/engines/FormantPopDrum.ts
Normal 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),
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
178
src/lib/audio/engines/RingCymbal.ts
Normal file
178
src/lib/audio/engines/RingCymbal.ts
Normal 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),
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
187
src/lib/audio/engines/TechnoKick.ts
Normal file
187
src/lib/audio/engines/TechnoKick.ts
Normal 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),
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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)));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user