Compare commits
2 Commits
467558efd2
...
38479f0253
| Author | SHA1 | Date | |
|---|---|---|---|
| 38479f0253 | |||
| 580aa4b96f |
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
|
4. Keep all DSP code, helpers, and types in the same file
|
||||||
5. Register in `src/lib/audio/engines/registry.ts`
|
5. Register in `src/lib/audio/engines/registry.ts`
|
||||||
|
|
||||||
|
### Adding CSound-Based Synthesis Engines
|
||||||
|
|
||||||
|
For complex DSP algorithms, you can leverage CSound's powerful audio language:
|
||||||
|
|
||||||
|
1. Create a single file in `src/lib/audio/engines/` extending the `CsoundEngine<ParamsType>` abstract class
|
||||||
|
2. Define a TypeScript interface for your parameters
|
||||||
|
3. Implement required methods:
|
||||||
|
- `getName()`: Engine display name
|
||||||
|
- `getDescription()`: Brief description
|
||||||
|
- `getType()`: Return `'generative'`, `'sample'`, or `'input'`
|
||||||
|
- `getOrchestra()`: Return CSound orchestra code as a string
|
||||||
|
- `getParametersForCsound(params)`: Map TypeScript params to CSound channel parameters
|
||||||
|
- `randomParams(pitchLock?)`: Generate random parameter values
|
||||||
|
- `mutateParams(params, mutationAmount?, pitchLock?)`: Mutate existing parameters
|
||||||
|
4. Keep all enums, interfaces, and helper logic in the same file
|
||||||
|
5. Register in `src/lib/audio/engines/registry.ts`
|
||||||
|
|
||||||
|
**CSound Orchestra Guidelines:**
|
||||||
|
- Use `instr 1` as your main instrument
|
||||||
|
- Read parameters via `chnget "paramName"`
|
||||||
|
- Duration is available as `p3`
|
||||||
|
- Time-based parameters (attack, decay, release) should be ratios (0-1) scaled by `p3`
|
||||||
|
- Output stereo audio with `outs aLeft, aRight`
|
||||||
|
- The base class handles WAV parsing, normalization, and fade-in
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```typescript
|
||||||
|
import { CsoundEngine, type CsoundParameter } from './base/CsoundEngine';
|
||||||
|
|
||||||
|
interface MyParams {
|
||||||
|
frequency: number;
|
||||||
|
resonance: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MyEngine extends CsoundEngine<MyParams> {
|
||||||
|
getName() { return 'My Engine'; }
|
||||||
|
getDescription() { return 'Description'; }
|
||||||
|
getType() { return 'generative' as const; }
|
||||||
|
|
||||||
|
protected getOrchestra(): string {
|
||||||
|
return `
|
||||||
|
instr 1
|
||||||
|
iFreq chnget "frequency"
|
||||||
|
iRes chnget "resonance"
|
||||||
|
aNoise noise 1, 0
|
||||||
|
aOut butterbp aNoise, iFreq, iRes
|
||||||
|
outs aOut, aOut
|
||||||
|
endin
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getParametersForCsound(params: MyParams): CsoundParameter[] {
|
||||||
|
return [
|
||||||
|
{ channelName: 'frequency', value: params.frequency },
|
||||||
|
{ channelName: 'resonance', value: params.resonance }
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
randomParams() { /* ... */ }
|
||||||
|
mutateParams(params, amount = 0.15) { /* ... */ }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### Adding Audio Processors
|
### Adding Audio Processors
|
||||||
|
|
||||||
1. Create a single file in `src/lib/audio/processors/` implementing the `AudioProcessor` interface
|
1. Create a single file in `src/lib/audio/processors/` implementing the `AudioProcessor` interface
|
||||||
|
|||||||
@ -9,7 +9,7 @@
|
|||||||
import type { EngineType } from "./lib/audio/engines/base/SynthEngine";
|
import type { EngineType } from "./lib/audio/engines/base/SynthEngine";
|
||||||
import { AudioService } from "./lib/audio/services/AudioService";
|
import { AudioService } from "./lib/audio/services/AudioService";
|
||||||
import { downloadWAV } from "./lib/audio/utils/WAVEncoder";
|
import { downloadWAV } from "./lib/audio/utils/WAVEncoder";
|
||||||
import { loadVolume, saveVolume, loadDuration, saveDuration, loadPitchLockEnabled, savePitchLockEnabled, loadPitchLockFrequency, savePitchLockFrequency } from "./lib/utils/settings";
|
import { loadVolume, saveVolume, loadDuration, saveDuration, loadPitchLockEnabled, savePitchLockEnabled, loadPitchLockFrequency, savePitchLockFrequency, loadExpandedCategories, saveExpandedCategories } from "./lib/utils/settings";
|
||||||
import { cropAudio, cutAudio, processSelection } from "./lib/audio/utils/AudioEdit";
|
import { cropAudio, cutAudio, processSelection } from "./lib/audio/utils/AudioEdit";
|
||||||
import { generateRandomColor } from "./lib/utils/colors";
|
import { generateRandomColor } from "./lib/utils/colors";
|
||||||
import { getRandomProcessor } from "./lib/audio/processors/registry";
|
import { getRandomProcessor } from "./lib/audio/processors/registry";
|
||||||
@ -19,6 +19,7 @@
|
|||||||
import { createKeyboardHandler } from "./lib/utils/keyboard";
|
import { createKeyboardHandler } from "./lib/utils/keyboard";
|
||||||
import { parseFrequencyInput, formatFrequency } from "./lib/utils/pitch";
|
import { parseFrequencyInput, formatFrequency } from "./lib/utils/pitch";
|
||||||
import { UndoManager, type AudioState } from "./lib/utils/UndoManager";
|
import { UndoManager, type AudioState } from "./lib/utils/UndoManager";
|
||||||
|
import type { EngineCategory } from "./lib/audio/engines/base/SynthEngine";
|
||||||
|
|
||||||
let currentEngineIndex = $state(0);
|
let currentEngineIndex = $state(0);
|
||||||
const engine = $derived(engines[currentEngineIndex]);
|
const engine = $derived(engines[currentEngineIndex]);
|
||||||
@ -46,6 +47,7 @@
|
|||||||
let selectionEnd = $state<number | null>(null);
|
let selectionEnd = $state<number | null>(null);
|
||||||
let canUndo = $state(false);
|
let canUndo = $state(false);
|
||||||
let sidebarOpen = $state(false);
|
let sidebarOpen = $state(false);
|
||||||
|
let expandedCategories = $state<Set<string>>(loadExpandedCategories());
|
||||||
|
|
||||||
const showDuration = $derived(engineType !== 'sample');
|
const showDuration = $derived(engineType !== 'sample');
|
||||||
const showRandomButton = $derived(engineType === 'generative');
|
const showRandomButton = $derived(engineType === 'generative');
|
||||||
@ -74,6 +76,33 @@
|
|||||||
savePitchLockFrequency(pitchLockFrequency);
|
savePitchLockFrequency(pitchLockFrequency);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
saveExpandedCategories(expandedCategories);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Group engines by category
|
||||||
|
const enginesByCategory = $derived.by(() => {
|
||||||
|
const grouped = new Map<EngineCategory, typeof engines>();
|
||||||
|
for (const engine of engines) {
|
||||||
|
const category = engine.getCategory();
|
||||||
|
if (!grouped.has(category)) {
|
||||||
|
grouped.set(category, []);
|
||||||
|
}
|
||||||
|
grouped.get(category)!.push(engine);
|
||||||
|
}
|
||||||
|
return grouped;
|
||||||
|
});
|
||||||
|
|
||||||
|
function toggleCategory(category: string) {
|
||||||
|
const newSet = new Set(expandedCategories);
|
||||||
|
if (newSet.has(category)) {
|
||||||
|
newSet.delete(category);
|
||||||
|
} else {
|
||||||
|
newSet.add(category);
|
||||||
|
}
|
||||||
|
expandedCategories = newSet;
|
||||||
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
audioService.setPlaybackUpdateCallback((position) => {
|
audioService.setPlaybackUpdateCallback((position) => {
|
||||||
playbackPosition = position;
|
playbackPosition = position;
|
||||||
@ -485,7 +514,21 @@
|
|||||||
<div class="content-wrapper">
|
<div class="content-wrapper">
|
||||||
<div class="sidebar" class:open={sidebarOpen}>
|
<div class="sidebar" class:open={sidebarOpen}>
|
||||||
<div class="sidebar-content">
|
<div class="sidebar-content">
|
||||||
{#each engines as currentEngine, index}
|
{#each Array.from(enginesByCategory.entries()) as [category, categoryEngines]}
|
||||||
|
<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
|
<button
|
||||||
class="engine-button"
|
class="engine-button"
|
||||||
class:active={currentEngineIndex === index}
|
class:active={currentEngineIndex === index}
|
||||||
@ -495,6 +538,9 @@
|
|||||||
{currentEngine.getName()}
|
{currentEngine.getName()}
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -693,12 +739,50 @@
|
|||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.category-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.35rem;
|
||||||
|
font-size: 0.65rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
padding: 0.5rem 0.5rem;
|
||||||
|
background-color: #1a1a1a;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 1px solid #333;
|
||||||
|
color: #999;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.2s, background-color 0.2s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-header:hover {
|
||||||
|
color: #ccc;
|
||||||
|
background-color: #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-arrow {
|
||||||
|
transition: transform 0.2s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-header.collapsed .category-arrow {
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
}
|
||||||
|
|
||||||
.engine-button {
|
.engine-button {
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
position: relative;
|
position: relative;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
padding: 0.5rem 0.5rem;
|
padding: 0.5rem 0.5rem 0.5rem 1rem;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
|||||||
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> {
|
export class AdditiveEngine implements SynthEngine<AdditiveParams> {
|
||||||
getName(): string {
|
getName(): string {
|
||||||
return 'Prism';
|
return 'Glass Prism';
|
||||||
}
|
}
|
||||||
|
|
||||||
getDescription(): string {
|
getDescription(): string {
|
||||||
@ -91,6 +91,10 @@ export class AdditiveEngine implements SynthEngine<AdditiveParams> {
|
|||||||
return 'generative' as const;
|
return 'generative' as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getCategory() {
|
||||||
|
return 'Additive' as const;
|
||||||
|
}
|
||||||
|
|
||||||
generate(params: AdditiveParams, sampleRate: number, duration: number): [Float32Array, Float32Array] {
|
generate(params: AdditiveParams, sampleRate: number, duration: number): [Float32Array, Float32Array] {
|
||||||
const numSamples = Math.floor(sampleRate * duration);
|
const numSamples = Math.floor(sampleRate * duration);
|
||||||
const leftBuffer = new Float32Array(numSamples);
|
const leftBuffer = new Float32Array(numSamples);
|
||||||
|
|||||||
@ -57,7 +57,7 @@ interface BassDrumParams {
|
|||||||
|
|
||||||
export class BassDrum implements SynthEngine {
|
export class BassDrum implements SynthEngine {
|
||||||
getName(): string {
|
getName(): string {
|
||||||
return 'Kick';
|
return 'Dark Kick';
|
||||||
}
|
}
|
||||||
|
|
||||||
getDescription(): string {
|
getDescription(): string {
|
||||||
@ -68,6 +68,10 @@ export class BassDrum implements SynthEngine {
|
|||||||
return 'generative' as const;
|
return 'generative' as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getCategory() {
|
||||||
|
return 'Percussion' as const;
|
||||||
|
}
|
||||||
|
|
||||||
randomParams(pitchLock?: PitchLock): BassDrumParams {
|
randomParams(pitchLock?: PitchLock): BassDrumParams {
|
||||||
// Choose a kick character/style
|
// Choose a kick character/style
|
||||||
const styleRoll = Math.random();
|
const styleRoll = Math.random();
|
||||||
|
|||||||
@ -73,6 +73,10 @@ export class Benjolin implements SynthEngine<BenjolinParams> {
|
|||||||
return 'generative' as const;
|
return 'generative' as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getCategory() {
|
||||||
|
return 'Experimental' as const;
|
||||||
|
}
|
||||||
|
|
||||||
generate(params: BenjolinParams, sampleRate: number, duration: number): [Float32Array, Float32Array] {
|
generate(params: BenjolinParams, sampleRate: number, duration: number): [Float32Array, Float32Array] {
|
||||||
const numSamples = Math.floor(duration * sampleRate);
|
const numSamples = Math.floor(duration * sampleRate);
|
||||||
const left = new Float32Array(numSamples);
|
const left = new Float32Array(numSamples);
|
||||||
|
|||||||
@ -67,6 +67,10 @@ export class DubSiren implements SynthEngine<DubSirenParams> {
|
|||||||
return 'generative' as const;
|
return 'generative' as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getCategory() {
|
||||||
|
return 'Experimental' as const;
|
||||||
|
}
|
||||||
|
|
||||||
generate(params: DubSirenParams, sampleRate: number, duration: number): [Float32Array, Float32Array] {
|
generate(params: DubSirenParams, sampleRate: number, duration: number): [Float32Array, Float32Array] {
|
||||||
const numSamples = Math.floor(sampleRate * duration);
|
const numSamples = Math.floor(sampleRate * duration);
|
||||||
const leftBuffer = new Float32Array(numSamples);
|
const leftBuffer = new Float32Array(numSamples);
|
||||||
|
|||||||
@ -46,6 +46,10 @@ export class DustNoise implements SynthEngine {
|
|||||||
return 'generative' as const;
|
return 'generative' as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getCategory() {
|
||||||
|
return 'Noise' as const;
|
||||||
|
}
|
||||||
|
|
||||||
randomParams(pitchLock?: PitchLock): DustNoiseParams {
|
randomParams(pitchLock?: PitchLock): DustNoiseParams {
|
||||||
const characterBias = Math.random();
|
const characterBias = Math.random();
|
||||||
|
|
||||||
|
|||||||
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),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
408
src/lib/audio/engines/FormantFM.ts
Normal file
408
src/lib/audio/engines/FormantFM.ts
Normal file
@ -0,0 +1,408 @@
|
|||||||
|
import { CsoundEngine, type CsoundParameter } from './base/CsoundEngine';
|
||||||
|
import type { PitchLock } from './base/SynthEngine';
|
||||||
|
|
||||||
|
enum VowelType {
|
||||||
|
A,
|
||||||
|
E,
|
||||||
|
I,
|
||||||
|
O,
|
||||||
|
U,
|
||||||
|
AE,
|
||||||
|
OE,
|
||||||
|
UE,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ModulationType {
|
||||||
|
SimpleFM,
|
||||||
|
DoubleFM,
|
||||||
|
RingMod,
|
||||||
|
CrossFM,
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FormantBand {
|
||||||
|
frequency: number;
|
||||||
|
bandwidth: number;
|
||||||
|
amplitude: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FormantFMParams {
|
||||||
|
baseFreq: number;
|
||||||
|
vowel: VowelType;
|
||||||
|
vowelMorph: number;
|
||||||
|
modulationType: ModulationType;
|
||||||
|
modIndex: number;
|
||||||
|
modRatio: number;
|
||||||
|
attack: number;
|
||||||
|
decay: number;
|
||||||
|
sustain: number;
|
||||||
|
release: number;
|
||||||
|
brightness: number;
|
||||||
|
vibrato: number;
|
||||||
|
vibratoRate: number;
|
||||||
|
detune: number;
|
||||||
|
noise: number;
|
||||||
|
formantLFORate: number;
|
||||||
|
formantLFODepth: number;
|
||||||
|
modIndexLFORate: number;
|
||||||
|
modIndexLFODepth: number;
|
||||||
|
chaos: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FormantFM extends CsoundEngine<FormantFMParams> {
|
||||||
|
getName(): string {
|
||||||
|
return 'Formant FM';
|
||||||
|
}
|
||||||
|
|
||||||
|
getDescription(): string {
|
||||||
|
return 'FM synthesis with formant filters creating vowel-like sounds and vocal textures';
|
||||||
|
}
|
||||||
|
|
||||||
|
getType() {
|
||||||
|
return 'generative' as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
getCategory() {
|
||||||
|
return 'FM' as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getOrchestra(): string {
|
||||||
|
return `
|
||||||
|
instr 1
|
||||||
|
iBaseFreq chnget "baseFreq"
|
||||||
|
iVowel chnget "vowel"
|
||||||
|
iVowelMorph chnget "vowelMorph"
|
||||||
|
iModType chnget "modulationType"
|
||||||
|
iModIndex chnget "modIndex"
|
||||||
|
iModRatio chnget "modRatio"
|
||||||
|
iAttack chnget "attack"
|
||||||
|
iDecay chnget "decay"
|
||||||
|
iSustain chnget "sustain"
|
||||||
|
iRelease chnget "release"
|
||||||
|
iBrightness chnget "brightness"
|
||||||
|
iVibrato chnget "vibrato"
|
||||||
|
iVibratoRate chnget "vibratoRate"
|
||||||
|
iDetune chnget "detune"
|
||||||
|
iNoise chnget "noise"
|
||||||
|
iFormantLFORate chnget "formantLFORate"
|
||||||
|
iFormantLFODepth chnget "formantLFODepth"
|
||||||
|
iModIndexLFORate chnget "modIndexLFORate"
|
||||||
|
iModIndexLFODepth chnget "modIndexLFODepth"
|
||||||
|
iChaos chnget "chaos"
|
||||||
|
|
||||||
|
idur = p3
|
||||||
|
iAttackTime = iAttack * idur
|
||||||
|
iDecayTime = iDecay * idur
|
||||||
|
iReleaseTime = iRelease * idur
|
||||||
|
|
||||||
|
; Envelope
|
||||||
|
kEnv madsr iAttackTime, iDecayTime, iSustain, iReleaseTime
|
||||||
|
|
||||||
|
; Vibrato LFO with chaos
|
||||||
|
kVib oscili iVibrato * iBaseFreq * 0.02, iVibratoRate
|
||||||
|
kChaosLFO1 oscili iChaos * iBaseFreq * 0.01, iVibratoRate * 1.618
|
||||||
|
kChaosLFO2 oscili iChaos * iBaseFreq * 0.005, iVibratoRate * 2.414
|
||||||
|
kFreq = iBaseFreq + kVib + kChaosLFO1 + kChaosLFO2
|
||||||
|
|
||||||
|
; Get formant parameters based on vowel type
|
||||||
|
if iVowel == 0 then
|
||||||
|
; A (as in "father")
|
||||||
|
iF1 = 730
|
||||||
|
iF2 = 1090
|
||||||
|
iF3 = 2440
|
||||||
|
iA1 = 1.0
|
||||||
|
iA2 = 0.5
|
||||||
|
iA3 = 0.25
|
||||||
|
iBW1 = 80
|
||||||
|
iBW2 = 90
|
||||||
|
iBW3 = 120
|
||||||
|
elseif iVowel == 1 then
|
||||||
|
; E (as in "bet")
|
||||||
|
iF1 = 530
|
||||||
|
iF2 = 1840
|
||||||
|
iF3 = 2480
|
||||||
|
iA1 = 1.0
|
||||||
|
iA2 = 0.6
|
||||||
|
iA3 = 0.3
|
||||||
|
iBW1 = 60
|
||||||
|
iBW2 = 100
|
||||||
|
iBW3 = 120
|
||||||
|
elseif iVowel == 2 then
|
||||||
|
; I (as in "bit")
|
||||||
|
iF1 = 390
|
||||||
|
iF2 = 1990
|
||||||
|
iF3 = 2550
|
||||||
|
iA1 = 1.0
|
||||||
|
iA2 = 0.7
|
||||||
|
iA3 = 0.2
|
||||||
|
iBW1 = 50
|
||||||
|
iBW2 = 100
|
||||||
|
iBW3 = 120
|
||||||
|
elseif iVowel == 3 then
|
||||||
|
; O (as in "boat")
|
||||||
|
iF1 = 570
|
||||||
|
iF2 = 840
|
||||||
|
iF3 = 2410
|
||||||
|
iA1 = 1.0
|
||||||
|
iA2 = 0.45
|
||||||
|
iA3 = 0.28
|
||||||
|
iBW1 = 70
|
||||||
|
iBW2 = 80
|
||||||
|
iBW3 = 100
|
||||||
|
elseif iVowel == 4 then
|
||||||
|
; U (as in "boot")
|
||||||
|
iF1 = 440
|
||||||
|
iF2 = 1020
|
||||||
|
iF3 = 2240
|
||||||
|
iA1 = 1.0
|
||||||
|
iA2 = 0.4
|
||||||
|
iA3 = 0.2
|
||||||
|
iBW1 = 70
|
||||||
|
iBW2 = 80
|
||||||
|
iBW3 = 100
|
||||||
|
elseif iVowel == 5 then
|
||||||
|
; AE (as in "bat")
|
||||||
|
iF1 = 660
|
||||||
|
iF2 = 1720
|
||||||
|
iF3 = 2410
|
||||||
|
iA1 = 1.0
|
||||||
|
iA2 = 0.55
|
||||||
|
iA3 = 0.3
|
||||||
|
iBW1 = 80
|
||||||
|
iBW2 = 90
|
||||||
|
iBW3 = 120
|
||||||
|
elseif iVowel == 6 then
|
||||||
|
; OE (as in "bird")
|
||||||
|
iF1 = 490
|
||||||
|
iF2 = 1350
|
||||||
|
iF3 = 1690
|
||||||
|
iA1 = 1.0
|
||||||
|
iA2 = 0.5
|
||||||
|
iA3 = 0.4
|
||||||
|
iBW1 = 70
|
||||||
|
iBW2 = 80
|
||||||
|
iBW3 = 100
|
||||||
|
else
|
||||||
|
; UE (as in "about")
|
||||||
|
iF1 = 520
|
||||||
|
iF2 = 1190
|
||||||
|
iF3 = 2390
|
||||||
|
iA1 = 1.0
|
||||||
|
iA2 = 0.45
|
||||||
|
iA3 = 0.25
|
||||||
|
iBW1 = 70
|
||||||
|
iBW2 = 80
|
||||||
|
iBW3 = 110
|
||||||
|
endif
|
||||||
|
|
||||||
|
; Modulate formant frequencies with multiple LFOs
|
||||||
|
kFormantShift = 1 + (iVowelMorph - 0.5) * 0.4
|
||||||
|
kFormantLFO oscili iFormantLFODepth * 0.3, iFormantLFORate
|
||||||
|
kFormantShiftModulated = kFormantShift + kFormantLFO
|
||||||
|
kF1 = iF1 * kFormantShiftModulated
|
||||||
|
kF2 = iF2 * kFormantShiftModulated
|
||||||
|
kF3 = iF3 * kFormantShiftModulated
|
||||||
|
|
||||||
|
; Modulation index LFO
|
||||||
|
kModIndexLFO oscili iModIndexLFODepth * iModIndex, iModIndexLFORate
|
||||||
|
kModIndexDynamic = iModIndex + kModIndexLFO
|
||||||
|
|
||||||
|
; Generate carrier and modulator based on modulation type
|
||||||
|
kModFreq = kFreq * iModRatio
|
||||||
|
|
||||||
|
if iModType == 0 then
|
||||||
|
; Simple FM with dynamic modulation
|
||||||
|
aMod oscili kModIndexDynamic * kModFreq, kModFreq
|
||||||
|
aCarrier oscili 0.5, kFreq + aMod
|
||||||
|
|
||||||
|
elseif iModType == 1 then
|
||||||
|
; Double FM (modulator modulates itself) with chaos
|
||||||
|
kChaosModDepth = 1 + (iChaos * 0.5)
|
||||||
|
aMod1 oscili kModIndexDynamic * 0.3 * kModFreq * kChaosModDepth, kModFreq * 0.5
|
||||||
|
aMod2 oscili kModIndexDynamic * kModFreq, kModFreq + aMod1
|
||||||
|
aCarrier oscili 0.5, kFreq + aMod2
|
||||||
|
|
||||||
|
elseif iModType == 2 then
|
||||||
|
; Ring modulation with frequency wobble
|
||||||
|
kRingModWobble oscili iChaos * 0.2, iFormantLFORate * 0.7
|
||||||
|
aMod oscili 0.5, kModFreq * (1 + kRingModWobble)
|
||||||
|
aCarrierTemp oscili 0.5, kFreq
|
||||||
|
aCarrier = aCarrierTemp * aMod
|
||||||
|
|
||||||
|
else
|
||||||
|
; Cross FM with multiple carriers
|
||||||
|
aMod oscili kModIndexDynamic * kModFreq, kModFreq
|
||||||
|
aCarrier1 oscili 0.4, kFreq + aMod
|
||||||
|
aCarrier2 oscili 0.3, kFreq * 0.5 + aMod * 0.5
|
||||||
|
kThirdCarrierFreq = kFreq * (1.5 + iChaos * 0.3)
|
||||||
|
aCarrier3 oscili 0.2 * iChaos, kThirdCarrierFreq + aMod * 0.3
|
||||||
|
aCarrier = aCarrier1 + aCarrier2 + aCarrier3
|
||||||
|
endif
|
||||||
|
|
||||||
|
; Add brightness via high-frequency content
|
||||||
|
aCarrierBright oscili 0.15 * iBrightness, kFreq * 2
|
||||||
|
aCarrierMix = aCarrier + aCarrierBright
|
||||||
|
|
||||||
|
; Add subtle noise for breathiness
|
||||||
|
aNoise noise 0.08 * iNoise, 0
|
||||||
|
aCarrierFinal = aCarrierMix + aNoise
|
||||||
|
|
||||||
|
; Apply formant filters (bandpass filters at formant frequencies)
|
||||||
|
aFormant1 butterbp aCarrierFinal, kF1, iBW1
|
||||||
|
aFormant1Scaled = aFormant1 * iA1
|
||||||
|
|
||||||
|
aFormant2 butterbp aCarrierFinal, kF2, iBW2
|
||||||
|
aFormant2Scaled = aFormant2 * iA2
|
||||||
|
|
||||||
|
aFormant3 butterbp aCarrierFinal, kF3, iBW3
|
||||||
|
aFormant3Scaled = aFormant3 * iA3
|
||||||
|
|
||||||
|
; Mix formants
|
||||||
|
aMix = (aFormant1Scaled + aFormant2Scaled + aFormant3Scaled) * 0.6
|
||||||
|
|
||||||
|
; Apply envelope
|
||||||
|
aOut = aMix * kEnv
|
||||||
|
|
||||||
|
; Stereo - slightly different phase for right channel
|
||||||
|
iDetuneFactor = 1 + (iDetune * 0.5)
|
||||||
|
kFreqR = iBaseFreq * iDetuneFactor + kVib
|
||||||
|
|
||||||
|
; Regenerate right channel with detuned frequency
|
||||||
|
kModFreqR = kFreqR * iModRatio
|
||||||
|
|
||||||
|
if iModType == 0 then
|
||||||
|
aModR oscili iModIndex * kModFreqR, kModFreqR
|
||||||
|
aCarrierR oscili 0.5, kFreqR + aModR
|
||||||
|
elseif iModType == 1 then
|
||||||
|
aMod1R oscili iModIndex * 0.3 * kModFreqR, kModFreqR * 0.5
|
||||||
|
aMod2R oscili iModIndex * kModFreqR, kModFreqR + aMod1R
|
||||||
|
aCarrierR oscili 0.5, kFreqR + aMod2R
|
||||||
|
elseif iModType == 2 then
|
||||||
|
aModR oscili 0.5, kModFreqR
|
||||||
|
aCarrierTempR oscili 0.5, kFreqR
|
||||||
|
aCarrierR = aCarrierTempR * aModR
|
||||||
|
else
|
||||||
|
aModR oscili iModIndex * kModFreqR, kModFreqR
|
||||||
|
aCarrier1R oscili 0.4, kFreqR + aModR
|
||||||
|
aCarrier2R oscili 0.3, kFreqR * 0.5 + aModR * 0.5
|
||||||
|
aCarrierR = aCarrier1R + aCarrier2R
|
||||||
|
endif
|
||||||
|
|
||||||
|
aCarrierBrightR oscili 0.15 * iBrightness, kFreqR * 2
|
||||||
|
aCarrierMixR = aCarrierR + aCarrierBrightR
|
||||||
|
aNoiseR noise 0.08 * iNoise, 0
|
||||||
|
aCarrierFinalR = aCarrierMixR + aNoiseR
|
||||||
|
|
||||||
|
kF1R = iF1 * kFormantShift
|
||||||
|
kF2R = iF2 * kFormantShift
|
||||||
|
kF3R = iF3 * kFormantShift
|
||||||
|
|
||||||
|
aFormant1R butterbp aCarrierFinalR, kF1R, iBW1
|
||||||
|
aFormant1ScaledR = aFormant1R * iA1
|
||||||
|
aFormant2R butterbp aCarrierFinalR, kF2R, iBW2
|
||||||
|
aFormant2ScaledR = aFormant2R * iA2
|
||||||
|
aFormant3R butterbp aCarrierFinalR, kF3R, iBW3
|
||||||
|
aFormant3ScaledR = aFormant3R * iA3
|
||||||
|
|
||||||
|
aMixR = (aFormant1ScaledR + aFormant2ScaledR + aFormant3ScaledR) * 0.6
|
||||||
|
aOutR = aMixR * kEnv
|
||||||
|
|
||||||
|
outs aOut, aOutR
|
||||||
|
endin
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getParametersForCsound(params: FormantFMParams): CsoundParameter[] {
|
||||||
|
return [
|
||||||
|
{ channelName: 'baseFreq', value: params.baseFreq },
|
||||||
|
{ channelName: 'vowel', value: params.vowel },
|
||||||
|
{ channelName: 'vowelMorph', value: params.vowelMorph },
|
||||||
|
{ channelName: 'modulationType', value: params.modulationType },
|
||||||
|
{ channelName: 'modIndex', value: params.modIndex },
|
||||||
|
{ channelName: 'modRatio', value: params.modRatio },
|
||||||
|
{ channelName: 'attack', value: params.attack },
|
||||||
|
{ channelName: 'decay', value: params.decay },
|
||||||
|
{ channelName: 'sustain', value: params.sustain },
|
||||||
|
{ channelName: 'release', value: params.release },
|
||||||
|
{ channelName: 'brightness', value: params.brightness },
|
||||||
|
{ channelName: 'vibrato', value: params.vibrato },
|
||||||
|
{ channelName: 'vibratoRate', value: params.vibratoRate },
|
||||||
|
{ channelName: 'detune', value: params.detune },
|
||||||
|
{ channelName: 'noise', value: params.noise },
|
||||||
|
{ channelName: 'formantLFORate', value: params.formantLFORate },
|
||||||
|
{ channelName: 'formantLFODepth', value: params.formantLFODepth },
|
||||||
|
{ channelName: 'modIndexLFORate', value: params.modIndexLFORate },
|
||||||
|
{ channelName: 'modIndexLFODepth', value: params.modIndexLFODepth },
|
||||||
|
{ channelName: 'chaos', value: params.chaos },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
randomParams(pitchLock?: PitchLock): FormantFMParams {
|
||||||
|
const baseFreqChoices = [82.4, 110, 146.8, 220, 293.7, 440];
|
||||||
|
const baseFreq = pitchLock?.enabled
|
||||||
|
? pitchLock.frequency
|
||||||
|
: this.randomChoice(baseFreqChoices) * this.randomRange(0.95, 1.05);
|
||||||
|
|
||||||
|
const modulationRatios = [0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 5, 7, 9];
|
||||||
|
|
||||||
|
return {
|
||||||
|
baseFreq,
|
||||||
|
vowel: this.randomInt(0, 7) as VowelType,
|
||||||
|
vowelMorph: this.randomRange(0.3, 0.7),
|
||||||
|
modulationType: this.randomInt(0, 3) as ModulationType,
|
||||||
|
modIndex: this.randomRange(1, 10),
|
||||||
|
modRatio: this.randomChoice(modulationRatios),
|
||||||
|
attack: this.randomRange(0.001, 0.15),
|
||||||
|
decay: this.randomRange(0.05, 0.25),
|
||||||
|
sustain: this.randomRange(0.3, 0.8),
|
||||||
|
release: this.randomRange(0.1, 0.4),
|
||||||
|
brightness: this.randomRange(0, 0.6),
|
||||||
|
vibrato: this.randomRange(0, 0.5),
|
||||||
|
vibratoRate: this.randomRange(3, 8),
|
||||||
|
detune: this.randomRange(0.001, 0.01),
|
||||||
|
noise: this.randomRange(0, 0.3),
|
||||||
|
formantLFORate: this.randomRange(0.2, 6),
|
||||||
|
formantLFODepth: this.randomRange(0, 0.8),
|
||||||
|
modIndexLFORate: this.randomRange(0.5, 12),
|
||||||
|
modIndexLFODepth: this.randomRange(0, 0.7),
|
||||||
|
chaos: this.randomRange(0, 0.8),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
mutateParams(
|
||||||
|
params: FormantFMParams,
|
||||||
|
mutationAmount: number = 0.15,
|
||||||
|
pitchLock?: PitchLock
|
||||||
|
): FormantFMParams {
|
||||||
|
const baseFreq = pitchLock?.enabled ? pitchLock.frequency : params.baseFreq;
|
||||||
|
const modulationRatios = [0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 5, 7, 9];
|
||||||
|
|
||||||
|
return {
|
||||||
|
baseFreq,
|
||||||
|
vowel: Math.random() < 0.1 ? (this.randomInt(0, 7) as VowelType) : params.vowel,
|
||||||
|
vowelMorph: this.mutateValue(params.vowelMorph, mutationAmount, 0, 1),
|
||||||
|
modulationType:
|
||||||
|
Math.random() < 0.08
|
||||||
|
? (this.randomInt(0, 3) as ModulationType)
|
||||||
|
: params.modulationType,
|
||||||
|
modIndex: this.mutateValue(params.modIndex, mutationAmount, 0.5, 15),
|
||||||
|
modRatio:
|
||||||
|
Math.random() < 0.12
|
||||||
|
? this.randomChoice(modulationRatios)
|
||||||
|
: params.modRatio,
|
||||||
|
attack: this.mutateValue(params.attack, mutationAmount, 0.001, 0.25),
|
||||||
|
decay: this.mutateValue(params.decay, mutationAmount, 0.02, 0.4),
|
||||||
|
sustain: this.mutateValue(params.sustain, mutationAmount, 0.1, 0.9),
|
||||||
|
release: this.mutateValue(params.release, mutationAmount, 0.05, 0.6),
|
||||||
|
brightness: this.mutateValue(params.brightness, mutationAmount, 0, 1),
|
||||||
|
vibrato: this.mutateValue(params.vibrato, mutationAmount, 0, 0.6),
|
||||||
|
vibratoRate: this.mutateValue(params.vibratoRate, mutationAmount, 2, 10),
|
||||||
|
detune: this.mutateValue(params.detune, mutationAmount, 0.0005, 0.02),
|
||||||
|
noise: this.mutateValue(params.noise, mutationAmount, 0, 0.5),
|
||||||
|
formantLFORate: this.mutateValue(params.formantLFORate, mutationAmount, 0.1, 8),
|
||||||
|
formantLFODepth: this.mutateValue(params.formantLFODepth, mutationAmount, 0, 1),
|
||||||
|
modIndexLFORate: this.mutateValue(params.modIndexLFORate, mutationAmount, 0.3, 15),
|
||||||
|
modIndexLFODepth: this.mutateValue(params.modIndexLFODepth, mutationAmount, 0, 1),
|
||||||
|
chaos: this.mutateValue(params.chaos, mutationAmount, 0, 1),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
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;
|
return 'generative' as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getCategory() {
|
||||||
|
return 'FM' as const;
|
||||||
|
}
|
||||||
|
|
||||||
generate(params: FourOpFMParams, sampleRate: number, duration: number): [Float32Array, Float32Array] {
|
generate(params: FourOpFMParams, sampleRate: number, duration: number): [Float32Array, Float32Array] {
|
||||||
const numSamples = Math.floor(sampleRate * duration);
|
const numSamples = Math.floor(sampleRate * duration);
|
||||||
const leftBuffer = new Float32Array(numSamples);
|
const leftBuffer = new Float32Array(numSamples);
|
||||||
|
|||||||
@ -19,7 +19,7 @@ interface HiHatParams {
|
|||||||
|
|
||||||
export class HiHat implements SynthEngine {
|
export class HiHat implements SynthEngine {
|
||||||
getName(): string {
|
getName(): string {
|
||||||
return 'Hi-Hat';
|
return 'Noise Hi-Hat';
|
||||||
}
|
}
|
||||||
|
|
||||||
getDescription(): string {
|
getDescription(): string {
|
||||||
@ -30,6 +30,10 @@ export class HiHat implements SynthEngine {
|
|||||||
return 'generative' as const;
|
return 'generative' as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getCategory() {
|
||||||
|
return 'Percussion' as const;
|
||||||
|
}
|
||||||
|
|
||||||
randomParams(pitchLock?: PitchLock): HiHatParams {
|
randomParams(pitchLock?: PitchLock): HiHatParams {
|
||||||
return {
|
return {
|
||||||
decay: Math.random(),
|
decay: Math.random(),
|
||||||
|
|||||||
@ -21,6 +21,10 @@ export class Input implements SynthEngine<InputParams> {
|
|||||||
return 'input' as const;
|
return 'input' as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getCategory() {
|
||||||
|
return 'Utility' as const;
|
||||||
|
}
|
||||||
|
|
||||||
async record(duration: number): Promise<void> {
|
async record(duration: number): Promise<void> {
|
||||||
const stream = await navigator.mediaDevices.getUserMedia({
|
const stream = await navigator.mediaDevices.getUserMedia({
|
||||||
audio: {
|
audio: {
|
||||||
|
|||||||
@ -39,6 +39,10 @@ export class KarplusStrong implements SynthEngine<KarplusStrongParams> {
|
|||||||
return 'generative' as const;
|
return 'generative' as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getCategory() {
|
||||||
|
return 'Physical' as const;
|
||||||
|
}
|
||||||
|
|
||||||
generate(params: KarplusStrongParams, sampleRate: number, duration: number): [Float32Array, Float32Array] {
|
generate(params: KarplusStrongParams, sampleRate: number, duration: number): [Float32Array, Float32Array] {
|
||||||
const numSamples = Math.floor(sampleRate * duration);
|
const numSamples = Math.floor(sampleRate * duration);
|
||||||
const leftBuffer = new Float32Array(numSamples);
|
const leftBuffer = new Float32Array(numSamples);
|
||||||
|
|||||||
317
src/lib/audio/engines/MassiveAdditive.ts
Normal file
317
src/lib/audio/engines/MassiveAdditive.ts
Normal file
@ -0,0 +1,317 @@
|
|||||||
|
import { CsoundEngine, type CsoundParameter } from './base/CsoundEngine';
|
||||||
|
import type { PitchLock } from './base/SynthEngine';
|
||||||
|
|
||||||
|
interface MassiveAdditiveParams {
|
||||||
|
baseFreq: number;
|
||||||
|
numPartials: number;
|
||||||
|
spectrumShape: number;
|
||||||
|
harmonicSpread: number;
|
||||||
|
inharmonicity: number;
|
||||||
|
attack: number;
|
||||||
|
decay: number;
|
||||||
|
sustain: number;
|
||||||
|
release: number;
|
||||||
|
ampLFORate: number;
|
||||||
|
ampLFODepth: number;
|
||||||
|
ampLFOPhaseSpread: number;
|
||||||
|
freqLFORate: number;
|
||||||
|
freqLFODepth: number;
|
||||||
|
partialDecayRate: number;
|
||||||
|
partialAttackSpread: number;
|
||||||
|
oddEvenBalance: number;
|
||||||
|
shimmer: number;
|
||||||
|
shimmerRate: number;
|
||||||
|
stereoSpread: number;
|
||||||
|
brightness: number;
|
||||||
|
chaos: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MassiveAdditive extends CsoundEngine<MassiveAdditiveParams> {
|
||||||
|
getName(): string {
|
||||||
|
return 'Spectral Add';
|
||||||
|
}
|
||||||
|
|
||||||
|
getDescription(): string {
|
||||||
|
return 'Spectral additive synthesis with octave doubling, micro-detuned beating, and evolving harmonic content';
|
||||||
|
}
|
||||||
|
|
||||||
|
getType() {
|
||||||
|
return 'generative' as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
getCategory() {
|
||||||
|
return 'Additive' as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getOrchestra(): string {
|
||||||
|
return `
|
||||||
|
; Function tables for sine wave
|
||||||
|
gisine ftgen 0, 0, 16384, 10, 1
|
||||||
|
|
||||||
|
instr 1
|
||||||
|
iBaseFreq chnget "baseFreq"
|
||||||
|
iNumPartials chnget "numPartials"
|
||||||
|
iSpectrumShape chnget "spectrumShape"
|
||||||
|
iHarmonicSpread chnget "harmonicSpread"
|
||||||
|
iInharmonicity chnget "inharmonicity"
|
||||||
|
iAttack chnget "attack"
|
||||||
|
iDecay chnget "decay"
|
||||||
|
iSustain chnget "sustain"
|
||||||
|
iRelease chnget "release"
|
||||||
|
iAmpLFORate chnget "ampLFORate"
|
||||||
|
iAmpLFODepth chnget "ampLFODepth"
|
||||||
|
iAmpLFOPhaseSpread chnget "ampLFOPhaseSpread"
|
||||||
|
iFreqLFORate chnget "freqLFORate"
|
||||||
|
iFreqLFODepth chnget "freqLFODepth"
|
||||||
|
iPartialDecayRate chnget "partialDecayRate"
|
||||||
|
iPartialAttackSpread chnget "partialAttackSpread"
|
||||||
|
iOddEvenBalance chnget "oddEvenBalance"
|
||||||
|
iShimmer chnget "shimmer"
|
||||||
|
iShimmerRate chnget "shimmerRate"
|
||||||
|
iStereoSpread chnget "stereoSpread"
|
||||||
|
iBrightness chnget "brightness"
|
||||||
|
iChaos chnget "chaos"
|
||||||
|
|
||||||
|
idur = p3
|
||||||
|
iAttackTime = iAttack * idur
|
||||||
|
iDecayTime = iDecay * idur
|
||||||
|
iReleaseTime = iRelease * idur
|
||||||
|
|
||||||
|
; Main envelope
|
||||||
|
kEnv madsr iAttackTime, iDecayTime, iSustain, iReleaseTime
|
||||||
|
|
||||||
|
; Global modulation LFOs
|
||||||
|
kGlobalAmpLFO oscili 1, iAmpLFORate * 0.5
|
||||||
|
kShimmerLFO oscili 1, iShimmerRate
|
||||||
|
kFreqLFO oscili iFreqLFODepth * 0.02, iFreqLFORate
|
||||||
|
|
||||||
|
; Chaos LFOs
|
||||||
|
kChaosLFO1 oscili 1, iAmpLFORate * 1.618
|
||||||
|
kChaosLFO2 oscili 1, iAmpLFORate * 2.414
|
||||||
|
|
||||||
|
; Create frequency and amplitude tables dynamically
|
||||||
|
iPartialCount = min(iNumPartials, 64)
|
||||||
|
|
||||||
|
; Generate tables for partial frequencies and amplitudes
|
||||||
|
gifreq ftgen 0, 0, 128, -2, 0
|
||||||
|
giamp ftgen 0, 0, 128, -2, 0
|
||||||
|
gifreq2 ftgen 0, 0, 128, -2, 0
|
||||||
|
giamp2 ftgen 0, 0, 128, -2, 0
|
||||||
|
|
||||||
|
iPartialIndex = 0
|
||||||
|
loop_setup:
|
||||||
|
iN = iPartialIndex + 1
|
||||||
|
|
||||||
|
; Calculate frequency ratio with harmonicity and inharmonicity
|
||||||
|
iHarmonic = iN * iHarmonicSpread
|
||||||
|
iInharmonicShift = iInharmonicity * iN * iN * 0.001
|
||||||
|
iFreqRatio = iHarmonic * (1 + iInharmonicShift)
|
||||||
|
|
||||||
|
; Calculate amplitude based on spectrum shape
|
||||||
|
iAmpFalloff = 1 / pow(iN, 1 + iSpectrumShape * 2)
|
||||||
|
|
||||||
|
; Brightness boost for higher partials
|
||||||
|
iBrightnessBoost = 1 + (iBrightness * (iN / iPartialCount) * 2)
|
||||||
|
|
||||||
|
; Odd/even balance
|
||||||
|
iOddEvenFactor = 1
|
||||||
|
if (iN % 2) == 0 then
|
||||||
|
iOddEvenFactor = iOddEvenBalance
|
||||||
|
else
|
||||||
|
iOddEvenFactor = 2 - iOddEvenBalance
|
||||||
|
endif
|
||||||
|
|
||||||
|
iAmp = iAmpFalloff * iBrightnessBoost * iOddEvenFactor
|
||||||
|
|
||||||
|
; Create alternate partial distribution for morphing
|
||||||
|
iFreqRatio2 = iFreqRatio * (1 + (iChaos * 0.05 * sin(iN * 0.7)))
|
||||||
|
iAmp2 = iAmp * (1 - (iN / iPartialCount) * 0.3)
|
||||||
|
|
||||||
|
; Write to tables
|
||||||
|
tableiw iFreqRatio, iPartialIndex, gifreq
|
||||||
|
tableiw iAmp, iPartialIndex, giamp
|
||||||
|
tableiw iFreqRatio2, iPartialIndex, gifreq2
|
||||||
|
tableiw iAmp2, iPartialIndex, giamp2
|
||||||
|
|
||||||
|
iPartialIndex = iPartialIndex + 1
|
||||||
|
if iPartialIndex < iPartialCount goto loop_setup
|
||||||
|
|
||||||
|
; Generate additive synthesis with heavy modulation and octave doubling
|
||||||
|
|
||||||
|
; Slow modulation LFOs for spectral evolution
|
||||||
|
kLFO1 oscili 1, iAmpLFORate * 0.53
|
||||||
|
kLFO2 oscili 1, iAmpLFORate * 0.89
|
||||||
|
kLFO3 oscili 1, iAmpLFORate * 1.37
|
||||||
|
kShimmerLFO1 oscili 1, iShimmerRate * 0.67
|
||||||
|
kShimmerLFO2 oscili 1, iShimmerRate * 1.13
|
||||||
|
|
||||||
|
; Very slow spectral morphing
|
||||||
|
kSpectralMorph oscili 1, iAmpLFORate * 0.19
|
||||||
|
kMorphAmount = (kSpectralMorph + 1) * 0.5
|
||||||
|
|
||||||
|
; Minimal frequency wobble for organic feel
|
||||||
|
kFreqWobble = kFreqLFO * 0.5
|
||||||
|
|
||||||
|
; Micro-detuning for beating (0.1 to 0.5 Hz beating)
|
||||||
|
iMicroDetune = 0.0003 + (iChaos * 0.0005)
|
||||||
|
|
||||||
|
; === FUNDAMENTAL OCTAVE ===
|
||||||
|
; Main voice at fundamental frequency
|
||||||
|
kcps = iBaseFreq * (1 + kFreqWobble)
|
||||||
|
kAmpMain = kEnv * (0.6 + kLFO1 * iAmpLFODepth * 0.2)
|
||||||
|
aMainVoice adsynt2 kAmpMain, kcps, gisine, gifreq, giamp, iPartialCount
|
||||||
|
|
||||||
|
; Slightly detuned fundamental for beating
|
||||||
|
kcpsDetune = iBaseFreq * (1 + iMicroDetune) * (1 + kFreqWobble * 0.93)
|
||||||
|
kAmpDetune = kEnv * (0.5 + kLFO2 * iAmpLFODepth * 0.25)
|
||||||
|
aDetuneVoice adsynt2 kAmpDetune, kcpsDetune, gisine, gifreq, giamp, iPartialCount, 0.25
|
||||||
|
|
||||||
|
; Morphing spectral variant
|
||||||
|
kAmpMorph = kEnv * kMorphAmount * (0.4 + kShimmerLFO1 * iShimmer * 0.3)
|
||||||
|
aMorphVoice adsynt2 kAmpMorph, kcps, gisine, gifreq2, giamp2, iPartialCount, 0.5
|
||||||
|
|
||||||
|
; === OCTAVE UP ===
|
||||||
|
; One octave higher with micro-detune for beating
|
||||||
|
kcpsOctUp = (iBaseFreq * 2) * (1 + kFreqWobble * 1.07)
|
||||||
|
kAmpOctUp = kEnv * (0.35 + kLFO3 * iAmpLFODepth * 0.2 + kShimmerLFO2 * iShimmer * 0.25)
|
||||||
|
aOctaveUp adsynt2 kAmpOctUp, kcpsOctUp, gisine, gifreq, giamp, iPartialCount, 0.33
|
||||||
|
|
||||||
|
; Detuned octave up for complex beating
|
||||||
|
kcpsOctUpDetune = (iBaseFreq * 2) * (1 - iMicroDetune * 0.8) * (1 + kFreqWobble * 1.11)
|
||||||
|
kAmpOctUpDetune = kEnv * (0.3 + kLFO1 * iAmpLFODepth * 0.25)
|
||||||
|
aOctaveUpDetune adsynt2 kAmpOctUpDetune, kcpsOctUpDetune, gisine, gifreq, giamp, iPartialCount, 0.67
|
||||||
|
|
||||||
|
; === OCTAVE DOWN ===
|
||||||
|
; One octave lower with micro-detune
|
||||||
|
kcpsOctDown = (iBaseFreq * 0.5) * (1 + kFreqWobble * 0.97)
|
||||||
|
kAmpOctDown = kEnv * (0.4 + kShimmerLFO1 * iShimmer * 0.3)
|
||||||
|
aOctaveDown adsynt2 kAmpOctDown, kcpsOctDown, gisine, gifreq, giamp, iPartialCount, 0.17
|
||||||
|
|
||||||
|
; Detuned octave down for sub-harmonic beating
|
||||||
|
kcpsOctDownDetune = (iBaseFreq * 0.5) * (1 + iMicroDetune * 1.2) * (1 + kFreqWobble * 1.03)
|
||||||
|
kAmpOctDownDetune = kEnv * (0.35 + kLFO2 * iAmpLFODepth * 0.2)
|
||||||
|
aOctaveDownDetune adsynt2 kAmpOctDownDetune, kcpsOctDownDetune, gisine, gifreq2, giamp, iPartialCount, 0.83
|
||||||
|
|
||||||
|
; === STEREO MIXING ===
|
||||||
|
; Left channel: emphasize fundamental and octave down
|
||||||
|
aOutL = aMainVoice * 0.7 + aDetuneVoice * 0.6 + aMorphVoice * 0.5
|
||||||
|
aOutL = aOutL + aOctaveUp * 0.4 + aOctaveUpDetune * 0.35
|
||||||
|
aOutL = aOutL + aOctaveDown * 0.55 + aOctaveDownDetune * 0.45
|
||||||
|
|
||||||
|
; Right channel: emphasize octaves with different balance
|
||||||
|
aOutR = aMainVoice * 0.65 + aDetuneVoice * 0.55 + aMorphVoice * 0.6
|
||||||
|
aOutR = aOutR + aOctaveUp * 0.5 + aOctaveUpDetune * 0.4
|
||||||
|
aOutR = aOutR + aOctaveDown * 0.45 + aOctaveDownDetune * 0.5
|
||||||
|
|
||||||
|
; Subtle stereo width from chaos
|
||||||
|
kStereoMod = kChaosLFO1 * iChaos * 0.1
|
||||||
|
aOutL = aOutL * (1 - kStereoMod * iStereoSpread)
|
||||||
|
aOutR = aOutR * (1 + kStereoMod * iStereoSpread)
|
||||||
|
|
||||||
|
; Normalize to prevent clipping
|
||||||
|
aOutL = aOutL * 0.28
|
||||||
|
aOutR = aOutR * 0.28
|
||||||
|
|
||||||
|
outs aOutL, aOutR
|
||||||
|
endin
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getParametersForCsound(params: MassiveAdditiveParams): CsoundParameter[] {
|
||||||
|
return [
|
||||||
|
{ channelName: 'baseFreq', value: params.baseFreq },
|
||||||
|
{ channelName: 'numPartials', value: params.numPartials },
|
||||||
|
{ channelName: 'spectrumShape', value: params.spectrumShape },
|
||||||
|
{ channelName: 'harmonicSpread', value: params.harmonicSpread },
|
||||||
|
{ channelName: 'inharmonicity', value: params.inharmonicity },
|
||||||
|
{ channelName: 'attack', value: params.attack },
|
||||||
|
{ channelName: 'decay', value: params.decay },
|
||||||
|
{ channelName: 'sustain', value: params.sustain },
|
||||||
|
{ channelName: 'release', value: params.release },
|
||||||
|
{ channelName: 'ampLFORate', value: params.ampLFORate },
|
||||||
|
{ channelName: 'ampLFODepth', value: params.ampLFODepth },
|
||||||
|
{ channelName: 'ampLFOPhaseSpread', value: params.ampLFOPhaseSpread },
|
||||||
|
{ channelName: 'freqLFORate', value: params.freqLFORate },
|
||||||
|
{ channelName: 'freqLFODepth', value: params.freqLFODepth },
|
||||||
|
{ channelName: 'partialDecayRate', value: params.partialDecayRate },
|
||||||
|
{ channelName: 'partialAttackSpread', value: params.partialAttackSpread },
|
||||||
|
{ channelName: 'oddEvenBalance', value: params.oddEvenBalance },
|
||||||
|
{ channelName: 'shimmer', value: params.shimmer },
|
||||||
|
{ channelName: 'shimmerRate', value: params.shimmerRate },
|
||||||
|
{ channelName: 'stereoSpread', value: params.stereoSpread },
|
||||||
|
{ channelName: 'brightness', value: params.brightness },
|
||||||
|
{ channelName: 'chaos', value: params.chaos },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
randomParams(pitchLock?: PitchLock): MassiveAdditiveParams {
|
||||||
|
const baseFreqChoices = [55, 82.4, 110, 146.8, 220, 293.7, 440];
|
||||||
|
const baseFreq = pitchLock?.enabled
|
||||||
|
? pitchLock.frequency
|
||||||
|
: this.randomChoice(baseFreqChoices) * this.randomRange(0.98, 1.02);
|
||||||
|
|
||||||
|
return {
|
||||||
|
baseFreq,
|
||||||
|
numPartials: this.randomInt(32, 64),
|
||||||
|
spectrumShape: this.randomRange(0.3, 0.7),
|
||||||
|
harmonicSpread: this.randomChoice([1, 1.5, 2, 3]),
|
||||||
|
inharmonicity: this.randomRange(0, 0.5),
|
||||||
|
attack: this.randomRange(0.05, 0.3),
|
||||||
|
decay: this.randomRange(0.15, 0.5),
|
||||||
|
sustain: this.randomRange(0.5, 0.9),
|
||||||
|
release: this.randomRange(0.2, 0.6),
|
||||||
|
ampLFORate: this.randomRange(0.2, 2),
|
||||||
|
ampLFODepth: this.randomRange(0.3, 0.8),
|
||||||
|
ampLFOPhaseSpread: this.randomRange(0.5, 1),
|
||||||
|
freqLFORate: this.randomRange(0.1, 1.5),
|
||||||
|
freqLFODepth: this.randomRange(0.05, 0.3),
|
||||||
|
partialDecayRate: this.randomRange(0.3, 0.8),
|
||||||
|
partialAttackSpread: this.randomRange(0.1, 0.5),
|
||||||
|
oddEvenBalance: this.randomRange(0.5, 1.5),
|
||||||
|
shimmer: this.randomRange(0.3, 0.9),
|
||||||
|
shimmerRate: this.randomRange(0.05, 0.8),
|
||||||
|
stereoSpread: this.randomRange(0.4, 1),
|
||||||
|
brightness: this.randomRange(0.3, 0.9),
|
||||||
|
chaos: this.randomRange(0.1, 0.7),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
mutateParams(
|
||||||
|
params: MassiveAdditiveParams,
|
||||||
|
mutationAmount: number = 0.15,
|
||||||
|
pitchLock?: PitchLock
|
||||||
|
): MassiveAdditiveParams {
|
||||||
|
const baseFreq = pitchLock?.enabled ? pitchLock.frequency : params.baseFreq;
|
||||||
|
|
||||||
|
return {
|
||||||
|
baseFreq,
|
||||||
|
numPartials:
|
||||||
|
Math.random() < 0.15
|
||||||
|
? this.randomInt(16, 64)
|
||||||
|
: Math.round(this.mutateValue(params.numPartials, mutationAmount, 16, 64)),
|
||||||
|
spectrumShape: this.mutateValue(params.spectrumShape, mutationAmount, 0, 1),
|
||||||
|
harmonicSpread:
|
||||||
|
Math.random() < 0.1
|
||||||
|
? this.randomChoice([0.5, 1, 1.5, 2])
|
||||||
|
: params.harmonicSpread,
|
||||||
|
inharmonicity: this.mutateValue(params.inharmonicity, mutationAmount, 0, 0.5),
|
||||||
|
attack: this.mutateValue(params.attack, mutationAmount, 0.001, 0.4),
|
||||||
|
decay: this.mutateValue(params.decay, mutationAmount, 0.05, 0.6),
|
||||||
|
sustain: this.mutateValue(params.sustain, mutationAmount, 0.2, 0.9),
|
||||||
|
release: this.mutateValue(params.release, mutationAmount, 0.05, 0.8),
|
||||||
|
ampLFORate: this.mutateValue(params.ampLFORate, mutationAmount, 0.1, 6),
|
||||||
|
ampLFODepth: this.mutateValue(params.ampLFODepth, mutationAmount, 0, 1),
|
||||||
|
ampLFOPhaseSpread: this.mutateValue(params.ampLFOPhaseSpread, mutationAmount, 0, 1),
|
||||||
|
freqLFORate: this.mutateValue(params.freqLFORate, mutationAmount, 0.1, 5),
|
||||||
|
freqLFODepth: this.mutateValue(params.freqLFODepth, mutationAmount, 0, 0.8),
|
||||||
|
partialDecayRate: this.mutateValue(params.partialDecayRate, mutationAmount, 0, 1),
|
||||||
|
partialAttackSpread: this.mutateValue(params.partialAttackSpread, mutationAmount, 0, 0.8),
|
||||||
|
oddEvenBalance: this.mutateValue(params.oddEvenBalance, mutationAmount, 0.2, 1.8),
|
||||||
|
shimmer: this.mutateValue(params.shimmer, mutationAmount, 0, 1),
|
||||||
|
shimmerRate: this.mutateValue(params.shimmerRate, mutationAmount, 0.05, 2),
|
||||||
|
stereoSpread: this.mutateValue(params.stereoSpread, mutationAmount, 0, 1),
|
||||||
|
brightness: this.mutateValue(params.brightness, mutationAmount, 0, 1),
|
||||||
|
chaos: this.mutateValue(params.chaos, mutationAmount, 0, 0.8),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -39,7 +39,7 @@ interface NoiseDrumParams {
|
|||||||
|
|
||||||
export class NoiseDrum implements SynthEngine {
|
export class NoiseDrum implements SynthEngine {
|
||||||
getName(): string {
|
getName(): string {
|
||||||
return 'NPerc';
|
return 'Noise Perc';
|
||||||
}
|
}
|
||||||
|
|
||||||
getDescription(): string {
|
getDescription(): string {
|
||||||
@ -50,6 +50,10 @@ export class NoiseDrum implements SynthEngine {
|
|||||||
return 'generative' as const;
|
return 'generative' as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getCategory() {
|
||||||
|
return 'Percussion' as const;
|
||||||
|
}
|
||||||
|
|
||||||
randomParams(): NoiseDrumParams {
|
randomParams(): NoiseDrumParams {
|
||||||
// Intelligent parameter generation based on correlated characteristics
|
// Intelligent parameter generation based on correlated characteristics
|
||||||
|
|
||||||
|
|||||||
@ -38,6 +38,10 @@ export class ParticleNoise implements SynthEngine {
|
|||||||
return 'generative' as const;
|
return 'generative' as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getCategory() {
|
||||||
|
return 'Noise' as const;
|
||||||
|
}
|
||||||
|
|
||||||
randomParams(pitchLock?: PitchLock): ParticleNoiseParams {
|
randomParams(pitchLock?: PitchLock): ParticleNoiseParams {
|
||||||
const densityBias = Math.random();
|
const densityBias = Math.random();
|
||||||
|
|
||||||
|
|||||||
@ -65,7 +65,7 @@ export class PhaseDistortionFM implements SynthEngine<PhaseDistortionFMParams> {
|
|||||||
private static workletURL: string | null = null;
|
private static workletURL: string | null = null;
|
||||||
|
|
||||||
getName(): string {
|
getName(): string {
|
||||||
return 'PD';
|
return 'Phase Dist';
|
||||||
}
|
}
|
||||||
|
|
||||||
getDescription(): string {
|
getDescription(): string {
|
||||||
@ -76,6 +76,10 @@ export class PhaseDistortionFM implements SynthEngine<PhaseDistortionFMParams> {
|
|||||||
return 'generative' as const;
|
return 'generative' as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getCategory() {
|
||||||
|
return 'FM' as const;
|
||||||
|
}
|
||||||
|
|
||||||
generate(params: PhaseDistortionFMParams, sampleRate: number, duration: number): [Float32Array, Float32Array] {
|
generate(params: PhaseDistortionFMParams, sampleRate: number, duration: number): [Float32Array, Float32Array] {
|
||||||
const numSamples = Math.floor(sampleRate * duration);
|
const numSamples = Math.floor(sampleRate * duration);
|
||||||
const leftBuffer = new Float32Array(numSamples);
|
const leftBuffer = new Float32Array(numSamples);
|
||||||
|
|||||||
@ -82,6 +82,10 @@ export class Ring implements SynthEngine<RingParams> {
|
|||||||
return 'generative' as const;
|
return 'generative' as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getCategory() {
|
||||||
|
return 'Modulation' as const;
|
||||||
|
}
|
||||||
|
|
||||||
generate(params: RingParams, sampleRate: number, duration: number): [Float32Array, Float32Array] {
|
generate(params: RingParams, sampleRate: number, duration: number): [Float32Array, Float32Array] {
|
||||||
const numSamples = Math.floor(sampleRate * duration);
|
const numSamples = Math.floor(sampleRate * duration);
|
||||||
const leftBuffer = new Float32Array(numSamples);
|
const leftBuffer = new Float32Array(numSamples);
|
||||||
|
|||||||
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;
|
return 'sample' as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getCategory() {
|
||||||
|
return 'Utility' as const;
|
||||||
|
}
|
||||||
|
|
||||||
async loadFile(file: File): Promise<void> {
|
async loadFile(file: File): Promise<void> {
|
||||||
const arrayBuffer = await file.arrayBuffer();
|
const arrayBuffer = await file.arrayBuffer();
|
||||||
const audioContext = new AudioContext();
|
const audioContext = new AudioContext();
|
||||||
|
|||||||
@ -23,7 +23,7 @@ interface SnareParams {
|
|||||||
|
|
||||||
export class Snare implements SynthEngine {
|
export class Snare implements SynthEngine {
|
||||||
getName(): string {
|
getName(): string {
|
||||||
return 'Snare';
|
return 'Noise Snare';
|
||||||
}
|
}
|
||||||
|
|
||||||
getDescription(): string {
|
getDescription(): string {
|
||||||
@ -34,6 +34,10 @@ export class Snare implements SynthEngine {
|
|||||||
return 'generative' as const;
|
return 'generative' as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getCategory() {
|
||||||
|
return 'Percussion' as const;
|
||||||
|
}
|
||||||
|
|
||||||
randomParams(pitchLock?: PitchLock): SnareParams {
|
randomParams(pitchLock?: PitchLock): SnareParams {
|
||||||
return {
|
return {
|
||||||
baseFreq: pitchLock ? this.freqToParam(pitchLock.frequency) : 0.3 + Math.random() * 0.4,
|
baseFreq: pitchLock ? this.freqToParam(pitchLock.frequency) : 0.3 + Math.random() * 0.4,
|
||||||
|
|||||||
@ -50,6 +50,10 @@ export class SubtractiveThreeOsc extends CsoundEngine<SubtractiveThreeOscParams>
|
|||||||
return 'generative' as const;
|
return 'generative' as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getCategory() {
|
||||||
|
return 'Subtractive' as const;
|
||||||
|
}
|
||||||
|
|
||||||
protected getOrchestra(): string {
|
protected getOrchestra(): string {
|
||||||
return `
|
return `
|
||||||
instr 1
|
instr 1
|
||||||
|
|||||||
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;
|
return 'generative' as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getCategory() {
|
||||||
|
return 'FM' as const;
|
||||||
|
}
|
||||||
|
|
||||||
generate(params: TwoOpFMParams, sampleRate: number, duration: number): [Float32Array, Float32Array] {
|
generate(params: TwoOpFMParams, sampleRate: number, duration: number): [Float32Array, Float32Array] {
|
||||||
const numSamples = Math.floor(sampleRate * duration);
|
const numSamples = Math.floor(sampleRate * duration);
|
||||||
const leftBuffer = new Float32Array(numSamples);
|
const leftBuffer = new Float32Array(numSamples);
|
||||||
|
|||||||
@ -38,6 +38,10 @@ export class ZzfxEngine implements SynthEngine<ZzfxParams> {
|
|||||||
return 'generative' as const;
|
return 'generative' as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getCategory() {
|
||||||
|
return 'Experimental' as const;
|
||||||
|
}
|
||||||
|
|
||||||
generate(params: ZzfxParams, sampleRate: number, duration: number): [Float32Array, Float32Array] {
|
generate(params: ZzfxParams, sampleRate: number, duration: number): [Float32Array, Float32Array] {
|
||||||
// ZZFX uses 44100 sample rate internally
|
// ZZFX uses 44100 sample rate internally
|
||||||
const zzfxSampleRate = 44100;
|
const zzfxSampleRate = 44100;
|
||||||
|
|||||||
@ -6,6 +6,17 @@
|
|||||||
|
|
||||||
export type EngineType = 'generative' | 'sample' | 'input';
|
export type EngineType = 'generative' | 'sample' | 'input';
|
||||||
|
|
||||||
|
export type EngineCategory =
|
||||||
|
| 'Additive'
|
||||||
|
| 'Subtractive'
|
||||||
|
| 'FM'
|
||||||
|
| 'Percussion'
|
||||||
|
| 'Noise'
|
||||||
|
| 'Physical'
|
||||||
|
| 'Modulation'
|
||||||
|
| 'Experimental'
|
||||||
|
| 'Utility';
|
||||||
|
|
||||||
export interface PitchLock {
|
export interface PitchLock {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
frequency: number; // Frequency in Hz
|
frequency: number; // Frequency in Hz
|
||||||
@ -15,6 +26,7 @@ export interface SynthEngine<T = any> {
|
|||||||
getName(): string;
|
getName(): string;
|
||||||
getDescription(): string;
|
getDescription(): string;
|
||||||
getType(): EngineType;
|
getType(): EngineType;
|
||||||
|
getCategory(): EngineCategory;
|
||||||
generate(params: T, sampleRate: number, duration: number, pitchLock?: PitchLock): [Float32Array, Float32Array] | Promise<[Float32Array, Float32Array]>;
|
generate(params: T, sampleRate: number, duration: number, pitchLock?: PitchLock): [Float32Array, Float32Array] | Promise<[Float32Array, Float32Array]>;
|
||||||
randomParams(pitchLock?: PitchLock): T;
|
randomParams(pitchLock?: PitchLock): T;
|
||||||
mutateParams(params: T, mutationAmount?: number, pitchLock?: PitchLock): T;
|
mutateParams(params: T, mutationAmount?: number, pitchLock?: PitchLock): T;
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import type { SynthEngine } from './base/SynthEngine';
|
|||||||
import { FourOpFM } from './FourOpFM';
|
import { FourOpFM } from './FourOpFM';
|
||||||
import { TwoOpFM } from './TwoOpFM';
|
import { TwoOpFM } from './TwoOpFM';
|
||||||
import { PhaseDistortionFM } from './PhaseDistortionFM';
|
import { PhaseDistortionFM } from './PhaseDistortionFM';
|
||||||
|
import { FormantFM } from './FormantFM';
|
||||||
import { DubSiren } from './DubSiren';
|
import { DubSiren } from './DubSiren';
|
||||||
import { Benjolin } from './Benjolin';
|
import { Benjolin } from './Benjolin';
|
||||||
import { ZzfxEngine } from './ZzfxEngine';
|
import { ZzfxEngine } from './ZzfxEngine';
|
||||||
@ -17,6 +18,13 @@ import { HiHat } from './HiHat';
|
|||||||
import { ParticleNoise } from './ParticleNoise';
|
import { ParticleNoise } from './ParticleNoise';
|
||||||
import { DustNoise } from './DustNoise';
|
import { DustNoise } from './DustNoise';
|
||||||
import { SubtractiveThreeOsc } from './SubtractiveThreeOsc';
|
import { SubtractiveThreeOsc } from './SubtractiveThreeOsc';
|
||||||
|
import { MassiveAdditive } from './MassiveAdditive';
|
||||||
|
import { FormantPopDrum } from './FormantPopDrum';
|
||||||
|
import { TechnoKick } from './TechnoKick';
|
||||||
|
import { FMTomTom } from './FMTomTom';
|
||||||
|
import { RingCymbal } from './RingCymbal';
|
||||||
|
import { AdditiveBass } from './AdditiveBass';
|
||||||
|
import { FeedbackSnare } from './FeedbackSnare';
|
||||||
|
|
||||||
export const engines: SynthEngine[] = [
|
export const engines: SynthEngine[] = [
|
||||||
new Sample(),
|
new Sample(),
|
||||||
@ -24,6 +32,7 @@ export const engines: SynthEngine[] = [
|
|||||||
new FourOpFM(),
|
new FourOpFM(),
|
||||||
new TwoOpFM(),
|
new TwoOpFM(),
|
||||||
new PhaseDistortionFM(),
|
new PhaseDistortionFM(),
|
||||||
|
new FormantFM(),
|
||||||
new DubSiren(),
|
new DubSiren(),
|
||||||
new Benjolin(),
|
new Benjolin(),
|
||||||
new ZzfxEngine(),
|
new ZzfxEngine(),
|
||||||
@ -31,10 +40,17 @@ export const engines: SynthEngine[] = [
|
|||||||
new Snare(),
|
new Snare(),
|
||||||
new BassDrum(),
|
new BassDrum(),
|
||||||
new HiHat(),
|
new HiHat(),
|
||||||
|
new FormantPopDrum(),
|
||||||
|
new TechnoKick(),
|
||||||
|
new FMTomTom(),
|
||||||
|
new RingCymbal(),
|
||||||
|
new AdditiveBass(),
|
||||||
|
new FeedbackSnare(),
|
||||||
new Ring(),
|
new Ring(),
|
||||||
new KarplusStrong(),
|
new KarplusStrong(),
|
||||||
new AdditiveEngine(),
|
new AdditiveEngine(),
|
||||||
new ParticleNoise(),
|
new ParticleNoise(),
|
||||||
new DustNoise(),
|
new DustNoise(),
|
||||||
new SubtractiveThreeOsc(),
|
new SubtractiveThreeOsc(),
|
||||||
|
new MassiveAdditive(),
|
||||||
];
|
];
|
||||||
|
|||||||
@ -8,6 +8,7 @@ const STORAGE_KEYS = {
|
|||||||
DURATION: 'duration',
|
DURATION: 'duration',
|
||||||
PITCH_LOCK_ENABLED: 'pitchLockEnabled',
|
PITCH_LOCK_ENABLED: 'pitchLockEnabled',
|
||||||
PITCH_LOCK_FREQUENCY: 'pitchLockFrequency',
|
PITCH_LOCK_FREQUENCY: 'pitchLockFrequency',
|
||||||
|
EXPANDED_CATEGORIES: 'expandedCategories',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export function loadVolume(): number {
|
export function loadVolume(): number {
|
||||||
@ -45,3 +46,20 @@ export function loadPitchLockFrequency(): number {
|
|||||||
export function savePitchLockFrequency(frequency: number): void {
|
export function savePitchLockFrequency(frequency: number): void {
|
||||||
localStorage.setItem(STORAGE_KEYS.PITCH_LOCK_FREQUENCY, frequency.toString());
|
localStorage.setItem(STORAGE_KEYS.PITCH_LOCK_FREQUENCY, frequency.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function loadExpandedCategories(): Set<string> {
|
||||||
|
const stored = localStorage.getItem(STORAGE_KEYS.EXPANDED_CATEGORIES);
|
||||||
|
if (stored) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(stored);
|
||||||
|
return new Set(Array.isArray(parsed) ? parsed : []);
|
||||||
|
} catch {
|
||||||
|
return new Set();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new Set();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveExpandedCategories(categories: Set<string>): void {
|
||||||
|
localStorage.setItem(STORAGE_KEYS.EXPANDED_CATEGORIES, JSON.stringify(Array.from(categories)));
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user