Compare commits
6 Commits
fb92c3ae2a
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 3cadc23238 | |||
| aba4abb054 | |||
| dfb57a082f | |||
| 6116745795 | |||
| 65a1e16781 | |||
| b700c68b4d |
@ -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
|
||||
@ -130,3 +130,5 @@ endin
|
||||
|
||||
- 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 |
363
src/App.svelte
363
src/App.svelte
@ -5,15 +5,35 @@
|
||||
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, loadExpandedCategories, saveExpandedCategories } 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";
|
||||
@ -32,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);
|
||||
@ -48,15 +69,25 @@
|
||||
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(() => {
|
||||
@ -80,6 +111,10 @@
|
||||
saveExpandedCategories(expandedCategories);
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
saveSelectedProcessorCategory(selectedProcessorCategory);
|
||||
});
|
||||
|
||||
// Group engines by category
|
||||
const enginesByCategory = $derived.by(() => {
|
||||
const grouped = new Map<EngineCategory, typeof engines>();
|
||||
@ -103,6 +138,10 @@
|
||||
expandedCategories = newSet;
|
||||
}
|
||||
|
||||
function selectProcessorCategory(category: ProcessorCategory | 'All') {
|
||||
selectedProcessorCategory = category;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
audioService.setPlaybackUpdateCallback((position) => {
|
||||
playbackPosition = position;
|
||||
@ -116,7 +155,7 @@
|
||||
currentParams,
|
||||
isProcessed,
|
||||
waveformColor,
|
||||
currentEngineIndex
|
||||
currentEngineIndex,
|
||||
);
|
||||
}
|
||||
|
||||
@ -129,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;
|
||||
@ -154,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));
|
||||
},
|
||||
@ -219,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);
|
||||
}
|
||||
|
||||
@ -230,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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -264,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();
|
||||
@ -288,7 +348,7 @@
|
||||
clearSelection();
|
||||
sidebarOpen = false;
|
||||
|
||||
if (engineType === 'generative') {
|
||||
if (engineType === "generative") {
|
||||
generateRandom();
|
||||
}
|
||||
}
|
||||
@ -311,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}`);
|
||||
}
|
||||
}
|
||||
@ -329,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;
|
||||
@ -344,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;
|
||||
}
|
||||
|
||||
@ -365,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() {
|
||||
@ -389,7 +454,8 @@
|
||||
}
|
||||
|
||||
function cropSelection() {
|
||||
if (!currentBuffer || selectionStart === null || selectionEnd === null) return;
|
||||
if (!currentBuffer || selectionStart === null || selectionEnd === null)
|
||||
return;
|
||||
|
||||
pushState();
|
||||
|
||||
@ -397,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();
|
||||
@ -405,7 +476,8 @@
|
||||
}
|
||||
|
||||
function cutSelection() {
|
||||
if (!currentBuffer || selectionStart === null || selectionEnd === null) return;
|
||||
if (!currentBuffer || selectionStart === null || selectionEnd === null)
|
||||
return;
|
||||
|
||||
pushState();
|
||||
|
||||
@ -434,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>
|
||||
@ -521,8 +616,19 @@
|
||||
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
|
||||
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>
|
||||
@ -545,80 +651,87 @@
|
||||
</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}
|
||||
@ -692,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 {
|
||||
@ -759,7 +874,9 @@
|
||||
color: #999;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s, background-color 0.2s;
|
||||
transition:
|
||||
color 0.2s,
|
||||
background-color 0.2s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@ -950,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;
|
||||
}
|
||||
@ -981,7 +1101,6 @@
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.sidebar {
|
||||
position: static;
|
||||
|
||||
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),
|
||||
};
|
||||
}
|
||||
}
|
||||
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)
|
||||
};
|
||||
}
|
||||
}
|
||||
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)
|
||||
};
|
||||
}
|
||||
}
|
||||
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)
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -26,6 +26,11 @@ 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(),
|
||||
@ -55,4 +60,9 @@ export const engines: SynthEngine[] = [
|
||||
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 ' ':
|
||||
|
||||
@ -9,6 +9,7 @@ const STORAGE_KEYS = {
|
||||
PITCH_LOCK_ENABLED: 'pitchLockEnabled',
|
||||
PITCH_LOCK_FREQUENCY: 'pitchLockFrequency',
|
||||
EXPANDED_CATEGORIES: 'expandedCategories',
|
||||
SELECTED_PROCESSOR_CATEGORY: 'selectedProcessorCategory',
|
||||
} as const;
|
||||
|
||||
export function loadVolume(): number {
|
||||
@ -63,3 +64,12 @@ export function loadExpandedCategories(): Set<string> {
|
||||
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