Compare commits
9 Commits
467558efd2
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 3cadc23238 | |||
| aba4abb054 | |||
| dfb57a082f | |||
| 6116745795 | |||
| 65a1e16781 | |||
| b700c68b4d | |||
| fb92c3ae2a | |||
| 38479f0253 | |||
| 580aa4b96f |
71
README.md
71
README.md
@ -1,4 +1,4 @@
|
||||
# poof
|
||||
# rsgp
|
||||
|
||||
Audio synthesis web application for generating random sound samples. Users generate sounds via different synthesis engines, mutate parameters, apply audio processors in multiple passes, and export as WAV. Built for musicians seeking unexpected textures and one-shots.
|
||||
|
||||
@ -39,8 +39,8 @@ pnpm build
|
||||
## Docker
|
||||
|
||||
```sh
|
||||
docker build -t poof .
|
||||
docker run -p 8080:80 poof
|
||||
docker build -t rsgp .
|
||||
docker run -p 8080:80 rsgp
|
||||
```
|
||||
|
||||
Opens on http://localhost:8080
|
||||
@ -55,6 +55,69 @@ Opens on http://localhost:8080
|
||||
4. Keep all DSP code, helpers, and types in the same file
|
||||
5. Register in `src/lib/audio/engines/registry.ts`
|
||||
|
||||
### Adding CSound-Based Synthesis Engines
|
||||
|
||||
For complex DSP algorithms, you can leverage CSound's powerful audio language:
|
||||
|
||||
1. Create a single file in `src/lib/audio/engines/` extending the `CsoundEngine<ParamsType>` abstract class
|
||||
2. Define a TypeScript interface for your parameters
|
||||
3. Implement required methods:
|
||||
- `getName()`: Engine display name
|
||||
- `getDescription()`: Brief description
|
||||
- `getType()`: Return `'generative'`, `'sample'`, or `'input'`
|
||||
- `getOrchestra()`: Return CSound orchestra code as a string
|
||||
- `getParametersForCsound(params)`: Map TypeScript params to CSound channel parameters
|
||||
- `randomParams(pitchLock?)`: Generate random parameter values
|
||||
- `mutateParams(params, mutationAmount?, pitchLock?)`: Mutate existing parameters
|
||||
4. Keep all enums, interfaces, and helper logic in the same file
|
||||
5. Register in `src/lib/audio/engines/registry.ts`
|
||||
|
||||
**CSound Orchestra Guidelines:**
|
||||
- Use `instr 1` as your main instrument
|
||||
- Read parameters via `chnget "paramName"`
|
||||
- Duration is available as `p3`
|
||||
- Time-based parameters (attack, decay, release) should be ratios (0-1) scaled by `p3`
|
||||
- Output stereo audio with `outs aLeft, aRight`
|
||||
- The base class handles WAV parsing, normalization, and fade-in
|
||||
|
||||
**Example:**
|
||||
```typescript
|
||||
import { CsoundEngine, type CsoundParameter } from './base/CsoundEngine';
|
||||
|
||||
interface MyParams {
|
||||
frequency: number;
|
||||
resonance: number;
|
||||
}
|
||||
|
||||
export class MyEngine extends CsoundEngine<MyParams> {
|
||||
getName() { return 'My Engine'; }
|
||||
getDescription() { return 'Description'; }
|
||||
getType() { return 'generative' as const; }
|
||||
|
||||
protected getOrchestra(): string {
|
||||
return `
|
||||
instr 1
|
||||
iFreq chnget "frequency"
|
||||
iRes chnget "resonance"
|
||||
aNoise noise 1, 0
|
||||
aOut butterbp aNoise, iFreq, iRes
|
||||
outs aOut, aOut
|
||||
endin
|
||||
`;
|
||||
}
|
||||
|
||||
protected getParametersForCsound(params: MyParams): CsoundParameter[] {
|
||||
return [
|
||||
{ channelName: 'frequency', value: params.frequency },
|
||||
{ channelName: 'resonance', value: params.resonance }
|
||||
];
|
||||
}
|
||||
|
||||
randomParams() { /* ... */ }
|
||||
mutateParams(params, amount = 0.15) { /* ... */ }
|
||||
}
|
||||
```
|
||||
|
||||
### Adding Audio Processors
|
||||
|
||||
1. Create a single file in `src/lib/audio/processors/` implementing the `AudioProcessor` interface
|
||||
@ -67,3 +130,5 @@ Opens on http://localhost:8080
|
||||
|
||||
- Wavetables from [Adventure Kid Waveforms](https://www.adventurekid.se/akrt/waveforms/adventure-kid-waveforms/) by Kristoffer Ekstrand
|
||||
- [Garten Salat](https://garten.salat.dev/) by Felix Roos for drum synthesis inspiration.
|
||||
- [Csound](https://csound.com/) for powerful audio synthesis capabilities.
|
||||
- Steven Yi for some synths: [csound-live-code](https://github.com/kunstmusik/csound-live-code).
|
||||
|
||||
@ -4,9 +4,9 @@
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="description" content="Poof: a sample generator" />
|
||||
<meta name="description" content="RSGP: Random Sample Generator and Processor" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<title>Poof</title>
|
||||
<title>RSGP</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "poof",
|
||||
"name": "rsgp",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
|
||||
BIN
public/tutorial.gif
Normal file
BIN
public/tutorial.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 972 KiB |
461
src/App.svelte
461
src/App.svelte
@ -5,20 +5,41 @@
|
||||
import WelcomeModal from "./lib/components/WelcomeModal.svelte";
|
||||
import ProcessorPopup from "./lib/components/ProcessorPopup.svelte";
|
||||
import { engines } from "./lib/audio/engines/registry";
|
||||
import type { SynthEngine, PitchLock } from "./lib/audio/engines/base/SynthEngine";
|
||||
import type {
|
||||
SynthEngine,
|
||||
PitchLock,
|
||||
} from "./lib/audio/engines/base/SynthEngine";
|
||||
import type { EngineType } from "./lib/audio/engines/base/SynthEngine";
|
||||
import { AudioService } from "./lib/audio/services/AudioService";
|
||||
import { downloadWAV } from "./lib/audio/utils/WAVEncoder";
|
||||
import { loadVolume, saveVolume, loadDuration, saveDuration, loadPitchLockEnabled, savePitchLockEnabled, loadPitchLockFrequency, savePitchLockFrequency } from "./lib/utils/settings";
|
||||
import { cropAudio, cutAudio, processSelection } from "./lib/audio/utils/AudioEdit";
|
||||
import {
|
||||
loadVolume,
|
||||
saveVolume,
|
||||
loadDuration,
|
||||
saveDuration,
|
||||
loadPitchLockEnabled,
|
||||
savePitchLockEnabled,
|
||||
loadPitchLockFrequency,
|
||||
savePitchLockFrequency,
|
||||
loadExpandedCategories,
|
||||
saveExpandedCategories,
|
||||
loadSelectedProcessorCategory,
|
||||
saveSelectedProcessorCategory,
|
||||
} from "./lib/utils/settings";
|
||||
import {
|
||||
cropAudio,
|
||||
cutAudio,
|
||||
processSelection,
|
||||
} from "./lib/audio/utils/AudioEdit";
|
||||
import { generateRandomColor } from "./lib/utils/colors";
|
||||
import { getRandomProcessor } from "./lib/audio/processors/registry";
|
||||
import type { AudioProcessor } from "./lib/audio/processors/AudioProcessor";
|
||||
import type { AudioProcessor, ProcessorCategory } from "./lib/audio/processors/AudioProcessor";
|
||||
import { Sample } from "./lib/audio/engines/Sample";
|
||||
import { Input } from "./lib/audio/engines/Input";
|
||||
import { createKeyboardHandler } from "./lib/utils/keyboard";
|
||||
import { parseFrequencyInput, formatFrequency } from "./lib/utils/pitch";
|
||||
import { UndoManager, type AudioState } from "./lib/utils/UndoManager";
|
||||
import type { EngineCategory } from "./lib/audio/engines/base/SynthEngine";
|
||||
|
||||
let currentEngineIndex = $state(0);
|
||||
const engine = $derived(engines[currentEngineIndex]);
|
||||
@ -31,6 +52,7 @@
|
||||
let duration = $state(loadDuration());
|
||||
let volume = $state(loadVolume());
|
||||
let playbackPosition = $state(-1);
|
||||
let cuePoint = $state(0);
|
||||
let waveformColor = $state(generateRandomColor());
|
||||
let showModal = $state(true);
|
||||
let isProcessed = $state(false);
|
||||
@ -46,15 +68,26 @@
|
||||
let selectionEnd = $state<number | null>(null);
|
||||
let canUndo = $state(false);
|
||||
let sidebarOpen = $state(false);
|
||||
let expandedCategories = $state<Set<string>>(loadExpandedCategories());
|
||||
let selectedProcessorCategory = $state<ProcessorCategory | 'All'>(
|
||||
loadSelectedProcessorCategory()
|
||||
);
|
||||
|
||||
const showDuration = $derived(engineType !== 'sample');
|
||||
const showRandomButton = $derived(engineType === 'generative');
|
||||
const showRecordButton = $derived(engineType === 'input');
|
||||
const showFileDropZone = $derived(engineType === 'sample' && !currentBuffer);
|
||||
const showMutateButton = $derived(engineType === 'generative' && !isProcessed && currentBuffer);
|
||||
const showPitchLock = $derived(engineType === 'generative');
|
||||
const pitchLock = $derived<PitchLock>({ enabled: pitchLockEnabled, frequency: pitchLockFrequency });
|
||||
const hasSelection = $derived(selectionStart !== null && selectionEnd !== null && currentBuffer !== null);
|
||||
const showDuration = $derived(engineType !== "sample");
|
||||
const showRandomButton = $derived(engineType === "generative");
|
||||
const showRecordButton = $derived(engineType === "input");
|
||||
const showFileDropZone = $derived(engineType === "sample" && !currentBuffer);
|
||||
const showMutateButton = $derived(
|
||||
engineType === "generative" && !isProcessed && currentBuffer,
|
||||
);
|
||||
const showPitchLock = $derived(engineType === "generative");
|
||||
const pitchLock = $derived<PitchLock>({
|
||||
enabled: pitchLockEnabled,
|
||||
frequency: pitchLockFrequency,
|
||||
});
|
||||
const hasSelection = $derived(
|
||||
selectionStart !== null && selectionEnd !== null && currentBuffer !== null,
|
||||
);
|
||||
const showEditButtons = $derived(hasSelection);
|
||||
|
||||
$effect(() => {
|
||||
@ -74,6 +107,41 @@
|
||||
savePitchLockFrequency(pitchLockFrequency);
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
saveExpandedCategories(expandedCategories);
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
saveSelectedProcessorCategory(selectedProcessorCategory);
|
||||
});
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
function selectProcessorCategory(category: ProcessorCategory | 'All') {
|
||||
selectedProcessorCategory = category;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
audioService.setPlaybackUpdateCallback((position) => {
|
||||
playbackPosition = position;
|
||||
@ -87,7 +155,7 @@
|
||||
currentParams,
|
||||
isProcessed,
|
||||
waveformColor,
|
||||
currentEngineIndex
|
||||
currentEngineIndex,
|
||||
);
|
||||
}
|
||||
|
||||
@ -100,7 +168,10 @@
|
||||
}
|
||||
|
||||
function restoreState(state: AudioState): void {
|
||||
currentBuffer = audioService.createAudioBuffer([state.leftChannel, state.rightChannel]);
|
||||
currentBuffer = audioService.createAudioBuffer([
|
||||
state.leftChannel,
|
||||
state.rightChannel,
|
||||
]);
|
||||
currentParams = state.params;
|
||||
isProcessed = state.isProcessed;
|
||||
waveformColor = state.waveformColor;
|
||||
@ -125,7 +196,7 @@
|
||||
onProcess: processSound,
|
||||
onDownload: download,
|
||||
onUndo: undo,
|
||||
onPlayFromStart: replaySound,
|
||||
onPlayFromStart: togglePlayback,
|
||||
onDurationDecrease: (large) => {
|
||||
duration = Math.max(0.05, duration - (large ? 1 : 0.05));
|
||||
},
|
||||
@ -190,8 +261,14 @@
|
||||
if (!currentParams) return;
|
||||
|
||||
const sampleRate = audioService.getSampleRate();
|
||||
const data = await engine.generate(currentParams, sampleRate, duration, pitchLock);
|
||||
const data = await engine.generate(
|
||||
currentParams,
|
||||
sampleRate,
|
||||
duration,
|
||||
pitchLock,
|
||||
);
|
||||
currentBuffer = audioService.createAudioBuffer(data);
|
||||
cuePoint = 0;
|
||||
audioService.play(currentBuffer);
|
||||
}
|
||||
|
||||
@ -201,9 +278,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
function playFromPosition(offset: number) {
|
||||
if (currentBuffer) {
|
||||
audioService.play(currentBuffer, offset);
|
||||
function setCuePoint(offset: number) {
|
||||
cuePoint = offset;
|
||||
}
|
||||
|
||||
function togglePlayback() {
|
||||
if (playbackPosition >= 0) {
|
||||
audioService.stop();
|
||||
} else if (currentBuffer) {
|
||||
audioService.play(currentBuffer, cuePoint);
|
||||
}
|
||||
}
|
||||
|
||||
@ -235,15 +318,21 @@
|
||||
start,
|
||||
end,
|
||||
processor,
|
||||
sampleRate
|
||||
sampleRate,
|
||||
);
|
||||
} else {
|
||||
const leftChannel = currentBuffer.getChannelData(0);
|
||||
const rightChannel = currentBuffer.getChannelData(1);
|
||||
[processedLeft, processedRight] = await processor.process(leftChannel, rightChannel);
|
||||
[processedLeft, processedRight] = await processor.process(
|
||||
leftChannel,
|
||||
rightChannel,
|
||||
);
|
||||
}
|
||||
|
||||
currentBuffer = audioService.createAudioBuffer([processedLeft, processedRight]);
|
||||
currentBuffer = audioService.createAudioBuffer([
|
||||
processedLeft,
|
||||
processedRight,
|
||||
]);
|
||||
isProcessed = true;
|
||||
audioService.play(currentBuffer);
|
||||
hideProcessorPopup();
|
||||
@ -259,7 +348,7 @@
|
||||
clearSelection();
|
||||
sidebarOpen = false;
|
||||
|
||||
if (engineType === 'generative') {
|
||||
if (engineType === "generative") {
|
||||
generateRandom();
|
||||
}
|
||||
}
|
||||
@ -282,7 +371,7 @@
|
||||
isProcessed = false;
|
||||
regenerateBuffer();
|
||||
} catch (error) {
|
||||
console.error('Failed to load audio file:', error);
|
||||
console.error("Failed to load audio file:", error);
|
||||
alert(`Failed to load audio file: ${error}`);
|
||||
}
|
||||
}
|
||||
@ -300,7 +389,7 @@
|
||||
isProcessed = false;
|
||||
regenerateBuffer();
|
||||
} catch (error) {
|
||||
console.error('Failed to record audio:', error);
|
||||
console.error("Failed to record audio:", error);
|
||||
alert(`Failed to record audio: ${error}`);
|
||||
} finally {
|
||||
isRecording = false;
|
||||
@ -315,8 +404,8 @@
|
||||
if (!files?.length) return;
|
||||
|
||||
const file = files[0];
|
||||
if (!file.type.startsWith('audio/')) {
|
||||
alert('Please drop an audio file');
|
||||
if (!file.type.startsWith("audio/")) {
|
||||
alert("Please drop an audio file");
|
||||
return;
|
||||
}
|
||||
|
||||
@ -336,12 +425,17 @@
|
||||
function showPopup() {
|
||||
if (popupTimeout) clearTimeout(popupTimeout);
|
||||
showProcessorPopup = true;
|
||||
popupTimeout = setTimeout(() => showProcessorPopup = false, 2000);
|
||||
popupTimeout = setTimeout(() => (showProcessorPopup = false), 2000);
|
||||
}
|
||||
|
||||
function keepPopupOpen() {
|
||||
if (popupTimeout) clearTimeout(popupTimeout);
|
||||
showProcessorPopup = true;
|
||||
}
|
||||
|
||||
function scheduleHidePopup() {
|
||||
if (popupTimeout) clearTimeout(popupTimeout);
|
||||
popupTimeout = setTimeout(() => showProcessorPopup = false, 200);
|
||||
popupTimeout = setTimeout(() => (showProcessorPopup = false), 200);
|
||||
}
|
||||
|
||||
function hideProcessorPopup() {
|
||||
@ -360,7 +454,8 @@
|
||||
}
|
||||
|
||||
function cropSelection() {
|
||||
if (!currentBuffer || selectionStart === null || selectionEnd === null) return;
|
||||
if (!currentBuffer || selectionStart === null || selectionEnd === null)
|
||||
return;
|
||||
|
||||
pushState();
|
||||
|
||||
@ -368,7 +463,12 @@
|
||||
const end = Math.max(selectionStart, selectionEnd);
|
||||
|
||||
const sampleRate = audioService.getSampleRate();
|
||||
const [newLeft, newRight] = cropAudio(currentBuffer, start, end, sampleRate);
|
||||
const [newLeft, newRight] = cropAudio(
|
||||
currentBuffer,
|
||||
start,
|
||||
end,
|
||||
sampleRate,
|
||||
);
|
||||
|
||||
currentBuffer = audioService.createAudioBuffer([newLeft, newRight]);
|
||||
clearSelection();
|
||||
@ -376,7 +476,8 @@
|
||||
}
|
||||
|
||||
function cutSelection() {
|
||||
if (!currentBuffer || selectionStart === null || selectionEnd === null) return;
|
||||
if (!currentBuffer || selectionStart === null || selectionEnd === null)
|
||||
return;
|
||||
|
||||
pushState();
|
||||
|
||||
@ -405,31 +506,54 @@
|
||||
|
||||
<div class="container">
|
||||
{#if sidebarOpen}
|
||||
<div class="sidebar-overlay" onclick={toggleSidebar} onkeydown={(e) => e.key === 'Escape' && toggleSidebar()} role="button" tabindex="-1" aria-label="Close sidebar"></div>
|
||||
<div
|
||||
class="sidebar-overlay"
|
||||
onclick={toggleSidebar}
|
||||
onkeydown={(e) => e.key === "Escape" && toggleSidebar()}
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
aria-label="Close sidebar"
|
||||
></div>
|
||||
{/if}
|
||||
<div class="top-bar">
|
||||
<button class="hamburger" onclick={toggleSidebar} aria-label="Toggle engine menu">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="3" y1="6" x2="21" y2="6"/>
|
||||
<line x1="3" y1="12" x2="21" y2="12"/>
|
||||
<line x1="3" y1="18" x2="21" y2="18"/>
|
||||
</svg>
|
||||
</button>
|
||||
<h1 class="app-title">Poof: a sample generator</h1>
|
||||
<div class="controls-group">
|
||||
<button
|
||||
class="hamburger"
|
||||
onclick={toggleSidebar}
|
||||
aria-label="Toggle engine menu"
|
||||
>
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<line x1="3" y1="6" x2="21" y2="6" />
|
||||
<line x1="3" y1="12" x2="21" y2="12" />
|
||||
<line x1="3" y1="18" x2="21" y2="18" />
|
||||
</svg>
|
||||
</button>
|
||||
<h1 class="app-title">RSGP: Random Sample Generator and Processor</h1>
|
||||
<div class="controls-group">
|
||||
{#if showPitchLock}
|
||||
<div class="control-item pitch-lock-control">
|
||||
<div class="control-header">
|
||||
<label for="pitch">Pitch</label>
|
||||
<label class="custom-checkbox" title="Lock pitch across random/mutate">
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={pitchLockEnabled}
|
||||
/>
|
||||
<label
|
||||
class="custom-checkbox"
|
||||
title="Lock pitch across random/mutate"
|
||||
>
|
||||
<input type="checkbox" bind:checked={pitchLockEnabled} />
|
||||
<span class="checkbox-box" class:checked={pitchLockEnabled}>
|
||||
{#if pitchLockEnabled}
|
||||
<svg width="10" height="10" viewBox="0 0 10 10" fill="none">
|
||||
<path d="M1 5L4 8L9 2" stroke="currentColor" stroke-width="2" stroke-linecap="square"/>
|
||||
<path
|
||||
d="M1 5L4 8L9 2"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="square"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
</span>
|
||||
@ -485,94 +609,129 @@
|
||||
<div class="content-wrapper">
|
||||
<div class="sidebar" class:open={sidebarOpen}>
|
||||
<div class="sidebar-content">
|
||||
{#each engines as currentEngine, index}
|
||||
<button
|
||||
class="engine-button"
|
||||
class:active={currentEngineIndex === index}
|
||||
data-description={currentEngine.getDescription()}
|
||||
onclick={() => switchEngine(index)}
|
||||
>
|
||||
{currentEngine.getName()}
|
||||
</button>
|
||||
{#each Array.from(enginesByCategory.entries()) as [category, categoryEngines]}
|
||||
<div class="category-section">
|
||||
<button
|
||||
class="category-header"
|
||||
class:collapsed={!expandedCategories.has(category)}
|
||||
onclick={() => toggleCategory(category)}
|
||||
>
|
||||
<svg
|
||||
class="category-arrow"
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 12 12"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M3 4.5L6 7.5L9 4.5"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="square"
|
||||
/>
|
||||
</svg>
|
||||
<span>{category}</span>
|
||||
</button>
|
||||
{#if expandedCategories.has(category)}
|
||||
{#each categoryEngines as currentEngine}
|
||||
{@const index = engines.indexOf(currentEngine)}
|
||||
<button
|
||||
class="engine-button"
|
||||
class:active={currentEngineIndex === index}
|
||||
data-description={currentEngine.getDescription()}
|
||||
onclick={() => switchEngine(index)}
|
||||
>
|
||||
{currentEngine.getName()}
|
||||
</button>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main-area">
|
||||
<div class="waveform-container">
|
||||
{#if showFileDropZone}
|
||||
<div
|
||||
class="file-drop-zone"
|
||||
class:drag-over={isDragOver}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
ondrop={handleDrop}
|
||||
ondragover={handleDragOver}
|
||||
ondragleave={handleDragLeave}
|
||||
>
|
||||
<div class="drop-zone-content">
|
||||
<h2>Drop an audio file here</h2>
|
||||
<label for="file-input" class="file-input-label">
|
||||
<input
|
||||
id="file-input"
|
||||
type="file"
|
||||
accept="audio/*"
|
||||
onchange={handleFileInput}
|
||||
style="display: none;"
|
||||
/>
|
||||
Choose a file
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<WaveformDisplay
|
||||
buffer={currentBuffer}
|
||||
color={waveformColor}
|
||||
{playbackPosition}
|
||||
{selectionStart}
|
||||
{selectionEnd}
|
||||
onselectionchange={handleSelectionChange}
|
||||
onclick={playFromPosition}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<div class="bottom-controls">
|
||||
{#if showRandomButton}
|
||||
<button onclick={generateRandom}>Random (R)</button>
|
||||
{/if}
|
||||
{#if showRecordButton}
|
||||
<button onclick={recordAudio} disabled={isRecording}>
|
||||
{isRecording ? 'Recording...' : 'Record'}
|
||||
</button>
|
||||
{/if}
|
||||
{#if showMutateButton}
|
||||
<button onclick={mutate}>Mutate (M)</button>
|
||||
{/if}
|
||||
{#if showEditButtons}
|
||||
<button onclick={cropSelection}>Crop</button>
|
||||
<button onclick={cutSelection}>Cut</button>
|
||||
{/if}
|
||||
{#if currentBuffer}
|
||||
<div class="waveform-container">
|
||||
{#if showFileDropZone}
|
||||
<div
|
||||
class="process-button-container"
|
||||
role="group"
|
||||
onmouseenter={showPopup}
|
||||
onmouseleave={scheduleHidePopup}
|
||||
class="file-drop-zone"
|
||||
class:drag-over={isDragOver}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
ondrop={handleDrop}
|
||||
ondragover={handleDragOver}
|
||||
ondragleave={handleDragLeave}
|
||||
>
|
||||
<button onclick={processSound}>Process (P)</button>
|
||||
{#if showProcessorPopup}
|
||||
<ProcessorPopup onselect={applyProcessor} />
|
||||
{/if}
|
||||
<div class="drop-zone-content">
|
||||
<h2>Drop an audio file here</h2>
|
||||
<label for="file-input" class="file-input-label">
|
||||
<input
|
||||
id="file-input"
|
||||
type="file"
|
||||
accept="audio/*"
|
||||
onchange={handleFileInput}
|
||||
style="display: none;"
|
||||
/>
|
||||
Choose a file
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<button onclick={download}>Download (D)</button>
|
||||
<button onclick={undo} disabled={!canUndo}>Undo (Z)</button>
|
||||
{:else}
|
||||
<WaveformDisplay
|
||||
buffer={currentBuffer}
|
||||
color={waveformColor}
|
||||
{playbackPosition}
|
||||
{cuePoint}
|
||||
{selectionStart}
|
||||
{selectionEnd}
|
||||
onselectionchange={handleSelectionChange}
|
||||
onclick={setCuePoint}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<div class="bottom-controls">
|
||||
{#if showRandomButton}
|
||||
<button onclick={generateRandom}>Random (R)</button>
|
||||
{/if}
|
||||
{#if showRecordButton}
|
||||
<button onclick={recordAudio} disabled={isRecording}>
|
||||
{isRecording ? "Recording..." : "Record"}
|
||||
</button>
|
||||
{/if}
|
||||
{#if showMutateButton}
|
||||
<button onclick={mutate}>Mutate (M)</button>
|
||||
{/if}
|
||||
{#if showEditButtons}
|
||||
<button onclick={cropSelection}>Crop</button>
|
||||
<button onclick={cutSelection}>Cut</button>
|
||||
{/if}
|
||||
{#if currentBuffer}
|
||||
<div
|
||||
class="process-button-container"
|
||||
role="group"
|
||||
onmouseenter={showPopup}
|
||||
onmouseleave={scheduleHidePopup}
|
||||
>
|
||||
<button onclick={processSound}>Process (P)</button>
|
||||
{#if showProcessorPopup}
|
||||
<div role="menu" tabindex="-1" onmouseenter={keepPopupOpen} onmouseleave={scheduleHidePopup}>
|
||||
<ProcessorPopup
|
||||
onselect={applyProcessor}
|
||||
selectedCategory={selectedProcessorCategory}
|
||||
onselectcategory={selectProcessorCategory}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<button onclick={download}>Download (D)</button>
|
||||
<button onclick={undo} disabled={!canUndo}>Undo (Z)</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="vu-meter-container">
|
||||
<VUMeter buffer={currentBuffer} {playbackPosition} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="vu-meter-container">
|
||||
<VUMeter buffer={currentBuffer} {playbackPosition} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if showModal}
|
||||
@ -646,7 +805,9 @@
|
||||
border: 1px solid #3a3a3a;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s, background-color 0.2s;
|
||||
transition:
|
||||
border-color 0.2s,
|
||||
background-color 0.2s;
|
||||
}
|
||||
|
||||
.hamburger svg {
|
||||
@ -693,12 +854,52 @@
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.category-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.category-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
font-size: 0.65rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
padding: 0.5rem 0.5rem;
|
||||
background-color: #1a1a1a;
|
||||
border: none;
|
||||
border-bottom: 1px solid #333;
|
||||
color: #999;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
color 0.2s,
|
||||
background-color 0.2s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.category-header:hover {
|
||||
color: #ccc;
|
||||
background-color: #222;
|
||||
}
|
||||
|
||||
.category-arrow {
|
||||
transition: transform 0.2s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.category-header.collapsed .category-arrow {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.engine-button {
|
||||
opacity: 0.7;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
font-size: 0.75rem;
|
||||
padding: 0.5rem 0.5rem;
|
||||
padding: 0.5rem 0.5rem 0.5rem 1rem;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@ -866,7 +1067,10 @@
|
||||
color: #fff;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
transition: border-color 0.2s, background-color 0.2s, box-shadow 0.2s;
|
||||
transition:
|
||||
border-color 0.2s,
|
||||
background-color 0.2s,
|
||||
box-shadow 0.2s;
|
||||
font-variant-numeric: tabular-nums;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@ -897,7 +1101,6 @@
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.sidebar {
|
||||
position: static;
|
||||
|
||||
228
src/lib/audio/engines/AdditiveBass.ts
Normal file
228
src/lib/audio/engines/AdditiveBass.ts
Normal file
@ -0,0 +1,228 @@
|
||||
import { CsoundEngine, type CsoundParameter } from './base/CsoundEngine';
|
||||
import type { PitchLock } from './base/SynthEngine';
|
||||
|
||||
interface AdditiveBassParams {
|
||||
baseFreq: number;
|
||||
pitchSweep: number;
|
||||
pitchDecay: number;
|
||||
overtoneAmp: number;
|
||||
overtoneFreqMult: number;
|
||||
noiseAmp: number;
|
||||
noiseDecay: number;
|
||||
filterResonance: number;
|
||||
filterCutoff: number;
|
||||
attack: number;
|
||||
decay: number;
|
||||
waveshape: number;
|
||||
bodyResonance: number;
|
||||
click: number;
|
||||
harmonicSpread: number;
|
||||
}
|
||||
|
||||
export class AdditiveBass extends CsoundEngine<AdditiveBassParams> {
|
||||
getName(): string {
|
||||
return 'Additive Bass';
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return 'Deep bass drum using additive synthesis with pink noise and waveshaping';
|
||||
}
|
||||
|
||||
getType() {
|
||||
return 'generative' as const;
|
||||
}
|
||||
|
||||
getCategory() {
|
||||
return 'Percussion' as const;
|
||||
}
|
||||
|
||||
protected getOrchestra(): string {
|
||||
return `
|
||||
instr 1
|
||||
iBaseFreq chnget "baseFreq"
|
||||
iPitchSweep chnget "pitchSweep"
|
||||
iPitchDecay chnget "pitchDecay"
|
||||
iOvertoneAmp chnget "overtoneAmp"
|
||||
iOvertoneFreqMult chnget "overtoneFreqMult"
|
||||
iNoiseAmp chnget "noiseAmp"
|
||||
iNoiseDecay chnget "noiseDecay"
|
||||
iFilterResonance chnget "filterResonance"
|
||||
iFilterCutoff chnget "filterCutoff"
|
||||
iAttack chnget "attack"
|
||||
iDecay chnget "decay"
|
||||
iWaveshape chnget "waveshape"
|
||||
iBodyResonance chnget "bodyResonance"
|
||||
iClick chnget "click"
|
||||
iHarmonicSpread chnget "harmonicSpread"
|
||||
|
||||
idur = p3
|
||||
iAttackTime = iAttack * idur
|
||||
iDecayTime = iDecay * idur
|
||||
iPitchDecayTime = iPitchDecay * idur
|
||||
iNoiseDecayTime = iNoiseDecay * idur
|
||||
|
||||
; Pitch envelope: exponential sweep from high to low
|
||||
kPitchEnv expseg iBaseFreq * (1 + iPitchSweep * 3), iPitchDecayTime, iBaseFreq, idur - iPitchDecayTime, iBaseFreq * 0.95
|
||||
|
||||
; Main amplitude envelope with attack and decay
|
||||
kAmpEnv linseg 0, iAttackTime, 1, iDecayTime, 0.001, 0.001, 0
|
||||
kAmpEnv = kAmpEnv * kAmpEnv
|
||||
|
||||
; Generate fundamental sine wave
|
||||
aFund oscili 0.7, kPitchEnv
|
||||
|
||||
; Generate overtone at multiple of fundamental
|
||||
aOvertone oscili iOvertoneAmp, kPitchEnv * iOvertoneFreqMult
|
||||
|
||||
; Add harmonic spread (additional harmonics)
|
||||
aHarm2 oscili iHarmonicSpread * 0.3, kPitchEnv * 3
|
||||
aHarm3 oscili iHarmonicSpread * 0.2, kPitchEnv * 5
|
||||
|
||||
; Mix oscillators
|
||||
aMix = aFund + aOvertone + aHarm2 + aHarm3
|
||||
|
||||
; Apply waveshaping (hyperbolic tangent style)
|
||||
if iWaveshape > 0.1 then
|
||||
aMix = tanh(aMix * (1 + iWaveshape * 3))
|
||||
endif
|
||||
|
||||
; Generate pink noise
|
||||
aPink pinkish 1
|
||||
|
||||
; Noise envelope (fast decay)
|
||||
kNoiseEnv expseg 1, iNoiseDecayTime, 0.001, idur - iNoiseDecayTime, 0.001
|
||||
aPinkScaled = aPink * iNoiseAmp * kNoiseEnv
|
||||
|
||||
; Add noise to mix
|
||||
aMix = aMix + aPinkScaled
|
||||
|
||||
; Click transient (high frequency burst at start)
|
||||
if iClick > 0.1 then
|
||||
kClickEnv linseg 1, 0.005, 0, idur - 0.005, 0
|
||||
aClick oscili iClick * 0.4, kPitchEnv * 8
|
||||
aMix = aMix + aClick * kClickEnv
|
||||
endif
|
||||
|
||||
; Apply resonant low-pass filter
|
||||
kFilterFreq = iFilterCutoff * (1 + kPitchEnv / iBaseFreq * 0.5)
|
||||
aFiltered rezzy aMix, kFilterFreq, iFilterResonance
|
||||
|
||||
; Body resonance (second resonant filter at fundamental)
|
||||
if iBodyResonance > 0.1 then
|
||||
aBodyFilt butterbp aFiltered, kPitchEnv * 0.5, 20
|
||||
aFiltered = aFiltered + aBodyFilt * iBodyResonance
|
||||
endif
|
||||
|
||||
; Apply main envelope
|
||||
aOut = aFiltered * kAmpEnv * 0.5
|
||||
|
||||
; Stereo - slightly different phase and detune for right channel
|
||||
kPitchEnvR expseg iBaseFreq * 1.002 * (1 + iPitchSweep * 3), iPitchDecayTime, iBaseFreq * 1.002, idur - iPitchDecayTime, iBaseFreq * 0.952
|
||||
|
||||
aFundR oscili 0.7, kPitchEnvR
|
||||
aOvertoneR oscili iOvertoneAmp, kPitchEnvR * iOvertoneFreqMult
|
||||
aHarm2R oscili iHarmonicSpread * 0.3, kPitchEnvR * 3
|
||||
aHarm3R oscili iHarmonicSpread * 0.2, kPitchEnvR * 5
|
||||
|
||||
aMixR = aFundR + aOvertoneR + aHarm2R + aHarm3R
|
||||
|
||||
if iWaveshape > 0.1 then
|
||||
aMixR = tanh(aMixR * (1 + iWaveshape * 3))
|
||||
endif
|
||||
|
||||
aPinkR pinkish 1
|
||||
aPinkScaledR = aPinkR * iNoiseAmp * kNoiseEnv
|
||||
aMixR = aMixR + aPinkScaledR
|
||||
|
||||
if iClick > 0.1 then
|
||||
aClickR oscili iClick * 0.4, kPitchEnvR * 8
|
||||
aMixR = aMixR + aClickR * kClickEnv
|
||||
endif
|
||||
|
||||
kFilterFreqR = iFilterCutoff * (1 + kPitchEnvR / iBaseFreq * 0.5)
|
||||
aFilteredR rezzy aMixR, kFilterFreqR, iFilterResonance
|
||||
|
||||
if iBodyResonance > 0.1 then
|
||||
aBodyFiltR butterbp aFilteredR, kPitchEnvR * 0.5, 20
|
||||
aFilteredR = aFilteredR + aBodyFiltR * iBodyResonance
|
||||
endif
|
||||
|
||||
aOutR = aFilteredR * kAmpEnv * 0.5
|
||||
|
||||
outs aOut, aOutR
|
||||
endin
|
||||
`;
|
||||
}
|
||||
|
||||
protected getParametersForCsound(params: AdditiveBassParams): CsoundParameter[] {
|
||||
return [
|
||||
{ channelName: 'baseFreq', value: params.baseFreq },
|
||||
{ channelName: 'pitchSweep', value: params.pitchSweep },
|
||||
{ channelName: 'pitchDecay', value: params.pitchDecay },
|
||||
{ channelName: 'overtoneAmp', value: params.overtoneAmp },
|
||||
{ channelName: 'overtoneFreqMult', value: params.overtoneFreqMult },
|
||||
{ channelName: 'noiseAmp', value: params.noiseAmp },
|
||||
{ channelName: 'noiseDecay', value: params.noiseDecay },
|
||||
{ channelName: 'filterResonance', value: params.filterResonance },
|
||||
{ channelName: 'filterCutoff', value: params.filterCutoff },
|
||||
{ channelName: 'attack', value: params.attack },
|
||||
{ channelName: 'decay', value: params.decay },
|
||||
{ channelName: 'waveshape', value: params.waveshape },
|
||||
{ channelName: 'bodyResonance', value: params.bodyResonance },
|
||||
{ channelName: 'click', value: params.click },
|
||||
{ channelName: 'harmonicSpread', value: params.harmonicSpread },
|
||||
];
|
||||
}
|
||||
|
||||
randomParams(pitchLock?: PitchLock): AdditiveBassParams {
|
||||
const baseFreq = pitchLock?.enabled ? pitchLock.frequency : this.randomRange(35, 80);
|
||||
|
||||
const overtoneMultChoices = [1.5, 2.0, 2.5, 3.0, 4.0];
|
||||
|
||||
return {
|
||||
baseFreq,
|
||||
pitchSweep: this.randomRange(0.3, 1.0),
|
||||
pitchDecay: this.randomRange(0.02, 0.15),
|
||||
overtoneAmp: this.randomRange(0.2, 0.7),
|
||||
overtoneFreqMult: this.randomChoice(overtoneMultChoices),
|
||||
noiseAmp: this.randomRange(0.05, 0.3),
|
||||
noiseDecay: this.randomRange(0.01, 0.08),
|
||||
filterResonance: this.randomRange(5, 25),
|
||||
filterCutoff: this.randomRange(100, 800),
|
||||
attack: this.randomRange(0.001, 0.02),
|
||||
decay: this.randomRange(0.3, 0.8),
|
||||
waveshape: this.randomRange(0, 0.7),
|
||||
bodyResonance: this.randomRange(0, 0.5),
|
||||
click: this.randomRange(0, 0.6),
|
||||
harmonicSpread: this.randomRange(0, 0.5),
|
||||
};
|
||||
}
|
||||
|
||||
mutateParams(
|
||||
params: AdditiveBassParams,
|
||||
mutationAmount: number = 0.15,
|
||||
pitchLock?: PitchLock
|
||||
): AdditiveBassParams {
|
||||
const baseFreq = pitchLock?.enabled ? pitchLock.frequency : params.baseFreq;
|
||||
const overtoneMultChoices = [1.5, 2.0, 2.5, 3.0, 4.0];
|
||||
|
||||
return {
|
||||
baseFreq,
|
||||
pitchSweep: this.mutateValue(params.pitchSweep, mutationAmount, 0.1, 1.5),
|
||||
pitchDecay: this.mutateValue(params.pitchDecay, mutationAmount, 0.01, 0.25),
|
||||
overtoneAmp: this.mutateValue(params.overtoneAmp, mutationAmount, 0, 1.0),
|
||||
overtoneFreqMult:
|
||||
Math.random() < 0.1 ? this.randomChoice(overtoneMultChoices) : params.overtoneFreqMult,
|
||||
noiseAmp: this.mutateValue(params.noiseAmp, mutationAmount, 0, 0.5),
|
||||
noiseDecay: this.mutateValue(params.noiseDecay, mutationAmount, 0.005, 0.15),
|
||||
filterResonance: this.mutateValue(params.filterResonance, mutationAmount, 2, 40),
|
||||
filterCutoff: this.mutateValue(params.filterCutoff, mutationAmount, 80, 1200),
|
||||
attack: this.mutateValue(params.attack, mutationAmount, 0.001, 0.05),
|
||||
decay: this.mutateValue(params.decay, mutationAmount, 0.15, 0.95),
|
||||
waveshape: this.mutateValue(params.waveshape, mutationAmount, 0, 1),
|
||||
bodyResonance: this.mutateValue(params.bodyResonance, mutationAmount, 0, 0.8),
|
||||
click: this.mutateValue(params.click, mutationAmount, 0, 0.8),
|
||||
harmonicSpread: this.mutateValue(params.harmonicSpread, mutationAmount, 0, 0.7),
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -80,7 +80,7 @@ export interface AdditiveParams {
|
||||
|
||||
export class AdditiveEngine implements SynthEngine<AdditiveParams> {
|
||||
getName(): string {
|
||||
return 'Prism';
|
||||
return 'Glass Prism';
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
@ -91,6 +91,10 @@ export class AdditiveEngine implements SynthEngine<AdditiveParams> {
|
||||
return 'generative' as const;
|
||||
}
|
||||
|
||||
getCategory() {
|
||||
return 'Additive' as const;
|
||||
}
|
||||
|
||||
generate(params: AdditiveParams, sampleRate: number, duration: number): [Float32Array, Float32Array] {
|
||||
const numSamples = Math.floor(sampleRate * duration);
|
||||
const leftBuffer = new Float32Array(numSamples);
|
||||
|
||||
@ -57,7 +57,7 @@ interface BassDrumParams {
|
||||
|
||||
export class BassDrum implements SynthEngine {
|
||||
getName(): string {
|
||||
return 'Kick';
|
||||
return 'Dark Kick';
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
@ -68,6 +68,10 @@ export class BassDrum implements SynthEngine {
|
||||
return 'generative' as const;
|
||||
}
|
||||
|
||||
getCategory() {
|
||||
return 'Percussion' as const;
|
||||
}
|
||||
|
||||
randomParams(pitchLock?: PitchLock): BassDrumParams {
|
||||
// Choose a kick character/style
|
||||
const styleRoll = Math.random();
|
||||
|
||||
@ -73,6 +73,10 @@ export class Benjolin implements SynthEngine<BenjolinParams> {
|
||||
return 'generative' as const;
|
||||
}
|
||||
|
||||
getCategory() {
|
||||
return 'Experimental' as const;
|
||||
}
|
||||
|
||||
generate(params: BenjolinParams, sampleRate: number, duration: number): [Float32Array, Float32Array] {
|
||||
const numSamples = Math.floor(duration * sampleRate);
|
||||
const left = new Float32Array(numSamples);
|
||||
|
||||
350
src/lib/audio/engines/CombResonator.ts
Normal file
350
src/lib/audio/engines/CombResonator.ts
Normal file
@ -0,0 +1,350 @@
|
||||
import { CsoundEngine, type CsoundParameter } from './base/CsoundEngine';
|
||||
import type { PitchLock } from './base/SynthEngine';
|
||||
|
||||
enum SourceType {
|
||||
Saw = 0,
|
||||
Pulse = 1,
|
||||
NoiseBurst = 2,
|
||||
Triangle = 3,
|
||||
}
|
||||
|
||||
interface CombParams {
|
||||
feedback: number;
|
||||
brightness: number;
|
||||
}
|
||||
|
||||
interface ResonatorParams {
|
||||
frequency: number;
|
||||
resonance: number;
|
||||
envAmount: number;
|
||||
attack: number;
|
||||
decay: number;
|
||||
sustain: number;
|
||||
release: number;
|
||||
}
|
||||
|
||||
interface DelayParams {
|
||||
time1: number;
|
||||
time2: number;
|
||||
feedback: number;
|
||||
filterFreq: number;
|
||||
filterSweep: number;
|
||||
mix: number;
|
||||
}
|
||||
|
||||
export interface CombResonatorParams {
|
||||
baseFreq: number;
|
||||
sourceType: SourceType;
|
||||
sourceDecay: number;
|
||||
comb: CombParams;
|
||||
resonator: ResonatorParams;
|
||||
delay: DelayParams;
|
||||
stereoWidth: number;
|
||||
}
|
||||
|
||||
export class CombResonator extends CsoundEngine<CombResonatorParams> {
|
||||
getName(): string {
|
||||
return 'CombRes';
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return 'Comb filter and resonator for metallic and bell-like sounds';
|
||||
}
|
||||
|
||||
getType() {
|
||||
return 'generative' as const;
|
||||
}
|
||||
|
||||
getCategory() {
|
||||
return 'Subtractive' as const;
|
||||
}
|
||||
|
||||
protected getOrchestra(): string {
|
||||
return `
|
||||
instr 1
|
||||
ibasefreq chnget "basefreq"
|
||||
isourcetype chnget "sourcetype"
|
||||
isourcedecay chnget "sourcedecay"
|
||||
icombfeedback chnget "comb_feedback"
|
||||
icombbright chnget "comb_brightness"
|
||||
iresfreq chnget "res_frequency"
|
||||
iresres chnget "res_resonance"
|
||||
iresenvamt chnget "res_envamt"
|
||||
iresattack chnget "res_attack"
|
||||
iresdecay chnget "res_decay"
|
||||
iressustain chnget "res_sustain"
|
||||
iresrelease chnget "res_release"
|
||||
ideltime1 chnget "delay_time1"
|
||||
ideltime2 chnget "delay_time2"
|
||||
idelfeedback chnget "delay_feedback"
|
||||
ifilterfreq chnget "delay_filterfreq"
|
||||
ifiltersweep chnget "delay_filtersweep"
|
||||
idelmix chnget "delay_mix"
|
||||
istereo chnget "stereowidth"
|
||||
|
||||
idur = p3
|
||||
|
||||
; Convert ratios to time values
|
||||
iresatt = iresattack * idur
|
||||
iresdec = iresdecay * idur
|
||||
iresrel = iresrelease * idur
|
||||
isrcdec = isourcedecay * idur
|
||||
idt1 = ideltime1 * idur
|
||||
idt2 = ideltime2 * idur
|
||||
|
||||
; Stereo detuning
|
||||
idetune = 1 + (istereo * 0.003)
|
||||
ifreqL = ibasefreq / idetune
|
||||
ifreqR = ibasefreq * idetune
|
||||
|
||||
; Source envelope - sharp attack, exponential decay
|
||||
ksrcenv expon 1, isrcdec, 0.001
|
||||
|
||||
; Generate source signals with more amplitude
|
||||
if isourcetype == 0 then
|
||||
; Saw wave with harmonics
|
||||
asrcL vco2 ksrcenv * 0.8, ifreqL, 0
|
||||
asrcR vco2 ksrcenv * 0.8, ifreqR, 0
|
||||
elseif isourcetype == 1 then
|
||||
; Pulse wave
|
||||
asrcL vco2 ksrcenv * 0.8, ifreqL, 2, 0.3
|
||||
asrcR vco2 ksrcenv * 0.8, ifreqR, 2, 0.3
|
||||
elseif isourcetype == 2 then
|
||||
; Noise burst with pitch
|
||||
anoise1 noise ksrcenv * 0.6, 0
|
||||
anoise2 noise ksrcenv * 0.6, 0
|
||||
asrcL butterbp anoise1, ifreqL, ifreqL * 2
|
||||
asrcR butterbp anoise2, ifreqR, ifreqR * 2
|
||||
else
|
||||
; Triangle wave
|
||||
asrcL vco2 ksrcenv * 0.8, ifreqL, 12
|
||||
asrcR vco2 ksrcenv * 0.8, ifreqR, 12
|
||||
endif
|
||||
|
||||
; Direct path for immediate sound (no delay)
|
||||
adirectL = asrcL * 0.3
|
||||
adirectR = asrcR * 0.3
|
||||
|
||||
; Comb filter using vcomb for better feedback control
|
||||
idelaytimeL = 1 / ifreqL
|
||||
idelaytimeR = 1 / ifreqR
|
||||
ilooptime = 0.1
|
||||
|
||||
acombL vcomb asrcL, idelaytimeL, ilooptime, icombfeedback
|
||||
acombR vcomb asrcR, idelaytimeR, ilooptime, icombfeedback
|
||||
|
||||
; Damping filter
|
||||
adampl tone acombL, ibasefreq * icombbright
|
||||
adampR tone acombR, ibasefreq * icombbright
|
||||
|
||||
; Resonator envelope for filter sweep
|
||||
kresenv madsr iresatt, iresdec, iressustain, iresrel
|
||||
kresfreq = iresfreq + (kresenv * iresenvamt * ibasefreq * 4)
|
||||
kresfreq = limit(kresfreq, 80, 18000)
|
||||
|
||||
; Multiple resonators for richer tone
|
||||
kbw = kresfreq / iresres
|
||||
ares1L butterbp adampl, kresfreq, kbw
|
||||
ares1R butterbp adampR, kresfreq, kbw
|
||||
|
||||
ares2L butterbp adampl, kresfreq * 1.5, kbw * 1.2
|
||||
ares2R butterbp adampR, kresfreq * 1.5, kbw * 1.2
|
||||
|
||||
; Mix resonators
|
||||
aresL = ares1L + (ares2L * 0.5)
|
||||
aresR = ares1R + (ares2R * 0.5)
|
||||
|
||||
; Dry signal (this plays immediately)
|
||||
adryL = (adampl * 0.2) + (aresL * 0.8)
|
||||
adryR = (adampR * 0.2) + (aresR * 0.8)
|
||||
|
||||
; Direct signal envelope: loud at start, fades out quickly
|
||||
kdirectenv linseg 1, idur * 0.05, 0, idur * 0.95, 0
|
||||
|
||||
; Crossfade envelope: start with dry signal, fade in delays
|
||||
kdryenv linseg 0.6, idur * 0.15, 0.3, idur * 0.85, 0.2
|
||||
kwetenv linseg 0, idur * 0.1, 1, idur * 0.9, 1
|
||||
|
||||
; Extreme filter sweep envelopes with multiple segments
|
||||
kfilter1 linseg ifilterfreq, idur * 0.2, ifilterfreq + (ifiltersweep * 12000), idur * 0.3, ifilterfreq - (ifiltersweep * 5000), idur * 0.5, ifilterfreq + (ifiltersweep * 15000)
|
||||
kfilter1 = limit(kfilter1, 250, 19000)
|
||||
|
||||
kfilter2 expseg ifilterfreq * 2, idur * 0.15, ifilterfreq * 0.3 + 200, idur * 0.35, ifilterfreq * 3 + 100, idur * 0.5, ifilterfreq + (ifiltersweep * 10000) + 100
|
||||
kfilter2 = limit(kfilter2, 200, 18000)
|
||||
|
||||
kfilter3 linseg ifilterfreq * 0.5, idur * 0.25, ifilterfreq + (ifiltersweep * 8000), idur * 0.25, ifilterfreq * 2, idur * 0.5, ifilterfreq - (ifiltersweep * 3000)
|
||||
kfilter3 = limit(kfilter3, 300, 16000)
|
||||
|
||||
kfilter4 expon ifilterfreq + 100, idur, ifilterfreq + (ifiltersweep * 14000) + 100
|
||||
kfilter4 = limit(kfilter4, 350, 17000)
|
||||
|
||||
; LFO for delay time modulation
|
||||
klfo1 lfo 0.03, 3 + (ifiltersweep * 2)
|
||||
klfo2 lfo 0.04, 5 - (ifiltersweep * 1.5)
|
||||
|
||||
; Multi-tap delay line 1 (Left -> Right) with modulation
|
||||
abuf1 delayr idt1 * 1.1
|
||||
kdt1a = (idt1 * 0.2) + klfo1
|
||||
kdt1b = (idt1 * 0.45) - klfo2
|
||||
kdt1c = (idt1 * 0.75) + (klfo1 * 0.5)
|
||||
kdt1d = idt1
|
||||
adel1a deltap3 kdt1a
|
||||
adel1b deltap3 kdt1b
|
||||
adel1c deltap3 kdt1c
|
||||
adel1d deltap3 kdt1d
|
||||
delayw adryL + (adel1d * idelfeedback * 0.95)
|
||||
|
||||
afilt1a butterbp adel1a, kfilter1, kfilter1 * 0.15
|
||||
afilt1b butterbp adel1b, kfilter2, kfilter2 * 0.2
|
||||
afilt1c butterbp adel1c, kfilter3, kfilter3 * 0.25
|
||||
afilt1d butterbp adel1d, kfilter4, kfilter4 * 0.18
|
||||
|
||||
adelR = (afilt1a * 0.6) + (afilt1b * 0.8) + (afilt1c * 0.9) + (afilt1d * 0.7)
|
||||
|
||||
; Multi-tap delay line 2 (Right -> Left) with modulation
|
||||
abuf2 delayr idt2 * 1.1
|
||||
kdt2a = (idt2 * 0.18) - klfo2
|
||||
kdt2b = (idt2 * 0.42) + klfo1
|
||||
kdt2c = (idt2 * 0.7) - (klfo2 * 0.5)
|
||||
kdt2d = idt2
|
||||
adel2a deltap3 kdt2a
|
||||
adel2b deltap3 kdt2b
|
||||
adel2c deltap3 kdt2c
|
||||
adel2d deltap3 kdt2d
|
||||
delayw adryR + (adel2d * idelfeedback * 0.95)
|
||||
|
||||
afilt2a butterbp adel2a, kfilter1 * 1.4, kfilter1 * 0.12
|
||||
afilt2b butterbp adel2b, kfilter2 * 0.7, kfilter2 * 0.22
|
||||
afilt2c butterbp adel2c, kfilter3 * 1.2, kfilter3 * 0.16
|
||||
afilt2d butterbp adel2d, kfilter4 * 0.9, kfilter4 * 0.2
|
||||
|
||||
adelL = (afilt2a * 0.7) + (afilt2b * 0.6) + (afilt2c * 0.85) + (afilt2d * 0.8)
|
||||
|
||||
; Additional chaotic resonant delays
|
||||
abuf3 delayr idt1 * 1.6
|
||||
kdt3 = (idt1 * 1.4) + (klfo1 * 2)
|
||||
adel3 deltap3 kdt3
|
||||
delayw (adryL + adryR) * 0.5 + (adel3 * idelfeedback * 0.85)
|
||||
afilt3 butterbp adel3, kfilter2 * 0.6, kfilter2 * 0.1
|
||||
|
||||
abuf4 delayr idt2 * 1.8
|
||||
kdt4 = (idt2 * 1.6) - (klfo2 * 2)
|
||||
adel4 deltap3 kdt4
|
||||
delayw (adryR + adryL) * 0.5 + (adel4 * idelfeedback * 0.8)
|
||||
afilt4 butterbp adel4, kfilter3 * 1.3, kfilter3 * 0.12
|
||||
|
||||
abuf5 delayr idt1 * 2.2
|
||||
kdt5 = (idt1 * 2.0) + klfo1 + klfo2
|
||||
adel5 deltap3 kdt5
|
||||
delayw (adelL + adelR) * 0.4 + (adel5 * idelfeedback * 0.75)
|
||||
afilt5 butterbp adel5, kfilter4 * 0.8, kfilter4 * 0.08
|
||||
|
||||
; Mix: direct signal (immediate), dry (soon after), delays (build up)
|
||||
amixL = (adirectL * kdirectenv) + (adryL * kdryenv) + ((adelL * 1.2 + afilt3 * 0.8 + afilt4 * 0.7 + afilt5 * 0.6) * idelmix * kwetenv)
|
||||
amixR = (adirectR * kdirectenv) + (adryR * kdryenv) + ((adelR * 1.2 + afilt4 * 0.8 + afilt3 * 0.7 + afilt5 * 0.6) * idelmix * kwetenv)
|
||||
|
||||
outs amixL, amixR
|
||||
endin
|
||||
`;
|
||||
}
|
||||
|
||||
protected getParametersForCsound(params: CombResonatorParams): CsoundParameter[] {
|
||||
return [
|
||||
{ channelName: 'basefreq', value: params.baseFreq },
|
||||
{ channelName: 'sourcetype', value: params.sourceType },
|
||||
{ channelName: 'sourcedecay', value: params.sourceDecay },
|
||||
{ channelName: 'comb_feedback', value: params.comb.feedback },
|
||||
{ channelName: 'comb_brightness', value: params.comb.brightness },
|
||||
{ channelName: 'res_frequency', value: params.resonator.frequency },
|
||||
{ channelName: 'res_resonance', value: params.resonator.resonance },
|
||||
{ channelName: 'res_envamt', value: params.resonator.envAmount },
|
||||
{ channelName: 'res_attack', value: params.resonator.attack },
|
||||
{ channelName: 'res_decay', value: params.resonator.decay },
|
||||
{ channelName: 'res_sustain', value: params.resonator.sustain },
|
||||
{ channelName: 'res_release', value: params.resonator.release },
|
||||
{ channelName: 'delay_time1', value: params.delay.time1 },
|
||||
{ channelName: 'delay_time2', value: params.delay.time2 },
|
||||
{ channelName: 'delay_feedback', value: params.delay.feedback },
|
||||
{ channelName: 'delay_filterfreq', value: params.delay.filterFreq },
|
||||
{ channelName: 'delay_filtersweep', value: params.delay.filterSweep },
|
||||
{ channelName: 'delay_mix', value: params.delay.mix },
|
||||
{ channelName: 'stereowidth', value: params.stereoWidth },
|
||||
];
|
||||
}
|
||||
|
||||
randomParams(pitchLock?: PitchLock): CombResonatorParams {
|
||||
let baseFreq: number;
|
||||
if (pitchLock?.enabled) {
|
||||
baseFreq = pitchLock.frequency;
|
||||
} else {
|
||||
const baseFreqChoices = [82.4, 110, 146.8, 220, 293.7, 440, 587.3, 880];
|
||||
baseFreq = this.randomChoice(baseFreqChoices) * this.randomRange(0.97, 1.03);
|
||||
}
|
||||
|
||||
return {
|
||||
baseFreq,
|
||||
sourceType: this.randomInt(0, 3) as SourceType,
|
||||
sourceDecay: this.randomRange(0.02, 0.15),
|
||||
comb: {
|
||||
feedback: this.randomRange(0.7, 0.95),
|
||||
brightness: this.randomRange(3, 20),
|
||||
},
|
||||
resonator: {
|
||||
frequency: baseFreq * this.randomChoice([1, 1.3, 1.6, 2, 2.4, 3, 3.5, 4, 5]),
|
||||
resonance: this.randomRange(8, 80),
|
||||
envAmount: this.randomRange(0.3, 2.0),
|
||||
attack: this.randomRange(0.001, 0.05),
|
||||
decay: this.randomRange(0.15, 0.6),
|
||||
sustain: this.randomRange(0.05, 0.4),
|
||||
release: this.randomRange(0.3, 0.8),
|
||||
},
|
||||
delay: {
|
||||
time1: this.randomRange(0.08, 0.35),
|
||||
time2: this.randomRange(0.1, 0.4),
|
||||
feedback: this.randomRange(0.82, 0.98),
|
||||
filterFreq: this.randomRange(400, 5000),
|
||||
filterSweep: this.randomRange(-1.2, 2.0),
|
||||
mix: this.randomRange(0.6, 1.0),
|
||||
},
|
||||
stereoWidth: this.randomRange(0.2, 0.8),
|
||||
};
|
||||
}
|
||||
|
||||
mutateParams(
|
||||
params: CombResonatorParams,
|
||||
mutationAmount: number = 0.15,
|
||||
pitchLock?: PitchLock
|
||||
): CombResonatorParams {
|
||||
const baseFreq = pitchLock?.enabled ? pitchLock.frequency : params.baseFreq;
|
||||
|
||||
return {
|
||||
baseFreq,
|
||||
sourceType: Math.random() < 0.15 ? (this.randomInt(0, 3) as SourceType) : params.sourceType,
|
||||
sourceDecay: this.mutateValue(params.sourceDecay, mutationAmount, 0.02, 0.5),
|
||||
comb: {
|
||||
feedback: this.mutateValue(params.comb.feedback, mutationAmount, 0.3, 0.95),
|
||||
brightness: this.mutateValue(params.comb.brightness, mutationAmount, 1, 20),
|
||||
},
|
||||
resonator: {
|
||||
frequency: Math.random() < 0.2
|
||||
? baseFreq * this.randomChoice([1, 1.5, 2, 2.5, 3, 3.5, 4])
|
||||
: this.mutateValue(params.resonator.frequency, mutationAmount, baseFreq * 0.5, baseFreq * 6),
|
||||
resonance: this.mutateValue(params.resonator.resonance, mutationAmount, 3, 80),
|
||||
envAmount: this.mutateValue(params.resonator.envAmount, mutationAmount, 0, 2),
|
||||
attack: this.mutateValue(params.resonator.attack, mutationAmount, 0.001, 0.2),
|
||||
decay: this.mutateValue(params.resonator.decay, mutationAmount, 0.05, 0.6),
|
||||
sustain: this.mutateValue(params.resonator.sustain, mutationAmount, 0, 0.7),
|
||||
release: this.mutateValue(params.resonator.release, mutationAmount, 0.1, 0.9),
|
||||
},
|
||||
delay: {
|
||||
time1: this.mutateValue(params.delay.time1, mutationAmount, 0.05, 0.5),
|
||||
time2: this.mutateValue(params.delay.time2, mutationAmount, 0.08, 0.55),
|
||||
feedback: this.mutateValue(params.delay.feedback, mutationAmount, 0.75, 0.99),
|
||||
filterFreq: this.mutateValue(params.delay.filterFreq, mutationAmount, 350, 7000),
|
||||
filterSweep: this.mutateValue(params.delay.filterSweep, mutationAmount, -1.5, 2.5),
|
||||
mix: this.mutateValue(params.delay.mix, mutationAmount, 0.5, 1.0),
|
||||
},
|
||||
stereoWidth: this.mutateValue(params.stereoWidth, mutationAmount, 0, 1),
|
||||
};
|
||||
}
|
||||
}
|
||||
334
src/lib/audio/engines/Dripwater.ts
Normal file
334
src/lib/audio/engines/Dripwater.ts
Normal file
@ -0,0 +1,334 @@
|
||||
import { CsoundEngine, type CsoundParameter } from './base/CsoundEngine';
|
||||
import type { PitchLock } from './base/SynthEngine';
|
||||
|
||||
interface DripwaterParams {
|
||||
baseFreq: number;
|
||||
dropRate: number;
|
||||
numElements: number;
|
||||
damping: number;
|
||||
shakeIntensity: number;
|
||||
freqRatio1: number;
|
||||
freqRatio2: number;
|
||||
freqRatio3: number;
|
||||
attack: number;
|
||||
release: number;
|
||||
amplitude: number;
|
||||
delayTime1: number;
|
||||
delayTime2: number;
|
||||
delayTime3: number;
|
||||
delayTime4: number;
|
||||
delayGain1: number;
|
||||
delayGain2: number;
|
||||
delayGain3: number;
|
||||
delayGain4: number;
|
||||
feedbackDelayTime: number;
|
||||
feedbackAmount: number;
|
||||
modDepth: number;
|
||||
modRate: number;
|
||||
stereoSpread: number;
|
||||
combDelayTime: number;
|
||||
combFeedback: number;
|
||||
reverseDelayTime: number;
|
||||
reverseGain: number;
|
||||
cascadeDelay1: number;
|
||||
cascadeDelay2: number;
|
||||
cascadeFeedback: number;
|
||||
}
|
||||
|
||||
export class Dripwater extends CsoundEngine<DripwaterParams> {
|
||||
getName(): string {
|
||||
return 'Weird Waters';
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return 'Physical model of water droplets through cascading delays, comb filters, and reverse echoes';
|
||||
}
|
||||
|
||||
getType() {
|
||||
return 'generative' as const;
|
||||
}
|
||||
|
||||
getCategory() {
|
||||
return 'Noise' as const;
|
||||
}
|
||||
|
||||
protected getOrchestra(): string {
|
||||
return `
|
||||
instr 1
|
||||
iBaseFreq chnget "baseFreq"
|
||||
iDropRate chnget "dropRate"
|
||||
iNumElements chnget "numElements"
|
||||
iDamping chnget "damping"
|
||||
iShakeIntensity chnget "shakeIntensity"
|
||||
iFreqRatio1 chnget "freqRatio1"
|
||||
iFreqRatio2 chnget "freqRatio2"
|
||||
iFreqRatio3 chnget "freqRatio3"
|
||||
iAttack chnget "attack"
|
||||
iRelease chnget "release"
|
||||
iAmplitude chnget "amplitude"
|
||||
iDelayTime1 chnget "delayTime1"
|
||||
iDelayTime2 chnget "delayTime2"
|
||||
iDelayTime3 chnget "delayTime3"
|
||||
iDelayTime4 chnget "delayTime4"
|
||||
iDelayGain1 chnget "delayGain1"
|
||||
iDelayGain2 chnget "delayGain2"
|
||||
iDelayGain3 chnget "delayGain3"
|
||||
iDelayGain4 chnget "delayGain4"
|
||||
iFeedbackDelayTime chnget "feedbackDelayTime"
|
||||
iFeedbackAmount chnget "feedbackAmount"
|
||||
iModDepth chnget "modDepth"
|
||||
iModRate chnget "modRate"
|
||||
iStereoSpread chnget "stereoSpread"
|
||||
iCombDelayTime chnget "combDelayTime"
|
||||
iCombFeedback chnget "combFeedback"
|
||||
iReverseDelayTime chnget "reverseDelayTime"
|
||||
iReverseGain chnget "reverseGain"
|
||||
iCascadeDelay1 chnget "cascadeDelay1"
|
||||
iCascadeDelay2 chnget "cascadeDelay2"
|
||||
iCascadeFeedback chnget "cascadeFeedback"
|
||||
|
||||
idur = p3
|
||||
iAttackTime = iAttack * idur
|
||||
iReleaseTime = iRelease * idur
|
||||
|
||||
; Scale delay times by duration so they fill the entire sound
|
||||
iScaledDelayTime1 = iDelayTime1 * idur
|
||||
iScaledDelayTime2 = iDelayTime2 * idur
|
||||
iScaledDelayTime3 = iDelayTime3 * idur
|
||||
iScaledDelayTime4 = iDelayTime4 * idur
|
||||
iScaledFeedbackDelay = iFeedbackDelayTime * idur
|
||||
iModDepthScaled = iModDepth * idur * 0.1
|
||||
iScaledCombDelay = iCombDelayTime * idur
|
||||
iScaledReverseDelay = iReverseDelayTime * idur
|
||||
iScaledCascade1 = iCascadeDelay1 * idur
|
||||
iScaledCascade2 = iCascadeDelay2 * idur
|
||||
|
||||
; Calculate resonant frequencies from base frequency and ratios
|
||||
iFreq1 = iBaseFreq * iFreqRatio1
|
||||
iFreq2 = iBaseFreq * iFreqRatio2
|
||||
iFreq3 = iBaseFreq * iFreqRatio3
|
||||
|
||||
; Generate dripwater sound
|
||||
; Signature: ares dripwater kamp, idettack, inum, idamp, imaxshake, ifreq, ifreq1, ifreq2
|
||||
; Reduce amplitude to prevent clipping with all the delays
|
||||
kAmp = iAmplitude * 0.08
|
||||
aDrip dripwater kAmp, iDropRate, iNumElements, iDamping, iShakeIntensity, iFreq1, iFreq2, iFreq3
|
||||
|
||||
; Apply AR envelope
|
||||
kEnv linsegr 0, iAttackTime, 1, iReleaseTime, 0
|
||||
|
||||
aDry = aDrip * kEnv
|
||||
|
||||
; Apply multitap delay with duration-scaled times
|
||||
; Signature: a1 [, a2, a3, ...] multitap asource [, itime1, igain1, itime2, igain2, ...]
|
||||
aMultitap multitap aDry, iScaledDelayTime1, iDelayGain1, iScaledDelayTime2, iDelayGain2, iScaledDelayTime3, iDelayGain3, iScaledDelayTime4, iDelayGain4
|
||||
|
||||
; Add modulated feedback delay
|
||||
; LFO modulates delay time
|
||||
kLFO oscili iModDepthScaled, iModRate
|
||||
kDelayTime = iScaledFeedbackDelay + kLFO
|
||||
kDelayTime = max(kDelayTime, 0.001)
|
||||
|
||||
; Variable delay with feedback
|
||||
aBuf delayr idur
|
||||
aTapL deltapi kDelayTime
|
||||
delayw aDry + (aTapL * iFeedbackAmount)
|
||||
|
||||
; Comb filter delay (creates resonance)
|
||||
aComb comb aDry, iScaledCombDelay, iCombFeedback
|
||||
|
||||
; Reverse delay using delayr/delayw with interpolation
|
||||
aBufRev delayr iScaledReverseDelay
|
||||
aReverseTap deltapi iScaledReverseDelay * (1 - (line:a(0, idur, 1)))
|
||||
delayw aDry
|
||||
aReverse = aReverseTap * iReverseGain
|
||||
|
||||
; Cascaded delays (two delays feeding into each other)
|
||||
aBuf1 delayr idur
|
||||
aCascade1 deltapi iScaledCascade1
|
||||
aBuf2 delayr idur
|
||||
aCascade2 deltapi iScaledCascade2
|
||||
delayw aCascade1 * iCascadeFeedback
|
||||
delayw aDry + (aCascade2 * iCascadeFeedback)
|
||||
|
||||
aOut = aDry + aMultitap + aTapL + aComb + aReverse + aCascade1 + aCascade2
|
||||
|
||||
; Create stereo width with slightly different parameters for right channel
|
||||
; Slightly detune the resonant frequencies for right channel
|
||||
iFreq1_R = iFreq1 * 1.003
|
||||
iFreq2_R = iFreq2 * 0.998
|
||||
iFreq3_R = iFreq3 * 1.002
|
||||
|
||||
; Use slightly different initial values for variation
|
||||
iDropRate_R = iDropRate * 1.02
|
||||
iShakeIntensity_R = iShakeIntensity * 0.98
|
||||
|
||||
kAmp_R = iAmplitude * 0.08
|
||||
aDrip_R dripwater kAmp_R, iDropRate_R, iNumElements, iDamping, iShakeIntensity_R, iFreq1_R, iFreq2_R, iFreq3_R
|
||||
|
||||
aDry_R = aDrip_R * kEnv
|
||||
|
||||
; Apply multitap delay to right channel with slightly different times for stereo width
|
||||
iScaledDelayTime1_R = iScaledDelayTime1 * 1.03
|
||||
iScaledDelayTime2_R = iScaledDelayTime2 * 0.97
|
||||
iScaledDelayTime3_R = iScaledDelayTime3 * 1.05
|
||||
iScaledDelayTime4_R = iScaledDelayTime4 * 0.95
|
||||
|
||||
aMultitap_R multitap aDry_R, iScaledDelayTime1_R, iDelayGain1, iScaledDelayTime2_R, iDelayGain2, iScaledDelayTime3_R, iDelayGain3, iScaledDelayTime4_R, iDelayGain4
|
||||
|
||||
; Right channel modulated delay with stereo spread
|
||||
kLFO_R oscili iModDepthScaled, iModRate * 1.07
|
||||
kDelayTime_R = (iScaledFeedbackDelay * iStereoSpread) + kLFO_R
|
||||
kDelayTime_R = max(kDelayTime_R, 0.001)
|
||||
|
||||
aBuf_R delayr idur
|
||||
aTapR deltapi kDelayTime_R
|
||||
delayw aDry_R + (aTapR * iFeedbackAmount)
|
||||
|
||||
; Comb filter for right channel
|
||||
iScaledCombDelay_R = iScaledCombDelay * 1.07
|
||||
aComb_R comb aDry_R, iScaledCombDelay_R, iCombFeedback
|
||||
|
||||
; Reverse delay for right channel
|
||||
iScaledReverseDelay_R = iScaledReverseDelay * 0.93
|
||||
aBufRev_R delayr iScaledReverseDelay_R
|
||||
aReverseTap_R deltapi iScaledReverseDelay_R * (1 - (line:a(0, idur, 1)))
|
||||
delayw aDry_R
|
||||
aReverse_R = aReverseTap_R * iReverseGain
|
||||
|
||||
; Cascaded delays for right channel
|
||||
iScaledCascade1_R = iScaledCascade1 * 1.05
|
||||
iScaledCascade2_R = iScaledCascade2 * 0.95
|
||||
aBuf1_R delayr idur
|
||||
aCascade1_R deltapi iScaledCascade1_R
|
||||
aBuf2_R delayr idur
|
||||
aCascade2_R deltapi iScaledCascade2_R
|
||||
delayw aCascade1_R * iCascadeFeedback
|
||||
delayw aDry_R + (aCascade2_R * iCascadeFeedback)
|
||||
|
||||
aOut_R = aDry_R + aMultitap_R + aTapR + aComb_R + aReverse_R + aCascade1_R + aCascade2_R
|
||||
|
||||
outs aOut, aOut_R
|
||||
endin
|
||||
`;
|
||||
}
|
||||
|
||||
protected getParametersForCsound(params: DripwaterParams): CsoundParameter[] {
|
||||
return [
|
||||
{ channelName: 'baseFreq', value: params.baseFreq },
|
||||
{ channelName: 'dropRate', value: params.dropRate },
|
||||
{ channelName: 'numElements', value: params.numElements },
|
||||
{ channelName: 'damping', value: params.damping },
|
||||
{ channelName: 'shakeIntensity', value: params.shakeIntensity },
|
||||
{ channelName: 'freqRatio1', value: params.freqRatio1 },
|
||||
{ channelName: 'freqRatio2', value: params.freqRatio2 },
|
||||
{ channelName: 'freqRatio3', value: params.freqRatio3 },
|
||||
{ channelName: 'attack', value: params.attack },
|
||||
{ channelName: 'release', value: params.release },
|
||||
{ channelName: 'amplitude', value: params.amplitude },
|
||||
{ channelName: 'delayTime1', value: params.delayTime1 },
|
||||
{ channelName: 'delayTime2', value: params.delayTime2 },
|
||||
{ channelName: 'delayTime3', value: params.delayTime3 },
|
||||
{ channelName: 'delayTime4', value: params.delayTime4 },
|
||||
{ channelName: 'delayGain1', value: params.delayGain1 },
|
||||
{ channelName: 'delayGain2', value: params.delayGain2 },
|
||||
{ channelName: 'delayGain3', value: params.delayGain3 },
|
||||
{ channelName: 'delayGain4', value: params.delayGain4 },
|
||||
{ channelName: 'feedbackDelayTime', value: params.feedbackDelayTime },
|
||||
{ channelName: 'feedbackAmount', value: params.feedbackAmount },
|
||||
{ channelName: 'modDepth', value: params.modDepth },
|
||||
{ channelName: 'modRate', value: params.modRate },
|
||||
{ channelName: 'stereoSpread', value: params.stereoSpread },
|
||||
{ channelName: 'combDelayTime', value: params.combDelayTime },
|
||||
{ channelName: 'combFeedback', value: params.combFeedback },
|
||||
{ channelName: 'reverseDelayTime', value: params.reverseDelayTime },
|
||||
{ channelName: 'reverseGain', value: params.reverseGain },
|
||||
{ channelName: 'cascadeDelay1', value: params.cascadeDelay1 },
|
||||
{ channelName: 'cascadeDelay2', value: params.cascadeDelay2 },
|
||||
{ channelName: 'cascadeFeedback', value: params.cascadeFeedback },
|
||||
];
|
||||
}
|
||||
|
||||
randomParams(pitchLock?: PitchLock): DripwaterParams {
|
||||
const baseFreqChoices = [55, 82.4, 110, 146.8, 220, 293.7, 440, 587.3, 880];
|
||||
const baseFreq = pitchLock?.enabled
|
||||
? pitchLock.frequency
|
||||
: this.randomChoice(baseFreqChoices) * this.randomRange(0.95, 1.05);
|
||||
|
||||
return {
|
||||
baseFreq,
|
||||
dropRate: this.randomRange(2, 40),
|
||||
numElements: this.randomInt(4, 48),
|
||||
damping: this.randomRange(0.1, 0.85),
|
||||
shakeIntensity: this.randomRange(0.2, 0.9),
|
||||
freqRatio1: this.randomRange(1.0, 3.5),
|
||||
freqRatio2: this.randomRange(2.5, 7.0),
|
||||
freqRatio3: this.randomRange(5.0, 14.0),
|
||||
attack: this.randomRange(0.001, 0.04),
|
||||
release: this.randomRange(0.15, 0.9),
|
||||
amplitude: this.randomRange(0.6, 0.95),
|
||||
delayTime1: this.randomRange(0.1, 0.3),
|
||||
delayTime2: this.randomRange(0.25, 0.5),
|
||||
delayTime3: this.randomRange(0.45, 0.7),
|
||||
delayTime4: this.randomRange(0.65, 0.95),
|
||||
delayGain1: this.randomRange(0.4, 0.7),
|
||||
delayGain2: this.randomRange(0.3, 0.55),
|
||||
delayGain3: this.randomRange(0.2, 0.4),
|
||||
delayGain4: this.randomRange(0.1, 0.25),
|
||||
feedbackDelayTime: this.randomRange(0.15, 0.5),
|
||||
feedbackAmount: this.randomRange(0.3, 0.7),
|
||||
modDepth: this.randomRange(0.02, 0.12),
|
||||
modRate: this.randomRange(0.1, 3.0),
|
||||
stereoSpread: this.randomRange(0.85, 1.15),
|
||||
combDelayTime: this.randomRange(0.05, 0.25),
|
||||
combFeedback: this.randomRange(0.4, 0.8),
|
||||
reverseDelayTime: this.randomRange(0.3, 0.8),
|
||||
reverseGain: this.randomRange(0.15, 0.4),
|
||||
cascadeDelay1: this.randomRange(0.2, 0.45),
|
||||
cascadeDelay2: this.randomRange(0.35, 0.65),
|
||||
cascadeFeedback: this.randomRange(0.3, 0.65),
|
||||
};
|
||||
}
|
||||
|
||||
mutateParams(
|
||||
params: DripwaterParams,
|
||||
mutationAmount: number = 0.15,
|
||||
pitchLock?: PitchLock
|
||||
): DripwaterParams {
|
||||
const baseFreq = pitchLock?.enabled ? pitchLock.frequency : params.baseFreq;
|
||||
|
||||
return {
|
||||
baseFreq,
|
||||
dropRate: this.mutateValue(params.dropRate, mutationAmount, 1, 50),
|
||||
numElements: Math.round(this.mutateValue(params.numElements, mutationAmount, 2, 64)),
|
||||
damping: this.mutateValue(params.damping, mutationAmount, 0.0, 0.95),
|
||||
shakeIntensity: this.mutateValue(params.shakeIntensity, mutationAmount, 0.1, 1.0),
|
||||
freqRatio1: this.mutateValue(params.freqRatio1, mutationAmount, 1.0, 4.0),
|
||||
freqRatio2: this.mutateValue(params.freqRatio2, mutationAmount, 2.0, 8.0),
|
||||
freqRatio3: this.mutateValue(params.freqRatio3, mutationAmount, 4.0, 16.0),
|
||||
attack: this.mutateValue(params.attack, mutationAmount, 0.001, 0.05),
|
||||
release: this.mutateValue(params.release, mutationAmount, 0.1, 0.95),
|
||||
amplitude: this.mutateValue(params.amplitude, mutationAmount, 0.5, 1.0),
|
||||
delayTime1: this.mutateValue(params.delayTime1, mutationAmount, 0.05, 0.35),
|
||||
delayTime2: this.mutateValue(params.delayTime2, mutationAmount, 0.2, 0.6),
|
||||
delayTime3: this.mutateValue(params.delayTime3, mutationAmount, 0.4, 0.8),
|
||||
delayTime4: this.mutateValue(params.delayTime4, mutationAmount, 0.6, 0.98),
|
||||
delayGain1: this.mutateValue(params.delayGain1, mutationAmount, 0.3, 0.8),
|
||||
delayGain2: this.mutateValue(params.delayGain2, mutationAmount, 0.2, 0.65),
|
||||
delayGain3: this.mutateValue(params.delayGain3, mutationAmount, 0.1, 0.5),
|
||||
delayGain4: this.mutateValue(params.delayGain4, mutationAmount, 0.05, 0.35),
|
||||
feedbackDelayTime: this.mutateValue(params.feedbackDelayTime, mutationAmount, 0.1, 0.6),
|
||||
feedbackAmount: this.mutateValue(params.feedbackAmount, mutationAmount, 0.2, 0.8),
|
||||
modDepth: this.mutateValue(params.modDepth, mutationAmount, 0.01, 0.15),
|
||||
modRate: this.mutateValue(params.modRate, mutationAmount, 0.05, 4.0),
|
||||
stereoSpread: this.mutateValue(params.stereoSpread, mutationAmount, 0.7, 1.3),
|
||||
combDelayTime: this.mutateValue(params.combDelayTime, mutationAmount, 0.03, 0.3),
|
||||
combFeedback: this.mutateValue(params.combFeedback, mutationAmount, 0.3, 0.85),
|
||||
reverseDelayTime: this.mutateValue(params.reverseDelayTime, mutationAmount, 0.2, 0.9),
|
||||
reverseGain: this.mutateValue(params.reverseGain, mutationAmount, 0.1, 0.5),
|
||||
cascadeDelay1: this.mutateValue(params.cascadeDelay1, mutationAmount, 0.15, 0.5),
|
||||
cascadeDelay2: this.mutateValue(params.cascadeDelay2, mutationAmount, 0.3, 0.7),
|
||||
cascadeFeedback: this.mutateValue(params.cascadeFeedback, mutationAmount, 0.2, 0.75),
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -67,6 +67,10 @@ export class DubSiren implements SynthEngine<DubSirenParams> {
|
||||
return 'generative' as const;
|
||||
}
|
||||
|
||||
getCategory() {
|
||||
return 'Experimental' as const;
|
||||
}
|
||||
|
||||
generate(params: DubSirenParams, sampleRate: number, duration: number): [Float32Array, Float32Array] {
|
||||
const numSamples = Math.floor(sampleRate * duration);
|
||||
const leftBuffer = new Float32Array(numSamples);
|
||||
|
||||
@ -46,6 +46,10 @@ export class DustNoise implements SynthEngine {
|
||||
return 'generative' as const;
|
||||
}
|
||||
|
||||
getCategory() {
|
||||
return 'Noise' as const;
|
||||
}
|
||||
|
||||
randomParams(pitchLock?: PitchLock): DustNoiseParams {
|
||||
const characterBias = Math.random();
|
||||
|
||||
|
||||
184
src/lib/audio/engines/FMTomTom.ts
Normal file
184
src/lib/audio/engines/FMTomTom.ts
Normal file
@ -0,0 +1,184 @@
|
||||
import { CsoundEngine, type CsoundParameter } from './base/CsoundEngine';
|
||||
import type { PitchLock } from './base/SynthEngine';
|
||||
|
||||
interface FMTomTomParams {
|
||||
baseFreq: number;
|
||||
pitchBendAmount: number;
|
||||
pitchBendDecay: number;
|
||||
modIndex: number;
|
||||
modRatio: number;
|
||||
noiseHPFreq: number;
|
||||
noiseResonance: number;
|
||||
noiseMix: number;
|
||||
ampAttack: number;
|
||||
ampDecay: number;
|
||||
sustain: number;
|
||||
release: number;
|
||||
tonality: number;
|
||||
stereoDetune: number;
|
||||
}
|
||||
|
||||
export class FMTomTom extends CsoundEngine<FMTomTomParams> {
|
||||
getName(): string {
|
||||
return 'FM Tom-Tom';
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return 'High-pass filtered noise modulating a sine oscillator with pitch bend envelope simulating tom-tom membrane';
|
||||
}
|
||||
|
||||
getType() {
|
||||
return 'generative' as const;
|
||||
}
|
||||
|
||||
getCategory() {
|
||||
return 'Percussion' as const;
|
||||
}
|
||||
|
||||
protected getOrchestra(): string {
|
||||
return `
|
||||
instr 1
|
||||
iBaseFreq chnget "baseFreq"
|
||||
iPitchBendAmount chnget "pitchBendAmount"
|
||||
iPitchBendDecay chnget "pitchBendDecay"
|
||||
iModIndex chnget "modIndex"
|
||||
iModRatio chnget "modRatio"
|
||||
iNoiseHPFreq chnget "noiseHPFreq"
|
||||
iNoiseResonance chnget "noiseResonance"
|
||||
iNoiseMix chnget "noiseMix"
|
||||
iAmpAttack chnget "ampAttack"
|
||||
iAmpDecay chnget "ampDecay"
|
||||
iSustain chnget "sustain"
|
||||
iRelease chnget "release"
|
||||
iTonality chnget "tonality"
|
||||
iStereoDetune chnget "stereoDetune"
|
||||
|
||||
idur = p3
|
||||
iPitchBendTime = iPitchBendDecay * idur
|
||||
iAmpAttackTime = iAmpAttack * idur
|
||||
iAmpDecayTime = iAmpDecay * idur
|
||||
iReleaseTime = iRelease * idur
|
||||
|
||||
; Pitch bend envelope (simulates drum membrane tightening)
|
||||
; Starts at higher pitch and decays to base pitch
|
||||
iPitchStart = iBaseFreq * (1 + iPitchBendAmount)
|
||||
kPitchEnv expseg iPitchStart, iPitchBendTime, iBaseFreq, idur - iPitchBendTime, iBaseFreq
|
||||
|
||||
; Generate high-pass filtered noise for modulation
|
||||
aNoise noise 1, 0
|
||||
aNoiseHP butterhp aNoise, iNoiseHPFreq
|
||||
aNoiseFiltered butterbp aNoiseHP, iNoiseHPFreq * 2, iNoiseResonance
|
||||
|
||||
; Scale noise for FM modulation
|
||||
aNoiseScaled = aNoiseFiltered * iModIndex * kPitchEnv * iTonality
|
||||
|
||||
; FM synthesis: noise modulates sine oscillator
|
||||
aModulator oscili iModIndex * kPitchEnv, kPitchEnv * iModRatio
|
||||
aCarrier oscili 0.5, kPitchEnv + aModulator + aNoiseScaled
|
||||
|
||||
; Add direct noise component for more realistic tom sound
|
||||
aNoiseDirect = aNoiseFiltered * iNoiseMix * 0.3
|
||||
|
||||
; Mix carrier and noise
|
||||
aMix = aCarrier * (1 - iNoiseMix * 0.5) + aNoiseDirect
|
||||
|
||||
; Amplitude envelope (ADSR-like with fast attack and decay)
|
||||
kAmpEnv expseg 0.001, iAmpAttackTime, 1, iAmpDecayTime, iSustain, idur - iAmpAttackTime - iAmpDecayTime - iReleaseTime, iSustain, iReleaseTime, 0.001
|
||||
|
||||
; Apply amplitude envelope
|
||||
aOut = aMix * kAmpEnv
|
||||
|
||||
; Right channel with stereo detune
|
||||
iBaseFreqR = iBaseFreq * (1 + iStereoDetune * 0.02)
|
||||
iPitchStartR = iBaseFreqR * (1 + iPitchBendAmount)
|
||||
kPitchEnvR expseg iPitchStartR, iPitchBendTime, iBaseFreqR, idur - iPitchBendTime, iBaseFreqR
|
||||
|
||||
aNoiseR noise 1, 0
|
||||
aNoiseHPR butterhp aNoiseR, iNoiseHPFreq * (1 + iStereoDetune * 0.01)
|
||||
aNoiseFilteredR butterbp aNoiseHPR, iNoiseHPFreq * 2 * (1 + iStereoDetune * 0.01), iNoiseResonance
|
||||
|
||||
aNoiseScaledR = aNoiseFilteredR * iModIndex * kPitchEnvR * iTonality
|
||||
aModulatorR oscili iModIndex * kPitchEnvR, kPitchEnvR * iModRatio
|
||||
aCarrierR oscili 0.5, kPitchEnvR + aModulatorR + aNoiseScaledR
|
||||
|
||||
aNoiseDirectR = aNoiseFilteredR * iNoiseMix * 0.3
|
||||
aMixR = aCarrierR * (1 - iNoiseMix * 0.5) + aNoiseDirectR
|
||||
aOutR = aMixR * kAmpEnv
|
||||
|
||||
outs aOut, aOutR
|
||||
endin
|
||||
`;
|
||||
}
|
||||
|
||||
protected getParametersForCsound(params: FMTomTomParams): CsoundParameter[] {
|
||||
return [
|
||||
{ channelName: 'baseFreq', value: params.baseFreq },
|
||||
{ channelName: 'pitchBendAmount', value: params.pitchBendAmount },
|
||||
{ channelName: 'pitchBendDecay', value: params.pitchBendDecay },
|
||||
{ channelName: 'modIndex', value: params.modIndex },
|
||||
{ channelName: 'modRatio', value: params.modRatio },
|
||||
{ channelName: 'noiseHPFreq', value: params.noiseHPFreq },
|
||||
{ channelName: 'noiseResonance', value: params.noiseResonance },
|
||||
{ channelName: 'noiseMix', value: params.noiseMix },
|
||||
{ channelName: 'ampAttack', value: params.ampAttack },
|
||||
{ channelName: 'ampDecay', value: params.ampDecay },
|
||||
{ channelName: 'sustain', value: params.sustain },
|
||||
{ channelName: 'release', value: params.release },
|
||||
{ channelName: 'tonality', value: params.tonality },
|
||||
{ channelName: 'stereoDetune', value: params.stereoDetune },
|
||||
];
|
||||
}
|
||||
|
||||
randomParams(pitchLock?: PitchLock): FMTomTomParams {
|
||||
const baseFreqChoices = [80, 100, 120, 150, 180, 220, 260, 300];
|
||||
const baseFreq = pitchLock?.enabled
|
||||
? pitchLock.frequency
|
||||
: this.randomChoice(baseFreqChoices) * this.randomRange(0.9, 1.1);
|
||||
|
||||
const modRatios = [0.5, 1, 1.5, 2, 2.5, 3];
|
||||
|
||||
return {
|
||||
baseFreq,
|
||||
pitchBendAmount: this.randomRange(0.2, 0.8),
|
||||
pitchBendDecay: this.randomRange(0.05, 0.2),
|
||||
modIndex: this.randomRange(1, 8),
|
||||
modRatio: this.randomChoice(modRatios),
|
||||
noiseHPFreq: this.randomRange(200, 800),
|
||||
noiseResonance: this.randomRange(20, 100),
|
||||
noiseMix: this.randomRange(0.1, 0.6),
|
||||
ampAttack: this.randomRange(0.001, 0.01),
|
||||
ampDecay: this.randomRange(0.1, 0.3),
|
||||
sustain: this.randomRange(0.2, 0.6),
|
||||
release: this.randomRange(0.2, 0.5),
|
||||
tonality: this.randomRange(0.3, 0.9),
|
||||
stereoDetune: this.randomRange(0, 0.5),
|
||||
};
|
||||
}
|
||||
|
||||
mutateParams(
|
||||
params: FMTomTomParams,
|
||||
mutationAmount: number = 0.15,
|
||||
pitchLock?: PitchLock
|
||||
): FMTomTomParams {
|
||||
const baseFreq = pitchLock?.enabled ? pitchLock.frequency : params.baseFreq;
|
||||
const modRatios = [0.5, 1, 1.5, 2, 2.5, 3];
|
||||
|
||||
return {
|
||||
baseFreq,
|
||||
pitchBendAmount: this.mutateValue(params.pitchBendAmount, mutationAmount, 0.1, 1),
|
||||
pitchBendDecay: this.mutateValue(params.pitchBendDecay, mutationAmount, 0.02, 0.4),
|
||||
modIndex: this.mutateValue(params.modIndex, mutationAmount, 0.5, 12),
|
||||
modRatio:
|
||||
Math.random() < 0.15 ? this.randomChoice(modRatios) : params.modRatio,
|
||||
noiseHPFreq: this.mutateValue(params.noiseHPFreq, mutationAmount, 100, 1200),
|
||||
noiseResonance: this.mutateValue(params.noiseResonance, mutationAmount, 15, 150),
|
||||
noiseMix: this.mutateValue(params.noiseMix, mutationAmount, 0, 0.8),
|
||||
ampAttack: this.mutateValue(params.ampAttack, mutationAmount, 0.001, 0.02),
|
||||
ampDecay: this.mutateValue(params.ampDecay, mutationAmount, 0.05, 0.5),
|
||||
sustain: this.mutateValue(params.sustain, mutationAmount, 0.1, 0.8),
|
||||
release: this.mutateValue(params.release, mutationAmount, 0.1, 0.7),
|
||||
tonality: this.mutateValue(params.tonality, mutationAmount, 0.1, 1),
|
||||
stereoDetune: this.mutateValue(params.stereoDetune, mutationAmount, 0, 1),
|
||||
};
|
||||
}
|
||||
}
|
||||
247
src/lib/audio/engines/FeedbackSnare.ts
Normal file
247
src/lib/audio/engines/FeedbackSnare.ts
Normal file
@ -0,0 +1,247 @@
|
||||
import { CsoundEngine, type CsoundParameter } from './base/CsoundEngine';
|
||||
import type { PitchLock } from './base/SynthEngine';
|
||||
|
||||
interface FeedbackSnareParams {
|
||||
baseFreq: number;
|
||||
tonalDecay: number;
|
||||
noiseDecay: number;
|
||||
toneResonance: number;
|
||||
springDecay: number;
|
||||
springTone: number;
|
||||
pitchBend: number;
|
||||
pitchBendSpeed: number;
|
||||
pulseRate: number;
|
||||
feedbackAmount: number;
|
||||
delayTime: number;
|
||||
crossFeedMix: number;
|
||||
snap: number;
|
||||
brightness: number;
|
||||
}
|
||||
|
||||
export class FeedbackSnare extends CsoundEngine<FeedbackSnareParams> {
|
||||
getName(): string {
|
||||
return 'Feedback Snare';
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return 'Complex snare using cross-feedback delay network with pulsed noise modulation';
|
||||
}
|
||||
|
||||
getType() {
|
||||
return 'generative' as const;
|
||||
}
|
||||
|
||||
getCategory() {
|
||||
return 'Percussion' as const;
|
||||
}
|
||||
|
||||
protected getOrchestra(): string {
|
||||
return `
|
||||
instr 1
|
||||
iBaseFreq chnget "baseFreq"
|
||||
iTonalDecay chnget "tonalDecay"
|
||||
iNoiseDecay chnget "noiseDecay"
|
||||
iToneResonance chnget "toneResonance"
|
||||
iSpringDecay chnget "springDecay"
|
||||
iSpringTone chnget "springTone"
|
||||
iPitchBend chnget "pitchBend"
|
||||
iPitchBendSpeed chnget "pitchBendSpeed"
|
||||
iPulseRate chnget "pulseRate"
|
||||
iFeedbackAmount chnget "feedbackAmount"
|
||||
iDelayTime chnget "delayTime"
|
||||
iCrossFeedMix chnget "crossFeedMix"
|
||||
iSnap chnget "snap"
|
||||
iBrightness chnget "brightness"
|
||||
|
||||
idur = p3
|
||||
iTonalDecayTime = iTonalDecay * idur
|
||||
iNoiseDecayTime = iNoiseDecay * idur
|
||||
iSpringDecayTime = iSpringDecay * idur
|
||||
iPitchBendTime = iPitchBendSpeed * idur
|
||||
|
||||
; Pitch envelope with bend
|
||||
kPitchEnv expseg iBaseFreq * (1 + iPitchBend * 2), iPitchBendTime, iBaseFreq, idur - iPitchBendTime, iBaseFreq * 0.95
|
||||
|
||||
; Generate square wave pulse for tonal component
|
||||
aPulse vco2 0.5, kPitchEnv, 2, 0.5
|
||||
|
||||
; Tonal envelope
|
||||
kToneEnv expseg 1, iTonalDecayTime, 0.001, idur - iTonalDecayTime, 0.001
|
||||
aTonal = aPulse * kToneEnv
|
||||
|
||||
; Apply drum tone resonant filter
|
||||
aDrumTone rezzy aTonal, kPitchEnv, iToneResonance
|
||||
|
||||
; Generate white noise
|
||||
aNoise noise 1, 0
|
||||
|
||||
; Pulse modulation of noise (creates rhythmic texture)
|
||||
kPulseMod oscili 1, iPulseRate
|
||||
kPulseMod = (kPulseMod + 1) * 0.5
|
||||
aNoiseModulated = aNoise * kPulseMod
|
||||
|
||||
; Noise envelope
|
||||
kNoiseEnv expseg 1, iNoiseDecayTime, 0.001, idur - iNoiseDecayTime, 0.001
|
||||
|
||||
; Parallel filters on noise
|
||||
; Bandpass filter 1 (body)
|
||||
aNoiseBody butterbp aNoiseModulated, iBaseFreq * 1.5, 100
|
||||
; Bandpass filter 2 (mid)
|
||||
aNoiseMid butterbp aNoiseModulated, iBaseFreq * 3, 200
|
||||
; Highpass filter (crispness)
|
||||
aNoiseHigh butterhp aNoiseModulated, 3000
|
||||
|
||||
; Mix noise components
|
||||
aNoiseMix = (aNoiseBody * 0.4 + aNoiseMid * 0.3 + aNoiseHigh * 0.3 * iBrightness) * kNoiseEnv
|
||||
|
||||
; Mix tonal and noise
|
||||
aMix = aDrumTone * 0.5 + aNoiseMix * 0.5
|
||||
|
||||
; Cross-feedback delay network (simulates spring/snare wires)
|
||||
; Create two delay lines that feed back into each other
|
||||
aDelay1Init init 0
|
||||
aDelay2Init init 0
|
||||
|
||||
; Spring tone filter (for the delayed signal)
|
||||
iSpringFreq = 800 + iSpringTone * 4000
|
||||
|
||||
; Delay line 1
|
||||
aDelayIn1 = aMix + aDelay2Init * iFeedbackAmount * iCrossFeedMix
|
||||
aDelay1 vdelay aDelayIn1, iDelayTime * 1000, 50
|
||||
aDelay1Filt butterbp aDelay1, iSpringFreq, 100
|
||||
aDelay1Out = aDelay1Filt * exp(-p3 / iSpringDecayTime)
|
||||
|
||||
; Delay line 2
|
||||
aDelayIn2 = aMix + aDelay1Out * iFeedbackAmount
|
||||
aDelay2 vdelay aDelayIn2, iDelayTime * 1.3 * 1000, 50
|
||||
aDelay2Filt butterbp aDelay2, iSpringFreq * 1.2, 120
|
||||
aDelay2Out = aDelay2Filt * exp(-p3 / iSpringDecayTime)
|
||||
|
||||
; Update feedback
|
||||
aDelay1Init = aDelay1Out
|
||||
aDelay2Init = aDelay2Out
|
||||
|
||||
; Mix dry and delay
|
||||
aOut = aMix * 0.6 + aDelay1Out * 0.2 + aDelay2Out * 0.2
|
||||
|
||||
; Add snap transient
|
||||
if iSnap > 0.1 then
|
||||
kSnapEnv linseg 1, 0.003, 0, idur - 0.003, 0
|
||||
aSnap noise iSnap * 0.5, 0
|
||||
aSnapFilt butterhp aSnap, 8000
|
||||
aOut = aOut + aSnapFilt * kSnapEnv
|
||||
endif
|
||||
|
||||
; Final output scaling
|
||||
aOut = aOut * 0.4
|
||||
|
||||
; Right channel with slightly different parameters
|
||||
aPulseR vco2 0.5, kPitchEnv * 1.002, 2, 0.5
|
||||
aTonalR = aPulseR * kToneEnv
|
||||
aDrumToneR rezzy aTonalR, kPitchEnv * 1.002, iToneResonance
|
||||
|
||||
aNoiseR noise 1, 0
|
||||
aNoiseModulatedR = aNoiseR * kPulseMod
|
||||
aNoiseBodyR butterbp aNoiseModulatedR, iBaseFreq * 1.52, 100
|
||||
aNoiseMidR butterbp aNoiseModulatedR, iBaseFreq * 3.03, 200
|
||||
aNoiseHighR butterhp aNoiseModulatedR, 3100
|
||||
|
||||
aNoiseMixR = (aNoiseBodyR * 0.4 + aNoiseMidR * 0.3 + aNoiseHighR * 0.3 * iBrightness) * kNoiseEnv
|
||||
aMixR = aDrumToneR * 0.5 + aNoiseMixR * 0.5
|
||||
|
||||
aDelay1InitR init 0
|
||||
aDelay2InitR init 0
|
||||
|
||||
aDelayIn1R = aMixR + aDelay2InitR * iFeedbackAmount * iCrossFeedMix
|
||||
aDelay1R vdelay aDelayIn1R, iDelayTime * 1.05 * 1000, 50
|
||||
aDelay1FiltR butterbp aDelay1R, iSpringFreq * 1.01, 100
|
||||
aDelay1OutR = aDelay1FiltR * exp(-p3 / iSpringDecayTime)
|
||||
|
||||
aDelayIn2R = aMixR + aDelay1OutR * iFeedbackAmount
|
||||
aDelay2R vdelay aDelayIn2R, iDelayTime * 1.35 * 1000, 50
|
||||
aDelay2FiltR butterbp aDelay2R, iSpringFreq * 1.22, 120
|
||||
aDelay2OutR = aDelay2FiltR * exp(-p3 / iSpringDecayTime)
|
||||
|
||||
aDelay1InitR = aDelay1OutR
|
||||
aDelay2InitR = aDelay2OutR
|
||||
|
||||
aOutR = aMixR * 0.6 + aDelay1OutR * 0.2 + aDelay2OutR * 0.2
|
||||
|
||||
if iSnap > 0.1 then
|
||||
aSnapR noise iSnap * 0.5, 0
|
||||
aSnapFiltR butterhp aSnapR, 8100
|
||||
aOutR = aOutR + aSnapFiltR * kSnapEnv
|
||||
endif
|
||||
|
||||
aOutR = aOutR * 0.4
|
||||
|
||||
outs aOut, aOutR
|
||||
endin
|
||||
`;
|
||||
}
|
||||
|
||||
protected getParametersForCsound(params: FeedbackSnareParams): CsoundParameter[] {
|
||||
return [
|
||||
{ channelName: 'baseFreq', value: params.baseFreq },
|
||||
{ channelName: 'tonalDecay', value: params.tonalDecay },
|
||||
{ channelName: 'noiseDecay', value: params.noiseDecay },
|
||||
{ channelName: 'toneResonance', value: params.toneResonance },
|
||||
{ channelName: 'springDecay', value: params.springDecay },
|
||||
{ channelName: 'springTone', value: params.springTone },
|
||||
{ channelName: 'pitchBend', value: params.pitchBend },
|
||||
{ channelName: 'pitchBendSpeed', value: params.pitchBendSpeed },
|
||||
{ channelName: 'pulseRate', value: params.pulseRate },
|
||||
{ channelName: 'feedbackAmount', value: params.feedbackAmount },
|
||||
{ channelName: 'delayTime', value: params.delayTime },
|
||||
{ channelName: 'crossFeedMix', value: params.crossFeedMix },
|
||||
{ channelName: 'snap', value: params.snap },
|
||||
{ channelName: 'brightness', value: params.brightness },
|
||||
];
|
||||
}
|
||||
|
||||
randomParams(pitchLock?: PitchLock): FeedbackSnareParams {
|
||||
const baseFreq = pitchLock?.enabled ? pitchLock.frequency : this.randomRange(150, 350);
|
||||
|
||||
return {
|
||||
baseFreq,
|
||||
tonalDecay: this.randomRange(0.1, 0.3),
|
||||
noiseDecay: this.randomRange(0.3, 0.7),
|
||||
toneResonance: this.randomRange(5, 25),
|
||||
springDecay: this.randomRange(0.2, 0.6),
|
||||
springTone: this.randomRange(0.2, 0.8),
|
||||
pitchBend: this.randomRange(0.3, 0.9),
|
||||
pitchBendSpeed: this.randomRange(0.01, 0.05),
|
||||
pulseRate: this.randomRange(50, 300),
|
||||
feedbackAmount: this.randomRange(0.3, 0.7),
|
||||
delayTime: this.randomRange(0.005, 0.025),
|
||||
crossFeedMix: this.randomRange(0.4, 0.9),
|
||||
snap: this.randomRange(0, 0.6),
|
||||
brightness: this.randomRange(0.3, 0.9),
|
||||
};
|
||||
}
|
||||
|
||||
mutateParams(
|
||||
params: FeedbackSnareParams,
|
||||
mutationAmount: number = 0.15,
|
||||
pitchLock?: PitchLock
|
||||
): FeedbackSnareParams {
|
||||
const baseFreq = pitchLock?.enabled ? pitchLock.frequency : params.baseFreq;
|
||||
|
||||
return {
|
||||
baseFreq,
|
||||
tonalDecay: this.mutateValue(params.tonalDecay, mutationAmount, 0.05, 0.5),
|
||||
noiseDecay: this.mutateValue(params.noiseDecay, mutationAmount, 0.2, 0.9),
|
||||
toneResonance: this.mutateValue(params.toneResonance, mutationAmount, 2, 40),
|
||||
springDecay: this.mutateValue(params.springDecay, mutationAmount, 0.1, 0.8),
|
||||
springTone: this.mutateValue(params.springTone, mutationAmount, 0, 1),
|
||||
pitchBend: this.mutateValue(params.pitchBend, mutationAmount, 0.1, 1.2),
|
||||
pitchBendSpeed: this.mutateValue(params.pitchBendSpeed, mutationAmount, 0.005, 0.1),
|
||||
pulseRate: this.mutateValue(params.pulseRate, mutationAmount, 20, 500),
|
||||
feedbackAmount: this.mutateValue(params.feedbackAmount, mutationAmount, 0.1, 0.85),
|
||||
delayTime: this.mutateValue(params.delayTime, mutationAmount, 0.003, 0.04),
|
||||
crossFeedMix: this.mutateValue(params.crossFeedMix, mutationAmount, 0.2, 1),
|
||||
snap: this.mutateValue(params.snap, mutationAmount, 0, 0.8),
|
||||
brightness: this.mutateValue(params.brightness, mutationAmount, 0, 1),
|
||||
};
|
||||
}
|
||||
}
|
||||
296
src/lib/audio/engines/Form1.ts
Normal file
296
src/lib/audio/engines/Form1.ts
Normal file
@ -0,0 +1,296 @@
|
||||
import { CsoundEngine, type CsoundParameter } from './base/CsoundEngine';
|
||||
import type { PitchLock } from './base/SynthEngine';
|
||||
|
||||
interface Form1Params {
|
||||
frequency: number;
|
||||
vibratoRate: number;
|
||||
vibratoDepth: number;
|
||||
formant1Freq: number;
|
||||
formant1BW: number;
|
||||
formant1Amp: number;
|
||||
formant2Freq: number;
|
||||
formant2BW: number;
|
||||
formant2Amp: number;
|
||||
formant3Freq: number;
|
||||
formant3BW: number;
|
||||
formant3Amp: number;
|
||||
formant4Freq: number;
|
||||
formant4BW: number;
|
||||
formant4Amp: number;
|
||||
formant5Freq: number;
|
||||
formant5BW: number;
|
||||
formant5Amp: number;
|
||||
attack: number;
|
||||
decay: number;
|
||||
sustain: number;
|
||||
release: number;
|
||||
}
|
||||
|
||||
export class Form1 extends CsoundEngine<Form1Params> {
|
||||
getName(): string {
|
||||
return 'Throat';
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return 'Multi-voice formant vocal synthesizer with deep sub-bass and wide stereo field';
|
||||
}
|
||||
|
||||
getType(): 'generative' | 'sample' | 'input' {
|
||||
return 'generative';
|
||||
}
|
||||
|
||||
getCategory(): 'Additive' | 'Subtractive' | 'FM' | 'Percussion' | 'Noise' | 'Physical' | 'Modulation' | 'Experimental' | 'Utility' {
|
||||
return 'Physical';
|
||||
}
|
||||
|
||||
getOrchestra(): string {
|
||||
return `
|
||||
instr 1
|
||||
ifreq = chnget("frequency")
|
||||
iamp = 0.3
|
||||
ivibratoRate = chnget("vibratoRate")
|
||||
ivibratoDepth = chnget("vibratoDepth")
|
||||
|
||||
if1freq = chnget("formant1Freq")
|
||||
if1bw = chnget("formant1BW")
|
||||
if1amp = chnget("formant1Amp")
|
||||
if2freq = chnget("formant2Freq")
|
||||
if2bw = chnget("formant2BW")
|
||||
if2amp = chnget("formant2Amp")
|
||||
if3freq = chnget("formant3Freq")
|
||||
if3bw = chnget("formant3BW")
|
||||
if3amp = chnget("formant3Amp")
|
||||
if4freq = chnget("formant4Freq")
|
||||
if4bw = chnget("formant4BW")
|
||||
if4amp = chnget("formant4Amp")
|
||||
if5freq = chnget("formant5Freq")
|
||||
if5bw = chnget("formant5BW")
|
||||
if5amp = chnget("formant5Amp")
|
||||
|
||||
iatt = chnget("attack") * p3
|
||||
idec = chnget("decay") * p3
|
||||
isus = chnget("sustain")
|
||||
irel = chnget("release") * p3
|
||||
|
||||
; Amplitude envelope
|
||||
aenv = madsr(iatt, idec, isus, irel)
|
||||
|
||||
; Dual vibrato LFOs for stereo width
|
||||
kvibL = lfo(ivibratoDepth, ivibratoRate)
|
||||
kvibR = lfo(ivibratoDepth, ivibratoRate * 1.03)
|
||||
|
||||
; === LEFT CHANNEL VOICES ===
|
||||
; Voice 1L: Main voice
|
||||
kfreq1L = ifreq * (1 + kvibL)
|
||||
inumharm1L = int(sr / (2 * ifreq))
|
||||
asig1L = buzz(1, kfreq1L, inumharm1L, -1)
|
||||
|
||||
; Voice 2L: Slightly detuned (0.987x for beating)
|
||||
kfreq2L = ifreq * 0.987 * (1 + kvibL * 1.02)
|
||||
inumharm2L = int(sr / (2 * ifreq * 0.987))
|
||||
asig2L = buzz(0.85, kfreq2L, inumharm2L, -1)
|
||||
|
||||
; Voice 3L: Slightly detuned up (1.013x for beating)
|
||||
kfreq3L = ifreq * 1.013 * (1 + kvibL * 0.98)
|
||||
inumharm3L = int(sr / (2 * ifreq * 1.013))
|
||||
asig3L = buzz(0.8, kfreq3L, inumharm3L, -1)
|
||||
|
||||
; Voice 4L: Octave down
|
||||
kfreq4L = ifreq * 0.5 * (1 + kvibL * 1.04)
|
||||
inumharm4L = int(sr / (2 * ifreq * 0.5))
|
||||
asig4L = buzz(0.65, kfreq4L, inumharm4L, -1)
|
||||
|
||||
; Voice 5L: Octave up
|
||||
kfreq5L = ifreq * 2 * (1 + kvibL * 0.96)
|
||||
inumharm5L = int(sr / (2 * ifreq * 2))
|
||||
asig5L = buzz(0.45, kfreq5L, inumharm5L, -1)
|
||||
|
||||
; === RIGHT CHANNEL VOICES (different detuning) ===
|
||||
; Voice 1R: Main voice slightly different
|
||||
kfreq1R = ifreq * 1.002 * (1 + kvibR)
|
||||
inumharm1R = int(sr / (2 * ifreq * 1.002))
|
||||
asig1R = buzz(1, kfreq1R, inumharm1R, -1)
|
||||
|
||||
; Voice 2R: Different detuning (0.993x)
|
||||
kfreq2R = ifreq * 0.993 * (1 + kvibR * 1.01)
|
||||
inumharm2R = int(sr / (2 * ifreq * 0.993))
|
||||
asig2R = buzz(0.85, kfreq2R, inumharm2R, -1)
|
||||
|
||||
; Voice 3R: Different detuning up (1.007x)
|
||||
kfreq3R = ifreq * 1.007 * (1 + kvibR * 0.99)
|
||||
inumharm3R = int(sr / (2 * ifreq * 1.007))
|
||||
asig3R = buzz(0.8, kfreq3R, inumharm3R, -1)
|
||||
|
||||
; Voice 4R: Octave down different phase
|
||||
kfreq4R = ifreq * 0.501 * (1 + kvibR * 1.06)
|
||||
inumharm4R = int(sr / (2 * ifreq * 0.501))
|
||||
asig4R = buzz(0.6, kfreq4R, inumharm4R, -1)
|
||||
|
||||
; Voice 5R: Octave up different phase
|
||||
kfreq5R = ifreq * 1.998 * (1 + kvibR * 0.94)
|
||||
inumharm5R = int(sr / (2 * ifreq * 1.998))
|
||||
asig5R = buzz(0.4, kfreq5R, inumharm5R, -1)
|
||||
|
||||
; Sub-bass: Buzz oscillator one octave below with harmonic content
|
||||
kfreqSubL = ifreq * 0.5 * (1 + kvibL * 0.5)
|
||||
kfreqSubR = ifreq * 0.502 * (1 + kvibR * 0.5)
|
||||
inumharmSubL = int(sr / (2 * ifreq * 0.5))
|
||||
inumharmSubR = int(sr / (2 * ifreq * 0.502))
|
||||
aSubRawL = buzz(1, kfreqSubL, inumharmSubL, -1)
|
||||
aSubRawR = buzz(1, kfreqSubR, inumharmSubR, -1)
|
||||
|
||||
; Apply very wide formants for vocal character with depth
|
||||
aSubF1L = butterbp(aSubRawL, if1freq * 0.6, if1bw * 8)
|
||||
aSubF2L = butterbp(aSubRawL, if2freq * 0.7, if2bw * 10)
|
||||
aSubF3L = butterbp(aSubRawL, if3freq * 0.65, if3bw * 9)
|
||||
aSubF1R = butterbp(aSubRawR, if1freq * 0.62, if1bw * 8.5)
|
||||
aSubF2R = butterbp(aSubRawR, if2freq * 0.68, if2bw * 10.5)
|
||||
aSubF3R = butterbp(aSubRawR, if3freq * 0.67, if3bw * 9.5)
|
||||
|
||||
; Mix sub with formants for depth and complexity
|
||||
aSubMixL = (aSubRawL * 0.3) + (aSubF1L * 0.3) + (aSubF2L * 0.25) + (aSubF3L * 0.15)
|
||||
aSubMixR = (aSubRawR * 0.3) + (aSubF1R * 0.3) + (aSubF2R * 0.25) + (aSubF3R * 0.15)
|
||||
|
||||
; Add gentle low-pass for warmth and smooth out harsh frequencies
|
||||
aSubL = butterlp(aSubMixL, 1200)
|
||||
aSubR = butterlp(aSubMixR, 1250)
|
||||
|
||||
; Scale sub-bass
|
||||
aSubL = aSubL * 0.8
|
||||
aSubR = aSubR * 0.8
|
||||
|
||||
; Mix voices per channel (sub added separately later)
|
||||
asigMixL = asig1L + asig2L + asig3L + asig4L + asig5L
|
||||
asigMixR = asig1R + asig2R + asig3R + asig4R + asig5R
|
||||
|
||||
; === LEFT CHANNEL FORMANTS ===
|
||||
; Main formants
|
||||
a1L = butterbp(asigMixL * if1amp, if1freq, if1bw)
|
||||
a2L = butterbp(asigMixL * if2amp, if2freq, if2bw)
|
||||
a3L = butterbp(asigMixL * if3amp, if3freq, if3bw)
|
||||
a4L = butterbp(asigMixL * if4amp, if4freq, if4bw)
|
||||
a5L = butterbp(asigMixL * if5amp, if5freq, if5bw)
|
||||
|
||||
; Additional formant layers
|
||||
a1bL = butterbp(asigMixL * if1amp * 0.35, if1freq * 1.025, if1bw * 1.12)
|
||||
a2bL = butterbp(asigMixL * if2amp * 0.3, if2freq * 0.975, if2bw * 1.18)
|
||||
a3bL = butterbp(asigMixL * if3amp * 0.25, if3freq * 1.035, if3bw * 1.1)
|
||||
|
||||
; === RIGHT CHANNEL FORMANTS (different shifts) ===
|
||||
; Main formants
|
||||
a1R = butterbp(asigMixR * if1amp, if1freq * 1.005, if1bw * 1.02)
|
||||
a2R = butterbp(asigMixR * if2amp, if2freq * 0.995, if2bw * 1.03)
|
||||
a3R = butterbp(asigMixR * if3amp, if3freq * 1.008, if3bw * 1.01)
|
||||
a4R = butterbp(asigMixR * if4amp, if4freq * 0.997, if4bw * 1.04)
|
||||
a5R = butterbp(asigMixR * if5amp, if5freq * 1.003, if5bw * 1.02)
|
||||
|
||||
; Additional formant layers
|
||||
a1bR = butterbp(asigMixR * if1amp * 0.3, if1freq * 0.98, if1bw * 1.15)
|
||||
a2bR = butterbp(asigMixR * if2amp * 0.28, if2freq * 1.022, if2bw * 1.2)
|
||||
a3bR = butterbp(asigMixR * if3amp * 0.22, if3freq * 0.97, if3bw * 1.12)
|
||||
|
||||
; Combine formants per channel
|
||||
asigOutL = a1L + a2L + a3L + a4L + a5L + a1bL + a2bL + a3bL
|
||||
asigOutR = a1R + a2R + a3R + a4R + a5R + a1bR + a2bR + a3bR
|
||||
|
||||
; Apply envelope and level
|
||||
asigFormantL = asigOutL * aenv * iamp * 4
|
||||
asigFormantR = asigOutR * aenv * iamp * 4
|
||||
|
||||
; Add sub-bass directly (bypassing formants)
|
||||
aL = asigFormantL + (aSubL * aenv)
|
||||
aR = asigFormantR + (aSubR * aenv)
|
||||
|
||||
outs aL, aR
|
||||
endin
|
||||
`;
|
||||
}
|
||||
|
||||
getParametersForCsound(params: Form1Params): CsoundParameter[] {
|
||||
return [
|
||||
{ channelName: 'frequency', value: params.frequency },
|
||||
{ channelName: 'vibratoRate', value: params.vibratoRate },
|
||||
{ channelName: 'vibratoDepth', value: params.vibratoDepth },
|
||||
{ channelName: 'formant1Freq', value: params.formant1Freq },
|
||||
{ channelName: 'formant1BW', value: params.formant1BW },
|
||||
{ channelName: 'formant1Amp', value: params.formant1Amp },
|
||||
{ channelName: 'formant2Freq', value: params.formant2Freq },
|
||||
{ channelName: 'formant2BW', value: params.formant2BW },
|
||||
{ channelName: 'formant2Amp', value: params.formant2Amp },
|
||||
{ channelName: 'formant3Freq', value: params.formant3Freq },
|
||||
{ channelName: 'formant3BW', value: params.formant3BW },
|
||||
{ channelName: 'formant3Amp', value: params.formant3Amp },
|
||||
{ channelName: 'formant4Freq', value: params.formant4Freq },
|
||||
{ channelName: 'formant4BW', value: params.formant4BW },
|
||||
{ channelName: 'formant4Amp', value: params.formant4Amp },
|
||||
{ channelName: 'formant5Freq', value: params.formant5Freq },
|
||||
{ channelName: 'formant5BW', value: params.formant5BW },
|
||||
{ channelName: 'formant5Amp', value: params.formant5Amp },
|
||||
{ channelName: 'attack', value: params.attack },
|
||||
{ channelName: 'decay', value: params.decay },
|
||||
{ channelName: 'sustain', value: params.sustain },
|
||||
{ channelName: 'release', value: params.release }
|
||||
];
|
||||
}
|
||||
|
||||
randomParams(pitchLock?: PitchLock): Form1Params {
|
||||
const frequency = pitchLock?.enabled ? pitchLock.frequency : 55 * Math.pow(2, Math.random() * 4);
|
||||
|
||||
return {
|
||||
frequency,
|
||||
vibratoRate: 2 + Math.random() * 6,
|
||||
vibratoDepth: 0.001 + Math.random() * 0.008,
|
||||
formant1Freq: 400 + Math.random() * 800,
|
||||
formant1BW: 40 + Math.random() * 100,
|
||||
formant1Amp: 0.8 + Math.random() * 0.2,
|
||||
formant2Freq: 800 + Math.random() * 600,
|
||||
formant2BW: 50 + Math.random() * 100,
|
||||
formant2Amp: 0.4 + Math.random() * 0.4,
|
||||
formant3Freq: 2000 + Math.random() * 1500,
|
||||
formant3BW: 80 + Math.random() * 120,
|
||||
formant3Amp: 0.05 + Math.random() * 0.15,
|
||||
formant4Freq: 3000 + Math.random() * 1500,
|
||||
formant4BW: 100 + Math.random() * 100,
|
||||
formant4Amp: 0.1 + Math.random() * 0.2,
|
||||
formant5Freq: 4000 + Math.random() * 1500,
|
||||
formant5BW: 100 + Math.random() * 150,
|
||||
formant5Amp: 0.01 + Math.random() * 0.1,
|
||||
attack: Math.random() * 0.15,
|
||||
decay: 0.05 + Math.random() * 0.3,
|
||||
sustain: 0.3 + Math.random() * 0.5,
|
||||
release: 0.05 + Math.random() * 0.4
|
||||
};
|
||||
}
|
||||
|
||||
mutateParams(params: Form1Params, mutationAmount = 0.2, pitchLock?: PitchLock): Form1Params {
|
||||
const mutate = (value: number, min: number, max: number) => {
|
||||
const change = (Math.random() - 0.5) * 2 * mutationAmount * (max - min);
|
||||
return Math.max(min, Math.min(max, value + change));
|
||||
};
|
||||
|
||||
return {
|
||||
frequency: pitchLock?.enabled ? pitchLock.frequency : mutate(params.frequency, 55, 55 * Math.pow(2, 4)),
|
||||
vibratoRate: mutate(params.vibratoRate, 2, 8),
|
||||
vibratoDepth: mutate(params.vibratoDepth, 0.001, 0.009),
|
||||
formant1Freq: mutate(params.formant1Freq, 400, 1200),
|
||||
formant1BW: mutate(params.formant1BW, 40, 140),
|
||||
formant1Amp: mutate(params.formant1Amp, 0.8, 1.0),
|
||||
formant2Freq: mutate(params.formant2Freq, 800, 1400),
|
||||
formant2BW: mutate(params.formant2BW, 50, 150),
|
||||
formant2Amp: mutate(params.formant2Amp, 0.4, 0.8),
|
||||
formant3Freq: mutate(params.formant3Freq, 2000, 3500),
|
||||
formant3BW: mutate(params.formant3BW, 80, 200),
|
||||
formant3Amp: mutate(params.formant3Amp, 0.05, 0.2),
|
||||
formant4Freq: mutate(params.formant4Freq, 3000, 4500),
|
||||
formant4BW: mutate(params.formant4BW, 100, 200),
|
||||
formant4Amp: mutate(params.formant4Amp, 0.1, 0.3),
|
||||
formant5Freq: mutate(params.formant5Freq, 4000, 5500),
|
||||
formant5BW: mutate(params.formant5BW, 100, 250),
|
||||
formant5Amp: mutate(params.formant5Amp, 0.01, 0.11),
|
||||
attack: mutate(params.attack, 0, 0.15),
|
||||
decay: mutate(params.decay, 0.05, 0.35),
|
||||
sustain: mutate(params.sustain, 0.3, 0.8),
|
||||
release: mutate(params.release, 0.05, 0.45)
|
||||
};
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
getCategory() {
|
||||
return 'FM' as const;
|
||||
}
|
||||
|
||||
generate(params: FourOpFMParams, sampleRate: number, duration: number): [Float32Array, Float32Array] {
|
||||
const numSamples = Math.floor(sampleRate * duration);
|
||||
const leftBuffer = new Float32Array(numSamples);
|
||||
|
||||
@ -19,7 +19,7 @@ interface HiHatParams {
|
||||
|
||||
export class HiHat implements SynthEngine {
|
||||
getName(): string {
|
||||
return 'Hi-Hat';
|
||||
return 'Noise Hi-Hat';
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
@ -30,6 +30,10 @@ export class HiHat implements SynthEngine {
|
||||
return 'generative' as const;
|
||||
}
|
||||
|
||||
getCategory() {
|
||||
return 'Percussion' as const;
|
||||
}
|
||||
|
||||
randomParams(pitchLock?: PitchLock): HiHatParams {
|
||||
return {
|
||||
decay: Math.random(),
|
||||
|
||||
@ -21,6 +21,10 @@ export class Input implements SynthEngine<InputParams> {
|
||||
return 'input' as const;
|
||||
}
|
||||
|
||||
getCategory() {
|
||||
return 'Utility' as const;
|
||||
}
|
||||
|
||||
async record(duration: number): Promise<void> {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({
|
||||
audio: {
|
||||
|
||||
@ -39,6 +39,10 @@ export class KarplusStrong implements SynthEngine<KarplusStrongParams> {
|
||||
return 'generative' as const;
|
||||
}
|
||||
|
||||
getCategory() {
|
||||
return 'Physical' as const;
|
||||
}
|
||||
|
||||
generate(params: KarplusStrongParams, sampleRate: number, duration: number): [Float32Array, Float32Array] {
|
||||
const numSamples = Math.floor(sampleRate * duration);
|
||||
const leftBuffer = new Float32Array(numSamples);
|
||||
|
||||
244
src/lib/audio/engines/LaserSweep.ts
Normal file
244
src/lib/audio/engines/LaserSweep.ts
Normal file
@ -0,0 +1,244 @@
|
||||
import { CsoundEngine, type CsoundParameter } from './base/CsoundEngine';
|
||||
|
||||
interface LaserSweepParams {
|
||||
startFreq: number;
|
||||
endFreq: number;
|
||||
sweepCurve: number;
|
||||
resonance: number;
|
||||
brightness: number;
|
||||
delayTime: number;
|
||||
delayFeedback: number;
|
||||
reverbMix: number;
|
||||
reverbSize: number;
|
||||
noiseAmount: number;
|
||||
decayShape: number;
|
||||
waveformMix: number;
|
||||
mainWaveType: number;
|
||||
secondWaveType: number;
|
||||
pulseWidth: number;
|
||||
filterTracking: number;
|
||||
subOctave: number;
|
||||
subOctaveLevel: number;
|
||||
}
|
||||
|
||||
export class LaserSweep extends CsoundEngine<LaserSweepParams> {
|
||||
getName(): string {
|
||||
return 'Laser Sweep';
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return 'Sweeping laser beam with delay and reverb';
|
||||
}
|
||||
|
||||
getType(): 'generative' | 'sample' | 'input' {
|
||||
return 'generative';
|
||||
}
|
||||
|
||||
getCategory() {
|
||||
return 'Experimental' as const;
|
||||
}
|
||||
|
||||
getOrchestra(): string {
|
||||
return `
|
||||
sr = 44100
|
||||
ksmps = 64
|
||||
nchnls = 2
|
||||
0dbfs = 1
|
||||
|
||||
instr 1
|
||||
iDur = p3
|
||||
|
||||
iStartFreq chnget "startFreq"
|
||||
iEndFreq chnget "endFreq"
|
||||
iSweepCurve chnget "sweepCurve"
|
||||
iResonance chnget "resonance"
|
||||
iBrightness chnget "brightness"
|
||||
iDelayTime chnget "delayTime"
|
||||
iDelayFeedback chnget "delayFeedback"
|
||||
iReverbMix chnget "reverbMix"
|
||||
iReverbSize chnget "reverbSize"
|
||||
iNoiseAmount chnget "noiseAmount"
|
||||
iDecayShape chnget "decayShape"
|
||||
iWaveformMix chnget "waveformMix"
|
||||
iMainWaveType chnget "mainWaveType"
|
||||
iSecondWaveType chnget "secondWaveType"
|
||||
iPulseWidth chnget "pulseWidth"
|
||||
iFilterTracking chnget "filterTracking"
|
||||
iSubOctave chnget "subOctave"
|
||||
iSubOctaveLevel chnget "subOctaveLevel"
|
||||
|
||||
if iDecayShape < 0.33 then
|
||||
kEnv expseg 1, iDur * 0.85, 1, iDur * 0.15, 0.001
|
||||
elseif iDecayShape < 0.67 then
|
||||
kEnv linseg 1, iDur * 0.85, 1, iDur * 0.15, 0
|
||||
else
|
||||
kEnv expseg 1, iDur * 0.8, 1, iDur * 0.18, 0.001
|
||||
endif
|
||||
|
||||
kFreq expseg iStartFreq, iDur * iSweepCurve, iEndFreq
|
||||
kFreqLimited limit kFreq, 40, 16000
|
||||
|
||||
if iMainWaveType < 0.2 then
|
||||
aMain vco2 0.6, kFreqLimited, 10
|
||||
elseif iMainWaveType < 0.4 then
|
||||
aMain vco2 0.6, kFreqLimited, 0
|
||||
elseif iMainWaveType < 0.6 then
|
||||
aMain vco2 0.6, kFreqLimited, 2, iPulseWidth
|
||||
elseif iMainWaveType < 0.8 then
|
||||
aMain vco2 0.6, kFreqLimited, 12
|
||||
else
|
||||
aMain oscili 0.6, kFreqLimited
|
||||
endif
|
||||
|
||||
if iSecondWaveType < 0.2 then
|
||||
aSecond vco2 0.5, kFreqLimited, 2, iPulseWidth
|
||||
elseif iSecondWaveType < 0.4 then
|
||||
aSecond vco2 0.5, kFreqLimited, 0
|
||||
elseif iSecondWaveType < 0.6 then
|
||||
aSecond vco2 0.5, kFreqLimited, 12
|
||||
elseif iSecondWaveType < 0.8 then
|
||||
aSecond vco2 0.5, kFreqLimited, 10
|
||||
else
|
||||
aSecond oscili 0.5, kFreqLimited * 1.5
|
||||
endif
|
||||
|
||||
aNoise rand 0.3
|
||||
kNoiseFiltFreq limit kFreqLimited * 0.5, 50, 12000
|
||||
aNoise butterbp aNoise, kNoiseFiltFreq, kNoiseFiltFreq * 0.5
|
||||
|
||||
if iSubOctaveLevel > 0.01 then
|
||||
if iSubOctave < 0.5 then
|
||||
kSubFreq = kFreqLimited * 0.5
|
||||
else
|
||||
kSubFreq = kFreqLimited * 0.25
|
||||
endif
|
||||
aSub oscili iSubOctaveLevel * 0.7, kSubFreq
|
||||
else
|
||||
aSub = 0
|
||||
endif
|
||||
|
||||
aOsc = aMain + aSecond * iWaveformMix + aSecond * iBrightness * 0.3 + aNoise * iNoiseAmount + aSub
|
||||
|
||||
kFilterFreq = kFreqLimited * (1 + (1 - iFilterTracking) * 2)
|
||||
kFilterFreq limit kFilterFreq, 50, 15000
|
||||
|
||||
kBandwidth = kFilterFreq * (0.1 + iResonance * 0.3)
|
||||
aFilt butterbp aOsc, kFilterFreq, kBandwidth
|
||||
|
||||
aFilt = aFilt * kEnv * 0.8
|
||||
|
||||
if iDelayTime > 0.01 then
|
||||
aDlyL vdelay3 aFilt, iDelayTime * 1000, 2000
|
||||
aDlyR vdelay3 aFilt, iDelayTime * 1000 * 1.1, 2000
|
||||
|
||||
aDlyL = aDlyL * iDelayFeedback * kEnv
|
||||
aDlyR = aDlyR * iDelayFeedback * kEnv
|
||||
else
|
||||
aDlyL = 0
|
||||
aDlyR = 0
|
||||
endif
|
||||
|
||||
if iReverbMix > 0.01 then
|
||||
aWetL, aWetR reverbsc aFilt + aDlyL * 0.5, aFilt + aDlyR * 0.5, iReverbSize, 8000
|
||||
else
|
||||
aWetL = 0
|
||||
aWetR = 0
|
||||
endif
|
||||
|
||||
aOutL = aFilt + aDlyL * 0.4 + aWetL * iReverbMix
|
||||
aOutR = aFilt + aDlyR * 0.4 + aWetR * iReverbMix
|
||||
|
||||
outs aOutL, aOutR
|
||||
endin
|
||||
`;
|
||||
}
|
||||
|
||||
getParametersForCsound(params: LaserSweepParams): CsoundParameter[] {
|
||||
return [
|
||||
{ channelName: 'startFreq', value: params.startFreq },
|
||||
{ channelName: 'endFreq', value: params.endFreq },
|
||||
{ channelName: 'sweepCurve', value: params.sweepCurve },
|
||||
{ channelName: 'resonance', value: params.resonance },
|
||||
{ channelName: 'brightness', value: params.brightness },
|
||||
{ channelName: 'delayTime', value: params.delayTime },
|
||||
{ channelName: 'delayFeedback', value: params.delayFeedback },
|
||||
{ channelName: 'reverbMix', value: params.reverbMix },
|
||||
{ channelName: 'reverbSize', value: params.reverbSize },
|
||||
{ channelName: 'noiseAmount', value: params.noiseAmount },
|
||||
{ channelName: 'decayShape', value: params.decayShape },
|
||||
{ channelName: 'waveformMix', value: params.waveformMix },
|
||||
{ channelName: 'mainWaveType', value: params.mainWaveType },
|
||||
{ channelName: 'secondWaveType', value: params.secondWaveType },
|
||||
{ channelName: 'pulseWidth', value: params.pulseWidth },
|
||||
{ channelName: 'filterTracking', value: params.filterTracking },
|
||||
{ channelName: 'subOctave', value: params.subOctave },
|
||||
{ channelName: 'subOctaveLevel', value: params.subOctaveLevel }
|
||||
];
|
||||
}
|
||||
|
||||
randomParams(pitchLock?: number): LaserSweepParams {
|
||||
const isUpward = Math.random() > 0.3;
|
||||
|
||||
const startFreq = isUpward
|
||||
? 60 + Math.random() * 200
|
||||
: 1000 + Math.random() * 10000;
|
||||
|
||||
const endFreq = isUpward
|
||||
? 1000 + Math.random() * 10000
|
||||
: 60 + Math.random() * 200;
|
||||
|
||||
return {
|
||||
startFreq,
|
||||
endFreq,
|
||||
sweepCurve: 0.2 + Math.random() * 0.7,
|
||||
resonance: 0.2 + Math.random() * 0.7,
|
||||
brightness: 0.1 + Math.random() * 0.8,
|
||||
delayTime: Math.random() < 0.3 ? 0 : 0.05 + Math.random() * 0.5,
|
||||
delayFeedback: 0.1 + Math.random() * 0.7,
|
||||
reverbMix: Math.random() < 0.2 ? 0 : 0.1 + Math.random() * 0.5,
|
||||
reverbSize: 0.4 + Math.random() * 0.5,
|
||||
noiseAmount: 0.05 + Math.random() * 0.5,
|
||||
decayShape: Math.random(),
|
||||
waveformMix: 0.3 + Math.random() * 0.7,
|
||||
mainWaveType: Math.random(),
|
||||
secondWaveType: Math.random(),
|
||||
pulseWidth: 0.1 + Math.random() * 0.8,
|
||||
filterTracking: 0.3 + Math.random() * 0.7,
|
||||
subOctave: Math.random(),
|
||||
subOctaveLevel: Math.random() < 0.4 ? 0 : 0.3 + Math.random() * 0.7
|
||||
};
|
||||
}
|
||||
|
||||
mutateParams(
|
||||
params: LaserSweepParams,
|
||||
mutationAmount: number = 0.3,
|
||||
pitchLock?: number
|
||||
): LaserSweepParams {
|
||||
const mutate = (value: number, min: number, max: number): number => {
|
||||
const range = max - min;
|
||||
const change = (Math.random() - 0.5) * range * mutationAmount;
|
||||
return Math.max(min, Math.min(max, value + change));
|
||||
};
|
||||
|
||||
return {
|
||||
startFreq: mutate(params.startFreq, 40, 12000),
|
||||
endFreq: mutate(params.endFreq, 40, 12000),
|
||||
sweepCurve: mutate(params.sweepCurve, 0.1, 0.95),
|
||||
resonance: mutate(params.resonance, 0.1, 1.0),
|
||||
brightness: mutate(params.brightness, 0.0, 1.0),
|
||||
delayTime: mutate(params.delayTime, 0.0, 0.6),
|
||||
delayFeedback: mutate(params.delayFeedback, 0.0, 0.85),
|
||||
reverbMix: mutate(params.reverbMix, 0.0, 0.7),
|
||||
reverbSize: mutate(params.reverbSize, 0.3, 0.95),
|
||||
noiseAmount: mutate(params.noiseAmount, 0.0, 0.7),
|
||||
decayShape: mutate(params.decayShape, 0.0, 1.0),
|
||||
waveformMix: mutate(params.waveformMix, 0.0, 1.0),
|
||||
mainWaveType: mutate(params.mainWaveType, 0.0, 1.0),
|
||||
secondWaveType: mutate(params.secondWaveType, 0.0, 1.0),
|
||||
pulseWidth: mutate(params.pulseWidth, 0.05, 0.95),
|
||||
filterTracking: mutate(params.filterTracking, 0.0, 1.0),
|
||||
subOctave: mutate(params.subOctave, 0.0, 1.0),
|
||||
subOctaveLevel: mutate(params.subOctaveLevel, 0.0, 1.0)
|
||||
};
|
||||
}
|
||||
}
|
||||
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 {
|
||||
getName(): string {
|
||||
return 'NPerc';
|
||||
return 'Noise Perc';
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
@ -50,6 +50,10 @@ export class NoiseDrum implements SynthEngine {
|
||||
return 'generative' as const;
|
||||
}
|
||||
|
||||
getCategory() {
|
||||
return 'Percussion' as const;
|
||||
}
|
||||
|
||||
randomParams(): NoiseDrumParams {
|
||||
// Intelligent parameter generation based on correlated characteristics
|
||||
|
||||
@ -93,16 +97,16 @@ export class NoiseDrum implements SynthEngine {
|
||||
const filterType = filterFreq < 0.3 ?
|
||||
Math.random() * 0.35 : // Low freq prefers lowpass
|
||||
filterFreq > 0.7 ?
|
||||
0.5 + Math.random() * 0.5 : // High freq prefers highpass/bandpass
|
||||
Math.random(); // Mid freq - any type
|
||||
0.5 + Math.random() * 0.5 : // High freq prefers highpass/bandpass
|
||||
Math.random(); // Mid freq - any type
|
||||
|
||||
// Decay time inversely correlates with frequency
|
||||
const decayBias = Math.random();
|
||||
const ampDecay = filterFreq < 0.3 ?
|
||||
0.25 + decayBias * 0.4 : // Low freq can be longer
|
||||
filterFreq > 0.6 ?
|
||||
0.08 + decayBias * 0.35 : // High freq shorter
|
||||
0.2 + decayBias * 0.45; // Mid range
|
||||
0.08 + decayBias * 0.35 : // High freq shorter
|
||||
0.2 + decayBias * 0.45; // Mid range
|
||||
|
||||
// Attack is generally very short for percussion
|
||||
const ampAttack = Math.random() < 0.85 ?
|
||||
@ -372,7 +376,7 @@ export class NoiseDrum implements SynthEngine {
|
||||
|
||||
// Blend body resonance - SUBTLE
|
||||
sample = sample * (1 - params.bodyAmount * 0.4) +
|
||||
bodyFiltered.output * params.bodyAmount * 0.6 * bodyEnv;
|
||||
bodyFiltered.output * params.bodyAmount * 0.6 * bodyEnv;
|
||||
}
|
||||
|
||||
// Apply amplitude envelope
|
||||
@ -434,7 +438,7 @@ export class NoiseDrum implements SynthEngine {
|
||||
pinkState[5] = -0.7616 * pinkState[5] - whiteNoise * 0.0168980;
|
||||
|
||||
const pink = pinkState[0] + pinkState[1] + pinkState[2] + pinkState[3] +
|
||||
pinkState[4] + pinkState[5] + pinkState[6] + whiteNoise * 0.5362;
|
||||
pinkState[4] + pinkState[5] + pinkState[6] + whiteNoise * 0.5362;
|
||||
pinkState[6] = whiteNoise * 0.115926;
|
||||
|
||||
return pink * 0.11;
|
||||
|
||||
@ -38,6 +38,10 @@ export class ParticleNoise implements SynthEngine {
|
||||
return 'generative' as const;
|
||||
}
|
||||
|
||||
getCategory() {
|
||||
return 'Noise' as const;
|
||||
}
|
||||
|
||||
randomParams(pitchLock?: PitchLock): ParticleNoiseParams {
|
||||
const densityBias = Math.random();
|
||||
|
||||
|
||||
@ -65,7 +65,7 @@ export class PhaseDistortionFM implements SynthEngine<PhaseDistortionFMParams> {
|
||||
private static workletURL: string | null = null;
|
||||
|
||||
getName(): string {
|
||||
return 'PD';
|
||||
return 'Phase Dist';
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
@ -76,6 +76,10 @@ export class PhaseDistortionFM implements SynthEngine<PhaseDistortionFMParams> {
|
||||
return 'generative' as const;
|
||||
}
|
||||
|
||||
getCategory() {
|
||||
return 'FM' as const;
|
||||
}
|
||||
|
||||
generate(params: PhaseDistortionFMParams, sampleRate: number, duration: number): [Float32Array, Float32Array] {
|
||||
const numSamples = Math.floor(sampleRate * duration);
|
||||
const leftBuffer = new Float32Array(numSamples);
|
||||
|
||||
@ -82,6 +82,10 @@ export class Ring implements SynthEngine<RingParams> {
|
||||
return 'generative' as const;
|
||||
}
|
||||
|
||||
getCategory() {
|
||||
return 'Modulation' as const;
|
||||
}
|
||||
|
||||
generate(params: RingParams, sampleRate: number, duration: number): [Float32Array, Float32Array] {
|
||||
const numSamples = Math.floor(sampleRate * duration);
|
||||
const leftBuffer = new Float32Array(numSamples);
|
||||
|
||||
178
src/lib/audio/engines/RingCymbal.ts
Normal file
178
src/lib/audio/engines/RingCymbal.ts
Normal file
@ -0,0 +1,178 @@
|
||||
import { CsoundEngine, type CsoundParameter } from './base/CsoundEngine';
|
||||
import type { PitchLock } from './base/SynthEngine';
|
||||
|
||||
interface RingCymbalParams {
|
||||
baseFreq: number;
|
||||
overtone1Freq: number;
|
||||
overtone2Freq: number;
|
||||
overtone3Freq: number;
|
||||
overtone1Vol: number;
|
||||
overtone2Vol: number;
|
||||
overtone3Vol: number;
|
||||
filterCutoff: number;
|
||||
resonance: number;
|
||||
decay: number;
|
||||
attack: number;
|
||||
noise: number;
|
||||
brightness: number;
|
||||
spread: number;
|
||||
}
|
||||
|
||||
export class RingCymbal extends CsoundEngine<RingCymbalParams> {
|
||||
getName(): string {
|
||||
return 'Ring Cymbal';
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return 'Metallic cymbal using ring modulation with noise and multiple oscillators';
|
||||
}
|
||||
|
||||
getType() {
|
||||
return 'generative' as const;
|
||||
}
|
||||
|
||||
getCategory() {
|
||||
return 'Percussion' as const;
|
||||
}
|
||||
|
||||
protected getOrchestra(): string {
|
||||
return `
|
||||
instr 1
|
||||
iBaseFreq chnget "baseFreq"
|
||||
iOvertone1Freq chnget "overtone1Freq"
|
||||
iOvertone2Freq chnget "overtone2Freq"
|
||||
iOvertone3Freq chnget "overtone3Freq"
|
||||
iOvertone1Vol chnget "overtone1Vol"
|
||||
iOvertone2Vol chnget "overtone2Vol"
|
||||
iOvertone3Vol chnget "overtone3Vol"
|
||||
iFilterCutoff chnget "filterCutoff"
|
||||
iResonance chnget "resonance"
|
||||
iDecay chnget "decay"
|
||||
iAttack chnget "attack"
|
||||
iNoise chnget "noise"
|
||||
iBrightness chnget "brightness"
|
||||
iSpread chnget "spread"
|
||||
|
||||
idur = p3
|
||||
iDecayTime = iDecay * idur
|
||||
iAttackTime = iAttack * idur
|
||||
|
||||
; Exponential decay envelope with attack
|
||||
kEnv linseg 0, iAttackTime, 1, iDecayTime - iAttackTime, 0.001, 0.001, 0
|
||||
kEnv = kEnv * kEnv
|
||||
|
||||
; Generate white noise source
|
||||
aNoise noise 1, 0
|
||||
|
||||
; Generate impulse oscillators at different frequencies
|
||||
aOsc1 oscili 1, iBaseFreq
|
||||
aOsc2 oscili iOvertone1Vol, iOvertone1Freq
|
||||
aOsc3 oscili iOvertone2Vol, iOvertone2Freq
|
||||
aOsc4 oscili iOvertone3Vol, iOvertone3Freq
|
||||
|
||||
; Ring modulation: multiply noise with oscillators
|
||||
aRing1 = aNoise * aOsc1
|
||||
aRing2 = aNoise * aOsc2
|
||||
aRing3 = aNoise * aOsc3
|
||||
aRing4 = aNoise * aOsc4
|
||||
|
||||
; Mix ring modulated signals
|
||||
aMix = (aRing1 + aRing2 + aRing3 + aRing4) * 0.25
|
||||
|
||||
; Add raw noise for character
|
||||
aMix = aMix * (1 - iNoise) + aNoise * iNoise * 0.3
|
||||
|
||||
; Apply resonant high-pass filter for metallic character
|
||||
aFiltered butterhp aMix, iFilterCutoff, iResonance
|
||||
|
||||
; Additional high-pass for brightness
|
||||
if iBrightness > 0.3 then
|
||||
aFiltered butterhp aFiltered, iFilterCutoff * (1 + iBrightness), iResonance * 0.5
|
||||
endif
|
||||
|
||||
; Apply envelope
|
||||
aOut = aFiltered * kEnv * 0.4
|
||||
|
||||
; Stereo spread using slightly different filter parameters
|
||||
iFilterCutoffR = iFilterCutoff * (1 + iSpread * 0.1)
|
||||
aFilteredR butterhp aMix, iFilterCutoffR, iResonance
|
||||
|
||||
if iBrightness > 0.3 then
|
||||
aFilteredR butterhp aFilteredR, iFilterCutoffR * (1 + iBrightness), iResonance * 0.5
|
||||
endif
|
||||
|
||||
aOutR = aFilteredR * kEnv * 0.4
|
||||
|
||||
outs aOut, aOutR
|
||||
endin
|
||||
`;
|
||||
}
|
||||
|
||||
protected getParametersForCsound(params: RingCymbalParams): CsoundParameter[] {
|
||||
return [
|
||||
{ channelName: 'baseFreq', value: params.baseFreq },
|
||||
{ channelName: 'overtone1Freq', value: params.overtone1Freq },
|
||||
{ channelName: 'overtone2Freq', value: params.overtone2Freq },
|
||||
{ channelName: 'overtone3Freq', value: params.overtone3Freq },
|
||||
{ channelName: 'overtone1Vol', value: params.overtone1Vol },
|
||||
{ channelName: 'overtone2Vol', value: params.overtone2Vol },
|
||||
{ channelName: 'overtone3Vol', value: params.overtone3Vol },
|
||||
{ channelName: 'filterCutoff', value: params.filterCutoff },
|
||||
{ channelName: 'resonance', value: params.resonance },
|
||||
{ channelName: 'decay', value: params.decay },
|
||||
{ channelName: 'attack', value: params.attack },
|
||||
{ channelName: 'noise', value: params.noise },
|
||||
{ channelName: 'brightness', value: params.brightness },
|
||||
{ channelName: 'spread', value: params.spread },
|
||||
];
|
||||
}
|
||||
|
||||
randomParams(pitchLock?: PitchLock): RingCymbalParams {
|
||||
const baseFreq = pitchLock?.enabled ? pitchLock.frequency : this.randomRange(800, 1800);
|
||||
|
||||
const inharmonicRatios = [1.4, 1.7, 2.1, 2.3, 2.9, 3.1, 3.7, 4.3];
|
||||
|
||||
return {
|
||||
baseFreq,
|
||||
overtone1Freq: baseFreq * this.randomChoice(inharmonicRatios),
|
||||
overtone2Freq: baseFreq * this.randomChoice(inharmonicRatios),
|
||||
overtone3Freq: baseFreq * this.randomChoice(inharmonicRatios),
|
||||
overtone1Vol: this.randomRange(0.4, 1.0),
|
||||
overtone2Vol: this.randomRange(0.3, 0.9),
|
||||
overtone3Vol: this.randomRange(0.2, 0.7),
|
||||
filterCutoff: this.randomRange(3000, 8000),
|
||||
resonance: this.randomRange(2, 8),
|
||||
decay: this.randomRange(0.3, 0.9),
|
||||
attack: this.randomRange(0.001, 0.02),
|
||||
noise: this.randomRange(0.1, 0.5),
|
||||
brightness: this.randomRange(0, 0.8),
|
||||
spread: this.randomRange(0.01, 0.15),
|
||||
};
|
||||
}
|
||||
|
||||
mutateParams(
|
||||
params: RingCymbalParams,
|
||||
mutationAmount: number = 0.15,
|
||||
pitchLock?: PitchLock
|
||||
): RingCymbalParams {
|
||||
const baseFreq = pitchLock?.enabled ? pitchLock.frequency : params.baseFreq;
|
||||
const freqRatio = baseFreq / params.baseFreq;
|
||||
|
||||
return {
|
||||
baseFreq,
|
||||
overtone1Freq: this.mutateValue(params.overtone1Freq * freqRatio, mutationAmount, baseFreq * 1.2, baseFreq * 5),
|
||||
overtone2Freq: this.mutateValue(params.overtone2Freq * freqRatio, mutationAmount, baseFreq * 1.2, baseFreq * 5),
|
||||
overtone3Freq: this.mutateValue(params.overtone3Freq * freqRatio, mutationAmount, baseFreq * 1.2, baseFreq * 5),
|
||||
overtone1Vol: this.mutateValue(params.overtone1Vol, mutationAmount, 0.2, 1.0),
|
||||
overtone2Vol: this.mutateValue(params.overtone2Vol, mutationAmount, 0.1, 1.0),
|
||||
overtone3Vol: this.mutateValue(params.overtone3Vol, mutationAmount, 0.1, 0.9),
|
||||
filterCutoff: this.mutateValue(params.filterCutoff, mutationAmount, 2000, 10000),
|
||||
resonance: this.mutateValue(params.resonance, mutationAmount, 1, 12),
|
||||
decay: this.mutateValue(params.decay, mutationAmount, 0.2, 1.0),
|
||||
attack: this.mutateValue(params.attack, mutationAmount, 0.001, 0.05),
|
||||
noise: this.mutateValue(params.noise, mutationAmount, 0, 0.7),
|
||||
brightness: this.mutateValue(params.brightness, mutationAmount, 0, 1),
|
||||
spread: this.mutateValue(params.spread, mutationAmount, 0.005, 0.25),
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -21,6 +21,10 @@ export class Sample implements SynthEngine<SampleParams> {
|
||||
return 'sample' as const;
|
||||
}
|
||||
|
||||
getCategory() {
|
||||
return 'Utility' as const;
|
||||
}
|
||||
|
||||
async loadFile(file: File): Promise<void> {
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const audioContext = new AudioContext();
|
||||
|
||||
@ -23,7 +23,7 @@ interface SnareParams {
|
||||
|
||||
export class Snare implements SynthEngine {
|
||||
getName(): string {
|
||||
return 'Snare';
|
||||
return 'Noise Snare';
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
@ -34,6 +34,10 @@ export class Snare implements SynthEngine {
|
||||
return 'generative' as const;
|
||||
}
|
||||
|
||||
getCategory() {
|
||||
return 'Percussion' as const;
|
||||
}
|
||||
|
||||
randomParams(pitchLock?: PitchLock): SnareParams {
|
||||
return {
|
||||
baseFreq: pitchLock ? this.freqToParam(pitchLock.frequency) : 0.3 + Math.random() * 0.4,
|
||||
|
||||
149
src/lib/audio/engines/Squine1.ts
Normal file
149
src/lib/audio/engines/Squine1.ts
Normal file
@ -0,0 +1,149 @@
|
||||
import { CsoundEngine, type CsoundParameter } from './base/CsoundEngine';
|
||||
import type { PitchLock } from './base/SynthEngine';
|
||||
|
||||
interface Squine1Params {
|
||||
frequency: number;
|
||||
shapeAmount1: number;
|
||||
shapeAmount2: number;
|
||||
modulationDepth1: number;
|
||||
modulationDepth2: number;
|
||||
osc2Detune: number;
|
||||
osc2Level: number;
|
||||
hpfCutoff: number;
|
||||
attack: number;
|
||||
decay: number;
|
||||
sustain: number;
|
||||
release: number;
|
||||
}
|
||||
|
||||
export class Squine1 extends CsoundEngine<Squine1Params> {
|
||||
getName(): string {
|
||||
return 'Squine1';
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return 'Squinewave synthesizer with waveshaping and dual oscillators';
|
||||
}
|
||||
|
||||
getType(): 'generative' | 'sample' | 'input' {
|
||||
return 'generative';
|
||||
}
|
||||
|
||||
getCategory(): 'Additive' | 'Subtractive' | 'FM' | 'Percussion' | 'Noise' | 'Physical' | 'Modulation' | 'Experimental' | 'Utility' {
|
||||
return 'Subtractive';
|
||||
}
|
||||
|
||||
getOrchestra(): string {
|
||||
return `
|
||||
instr 1
|
||||
ifreq = chnget("frequency")
|
||||
iamp = 0.5
|
||||
ishape1 = chnget("shapeAmount1")
|
||||
ishape2 = chnget("shapeAmount2")
|
||||
imod1 = chnget("modulationDepth1")
|
||||
imod2 = chnget("modulationDepth2")
|
||||
idetune = chnget("osc2Detune")
|
||||
iosc2Level = chnget("osc2Level")
|
||||
ihpf = chnget("hpfCutoff")
|
||||
iatt = chnget("attack") * p3
|
||||
idec = chnget("decay") * p3
|
||||
isus = chnget("sustain")
|
||||
irel = chnget("release") * p3
|
||||
|
||||
; First oscillator with waveshaping (squine approximation)
|
||||
aosc1 = poscil(1, ifreq)
|
||||
|
||||
; Create dynamic shape envelope for osc1
|
||||
kshape1 = expseg(ishape1, p3, ishape1 * imod1)
|
||||
|
||||
; Waveshaping: blend between sine and square-like
|
||||
asig1 = tanh(aosc1 * kshape1 * 5) / tanh(kshape1 * 5)
|
||||
|
||||
; Second oscillator slightly detuned
|
||||
aosc2 = poscil(1, ifreq * (1 + idetune))
|
||||
|
||||
; Create dynamic shape envelope for osc2
|
||||
kshape2 = expseg(ishape2, p3, ishape2 * imod2)
|
||||
|
||||
; Waveshaping for second oscillator
|
||||
asig2 = tanh(aosc2 * kshape2 * 5) / tanh(kshape2 * 5)
|
||||
|
||||
; Mix oscillators
|
||||
asig = asig1 + (asig2 * iosc2Level)
|
||||
|
||||
; High-pass filter to remove DC and rumble
|
||||
kcutoff = max(ihpf, 20)
|
||||
asig = butterhp(asig, kcutoff)
|
||||
|
||||
; Amplitude envelope
|
||||
aenv = madsr(iatt, idec, isus, irel)
|
||||
asig = asig * aenv * iamp
|
||||
|
||||
; DC blocker
|
||||
asig = dcblock2(asig)
|
||||
|
||||
outs asig, asig
|
||||
endin
|
||||
`;
|
||||
}
|
||||
|
||||
getParametersForCsound(params: Squine1Params): CsoundParameter[] {
|
||||
return [
|
||||
{ channelName: 'frequency', value: params.frequency },
|
||||
{ channelName: 'shapeAmount1', value: params.shapeAmount1 },
|
||||
{ channelName: 'shapeAmount2', value: params.shapeAmount2 },
|
||||
{ channelName: 'modulationDepth1', value: params.modulationDepth1 },
|
||||
{ channelName: 'modulationDepth2', value: params.modulationDepth2 },
|
||||
{ channelName: 'osc2Detune', value: params.osc2Detune },
|
||||
{ channelName: 'osc2Level', value: params.osc2Level },
|
||||
{ channelName: 'hpfCutoff', value: params.hpfCutoff },
|
||||
{ channelName: 'attack', value: params.attack },
|
||||
{ channelName: 'decay', value: params.decay },
|
||||
{ channelName: 'sustain', value: params.sustain },
|
||||
{ channelName: 'release', value: params.release }
|
||||
];
|
||||
}
|
||||
|
||||
randomParams(pitchLock?: PitchLock): Squine1Params {
|
||||
const frequency = pitchLock?.enabled ? pitchLock.frequency : 55 * Math.pow(2, Math.random() * 5);
|
||||
|
||||
return {
|
||||
frequency,
|
||||
shapeAmount1: 0.2 + Math.random() * 0.8,
|
||||
shapeAmount2: 0.2 + Math.random() * 0.8,
|
||||
modulationDepth1: 0.05 + Math.random() * 0.7,
|
||||
modulationDepth2: 0.05 + Math.random() * 0.7,
|
||||
osc2Detune: 0.0001 + Math.random() * 0.005,
|
||||
osc2Level: 0.01 + Math.random() * 0.15,
|
||||
hpfCutoff: frequency * 0.5 + Math.random() * frequency,
|
||||
attack: 0.001 + Math.random() * 0.05,
|
||||
decay: 0.05 + Math.random() * 0.3,
|
||||
sustain: 0.2 + Math.random() * 0.6,
|
||||
release: 0.01 + Math.random() * 0.3
|
||||
};
|
||||
}
|
||||
|
||||
mutateParams(params: Squine1Params, mutationAmount = 0.2, pitchLock?: PitchLock): Squine1Params {
|
||||
const mutate = (value: number, min: number, max: number) => {
|
||||
const change = (Math.random() - 0.5) * 2 * mutationAmount * (max - min);
|
||||
return Math.max(min, Math.min(max, value + change));
|
||||
};
|
||||
|
||||
const newFreq = pitchLock?.enabled ? pitchLock.frequency : mutate(params.frequency, 55, 55 * Math.pow(2, 5));
|
||||
|
||||
return {
|
||||
frequency: newFreq,
|
||||
shapeAmount1: mutate(params.shapeAmount1, 0.2, 1.0),
|
||||
shapeAmount2: mutate(params.shapeAmount2, 0.2, 1.0),
|
||||
modulationDepth1: mutate(params.modulationDepth1, 0.05, 0.75),
|
||||
modulationDepth2: mutate(params.modulationDepth2, 0.05, 0.75),
|
||||
osc2Detune: mutate(params.osc2Detune, 0.0001, 0.0051),
|
||||
osc2Level: mutate(params.osc2Level, 0.01, 0.16),
|
||||
hpfCutoff: mutate(params.hpfCutoff, newFreq * 0.5, newFreq * 1.5),
|
||||
attack: mutate(params.attack, 0.001, 0.051),
|
||||
decay: mutate(params.decay, 0.05, 0.35),
|
||||
sustain: mutate(params.sustain, 0.2, 0.8),
|
||||
release: mutate(params.release, 0.01, 0.31)
|
||||
};
|
||||
}
|
||||
}
|
||||
135
src/lib/audio/engines/Sub8.ts
Normal file
135
src/lib/audio/engines/Sub8.ts
Normal file
@ -0,0 +1,135 @@
|
||||
import { CsoundEngine, type CsoundParameter } from './base/CsoundEngine';
|
||||
import type { PitchLock } from './base/SynthEngine';
|
||||
|
||||
interface Sub8Params {
|
||||
frequency: number;
|
||||
filterCutoff: number;
|
||||
filterEnvAmount: number;
|
||||
resonance: number;
|
||||
saturation: number;
|
||||
osc1Level: number;
|
||||
osc2Level: number;
|
||||
osc3Level: number;
|
||||
attack: number;
|
||||
decay: number;
|
||||
sustain: number;
|
||||
release: number;
|
||||
}
|
||||
|
||||
export class Sub8 extends CsoundEngine<Sub8Params> {
|
||||
getName(): string {
|
||||
return 'Sub8';
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return 'Multi-oscillator subtractive synth with diode ladder filter and saturation';
|
||||
}
|
||||
|
||||
getType(): 'generative' | 'sample' | 'input' {
|
||||
return 'generative';
|
||||
}
|
||||
|
||||
getCategory(): 'Additive' | 'Subtractive' | 'FM' | 'Percussion' | 'Noise' | 'Physical' | 'Modulation' | 'Experimental' | 'Utility' {
|
||||
return 'Subtractive';
|
||||
}
|
||||
|
||||
getOrchestra(): string {
|
||||
return `
|
||||
instr 1
|
||||
ifreq = chnget("frequency")
|
||||
iamp = 0.5
|
||||
ifilterCutoff = chnget("filterCutoff")
|
||||
ifilterEnvAmount = chnget("filterEnvAmount")
|
||||
ireso = chnget("resonance")
|
||||
isat = chnget("saturation")
|
||||
iosc1Level = chnget("osc1Level")
|
||||
iosc2Level = chnget("osc2Level")
|
||||
iosc3Level = chnget("osc3Level")
|
||||
iatt = chnget("attack") * p3
|
||||
idec = chnget("decay") * p3
|
||||
isus = chnget("sustain")
|
||||
irel = chnget("release") * p3
|
||||
|
||||
; Three detuned oscillators
|
||||
asig1 = vco2(iamp * iosc1Level, ifreq, 10)
|
||||
asig2 = vco2(iamp * iosc2Level, ifreq * 2, 10)
|
||||
asig3 = vco2(iamp * iosc3Level, ifreq * 3.5, 12)
|
||||
asig = asig1 + asig2 + asig3
|
||||
|
||||
; Saturation
|
||||
asig = tanh(asig * isat) / tanh(isat)
|
||||
|
||||
; Filter envelope
|
||||
aenv = madsr(iatt, idec, isus, irel)
|
||||
kcutoff = ifilterCutoff + (aenv * ifilterEnvAmount)
|
||||
kcutoff = limit(kcutoff, 20, 18000)
|
||||
|
||||
; Diode ladder filter
|
||||
asig = diode_ladder(asig, kcutoff, ireso)
|
||||
|
||||
; Final amplitude envelope
|
||||
asig = asig * aenv * 0.7
|
||||
|
||||
outs asig, asig
|
||||
endin
|
||||
`;
|
||||
}
|
||||
|
||||
getParametersForCsound(params: Sub8Params): CsoundParameter[] {
|
||||
return [
|
||||
{ channelName: 'frequency', value: params.frequency },
|
||||
{ channelName: 'filterCutoff', value: params.filterCutoff },
|
||||
{ channelName: 'filterEnvAmount', value: params.filterEnvAmount },
|
||||
{ channelName: 'resonance', value: params.resonance },
|
||||
{ channelName: 'saturation', value: params.saturation },
|
||||
{ channelName: 'osc1Level', value: params.osc1Level },
|
||||
{ channelName: 'osc2Level', value: params.osc2Level },
|
||||
{ channelName: 'osc3Level', value: params.osc3Level },
|
||||
{ channelName: 'attack', value: params.attack },
|
||||
{ channelName: 'decay', value: params.decay },
|
||||
{ channelName: 'sustain', value: params.sustain },
|
||||
{ channelName: 'release', value: params.release }
|
||||
];
|
||||
}
|
||||
|
||||
randomParams(pitchLock?: PitchLock): Sub8Params {
|
||||
const frequency = pitchLock?.enabled ? pitchLock.frequency : 55 * Math.pow(2, Math.random() * 5);
|
||||
|
||||
return {
|
||||
frequency,
|
||||
filterCutoff: 200 + Math.random() * 3800,
|
||||
filterEnvAmount: Math.random() * 6000,
|
||||
resonance: 0.5 + Math.random() * 14.5,
|
||||
saturation: 1 + Math.random() * 9,
|
||||
osc1Level: 0.5 + Math.random() * 0.5,
|
||||
osc2Level: Math.random() * 0.5,
|
||||
osc3Level: Math.random() * 0.3,
|
||||
attack: Math.random() * 0.1,
|
||||
decay: 0.05 + Math.random() * 0.3,
|
||||
sustain: 0.2 + Math.random() * 0.6,
|
||||
release: 0.05 + Math.random() * 0.4
|
||||
};
|
||||
}
|
||||
|
||||
mutateParams(params: Sub8Params, mutationAmount = 0.2, pitchLock?: PitchLock): Sub8Params {
|
||||
const mutate = (value: number, min: number, max: number) => {
|
||||
const change = (Math.random() - 0.5) * 2 * mutationAmount * (max - min);
|
||||
return Math.max(min, Math.min(max, value + change));
|
||||
};
|
||||
|
||||
return {
|
||||
frequency: pitchLock?.enabled ? pitchLock.frequency : mutate(params.frequency, 55, 55 * Math.pow(2, 5)),
|
||||
filterCutoff: mutate(params.filterCutoff, 200, 4000),
|
||||
filterEnvAmount: mutate(params.filterEnvAmount, 0, 6000),
|
||||
resonance: mutate(params.resonance, 0.5, 15),
|
||||
saturation: mutate(params.saturation, 1, 10),
|
||||
osc1Level: mutate(params.osc1Level, 0.5, 1),
|
||||
osc2Level: mutate(params.osc2Level, 0, 0.5),
|
||||
osc3Level: mutate(params.osc3Level, 0, 0.3),
|
||||
attack: mutate(params.attack, 0, 0.1),
|
||||
decay: mutate(params.decay, 0.05, 0.35),
|
||||
sustain: mutate(params.sustain, 0.2, 0.8),
|
||||
release: mutate(params.release, 0.05, 0.45)
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -50,6 +50,10 @@ export class SubtractiveThreeOsc extends CsoundEngine<SubtractiveThreeOscParams>
|
||||
return 'generative' as const;
|
||||
}
|
||||
|
||||
getCategory() {
|
||||
return 'Subtractive' as const;
|
||||
}
|
||||
|
||||
protected getOrchestra(): string {
|
||||
return `
|
||||
instr 1
|
||||
|
||||
187
src/lib/audio/engines/TechnoKick.ts
Normal file
187
src/lib/audio/engines/TechnoKick.ts
Normal file
@ -0,0 +1,187 @@
|
||||
import { CsoundEngine, type CsoundParameter } from './base/CsoundEngine';
|
||||
import type { PitchLock } from './base/SynthEngine';
|
||||
|
||||
interface TechnoKickParams {
|
||||
startFreq: number;
|
||||
endFreq: number;
|
||||
freqDecay: number;
|
||||
resonance: number;
|
||||
cutoffStart: number;
|
||||
cutoffEnd: number;
|
||||
cutoffDecay: number;
|
||||
ampAttack: number;
|
||||
ampDecay: number;
|
||||
noiseMix: number;
|
||||
punch: number;
|
||||
stereoWidth: number;
|
||||
}
|
||||
|
||||
export class TechnoKick extends CsoundEngine<TechnoKickParams> {
|
||||
getName(): string {
|
||||
return 'Techno Kick';
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return 'Noise through resonant low-pass filter with frequency sweep and RMS compression for punchy electronic kicks';
|
||||
}
|
||||
|
||||
getType() {
|
||||
return 'generative' as const;
|
||||
}
|
||||
|
||||
getCategory() {
|
||||
return 'Percussion' as const;
|
||||
}
|
||||
|
||||
protected getOrchestra(): string {
|
||||
return `
|
||||
instr 1
|
||||
iStartFreq chnget "startFreq"
|
||||
iEndFreq chnget "endFreq"
|
||||
iFreqDecay chnget "freqDecay"
|
||||
iResonance chnget "resonance"
|
||||
iCutoffStart chnget "cutoffStart"
|
||||
iCutoffEnd chnget "cutoffEnd"
|
||||
iCutoffDecay chnget "cutoffDecay"
|
||||
iAmpAttack chnget "ampAttack"
|
||||
iAmpDecay chnget "ampDecay"
|
||||
iNoiseMix chnget "noiseMix"
|
||||
iPunch chnget "punch"
|
||||
iStereoWidth chnget "stereoWidth"
|
||||
|
||||
idur = p3
|
||||
iFreqDecayTime = iFreqDecay * idur
|
||||
iCutoffDecayTime = iCutoffDecay * idur
|
||||
iAmpAttackTime = iAmpAttack * idur
|
||||
iAmpDecayTime = iAmpDecay * idur
|
||||
|
||||
; Generate random noise
|
||||
aNoise noise 1, 0
|
||||
|
||||
; Frequency envelope for the filter cutoff (exponential sweep)
|
||||
kFreqEnv expseg iStartFreq, iFreqDecayTime, iEndFreq, idur - iFreqDecayTime, iEndFreq
|
||||
|
||||
; Cutoff modulation envelope
|
||||
kCutoffEnv expseg iCutoffStart, iCutoffDecayTime, iCutoffEnd, idur - iCutoffDecayTime, iCutoffEnd
|
||||
|
||||
; Apply resonant low-pass filter (rezzy)
|
||||
aFiltered rezzy aNoise * (1 + iNoiseMix), kCutoffEnv, iResonance
|
||||
|
||||
; Add sine sub-bass component for more weight
|
||||
kSubFreqEnv expseg iStartFreq * 0.5, iFreqDecayTime, iEndFreq * 0.5, idur - iFreqDecayTime, iEndFreq * 0.5
|
||||
aSubBass oscili 0.6, kSubFreqEnv
|
||||
|
||||
; Mix filtered noise and sub-bass
|
||||
aMix = aFiltered * 0.5 + aSubBass
|
||||
|
||||
; Amplitude envelope (exponential)
|
||||
kAmpEnv expseg 0.001, iAmpAttackTime, 1, iAmpDecayTime, 0.001, idur - iAmpAttackTime - iAmpDecayTime, 0.001
|
||||
|
||||
; Apply amplitude envelope
|
||||
aEnveloped = aMix * kAmpEnv
|
||||
|
||||
; RMS compression for punch
|
||||
; Calculate RMS of signal
|
||||
kRMS rms aEnveloped
|
||||
if kRMS < 0.01 then
|
||||
kCompGain = 1
|
||||
else
|
||||
kCompGain = 1 + iPunch * (0.3 / kRMS - 1)
|
||||
endif
|
||||
kCompGain limit kCompGain, 0.5, 3
|
||||
|
||||
aOut = aEnveloped * kCompGain
|
||||
|
||||
; Right channel with stereo width
|
||||
iStartFreqR = iStartFreq * (1 + iStereoWidth * 0.01)
|
||||
iEndFreqR = iEndFreq * (1 + iStereoWidth * 0.01)
|
||||
|
||||
kFreqEnvR expseg iStartFreqR, iFreqDecayTime, iEndFreqR, idur - iFreqDecayTime, iEndFreqR
|
||||
kCutoffEnvR expseg iCutoffStart * (1 + iStereoWidth * 0.02), iCutoffDecayTime, iCutoffEnd * (1 + iStereoWidth * 0.02), idur - iCutoffDecayTime, iCutoffEnd * (1 + iStereoWidth * 0.02)
|
||||
|
||||
aNoiseR noise 1, 0
|
||||
aFilteredR rezzy aNoiseR * (1 + iNoiseMix), kCutoffEnvR, iResonance
|
||||
|
||||
kSubFreqEnvR expseg iStartFreqR * 0.5, iFreqDecayTime, iEndFreqR * 0.5, idur - iFreqDecayTime, iEndFreqR * 0.5
|
||||
aSubBassR oscili 0.6, kSubFreqEnvR
|
||||
|
||||
aMixR = aFilteredR * 0.5 + aSubBassR
|
||||
aEnvelopedR = aMixR * kAmpEnv
|
||||
|
||||
kRMSR rms aEnvelopedR
|
||||
if kRMSR < 0.01 then
|
||||
kCompGainR = 1
|
||||
else
|
||||
kCompGainR = 1 + iPunch * (0.3 / kRMSR - 1)
|
||||
endif
|
||||
kCompGainR limit kCompGainR, 0.5, 3
|
||||
|
||||
aOutR = aEnvelopedR * kCompGainR
|
||||
|
||||
outs aOut, aOutR
|
||||
endin
|
||||
`;
|
||||
}
|
||||
|
||||
protected getParametersForCsound(params: TechnoKickParams): CsoundParameter[] {
|
||||
return [
|
||||
{ channelName: 'startFreq', value: params.startFreq },
|
||||
{ channelName: 'endFreq', value: params.endFreq },
|
||||
{ channelName: 'freqDecay', value: params.freqDecay },
|
||||
{ channelName: 'resonance', value: params.resonance },
|
||||
{ channelName: 'cutoffStart', value: params.cutoffStart },
|
||||
{ channelName: 'cutoffEnd', value: params.cutoffEnd },
|
||||
{ channelName: 'cutoffDecay', value: params.cutoffDecay },
|
||||
{ channelName: 'ampAttack', value: params.ampAttack },
|
||||
{ channelName: 'ampDecay', value: params.ampDecay },
|
||||
{ channelName: 'noiseMix', value: params.noiseMix },
|
||||
{ channelName: 'punch', value: params.punch },
|
||||
{ channelName: 'stereoWidth', value: params.stereoWidth },
|
||||
];
|
||||
}
|
||||
|
||||
randomParams(pitchLock?: PitchLock): TechnoKickParams {
|
||||
const endFreqChoices = [40, 45, 50, 55, 60, 70, 80];
|
||||
const endFreq = pitchLock?.enabled
|
||||
? pitchLock.frequency
|
||||
: this.randomChoice(endFreqChoices) * this.randomRange(0.95, 1.05);
|
||||
|
||||
return {
|
||||
startFreq: this.randomRange(800, 1200),
|
||||
endFreq,
|
||||
freqDecay: this.randomRange(0.05, 0.25),
|
||||
resonance: this.randomRange(5, 40),
|
||||
cutoffStart: this.randomRange(300, 800),
|
||||
cutoffEnd: this.randomRange(80, 200),
|
||||
cutoffDecay: this.randomRange(0.1, 0.4),
|
||||
ampAttack: this.randomRange(0.001, 0.005),
|
||||
ampDecay: this.randomRange(0.2, 0.6),
|
||||
noiseMix: this.randomRange(0.1, 0.8),
|
||||
punch: this.randomRange(0.3, 0.9),
|
||||
stereoWidth: this.randomRange(0, 0.3),
|
||||
};
|
||||
}
|
||||
|
||||
mutateParams(
|
||||
params: TechnoKickParams,
|
||||
mutationAmount: number = 0.15,
|
||||
pitchLock?: PitchLock
|
||||
): TechnoKickParams {
|
||||
const endFreq = pitchLock?.enabled ? pitchLock.frequency : params.endFreq;
|
||||
|
||||
return {
|
||||
startFreq: this.mutateValue(params.startFreq, mutationAmount, 600, 1500),
|
||||
endFreq,
|
||||
freqDecay: this.mutateValue(params.freqDecay, mutationAmount, 0.02, 0.4),
|
||||
resonance: this.mutateValue(params.resonance, mutationAmount, 3, 50),
|
||||
cutoffStart: this.mutateValue(params.cutoffStart, mutationAmount, 200, 1000),
|
||||
cutoffEnd: this.mutateValue(params.cutoffEnd, mutationAmount, 60, 300),
|
||||
cutoffDecay: this.mutateValue(params.cutoffDecay, mutationAmount, 0.05, 0.6),
|
||||
ampAttack: this.mutateValue(params.ampAttack, mutationAmount, 0.001, 0.01),
|
||||
ampDecay: this.mutateValue(params.ampDecay, mutationAmount, 0.1, 0.8),
|
||||
noiseMix: this.mutateValue(params.noiseMix, mutationAmount, 0, 1),
|
||||
punch: this.mutateValue(params.punch, mutationAmount, 0.1, 1),
|
||||
stereoWidth: this.mutateValue(params.stereoWidth, mutationAmount, 0, 0.5),
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -73,6 +73,10 @@ export class TwoOpFM implements SynthEngine<TwoOpFMParams> {
|
||||
return 'generative' as const;
|
||||
}
|
||||
|
||||
getCategory() {
|
||||
return 'FM' as const;
|
||||
}
|
||||
|
||||
generate(params: TwoOpFMParams, sampleRate: number, duration: number): [Float32Array, Float32Array] {
|
||||
const numSamples = Math.floor(sampleRate * duration);
|
||||
const leftBuffer = new Float32Array(numSamples);
|
||||
|
||||
@ -38,6 +38,10 @@ export class ZzfxEngine implements SynthEngine<ZzfxParams> {
|
||||
return 'generative' as const;
|
||||
}
|
||||
|
||||
getCategory() {
|
||||
return 'Experimental' as const;
|
||||
}
|
||||
|
||||
generate(params: ZzfxParams, sampleRate: number, duration: number): [Float32Array, Float32Array] {
|
||||
// ZZFX uses 44100 sample rate internally
|
||||
const zzfxSampleRate = 44100;
|
||||
|
||||
@ -45,11 +45,17 @@ export abstract class CsoundEngine<T = any> implements SynthEngine<T> {
|
||||
|
||||
await csound.terminateInstance();
|
||||
|
||||
const leftChannel = new Float32Array(audioBuffer.leftChannel);
|
||||
const rightChannel = new Float32Array(audioBuffer.rightChannel);
|
||||
let leftChannel = new Float32Array(audioBuffer.leftChannel);
|
||||
let rightChannel = new Float32Array(audioBuffer.rightChannel);
|
||||
|
||||
this.removeDCOffset(leftChannel, rightChannel);
|
||||
|
||||
const trimmed = this.trimToZeroCrossing(leftChannel, rightChannel, sampleRate);
|
||||
leftChannel = trimmed.left;
|
||||
rightChannel = trimmed.right;
|
||||
|
||||
// Apply short fade-in to prevent click at start
|
||||
this.applyFadeIn(leftChannel, rightChannel, sampleRate);
|
||||
this.applyFadeOut(leftChannel, rightChannel, sampleRate);
|
||||
|
||||
const peak = this.findPeak(leftChannel, rightChannel);
|
||||
if (peak > 0.001) {
|
||||
@ -218,22 +224,95 @@ e
|
||||
}
|
||||
}
|
||||
|
||||
private removeDCOffset(leftChannel: Float32Array, rightChannel: Float32Array): void {
|
||||
let leftSum = 0;
|
||||
let rightSum = 0;
|
||||
const length = leftChannel.length;
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
leftSum += leftChannel[i];
|
||||
rightSum += rightChannel[i];
|
||||
}
|
||||
|
||||
const leftDC = leftSum / length;
|
||||
const rightDC = rightSum / length;
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
leftChannel[i] -= leftDC;
|
||||
rightChannel[i] -= rightDC;
|
||||
}
|
||||
}
|
||||
|
||||
private trimToZeroCrossing(
|
||||
leftChannel: Float32Array,
|
||||
rightChannel: Float32Array,
|
||||
sampleRate: number
|
||||
): { left: Float32Array; right: Float32Array } {
|
||||
const maxSearchSamples = Math.min(Math.floor(sampleRate * 0.01), leftChannel.length);
|
||||
let trimIndex = 0;
|
||||
|
||||
for (let i = 1; i < maxSearchSamples; i++) {
|
||||
const prevL = leftChannel[i - 1];
|
||||
const currL = leftChannel[i];
|
||||
const prevR = rightChannel[i - 1];
|
||||
const currR = rightChannel[i];
|
||||
|
||||
if (
|
||||
(prevL <= 0 && currL >= 0) || (prevL >= 0 && currL <= 0) ||
|
||||
(prevR <= 0 && currR >= 0) || (prevR >= 0 && currR <= 0)
|
||||
) {
|
||||
trimIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (trimIndex > 0) {
|
||||
const newLeft = new Float32Array(leftChannel.length - trimIndex);
|
||||
const newRight = new Float32Array(rightChannel.length - trimIndex);
|
||||
newLeft.set(leftChannel.subarray(trimIndex));
|
||||
newRight.set(rightChannel.subarray(trimIndex));
|
||||
return { left: newLeft, right: newRight };
|
||||
}
|
||||
|
||||
return { left: leftChannel, right: rightChannel };
|
||||
}
|
||||
|
||||
private applyFadeIn(
|
||||
leftChannel: Float32Array,
|
||||
rightChannel: Float32Array,
|
||||
sampleRate: number
|
||||
): void {
|
||||
const fadeInMs = 5; // 5ms fade-in to prevent clicks
|
||||
const fadeInMs = 5;
|
||||
const fadeSamples = Math.floor((fadeInMs / 1000) * sampleRate);
|
||||
const actualFadeSamples = Math.min(fadeSamples, leftChannel.length);
|
||||
|
||||
for (let i = 0; i < actualFadeSamples; i++) {
|
||||
const gain = i / actualFadeSamples;
|
||||
const phase = i / actualFadeSamples;
|
||||
const gain = 0.5 - 0.5 * Math.cos(phase * Math.PI);
|
||||
leftChannel[i] *= gain;
|
||||
rightChannel[i] *= gain;
|
||||
}
|
||||
}
|
||||
|
||||
private applyFadeOut(
|
||||
leftChannel: Float32Array,
|
||||
rightChannel: Float32Array,
|
||||
sampleRate: number
|
||||
): void {
|
||||
const fadeOutMs = 5;
|
||||
const fadeSamples = Math.floor((fadeOutMs / 1000) * sampleRate);
|
||||
const actualFadeSamples = Math.min(fadeSamples, leftChannel.length);
|
||||
const startSample = leftChannel.length - actualFadeSamples;
|
||||
|
||||
for (let i = 0; i < actualFadeSamples; i++) {
|
||||
const sampleIndex = startSample + i;
|
||||
const phase = i / actualFadeSamples;
|
||||
const gain = 0.5 + 0.5 * Math.cos(phase * Math.PI);
|
||||
leftChannel[sampleIndex] *= gain;
|
||||
rightChannel[sampleIndex] *= gain;
|
||||
}
|
||||
}
|
||||
|
||||
protected randomRange(min: number, max: number): number {
|
||||
return min + Math.random() * (max - min);
|
||||
}
|
||||
|
||||
@ -6,6 +6,17 @@
|
||||
|
||||
export type EngineType = 'generative' | 'sample' | 'input';
|
||||
|
||||
export type EngineCategory =
|
||||
| 'Additive'
|
||||
| 'Subtractive'
|
||||
| 'FM'
|
||||
| 'Percussion'
|
||||
| 'Noise'
|
||||
| 'Physical'
|
||||
| 'Modulation'
|
||||
| 'Experimental'
|
||||
| 'Utility';
|
||||
|
||||
export interface PitchLock {
|
||||
enabled: boolean;
|
||||
frequency: number; // Frequency in Hz
|
||||
@ -15,6 +26,7 @@ export interface SynthEngine<T = any> {
|
||||
getName(): string;
|
||||
getDescription(): string;
|
||||
getType(): EngineType;
|
||||
getCategory(): EngineCategory;
|
||||
generate(params: T, sampleRate: number, duration: number, pitchLock?: PitchLock): [Float32Array, Float32Array] | Promise<[Float32Array, Float32Array]>;
|
||||
randomParams(pitchLock?: PitchLock): T;
|
||||
mutateParams(params: T, mutationAmount?: number, pitchLock?: PitchLock): T;
|
||||
|
||||
@ -2,6 +2,7 @@ import type { SynthEngine } from './base/SynthEngine';
|
||||
import { FourOpFM } from './FourOpFM';
|
||||
import { TwoOpFM } from './TwoOpFM';
|
||||
import { PhaseDistortionFM } from './PhaseDistortionFM';
|
||||
import { FormantFM } from './FormantFM';
|
||||
import { DubSiren } from './DubSiren';
|
||||
import { Benjolin } from './Benjolin';
|
||||
import { ZzfxEngine } from './ZzfxEngine';
|
||||
@ -17,6 +18,19 @@ import { HiHat } from './HiHat';
|
||||
import { ParticleNoise } from './ParticleNoise';
|
||||
import { DustNoise } from './DustNoise';
|
||||
import { SubtractiveThreeOsc } from './SubtractiveThreeOsc';
|
||||
import { MassiveAdditive } from './MassiveAdditive';
|
||||
import { FormantPopDrum } from './FormantPopDrum';
|
||||
import { TechnoKick } from './TechnoKick';
|
||||
import { FMTomTom } from './FMTomTom';
|
||||
import { RingCymbal } from './RingCymbal';
|
||||
import { AdditiveBass } from './AdditiveBass';
|
||||
import { FeedbackSnare } from './FeedbackSnare';
|
||||
import { CombResonator } from './CombResonator';
|
||||
import { LaserSweep } from './LaserSweep';
|
||||
import { Dripwater } from './Dripwater';
|
||||
import { Sub8 } from './Sub8';
|
||||
import { Form1 } from './Form1';
|
||||
import { Squine1 } from './Squine1';
|
||||
|
||||
export const engines: SynthEngine[] = [
|
||||
new Sample(),
|
||||
@ -24,6 +38,7 @@ export const engines: SynthEngine[] = [
|
||||
new FourOpFM(),
|
||||
new TwoOpFM(),
|
||||
new PhaseDistortionFM(),
|
||||
new FormantFM(),
|
||||
new DubSiren(),
|
||||
new Benjolin(),
|
||||
new ZzfxEngine(),
|
||||
@ -31,10 +46,23 @@ export const engines: SynthEngine[] = [
|
||||
new Snare(),
|
||||
new BassDrum(),
|
||||
new HiHat(),
|
||||
new FormantPopDrum(),
|
||||
new TechnoKick(),
|
||||
new FMTomTom(),
|
||||
new RingCymbal(),
|
||||
new AdditiveBass(),
|
||||
new FeedbackSnare(),
|
||||
new Ring(),
|
||||
new KarplusStrong(),
|
||||
new AdditiveEngine(),
|
||||
new ParticleNoise(),
|
||||
new DustNoise(),
|
||||
new SubtractiveThreeOsc(),
|
||||
new CombResonator(),
|
||||
new MassiveAdditive(),
|
||||
new LaserSweep(),
|
||||
new Dripwater(),
|
||||
new Sub8(),
|
||||
new Form1(),
|
||||
new Squine1(),
|
||||
];
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
export type ProcessorCategory = 'Amplitude' | 'Filter' | 'Time' | 'Space' | 'Pitch' | 'Modulation' | 'Distortion' | 'Spectral' | 'Utility';
|
||||
|
||||
export interface AudioProcessor {
|
||||
getName(): string;
|
||||
getDescription(): string;
|
||||
getCategory(): ProcessorCategory;
|
||||
process(
|
||||
leftChannel: Float32Array,
|
||||
rightChannel: Float32Array
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { AudioProcessor } from "./AudioProcessor";
|
||||
import type { AudioProcessor, ProcessorCategory } from "./AudioProcessor";
|
||||
|
||||
export class BitCrusher implements AudioProcessor {
|
||||
getName(): string {
|
||||
@ -9,6 +9,10 @@ export class BitCrusher implements AudioProcessor {
|
||||
return "Reduces bit depth for lo-fi digital distortion";
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Distortion';
|
||||
}
|
||||
|
||||
async process(
|
||||
leftChannel: Float32Array,
|
||||
rightChannel: Float32Array
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { AudioProcessor } from './AudioProcessor';
|
||||
import type { AudioProcessor, ProcessorCategory } from './AudioProcessor';
|
||||
|
||||
export class Chorus implements AudioProcessor {
|
||||
private readonly sampleRate = 44100;
|
||||
@ -11,6 +11,10 @@ export class Chorus implements AudioProcessor {
|
||||
return 'Multiple delayed copies with pitch modulation for thick, ensemble sounds';
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Time';
|
||||
}
|
||||
|
||||
process(
|
||||
leftChannel: Float32Array,
|
||||
rightChannel: Float32Array
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { AudioProcessor } from "./AudioProcessor";
|
||||
import type { AudioProcessor, ProcessorCategory } from "./AudioProcessor";
|
||||
|
||||
export class Compressor implements AudioProcessor {
|
||||
getName(): string {
|
||||
@ -9,6 +9,10 @@ export class Compressor implements AudioProcessor {
|
||||
return "Reduces dynamic range by taming peaks with makeup gain";
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Amplitude';
|
||||
}
|
||||
|
||||
async process(
|
||||
leftChannel: Float32Array,
|
||||
rightChannel: Float32Array
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { AudioProcessor } from './AudioProcessor';
|
||||
import type { AudioProcessor, ProcessorCategory } from './AudioProcessor';
|
||||
|
||||
type RoomType = 'small' | 'medium' | 'large' | 'hall' | 'plate' | 'chamber';
|
||||
|
||||
@ -22,6 +22,10 @@ export class ConvolutionReverb implements AudioProcessor {
|
||||
return 'Realistic room ambience using Web Audio ConvolverNode with synthetic impulse responses';
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Space';
|
||||
}
|
||||
|
||||
async process(
|
||||
leftChannel: Float32Array,
|
||||
rightChannel: Float32Array
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { AudioProcessor } from "./AudioProcessor";
|
||||
import type { AudioProcessor, ProcessorCategory } from "./AudioProcessor";
|
||||
|
||||
export class DCOffsetRemover implements AudioProcessor {
|
||||
getName(): string {
|
||||
@ -9,6 +9,10 @@ export class DCOffsetRemover implements AudioProcessor {
|
||||
return "Removes DC offset bias from the audio signal";
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Utility';
|
||||
}
|
||||
|
||||
async process(
|
||||
leftChannel: Float32Array,
|
||||
rightChannel: Float32Array
|
||||
|
||||
31
src/lib/audio/processors/ExpFadeIn.ts
Normal file
31
src/lib/audio/processors/ExpFadeIn.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import type { AudioProcessor, ProcessorCategory } from './AudioProcessor';
|
||||
|
||||
export class ExpFadeIn implements AudioProcessor {
|
||||
getName(): string {
|
||||
return 'Fade In (Exp)';
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return 'Applies an exponential fade from silence to current level';
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Amplitude';
|
||||
}
|
||||
|
||||
process(leftIn: Float32Array, rightIn: Float32Array): [Float32Array, Float32Array] {
|
||||
const length = leftIn.length;
|
||||
|
||||
const leftOut = new Float32Array(length);
|
||||
const rightOut = new Float32Array(length);
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
const t = i / length;
|
||||
const gain = 1.0 - Math.exp(-5.0 * t);
|
||||
leftOut[i] = leftIn[i] * gain;
|
||||
rightOut[i] = rightIn[i] * gain;
|
||||
}
|
||||
|
||||
return [leftOut, rightOut];
|
||||
}
|
||||
}
|
||||
31
src/lib/audio/processors/ExpFadeOut.ts
Normal file
31
src/lib/audio/processors/ExpFadeOut.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import type { AudioProcessor, ProcessorCategory } from './AudioProcessor';
|
||||
|
||||
export class ExpFadeOut implements AudioProcessor {
|
||||
getName(): string {
|
||||
return 'Fade Out (Exp)';
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return 'Applies an exponential fade from current level to silence';
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Amplitude';
|
||||
}
|
||||
|
||||
process(leftIn: Float32Array, rightIn: Float32Array): [Float32Array, Float32Array] {
|
||||
const length = leftIn.length;
|
||||
|
||||
const leftOut = new Float32Array(length);
|
||||
const rightOut = new Float32Array(length);
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
const t = i / length;
|
||||
const gain = Math.exp(-5.0 * t);
|
||||
leftOut[i] = leftIn[i] * gain;
|
||||
rightOut[i] = rightIn[i] * gain;
|
||||
}
|
||||
|
||||
return [leftOut, rightOut];
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import type { AudioProcessor } from "./AudioProcessor";
|
||||
import type { AudioProcessor, ProcessorCategory } from "./AudioProcessor";
|
||||
|
||||
export class HaasEffect implements AudioProcessor {
|
||||
getName(): string {
|
||||
@ -9,6 +9,10 @@ export class HaasEffect implements AudioProcessor {
|
||||
return "Creates stereo width with micro-delay (precedence effect)";
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Space';
|
||||
}
|
||||
|
||||
async process(
|
||||
leftChannel: Float32Array,
|
||||
rightChannel: Float32Array
|
||||
|
||||
65
src/lib/audio/processors/HighPassSweepDown.ts
Normal file
65
src/lib/audio/processors/HighPassSweepDown.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import type { AudioProcessor, ProcessorCategory } from './AudioProcessor';
|
||||
|
||||
export class HighPassSweepDown implements AudioProcessor {
|
||||
getName(): string {
|
||||
return 'HP Sweep Down';
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return 'Sweeps a high-pass filter from thin to full';
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Filter';
|
||||
}
|
||||
|
||||
process(leftIn: Float32Array, rightIn: Float32Array): [Float32Array, Float32Array] {
|
||||
const length = leftIn.length;
|
||||
const leftOut = new Float32Array(length);
|
||||
const rightOut = new Float32Array(length);
|
||||
|
||||
const sampleRate = 44100;
|
||||
const startFreq = 8000;
|
||||
const endFreq = 20;
|
||||
const Q = 1.5;
|
||||
|
||||
let lx1 = 0, lx2 = 0, ly1 = 0, ly2 = 0;
|
||||
let rx1 = 0, rx2 = 0, ry1 = 0, ry2 = 0;
|
||||
|
||||
let b0 = 0, b1 = 0, b2 = 0, a1 = 0, a2 = 0;
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
if (i % 64 === 0) {
|
||||
const t = i / length;
|
||||
const freq = startFreq * Math.pow(endFreq / startFreq, t);
|
||||
const omega = 2.0 * Math.PI * freq / sampleRate;
|
||||
const alpha = Math.sin(omega) / (2.0 * Q);
|
||||
|
||||
const a0 = 1.0 + alpha;
|
||||
b0 = ((1.0 + Math.cos(omega)) / 2.0) / a0;
|
||||
b1 = (-(1.0 + Math.cos(omega))) / a0;
|
||||
b2 = b0;
|
||||
a1 = (-2.0 * Math.cos(omega)) / a0;
|
||||
a2 = (1.0 - alpha) / a0;
|
||||
}
|
||||
|
||||
const lx0 = leftIn[i];
|
||||
const rx0 = rightIn[i];
|
||||
|
||||
leftOut[i] = b0 * lx0 + b1 * lx1 + b2 * lx2 - a1 * ly1 - a2 * ly2;
|
||||
rightOut[i] = b0 * rx0 + b1 * rx1 + b2 * rx2 - a1 * ry1 - a2 * ry2;
|
||||
|
||||
lx2 = lx1;
|
||||
lx1 = lx0;
|
||||
ly2 = ly1;
|
||||
ly1 = leftOut[i];
|
||||
|
||||
rx2 = rx1;
|
||||
rx1 = rx0;
|
||||
ry2 = ry1;
|
||||
ry1 = rightOut[i];
|
||||
}
|
||||
|
||||
return [leftOut, rightOut];
|
||||
}
|
||||
}
|
||||
65
src/lib/audio/processors/HighPassSweepUp.ts
Normal file
65
src/lib/audio/processors/HighPassSweepUp.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import type { AudioProcessor, ProcessorCategory } from './AudioProcessor';
|
||||
|
||||
export class HighPassSweepUp implements AudioProcessor {
|
||||
getName(): string {
|
||||
return 'HP Sweep Up';
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return 'Sweeps a high-pass filter from full to thin';
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Filter';
|
||||
}
|
||||
|
||||
process(leftIn: Float32Array, rightIn: Float32Array): [Float32Array, Float32Array] {
|
||||
const length = leftIn.length;
|
||||
const leftOut = new Float32Array(length);
|
||||
const rightOut = new Float32Array(length);
|
||||
|
||||
const sampleRate = 44100;
|
||||
const startFreq = 20;
|
||||
const endFreq = 8000;
|
||||
const Q = 1.5;
|
||||
|
||||
let lx1 = 0, lx2 = 0, ly1 = 0, ly2 = 0;
|
||||
let rx1 = 0, rx2 = 0, ry1 = 0, ry2 = 0;
|
||||
|
||||
let b0 = 0, b1 = 0, b2 = 0, a1 = 0, a2 = 0;
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
if (i % 64 === 0) {
|
||||
const t = i / length;
|
||||
const freq = startFreq * Math.pow(endFreq / startFreq, t);
|
||||
const omega = 2.0 * Math.PI * freq / sampleRate;
|
||||
const alpha = Math.sin(omega) / (2.0 * Q);
|
||||
|
||||
const a0 = 1.0 + alpha;
|
||||
b0 = ((1.0 + Math.cos(omega)) / 2.0) / a0;
|
||||
b1 = (-(1.0 + Math.cos(omega))) / a0;
|
||||
b2 = b0;
|
||||
a1 = (-2.0 * Math.cos(omega)) / a0;
|
||||
a2 = (1.0 - alpha) / a0;
|
||||
}
|
||||
|
||||
const lx0 = leftIn[i];
|
||||
const rx0 = rightIn[i];
|
||||
|
||||
leftOut[i] = b0 * lx0 + b1 * lx1 + b2 * lx2 - a1 * ly1 - a2 * ly2;
|
||||
rightOut[i] = b0 * rx0 + b1 * rx1 + b2 * rx2 - a1 * ry1 - a2 * ry2;
|
||||
|
||||
lx2 = lx1;
|
||||
lx1 = lx0;
|
||||
ly2 = ly1;
|
||||
ly1 = leftOut[i];
|
||||
|
||||
rx2 = rx1;
|
||||
rx1 = rx0;
|
||||
ry2 = ry1;
|
||||
ry1 = rightOut[i];
|
||||
}
|
||||
|
||||
return [leftOut, rightOut];
|
||||
}
|
||||
}
|
||||
30
src/lib/audio/processors/LinearFadeIn.ts
Normal file
30
src/lib/audio/processors/LinearFadeIn.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import type { AudioProcessor, ProcessorCategory } from './AudioProcessor';
|
||||
|
||||
export class LinearFadeIn implements AudioProcessor {
|
||||
getName(): string {
|
||||
return 'Fade In';
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return 'Applies a linear fade from silence to current level';
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Amplitude';
|
||||
}
|
||||
|
||||
process(leftIn: Float32Array, rightIn: Float32Array): [Float32Array, Float32Array] {
|
||||
const length = leftIn.length;
|
||||
|
||||
const leftOut = new Float32Array(length);
|
||||
const rightOut = new Float32Array(length);
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
const gain = i / length;
|
||||
leftOut[i] = leftIn[i] * gain;
|
||||
rightOut[i] = rightIn[i] * gain;
|
||||
}
|
||||
|
||||
return [leftOut, rightOut];
|
||||
}
|
||||
}
|
||||
30
src/lib/audio/processors/LinearFadeOut.ts
Normal file
30
src/lib/audio/processors/LinearFadeOut.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import type { AudioProcessor, ProcessorCategory } from './AudioProcessor';
|
||||
|
||||
export class LinearFadeOut implements AudioProcessor {
|
||||
getName(): string {
|
||||
return 'Fade Out';
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return 'Applies a linear fade from current level to silence';
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Amplitude';
|
||||
}
|
||||
|
||||
process(leftIn: Float32Array, rightIn: Float32Array): [Float32Array, Float32Array] {
|
||||
const length = leftIn.length;
|
||||
|
||||
const leftOut = new Float32Array(length);
|
||||
const rightOut = new Float32Array(length);
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
const gain = 1.0 - i / length;
|
||||
leftOut[i] = leftIn[i] * gain;
|
||||
rightOut[i] = rightIn[i] * gain;
|
||||
}
|
||||
|
||||
return [leftOut, rightOut];
|
||||
}
|
||||
}
|
||||
65
src/lib/audio/processors/LowPassSweepDown.ts
Normal file
65
src/lib/audio/processors/LowPassSweepDown.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import type { AudioProcessor, ProcessorCategory } from './AudioProcessor';
|
||||
|
||||
export class LowPassSweepDown implements AudioProcessor {
|
||||
getName(): string {
|
||||
return 'LP Sweep Down';
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return 'Sweeps a low-pass filter from bright to dark';
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Filter';
|
||||
}
|
||||
|
||||
process(leftIn: Float32Array, rightIn: Float32Array): [Float32Array, Float32Array] {
|
||||
const length = leftIn.length;
|
||||
const leftOut = new Float32Array(length);
|
||||
const rightOut = new Float32Array(length);
|
||||
|
||||
const sampleRate = 44100;
|
||||
const startFreq = 18000;
|
||||
const endFreq = 100;
|
||||
const Q = 1.5;
|
||||
|
||||
let lx1 = 0, lx2 = 0, ly1 = 0, ly2 = 0;
|
||||
let rx1 = 0, rx2 = 0, ry1 = 0, ry2 = 0;
|
||||
|
||||
let b0 = 0, b1 = 0, b2 = 0, a1 = 0, a2 = 0;
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
if (i % 64 === 0) {
|
||||
const t = i / length;
|
||||
const freq = startFreq * Math.pow(endFreq / startFreq, t);
|
||||
const omega = 2.0 * Math.PI * freq / sampleRate;
|
||||
const alpha = Math.sin(omega) / (2.0 * Q);
|
||||
|
||||
const a0 = 1.0 + alpha;
|
||||
b0 = ((1.0 - Math.cos(omega)) / 2.0) / a0;
|
||||
b1 = (1.0 - Math.cos(omega)) / a0;
|
||||
b2 = b0;
|
||||
a1 = (-2.0 * Math.cos(omega)) / a0;
|
||||
a2 = (1.0 - alpha) / a0;
|
||||
}
|
||||
|
||||
const lx0 = leftIn[i];
|
||||
const rx0 = rightIn[i];
|
||||
|
||||
leftOut[i] = b0 * lx0 + b1 * lx1 + b2 * lx2 - a1 * ly1 - a2 * ly2;
|
||||
rightOut[i] = b0 * rx0 + b1 * rx1 + b2 * rx2 - a1 * ry1 - a2 * ry2;
|
||||
|
||||
lx2 = lx1;
|
||||
lx1 = lx0;
|
||||
ly2 = ly1;
|
||||
ly1 = leftOut[i];
|
||||
|
||||
rx2 = rx1;
|
||||
rx1 = rx0;
|
||||
ry2 = ry1;
|
||||
ry1 = rightOut[i];
|
||||
}
|
||||
|
||||
return [leftOut, rightOut];
|
||||
}
|
||||
}
|
||||
65
src/lib/audio/processors/LowPassSweepUp.ts
Normal file
65
src/lib/audio/processors/LowPassSweepUp.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import type { AudioProcessor, ProcessorCategory } from './AudioProcessor';
|
||||
|
||||
export class LowPassSweepUp implements AudioProcessor {
|
||||
getName(): string {
|
||||
return 'LP Sweep Up';
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return 'Sweeps a low-pass filter from dark to bright';
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Filter';
|
||||
}
|
||||
|
||||
process(leftIn: Float32Array, rightIn: Float32Array): [Float32Array, Float32Array] {
|
||||
const length = leftIn.length;
|
||||
const leftOut = new Float32Array(length);
|
||||
const rightOut = new Float32Array(length);
|
||||
|
||||
const sampleRate = 44100;
|
||||
const startFreq = 100;
|
||||
const endFreq = 18000;
|
||||
const Q = 1.5;
|
||||
|
||||
let lx1 = 0, lx2 = 0, ly1 = 0, ly2 = 0;
|
||||
let rx1 = 0, rx2 = 0, ry1 = 0, ry2 = 0;
|
||||
|
||||
let b0 = 0, b1 = 0, b2 = 0, a1 = 0, a2 = 0;
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
if (i % 64 === 0) {
|
||||
const t = i / length;
|
||||
const freq = startFreq * Math.pow(endFreq / startFreq, t);
|
||||
const omega = 2.0 * Math.PI * freq / sampleRate;
|
||||
const alpha = Math.sin(omega) / (2.0 * Q);
|
||||
|
||||
const a0 = 1.0 + alpha;
|
||||
b0 = ((1.0 - Math.cos(omega)) / 2.0) / a0;
|
||||
b1 = (1.0 - Math.cos(omega)) / a0;
|
||||
b2 = b0;
|
||||
a1 = (-2.0 * Math.cos(omega)) / a0;
|
||||
a2 = (1.0 - alpha) / a0;
|
||||
}
|
||||
|
||||
const lx0 = leftIn[i];
|
||||
const rx0 = rightIn[i];
|
||||
|
||||
leftOut[i] = b0 * lx0 + b1 * lx1 + b2 * lx2 - a1 * ly1 - a2 * ly2;
|
||||
rightOut[i] = b0 * rx0 + b1 * rx1 + b2 * rx2 - a1 * ry1 - a2 * ry2;
|
||||
|
||||
lx2 = lx1;
|
||||
lx1 = lx0;
|
||||
ly2 = ly1;
|
||||
ly1 = leftOut[i];
|
||||
|
||||
rx2 = rx1;
|
||||
rx1 = rx0;
|
||||
ry2 = ry1;
|
||||
ry1 = rightOut[i];
|
||||
}
|
||||
|
||||
return [leftOut, rightOut];
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import type { AudioProcessor } from './AudioProcessor';
|
||||
import type { AudioProcessor, ProcessorCategory } from './AudioProcessor';
|
||||
|
||||
export class MicroPitch implements AudioProcessor {
|
||||
getName(): string {
|
||||
@ -9,6 +9,10 @@ export class MicroPitch implements AudioProcessor {
|
||||
return 'Applies subtle random pitch variations for analog warmth and character';
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Pitch';
|
||||
}
|
||||
|
||||
process(
|
||||
leftChannel: Float32Array,
|
||||
rightChannel: Float32Array
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { AudioProcessor } from "./AudioProcessor";
|
||||
import type { AudioProcessor, ProcessorCategory } from "./AudioProcessor";
|
||||
|
||||
export class Normalize implements AudioProcessor {
|
||||
getName(): string {
|
||||
@ -9,6 +9,10 @@ export class Normalize implements AudioProcessor {
|
||||
return "Normalizes audio to maximum amplitude without clipping";
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Amplitude';
|
||||
}
|
||||
|
||||
async process(
|
||||
leftChannel: Float32Array,
|
||||
rightChannel: Float32Array
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { AudioProcessor } from "./AudioProcessor";
|
||||
import type { AudioProcessor, ProcessorCategory } from "./AudioProcessor";
|
||||
|
||||
export class OctaveDown implements AudioProcessor {
|
||||
getName(): string {
|
||||
@ -9,6 +9,10 @@ export class OctaveDown implements AudioProcessor {
|
||||
return "Shifts pitch down one octave by halving playback rate";
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Pitch';
|
||||
}
|
||||
|
||||
async process(
|
||||
leftChannel: Float32Array,
|
||||
rightChannel: Float32Array
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { AudioProcessor } from "./AudioProcessor";
|
||||
import type { AudioProcessor, ProcessorCategory } from "./AudioProcessor";
|
||||
|
||||
export class OctaveUp implements AudioProcessor {
|
||||
getName(): string {
|
||||
@ -9,6 +9,10 @@ export class OctaveUp implements AudioProcessor {
|
||||
return "Shifts pitch up one octave by doubling playback rate";
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Pitch';
|
||||
}
|
||||
|
||||
async process(
|
||||
leftChannel: Float32Array,
|
||||
rightChannel: Float32Array
|
||||
|
||||
34
src/lib/audio/processors/PanLeftToRight.ts
Normal file
34
src/lib/audio/processors/PanLeftToRight.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import type { AudioProcessor, ProcessorCategory } from './AudioProcessor';
|
||||
|
||||
export class PanLeftToRight implements AudioProcessor {
|
||||
getName(): string {
|
||||
return 'Pan L→R';
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return 'Gradually pans the sound from left to right';
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Space';
|
||||
}
|
||||
|
||||
process(leftIn: Float32Array, rightIn: Float32Array): [Float32Array, Float32Array] {
|
||||
const length = leftIn.length;
|
||||
const leftOut = new Float32Array(length);
|
||||
const rightOut = new Float32Array(length);
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
const t = i / length;
|
||||
const panAngle = t * Math.PI * 0.5;
|
||||
const leftGain = Math.cos(panAngle);
|
||||
const rightGain = Math.sin(panAngle);
|
||||
|
||||
const mono = (leftIn[i] + rightIn[i]) * 0.5;
|
||||
leftOut[i] = mono * leftGain;
|
||||
rightOut[i] = mono * rightGain;
|
||||
}
|
||||
|
||||
return [leftOut, rightOut];
|
||||
}
|
||||
}
|
||||
34
src/lib/audio/processors/PanRightToLeft.ts
Normal file
34
src/lib/audio/processors/PanRightToLeft.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import type { AudioProcessor, ProcessorCategory } from './AudioProcessor';
|
||||
|
||||
export class PanRightToLeft implements AudioProcessor {
|
||||
getName(): string {
|
||||
return 'Pan R→L';
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return 'Gradually pans the sound from right to left';
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Space';
|
||||
}
|
||||
|
||||
process(leftIn: Float32Array, rightIn: Float32Array): [Float32Array, Float32Array] {
|
||||
const length = leftIn.length;
|
||||
const leftOut = new Float32Array(length);
|
||||
const rightOut = new Float32Array(length);
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
const t = i / length;
|
||||
const panAngle = (1.0 - t) * Math.PI * 0.5;
|
||||
const leftGain = Math.cos(panAngle);
|
||||
const rightGain = Math.sin(panAngle);
|
||||
|
||||
const mono = (leftIn[i] + rightIn[i]) * 0.5;
|
||||
leftOut[i] = mono * leftGain;
|
||||
rightOut[i] = mono * rightGain;
|
||||
}
|
||||
|
||||
return [leftOut, rightOut];
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import type { AudioProcessor } from './AudioProcessor';
|
||||
import type { AudioProcessor, ProcessorCategory } from './AudioProcessor';
|
||||
|
||||
export class PanShuffler implements AudioProcessor {
|
||||
getName(): string {
|
||||
@ -9,6 +9,10 @@ export class PanShuffler implements AudioProcessor {
|
||||
return 'Smoothly pans segments across the stereo field';
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Space';
|
||||
}
|
||||
|
||||
process(
|
||||
leftChannel: Float32Array,
|
||||
rightChannel: Float32Array
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { AudioProcessor } from "./AudioProcessor";
|
||||
import type { AudioProcessor, ProcessorCategory } from "./AudioProcessor";
|
||||
|
||||
export class PhaseInverter implements AudioProcessor {
|
||||
getName(): string {
|
||||
@ -9,6 +9,10 @@ export class PhaseInverter implements AudioProcessor {
|
||||
return "Inverts polarity of one or both channels";
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Utility';
|
||||
}
|
||||
|
||||
async process(
|
||||
leftChannel: Float32Array,
|
||||
rightChannel: Float32Array
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { AudioProcessor } from './AudioProcessor';
|
||||
import type { AudioProcessor, ProcessorCategory } from './AudioProcessor';
|
||||
|
||||
export class Phaser implements AudioProcessor {
|
||||
private readonly sampleRate = 44100;
|
||||
@ -11,6 +11,10 @@ export class Phaser implements AudioProcessor {
|
||||
return 'Classic phaser effect with sweeping all-pass filters for swirling, spacey sounds';
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Filter';
|
||||
}
|
||||
|
||||
process(
|
||||
leftChannel: Float32Array,
|
||||
rightChannel: Float32Array
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { AudioProcessor } from './AudioProcessor';
|
||||
import type { AudioProcessor, ProcessorCategory } from './AudioProcessor';
|
||||
|
||||
export class PitchShifter implements AudioProcessor {
|
||||
getName(): string {
|
||||
@ -9,6 +9,10 @@ export class PitchShifter implements AudioProcessor {
|
||||
return 'Transposes audio up or down in semitones without changing duration';
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Pitch';
|
||||
}
|
||||
|
||||
process(
|
||||
leftChannel: Float32Array,
|
||||
rightChannel: Float32Array
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { AudioProcessor } from './AudioProcessor';
|
||||
import type { AudioProcessor, ProcessorCategory } from './AudioProcessor';
|
||||
|
||||
export class PitchWobble implements AudioProcessor {
|
||||
getName(): string {
|
||||
@ -9,6 +9,10 @@ export class PitchWobble implements AudioProcessor {
|
||||
return 'Variable-rate playback with LFO modulation for tape wow/vibrato effects';
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Pitch';
|
||||
}
|
||||
|
||||
process(
|
||||
leftChannel: Float32Array,
|
||||
rightChannel: Float32Array
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { AudioProcessor } from './AudioProcessor';
|
||||
import type { AudioProcessor, ProcessorCategory } from './AudioProcessor';
|
||||
|
||||
export class Resonator implements AudioProcessor {
|
||||
private readonly sampleRate = 44100;
|
||||
@ -11,6 +11,10 @@ export class Resonator implements AudioProcessor {
|
||||
return 'Multi-band resonant filter bank that adds tonal character through resonance';
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Filter';
|
||||
}
|
||||
|
||||
process(
|
||||
leftChannel: Float32Array,
|
||||
rightChannel: Float32Array
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { AudioProcessor } from './AudioProcessor';
|
||||
import type { AudioProcessor, ProcessorCategory } from './AudioProcessor';
|
||||
|
||||
export class Reverser implements AudioProcessor {
|
||||
getName(): string {
|
||||
@ -9,6 +9,10 @@ export class Reverser implements AudioProcessor {
|
||||
return 'Plays the sound backwards';
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Time';
|
||||
}
|
||||
|
||||
process(
|
||||
leftChannel: Float32Array,
|
||||
rightChannel: Float32Array
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { AudioProcessor } from "./AudioProcessor";
|
||||
import type { AudioProcessor, ProcessorCategory } from "./AudioProcessor";
|
||||
|
||||
export class RingModulator implements AudioProcessor {
|
||||
getName(): string {
|
||||
@ -9,6 +9,10 @@ export class RingModulator implements AudioProcessor {
|
||||
return "Frequency modulation for metallic, bell-like tones";
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Modulation';
|
||||
}
|
||||
|
||||
async process(
|
||||
leftChannel: Float32Array,
|
||||
rightChannel: Float32Array
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { AudioProcessor } from './AudioProcessor';
|
||||
import type { AudioProcessor, ProcessorCategory } from './AudioProcessor';
|
||||
|
||||
export class SegmentShuffler implements AudioProcessor {
|
||||
getName(): string {
|
||||
@ -9,6 +9,10 @@ export class SegmentShuffler implements AudioProcessor {
|
||||
return 'Randomly reorganizes and swaps parts of the sound';
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Time';
|
||||
}
|
||||
|
||||
process(
|
||||
leftChannel: Float32Array,
|
||||
rightChannel: Float32Array
|
||||
|
||||
50
src/lib/audio/processors/SlowTapeStop.ts
Normal file
50
src/lib/audio/processors/SlowTapeStop.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import type { AudioProcessor, ProcessorCategory } from './AudioProcessor';
|
||||
|
||||
export class SlowTapeStop implements AudioProcessor {
|
||||
getName(): string {
|
||||
return 'Slow Tape Stop';
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return 'Simulates a tape machine gradually slowing to a stop';
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Time';
|
||||
}
|
||||
|
||||
process(leftIn: Float32Array, rightIn: Float32Array): [Float32Array, Float32Array] {
|
||||
const length = leftIn.length;
|
||||
const leftOut = new Float32Array(length);
|
||||
const rightOut = new Float32Array(length);
|
||||
|
||||
const stopPoint = length * 0.4;
|
||||
const stopDuration = length * 0.6;
|
||||
|
||||
let readPos = 0;
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
if (i < stopPoint) {
|
||||
readPos = i;
|
||||
} else {
|
||||
const t = (i - stopPoint) / stopDuration;
|
||||
const curve = 1.0 - (t * t * t);
|
||||
const speed = Math.max(0, curve);
|
||||
readPos += speed;
|
||||
}
|
||||
|
||||
const idx = Math.floor(readPos);
|
||||
const frac = readPos - idx;
|
||||
|
||||
if (idx < length - 1) {
|
||||
leftOut[i] = leftIn[idx] * (1.0 - frac) + leftIn[idx + 1] * frac;
|
||||
rightOut[i] = rightIn[idx] * (1.0 - frac) + rightIn[idx + 1] * frac;
|
||||
} else if (idx < length) {
|
||||
leftOut[i] = leftIn[idx];
|
||||
rightOut[i] = rightIn[idx];
|
||||
}
|
||||
}
|
||||
|
||||
return [leftOut, rightOut];
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import type { AudioProcessor } from './AudioProcessor';
|
||||
import type { AudioProcessor, ProcessorCategory } from './AudioProcessor';
|
||||
|
||||
export class SpectralBlur implements AudioProcessor {
|
||||
getName(): string {
|
||||
@ -9,6 +9,10 @@ export class SpectralBlur implements AudioProcessor {
|
||||
return 'Smears frequency content across neighboring bins for dreamy, diffused textures';
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Spectral';
|
||||
}
|
||||
|
||||
process(
|
||||
leftChannel: Float32Array,
|
||||
rightChannel: Float32Array
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { AudioProcessor } from './AudioProcessor';
|
||||
import type { AudioProcessor, ProcessorCategory } from './AudioProcessor';
|
||||
|
||||
export class SpectralShift implements AudioProcessor {
|
||||
getName(): string {
|
||||
@ -9,6 +9,10 @@ export class SpectralShift implements AudioProcessor {
|
||||
return 'Shifts all frequencies by a fixed Hz amount creating inharmonic, metallic timbres';
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Spectral';
|
||||
}
|
||||
|
||||
process(
|
||||
leftChannel: Float32Array,
|
||||
rightChannel: Float32Array
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { AudioProcessor } from './AudioProcessor';
|
||||
import type { AudioProcessor, ProcessorCategory } from './AudioProcessor';
|
||||
|
||||
export class StereoSwap implements AudioProcessor {
|
||||
getName(): string {
|
||||
@ -9,6 +9,10 @@ export class StereoSwap implements AudioProcessor {
|
||||
return 'Swaps left and right channels';
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Space';
|
||||
}
|
||||
|
||||
process(
|
||||
leftChannel: Float32Array,
|
||||
rightChannel: Float32Array
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { AudioProcessor } from "./AudioProcessor";
|
||||
import type { AudioProcessor, ProcessorCategory } from "./AudioProcessor";
|
||||
|
||||
export class StereoWidener implements AudioProcessor {
|
||||
getName(): string {
|
||||
@ -9,6 +9,10 @@ export class StereoWidener implements AudioProcessor {
|
||||
return "Expands stereo field using mid-side processing";
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Space';
|
||||
}
|
||||
|
||||
async process(
|
||||
leftChannel: Float32Array,
|
||||
rightChannel: Float32Array
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { AudioProcessor } from './AudioProcessor';
|
||||
import type { AudioProcessor, ProcessorCategory } from './AudioProcessor';
|
||||
|
||||
export class Stutter implements AudioProcessor {
|
||||
getName(): string {
|
||||
@ -9,6 +9,10 @@ export class Stutter implements AudioProcessor {
|
||||
return 'Rapidly repeats small fragments with smooth crossfades';
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Time';
|
||||
}
|
||||
|
||||
process(
|
||||
leftChannel: Float32Array,
|
||||
rightChannel: Float32Array
|
||||
|
||||
49
src/lib/audio/processors/TapeSpeedUp.ts
Normal file
49
src/lib/audio/processors/TapeSpeedUp.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import type { AudioProcessor, ProcessorCategory } from './AudioProcessor';
|
||||
|
||||
export class TapeSpeedUp implements AudioProcessor {
|
||||
getName(): string {
|
||||
return 'Tape Speed Up';
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return 'Simulates a tape machine accelerating from slow to normal speed';
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Time';
|
||||
}
|
||||
|
||||
process(leftIn: Float32Array, rightIn: Float32Array): [Float32Array, Float32Array] {
|
||||
const length = leftIn.length;
|
||||
const leftOut = new Float32Array(length);
|
||||
const rightOut = new Float32Array(length);
|
||||
|
||||
const accelDuration = length * 0.3;
|
||||
|
||||
let readPos = 0;
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
if (i < accelDuration) {
|
||||
const t = i / accelDuration;
|
||||
const curve = t * t * t;
|
||||
const speed = curve;
|
||||
readPos += speed;
|
||||
} else {
|
||||
readPos += 1.0;
|
||||
}
|
||||
|
||||
const idx = Math.floor(readPos);
|
||||
const frac = readPos - idx;
|
||||
|
||||
if (idx < length - 1) {
|
||||
leftOut[i] = leftIn[idx] * (1.0 - frac) + leftIn[idx + 1] * frac;
|
||||
rightOut[i] = rightIn[idx] * (1.0 - frac) + rightIn[idx + 1] * frac;
|
||||
} else if (idx < length) {
|
||||
leftOut[i] = leftIn[idx];
|
||||
rightOut[i] = rightIn[idx];
|
||||
}
|
||||
}
|
||||
|
||||
return [leftOut, rightOut];
|
||||
}
|
||||
}
|
||||
50
src/lib/audio/processors/TapeStop.ts
Normal file
50
src/lib/audio/processors/TapeStop.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import type { AudioProcessor, ProcessorCategory } from './AudioProcessor';
|
||||
|
||||
export class TapeStop implements AudioProcessor {
|
||||
getName(): string {
|
||||
return 'Tape Stop';
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return 'Simulates a tape machine slowing to a stop';
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Time';
|
||||
}
|
||||
|
||||
process(leftIn: Float32Array, rightIn: Float32Array): [Float32Array, Float32Array] {
|
||||
const length = leftIn.length;
|
||||
const leftOut = new Float32Array(length);
|
||||
const rightOut = new Float32Array(length);
|
||||
|
||||
const stopPoint = length * 0.7;
|
||||
const stopDuration = length * 0.3;
|
||||
|
||||
let readPos = 0;
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
if (i < stopPoint) {
|
||||
readPos = i;
|
||||
} else {
|
||||
const t = (i - stopPoint) / stopDuration;
|
||||
const curve = 1.0 - (t * t * t);
|
||||
const speed = Math.max(0, curve);
|
||||
readPos += speed;
|
||||
}
|
||||
|
||||
const idx = Math.floor(readPos);
|
||||
const frac = readPos - idx;
|
||||
|
||||
if (idx < length - 1) {
|
||||
leftOut[i] = leftIn[idx] * (1.0 - frac) + leftIn[idx + 1] * frac;
|
||||
rightOut[i] = rightIn[idx] * (1.0 - frac) + rightIn[idx + 1] * frac;
|
||||
} else if (idx < length) {
|
||||
leftOut[i] = leftIn[idx];
|
||||
rightOut[i] = rightIn[idx];
|
||||
}
|
||||
}
|
||||
|
||||
return [leftOut, rightOut];
|
||||
}
|
||||
}
|
||||
48
src/lib/audio/processors/TapeWobble.ts
Normal file
48
src/lib/audio/processors/TapeWobble.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import type { AudioProcessor, ProcessorCategory } from './AudioProcessor';
|
||||
|
||||
export class TapeWobble implements AudioProcessor {
|
||||
getName(): string {
|
||||
return 'Tape Wobble';
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return 'Simulates tape machine speed instability with pitch variations';
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Time';
|
||||
}
|
||||
|
||||
process(leftIn: Float32Array, rightIn: Float32Array): [Float32Array, Float32Array] {
|
||||
const length = leftIn.length;
|
||||
const leftOut = new Float32Array(length);
|
||||
const rightOut = new Float32Array(length);
|
||||
|
||||
const sampleRate = 44100;
|
||||
const wobbleFreq = 2.0 + Math.random() * 2.0;
|
||||
const wobbleDepth = 0.015;
|
||||
|
||||
let readPos = 0;
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
const t = i / sampleRate;
|
||||
const wobble = Math.sin(2.0 * Math.PI * wobbleFreq * t) * wobbleDepth;
|
||||
const speed = 1.0 + wobble;
|
||||
|
||||
readPos += speed;
|
||||
|
||||
const idx = Math.floor(readPos);
|
||||
const frac = readPos - idx;
|
||||
|
||||
if (idx < length - 1) {
|
||||
leftOut[i] = leftIn[idx] * (1.0 - frac) + leftIn[idx + 1] * frac;
|
||||
rightOut[i] = rightIn[idx] * (1.0 - frac) + rightIn[idx + 1] * frac;
|
||||
} else if (idx < length) {
|
||||
leftOut[i] = leftIn[idx];
|
||||
rightOut[i] = rightIn[idx];
|
||||
}
|
||||
}
|
||||
|
||||
return [leftOut, rightOut];
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import type { AudioProcessor } from './AudioProcessor';
|
||||
import type { AudioProcessor, ProcessorCategory } from './AudioProcessor';
|
||||
|
||||
export class Tremolo implements AudioProcessor {
|
||||
getName(): string {
|
||||
@ -9,6 +9,10 @@ export class Tremolo implements AudioProcessor {
|
||||
return 'Applies rhythmic volume modulation';
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Amplitude';
|
||||
}
|
||||
|
||||
process(
|
||||
leftChannel: Float32Array,
|
||||
rightChannel: Float32Array
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { AudioProcessor } from "./AudioProcessor";
|
||||
import type { AudioProcessor, ProcessorCategory } from "./AudioProcessor";
|
||||
|
||||
export class TrimSilence implements AudioProcessor {
|
||||
getName(): string {
|
||||
@ -9,6 +9,10 @@ export class TrimSilence implements AudioProcessor {
|
||||
return "Removes leading and trailing silence from audio";
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Utility';
|
||||
}
|
||||
|
||||
async process(
|
||||
leftChannel: Float32Array,
|
||||
rightChannel: Float32Array
|
||||
|
||||
50
src/lib/audio/processors/VinylStop.ts
Normal file
50
src/lib/audio/processors/VinylStop.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import type { AudioProcessor, ProcessorCategory } from './AudioProcessor';
|
||||
|
||||
export class VinylStop implements AudioProcessor {
|
||||
getName(): string {
|
||||
return 'Vinyl Stop';
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return 'Simulates a turntable slowing to a stop with realistic physics';
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Time';
|
||||
}
|
||||
|
||||
process(leftIn: Float32Array, rightIn: Float32Array): [Float32Array, Float32Array] {
|
||||
const length = leftIn.length;
|
||||
const leftOut = new Float32Array(length);
|
||||
const rightOut = new Float32Array(length);
|
||||
|
||||
const stopPoint = length * 0.7;
|
||||
const stopDuration = length * 0.3;
|
||||
|
||||
let readPos = 0;
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
if (i < stopPoint) {
|
||||
readPos = i;
|
||||
} else {
|
||||
const t = (i - stopPoint) / stopDuration;
|
||||
const curve = Math.exp(-5.0 * t);
|
||||
const speed = Math.max(0, curve);
|
||||
readPos += speed;
|
||||
}
|
||||
|
||||
const idx = Math.floor(readPos);
|
||||
const frac = readPos - idx;
|
||||
|
||||
if (idx < length - 1) {
|
||||
leftOut[i] = leftIn[idx] * (1.0 - frac) + leftIn[idx + 1] * frac;
|
||||
rightOut[i] = rightIn[idx] * (1.0 - frac) + rightIn[idx + 1] * frac;
|
||||
} else if (idx < length) {
|
||||
leftOut[i] = leftIn[idx];
|
||||
rightOut[i] = rightIn[idx];
|
||||
}
|
||||
}
|
||||
|
||||
return [leftOut, rightOut];
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import type { AudioProcessor } from "./AudioProcessor";
|
||||
import type { AudioProcessor, ProcessorCategory } from "./AudioProcessor";
|
||||
|
||||
export class Waveshaper implements AudioProcessor {
|
||||
getName(): string {
|
||||
@ -9,6 +9,10 @@ export class Waveshaper implements AudioProcessor {
|
||||
return "Transfer function distortion with various curve shapes";
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Distortion';
|
||||
}
|
||||
|
||||
async process(
|
||||
leftChannel: Float32Array,
|
||||
rightChannel: Float32Array
|
||||
|
||||
@ -25,6 +25,21 @@ import { Waveshaper } from './Waveshaper';
|
||||
import { DCOffsetRemover } from './DCOffsetRemover';
|
||||
import { TrimSilence } from './TrimSilence';
|
||||
import { Resonator } from './Resonator';
|
||||
import { LinearFadeOut } from './LinearFadeOut';
|
||||
import { ExpFadeOut } from './ExpFadeOut';
|
||||
import { LinearFadeIn } from './LinearFadeIn';
|
||||
import { ExpFadeIn } from './ExpFadeIn';
|
||||
import { PanLeftToRight } from './PanLeftToRight';
|
||||
import { PanRightToLeft } from './PanRightToLeft';
|
||||
import { LowPassSweepDown } from './LowPassSweepDown';
|
||||
import { LowPassSweepUp } from './LowPassSweepUp';
|
||||
import { HighPassSweepDown } from './HighPassSweepDown';
|
||||
import { HighPassSweepUp } from './HighPassSweepUp';
|
||||
import { TapeStop } from './TapeStop';
|
||||
import { SlowTapeStop } from './SlowTapeStop';
|
||||
import { TapeSpeedUp } from './TapeSpeedUp';
|
||||
import { VinylStop } from './VinylStop';
|
||||
import { TapeWobble } from './TapeWobble';
|
||||
|
||||
const processors: AudioProcessor[] = [
|
||||
new SegmentShuffler(),
|
||||
@ -53,6 +68,21 @@ const processors: AudioProcessor[] = [
|
||||
new DCOffsetRemover(),
|
||||
new TrimSilence(),
|
||||
new Resonator(),
|
||||
new LinearFadeOut(),
|
||||
new ExpFadeOut(),
|
||||
new LinearFadeIn(),
|
||||
new ExpFadeIn(),
|
||||
new PanLeftToRight(),
|
||||
new PanRightToLeft(),
|
||||
new LowPassSweepDown(),
|
||||
new LowPassSweepUp(),
|
||||
new HighPassSweepDown(),
|
||||
new HighPassSweepUp(),
|
||||
new TapeStop(),
|
||||
new SlowTapeStop(),
|
||||
new TapeSpeedUp(),
|
||||
new VinylStop(),
|
||||
new TapeWobble(),
|
||||
];
|
||||
|
||||
export function getRandomProcessor(): AudioProcessor {
|
||||
|
||||
@ -1,28 +1,77 @@
|
||||
<script lang="ts">
|
||||
import { getAllProcessors } from "../audio/processors/registry";
|
||||
import type { AudioProcessor } from "../audio/processors/AudioProcessor";
|
||||
import type { AudioProcessor, ProcessorCategory } from "../audio/processors/AudioProcessor";
|
||||
|
||||
interface Props {
|
||||
onselect: (processor: AudioProcessor) => void;
|
||||
selectedCategory: ProcessorCategory | 'All';
|
||||
onselectcategory: (category: ProcessorCategory | 'All') => void;
|
||||
}
|
||||
|
||||
let { onselect }: Props = $props();
|
||||
let { onselect, selectedCategory, onselectcategory }: Props = $props();
|
||||
|
||||
const allProcessors = getAllProcessors().sort((a, b) =>
|
||||
a.getName().localeCompare(b.getName())
|
||||
const allProcessors = getAllProcessors();
|
||||
|
||||
const allCategories: ProcessorCategory[] = [
|
||||
'Amplitude',
|
||||
'Filter',
|
||||
'Time',
|
||||
'Space',
|
||||
'Pitch',
|
||||
'Modulation',
|
||||
'Distortion',
|
||||
'Spectral',
|
||||
'Utility'
|
||||
];
|
||||
|
||||
const categoryCountsMap = $derived.by(() => {
|
||||
const counts = new Map<ProcessorCategory, number>();
|
||||
for (const processor of allProcessors) {
|
||||
const category = processor.getCategory();
|
||||
counts.set(category, (counts.get(category) || 0) + 1);
|
||||
}
|
||||
return counts;
|
||||
});
|
||||
|
||||
const filteredProcessors = $derived(
|
||||
selectedCategory === 'All'
|
||||
? allProcessors.sort((a, b) => a.getName().localeCompare(b.getName()))
|
||||
: allProcessors
|
||||
.filter(p => p.getCategory() === selectedCategory)
|
||||
.sort((a, b) => a.getName().localeCompare(b.getName()))
|
||||
);
|
||||
</script>
|
||||
|
||||
<div class="processor-popup">
|
||||
{#each allProcessors as processor}
|
||||
<div class="popup-sidebar">
|
||||
<button
|
||||
class="processor-tile"
|
||||
data-description={processor.getDescription()}
|
||||
onclick={() => onselect(processor)}
|
||||
class="category-filter"
|
||||
class:active={selectedCategory === 'All'}
|
||||
onclick={() => onselectcategory('All')}
|
||||
>
|
||||
{processor.getName()}
|
||||
All ({allProcessors.length})
|
||||
</button>
|
||||
{/each}
|
||||
{#each allCategories as category}
|
||||
<button
|
||||
class="category-filter"
|
||||
class:active={selectedCategory === category}
|
||||
onclick={() => onselectcategory(category)}
|
||||
>
|
||||
{category} ({categoryCountsMap.get(category) || 0})
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="processors-grid">
|
||||
{#each filteredProcessors as processor}
|
||||
<button
|
||||
class="processor-tile"
|
||||
data-description={processor.getDescription()}
|
||||
onclick={() => onselect(processor)}
|
||||
>
|
||||
{processor.getName()}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@ -31,18 +80,74 @@
|
||||
bottom: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background-color: #000;
|
||||
background-color: #0a0a0a;
|
||||
border: 2px solid #fff;
|
||||
padding: 0.5rem;
|
||||
z-index: 1000;
|
||||
width: 90vw;
|
||||
max-width: 500px;
|
||||
margin-bottom: 0.5rem;
|
||||
max-height: 60vh;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.popup-sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #1a1a1a;
|
||||
border-right: 1px solid #333;
|
||||
min-width: 80px;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #444 transparent;
|
||||
}
|
||||
|
||||
.popup-sidebar::-webkit-scrollbar {
|
||||
width: 3px;
|
||||
}
|
||||
|
||||
.popup-sidebar::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.popup-sidebar::-webkit-scrollbar-thumb {
|
||||
background: #444;
|
||||
}
|
||||
|
||||
.category-filter {
|
||||
font-size: 0.65rem;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
padding: 0.5rem 0.4rem;
|
||||
background-color: #1a1a1a;
|
||||
border: none;
|
||||
border-bottom: 1px solid #2a2a2a;
|
||||
color: #888;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s, color 0.2s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.category-filter:hover {
|
||||
background-color: #222;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.category-filter.active {
|
||||
background-color: #0a0a0a;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.processors-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 0.4rem;
|
||||
width: 90vw;
|
||||
max-width: 400px;
|
||||
margin-bottom: 0.5rem;
|
||||
max-height: 60vh;
|
||||
padding: 0.5rem;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.processor-tile {
|
||||
@ -90,11 +195,23 @@
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.processor-popup {
|
||||
width: 550px;
|
||||
max-width: 550px;
|
||||
}
|
||||
|
||||
.popup-sidebar {
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.category-filter {
|
||||
font-size: 0.7rem;
|
||||
padding: 0.5rem 0.6rem;
|
||||
}
|
||||
|
||||
.processors-grid {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
width: 500px;
|
||||
max-width: 500px;
|
||||
padding: 0.6rem;
|
||||
gap: 0.5rem;
|
||||
padding: 0.6rem;
|
||||
}
|
||||
|
||||
.processor-tile {
|
||||
@ -114,9 +231,16 @@
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.processor-popup {
|
||||
width: 650px;
|
||||
max-width: 650px;
|
||||
}
|
||||
|
||||
.popup-sidebar {
|
||||
min-width: 110px;
|
||||
}
|
||||
|
||||
.processors-grid {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
width: 600px;
|
||||
max-width: 600px;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
buffer: AudioBuffer | null;
|
||||
color?: string;
|
||||
playbackPosition?: number;
|
||||
cuePoint?: number;
|
||||
selectionStart?: number | null;
|
||||
selectionEnd?: number | null;
|
||||
onselectionchange?: (start: number | null, end: number | null) => void;
|
||||
@ -15,6 +16,7 @@
|
||||
buffer,
|
||||
color = '#646cff',
|
||||
playbackPosition = 0,
|
||||
cuePoint = 0,
|
||||
selectionStart = null,
|
||||
selectionEnd = null,
|
||||
onselectionchange,
|
||||
@ -43,6 +45,7 @@
|
||||
buffer;
|
||||
color;
|
||||
playbackPosition;
|
||||
cuePoint;
|
||||
selectionStart;
|
||||
selectionEnd;
|
||||
draw();
|
||||
@ -251,6 +254,16 @@
|
||||
const duration = buffer.length / buffer.sampleRate;
|
||||
const x = (playbackPosition / duration) * width;
|
||||
|
||||
ctx.strokeStyle = '#fff';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, 0);
|
||||
ctx.lineTo(x, height);
|
||||
ctx.stroke();
|
||||
} else if (cuePoint > 0 && buffer) {
|
||||
const duration = buffer.length / buffer.sampleRate;
|
||||
const x = (cuePoint / duration) * width;
|
||||
|
||||
ctx.strokeStyle = '#fff';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
|
||||
@ -21,39 +21,35 @@
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
onkeydown={(e) => e.stopPropagation()}
|
||||
>
|
||||
<h1 id="modal-title">Poof: a sample generator</h1>
|
||||
<p class="description">
|
||||
Do you need to generate audio samples for your projects? Poof, it's
|
||||
already done! These are not the best samples you'll ever hear, but they
|
||||
have the right to exist, nonetheless, in the realm of all the random and
|
||||
haphazardly generated digital sounds. Have fun, give computers some love!
|
||||
</p>
|
||||
<ul>
|
||||
<li class="description">
|
||||
Generate audio samples using various audio synthesis generators. Random
|
||||
parameters.
|
||||
</li>
|
||||
<li class="description">
|
||||
Process each sound with with a growing collection of random effects.
|
||||
</li>
|
||||
<li class="description">Export your samples as WAV files.</li>
|
||||
</ul>
|
||||
<div class="modal-links">
|
||||
<p>
|
||||
Created by <a
|
||||
href="https://raphaelforment.fr"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">Raphaël Forment (BuboBubo)</a
|
||||
>
|
||||
</p>
|
||||
<p>
|
||||
Licensed under <a
|
||||
href="https://www.gnu.org/licenses/gpl-3.0.html"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">GPL 3.0</a
|
||||
>
|
||||
</p>
|
||||
<h1 id="modal-title">Random Sample Generator and Processor</h1>
|
||||
<div class="modal-columns">
|
||||
<div class="modal-column gif-column">
|
||||
<img src="/tutorial.gif" alt="Tutorial showing how to use RSGP" class="tutorial-gif" />
|
||||
</div>
|
||||
<div class="modal-column text-column">
|
||||
<ol>
|
||||
<li class="description">
|
||||
Generate new samples using a diverse collection of synth engines.
|
||||
</li>
|
||||
<li class="description">
|
||||
Process samples using a growing list of tools and effects.
|
||||
</li>
|
||||
<li class="description">Export your samples as WAV files.</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
<p class="modal-footer">
|
||||
Created by <a
|
||||
href="https://raphaelforment.fr"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">Raphaël Forment (BuboBubo)</a
|
||||
>
|
||||
• Licensed under <a
|
||||
href="https://www.gnu.org/licenses/gpl-3.0.html"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">GPL 3.0</a
|
||||
>
|
||||
</p>
|
||||
<button class="modal-close" onclick={onclose}>Start</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -98,7 +94,7 @@
|
||||
background-color: #000;
|
||||
border: 2px solid #fff;
|
||||
padding: 1.25rem;
|
||||
max-width: 500px;
|
||||
max-width: 800px;
|
||||
width: calc(100% - 2rem);
|
||||
color: #fff;
|
||||
max-height: 90vh;
|
||||
@ -108,60 +104,83 @@
|
||||
}
|
||||
|
||||
.modal-content h1 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-size: 1.75rem;
|
||||
margin: 0 0 1.575rem 0;
|
||||
font-size: 1.25rem;
|
||||
font-weight: bold;
|
||||
letter-spacing: 0.02em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.modal-columns {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.575rem;
|
||||
margin-bottom: 1.575rem;
|
||||
}
|
||||
|
||||
.modal-column {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.gif-column {
|
||||
flex: 0 0 auto;
|
||||
max-width: 420px;
|
||||
}
|
||||
|
||||
.text-column {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.text-column ol {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-evenly;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding-left: 2rem;
|
||||
}
|
||||
|
||||
.modal-content .description {
|
||||
margin: 0 0 1rem 0;
|
||||
margin: 0;
|
||||
line-height: 1.6;
|
||||
color: #e0e0e0;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.modal-content ul {
|
||||
margin: 0 0 1rem 0;
|
||||
padding-left: 1.25rem;
|
||||
.tutorial-gif {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border: 1px solid #444;
|
||||
}
|
||||
|
||||
.modal-content ul li {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.modal-content ul li:last-child {
|
||||
.text-column ol li {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.modal-links {
|
||||
margin: 1.25rem 0;
|
||||
padding: 0.875rem 0;
|
||||
border-top: 1px solid #444;
|
||||
border-bottom: 1px solid #444;
|
||||
}
|
||||
|
||||
.modal-links p {
|
||||
margin: 0.375rem 0;
|
||||
font-size: 0.8125rem;
|
||||
.modal-footer {
|
||||
margin: 0 0 1.575rem 0;
|
||||
font-size: 0.75rem;
|
||||
color: #bbb;
|
||||
line-height: 1.5;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.modal-links a {
|
||||
.modal-footer a {
|
||||
color: #646cff;
|
||||
text-decoration: none;
|
||||
word-break: break-word;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.modal-links a:hover {
|
||||
.modal-footer a:hover {
|
||||
color: #8891ff;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
margin-top: 1rem;
|
||||
margin-top: 0;
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
font-size: 1rem;
|
||||
@ -194,17 +213,13 @@
|
||||
}
|
||||
|
||||
.modal-content h1 {
|
||||
font-size: 2rem;
|
||||
margin: 0 0 0.75rem 0;
|
||||
font-size: 1.5rem;
|
||||
margin: 0 0 1.575rem 0;
|
||||
}
|
||||
|
||||
.modal-content .description {
|
||||
font-size: 0.9375rem;
|
||||
}
|
||||
|
||||
.modal-links p {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
@ -213,32 +228,29 @@
|
||||
}
|
||||
|
||||
.modal-content h1 {
|
||||
font-size: 2.5rem;
|
||||
margin: 0 0 1rem 0;
|
||||
font-size: 1.75rem;
|
||||
margin: 0 0 2.1rem 0;
|
||||
}
|
||||
|
||||
.modal-columns {
|
||||
flex-direction: row;
|
||||
gap: 2.1rem;
|
||||
margin-bottom: 2.1rem;
|
||||
}
|
||||
|
||||
.modal-content .description {
|
||||
margin: 0 0 1.25rem 0;
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.modal-content ul {
|
||||
margin: 0 0 1.25rem 0;
|
||||
}
|
||||
|
||||
.modal-links {
|
||||
margin: 1.75rem 0;
|
||||
padding: 1.125rem 0;
|
||||
}
|
||||
|
||||
.modal-links p {
|
||||
font-size: 0.9375rem;
|
||||
margin: 0.5rem 0;
|
||||
.modal-footer {
|
||||
margin: 0 0 2.1rem 0;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
margin-top: 1.25rem;
|
||||
margin-top: 0;
|
||||
padding: 0.875rem;
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
@ -253,9 +265,5 @@
|
||||
.modal-close {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.modal-links a {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -32,7 +32,7 @@ export function createKeyboardHandler(actions: KeyboardActions) {
|
||||
case 'p':
|
||||
actions.onProcess?.();
|
||||
break;
|
||||
case 's':
|
||||
case 'd':
|
||||
actions.onDownload?.();
|
||||
break;
|
||||
case ' ':
|
||||
|
||||
@ -8,6 +8,8 @@ const STORAGE_KEYS = {
|
||||
DURATION: 'duration',
|
||||
PITCH_LOCK_ENABLED: 'pitchLockEnabled',
|
||||
PITCH_LOCK_FREQUENCY: 'pitchLockFrequency',
|
||||
EXPANDED_CATEGORIES: 'expandedCategories',
|
||||
SELECTED_PROCESSOR_CATEGORY: 'selectedProcessorCategory',
|
||||
} as const;
|
||||
|
||||
export function loadVolume(): number {
|
||||
@ -45,3 +47,29 @@ export function loadPitchLockFrequency(): number {
|
||||
export function savePitchLockFrequency(frequency: number): void {
|
||||
localStorage.setItem(STORAGE_KEYS.PITCH_LOCK_FREQUENCY, frequency.toString());
|
||||
}
|
||||
|
||||
export function loadExpandedCategories(): Set<string> {
|
||||
const stored = localStorage.getItem(STORAGE_KEYS.EXPANDED_CATEGORIES);
|
||||
if (stored) {
|
||||
try {
|
||||
const parsed = JSON.parse(stored);
|
||||
return new Set(Array.isArray(parsed) ? parsed : []);
|
||||
} catch {
|
||||
return new Set();
|
||||
}
|
||||
}
|
||||
return new Set();
|
||||
}
|
||||
|
||||
export function saveExpandedCategories(categories: Set<string>): void {
|
||||
localStorage.setItem(STORAGE_KEYS.EXPANDED_CATEGORIES, JSON.stringify(Array.from(categories)));
|
||||
}
|
||||
|
||||
export function loadSelectedProcessorCategory(): string {
|
||||
const stored = localStorage.getItem(STORAGE_KEYS.SELECTED_PROCESSOR_CATEGORY);
|
||||
return stored || 'All';
|
||||
}
|
||||
|
||||
export function saveSelectedProcessorCategory(category: string): void {
|
||||
localStorage.setItem(STORAGE_KEYS.SELECTED_PROCESSOR_CATEGORY, category);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user