Compare commits

..

17 Commits

Author SHA1 Message Date
3cadc23238 Fixing warnings 2025-12-25 14:56:55 +01:00
aba4abb054 Feat: adding filter and 'All' button to processors 2025-11-11 11:19:05 +01:00
dfb57a082f OK, rename 2025-10-14 02:11:59 +02:00
6116745795 Working on processors a tiny bit 2025-10-13 18:09:47 +02:00
65a1e16781 note 2025-10-13 17:36:47 +02:00
b700c68b4d Quite a big update 2025-10-13 17:35:47 +02:00
fb92c3ae2a new interesting generator 2025-10-13 14:23:27 +02:00
38479f0253 Adding more CSound models 2025-10-13 13:45:33 +02:00
580aa4b96f Spectral additive 2025-10-13 13:09:59 +02:00
467558efd2 Rework the interface a bit 2025-10-13 12:41:39 +02:00
51e7c44c93 CSound based engine 2025-10-13 11:51:20 +02:00
179c52facc click on waveform 2025-10-13 11:17:20 +02:00
4df063f9b3 add undo option 2025-10-13 10:58:03 +02:00
cb730237f5 Adding two engines and one processor 2025-10-13 10:33:12 +02:00
c1f7cc02fd new engine 2025-10-12 23:39:14 +02:00
d118d3a52b phase distortion 2025-10-12 18:25:44 +02:00
57fb8a93dc current kick generator 2025-10-12 16:35:00 +02:00
94 changed files with 9659 additions and 472 deletions

View File

@ -1,4 +1,4 @@
# poof
# rsgp
Audio synthesis web application for generating random sound samples. Users generate sounds via different synthesis engines, mutate parameters, apply audio processors in multiple passes, and export as WAV. Built for musicians seeking unexpected textures and one-shots.
@ -39,8 +39,8 @@ pnpm build
## Docker
```sh
docker build -t poof .
docker run -p 8080:80 poof
docker build -t rsgp .
docker run -p 8080:80 rsgp
```
Opens on http://localhost:8080
@ -55,6 +55,69 @@ Opens on http://localhost:8080
4. Keep all DSP code, helpers, and types in the same file
5. Register in `src/lib/audio/engines/registry.ts`
### Adding CSound-Based Synthesis Engines
For complex DSP algorithms, you can leverage CSound's powerful audio language:
1. Create a single file in `src/lib/audio/engines/` extending the `CsoundEngine<ParamsType>` abstract class
2. Define a TypeScript interface for your parameters
3. Implement required methods:
- `getName()`: Engine display name
- `getDescription()`: Brief description
- `getType()`: Return `'generative'`, `'sample'`, or `'input'`
- `getOrchestra()`: Return CSound orchestra code as a string
- `getParametersForCsound(params)`: Map TypeScript params to CSound channel parameters
- `randomParams(pitchLock?)`: Generate random parameter values
- `mutateParams(params, mutationAmount?, pitchLock?)`: Mutate existing parameters
4. Keep all enums, interfaces, and helper logic in the same file
5. Register in `src/lib/audio/engines/registry.ts`
**CSound Orchestra Guidelines:**
- Use `instr 1` as your main instrument
- Read parameters via `chnget "paramName"`
- Duration is available as `p3`
- Time-based parameters (attack, decay, release) should be ratios (0-1) scaled by `p3`
- Output stereo audio with `outs aLeft, aRight`
- The base class handles WAV parsing, normalization, and fade-in
**Example:**
```typescript
import { CsoundEngine, type CsoundParameter } from './base/CsoundEngine';
interface MyParams {
frequency: number;
resonance: number;
}
export class MyEngine extends CsoundEngine<MyParams> {
getName() { return 'My Engine'; }
getDescription() { return 'Description'; }
getType() { return 'generative' as const; }
protected getOrchestra(): string {
return `
instr 1
iFreq chnget "frequency"
iRes chnget "resonance"
aNoise noise 1, 0
aOut butterbp aNoise, iFreq, iRes
outs aOut, aOut
endin
`;
}
protected getParametersForCsound(params: MyParams): CsoundParameter[] {
return [
{ channelName: 'frequency', value: params.frequency },
{ channelName: 'resonance', value: params.resonance }
];
}
randomParams() { /* ... */ }
mutateParams(params, amount = 0.15) { /* ... */ }
}
```
### Adding Audio Processors
1. Create a single file in `src/lib/audio/processors/` implementing the `AudioProcessor` interface
@ -65,4 +128,7 @@ Opens on http://localhost:8080
## Credits
Wavetables from [Adventure Kid Waveforms](https://www.adventurekid.se/akrt/waveforms/adventure-kid-waveforms/) by Kristoffer Ekstrand
- 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).

View File

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

View File

@ -1,5 +1,5 @@
{
"name": "poof",
"name": "rsgp",
"private": true,
"version": "0.0.0",
"type": "module",
@ -22,6 +22,7 @@
"vite": "npm:rolldown-vite@7.1.14"
},
"dependencies": {
"@csound/browser": "7.0.0-beta11",
"zzfx": "^1.3.2"
}
}

383
pnpm-lock.yaml generated
View File

@ -8,6 +8,9 @@ importers:
.:
dependencies:
'@csound/browser':
specifier: 7.0.0-beta11
version: 7.0.0-beta11
zzfx:
specifier: ^1.3.2
version: 1.3.2
@ -36,6 +39,13 @@ importers:
packages:
'@babel/runtime@7.28.4':
resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==}
engines: {node: '>=6.9.0'}
'@csound/browser@7.0.0-beta11':
resolution: {integrity: sha512-BGFTMXUdOJA1Xz1ETzbE/y8B/X6dpnrKThiqxDqj45K+ctOWtMqefgH6MojzJjWFwRs8UqhrJmVUq78SbMwGlw==}
'@emnapi/core@1.5.0':
resolution: {integrity: sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==}
@ -194,6 +204,10 @@ packages:
engines: {node: '>=0.4.0'}
hasBin: true
ansi-styles@4.3.0:
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
engines: {node: '>=8'}
ansis@4.2.0:
resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==}
engines: {node: '>=14'}
@ -202,18 +216,59 @@ packages:
resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==}
engines: {node: '>= 0.4'}
automation-events@7.1.13:
resolution: {integrity: sha512-1Hay5TQPzxsskSqPTH3YXyzE9Iirz82zZDse2vr3+kOR7Sc7om17qIEPsESchlNX0EgKxANwR40i2g/O3GM1Tw==}
engines: {node: '>=18.2.0'}
axobject-query@4.1.0:
resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==}
engines: {node: '>= 0.4'}
balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
brace-expansion@1.1.12:
resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==}
chalk@4.1.2:
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
engines: {node: '>=10'}
chokidar@4.0.3:
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
engines: {node: '>= 14.16.0'}
clone-buffer@1.0.0:
resolution: {integrity: sha512-KLLTJWrvwIP+OPfMn0x2PheDEP20RPUcGXj/ERegTgdmPEZylALQldygiqrPPu8P45uNuPs7ckmReLY6v/iA5g==}
engines: {node: '>= 0.10'}
clone-stats@1.0.0:
resolution: {integrity: sha512-au6ydSpg6nsrigcZ4m8Bc9hxjeW+GJ8xh5G3BJCMt4WXe1H10UNaVOamqQTmrx1kjVuxAHIQSNU6hY4Nsn9/ag==}
clone@2.1.2:
resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==}
engines: {node: '>=0.8'}
cloneable-readable@1.1.3:
resolution: {integrity: sha512-2EF8zTQOxYq70Y4XKtorQupqF0m49MBz2/yf5Bj+MHjvpG3Hy7sImifnqD6UA+TKYxeSV+u6qqQPawN5UvnpKQ==}
clsx@2.1.1:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'}
color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
core-util-is@1.0.3:
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
debug@4.4.3:
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
engines: {node: '>=6.0'}
@ -237,6 +292,9 @@ packages:
esrap@2.1.0:
resolution: {integrity: sha512-yzmPNpl7TBbMRC5Lj2JlJZNPml0tzqoqP5B1JXycNUwtqma9AKCO0M2wHrdgsHcy1WRW7S9rJknAMtByg3usgA==}
eventemitter3@4.0.7:
resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
fdir@6.5.0:
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
engines: {node: '>=12.0.0'}
@ -246,14 +304,68 @@ packages:
picomatch:
optional: true
fs.realpath@1.0.0:
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
glob@7.2.3:
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
deprecated: Glob versions prior to v9 are no longer supported
google-closure-compiler-java@20221102.0.1:
resolution: {integrity: sha512-rMKLEma3uSe/6MGHtivDezTv4u5iaDEyxoy9No+1WruPSZ5h1gBZLONcfCA8JaoGojFPdHZI1qbwT0EveEWnAg==}
google-closure-compiler-linux@20221102.0.1:
resolution: {integrity: sha512-rj1E1whT4j/giidQ44v4RoO8GcvU81VU9YB5RlRM0hWDvCGWjQasDABGnF/YLWLl5PXAAfJpa/hy8ckv5/r97g==}
cpu: [x32, x64]
os: [linux]
google-closure-compiler-osx@20221102.0.1:
resolution: {integrity: sha512-Cv993yr9a2DLFgYnsv4m6dNUk5jousd6W6la12x2fDbhxTLewYrw7CrCaVEVw1SU3XErVmdHOZQjFsVMhcZjCw==}
cpu: [x32, x64, arm64]
os: [darwin]
google-closure-compiler-windows@20221102.0.1:
resolution: {integrity: sha512-jRwHGekG/oDihHdKAEiYN5z0cBF+brL0bYtuEOXx4fAmq5tHe4OxKtSEEprCnVZZL0aG/boGprACPvsDRsXT7Q==}
cpu: [x32, x64]
os: [win32]
google-closure-compiler@20221102.0.1:
resolution: {integrity: sha512-edAlsyJEsy2I983xWBlBfdSme16uyY007HM2OwPOoWPEFgmR100ggUabJbIegXZgbSLH51kyeJMQKuWhiHgzcA==}
engines: {node: '>=10'}
hasBin: true
google-closure-library@20221102.0.0:
resolution: {integrity: sha512-M5+LWPS99tMB9dOGpZjLT9CdIYpnwBZiwB+dCmZFOOvwJiOWytntzJ/a/hoNF6zxD15l3GWwRJiEkL636D6DRQ==}
has-flag@4.0.0:
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
engines: {node: '>=8'}
inflight@1.0.6:
resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
inherits@2.0.4:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
is-reference@3.0.3:
resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==}
isarray@1.0.0:
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
jazz-midi@1.7.9:
resolution: {integrity: sha512-c8c4BBgwxdsIr1iVm53nadCrtH7BUlnX3V95ciK/gbvXN/ndE5+POskBalXgqlc/r9p2XUbdLTrgrC6fou5p9w==}
engines: {node: '>=10.0.0'}
jzz@1.9.6:
resolution: {integrity: sha512-J7ENLhXwfm2BNDKRUrL8eKtPhUS/CtMBpiafxQHDBcOWSocLhearDKEdh+ylnZFcr5OXWTed0gj6l/txeQA9vg==}
lightningcss-android-arm64@1.30.2:
resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==}
engines: {node: '>= 12.0.0'}
@ -330,6 +442,12 @@ packages:
magic-string@0.30.19:
resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==}
minimatch@3.1.2:
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
minimist@1.2.8:
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
mri@1.2.0:
resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==}
engines: {node: '>=4'}
@ -342,6 +460,16 @@ packages:
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
once@1.4.0:
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
pako@2.1.0:
resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==}
path-is-absolute@1.0.1:
resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==}
engines: {node: '>=0.10.0'}
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
@ -353,10 +481,31 @@ packages:
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
engines: {node: ^10 || ^12 || >=14}
process-nextick-args@2.0.1:
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
ramda@0.28.0:
resolution: {integrity: sha512-9QnLuG/kPVgWvMQ4aODhsBUFKOUmnbUnsSXACv+NCQZcHbeb+v8Lodp8OVxtRULN1/xOyYLLaL6npE6dMq5QTA==}
readable-stream@2.3.8:
resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
readdirp@4.1.2:
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
engines: {node: '>= 14.18.0'}
remove-trailing-separator@1.1.0:
resolution: {integrity: sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==}
replace-ext@1.0.1:
resolution: {integrity: sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw==}
engines: {node: '>= 0.10'}
rimraf@3.0.2:
resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==}
deprecated: Rimraf versions prior to v4 are no longer supported
hasBin: true
rolldown-vite@7.1.14:
resolution: {integrity: sha512-eSiiRJmovt8qDJkGyZuLnbxAOAdie6NCmmd0NkTC0RJI9duiSBTfr8X2mBYJOUFzxQa2USaHmL99J9uMxkjCyw==}
engines: {node: ^20.19.0 || >=22.12.0}
@ -406,10 +555,27 @@ packages:
resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==}
engines: {node: '>=6'}
safe-buffer@5.1.2:
resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}
source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
source-map@0.5.7:
resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==}
engines: {node: '>=0.10.0'}
standardized-audio-context@25.3.77:
resolution: {integrity: sha512-Ki9zNz6pKcC5Pi+QPjPyVsD9GwJIJWgryji0XL9cAJXMGyn+dPOf6Qik1AHei0+UNVcc4BOCa0hWLBzlwqsW/A==}
string_decoder@1.1.1:
resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}
supports-color@7.2.0:
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
engines: {node: '>=8'}
svelte-check@4.3.3:
resolution: {integrity: sha512-RYP0bEwenDXzfv0P1sKAwjZSlaRyqBn0Fz1TVni58lqyEiqgwztTpmodJrGzP6ZT2aHl4MbTvWP6gbmQ3FOnBg==}
engines: {node: '>= 18.0.0'}
@ -422,6 +588,9 @@ packages:
resolution: {integrity: sha512-8MxWVm2+3YwrFbPaxOlT1bbMi6OTenrAgks6soZfiaS8Fptk4EVyRIFhJc3RpO264EeSNwgjWAdki0ufg4zkGw==}
engines: {node: '>=18'}
text-encoding-shim@1.0.5:
resolution: {integrity: sha512-H7yYW+jRn4yhu60ygZ2f/eMhXPITRt4QSUTKzLm+eCaDsdX8avmgWpmtmHAzesjBVUTAypz9odu5RKUjX5HNYA==}
tinyglobby@0.2.15:
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
engines: {node: '>=12.0.0'}
@ -437,6 +606,19 @@ packages:
undici-types@7.14.0:
resolution: {integrity: sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==}
unmute-ios-audio@3.3.0:
resolution: {integrity: sha512-MmoCOrsS2gn3wLT2tT+hF56Q4V4kksIKn2LHrwAtX6umzQwQHDWSh1slMzH+0WuxTZ62s3w8/wsfIII1FQ7ACg==}
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
vinyl-sourcemaps-apply@0.2.1:
resolution: {integrity: sha512-+oDh3KYZBoZC8hfocrbrxbLUeaYtQK7J5WU5Br9VqWqmCll3tFJqKp97GC9GmMsVIL0qnx2DgEDVxdo5EZ5sSw==}
vinyl@2.2.1:
resolution: {integrity: sha512-LII3bXRFBZLlezoG5FfZVcXflZgWP/4dCwKtxd5ky9+LOtM4CS3bIRQsmR1KMnMW07jpE8fqR2lcxPZ+8sJIcw==}
engines: {node: '>= 0.10'}
vitefu@1.1.1:
resolution: {integrity: sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==}
peerDependencies:
@ -445,6 +627,12 @@ packages:
vite:
optional: true
web-midi-api@2.4.0:
resolution: {integrity: sha512-tTfLdxa5LpOP1NgWByV458iYKgSLhlsIwqCpfbcJuyjProNtuf5UnX97K4JNyuQCqkR+6thQAIsk2BOMSrKaCA==}
wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
zimmerframe@1.1.4:
resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==}
@ -453,6 +641,21 @@ packages:
snapshots:
'@babel/runtime@7.28.4': {}
'@csound/browser@7.0.0-beta11':
dependencies:
eventemitter3: 4.0.7
google-closure-compiler: 20221102.0.1
google-closure-library: 20221102.0.0
pako: 2.1.0
ramda: 0.28.0
rimraf: 3.0.2
standardized-audio-context: 25.3.77
text-encoding-shim: 1.0.5
unmute-ios-audio: 3.3.0
web-midi-api: 2.4.0
'@emnapi/core@1.5.0':
dependencies:
'@emnapi/wasi-threads': 1.1.0
@ -585,18 +788,61 @@ snapshots:
acorn@8.15.0: {}
ansi-styles@4.3.0:
dependencies:
color-convert: 2.0.1
ansis@4.2.0: {}
aria-query@5.3.2: {}
automation-events@7.1.13:
dependencies:
'@babel/runtime': 7.28.4
tslib: 2.8.1
axobject-query@4.1.0: {}
balanced-match@1.0.2: {}
brace-expansion@1.1.12:
dependencies:
balanced-match: 1.0.2
concat-map: 0.0.1
chalk@4.1.2:
dependencies:
ansi-styles: 4.3.0
supports-color: 7.2.0
chokidar@4.0.3:
dependencies:
readdirp: 4.1.2
clone-buffer@1.0.0: {}
clone-stats@1.0.0: {}
clone@2.1.2: {}
cloneable-readable@1.1.3:
dependencies:
inherits: 2.0.4
process-nextick-args: 2.0.1
readable-stream: 2.3.8
clsx@2.1.1: {}
color-convert@2.0.1:
dependencies:
color-name: 1.1.4
color-name@1.1.4: {}
concat-map@0.0.1: {}
core-util-is@1.0.3: {}
debug@4.4.3:
dependencies:
ms: 2.1.3
@ -611,17 +857,72 @@ snapshots:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
eventemitter3@4.0.7: {}
fdir@6.5.0(picomatch@4.0.3):
optionalDependencies:
picomatch: 4.0.3
fs.realpath@1.0.0: {}
fsevents@2.3.3:
optional: true
glob@7.2.3:
dependencies:
fs.realpath: 1.0.0
inflight: 1.0.6
inherits: 2.0.4
minimatch: 3.1.2
once: 1.4.0
path-is-absolute: 1.0.1
google-closure-compiler-java@20221102.0.1: {}
google-closure-compiler-linux@20221102.0.1:
optional: true
google-closure-compiler-osx@20221102.0.1:
optional: true
google-closure-compiler-windows@20221102.0.1:
optional: true
google-closure-compiler@20221102.0.1:
dependencies:
chalk: 4.1.2
google-closure-compiler-java: 20221102.0.1
minimist: 1.2.8
vinyl: 2.2.1
vinyl-sourcemaps-apply: 0.2.1
optionalDependencies:
google-closure-compiler-linux: 20221102.0.1
google-closure-compiler-osx: 20221102.0.1
google-closure-compiler-windows: 20221102.0.1
google-closure-library@20221102.0.0: {}
has-flag@4.0.0: {}
inflight@1.0.6:
dependencies:
once: 1.4.0
wrappy: 1.0.2
inherits@2.0.4: {}
is-reference@3.0.3:
dependencies:
'@types/estree': 1.0.8
isarray@1.0.0: {}
jazz-midi@1.7.9: {}
jzz@1.9.6:
dependencies:
jazz-midi: 1.7.9
lightningcss-android-arm64@1.30.2:
optional: true
@ -677,12 +978,26 @@ snapshots:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
minimatch@3.1.2:
dependencies:
brace-expansion: 1.1.12
minimist@1.2.8: {}
mri@1.2.0: {}
ms@2.1.3: {}
nanoid@3.3.11: {}
once@1.4.0:
dependencies:
wrappy: 1.0.2
pako@2.1.0: {}
path-is-absolute@1.0.1: {}
picocolors@1.1.1: {}
picomatch@4.0.3: {}
@ -693,8 +1008,30 @@ snapshots:
picocolors: 1.1.1
source-map-js: 1.2.1
process-nextick-args@2.0.1: {}
ramda@0.28.0: {}
readable-stream@2.3.8:
dependencies:
core-util-is: 1.0.3
inherits: 2.0.4
isarray: 1.0.0
process-nextick-args: 2.0.1
safe-buffer: 5.1.2
string_decoder: 1.1.1
util-deprecate: 1.0.2
readdirp@4.1.2: {}
remove-trailing-separator@1.1.0: {}
replace-ext@1.0.1: {}
rimraf@3.0.2:
dependencies:
glob: 7.2.3
rolldown-vite@7.1.14(@types/node@24.7.1):
dependencies:
'@oxc-project/runtime': 0.92.0
@ -733,8 +1070,26 @@ snapshots:
dependencies:
mri: 1.2.0
safe-buffer@5.1.2: {}
source-map-js@1.2.1: {}
source-map@0.5.7: {}
standardized-audio-context@25.3.77:
dependencies:
'@babel/runtime': 7.28.4
automation-events: 7.1.13
tslib: 2.8.1
string_decoder@1.1.1:
dependencies:
safe-buffer: 5.1.2
supports-color@7.2.0:
dependencies:
has-flag: 4.0.0
svelte-check@4.3.3(picomatch@4.0.3)(svelte@5.39.11)(typescript@5.9.3):
dependencies:
'@jridgewell/trace-mapping': 0.3.31
@ -764,22 +1119,46 @@ snapshots:
magic-string: 0.30.19
zimmerframe: 1.1.4
text-encoding-shim@1.0.5: {}
tinyglobby@0.2.15:
dependencies:
fdir: 6.5.0(picomatch@4.0.3)
picomatch: 4.0.3
tslib@2.8.1:
optional: true
tslib@2.8.1: {}
typescript@5.9.3: {}
undici-types@7.14.0: {}
unmute-ios-audio@3.3.0: {}
util-deprecate@1.0.2: {}
vinyl-sourcemaps-apply@0.2.1:
dependencies:
source-map: 0.5.7
vinyl@2.2.1:
dependencies:
clone: 2.1.2
clone-buffer: 1.0.0
clone-stats: 1.0.0
cloneable-readable: 1.1.3
remove-trailing-separator: 1.1.0
replace-ext: 1.0.1
vitefu@1.1.1(rolldown-vite@7.1.14(@types/node@24.7.1)):
optionalDependencies:
vite: rolldown-vite@7.1.14(@types/node@24.7.1)
web-midi-api@2.4.0:
dependencies:
jzz: 1.9.6
wrappy@1.0.2: {}
zimmerframe@1.1.4: {}
zzfx@1.3.2: {}

BIN
public/tutorial.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 972 KiB

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,228 @@
import { CsoundEngine, type CsoundParameter } from './base/CsoundEngine';
import type { PitchLock } from './base/SynthEngine';
interface AdditiveBassParams {
baseFreq: number;
pitchSweep: number;
pitchDecay: number;
overtoneAmp: number;
overtoneFreqMult: number;
noiseAmp: number;
noiseDecay: number;
filterResonance: number;
filterCutoff: number;
attack: number;
decay: number;
waveshape: number;
bodyResonance: number;
click: number;
harmonicSpread: number;
}
export class AdditiveBass extends CsoundEngine<AdditiveBassParams> {
getName(): string {
return 'Additive Bass';
}
getDescription(): string {
return 'Deep bass drum using additive synthesis with pink noise and waveshaping';
}
getType() {
return 'generative' as const;
}
getCategory() {
return 'Percussion' as const;
}
protected getOrchestra(): string {
return `
instr 1
iBaseFreq chnget "baseFreq"
iPitchSweep chnget "pitchSweep"
iPitchDecay chnget "pitchDecay"
iOvertoneAmp chnget "overtoneAmp"
iOvertoneFreqMult chnget "overtoneFreqMult"
iNoiseAmp chnget "noiseAmp"
iNoiseDecay chnget "noiseDecay"
iFilterResonance chnget "filterResonance"
iFilterCutoff chnget "filterCutoff"
iAttack chnget "attack"
iDecay chnget "decay"
iWaveshape chnget "waveshape"
iBodyResonance chnget "bodyResonance"
iClick chnget "click"
iHarmonicSpread chnget "harmonicSpread"
idur = p3
iAttackTime = iAttack * idur
iDecayTime = iDecay * idur
iPitchDecayTime = iPitchDecay * idur
iNoiseDecayTime = iNoiseDecay * idur
; Pitch envelope: exponential sweep from high to low
kPitchEnv expseg iBaseFreq * (1 + iPitchSweep * 3), iPitchDecayTime, iBaseFreq, idur - iPitchDecayTime, iBaseFreq * 0.95
; Main amplitude envelope with attack and decay
kAmpEnv linseg 0, iAttackTime, 1, iDecayTime, 0.001, 0.001, 0
kAmpEnv = kAmpEnv * kAmpEnv
; Generate fundamental sine wave
aFund oscili 0.7, kPitchEnv
; Generate overtone at multiple of fundamental
aOvertone oscili iOvertoneAmp, kPitchEnv * iOvertoneFreqMult
; Add harmonic spread (additional harmonics)
aHarm2 oscili iHarmonicSpread * 0.3, kPitchEnv * 3
aHarm3 oscili iHarmonicSpread * 0.2, kPitchEnv * 5
; Mix oscillators
aMix = aFund + aOvertone + aHarm2 + aHarm3
; Apply waveshaping (hyperbolic tangent style)
if iWaveshape > 0.1 then
aMix = tanh(aMix * (1 + iWaveshape * 3))
endif
; Generate pink noise
aPink pinkish 1
; Noise envelope (fast decay)
kNoiseEnv expseg 1, iNoiseDecayTime, 0.001, idur - iNoiseDecayTime, 0.001
aPinkScaled = aPink * iNoiseAmp * kNoiseEnv
; Add noise to mix
aMix = aMix + aPinkScaled
; Click transient (high frequency burst at start)
if iClick > 0.1 then
kClickEnv linseg 1, 0.005, 0, idur - 0.005, 0
aClick oscili iClick * 0.4, kPitchEnv * 8
aMix = aMix + aClick * kClickEnv
endif
; Apply resonant low-pass filter
kFilterFreq = iFilterCutoff * (1 + kPitchEnv / iBaseFreq * 0.5)
aFiltered rezzy aMix, kFilterFreq, iFilterResonance
; Body resonance (second resonant filter at fundamental)
if iBodyResonance > 0.1 then
aBodyFilt butterbp aFiltered, kPitchEnv * 0.5, 20
aFiltered = aFiltered + aBodyFilt * iBodyResonance
endif
; Apply main envelope
aOut = aFiltered * kAmpEnv * 0.5
; Stereo - slightly different phase and detune for right channel
kPitchEnvR expseg iBaseFreq * 1.002 * (1 + iPitchSweep * 3), iPitchDecayTime, iBaseFreq * 1.002, idur - iPitchDecayTime, iBaseFreq * 0.952
aFundR oscili 0.7, kPitchEnvR
aOvertoneR oscili iOvertoneAmp, kPitchEnvR * iOvertoneFreqMult
aHarm2R oscili iHarmonicSpread * 0.3, kPitchEnvR * 3
aHarm3R oscili iHarmonicSpread * 0.2, kPitchEnvR * 5
aMixR = aFundR + aOvertoneR + aHarm2R + aHarm3R
if iWaveshape > 0.1 then
aMixR = tanh(aMixR * (1 + iWaveshape * 3))
endif
aPinkR pinkish 1
aPinkScaledR = aPinkR * iNoiseAmp * kNoiseEnv
aMixR = aMixR + aPinkScaledR
if iClick > 0.1 then
aClickR oscili iClick * 0.4, kPitchEnvR * 8
aMixR = aMixR + aClickR * kClickEnv
endif
kFilterFreqR = iFilterCutoff * (1 + kPitchEnvR / iBaseFreq * 0.5)
aFilteredR rezzy aMixR, kFilterFreqR, iFilterResonance
if iBodyResonance > 0.1 then
aBodyFiltR butterbp aFilteredR, kPitchEnvR * 0.5, 20
aFilteredR = aFilteredR + aBodyFiltR * iBodyResonance
endif
aOutR = aFilteredR * kAmpEnv * 0.5
outs aOut, aOutR
endin
`;
}
protected getParametersForCsound(params: AdditiveBassParams): CsoundParameter[] {
return [
{ channelName: 'baseFreq', value: params.baseFreq },
{ channelName: 'pitchSweep', value: params.pitchSweep },
{ channelName: 'pitchDecay', value: params.pitchDecay },
{ channelName: 'overtoneAmp', value: params.overtoneAmp },
{ channelName: 'overtoneFreqMult', value: params.overtoneFreqMult },
{ channelName: 'noiseAmp', value: params.noiseAmp },
{ channelName: 'noiseDecay', value: params.noiseDecay },
{ channelName: 'filterResonance', value: params.filterResonance },
{ channelName: 'filterCutoff', value: params.filterCutoff },
{ channelName: 'attack', value: params.attack },
{ channelName: 'decay', value: params.decay },
{ channelName: 'waveshape', value: params.waveshape },
{ channelName: 'bodyResonance', value: params.bodyResonance },
{ channelName: 'click', value: params.click },
{ channelName: 'harmonicSpread', value: params.harmonicSpread },
];
}
randomParams(pitchLock?: PitchLock): AdditiveBassParams {
const baseFreq = pitchLock?.enabled ? pitchLock.frequency : this.randomRange(35, 80);
const overtoneMultChoices = [1.5, 2.0, 2.5, 3.0, 4.0];
return {
baseFreq,
pitchSweep: this.randomRange(0.3, 1.0),
pitchDecay: this.randomRange(0.02, 0.15),
overtoneAmp: this.randomRange(0.2, 0.7),
overtoneFreqMult: this.randomChoice(overtoneMultChoices),
noiseAmp: this.randomRange(0.05, 0.3),
noiseDecay: this.randomRange(0.01, 0.08),
filterResonance: this.randomRange(5, 25),
filterCutoff: this.randomRange(100, 800),
attack: this.randomRange(0.001, 0.02),
decay: this.randomRange(0.3, 0.8),
waveshape: this.randomRange(0, 0.7),
bodyResonance: this.randomRange(0, 0.5),
click: this.randomRange(0, 0.6),
harmonicSpread: this.randomRange(0, 0.5),
};
}
mutateParams(
params: AdditiveBassParams,
mutationAmount: number = 0.15,
pitchLock?: PitchLock
): AdditiveBassParams {
const baseFreq = pitchLock?.enabled ? pitchLock.frequency : params.baseFreq;
const overtoneMultChoices = [1.5, 2.0, 2.5, 3.0, 4.0];
return {
baseFreq,
pitchSweep: this.mutateValue(params.pitchSweep, mutationAmount, 0.1, 1.5),
pitchDecay: this.mutateValue(params.pitchDecay, mutationAmount, 0.01, 0.25),
overtoneAmp: this.mutateValue(params.overtoneAmp, mutationAmount, 0, 1.0),
overtoneFreqMult:
Math.random() < 0.1 ? this.randomChoice(overtoneMultChoices) : params.overtoneFreqMult,
noiseAmp: this.mutateValue(params.noiseAmp, mutationAmount, 0, 0.5),
noiseDecay: this.mutateValue(params.noiseDecay, mutationAmount, 0.005, 0.15),
filterResonance: this.mutateValue(params.filterResonance, mutationAmount, 2, 40),
filterCutoff: this.mutateValue(params.filterCutoff, mutationAmount, 80, 1200),
attack: this.mutateValue(params.attack, mutationAmount, 0.001, 0.05),
decay: this.mutateValue(params.decay, mutationAmount, 0.15, 0.95),
waveshape: this.mutateValue(params.waveshape, mutationAmount, 0, 1),
bodyResonance: this.mutateValue(params.bodyResonance, mutationAmount, 0, 0.8),
click: this.mutateValue(params.click, mutationAmount, 0, 0.8),
harmonicSpread: this.mutateValue(params.harmonicSpread, mutationAmount, 0, 0.7),
};
}
}

View File

@ -1,4 +1,4 @@
import type { SynthEngine, PitchLock } from './SynthEngine';
import type { SynthEngine, PitchLock } from './base/SynthEngine';
enum EnvCurve {
Linear,
@ -80,7 +80,7 @@ export interface AdditiveParams {
export class AdditiveEngine implements SynthEngine<AdditiveParams> {
getName(): string {
return 'Prism';
return 'Glass Prism';
}
getDescription(): string {
@ -91,6 +91,10 @@ export class AdditiveEngine implements SynthEngine<AdditiveParams> {
return 'generative' as const;
}
getCategory() {
return 'Additive' as const;
}
generate(params: AdditiveParams, sampleRate: number, duration: number): [Float32Array, Float32Array] {
const numSamples = Math.floor(sampleRate * duration);
const leftBuffer = new Float32Array(numSamples);

View File

@ -0,0 +1,608 @@
import type { PitchLock, SynthEngine } from './base/SynthEngine';
interface BassDrumParams {
// Core frequency (base pitch of the kick)
baseFreq: number;
// Pitch envelope (how much frequency sweeps down)
pitchMod: number;
// Pitch envelope speed
pitchDecay: number;
// Amplitude decay time
decay: number;
// Click/snap amount (high freq transient)
click: number;
// Attack noise burst (sharp transient)
attackNoise: number;
attackNoiseFreq: number; // Filter frequency for attack noise
// Body resonance
bodyResonance: number;
bodyFreq: number;
// Body tone variation
bodyDecay: number;
// Noise amount
noise: number;
// Noise decay
noiseDecay: number;
// Harmonic content
harmonics: number;
// Wave shape (0 = sine, 0.5 = triangle, 1 = square)
waveShape: number;
// Phase distortion amount
phaseDistortion: number;
// Distortion amount (0 = clean, 1 = heavy)
distortion: number;
// Tuning offset
tuning: number;
// Envelope curve parameters
attackTime: number; // Attack duration (0 = 0.5ms, 1 = 50ms)
attackCurve: number; // Attack curve shape (0 = soft/slow, 1 = sharp/fast)
ampDecayCurve: number; // Amplitude decay curve (0 = loose/boomy, 1 = tight/punchy)
pitchDecayCurve: number; // Pitch envelope curve (0 = loose, 1 = tight)
}
export class BassDrum implements SynthEngine {
getName(): string {
return 'Dark Kick';
}
getDescription(): string {
return 'Versatile kick drum synthesizer with varied styles from sub to tom';
}
getType() {
return 'generative' as const;
}
getCategory() {
return 'Percussion' as const;
}
randomParams(pitchLock?: PitchLock): BassDrumParams {
// Choose a kick character/style
const styleRoll = Math.random();
let baseFreq: number, pitchMod: number, pitchDecay: number, decay: number;
let click: number, attackNoise: number, attackNoiseFreq: number;
let bodyResonance: number, bodyFreq: number, bodyDecay: number;
let noise: number, noiseDecay: number, harmonics: number;
let waveShape: number, phaseDistortion: number, distortion: number;
let attackTime: number, attackCurve: number, ampDecayCurve: number, pitchDecayCurve: number;
if (styleRoll < 0.08) {
// Tom/bongo style (starts HIGH, decays VERY fast - rare!)
baseFreq = 0.6 + Math.random() * 0.4; // 83-115 Hz (very high!)
pitchMod = 0.8 + Math.random() * 0.2; // Extreme pitch sweep
pitchDecay = 0.01 + Math.random() * 0.08; // Ultra fast pitch decay (10-90ms)
decay = 0.05 + Math.random() * 0.25; // Very short decay
click = 0.3 + Math.random() * 0.5; // Good click
attackNoise = 0.4 + Math.random() * 0.6; // Strong attack
attackNoiseFreq = 0.7 + Math.random() * 0.3; // High filtered
bodyResonance = 0.1 + Math.random() * 0.3;
bodyFreq = 0.5 + Math.random() * 0.5;
bodyDecay = 0.05 + Math.random() * 0.25; // Short body decay
noise = Math.random() * 0.08;
noiseDecay = 0.02 + Math.random() * 0.15; // Fast noise decay
harmonics = 0.2 + Math.random() * 0.5; // Lots of harmonics
waveShape = 0.3 + Math.random() * 0.4; // Triangle-ish
phaseDistortion = 0.1 + Math.random() * 0.4; // Some phase distortion
distortion = 0; // Clean
attackTime = 0.0 + Math.random() * 0.2; // Very fast attack (0.5-10ms)
attackCurve = 0.6 + Math.random() * 0.3; // Sharp attack (0.6-0.9)
ampDecayCurve = 0.7 + Math.random() * 0.3; // Very tight decay (0.7-1.0)
pitchDecayCurve = 0.6 + Math.random() * 0.3; // Fast pitch sweep (0.6-0.9)
} else if (styleRoll < 0.25) {
// Tight, punchy 808-style kick (short, lots of pitch, CLEAN)
baseFreq = 0.2 + Math.random() * 0.4; // 56-88 Hz (mid-high kicks)
pitchMod = 0.7 + Math.random() * 0.3; // High pitch sweep
pitchDecay = 0.05 + Math.random() * 0.4; // Fast to medium pitch decay
decay = 0.1 + Math.random() * 0.35; // Short to medium decay
click = 0.2 + Math.random() * 0.4; // Good click
attackNoise = 0.3 + Math.random() * 0.5; // Sharp attack
attackNoiseFreq = 0.5 + Math.random() * 0.5; // Mid-high filtered
bodyResonance = 0.05 + Math.random() * 0.2;
bodyFreq = 0.3 + Math.random() * 0.5;
bodyDecay = 0.1 + Math.random() * 0.5; // Varied body decay
noise = Math.random() * 0.05; // Very minimal
noiseDecay = 0.05 + Math.random() * 0.4; // Varied noise decay
harmonics = 0.1 + Math.random() * 0.3;
waveShape = 0.2 + Math.random() * 0.5; // Sine to triangle
phaseDistortion = Math.random() * 0.3; // Light phase distortion
distortion = 0; // Clean
attackTime = 0.0 + Math.random() * 0.3; // Fast attack (0.5-15ms)
attackCurve = 0.3 + Math.random() * 0.3; // Medium-sharp attack (0.3-0.6)
ampDecayCurve = 0.5 + Math.random() * 0.3; // Medium-tight decay (0.5-0.8)
pitchDecayCurve = 0.4 + Math.random() * 0.3; // Medium pitch sweep (0.4-0.7)
} else if (styleRoll < 0.43) {
// Deep, smooth kick (long, clean, lots of body)
baseFreq = 0.0 + Math.random() * 0.35; // 40-68 Hz (low to mid kicks)
pitchMod = 0.3 + Math.random() * 0.4; // Moderate pitch sweep
pitchDecay = 0.2 + Math.random() * 0.6; // Medium to slow pitch decay
decay = 0.4 + Math.random() * 0.6; // Long decay
click = Math.random() * 0.2; // Minimal click
attackNoise = 0.1 + Math.random() * 0.4; // Some attack
attackNoiseFreq = 0.3 + Math.random() * 0.4; // Mid filtered
bodyResonance = 0.2 + Math.random() * 0.5; // Lots of body
bodyFreq = 0.1 + Math.random() * 0.4;
bodyDecay = 0.3 + Math.random() * 0.7; // Long body decay
noise = Math.random() * 0.08;
noiseDecay = 0.2 + Math.random() * 0.6; // Varied noise decay
harmonics = Math.random() * 0.2;
waveShape = Math.random() * 0.4; // Sine to triangle
phaseDistortion = Math.random() * 0.25; // Subtle phase distortion
distortion = 0; // Clean
attackTime = 0.1 + Math.random() * 0.4; // Slower attack (5-25ms)
attackCurve = 0.2 + Math.random() * 0.3; // Soft attack (0.2-0.5)
ampDecayCurve = 0.2 + Math.random() * 0.3; // Loose, boomy decay (0.2-0.5)
pitchDecayCurve = 0.2 + Math.random() * 0.3; // Slow pitch sweep (0.2-0.5)
} else if (styleRoll < 0.63) {
// Sub/electronic kick (very low, pure sine-like)
baseFreq = 0.0 + Math.random() * 0.25; // 40-60 Hz (sub bass kicks)
pitchMod = 0.1 + Math.random() * 0.4; // Low to moderate pitch sweep
pitchDecay = 0.15 + Math.random() * 0.5; // Varied pitch decay
decay = 0.4 + Math.random() * 0.6; // Long decay
click = Math.random() * 0.15; // Almost no click
attackNoise = Math.random() * 0.3; // Variable attack
attackNoiseFreq = 0.2 + Math.random() * 0.3; // Low filtered
bodyResonance = 0.3 + Math.random() * 0.6; // Strong body
bodyFreq = 0.05 + Math.random() * 0.3;
bodyDecay = 0.4 + Math.random() * 0.6; // Long body decay
noise = Math.random() * 0.03; // Almost no noise
noiseDecay = 0.1 + Math.random() * 0.4; // Varied noise decay
harmonics = Math.random() * 0.15;
waveShape = Math.random() * 0.3; // Mostly sine
phaseDistortion = Math.random() * 0.2; // Very subtle
distortion = 0; // Clean
attackTime = 0.2 + Math.random() * 0.4; // Medium attack (10-30ms)
attackCurve = 0.3 + Math.random() * 0.3; // Medium attack (0.3-0.6)
ampDecayCurve = 0.1 + Math.random() * 0.3; // Very loose, sustaining (0.1-0.4)
pitchDecayCurve = 0.1 + Math.random() * 0.3; // Gentle pitch sweep (0.1-0.4)
} else if (styleRoll < 0.78) {
// Snappy, clicky kick (fast attack, short)
baseFreq = 0.3 + Math.random() * 0.5; // 64-100 Hz (high kicks)
pitchMod = 0.5 + Math.random() * 0.5; // High pitch sweep
pitchDecay = 0.02 + Math.random() * 0.25; // Very fast pitch
decay = 0.08 + Math.random() * 0.35; // Very short to short
click = 0.4 + Math.random() * 0.6; // Lots of click
attackNoise = 0.5 + Math.random() * 0.5; // Strong attack burst
attackNoiseFreq = 0.6 + Math.random() * 0.4; // High filtered
bodyResonance = Math.random() * 0.2; // Minimal body
bodyFreq = 0.4 + Math.random() * 0.6;
bodyDecay = 0.05 + Math.random() * 0.3; // Short body decay
noise = Math.random() * 0.1;
noiseDecay = 0.02 + Math.random() * 0.2; // Fast noise decay
harmonics = 0.2 + Math.random() * 0.5;
waveShape = 0.4 + Math.random() * 0.6; // Triangle to square
phaseDistortion = 0.2 + Math.random() * 0.5; // More phase distortion
distortion = 0; // Clean
attackTime = 0.0 + Math.random() * 0.15; // Instant attack (0.5-8ms)
attackCurve = 0.7 + Math.random() * 0.3; // Very sharp attack (0.7-1.0)
ampDecayCurve = 0.7 + Math.random() * 0.3; // Very tight (0.7-1.0)
pitchDecayCurve = 0.7 + Math.random() * 0.3; // Fast pitch sweep (0.7-1.0)
} else if (styleRoll < 0.88) {
// Weird/experimental kick
baseFreq = Math.random() * 0.7; // Full range 35-96 Hz
pitchMod = Math.random();
pitchDecay = 0.05 + Math.random() * 0.8; // Any decay
decay = 0.1 + Math.random() * 0.8; // Any decay
click = Math.random() * 0.5;
attackNoise = Math.random() * 0.7;
attackNoiseFreq = Math.random();
bodyResonance = Math.random() * 0.7;
bodyFreq = Math.random();
bodyDecay = 0.1 + Math.random() * 0.9; // Any body decay
noise = Math.random() * 0.15;
noiseDecay = 0.05 + Math.random() * 0.8; // Any noise decay
harmonics = Math.random() * 0.6;
waveShape = Math.random(); // Any shape
phaseDistortion = Math.random() * 0.7; // Any amount
distortion = 0; // Clean
attackTime = Math.random(); // Full range (0.5-50ms)
attackCurve = Math.random(); // Full range (0.0-1.0)
ampDecayCurve = Math.random(); // Full range (0.0-1.0)
pitchDecayCurve = Math.random(); // Full range (0.0-1.0)
} else {
// Dirty/distorted kick (only 15% of the time!)
baseFreq = 0.1 + Math.random() * 0.5; // 44-84 Hz (low to mid)
pitchMod = 0.4 + Math.random() * 0.5;
pitchDecay = 0.1 + Math.random() * 0.6; // Varied pitch decay
decay = 0.2 + Math.random() * 0.6; // Varied decay
click = 0.2 + Math.random() * 0.5;
attackNoise = 0.3 + Math.random() * 0.5;
attackNoiseFreq = 0.4 + Math.random() * 0.5;
bodyResonance = 0.1 + Math.random() * 0.5;
bodyFreq = 0.2 + Math.random() * 0.6;
bodyDecay = 0.15 + Math.random() * 0.7; // Varied body decay
noise = 0.05 + Math.random() * 0.2;
noiseDecay = 0.15 + Math.random() * 0.6; // Varied noise decay
harmonics = 0.1 + Math.random() * 0.4;
waveShape = 0.3 + Math.random() * 0.5; // Varied shapes
phaseDistortion = 0.2 + Math.random() * 0.6; // Good amount
distortion = 0.3 + Math.random() * 0.5; // Only this style has distortion
attackTime = 0.1 + Math.random() * 0.4; // Medium attack (5-25ms)
attackCurve = 0.4 + Math.random() * 0.4; // Medium-sharp attack (0.4-0.8)
ampDecayCurve = 0.3 + Math.random() * 0.4; // Medium varied (0.3-0.7)
pitchDecayCurve = 0.3 + Math.random() * 0.4; // Medium varied (0.3-0.7)
}
return {
baseFreq: pitchLock ? this.freqToParam(pitchLock.frequency) : baseFreq,
pitchMod,
pitchDecay,
decay,
click,
attackNoise,
attackNoiseFreq,
bodyResonance,
bodyFreq,
bodyDecay,
noise,
noiseDecay,
harmonics,
waveShape,
phaseDistortion,
distortion,
tuning: pitchLock ? 0.5 : 0.4 + Math.random() * 0.2,
attackTime,
attackCurve,
ampDecayCurve,
pitchDecayCurve,
};
}
mutateParams(params: BassDrumParams, mutationAmount: number = 0.15, pitchLock?: PitchLock): BassDrumParams {
const mutate = (value: number, amount: number = mutationAmount): number => {
return Math.max(0, Math.min(1, value + (Math.random() - 0.5) * amount));
};
return {
baseFreq: pitchLock ? params.baseFreq : mutate(params.baseFreq, 0.2),
pitchMod: mutate(params.pitchMod, 0.2),
pitchDecay: mutate(params.pitchDecay, 0.2),
decay: mutate(params.decay, 0.25),
click: mutate(params.click, 0.2),
attackNoise: mutate(params.attackNoise, 0.25),
attackNoiseFreq: mutate(params.attackNoiseFreq, 0.25),
bodyResonance: mutate(params.bodyResonance, 0.2),
bodyFreq: mutate(params.bodyFreq, 0.25),
bodyDecay: mutate(params.bodyDecay, 0.2),
noise: mutate(params.noise, 0.15),
noiseDecay: mutate(params.noiseDecay, 0.2),
harmonics: mutate(params.harmonics, 0.2),
waveShape: mutate(params.waveShape, 0.25),
phaseDistortion: mutate(params.phaseDistortion, 0.25),
distortion: mutate(params.distortion, 0.2),
tuning: pitchLock ? params.tuning : mutate(params.tuning, 0.15),
attackTime: mutate(params.attackTime, 0.15),
attackCurve: mutate(params.attackCurve, 0.2),
ampDecayCurve: mutate(params.ampDecayCurve, 0.2),
pitchDecayCurve: mutate(params.pitchDecayCurve, 0.2),
};
}
generate(
params: BassDrumParams,
sampleRate: number,
duration: number,
pitchLock?: PitchLock
): [Float32Array, Float32Array] {
const numSamples = Math.floor(sampleRate * duration);
const left = new Float32Array(numSamples);
const right = new Float32Array(numSamples);
// Base frequency: 35Hz to 115Hz (full kick range from sub to high)
const baseFreq = pitchLock ? pitchLock.frequency : 35 + params.baseFreq * 80;
// Tuning offset: -12% to +12%
const tuningFactor = 0.88 + params.tuning * 0.24;
const tunedFreq = baseFreq * tuningFactor;
// Pitch modulation: when locked, starts at locked freq and decays down
// when not locked, starts higher and decays to base
const pitchMultiplier = 1 + params.pitchMod * 1.5; // 1x to 2.5x
const pitchDecayRatio = pitchLock ? 1 / pitchMultiplier : pitchMultiplier; // Invert for pitch lock
// Pitch decay time scaled by duration (50ms to 200ms)
const pitchDecayTime = (0.05 + params.pitchDecay * 0.15) * duration;
// Amplitude decay time scaled by duration
const ampDecayTime = (0.2 + params.decay * 1.8) * duration;
// Noise decay time
const noiseDecayTime = (0.02 + params.noiseDecay * 0.15) * duration; // 20ms to 170ms
// Body resonance frequency (40Hz to 200Hz for low body resonances)
const bodyFreq = 40 + params.bodyFreq * 160;
const bodyDecayTime = (0.1 + params.bodyDecay * 0.4) * duration; // 100ms to 500ms
// Attack time (0.5ms to 50ms, scaled by duration)
const attackTime = (0.0005 + params.attackTime * 0.0495) * duration;
// Click decay (very fast for transient)
const clickDecayTime = 0.003 + params.click * 0.007; // 3ms to 10ms
// Attack noise burst decay (super fast: 1-5ms)
const attackNoiseDecayTime = 0.001 + params.attackNoise * 0.004; // 1ms to 5ms
// Attack noise filter frequency (500Hz to 8000Hz)
const attackNoiseFilterFreq = 500 + params.attackNoiseFreq * 7500;
// Low-pass filter at 3000Hz
const filterFreq = 3000;
for (let channel = 0; channel < 2; channel++) {
const output = channel === 0 ? left : right;
// Triangle oscillator phase
let phase = 0;
// Low-pass filter state
let filterState = 0;
// Body resonance filter state
let bodyState1 = 0;
let bodyState2 = 0;
// Attack noise filter state (highpass for snap)
let attackNoiseState1 = 0;
let attackNoiseState2 = 0;
// Stereo variation
const stereoDetune = channel === 0 ? 0.999 : 1.001;
const stereoPhaseOffset = channel === 0 ? 0 : 0.05;
for (let i = 0; i < numSamples; i++) {
const t = i / sampleRate;
// Pitch envelope: curved decay
const pitchEnv = this.applyDecayCurve(t, pitchDecayTime, params.pitchDecayCurve);
// When pitch locked: start at locked freq, decay down
// When not locked: start higher, decay to base
const currentFreq = pitchLock
? tunedFreq * stereoDetune * (pitchDecayRatio + (1 - pitchDecayRatio) * pitchEnv)
: tunedFreq * stereoDetune * (1 + (pitchMultiplier - 1) * pitchEnv);
// Generate oscillator with wave shape morphing and phase distortion
phase += (2 * Math.PI * currentFreq) / sampleRate;
if (phase > 2 * Math.PI) phase -= 2 * Math.PI;
// Apply phase distortion (modulate the phase itself)
let distortedPhase = phase;
if (params.phaseDistortion > 0.01) {
// Phase distortion creates harmonic richness
const phaseMod = Math.sin(phase * 2) * params.phaseDistortion * 0.5;
distortedPhase = phase + phaseMod;
if (distortedPhase < 0) distortedPhase += 2 * Math.PI;
if (distortedPhase > 2 * Math.PI) distortedPhase -= 2 * Math.PI;
}
// Wave shape morphing: 0=sine, 0.5=triangle, 1=square
let waveform: number;
// Generate sine wave
const sine = Math.sin(distortedPhase);
// Generate triangle wave
const triangle = distortedPhase < Math.PI
? -1 + (2 * distortedPhase) / Math.PI
: 3 - (2 * distortedPhase) / Math.PI;
// Generate square wave
const square = distortedPhase < Math.PI ? 1 : -1;
// Morph between shapes
if (params.waveShape < 0.5) {
// Morph between sine and triangle
const mix = params.waveShape * 2; // 0 to 1
waveform = sine * (1 - mix) + triangle * mix;
} else {
// Morph between triangle and square
const mix = (params.waveShape - 0.5) * 2; // 0 to 1
waveform = triangle * (1 - mix) + square * mix;
}
// Attack envelope (curved)
const attackEnv = t < attackTime
? this.applyAttackCurve(t, attackTime, params.attackCurve)
: 1.0;
// Amplitude envelope: curved decay with attack
const ampEnv = this.applyDecayCurve(t, ampDecayTime, params.ampDecayCurve) * attackEnv;
// 1. Morphed oscillator (main tone)
let signal = waveform * ampEnv * 0.6;
// 1b. Add harmonics (2nd and 3rd) for more character
if (params.harmonics > 0.01) {
const harmonic2 = Math.sin(2 * distortedPhase) * params.harmonics * 0.3;
const harmonic3 = Math.sin(3 * distortedPhase) * params.harmonics * 0.15;
signal += (harmonic2 + harmonic3) * ampEnv;
}
// 2. Attack noise burst (super sharp transient, 1-5ms)
if (params.attackNoise > 0.01) {
const attackEnv = Math.exp(-t / attackNoiseDecayTime);
const attackNoise = Math.random() * 2 - 1;
// Highpass filter the attack noise for snap
const attackNoiseFiltered = this.highpassFilter(
attackNoise,
attackNoiseFilterFreq,
2.0,
sampleRate,
attackNoiseState1,
attackNoiseState2
);
attackNoiseState1 = attackNoiseFiltered.state1;
attackNoiseState2 = attackNoiseFiltered.state2;
signal += attackNoiseFiltered.output * params.attackNoise * attackEnv * 0.4;
}
// 3. Click/snap transient (fast decaying high-pitched sine)
if (params.click > 0.01) {
const clickEnv = Math.exp(-t / clickDecayTime);
const clickFreq = tunedFreq * 4; // High frequency for click
const clickOsc = Math.sin(2 * Math.PI * clickFreq * t);
signal += clickOsc * params.click * clickEnv * 0.15;
}
// 4. Body resonance (bandpass filtered feedback with decay)
if (params.bodyResonance > 0.05) {
const bodyEnv = Math.exp(-t / bodyDecayTime);
const bodyFiltered = this.bandpassFilter(
signal,
bodyFreq,
5 + params.bodyResonance * 10,
sampleRate,
bodyState1,
bodyState2
);
bodyState1 = bodyFiltered.state1;
bodyState2 = bodyFiltered.state2;
signal += bodyFiltered.output * params.bodyResonance * bodyEnv * 0.2;
}
// 5. Add sustaining noise
if (params.noise > 0.01) {
const noise = Math.random() * 2 - 1;
const noiseEnv = Math.exp(-t / noiseDecayTime);
signal += noise * params.noise * noiseEnv * 0.1;
}
// 6. Optional distortion/saturation (only if distortion > 0.2)
if (params.distortion > 0.2) {
const distAmount = 1 + params.distortion * 3;
signal = this.waveshaper(signal * distAmount, distAmount) / distAmount;
}
// 7. Low-pass filter at 3000Hz
const normalizedFreq = Math.min(filterFreq / sampleRate, 0.48);
const a = 2 * Math.PI * normalizedFreq;
filterState += a * (signal - filterState);
signal = filterState;
output[i] = signal;
}
}
// Normalize output to consistent level
let peak = 0;
for (let i = 0; i < numSamples; i++) {
peak = Math.max(peak, Math.abs(left[i]), Math.abs(right[i]));
}
if (peak > 0.001) {
const normGain = 0.5 / peak; // Normalize to 0.5 peak for more headroom
for (let i = 0; i < numSamples; i++) {
left[i] *= normGain;
right[i] *= normGain;
}
}
return [left, right];
}
private applyDecayCurve(t: number, decayTime: number, curveParam: number): number {
const normalized = Math.min(t / decayTime, 5);
const baseEnv = Math.exp(-normalized);
const exponent = 0.4 + curveParam * 2.1;
return Math.pow(baseEnv, exponent);
}
private applyAttackCurve(t: number, attackTime: number, curveParam: number): number {
const normalized = Math.min(t / attackTime, 1);
const exponent = 0.4 + curveParam * 2.1;
return Math.pow(normalized, exponent);
}
private bandpassFilter(
input: number,
freq: number,
q: number,
sampleRate: number,
state1: number,
state2: number
): { output: number; state1: number; state2: number } {
// State variable filter in bandpass mode
const normalizedFreq = Math.min(freq / sampleRate, 0.48);
const f = 2 * Math.sin(Math.PI * normalizedFreq);
const qRecip = 1 / Math.max(q, 0.5);
const lowpass = state2 + f * state1;
const highpass = input - lowpass - qRecip * state1;
const bandpass = f * highpass + state1;
// Update states with clamping
const newState1 = Math.max(-2, Math.min(2, bandpass));
const newState2 = Math.max(-2, Math.min(2, lowpass));
return {
output: bandpass,
state1: newState1,
state2: newState2,
};
}
private highpassFilter(
input: number,
freq: number,
q: number,
sampleRate: number,
state1: number,
state2: number
): { output: number; state1: number; state2: number } {
// State variable filter in highpass mode
const normalizedFreq = Math.min(freq / sampleRate, 0.48);
const f = 2 * Math.sin(Math.PI * normalizedFreq);
const qRecip = 1 / Math.max(q, 0.5);
const lowpass = state2 + f * state1;
const highpass = input - lowpass - qRecip * state1;
const bandpass = f * highpass + state1;
// Update states with clamping
const newState1 = Math.max(-2, Math.min(2, bandpass));
const newState2 = Math.max(-2, Math.min(2, lowpass));
return {
output: highpass,
state1: newState1,
state2: newState2,
};
}
private waveshaper(x: number, amount: number): number {
const PI = Math.PI;
if (Math.abs(x) < 0.001) return x;
return ((PI + amount) * x) / (PI + amount * Math.abs(x));
}
private softClip(x: number): number {
if (x > 1) return 1;
if (x < -1) return -1;
if (x > 0.66) return (3 - (2 - 3 * x) ** 2) / 3;
if (x < -0.66) return -(3 - (2 - 3 * -x) ** 2) / 3;
return x;
}
private freqToParam(freq: number): number {
// Map frequency to 0-1 range (35-115 Hz)
return Math.max(0, Math.min(1, (freq - 35) / 80));
}
}

View File

@ -1,4 +1,4 @@
import type { SynthEngine, PitchLock } from './SynthEngine';
import type { SynthEngine, PitchLock } from './base/SynthEngine';
interface BenjolinParams {
// Core oscillators
@ -73,6 +73,10 @@ export class Benjolin implements SynthEngine<BenjolinParams> {
return 'generative' as const;
}
getCategory() {
return 'Experimental' as const;
}
generate(params: BenjolinParams, sampleRate: number, duration: number): [Float32Array, Float32Array] {
const numSamples = Math.floor(duration * sampleRate);
const left = new Float32Array(numSamples);

View File

@ -0,0 +1,350 @@
import { CsoundEngine, type CsoundParameter } from './base/CsoundEngine';
import type { PitchLock } from './base/SynthEngine';
enum SourceType {
Saw = 0,
Pulse = 1,
NoiseBurst = 2,
Triangle = 3,
}
interface CombParams {
feedback: number;
brightness: number;
}
interface ResonatorParams {
frequency: number;
resonance: number;
envAmount: number;
attack: number;
decay: number;
sustain: number;
release: number;
}
interface DelayParams {
time1: number;
time2: number;
feedback: number;
filterFreq: number;
filterSweep: number;
mix: number;
}
export interface CombResonatorParams {
baseFreq: number;
sourceType: SourceType;
sourceDecay: number;
comb: CombParams;
resonator: ResonatorParams;
delay: DelayParams;
stereoWidth: number;
}
export class CombResonator extends CsoundEngine<CombResonatorParams> {
getName(): string {
return 'CombRes';
}
getDescription(): string {
return 'Comb filter and resonator for metallic and bell-like sounds';
}
getType() {
return 'generative' as const;
}
getCategory() {
return 'Subtractive' as const;
}
protected getOrchestra(): string {
return `
instr 1
ibasefreq chnget "basefreq"
isourcetype chnget "sourcetype"
isourcedecay chnget "sourcedecay"
icombfeedback chnget "comb_feedback"
icombbright chnget "comb_brightness"
iresfreq chnget "res_frequency"
iresres chnget "res_resonance"
iresenvamt chnget "res_envamt"
iresattack chnget "res_attack"
iresdecay chnget "res_decay"
iressustain chnget "res_sustain"
iresrelease chnget "res_release"
ideltime1 chnget "delay_time1"
ideltime2 chnget "delay_time2"
idelfeedback chnget "delay_feedback"
ifilterfreq chnget "delay_filterfreq"
ifiltersweep chnget "delay_filtersweep"
idelmix chnget "delay_mix"
istereo chnget "stereowidth"
idur = p3
; Convert ratios to time values
iresatt = iresattack * idur
iresdec = iresdecay * idur
iresrel = iresrelease * idur
isrcdec = isourcedecay * idur
idt1 = ideltime1 * idur
idt2 = ideltime2 * idur
; Stereo detuning
idetune = 1 + (istereo * 0.003)
ifreqL = ibasefreq / idetune
ifreqR = ibasefreq * idetune
; Source envelope - sharp attack, exponential decay
ksrcenv expon 1, isrcdec, 0.001
; Generate source signals with more amplitude
if isourcetype == 0 then
; Saw wave with harmonics
asrcL vco2 ksrcenv * 0.8, ifreqL, 0
asrcR vco2 ksrcenv * 0.8, ifreqR, 0
elseif isourcetype == 1 then
; Pulse wave
asrcL vco2 ksrcenv * 0.8, ifreqL, 2, 0.3
asrcR vco2 ksrcenv * 0.8, ifreqR, 2, 0.3
elseif isourcetype == 2 then
; Noise burst with pitch
anoise1 noise ksrcenv * 0.6, 0
anoise2 noise ksrcenv * 0.6, 0
asrcL butterbp anoise1, ifreqL, ifreqL * 2
asrcR butterbp anoise2, ifreqR, ifreqR * 2
else
; Triangle wave
asrcL vco2 ksrcenv * 0.8, ifreqL, 12
asrcR vco2 ksrcenv * 0.8, ifreqR, 12
endif
; Direct path for immediate sound (no delay)
adirectL = asrcL * 0.3
adirectR = asrcR * 0.3
; Comb filter using vcomb for better feedback control
idelaytimeL = 1 / ifreqL
idelaytimeR = 1 / ifreqR
ilooptime = 0.1
acombL vcomb asrcL, idelaytimeL, ilooptime, icombfeedback
acombR vcomb asrcR, idelaytimeR, ilooptime, icombfeedback
; Damping filter
adampl tone acombL, ibasefreq * icombbright
adampR tone acombR, ibasefreq * icombbright
; Resonator envelope for filter sweep
kresenv madsr iresatt, iresdec, iressustain, iresrel
kresfreq = iresfreq + (kresenv * iresenvamt * ibasefreq * 4)
kresfreq = limit(kresfreq, 80, 18000)
; Multiple resonators for richer tone
kbw = kresfreq / iresres
ares1L butterbp adampl, kresfreq, kbw
ares1R butterbp adampR, kresfreq, kbw
ares2L butterbp adampl, kresfreq * 1.5, kbw * 1.2
ares2R butterbp adampR, kresfreq * 1.5, kbw * 1.2
; Mix resonators
aresL = ares1L + (ares2L * 0.5)
aresR = ares1R + (ares2R * 0.5)
; Dry signal (this plays immediately)
adryL = (adampl * 0.2) + (aresL * 0.8)
adryR = (adampR * 0.2) + (aresR * 0.8)
; Direct signal envelope: loud at start, fades out quickly
kdirectenv linseg 1, idur * 0.05, 0, idur * 0.95, 0
; Crossfade envelope: start with dry signal, fade in delays
kdryenv linseg 0.6, idur * 0.15, 0.3, idur * 0.85, 0.2
kwetenv linseg 0, idur * 0.1, 1, idur * 0.9, 1
; Extreme filter sweep envelopes with multiple segments
kfilter1 linseg ifilterfreq, idur * 0.2, ifilterfreq + (ifiltersweep * 12000), idur * 0.3, ifilterfreq - (ifiltersweep * 5000), idur * 0.5, ifilterfreq + (ifiltersweep * 15000)
kfilter1 = limit(kfilter1, 250, 19000)
kfilter2 expseg ifilterfreq * 2, idur * 0.15, ifilterfreq * 0.3 + 200, idur * 0.35, ifilterfreq * 3 + 100, idur * 0.5, ifilterfreq + (ifiltersweep * 10000) + 100
kfilter2 = limit(kfilter2, 200, 18000)
kfilter3 linseg ifilterfreq * 0.5, idur * 0.25, ifilterfreq + (ifiltersweep * 8000), idur * 0.25, ifilterfreq * 2, idur * 0.5, ifilterfreq - (ifiltersweep * 3000)
kfilter3 = limit(kfilter3, 300, 16000)
kfilter4 expon ifilterfreq + 100, idur, ifilterfreq + (ifiltersweep * 14000) + 100
kfilter4 = limit(kfilter4, 350, 17000)
; LFO for delay time modulation
klfo1 lfo 0.03, 3 + (ifiltersweep * 2)
klfo2 lfo 0.04, 5 - (ifiltersweep * 1.5)
; Multi-tap delay line 1 (Left -> Right) with modulation
abuf1 delayr idt1 * 1.1
kdt1a = (idt1 * 0.2) + klfo1
kdt1b = (idt1 * 0.45) - klfo2
kdt1c = (idt1 * 0.75) + (klfo1 * 0.5)
kdt1d = idt1
adel1a deltap3 kdt1a
adel1b deltap3 kdt1b
adel1c deltap3 kdt1c
adel1d deltap3 kdt1d
delayw adryL + (adel1d * idelfeedback * 0.95)
afilt1a butterbp adel1a, kfilter1, kfilter1 * 0.15
afilt1b butterbp adel1b, kfilter2, kfilter2 * 0.2
afilt1c butterbp adel1c, kfilter3, kfilter3 * 0.25
afilt1d butterbp adel1d, kfilter4, kfilter4 * 0.18
adelR = (afilt1a * 0.6) + (afilt1b * 0.8) + (afilt1c * 0.9) + (afilt1d * 0.7)
; Multi-tap delay line 2 (Right -> Left) with modulation
abuf2 delayr idt2 * 1.1
kdt2a = (idt2 * 0.18) - klfo2
kdt2b = (idt2 * 0.42) + klfo1
kdt2c = (idt2 * 0.7) - (klfo2 * 0.5)
kdt2d = idt2
adel2a deltap3 kdt2a
adel2b deltap3 kdt2b
adel2c deltap3 kdt2c
adel2d deltap3 kdt2d
delayw adryR + (adel2d * idelfeedback * 0.95)
afilt2a butterbp adel2a, kfilter1 * 1.4, kfilter1 * 0.12
afilt2b butterbp adel2b, kfilter2 * 0.7, kfilter2 * 0.22
afilt2c butterbp adel2c, kfilter3 * 1.2, kfilter3 * 0.16
afilt2d butterbp adel2d, kfilter4 * 0.9, kfilter4 * 0.2
adelL = (afilt2a * 0.7) + (afilt2b * 0.6) + (afilt2c * 0.85) + (afilt2d * 0.8)
; Additional chaotic resonant delays
abuf3 delayr idt1 * 1.6
kdt3 = (idt1 * 1.4) + (klfo1 * 2)
adel3 deltap3 kdt3
delayw (adryL + adryR) * 0.5 + (adel3 * idelfeedback * 0.85)
afilt3 butterbp adel3, kfilter2 * 0.6, kfilter2 * 0.1
abuf4 delayr idt2 * 1.8
kdt4 = (idt2 * 1.6) - (klfo2 * 2)
adel4 deltap3 kdt4
delayw (adryR + adryL) * 0.5 + (adel4 * idelfeedback * 0.8)
afilt4 butterbp adel4, kfilter3 * 1.3, kfilter3 * 0.12
abuf5 delayr idt1 * 2.2
kdt5 = (idt1 * 2.0) + klfo1 + klfo2
adel5 deltap3 kdt5
delayw (adelL + adelR) * 0.4 + (adel5 * idelfeedback * 0.75)
afilt5 butterbp adel5, kfilter4 * 0.8, kfilter4 * 0.08
; Mix: direct signal (immediate), dry (soon after), delays (build up)
amixL = (adirectL * kdirectenv) + (adryL * kdryenv) + ((adelL * 1.2 + afilt3 * 0.8 + afilt4 * 0.7 + afilt5 * 0.6) * idelmix * kwetenv)
amixR = (adirectR * kdirectenv) + (adryR * kdryenv) + ((adelR * 1.2 + afilt4 * 0.8 + afilt3 * 0.7 + afilt5 * 0.6) * idelmix * kwetenv)
outs amixL, amixR
endin
`;
}
protected getParametersForCsound(params: CombResonatorParams): CsoundParameter[] {
return [
{ channelName: 'basefreq', value: params.baseFreq },
{ channelName: 'sourcetype', value: params.sourceType },
{ channelName: 'sourcedecay', value: params.sourceDecay },
{ channelName: 'comb_feedback', value: params.comb.feedback },
{ channelName: 'comb_brightness', value: params.comb.brightness },
{ channelName: 'res_frequency', value: params.resonator.frequency },
{ channelName: 'res_resonance', value: params.resonator.resonance },
{ channelName: 'res_envamt', value: params.resonator.envAmount },
{ channelName: 'res_attack', value: params.resonator.attack },
{ channelName: 'res_decay', value: params.resonator.decay },
{ channelName: 'res_sustain', value: params.resonator.sustain },
{ channelName: 'res_release', value: params.resonator.release },
{ channelName: 'delay_time1', value: params.delay.time1 },
{ channelName: 'delay_time2', value: params.delay.time2 },
{ channelName: 'delay_feedback', value: params.delay.feedback },
{ channelName: 'delay_filterfreq', value: params.delay.filterFreq },
{ channelName: 'delay_filtersweep', value: params.delay.filterSweep },
{ channelName: 'delay_mix', value: params.delay.mix },
{ channelName: 'stereowidth', value: params.stereoWidth },
];
}
randomParams(pitchLock?: PitchLock): CombResonatorParams {
let baseFreq: number;
if (pitchLock?.enabled) {
baseFreq = pitchLock.frequency;
} else {
const baseFreqChoices = [82.4, 110, 146.8, 220, 293.7, 440, 587.3, 880];
baseFreq = this.randomChoice(baseFreqChoices) * this.randomRange(0.97, 1.03);
}
return {
baseFreq,
sourceType: this.randomInt(0, 3) as SourceType,
sourceDecay: this.randomRange(0.02, 0.15),
comb: {
feedback: this.randomRange(0.7, 0.95),
brightness: this.randomRange(3, 20),
},
resonator: {
frequency: baseFreq * this.randomChoice([1, 1.3, 1.6, 2, 2.4, 3, 3.5, 4, 5]),
resonance: this.randomRange(8, 80),
envAmount: this.randomRange(0.3, 2.0),
attack: this.randomRange(0.001, 0.05),
decay: this.randomRange(0.15, 0.6),
sustain: this.randomRange(0.05, 0.4),
release: this.randomRange(0.3, 0.8),
},
delay: {
time1: this.randomRange(0.08, 0.35),
time2: this.randomRange(0.1, 0.4),
feedback: this.randomRange(0.82, 0.98),
filterFreq: this.randomRange(400, 5000),
filterSweep: this.randomRange(-1.2, 2.0),
mix: this.randomRange(0.6, 1.0),
},
stereoWidth: this.randomRange(0.2, 0.8),
};
}
mutateParams(
params: CombResonatorParams,
mutationAmount: number = 0.15,
pitchLock?: PitchLock
): CombResonatorParams {
const baseFreq = pitchLock?.enabled ? pitchLock.frequency : params.baseFreq;
return {
baseFreq,
sourceType: Math.random() < 0.15 ? (this.randomInt(0, 3) as SourceType) : params.sourceType,
sourceDecay: this.mutateValue(params.sourceDecay, mutationAmount, 0.02, 0.5),
comb: {
feedback: this.mutateValue(params.comb.feedback, mutationAmount, 0.3, 0.95),
brightness: this.mutateValue(params.comb.brightness, mutationAmount, 1, 20),
},
resonator: {
frequency: Math.random() < 0.2
? baseFreq * this.randomChoice([1, 1.5, 2, 2.5, 3, 3.5, 4])
: this.mutateValue(params.resonator.frequency, mutationAmount, baseFreq * 0.5, baseFreq * 6),
resonance: this.mutateValue(params.resonator.resonance, mutationAmount, 3, 80),
envAmount: this.mutateValue(params.resonator.envAmount, mutationAmount, 0, 2),
attack: this.mutateValue(params.resonator.attack, mutationAmount, 0.001, 0.2),
decay: this.mutateValue(params.resonator.decay, mutationAmount, 0.05, 0.6),
sustain: this.mutateValue(params.resonator.sustain, mutationAmount, 0, 0.7),
release: this.mutateValue(params.resonator.release, mutationAmount, 0.1, 0.9),
},
delay: {
time1: this.mutateValue(params.delay.time1, mutationAmount, 0.05, 0.5),
time2: this.mutateValue(params.delay.time2, mutationAmount, 0.08, 0.55),
feedback: this.mutateValue(params.delay.feedback, mutationAmount, 0.75, 0.99),
filterFreq: this.mutateValue(params.delay.filterFreq, mutationAmount, 350, 7000),
filterSweep: this.mutateValue(params.delay.filterSweep, mutationAmount, -1.5, 2.5),
mix: this.mutateValue(params.delay.mix, mutationAmount, 0.5, 1.0),
},
stereoWidth: this.mutateValue(params.stereoWidth, mutationAmount, 0, 1),
};
}
}

View 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),
};
}
}

View File

@ -1,4 +1,4 @@
import type { SynthEngine, PitchLock } from './SynthEngine';
import type { SynthEngine, PitchLock } from './base/SynthEngine';
enum OscillatorWaveform {
Sine,
@ -67,6 +67,10 @@ export class DubSiren implements SynthEngine<DubSirenParams> {
return 'generative' as const;
}
getCategory() {
return 'Experimental' as const;
}
generate(params: DubSirenParams, sampleRate: number, duration: number): [Float32Array, Float32Array] {
const numSamples = Math.floor(sampleRate * duration);
const leftBuffer = new Float32Array(numSamples);

View File

@ -0,0 +1,532 @@
import type { SynthEngine, PitchLock } from './base/SynthEngine';
interface DustNoiseParams {
// Dust density and character
dustDensity: number;
crackleAmount: number;
popDensity: number;
// Dust particle characteristics
particleDecay: number;
particlePitchRange: number;
particleResonance: number;
// Background texture
backgroundNoise: number;
noiseColor: number;
noiseFilter: number;
// Pops and clicks
popIntensity: number;
popPitchRange: number;
clickAmount: number;
// Dynamics and variation
dynamicRange: number;
irregularity: number;
// Stereo field
stereoWidth: number;
// Global envelope
globalAttack: number;
globalDecay: number;
}
export class DustNoise implements SynthEngine {
getName(): string {
return 'Pond';
}
getDescription(): string {
return 'Vinyl dust, crackle, and particle noise generator';
}
getType() {
return 'generative' as const;
}
getCategory() {
return 'Noise' as const;
}
randomParams(pitchLock?: PitchLock): DustNoiseParams {
const characterBias = Math.random();
let dustDensity: number;
let crackleAmount: number;
let popDensity: number;
let backgroundNoise: number;
if (characterBias < 0.5) {
// Very sparse, minimal particles
dustDensity = 0.01 + Math.random() * 0.08;
crackleAmount = Math.random() * 0.12;
popDensity = 0.01 + Math.random() * 0.05;
backgroundNoise = Math.random() * 0.08;
} else if (characterBias < 0.8) {
// Sparse, clean with occasional pops
dustDensity = 0.1 + Math.random() * 0.15;
crackleAmount = Math.random() * 0.25;
popDensity = 0.06 + Math.random() * 0.1;
backgroundNoise = 0.05 + Math.random() * 0.15;
} else {
// Medium vinyl character (was heavy)
dustDensity = 0.3 + Math.random() * 0.25;
crackleAmount = 0.25 + Math.random() * 0.3;
popDensity = 0.18 + Math.random() * 0.15;
backgroundNoise = 0.15 + Math.random() * 0.25;
}
const particleDecay = 0.3 + Math.random() * 0.6;
const particlePitchRange = 0.2 + Math.random() * 0.7;
const particleResonance = Math.random() * 0.6;
const noiseColor = Math.random();
const noiseFilter = Math.random() * 0.8;
const popIntensity = 0.3 + Math.random() * 0.6;
const popPitchRange = 0.2 + Math.random() * 0.7;
const clickAmount = Math.random() * 0.7;
const dynamicRange = 0.3 + Math.random() * 0.6;
const irregularity = Math.random() * 0.7;
const stereoWidth = Math.random() * 0.8;
const globalAttack = Math.random() * 0.08;
const globalDecay = 0.3 + Math.random() * 0.5;
return {
dustDensity,
crackleAmount,
popDensity,
particleDecay,
particlePitchRange,
particleResonance,
backgroundNoise,
noiseColor,
noiseFilter,
popIntensity,
popPitchRange,
clickAmount,
dynamicRange,
irregularity,
stereoWidth,
globalAttack,
globalDecay
};
}
mutateParams(params: DustNoiseParams, mutationAmount: number = 0.15, pitchLock?: PitchLock): DustNoiseParams {
const mutate = (value: number, amount: number = 0.15): number => {
return Math.max(0, Math.min(1, value + (Math.random() - 0.5) * amount));
};
return {
dustDensity: mutate(params.dustDensity, 0.2),
crackleAmount: mutate(params.crackleAmount, 0.25),
popDensity: mutate(params.popDensity, 0.2),
particleDecay: mutate(params.particleDecay, 0.2),
particlePitchRange: pitchLock?.enabled ? params.particlePitchRange : mutate(params.particlePitchRange, 0.25),
particleResonance: mutate(params.particleResonance, 0.2),
backgroundNoise: mutate(params.backgroundNoise, 0.2),
noiseColor: mutate(params.noiseColor, 0.25),
noiseFilter: mutate(params.noiseFilter, 0.2),
popIntensity: mutate(params.popIntensity, 0.2),
popPitchRange: pitchLock?.enabled ? params.popPitchRange : mutate(params.popPitchRange, 0.25),
clickAmount: mutate(params.clickAmount, 0.2),
dynamicRange: mutate(params.dynamicRange, 0.2),
irregularity: mutate(params.irregularity, 0.2),
stereoWidth: mutate(params.stereoWidth, 0.2),
globalAttack: mutate(params.globalAttack, 0.15),
globalDecay: mutate(params.globalDecay, 0.2)
};
}
generate(params: DustNoiseParams, sampleRate: number, duration: number, pitchLock?: PitchLock): [Float32Array, Float32Array] {
const numSamples = Math.floor(sampleRate * duration);
const left = new Float32Array(numSamples);
const right = new Float32Array(numSamples);
// Generate dust particles
const avgDustPerSecond = 5 + params.dustDensity * 120;
const totalDust = Math.floor(avgDustPerSecond * duration);
// Generate pops
const avgPopsPerSecond = 0.5 + params.popDensity * 12;
const totalPops = Math.floor(avgPopsPerSecond * duration);
// Create dust particles
const dustParticles: Array<{
startTime: number;
decay: number;
pitch: number;
amplitude: number;
resonance: number;
stereoOffset: number;
}> = [];
const baseDustPitch = pitchLock?.enabled ? pitchLock.frequency : 800 + params.particlePitchRange * 2000;
const basePopPitch = pitchLock?.enabled ? pitchLock.frequency : 200 + params.popPitchRange * 1000;
for (let i = 0; i < totalDust; i++) {
const startTime = Math.random() * duration;
const decay = (0.001 + params.particleDecay * 0.02) * (0.5 + Math.random() * 0.5);
const pitchVariation = pitchLock?.enabled ? 0.2 : params.particlePitchRange;
const pitchFreq = baseDustPitch + (Math.random() - 0.5) * pitchVariation * baseDustPitch;
const amplitude = (0.3 + Math.random() * 0.7) * (0.5 + params.dynamicRange * 0.5);
const resonance = params.particleResonance * (0.5 + Math.random() * 0.5);
const stereoOffset = (Math.random() - 0.5) * params.stereoWidth * 0.3;
dustParticles.push({
startTime,
decay,
pitch: pitchFreq,
amplitude,
resonance,
stereoOffset
});
}
// Create pops
const pops: Array<{
startTime: number;
intensity: number;
pitch: number;
isClick: boolean;
stereoOffset: number;
}> = [];
for (let i = 0; i < totalPops; i++) {
const startTime = Math.random() * duration;
const intensity = params.popIntensity * (0.5 + Math.random() * 0.5);
const pitchVariation = pitchLock?.enabled ? 0.2 : params.popPitchRange;
const pitchFreq = basePopPitch + (Math.random() - 0.5) * pitchVariation * basePopPitch;
const isClick = Math.random() < params.clickAmount;
const stereoOffset = (Math.random() - 0.5) * params.stereoWidth * 0.5;
pops.push({
startTime,
intensity,
pitch: pitchFreq,
isClick,
stereoOffset
});
}
// Sort events by time
dustParticles.sort((a, b) => a.startTime - b.startTime);
pops.sort((a, b) => a.startTime - b.startTime);
// Noise state
const pinkStateL = new Float32Array(7);
const pinkStateR = new Float32Array(7);
let brownStateL = 0;
let brownStateR = 0;
// Filter state for background noise
let bgFilterStateL1 = 0;
let bgFilterStateL2 = 0;
let bgFilterStateR1 = 0;
let bgFilterStateR2 = 0;
// Active particles
let dustIndex = 0;
let popIndex = 0;
const activeDust: Array<{
particle: typeof dustParticles[0];
startSample: number;
phase: number;
}> = [];
const activePops: Array<{
pop: typeof pops[0];
startSample: number;
phase: number;
}> = [];
// Crackle state (for vinyl crackle texture)
let cracklePhase = 0;
const crackleFreq = 20 + params.crackleAmount * 80;
for (let i = 0; i < numSamples; i++) {
const t = i / sampleRate;
// Add new dust particles
while (dustIndex < dustParticles.length && dustParticles[dustIndex].startTime <= t) {
activeDust.push({
particle: dustParticles[dustIndex],
startSample: i,
phase: Math.random() * Math.PI * 2
});
dustIndex++;
}
// Add new pops
while (popIndex < pops.length && pops[popIndex].startTime <= t) {
activePops.push({
pop: pops[popIndex],
startSample: i,
phase: Math.random() * Math.PI * 2
});
popIndex++;
}
// Global envelope
const globalEnv = this.globalEnvelope(
i,
numSamples,
params.globalAttack,
params.globalDecay,
duration,
sampleRate
);
// Background noise
const whiteL = Math.random() * 2 - 1;
const whiteR = Math.random() * 2 - 1;
brownStateL = this.updateBrownState(brownStateL, whiteL);
brownStateR = this.updateBrownState(brownStateR, whiteR);
let bgNoiseL = this.selectNoiseColor(params.noiseColor, whiteL, pinkStateL, brownStateL);
let bgNoiseR = this.selectNoiseColor(params.noiseColor, whiteR, pinkStateR, brownStateR);
// Filter background noise
if (params.noiseFilter > 0.1) {
const filterFreq = 500 + params.noiseFilter * 3000;
const filtered = this.stateVariableFilter(
bgNoiseL,
filterFreq,
1,
sampleRate,
bgFilterStateL1,
bgFilterStateL2
);
bgFilterStateL1 = filtered.state1;
bgFilterStateL2 = filtered.state2;
bgNoiseL = filtered.output;
const filteredR = this.stateVariableFilter(
bgNoiseR,
filterFreq,
1,
sampleRate,
bgFilterStateR1,
bgFilterStateR2
);
bgFilterStateR1 = filteredR.state1;
bgFilterStateR2 = filteredR.state2;
bgNoiseR = filteredR.output;
}
// Crackle modulation
cracklePhase += (2 * Math.PI * crackleFreq) / sampleRate;
const crackleMod = Math.sin(cracklePhase) * 0.5 + 0.5;
const crackleEnv = Math.pow(crackleMod, 3) * params.crackleAmount;
bgNoiseL *= params.backgroundNoise * (1 + crackleEnv);
bgNoiseR *= params.backgroundNoise * (1 + crackleEnv);
// Render dust particles
let dustL = 0;
let dustR = 0;
for (let d = activeDust.length - 1; d >= 0; d--) {
const active = activeDust[d];
const particle = active.particle;
const elapsed = (i - active.startSample) / sampleRate;
if (elapsed > particle.decay * 5) {
activeDust.splice(d, 1);
continue;
}
const env = Math.exp(-elapsed / particle.decay);
const phaseInc = (2 * Math.PI * particle.pitch) / sampleRate;
active.phase += phaseInc;
let signal = Math.sin(active.phase);
// Add resonance (filter-like character)
if (particle.resonance > 0.1) {
signal = signal * (1 - particle.resonance) +
Math.sin(active.phase * 2) * particle.resonance * 0.3 +
Math.sin(active.phase * 3) * particle.resonance * 0.15;
}
const output = signal * env * particle.amplitude;
const panL = 0.5 - particle.stereoOffset;
const panR = 0.5 + particle.stereoOffset;
dustL += output * panL;
dustR += output * panR;
}
// Render pops and clicks
let popL = 0;
let popR = 0;
for (let p = activePops.length - 1; p >= 0; p--) {
const active = activePops[p];
const pop = active.pop;
const elapsed = (i - active.startSample) / sampleRate;
const maxDuration = pop.isClick ? 0.001 : 0.008;
if (elapsed > maxDuration) {
activePops.splice(p, 1);
continue;
}
const env = Math.exp(-elapsed * (pop.isClick ? 2000 : 300));
let signal: number;
if (pop.isClick) {
// Sharp click (very short impulse)
signal = (Math.random() * 2 - 1) * (elapsed < 0.0003 ? 1 : 0.3);
} else {
// Pop with pitch
const phaseInc = (2 * Math.PI * pop.pitch) / sampleRate;
active.phase += phaseInc;
signal = Math.sin(active.phase) * 0.7 + (Math.random() * 2 - 1) * 0.3;
}
const output = signal * env * pop.intensity;
const panL = 0.5 - pop.stereoOffset;
const panR = 0.5 + pop.stereoOffset;
popL += output * panL;
popR += output * panR;
}
// Combine all elements
let sampleL = bgNoiseL + dustL + popL;
let sampleR = bgNoiseR + dustR + popR;
// Apply irregularity (random amplitude modulation)
if (params.irregularity > 0.1) {
const irregMod = 1 + (Math.random() - 0.5) * params.irregularity * 0.3;
sampleL *= irregMod;
sampleR *= irregMod;
}
// Apply global envelope
sampleL *= globalEnv;
sampleR *= globalEnv;
// Soft clipping
left[i] = this.softClip(sampleL * 0.6);
right[i] = this.softClip(sampleR * 0.6);
}
// Normalize
let peak = 0;
for (let i = 0; i < numSamples; i++) {
peak = Math.max(peak, Math.abs(left[i]), Math.abs(right[i]));
}
if (peak > 0.001) {
const normGain = 0.95 / peak;
for (let i = 0; i < numSamples; i++) {
left[i] *= normGain;
right[i] *= normGain;
}
}
return [left, right];
}
private globalEnvelope(
sample: number,
totalSamples: number,
attack: number,
decay: number,
duration: number,
sampleRate: number
): number {
const attackSamples = Math.floor(attack * duration * sampleRate);
const phase = sample / totalSamples;
if (sample < attackSamples && attackSamples > 0) {
const attackPhase = sample / attackSamples;
return attackPhase * attackPhase * (3 - 2 * attackPhase);
}
const decayRate = Math.max(decay, 0.1);
const decayPhase = (sample - attackSamples) / (totalSamples - attackSamples);
return Math.exp(-decayPhase / decayRate);
}
private updateBrownState(brownState: number, whiteNoise: number): number {
return (brownState + whiteNoise * 0.02) * 0.98;
}
private selectNoiseColor(
colorParam: number,
whiteNoise: number,
pinkState: Float32Array,
brownState: number
): number {
if (colorParam < 0.33) {
return whiteNoise;
} else if (colorParam < 0.66) {
pinkState[0] = 0.99886 * pinkState[0] + whiteNoise * 0.0555179;
pinkState[1] = 0.99332 * pinkState[1] + whiteNoise * 0.0750759;
pinkState[2] = 0.96900 * pinkState[2] + whiteNoise * 0.1538520;
pinkState[3] = 0.86650 * pinkState[3] + whiteNoise * 0.3104856;
pinkState[4] = 0.55000 * pinkState[4] + whiteNoise * 0.5329522;
pinkState[5] = -0.7616 * pinkState[5] - whiteNoise * 0.0168980;
const pink = pinkState[0] + pinkState[1] + pinkState[2] + pinkState[3] +
pinkState[4] + pinkState[5] + pinkState[6] + whiteNoise * 0.5362;
pinkState[6] = whiteNoise * 0.115926;
return pink * 0.11;
} else {
return brownState * 2.5;
}
}
private stateVariableFilter(
input: number,
cutoff: number,
resonance: number,
sampleRate: number,
state1: number,
state2: number
): { output: number; state1: number; state2: number } {
const normalizedFreq = Math.min(cutoff / sampleRate, 0.48);
const f = 2 * Math.sin(Math.PI * normalizedFreq);
const q = Math.max(1 / Math.min(resonance, 10), 0.02);
const lowpass = state2 + f * state1;
const highpass = input - lowpass - q * state1;
const bandpass = f * highpass + state1;
const newState1 = Math.max(-2, Math.min(2, Math.abs(bandpass) > 1e-10 ? bandpass : 0));
const newState2 = Math.max(-2, Math.min(2, Math.abs(lowpass) > 1e-10 ? lowpass : 0));
return {
output: bandpass,
state1: newState1,
state2: newState2
};
}
private softClip(x: number): number {
if (x > 1) {
return 1;
} else if (x < -1) {
return -1;
} else if (x > 0.66) {
return (3 - (2 - 3 * x) ** 2) / 3;
} else if (x < -0.66) {
return -(3 - (2 - 3 * -x) ** 2) / 3;
} else {
return x;
}
}
}

View File

@ -0,0 +1,184 @@
import { CsoundEngine, type CsoundParameter } from './base/CsoundEngine';
import type { PitchLock } from './base/SynthEngine';
interface FMTomTomParams {
baseFreq: number;
pitchBendAmount: number;
pitchBendDecay: number;
modIndex: number;
modRatio: number;
noiseHPFreq: number;
noiseResonance: number;
noiseMix: number;
ampAttack: number;
ampDecay: number;
sustain: number;
release: number;
tonality: number;
stereoDetune: number;
}
export class FMTomTom extends CsoundEngine<FMTomTomParams> {
getName(): string {
return 'FM Tom-Tom';
}
getDescription(): string {
return 'High-pass filtered noise modulating a sine oscillator with pitch bend envelope simulating tom-tom membrane';
}
getType() {
return 'generative' as const;
}
getCategory() {
return 'Percussion' as const;
}
protected getOrchestra(): string {
return `
instr 1
iBaseFreq chnget "baseFreq"
iPitchBendAmount chnget "pitchBendAmount"
iPitchBendDecay chnget "pitchBendDecay"
iModIndex chnget "modIndex"
iModRatio chnget "modRatio"
iNoiseHPFreq chnget "noiseHPFreq"
iNoiseResonance chnget "noiseResonance"
iNoiseMix chnget "noiseMix"
iAmpAttack chnget "ampAttack"
iAmpDecay chnget "ampDecay"
iSustain chnget "sustain"
iRelease chnget "release"
iTonality chnget "tonality"
iStereoDetune chnget "stereoDetune"
idur = p3
iPitchBendTime = iPitchBendDecay * idur
iAmpAttackTime = iAmpAttack * idur
iAmpDecayTime = iAmpDecay * idur
iReleaseTime = iRelease * idur
; Pitch bend envelope (simulates drum membrane tightening)
; Starts at higher pitch and decays to base pitch
iPitchStart = iBaseFreq * (1 + iPitchBendAmount)
kPitchEnv expseg iPitchStart, iPitchBendTime, iBaseFreq, idur - iPitchBendTime, iBaseFreq
; Generate high-pass filtered noise for modulation
aNoise noise 1, 0
aNoiseHP butterhp aNoise, iNoiseHPFreq
aNoiseFiltered butterbp aNoiseHP, iNoiseHPFreq * 2, iNoiseResonance
; Scale noise for FM modulation
aNoiseScaled = aNoiseFiltered * iModIndex * kPitchEnv * iTonality
; FM synthesis: noise modulates sine oscillator
aModulator oscili iModIndex * kPitchEnv, kPitchEnv * iModRatio
aCarrier oscili 0.5, kPitchEnv + aModulator + aNoiseScaled
; Add direct noise component for more realistic tom sound
aNoiseDirect = aNoiseFiltered * iNoiseMix * 0.3
; Mix carrier and noise
aMix = aCarrier * (1 - iNoiseMix * 0.5) + aNoiseDirect
; Amplitude envelope (ADSR-like with fast attack and decay)
kAmpEnv expseg 0.001, iAmpAttackTime, 1, iAmpDecayTime, iSustain, idur - iAmpAttackTime - iAmpDecayTime - iReleaseTime, iSustain, iReleaseTime, 0.001
; Apply amplitude envelope
aOut = aMix * kAmpEnv
; Right channel with stereo detune
iBaseFreqR = iBaseFreq * (1 + iStereoDetune * 0.02)
iPitchStartR = iBaseFreqR * (1 + iPitchBendAmount)
kPitchEnvR expseg iPitchStartR, iPitchBendTime, iBaseFreqR, idur - iPitchBendTime, iBaseFreqR
aNoiseR noise 1, 0
aNoiseHPR butterhp aNoiseR, iNoiseHPFreq * (1 + iStereoDetune * 0.01)
aNoiseFilteredR butterbp aNoiseHPR, iNoiseHPFreq * 2 * (1 + iStereoDetune * 0.01), iNoiseResonance
aNoiseScaledR = aNoiseFilteredR * iModIndex * kPitchEnvR * iTonality
aModulatorR oscili iModIndex * kPitchEnvR, kPitchEnvR * iModRatio
aCarrierR oscili 0.5, kPitchEnvR + aModulatorR + aNoiseScaledR
aNoiseDirectR = aNoiseFilteredR * iNoiseMix * 0.3
aMixR = aCarrierR * (1 - iNoiseMix * 0.5) + aNoiseDirectR
aOutR = aMixR * kAmpEnv
outs aOut, aOutR
endin
`;
}
protected getParametersForCsound(params: FMTomTomParams): CsoundParameter[] {
return [
{ channelName: 'baseFreq', value: params.baseFreq },
{ channelName: 'pitchBendAmount', value: params.pitchBendAmount },
{ channelName: 'pitchBendDecay', value: params.pitchBendDecay },
{ channelName: 'modIndex', value: params.modIndex },
{ channelName: 'modRatio', value: params.modRatio },
{ channelName: 'noiseHPFreq', value: params.noiseHPFreq },
{ channelName: 'noiseResonance', value: params.noiseResonance },
{ channelName: 'noiseMix', value: params.noiseMix },
{ channelName: 'ampAttack', value: params.ampAttack },
{ channelName: 'ampDecay', value: params.ampDecay },
{ channelName: 'sustain', value: params.sustain },
{ channelName: 'release', value: params.release },
{ channelName: 'tonality', value: params.tonality },
{ channelName: 'stereoDetune', value: params.stereoDetune },
];
}
randomParams(pitchLock?: PitchLock): FMTomTomParams {
const baseFreqChoices = [80, 100, 120, 150, 180, 220, 260, 300];
const baseFreq = pitchLock?.enabled
? pitchLock.frequency
: this.randomChoice(baseFreqChoices) * this.randomRange(0.9, 1.1);
const modRatios = [0.5, 1, 1.5, 2, 2.5, 3];
return {
baseFreq,
pitchBendAmount: this.randomRange(0.2, 0.8),
pitchBendDecay: this.randomRange(0.05, 0.2),
modIndex: this.randomRange(1, 8),
modRatio: this.randomChoice(modRatios),
noiseHPFreq: this.randomRange(200, 800),
noiseResonance: this.randomRange(20, 100),
noiseMix: this.randomRange(0.1, 0.6),
ampAttack: this.randomRange(0.001, 0.01),
ampDecay: this.randomRange(0.1, 0.3),
sustain: this.randomRange(0.2, 0.6),
release: this.randomRange(0.2, 0.5),
tonality: this.randomRange(0.3, 0.9),
stereoDetune: this.randomRange(0, 0.5),
};
}
mutateParams(
params: FMTomTomParams,
mutationAmount: number = 0.15,
pitchLock?: PitchLock
): FMTomTomParams {
const baseFreq = pitchLock?.enabled ? pitchLock.frequency : params.baseFreq;
const modRatios = [0.5, 1, 1.5, 2, 2.5, 3];
return {
baseFreq,
pitchBendAmount: this.mutateValue(params.pitchBendAmount, mutationAmount, 0.1, 1),
pitchBendDecay: this.mutateValue(params.pitchBendDecay, mutationAmount, 0.02, 0.4),
modIndex: this.mutateValue(params.modIndex, mutationAmount, 0.5, 12),
modRatio:
Math.random() < 0.15 ? this.randomChoice(modRatios) : params.modRatio,
noiseHPFreq: this.mutateValue(params.noiseHPFreq, mutationAmount, 100, 1200),
noiseResonance: this.mutateValue(params.noiseResonance, mutationAmount, 15, 150),
noiseMix: this.mutateValue(params.noiseMix, mutationAmount, 0, 0.8),
ampAttack: this.mutateValue(params.ampAttack, mutationAmount, 0.001, 0.02),
ampDecay: this.mutateValue(params.ampDecay, mutationAmount, 0.05, 0.5),
sustain: this.mutateValue(params.sustain, mutationAmount, 0.1, 0.8),
release: this.mutateValue(params.release, mutationAmount, 0.1, 0.7),
tonality: this.mutateValue(params.tonality, mutationAmount, 0.1, 1),
stereoDetune: this.mutateValue(params.stereoDetune, mutationAmount, 0, 1),
};
}
}

View File

@ -0,0 +1,247 @@
import { CsoundEngine, type CsoundParameter } from './base/CsoundEngine';
import type { PitchLock } from './base/SynthEngine';
interface FeedbackSnareParams {
baseFreq: number;
tonalDecay: number;
noiseDecay: number;
toneResonance: number;
springDecay: number;
springTone: number;
pitchBend: number;
pitchBendSpeed: number;
pulseRate: number;
feedbackAmount: number;
delayTime: number;
crossFeedMix: number;
snap: number;
brightness: number;
}
export class FeedbackSnare extends CsoundEngine<FeedbackSnareParams> {
getName(): string {
return 'Feedback Snare';
}
getDescription(): string {
return 'Complex snare using cross-feedback delay network with pulsed noise modulation';
}
getType() {
return 'generative' as const;
}
getCategory() {
return 'Percussion' as const;
}
protected getOrchestra(): string {
return `
instr 1
iBaseFreq chnget "baseFreq"
iTonalDecay chnget "tonalDecay"
iNoiseDecay chnget "noiseDecay"
iToneResonance chnget "toneResonance"
iSpringDecay chnget "springDecay"
iSpringTone chnget "springTone"
iPitchBend chnget "pitchBend"
iPitchBendSpeed chnget "pitchBendSpeed"
iPulseRate chnget "pulseRate"
iFeedbackAmount chnget "feedbackAmount"
iDelayTime chnget "delayTime"
iCrossFeedMix chnget "crossFeedMix"
iSnap chnget "snap"
iBrightness chnget "brightness"
idur = p3
iTonalDecayTime = iTonalDecay * idur
iNoiseDecayTime = iNoiseDecay * idur
iSpringDecayTime = iSpringDecay * idur
iPitchBendTime = iPitchBendSpeed * idur
; Pitch envelope with bend
kPitchEnv expseg iBaseFreq * (1 + iPitchBend * 2), iPitchBendTime, iBaseFreq, idur - iPitchBendTime, iBaseFreq * 0.95
; Generate square wave pulse for tonal component
aPulse vco2 0.5, kPitchEnv, 2, 0.5
; Tonal envelope
kToneEnv expseg 1, iTonalDecayTime, 0.001, idur - iTonalDecayTime, 0.001
aTonal = aPulse * kToneEnv
; Apply drum tone resonant filter
aDrumTone rezzy aTonal, kPitchEnv, iToneResonance
; Generate white noise
aNoise noise 1, 0
; Pulse modulation of noise (creates rhythmic texture)
kPulseMod oscili 1, iPulseRate
kPulseMod = (kPulseMod + 1) * 0.5
aNoiseModulated = aNoise * kPulseMod
; Noise envelope
kNoiseEnv expseg 1, iNoiseDecayTime, 0.001, idur - iNoiseDecayTime, 0.001
; Parallel filters on noise
; Bandpass filter 1 (body)
aNoiseBody butterbp aNoiseModulated, iBaseFreq * 1.5, 100
; Bandpass filter 2 (mid)
aNoiseMid butterbp aNoiseModulated, iBaseFreq * 3, 200
; Highpass filter (crispness)
aNoiseHigh butterhp aNoiseModulated, 3000
; Mix noise components
aNoiseMix = (aNoiseBody * 0.4 + aNoiseMid * 0.3 + aNoiseHigh * 0.3 * iBrightness) * kNoiseEnv
; Mix tonal and noise
aMix = aDrumTone * 0.5 + aNoiseMix * 0.5
; Cross-feedback delay network (simulates spring/snare wires)
; Create two delay lines that feed back into each other
aDelay1Init init 0
aDelay2Init init 0
; Spring tone filter (for the delayed signal)
iSpringFreq = 800 + iSpringTone * 4000
; Delay line 1
aDelayIn1 = aMix + aDelay2Init * iFeedbackAmount * iCrossFeedMix
aDelay1 vdelay aDelayIn1, iDelayTime * 1000, 50
aDelay1Filt butterbp aDelay1, iSpringFreq, 100
aDelay1Out = aDelay1Filt * exp(-p3 / iSpringDecayTime)
; Delay line 2
aDelayIn2 = aMix + aDelay1Out * iFeedbackAmount
aDelay2 vdelay aDelayIn2, iDelayTime * 1.3 * 1000, 50
aDelay2Filt butterbp aDelay2, iSpringFreq * 1.2, 120
aDelay2Out = aDelay2Filt * exp(-p3 / iSpringDecayTime)
; Update feedback
aDelay1Init = aDelay1Out
aDelay2Init = aDelay2Out
; Mix dry and delay
aOut = aMix * 0.6 + aDelay1Out * 0.2 + aDelay2Out * 0.2
; Add snap transient
if iSnap > 0.1 then
kSnapEnv linseg 1, 0.003, 0, idur - 0.003, 0
aSnap noise iSnap * 0.5, 0
aSnapFilt butterhp aSnap, 8000
aOut = aOut + aSnapFilt * kSnapEnv
endif
; Final output scaling
aOut = aOut * 0.4
; Right channel with slightly different parameters
aPulseR vco2 0.5, kPitchEnv * 1.002, 2, 0.5
aTonalR = aPulseR * kToneEnv
aDrumToneR rezzy aTonalR, kPitchEnv * 1.002, iToneResonance
aNoiseR noise 1, 0
aNoiseModulatedR = aNoiseR * kPulseMod
aNoiseBodyR butterbp aNoiseModulatedR, iBaseFreq * 1.52, 100
aNoiseMidR butterbp aNoiseModulatedR, iBaseFreq * 3.03, 200
aNoiseHighR butterhp aNoiseModulatedR, 3100
aNoiseMixR = (aNoiseBodyR * 0.4 + aNoiseMidR * 0.3 + aNoiseHighR * 0.3 * iBrightness) * kNoiseEnv
aMixR = aDrumToneR * 0.5 + aNoiseMixR * 0.5
aDelay1InitR init 0
aDelay2InitR init 0
aDelayIn1R = aMixR + aDelay2InitR * iFeedbackAmount * iCrossFeedMix
aDelay1R vdelay aDelayIn1R, iDelayTime * 1.05 * 1000, 50
aDelay1FiltR butterbp aDelay1R, iSpringFreq * 1.01, 100
aDelay1OutR = aDelay1FiltR * exp(-p3 / iSpringDecayTime)
aDelayIn2R = aMixR + aDelay1OutR * iFeedbackAmount
aDelay2R vdelay aDelayIn2R, iDelayTime * 1.35 * 1000, 50
aDelay2FiltR butterbp aDelay2R, iSpringFreq * 1.22, 120
aDelay2OutR = aDelay2FiltR * exp(-p3 / iSpringDecayTime)
aDelay1InitR = aDelay1OutR
aDelay2InitR = aDelay2OutR
aOutR = aMixR * 0.6 + aDelay1OutR * 0.2 + aDelay2OutR * 0.2
if iSnap > 0.1 then
aSnapR noise iSnap * 0.5, 0
aSnapFiltR butterhp aSnapR, 8100
aOutR = aOutR + aSnapFiltR * kSnapEnv
endif
aOutR = aOutR * 0.4
outs aOut, aOutR
endin
`;
}
protected getParametersForCsound(params: FeedbackSnareParams): CsoundParameter[] {
return [
{ channelName: 'baseFreq', value: params.baseFreq },
{ channelName: 'tonalDecay', value: params.tonalDecay },
{ channelName: 'noiseDecay', value: params.noiseDecay },
{ channelName: 'toneResonance', value: params.toneResonance },
{ channelName: 'springDecay', value: params.springDecay },
{ channelName: 'springTone', value: params.springTone },
{ channelName: 'pitchBend', value: params.pitchBend },
{ channelName: 'pitchBendSpeed', value: params.pitchBendSpeed },
{ channelName: 'pulseRate', value: params.pulseRate },
{ channelName: 'feedbackAmount', value: params.feedbackAmount },
{ channelName: 'delayTime', value: params.delayTime },
{ channelName: 'crossFeedMix', value: params.crossFeedMix },
{ channelName: 'snap', value: params.snap },
{ channelName: 'brightness', value: params.brightness },
];
}
randomParams(pitchLock?: PitchLock): FeedbackSnareParams {
const baseFreq = pitchLock?.enabled ? pitchLock.frequency : this.randomRange(150, 350);
return {
baseFreq,
tonalDecay: this.randomRange(0.1, 0.3),
noiseDecay: this.randomRange(0.3, 0.7),
toneResonance: this.randomRange(5, 25),
springDecay: this.randomRange(0.2, 0.6),
springTone: this.randomRange(0.2, 0.8),
pitchBend: this.randomRange(0.3, 0.9),
pitchBendSpeed: this.randomRange(0.01, 0.05),
pulseRate: this.randomRange(50, 300),
feedbackAmount: this.randomRange(0.3, 0.7),
delayTime: this.randomRange(0.005, 0.025),
crossFeedMix: this.randomRange(0.4, 0.9),
snap: this.randomRange(0, 0.6),
brightness: this.randomRange(0.3, 0.9),
};
}
mutateParams(
params: FeedbackSnareParams,
mutationAmount: number = 0.15,
pitchLock?: PitchLock
): FeedbackSnareParams {
const baseFreq = pitchLock?.enabled ? pitchLock.frequency : params.baseFreq;
return {
baseFreq,
tonalDecay: this.mutateValue(params.tonalDecay, mutationAmount, 0.05, 0.5),
noiseDecay: this.mutateValue(params.noiseDecay, mutationAmount, 0.2, 0.9),
toneResonance: this.mutateValue(params.toneResonance, mutationAmount, 2, 40),
springDecay: this.mutateValue(params.springDecay, mutationAmount, 0.1, 0.8),
springTone: this.mutateValue(params.springTone, mutationAmount, 0, 1),
pitchBend: this.mutateValue(params.pitchBend, mutationAmount, 0.1, 1.2),
pitchBendSpeed: this.mutateValue(params.pitchBendSpeed, mutationAmount, 0.005, 0.1),
pulseRate: this.mutateValue(params.pulseRate, mutationAmount, 20, 500),
feedbackAmount: this.mutateValue(params.feedbackAmount, mutationAmount, 0.1, 0.85),
delayTime: this.mutateValue(params.delayTime, mutationAmount, 0.003, 0.04),
crossFeedMix: this.mutateValue(params.crossFeedMix, mutationAmount, 0.2, 1),
snap: this.mutateValue(params.snap, mutationAmount, 0, 0.8),
brightness: this.mutateValue(params.brightness, mutationAmount, 0, 1),
};
}
}

View File

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

View File

@ -0,0 +1,408 @@
import { CsoundEngine, type CsoundParameter } from './base/CsoundEngine';
import type { PitchLock } from './base/SynthEngine';
enum VowelType {
A,
E,
I,
O,
U,
AE,
OE,
UE,
}
enum ModulationType {
SimpleFM,
DoubleFM,
RingMod,
CrossFM,
}
interface FormantBand {
frequency: number;
bandwidth: number;
amplitude: number;
}
interface FormantFMParams {
baseFreq: number;
vowel: VowelType;
vowelMorph: number;
modulationType: ModulationType;
modIndex: number;
modRatio: number;
attack: number;
decay: number;
sustain: number;
release: number;
brightness: number;
vibrato: number;
vibratoRate: number;
detune: number;
noise: number;
formantLFORate: number;
formantLFODepth: number;
modIndexLFORate: number;
modIndexLFODepth: number;
chaos: number;
}
export class FormantFM extends CsoundEngine<FormantFMParams> {
getName(): string {
return 'Formant FM';
}
getDescription(): string {
return 'FM synthesis with formant filters creating vowel-like sounds and vocal textures';
}
getType() {
return 'generative' as const;
}
getCategory() {
return 'FM' as const;
}
protected getOrchestra(): string {
return `
instr 1
iBaseFreq chnget "baseFreq"
iVowel chnget "vowel"
iVowelMorph chnget "vowelMorph"
iModType chnget "modulationType"
iModIndex chnget "modIndex"
iModRatio chnget "modRatio"
iAttack chnget "attack"
iDecay chnget "decay"
iSustain chnget "sustain"
iRelease chnget "release"
iBrightness chnget "brightness"
iVibrato chnget "vibrato"
iVibratoRate chnget "vibratoRate"
iDetune chnget "detune"
iNoise chnget "noise"
iFormantLFORate chnget "formantLFORate"
iFormantLFODepth chnget "formantLFODepth"
iModIndexLFORate chnget "modIndexLFORate"
iModIndexLFODepth chnget "modIndexLFODepth"
iChaos chnget "chaos"
idur = p3
iAttackTime = iAttack * idur
iDecayTime = iDecay * idur
iReleaseTime = iRelease * idur
; Envelope
kEnv madsr iAttackTime, iDecayTime, iSustain, iReleaseTime
; Vibrato LFO with chaos
kVib oscili iVibrato * iBaseFreq * 0.02, iVibratoRate
kChaosLFO1 oscili iChaos * iBaseFreq * 0.01, iVibratoRate * 1.618
kChaosLFO2 oscili iChaos * iBaseFreq * 0.005, iVibratoRate * 2.414
kFreq = iBaseFreq + kVib + kChaosLFO1 + kChaosLFO2
; Get formant parameters based on vowel type
if iVowel == 0 then
; A (as in "father")
iF1 = 730
iF2 = 1090
iF3 = 2440
iA1 = 1.0
iA2 = 0.5
iA3 = 0.25
iBW1 = 80
iBW2 = 90
iBW3 = 120
elseif iVowel == 1 then
; E (as in "bet")
iF1 = 530
iF2 = 1840
iF3 = 2480
iA1 = 1.0
iA2 = 0.6
iA3 = 0.3
iBW1 = 60
iBW2 = 100
iBW3 = 120
elseif iVowel == 2 then
; I (as in "bit")
iF1 = 390
iF2 = 1990
iF3 = 2550
iA1 = 1.0
iA2 = 0.7
iA3 = 0.2
iBW1 = 50
iBW2 = 100
iBW3 = 120
elseif iVowel == 3 then
; O (as in "boat")
iF1 = 570
iF2 = 840
iF3 = 2410
iA1 = 1.0
iA2 = 0.45
iA3 = 0.28
iBW1 = 70
iBW2 = 80
iBW3 = 100
elseif iVowel == 4 then
; U (as in "boot")
iF1 = 440
iF2 = 1020
iF3 = 2240
iA1 = 1.0
iA2 = 0.4
iA3 = 0.2
iBW1 = 70
iBW2 = 80
iBW3 = 100
elseif iVowel == 5 then
; AE (as in "bat")
iF1 = 660
iF2 = 1720
iF3 = 2410
iA1 = 1.0
iA2 = 0.55
iA3 = 0.3
iBW1 = 80
iBW2 = 90
iBW3 = 120
elseif iVowel == 6 then
; OE (as in "bird")
iF1 = 490
iF2 = 1350
iF3 = 1690
iA1 = 1.0
iA2 = 0.5
iA3 = 0.4
iBW1 = 70
iBW2 = 80
iBW3 = 100
else
; UE (as in "about")
iF1 = 520
iF2 = 1190
iF3 = 2390
iA1 = 1.0
iA2 = 0.45
iA3 = 0.25
iBW1 = 70
iBW2 = 80
iBW3 = 110
endif
; Modulate formant frequencies with multiple LFOs
kFormantShift = 1 + (iVowelMorph - 0.5) * 0.4
kFormantLFO oscili iFormantLFODepth * 0.3, iFormantLFORate
kFormantShiftModulated = kFormantShift + kFormantLFO
kF1 = iF1 * kFormantShiftModulated
kF2 = iF2 * kFormantShiftModulated
kF3 = iF3 * kFormantShiftModulated
; Modulation index LFO
kModIndexLFO oscili iModIndexLFODepth * iModIndex, iModIndexLFORate
kModIndexDynamic = iModIndex + kModIndexLFO
; Generate carrier and modulator based on modulation type
kModFreq = kFreq * iModRatio
if iModType == 0 then
; Simple FM with dynamic modulation
aMod oscili kModIndexDynamic * kModFreq, kModFreq
aCarrier oscili 0.5, kFreq + aMod
elseif iModType == 1 then
; Double FM (modulator modulates itself) with chaos
kChaosModDepth = 1 + (iChaos * 0.5)
aMod1 oscili kModIndexDynamic * 0.3 * kModFreq * kChaosModDepth, kModFreq * 0.5
aMod2 oscili kModIndexDynamic * kModFreq, kModFreq + aMod1
aCarrier oscili 0.5, kFreq + aMod2
elseif iModType == 2 then
; Ring modulation with frequency wobble
kRingModWobble oscili iChaos * 0.2, iFormantLFORate * 0.7
aMod oscili 0.5, kModFreq * (1 + kRingModWobble)
aCarrierTemp oscili 0.5, kFreq
aCarrier = aCarrierTemp * aMod
else
; Cross FM with multiple carriers
aMod oscili kModIndexDynamic * kModFreq, kModFreq
aCarrier1 oscili 0.4, kFreq + aMod
aCarrier2 oscili 0.3, kFreq * 0.5 + aMod * 0.5
kThirdCarrierFreq = kFreq * (1.5 + iChaos * 0.3)
aCarrier3 oscili 0.2 * iChaos, kThirdCarrierFreq + aMod * 0.3
aCarrier = aCarrier1 + aCarrier2 + aCarrier3
endif
; Add brightness via high-frequency content
aCarrierBright oscili 0.15 * iBrightness, kFreq * 2
aCarrierMix = aCarrier + aCarrierBright
; Add subtle noise for breathiness
aNoise noise 0.08 * iNoise, 0
aCarrierFinal = aCarrierMix + aNoise
; Apply formant filters (bandpass filters at formant frequencies)
aFormant1 butterbp aCarrierFinal, kF1, iBW1
aFormant1Scaled = aFormant1 * iA1
aFormant2 butterbp aCarrierFinal, kF2, iBW2
aFormant2Scaled = aFormant2 * iA2
aFormant3 butterbp aCarrierFinal, kF3, iBW3
aFormant3Scaled = aFormant3 * iA3
; Mix formants
aMix = (aFormant1Scaled + aFormant2Scaled + aFormant3Scaled) * 0.6
; Apply envelope
aOut = aMix * kEnv
; Stereo - slightly different phase for right channel
iDetuneFactor = 1 + (iDetune * 0.5)
kFreqR = iBaseFreq * iDetuneFactor + kVib
; Regenerate right channel with detuned frequency
kModFreqR = kFreqR * iModRatio
if iModType == 0 then
aModR oscili iModIndex * kModFreqR, kModFreqR
aCarrierR oscili 0.5, kFreqR + aModR
elseif iModType == 1 then
aMod1R oscili iModIndex * 0.3 * kModFreqR, kModFreqR * 0.5
aMod2R oscili iModIndex * kModFreqR, kModFreqR + aMod1R
aCarrierR oscili 0.5, kFreqR + aMod2R
elseif iModType == 2 then
aModR oscili 0.5, kModFreqR
aCarrierTempR oscili 0.5, kFreqR
aCarrierR = aCarrierTempR * aModR
else
aModR oscili iModIndex * kModFreqR, kModFreqR
aCarrier1R oscili 0.4, kFreqR + aModR
aCarrier2R oscili 0.3, kFreqR * 0.5 + aModR * 0.5
aCarrierR = aCarrier1R + aCarrier2R
endif
aCarrierBrightR oscili 0.15 * iBrightness, kFreqR * 2
aCarrierMixR = aCarrierR + aCarrierBrightR
aNoiseR noise 0.08 * iNoise, 0
aCarrierFinalR = aCarrierMixR + aNoiseR
kF1R = iF1 * kFormantShift
kF2R = iF2 * kFormantShift
kF3R = iF3 * kFormantShift
aFormant1R butterbp aCarrierFinalR, kF1R, iBW1
aFormant1ScaledR = aFormant1R * iA1
aFormant2R butterbp aCarrierFinalR, kF2R, iBW2
aFormant2ScaledR = aFormant2R * iA2
aFormant3R butterbp aCarrierFinalR, kF3R, iBW3
aFormant3ScaledR = aFormant3R * iA3
aMixR = (aFormant1ScaledR + aFormant2ScaledR + aFormant3ScaledR) * 0.6
aOutR = aMixR * kEnv
outs aOut, aOutR
endin
`;
}
protected getParametersForCsound(params: FormantFMParams): CsoundParameter[] {
return [
{ channelName: 'baseFreq', value: params.baseFreq },
{ channelName: 'vowel', value: params.vowel },
{ channelName: 'vowelMorph', value: params.vowelMorph },
{ channelName: 'modulationType', value: params.modulationType },
{ channelName: 'modIndex', value: params.modIndex },
{ channelName: 'modRatio', value: params.modRatio },
{ channelName: 'attack', value: params.attack },
{ channelName: 'decay', value: params.decay },
{ channelName: 'sustain', value: params.sustain },
{ channelName: 'release', value: params.release },
{ channelName: 'brightness', value: params.brightness },
{ channelName: 'vibrato', value: params.vibrato },
{ channelName: 'vibratoRate', value: params.vibratoRate },
{ channelName: 'detune', value: params.detune },
{ channelName: 'noise', value: params.noise },
{ channelName: 'formantLFORate', value: params.formantLFORate },
{ channelName: 'formantLFODepth', value: params.formantLFODepth },
{ channelName: 'modIndexLFORate', value: params.modIndexLFORate },
{ channelName: 'modIndexLFODepth', value: params.modIndexLFODepth },
{ channelName: 'chaos', value: params.chaos },
];
}
randomParams(pitchLock?: PitchLock): FormantFMParams {
const baseFreqChoices = [82.4, 110, 146.8, 220, 293.7, 440];
const baseFreq = pitchLock?.enabled
? pitchLock.frequency
: this.randomChoice(baseFreqChoices) * this.randomRange(0.95, 1.05);
const modulationRatios = [0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 5, 7, 9];
return {
baseFreq,
vowel: this.randomInt(0, 7) as VowelType,
vowelMorph: this.randomRange(0.3, 0.7),
modulationType: this.randomInt(0, 3) as ModulationType,
modIndex: this.randomRange(1, 10),
modRatio: this.randomChoice(modulationRatios),
attack: this.randomRange(0.001, 0.15),
decay: this.randomRange(0.05, 0.25),
sustain: this.randomRange(0.3, 0.8),
release: this.randomRange(0.1, 0.4),
brightness: this.randomRange(0, 0.6),
vibrato: this.randomRange(0, 0.5),
vibratoRate: this.randomRange(3, 8),
detune: this.randomRange(0.001, 0.01),
noise: this.randomRange(0, 0.3),
formantLFORate: this.randomRange(0.2, 6),
formantLFODepth: this.randomRange(0, 0.8),
modIndexLFORate: this.randomRange(0.5, 12),
modIndexLFODepth: this.randomRange(0, 0.7),
chaos: this.randomRange(0, 0.8),
};
}
mutateParams(
params: FormantFMParams,
mutationAmount: number = 0.15,
pitchLock?: PitchLock
): FormantFMParams {
const baseFreq = pitchLock?.enabled ? pitchLock.frequency : params.baseFreq;
const modulationRatios = [0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 5, 7, 9];
return {
baseFreq,
vowel: Math.random() < 0.1 ? (this.randomInt(0, 7) as VowelType) : params.vowel,
vowelMorph: this.mutateValue(params.vowelMorph, mutationAmount, 0, 1),
modulationType:
Math.random() < 0.08
? (this.randomInt(0, 3) as ModulationType)
: params.modulationType,
modIndex: this.mutateValue(params.modIndex, mutationAmount, 0.5, 15),
modRatio:
Math.random() < 0.12
? this.randomChoice(modulationRatios)
: params.modRatio,
attack: this.mutateValue(params.attack, mutationAmount, 0.001, 0.25),
decay: this.mutateValue(params.decay, mutationAmount, 0.02, 0.4),
sustain: this.mutateValue(params.sustain, mutationAmount, 0.1, 0.9),
release: this.mutateValue(params.release, mutationAmount, 0.05, 0.6),
brightness: this.mutateValue(params.brightness, mutationAmount, 0, 1),
vibrato: this.mutateValue(params.vibrato, mutationAmount, 0, 0.6),
vibratoRate: this.mutateValue(params.vibratoRate, mutationAmount, 2, 10),
detune: this.mutateValue(params.detune, mutationAmount, 0.0005, 0.02),
noise: this.mutateValue(params.noise, mutationAmount, 0, 0.5),
formantLFORate: this.mutateValue(params.formantLFORate, mutationAmount, 0.1, 8),
formantLFODepth: this.mutateValue(params.formantLFODepth, mutationAmount, 0, 1),
modIndexLFORate: this.mutateValue(params.modIndexLFORate, mutationAmount, 0.3, 15),
modIndexLFODepth: this.mutateValue(params.modIndexLFODepth, mutationAmount, 0, 1),
chaos: this.mutateValue(params.chaos, mutationAmount, 0, 1),
};
}
}

View File

@ -0,0 +1,142 @@
import { CsoundEngine, type CsoundParameter } from './base/CsoundEngine';
import type { PitchLock } from './base/SynthEngine';
interface FormantPopDrumParams {
formant1Freq: number;
formant1Width: number;
formant2Freq: number;
formant2Width: number;
noiseDecay: number;
ampAttack: number;
ampDecay: number;
brightness: number;
stereoSpread: number;
}
export class FormantPopDrum extends CsoundEngine<FormantPopDrumParams> {
getName(): string {
return 'Formant Pop Drum';
}
getDescription(): string {
return 'Short noise burst through dual bandpass filters creating marimba-like or wooden drum tones';
}
getType() {
return 'generative' as const;
}
getCategory() {
return 'Percussion' as const;
}
protected getOrchestra(): string {
return `
instr 1
iF1Freq chnget "formant1Freq"
iF1Width chnget "formant1Width"
iF2Freq chnget "formant2Freq"
iF2Width chnget "formant2Width"
iNoiseDecay chnget "noiseDecay"
iAmpAttack chnget "ampAttack"
iAmpDecay chnget "ampDecay"
iBrightness chnget "brightness"
iStereoSpread chnget "stereoSpread"
idur = p3
iNoiseDecayTime = iNoiseDecay * idur
iAmpAttackTime = iAmpAttack * idur
iAmpDecayTime = iAmpDecay * idur
; Declick envelope for noise (very short to avoid clicks)
kDeclickEnv linseg 0, 0.001, 1, iNoiseDecayTime, 0, idur - iNoiseDecayTime - 0.001, 0
; Generate random noise
aNoise noise 1, 0
; Apply declick envelope to noise
aNoiseEnv = aNoise * kDeclickEnv
; First bandpass filter (formant 1)
aFormant1 butterbp aNoiseEnv, iF1Freq, iF1Width
; Second bandpass filter (formant 2)
aFormant2 butterbp aNoiseEnv, iF2Freq, iF2Width
; Mix formants with brightness control
aMix = aFormant1 * (1 - iBrightness * 0.5) + aFormant2 * iBrightness
; Amplitude envelope (exponential decay)
kAmpEnv expseg 0.001, iAmpAttackTime, 1, iAmpDecayTime, 0.001, idur - iAmpAttackTime - iAmpDecayTime, 0.001
; Apply amplitude envelope
aOut = aMix * kAmpEnv
; Stereo output with slight frequency offset for right channel
iF1FreqR = iF1Freq * (1 + iStereoSpread * 0.02)
iF2FreqR = iF2Freq * (1 + iStereoSpread * 0.02)
aFormant1R butterbp aNoiseEnv, iF1FreqR, iF1Width
aFormant2R butterbp aNoiseEnv, iF2FreqR, iF2Width
aMixR = aFormant1R * (1 - iBrightness * 0.5) + aFormant2R * iBrightness
aOutR = aMixR * kAmpEnv
outs aOut, aOutR
endin
`;
}
protected getParametersForCsound(params: FormantPopDrumParams): CsoundParameter[] {
return [
{ channelName: 'formant1Freq', value: params.formant1Freq },
{ channelName: 'formant1Width', value: params.formant1Width },
{ channelName: 'formant2Freq', value: params.formant2Freq },
{ channelName: 'formant2Width', value: params.formant2Width },
{ channelName: 'noiseDecay', value: params.noiseDecay },
{ channelName: 'ampAttack', value: params.ampAttack },
{ channelName: 'ampDecay', value: params.ampDecay },
{ channelName: 'brightness', value: params.brightness },
{ channelName: 'stereoSpread', value: params.stereoSpread },
];
}
randomParams(pitchLock?: PitchLock): FormantPopDrumParams {
const formant1FreqChoices = [200, 250, 300, 400, 500, 600, 800, 1000];
const formant1Freq = pitchLock?.enabled
? pitchLock.frequency
: this.randomChoice(formant1FreqChoices) * this.randomRange(0.9, 1.1);
return {
formant1Freq,
formant1Width: this.randomRange(30, 120),
formant2Freq: formant1Freq * this.randomRange(1.5, 3.5),
formant2Width: this.randomRange(40, 150),
noiseDecay: this.randomRange(0.05, 0.3),
ampAttack: this.randomRange(0.001, 0.02),
ampDecay: this.randomRange(0.1, 0.6),
brightness: this.randomRange(0.2, 0.8),
stereoSpread: this.randomRange(0, 0.5),
};
}
mutateParams(
params: FormantPopDrumParams,
mutationAmount: number = 0.15,
pitchLock?: PitchLock
): FormantPopDrumParams {
const formant1Freq = pitchLock?.enabled ? pitchLock.frequency : params.formant1Freq;
return {
formant1Freq,
formant1Width: this.mutateValue(params.formant1Width, mutationAmount, 20, 200),
formant2Freq: this.mutateValue(params.formant2Freq, mutationAmount, 300, 4000),
formant2Width: this.mutateValue(params.formant2Width, mutationAmount, 30, 250),
noiseDecay: this.mutateValue(params.noiseDecay, mutationAmount, 0.02, 0.5),
ampAttack: this.mutateValue(params.ampAttack, mutationAmount, 0.001, 0.05),
ampDecay: this.mutateValue(params.ampDecay, mutationAmount, 0.05, 0.8),
brightness: this.mutateValue(params.brightness, mutationAmount, 0, 1),
stereoSpread: this.mutateValue(params.stereoSpread, mutationAmount, 0, 1),
};
}
}

View File

@ -1,4 +1,4 @@
import type { SynthEngine, PitchLock } from './SynthEngine';
import type { SynthEngine, PitchLock } from './base/SynthEngine';
enum EnvCurve {
Linear,
@ -75,6 +75,10 @@ export class FourOpFM implements SynthEngine<FourOpFMParams> {
return 'generative' as const;
}
getCategory() {
return 'FM' as const;
}
generate(params: FourOpFMParams, sampleRate: number, duration: number): [Float32Array, Float32Array] {
const numSamples = Math.floor(sampleRate * duration);
const leftBuffer = new Float32Array(numSamples);

View File

@ -0,0 +1,183 @@
import type { PitchLock, SynthEngine } from './base/SynthEngine';
interface HiHatParams {
// Decay time (0 = closed/tight, 1 = open/long)
decay: number;
// Tone/brightness (filter cutoff frequency)
tone: number;
// Noise mix (0 = pure metallic, 1 = more noise)
noise: number;
// Base pitch
pitch: number;
// Timbre (shifts frequency ratios for different metallic characters)
timbre: number;
}
export class HiHat implements SynthEngine {
getName(): string {
return 'Noise Hi-Hat';
}
getDescription(): string {
return 'Metallic hi-hat from square wave oscillators and noise';
}
getType() {
return 'generative' as const;
}
getCategory() {
return 'Percussion' as const;
}
randomParams(pitchLock?: PitchLock): HiHatParams {
return {
decay: Math.random(),
tone: 0.4 + Math.random() * 0.6,
noise: 0.3 + Math.random() * 0.5,
pitch: pitchLock ? this.freqToParam(pitchLock.frequency) : 0.4 + Math.random() * 0.2,
timbre: Math.random(),
};
}
mutateParams(params: HiHatParams, mutationAmount: number = 0.15, pitchLock?: PitchLock): HiHatParams {
const mutate = (value: number, amount: number = mutationAmount): number => {
return Math.max(0, Math.min(1, value + (Math.random() - 0.5) * amount));
};
return {
decay: mutate(params.decay, 0.25),
tone: mutate(params.tone, 0.25),
noise: mutate(params.noise, 0.2),
pitch: pitchLock ? params.pitch : mutate(params.pitch, 0.15),
timbre: mutate(params.timbre, 0.25),
};
}
generate(
params: HiHatParams,
sampleRate: number,
duration: number,
pitchLock?: PitchLock
): [Float32Array, Float32Array] {
const numSamples = Math.floor(sampleRate * duration);
const left = new Float32Array(numSamples);
const right = new Float32Array(numSamples);
// Decay time: 30ms (tight closed) to 800ms (long open)
const decayTime = (0.03 + params.decay * 0.77) * duration;
// Bandpass filter frequencies for metallic character (based on classic hi-hat synthesis)
// Pitch shifts all frequencies, timbre changes the spread
const basePitch = pitchLock ? pitchLock.frequency : 600 + params.pitch * 800;
const pitchScale = basePitch / 1000; // normalize to ~1000Hz center
// Timbre controls frequency spread: 0 = tight/focused, 1 = wide/trashy
const spread = 0.7 + params.timbre * 0.6;
const filterFreqs = [
4500 * pitchScale * spread,
6200 * pitchScale * spread,
7800 * pitchScale * spread,
9600 * pitchScale * spread,
11400 * pitchScale * spread,
13200 * pitchScale * spread,
];
// Tone controls overall brightness via highpass cutoff
const hpFreq = 4000 + params.tone * 6000; // 4kHz to 10kHz
for (let channel = 0; channel < 2; channel++) {
const output = channel === 0 ? left : right;
// Bandpass filter states (6 filters, 2 states each)
const bpStates1 = new Array(6).fill(0);
const bpStates2 = new Array(6).fill(0);
// Highpass filter state
let hp1 = 0, hp2 = 0;
// Stereo variation in filter frequencies
const stereoShift = channel === 0 ? 0.995 : 1.005;
for (let i = 0; i < numSamples; i++) {
const t = i / sampleRate;
// Exponential decay envelope
const env = Math.exp(-t / decayTime);
// White noise source
const noise = Math.random() * 2 - 1;
// Pass noise through 6 bandpass filters at different frequencies
let filtered = 0;
for (let j = 0; j < filterFreqs.length; j++) {
const freq = filterFreqs[j] * stereoShift;
const q = 8 + j * 2; // Higher Q for higher frequencies
const freqNorm = Math.min(freq / sampleRate, 0.48);
const f = 2 * Math.sin(Math.PI * freqNorm);
const qRecip = 1 / q;
const lp = bpStates2[j] + f * bpStates1[j];
const hp = noise - lp - qRecip * bpStates1[j];
const bp = f * hp + bpStates1[j];
bpStates1[j] = Math.max(-2, Math.min(2, bp));
bpStates2[j] = Math.max(-2, Math.min(2, lp));
filtered += bp;
}
// Average the bandpass outputs
let sample = filtered / filterFreqs.length;
// Add raw noise for more character (controlled by noise param)
sample = sample * (1 - params.noise * 0.5) + noise * params.noise * 0.3;
// Apply envelope
sample *= env;
// Highpass filter for brightness control
const hpNorm = Math.min(hpFreq / sampleRate, 0.48);
const hpA = 1 - hpNorm * 2;
const hpFiltered = hpA * (hp1 + sample - hp2);
hp2 = sample;
hp1 = hpFiltered;
sample = hpFiltered;
// Soft clip
if (sample > 0.7) sample = 0.7 + (sample - 0.7) * 0.3;
if (sample < -0.7) sample = -0.7 + (sample + 0.7) * 0.3;
output[i] = sample;
}
}
// Normalize
let peak = 0;
for (let i = 0; i < numSamples; i++) {
peak = Math.max(peak, Math.abs(left[i]), Math.abs(right[i]));
}
if (peak > 0.001) {
const normGain = 0.5 / peak;
for (let i = 0; i < numSamples; i++) {
left[i] *= normGain;
right[i] *= normGain;
}
}
return [left, right];
}
private freqToParam(freq: number): number {
// Map frequency to 0-1 range (600-1400 Hz)
return Math.max(0, Math.min(1, (freq - 600) / 800));
}
}

View File

@ -1,4 +1,4 @@
import type { SynthEngine, PitchLock } from './SynthEngine';
import type { SynthEngine, PitchLock } from './base/SynthEngine';
interface InputParams {
recorded: boolean;
@ -21,6 +21,10 @@ export class Input implements SynthEngine<InputParams> {
return 'input' as const;
}
getCategory() {
return 'Utility' as const;
}
async record(duration: number): Promise<void> {
const stream = await navigator.mediaDevices.getUserMedia({
audio: {

View File

@ -1,4 +1,4 @@
import type { SynthEngine, PitchLock } from './SynthEngine';
import type { SynthEngine, PitchLock } from './base/SynthEngine';
type HarmonicMode =
| 'single' // Just fundamental
@ -39,6 +39,10 @@ export class KarplusStrong implements SynthEngine<KarplusStrongParams> {
return 'generative' as const;
}
getCategory() {
return 'Physical' as const;
}
generate(params: KarplusStrongParams, sampleRate: number, duration: number): [Float32Array, Float32Array] {
const numSamples = Math.floor(sampleRate * duration);
const leftBuffer = new Float32Array(numSamples);

View File

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

View File

@ -0,0 +1,317 @@
import { CsoundEngine, type CsoundParameter } from './base/CsoundEngine';
import type { PitchLock } from './base/SynthEngine';
interface MassiveAdditiveParams {
baseFreq: number;
numPartials: number;
spectrumShape: number;
harmonicSpread: number;
inharmonicity: number;
attack: number;
decay: number;
sustain: number;
release: number;
ampLFORate: number;
ampLFODepth: number;
ampLFOPhaseSpread: number;
freqLFORate: number;
freqLFODepth: number;
partialDecayRate: number;
partialAttackSpread: number;
oddEvenBalance: number;
shimmer: number;
shimmerRate: number;
stereoSpread: number;
brightness: number;
chaos: number;
}
export class MassiveAdditive extends CsoundEngine<MassiveAdditiveParams> {
getName(): string {
return 'Spectral Add';
}
getDescription(): string {
return 'Spectral additive synthesis with octave doubling, micro-detuned beating, and evolving harmonic content';
}
getType() {
return 'generative' as const;
}
getCategory() {
return 'Additive' as const;
}
protected getOrchestra(): string {
return `
; Function tables for sine wave
gisine ftgen 0, 0, 16384, 10, 1
instr 1
iBaseFreq chnget "baseFreq"
iNumPartials chnget "numPartials"
iSpectrumShape chnget "spectrumShape"
iHarmonicSpread chnget "harmonicSpread"
iInharmonicity chnget "inharmonicity"
iAttack chnget "attack"
iDecay chnget "decay"
iSustain chnget "sustain"
iRelease chnget "release"
iAmpLFORate chnget "ampLFORate"
iAmpLFODepth chnget "ampLFODepth"
iAmpLFOPhaseSpread chnget "ampLFOPhaseSpread"
iFreqLFORate chnget "freqLFORate"
iFreqLFODepth chnget "freqLFODepth"
iPartialDecayRate chnget "partialDecayRate"
iPartialAttackSpread chnget "partialAttackSpread"
iOddEvenBalance chnget "oddEvenBalance"
iShimmer chnget "shimmer"
iShimmerRate chnget "shimmerRate"
iStereoSpread chnget "stereoSpread"
iBrightness chnget "brightness"
iChaos chnget "chaos"
idur = p3
iAttackTime = iAttack * idur
iDecayTime = iDecay * idur
iReleaseTime = iRelease * idur
; Main envelope
kEnv madsr iAttackTime, iDecayTime, iSustain, iReleaseTime
; Global modulation LFOs
kGlobalAmpLFO oscili 1, iAmpLFORate * 0.5
kShimmerLFO oscili 1, iShimmerRate
kFreqLFO oscili iFreqLFODepth * 0.02, iFreqLFORate
; Chaos LFOs
kChaosLFO1 oscili 1, iAmpLFORate * 1.618
kChaosLFO2 oscili 1, iAmpLFORate * 2.414
; Create frequency and amplitude tables dynamically
iPartialCount = min(iNumPartials, 64)
; Generate tables for partial frequencies and amplitudes
gifreq ftgen 0, 0, 128, -2, 0
giamp ftgen 0, 0, 128, -2, 0
gifreq2 ftgen 0, 0, 128, -2, 0
giamp2 ftgen 0, 0, 128, -2, 0
iPartialIndex = 0
loop_setup:
iN = iPartialIndex + 1
; Calculate frequency ratio with harmonicity and inharmonicity
iHarmonic = iN * iHarmonicSpread
iInharmonicShift = iInharmonicity * iN * iN * 0.001
iFreqRatio = iHarmonic * (1 + iInharmonicShift)
; Calculate amplitude based on spectrum shape
iAmpFalloff = 1 / pow(iN, 1 + iSpectrumShape * 2)
; Brightness boost for higher partials
iBrightnessBoost = 1 + (iBrightness * (iN / iPartialCount) * 2)
; Odd/even balance
iOddEvenFactor = 1
if (iN % 2) == 0 then
iOddEvenFactor = iOddEvenBalance
else
iOddEvenFactor = 2 - iOddEvenBalance
endif
iAmp = iAmpFalloff * iBrightnessBoost * iOddEvenFactor
; Create alternate partial distribution for morphing
iFreqRatio2 = iFreqRatio * (1 + (iChaos * 0.05 * sin(iN * 0.7)))
iAmp2 = iAmp * (1 - (iN / iPartialCount) * 0.3)
; Write to tables
tableiw iFreqRatio, iPartialIndex, gifreq
tableiw iAmp, iPartialIndex, giamp
tableiw iFreqRatio2, iPartialIndex, gifreq2
tableiw iAmp2, iPartialIndex, giamp2
iPartialIndex = iPartialIndex + 1
if iPartialIndex < iPartialCount goto loop_setup
; Generate additive synthesis with heavy modulation and octave doubling
; Slow modulation LFOs for spectral evolution
kLFO1 oscili 1, iAmpLFORate * 0.53
kLFO2 oscili 1, iAmpLFORate * 0.89
kLFO3 oscili 1, iAmpLFORate * 1.37
kShimmerLFO1 oscili 1, iShimmerRate * 0.67
kShimmerLFO2 oscili 1, iShimmerRate * 1.13
; Very slow spectral morphing
kSpectralMorph oscili 1, iAmpLFORate * 0.19
kMorphAmount = (kSpectralMorph + 1) * 0.5
; Minimal frequency wobble for organic feel
kFreqWobble = kFreqLFO * 0.5
; Micro-detuning for beating (0.1 to 0.5 Hz beating)
iMicroDetune = 0.0003 + (iChaos * 0.0005)
; === FUNDAMENTAL OCTAVE ===
; Main voice at fundamental frequency
kcps = iBaseFreq * (1 + kFreqWobble)
kAmpMain = kEnv * (0.6 + kLFO1 * iAmpLFODepth * 0.2)
aMainVoice adsynt2 kAmpMain, kcps, gisine, gifreq, giamp, iPartialCount
; Slightly detuned fundamental for beating
kcpsDetune = iBaseFreq * (1 + iMicroDetune) * (1 + kFreqWobble * 0.93)
kAmpDetune = kEnv * (0.5 + kLFO2 * iAmpLFODepth * 0.25)
aDetuneVoice adsynt2 kAmpDetune, kcpsDetune, gisine, gifreq, giamp, iPartialCount, 0.25
; Morphing spectral variant
kAmpMorph = kEnv * kMorphAmount * (0.4 + kShimmerLFO1 * iShimmer * 0.3)
aMorphVoice adsynt2 kAmpMorph, kcps, gisine, gifreq2, giamp2, iPartialCount, 0.5
; === OCTAVE UP ===
; One octave higher with micro-detune for beating
kcpsOctUp = (iBaseFreq * 2) * (1 + kFreqWobble * 1.07)
kAmpOctUp = kEnv * (0.35 + kLFO3 * iAmpLFODepth * 0.2 + kShimmerLFO2 * iShimmer * 0.25)
aOctaveUp adsynt2 kAmpOctUp, kcpsOctUp, gisine, gifreq, giamp, iPartialCount, 0.33
; Detuned octave up for complex beating
kcpsOctUpDetune = (iBaseFreq * 2) * (1 - iMicroDetune * 0.8) * (1 + kFreqWobble * 1.11)
kAmpOctUpDetune = kEnv * (0.3 + kLFO1 * iAmpLFODepth * 0.25)
aOctaveUpDetune adsynt2 kAmpOctUpDetune, kcpsOctUpDetune, gisine, gifreq, giamp, iPartialCount, 0.67
; === OCTAVE DOWN ===
; One octave lower with micro-detune
kcpsOctDown = (iBaseFreq * 0.5) * (1 + kFreqWobble * 0.97)
kAmpOctDown = kEnv * (0.4 + kShimmerLFO1 * iShimmer * 0.3)
aOctaveDown adsynt2 kAmpOctDown, kcpsOctDown, gisine, gifreq, giamp, iPartialCount, 0.17
; Detuned octave down for sub-harmonic beating
kcpsOctDownDetune = (iBaseFreq * 0.5) * (1 + iMicroDetune * 1.2) * (1 + kFreqWobble * 1.03)
kAmpOctDownDetune = kEnv * (0.35 + kLFO2 * iAmpLFODepth * 0.2)
aOctaveDownDetune adsynt2 kAmpOctDownDetune, kcpsOctDownDetune, gisine, gifreq2, giamp, iPartialCount, 0.83
; === STEREO MIXING ===
; Left channel: emphasize fundamental and octave down
aOutL = aMainVoice * 0.7 + aDetuneVoice * 0.6 + aMorphVoice * 0.5
aOutL = aOutL + aOctaveUp * 0.4 + aOctaveUpDetune * 0.35
aOutL = aOutL + aOctaveDown * 0.55 + aOctaveDownDetune * 0.45
; Right channel: emphasize octaves with different balance
aOutR = aMainVoice * 0.65 + aDetuneVoice * 0.55 + aMorphVoice * 0.6
aOutR = aOutR + aOctaveUp * 0.5 + aOctaveUpDetune * 0.4
aOutR = aOutR + aOctaveDown * 0.45 + aOctaveDownDetune * 0.5
; Subtle stereo width from chaos
kStereoMod = kChaosLFO1 * iChaos * 0.1
aOutL = aOutL * (1 - kStereoMod * iStereoSpread)
aOutR = aOutR * (1 + kStereoMod * iStereoSpread)
; Normalize to prevent clipping
aOutL = aOutL * 0.28
aOutR = aOutR * 0.28
outs aOutL, aOutR
endin
`;
}
protected getParametersForCsound(params: MassiveAdditiveParams): CsoundParameter[] {
return [
{ channelName: 'baseFreq', value: params.baseFreq },
{ channelName: 'numPartials', value: params.numPartials },
{ channelName: 'spectrumShape', value: params.spectrumShape },
{ channelName: 'harmonicSpread', value: params.harmonicSpread },
{ channelName: 'inharmonicity', value: params.inharmonicity },
{ channelName: 'attack', value: params.attack },
{ channelName: 'decay', value: params.decay },
{ channelName: 'sustain', value: params.sustain },
{ channelName: 'release', value: params.release },
{ channelName: 'ampLFORate', value: params.ampLFORate },
{ channelName: 'ampLFODepth', value: params.ampLFODepth },
{ channelName: 'ampLFOPhaseSpread', value: params.ampLFOPhaseSpread },
{ channelName: 'freqLFORate', value: params.freqLFORate },
{ channelName: 'freqLFODepth', value: params.freqLFODepth },
{ channelName: 'partialDecayRate', value: params.partialDecayRate },
{ channelName: 'partialAttackSpread', value: params.partialAttackSpread },
{ channelName: 'oddEvenBalance', value: params.oddEvenBalance },
{ channelName: 'shimmer', value: params.shimmer },
{ channelName: 'shimmerRate', value: params.shimmerRate },
{ channelName: 'stereoSpread', value: params.stereoSpread },
{ channelName: 'brightness', value: params.brightness },
{ channelName: 'chaos', value: params.chaos },
];
}
randomParams(pitchLock?: PitchLock): MassiveAdditiveParams {
const baseFreqChoices = [55, 82.4, 110, 146.8, 220, 293.7, 440];
const baseFreq = pitchLock?.enabled
? pitchLock.frequency
: this.randomChoice(baseFreqChoices) * this.randomRange(0.98, 1.02);
return {
baseFreq,
numPartials: this.randomInt(32, 64),
spectrumShape: this.randomRange(0.3, 0.7),
harmonicSpread: this.randomChoice([1, 1.5, 2, 3]),
inharmonicity: this.randomRange(0, 0.5),
attack: this.randomRange(0.05, 0.3),
decay: this.randomRange(0.15, 0.5),
sustain: this.randomRange(0.5, 0.9),
release: this.randomRange(0.2, 0.6),
ampLFORate: this.randomRange(0.2, 2),
ampLFODepth: this.randomRange(0.3, 0.8),
ampLFOPhaseSpread: this.randomRange(0.5, 1),
freqLFORate: this.randomRange(0.1, 1.5),
freqLFODepth: this.randomRange(0.05, 0.3),
partialDecayRate: this.randomRange(0.3, 0.8),
partialAttackSpread: this.randomRange(0.1, 0.5),
oddEvenBalance: this.randomRange(0.5, 1.5),
shimmer: this.randomRange(0.3, 0.9),
shimmerRate: this.randomRange(0.05, 0.8),
stereoSpread: this.randomRange(0.4, 1),
brightness: this.randomRange(0.3, 0.9),
chaos: this.randomRange(0.1, 0.7),
};
}
mutateParams(
params: MassiveAdditiveParams,
mutationAmount: number = 0.15,
pitchLock?: PitchLock
): MassiveAdditiveParams {
const baseFreq = pitchLock?.enabled ? pitchLock.frequency : params.baseFreq;
return {
baseFreq,
numPartials:
Math.random() < 0.15
? this.randomInt(16, 64)
: Math.round(this.mutateValue(params.numPartials, mutationAmount, 16, 64)),
spectrumShape: this.mutateValue(params.spectrumShape, mutationAmount, 0, 1),
harmonicSpread:
Math.random() < 0.1
? this.randomChoice([0.5, 1, 1.5, 2])
: params.harmonicSpread,
inharmonicity: this.mutateValue(params.inharmonicity, mutationAmount, 0, 0.5),
attack: this.mutateValue(params.attack, mutationAmount, 0.001, 0.4),
decay: this.mutateValue(params.decay, mutationAmount, 0.05, 0.6),
sustain: this.mutateValue(params.sustain, mutationAmount, 0.2, 0.9),
release: this.mutateValue(params.release, mutationAmount, 0.05, 0.8),
ampLFORate: this.mutateValue(params.ampLFORate, mutationAmount, 0.1, 6),
ampLFODepth: this.mutateValue(params.ampLFODepth, mutationAmount, 0, 1),
ampLFOPhaseSpread: this.mutateValue(params.ampLFOPhaseSpread, mutationAmount, 0, 1),
freqLFORate: this.mutateValue(params.freqLFORate, mutationAmount, 0.1, 5),
freqLFODepth: this.mutateValue(params.freqLFODepth, mutationAmount, 0, 0.8),
partialDecayRate: this.mutateValue(params.partialDecayRate, mutationAmount, 0, 1),
partialAttackSpread: this.mutateValue(params.partialAttackSpread, mutationAmount, 0, 0.8),
oddEvenBalance: this.mutateValue(params.oddEvenBalance, mutationAmount, 0.2, 1.8),
shimmer: this.mutateValue(params.shimmer, mutationAmount, 0, 1),
shimmerRate: this.mutateValue(params.shimmerRate, mutationAmount, 0.05, 2),
stereoSpread: this.mutateValue(params.stereoSpread, mutationAmount, 0, 1),
brightness: this.mutateValue(params.brightness, mutationAmount, 0, 1),
chaos: this.mutateValue(params.chaos, mutationAmount, 0, 0.8),
};
}
}

View File

@ -1,4 +1,4 @@
import type { SynthEngine } from './SynthEngine';
import type { SynthEngine } from './base/SynthEngine';
interface NoiseDrumParams {
// Noise characteristics
@ -39,7 +39,7 @@ interface NoiseDrumParams {
export class NoiseDrum implements SynthEngine {
getName(): string {
return 'Noise Drum';
return 'Noise Perc';
}
getDescription(): string {
@ -50,6 +50,10 @@ export class NoiseDrum implements SynthEngine {
return 'generative' as const;
}
getCategory() {
return 'Percussion' as const;
}
randomParams(): NoiseDrumParams {
// Intelligent parameter generation based on correlated characteristics
@ -93,16 +97,16 @@ export class NoiseDrum implements SynthEngine {
const filterType = filterFreq < 0.3 ?
Math.random() * 0.35 : // Low freq prefers lowpass
filterFreq > 0.7 ?
0.5 + Math.random() * 0.5 : // High freq prefers highpass/bandpass
Math.random(); // Mid freq - any type
0.5 + Math.random() * 0.5 : // High freq prefers highpass/bandpass
Math.random(); // Mid freq - any type
// Decay time inversely correlates with frequency
const decayBias = Math.random();
const ampDecay = filterFreq < 0.3 ?
0.25 + decayBias * 0.4 : // Low freq can be longer
filterFreq > 0.6 ?
0.08 + decayBias * 0.35 : // High freq shorter
0.2 + decayBias * 0.45; // Mid range
0.08 + decayBias * 0.35 : // High freq shorter
0.2 + decayBias * 0.45; // Mid range
// Attack is generally very short for percussion
const ampAttack = Math.random() < 0.85 ?
@ -372,7 +376,7 @@ export class NoiseDrum implements SynthEngine {
// Blend body resonance - SUBTLE
sample = sample * (1 - params.bodyAmount * 0.4) +
bodyFiltered.output * params.bodyAmount * 0.6 * bodyEnv;
bodyFiltered.output * params.bodyAmount * 0.6 * bodyEnv;
}
// Apply amplitude envelope
@ -434,7 +438,7 @@ export class NoiseDrum implements SynthEngine {
pinkState[5] = -0.7616 * pinkState[5] - whiteNoise * 0.0168980;
const pink = pinkState[0] + pinkState[1] + pinkState[2] + pinkState[3] +
pinkState[4] + pinkState[5] + pinkState[6] + whiteNoise * 0.5362;
pinkState[4] + pinkState[5] + pinkState[6] + whiteNoise * 0.5362;
pinkState[6] = whiteNoise * 0.115926;
return pink * 0.11;

View File

@ -0,0 +1,394 @@
import type { SynthEngine, PitchLock } from './base/SynthEngine';
interface ParticleNoiseParams {
// Particle characteristics
density: number;
impulseLength: number;
impulseLengthVariation: number;
// Pitch characteristics (affects filter frequency)
basePitch: number;
pitchVariation: number;
// Texture
noiseColor: number;
filterResonance: number;
clickiness: number;
// Spatial
stereoSpread: number;
panSpeed: number;
// Dynamics
globalEnvAttack: number;
globalEnvDecay: number;
velocity: number;
}
export class ParticleNoise implements SynthEngine {
getName(): string {
return 'Particle';
}
getDescription(): string {
return 'Very short noise impulses and clicks generator';
}
getType() {
return 'generative' as const;
}
getCategory() {
return 'Noise' as const;
}
randomParams(pitchLock?: PitchLock): ParticleNoiseParams {
const densityBias = Math.random();
let density: number;
let impulseLength: number;
let impulseLengthVariation: number;
if (densityBias < 0.5) {
// Very sparse particles
density = 0.01 + Math.random() * 0.08;
impulseLength = 0.01 + Math.random() * 0.04;
impulseLengthVariation = 0.5 + Math.random() * 0.4;
} else if (densityBias < 0.8) {
// Sparse particles
density = 0.1 + Math.random() * 0.15;
impulseLength = 0.008 + Math.random() * 0.03;
impulseLengthVariation = 0.3 + Math.random() * 0.4;
} else {
// Medium density
density = 0.3 + Math.random() * 0.25;
impulseLength = 0.005 + Math.random() * 0.02;
impulseLengthVariation = 0.15 + Math.random() * 0.35;
}
let basePitch: number;
if (pitchLock?.enabled) {
basePitch = Math.max(0, Math.min(1, (pitchLock.frequency - 100) / 2000));
} else {
basePitch = 0.2 + Math.random() * 0.7;
}
const pitchVariation = Math.random() * 0.6;
const noiseColor = Math.random();
const filterResonance = Math.random() * 0.5;
const clickiness = Math.random() * 0.8;
const stereoSpread = Math.random() * 0.8;
const panSpeed = Math.random() * 0.6;
const globalEnvAttack = Math.random() * 0.12;
const globalEnvDecay = 0.25 + Math.random() * 0.5;
const velocity = 0.6 + Math.random() * 0.4;
return {
density,
impulseLength,
impulseLengthVariation,
basePitch,
pitchVariation,
noiseColor,
filterResonance,
clickiness,
stereoSpread,
panSpeed,
globalEnvAttack,
globalEnvDecay,
velocity
};
}
mutateParams(params: ParticleNoiseParams, mutationAmount: number = 0.15, pitchLock?: PitchLock): ParticleNoiseParams {
const mutate = (value: number, amount: number = 0.15): number => {
return Math.max(0, Math.min(1, value + (Math.random() - 0.5) * amount));
};
return {
density: mutate(params.density, 0.2),
impulseLength: mutate(params.impulseLength, 0.2),
impulseLengthVariation: mutate(params.impulseLengthVariation, 0.2),
basePitch: pitchLock?.enabled ? params.basePitch : mutate(params.basePitch, 0.25),
pitchVariation: mutate(params.pitchVariation, 0.2),
noiseColor: mutate(params.noiseColor, 0.25),
filterResonance: mutate(params.filterResonance, 0.2),
clickiness: mutate(params.clickiness, 0.2),
stereoSpread: mutate(params.stereoSpread, 0.2),
panSpeed: mutate(params.panSpeed, 0.2),
globalEnvAttack: mutate(params.globalEnvAttack, 0.15),
globalEnvDecay: mutate(params.globalEnvDecay, 0.2),
velocity: mutate(params.velocity, 0.15)
};
}
generate(params: ParticleNoiseParams, sampleRate: number, duration: number, pitchLock?: PitchLock): [Float32Array, Float32Array] {
const numSamples = Math.floor(sampleRate * duration);
const left = new Float32Array(numSamples);
const right = new Float32Array(numSamples);
// Calculate number of grains based on density
const avgGrainsPerSecond = 2 + params.density * 80;
const totalGrains = Math.floor(avgGrainsPerSecond * duration);
// Pre-generate impulse timings and parameters
const impulses: Array<{
startTime: number;
duration: number;
filterFreq: number;
pan: number;
amplitude: number;
isClick: boolean;
}> = [];
const baseFilterFreq = pitchLock?.enabled ? pitchLock.frequency : 200 + params.basePitch * 3000;
for (let i = 0; i < totalGrains; i++) {
const startTime = Math.random() * duration;
const baseImpulseDuration = 0.0005 + params.impulseLength * 0.003;
const impulseDuration = baseImpulseDuration * (0.5 + Math.random() * params.impulseLengthVariation);
const filterOffset = (Math.random() - 0.5) * params.pitchVariation * baseFilterFreq * 2;
const filterFreq = Math.max(100, baseFilterFreq + filterOffset);
const pan = Math.random();
const amplitude = 0.5 + Math.random() * 0.5;
const isClick = Math.random() < params.clickiness;
impulses.push({
startTime,
duration: impulseDuration,
filterFreq,
pan,
amplitude,
isClick
});
}
// Sort impulses by start time for efficient processing
impulses.sort((a, b) => a.startTime - b.startTime);
// Noise state for colored noise generation
const pinkStateL = new Float32Array(7);
const pinkStateR = new Float32Array(7);
let brownStateL = 0;
let brownStateR = 0;
let impulseIndex = 0;
const activeImpulses: Array<{
impulse: typeof impulses[0];
startSample: number;
filterState1: number;
filterState2: number;
}> = [];
for (let i = 0; i < numSamples; i++) {
const t = i / sampleRate;
// Add new impulses that should start at this sample
while (impulseIndex < impulses.length && impulses[impulseIndex].startTime <= t) {
activeImpulses.push({
impulse: impulses[impulseIndex],
startSample: i,
filterState1: 0,
filterState2: 0
});
impulseIndex++;
}
// Global envelope
const globalEnv = this.globalEnvelope(
i,
numSamples,
params.globalEnvAttack,
params.globalEnvDecay,
duration,
sampleRate
);
// Pan modulation
const panLFO = Math.sin(2 * Math.PI * (0.1 + params.panSpeed * 2) * t);
let sampleL = 0;
let sampleR = 0;
// Render all active impulses
for (let g = activeImpulses.length - 1; g >= 0; g--) {
const active = activeImpulses[g];
const impulse = active.impulse;
const impulseSample = i - active.startSample;
const impulseTime = impulseSample / sampleRate;
if (impulseTime >= impulse.duration) {
activeImpulses.splice(g, 1);
continue;
}
const impulsePhase = impulseTime / impulse.duration;
// Very fast exponential decay envelope
const impulseEnv = Math.exp(-impulsePhase * 15);
// Generate noise burst
const whiteL = Math.random() * 2 - 1;
const whiteR = Math.random() * 2 - 1;
brownStateL = this.updateBrownState(brownStateL, whiteL);
brownStateR = this.updateBrownState(brownStateR, whiteR);
let noiseL = this.selectNoiseColor(params.noiseColor, whiteL, pinkStateL, brownStateL);
let noiseR = this.selectNoiseColor(params.noiseColor, whiteR, pinkStateR, brownStateR);
// For clicks, use pure white noise burst
if (impulse.isClick) {
noiseL = whiteL;
noiseR = whiteR;
} else if (params.filterResonance > 0.1) {
// Apply resonant filter for tonal color
const resonance = 2 + params.filterResonance * 8;
const filtered = this.stateVariableFilter(
noiseL,
impulse.filterFreq,
resonance,
active.filterState1,
active.filterState2
);
active.filterState1 = filtered.state1;
active.filterState2 = filtered.state2;
noiseL = filtered.output;
noiseR = filtered.output;
}
// Apply impulse envelope and amplitude
const impulseOutput = noiseL * impulseEnv * impulse.amplitude * params.velocity;
// Apply panning with modulation
const panMod = impulse.pan + panLFO * params.stereoSpread * 0.15;
const panClamp = Math.max(0, Math.min(1, panMod));
const panL = Math.cos(panClamp * Math.PI * 0.5);
const panR = Math.sin(panClamp * Math.PI * 0.5);
sampleL += impulseOutput * panL;
sampleR += impulseOutput * panR * (impulse.isClick ? 1 : 1 + (Math.random() - 0.5) * params.stereoSpread * 0.2);
}
// Apply global envelope
sampleL *= globalEnv;
sampleR *= globalEnv;
// Soft clipping
left[i] = this.softClip(sampleL * 0.7);
right[i] = this.softClip(sampleR * 0.7);
}
// Normalize
let peak = 0;
for (let i = 0; i < numSamples; i++) {
peak = Math.max(peak, Math.abs(left[i]), Math.abs(right[i]));
}
if (peak > 0.001) {
const normGain = 0.95 / peak;
for (let i = 0; i < numSamples; i++) {
left[i] *= normGain;
right[i] *= normGain;
}
}
return [left, right];
}
private globalEnvelope(
sample: number,
totalSamples: number,
attack: number,
decay: number,
duration: number,
sampleRate: number
): number {
const attackSamples = Math.floor(attack * duration * sampleRate);
const phase = sample / totalSamples;
if (sample < attackSamples && attackSamples > 0) {
const attackPhase = sample / attackSamples;
return attackPhase * attackPhase * (3 - 2 * attackPhase);
}
const decayRate = Math.max(decay, 0.1);
const decayPhase = (sample - attackSamples) / (totalSamples - attackSamples);
return Math.exp(-decayPhase / decayRate);
}
private stateVariableFilter(
input: number,
cutoff: number,
resonance: number,
state1: number,
state2: number
): { output: number; state1: number; state2: number } {
const normalizedFreq = Math.min(cutoff / 44100, 0.48);
const f = 2 * Math.sin(Math.PI * normalizedFreq);
const q = Math.max(1 / Math.min(resonance, 20), 0.01);
const lowpass = state2 + f * state1;
const highpass = input - lowpass - q * state1;
const bandpass = f * highpass + state1;
const newState1 = Math.max(-3, Math.min(3, Math.abs(bandpass) > 1e-10 ? bandpass : 0));
const newState2 = Math.max(-3, Math.min(3, Math.abs(lowpass) > 1e-10 ? lowpass : 0));
return {
output: bandpass,
state1: newState1,
state2: newState2
};
}
private updateBrownState(brownState: number, whiteNoise: number): number {
return (brownState + whiteNoise * 0.02) * 0.98;
}
private selectNoiseColor(
colorParam: number,
whiteNoise: number,
pinkState: Float32Array,
brownState: number
): number {
if (colorParam < 0.33) {
return whiteNoise;
} else if (colorParam < 0.66) {
pinkState[0] = 0.99886 * pinkState[0] + whiteNoise * 0.0555179;
pinkState[1] = 0.99332 * pinkState[1] + whiteNoise * 0.0750759;
pinkState[2] = 0.96900 * pinkState[2] + whiteNoise * 0.1538520;
pinkState[3] = 0.86650 * pinkState[3] + whiteNoise * 0.3104856;
pinkState[4] = 0.55000 * pinkState[4] + whiteNoise * 0.5329522;
pinkState[5] = -0.7616 * pinkState[5] - whiteNoise * 0.0168980;
const pink = pinkState[0] + pinkState[1] + pinkState[2] + pinkState[3] +
pinkState[4] + pinkState[5] + pinkState[6] + whiteNoise * 0.5362;
pinkState[6] = whiteNoise * 0.115926;
return pink * 0.11;
} else {
return brownState * 2.5;
}
}
private softClip(x: number): number {
if (x > 1) {
return 1;
} else if (x < -1) {
return -1;
} else if (x > 0.66) {
return (3 - (2 - 3 * x) ** 2) / 3;
} else if (x < -0.66) {
return -(3 - (2 - 3 * -x) ** 2) / 3;
} else {
return x;
}
}
}

View File

@ -0,0 +1,592 @@
import type { SynthEngine, PitchLock } from './base/SynthEngine';
enum PDWaveform {
Sine,
SawUp,
SawDown,
Pulse,
DoubleSine,
ResonantSaw,
}
enum PDAlgorithm {
Single, // Single oscillator with PD
Dual, // Two oscillators, different waveforms
Detune, // Two slightly detuned oscillators
Octave, // Oscillator + octave up
Fifth, // Base + fifth up (1.5x frequency)
Sub, // Base + octave down
Stack, // Base + fifth + octave
Wide, // Heavily detuned oscillators
}
enum LFOWaveform {
Sine,
Triangle,
Square,
Saw,
SampleHold,
RandomWalk,
}
interface PDOscillatorParams {
waveform: PDWaveform;
level: number;
detune: number; // in cents
dcw: number; // Digitally Controlled Waveshaping (0-1, controls brightness)
pulseWidth: number; // 0-1, controls pulse wave width
attack: number;
decay: number;
sustain: number;
release: number;
dcwAttack: number;
dcwDecay: number;
dcwSustain: number;
dcwRelease: number;
}
interface LFOParams {
rate: number;
depth: number;
waveform: LFOWaveform;
target: 'pitch' | 'dcw';
}
export interface PhaseDistortionFMParams {
baseFreq: number;
algorithm: PDAlgorithm;
oscillators: [PDOscillatorParams, PDOscillatorParams];
lfo: LFOParams;
stereoWidth: number;
}
export class PhaseDistortionFM implements SynthEngine<PhaseDistortionFMParams> {
private static workletLoaded = false;
private static workletURL: string | null = null;
getName(): string {
return 'Phase Dist';
}
getDescription(): string {
return 'Casio CZ-style Phase Distortion synthesis with DCW envelopes';
}
getType() {
return 'generative' as const;
}
getCategory() {
return 'FM' as const;
}
generate(params: PhaseDistortionFMParams, sampleRate: number, duration: number): [Float32Array, Float32Array] {
const numSamples = Math.floor(sampleRate * duration);
const leftBuffer = new Float32Array(numSamples);
const rightBuffer = new Float32Array(numSamples);
const TAU = Math.PI * 2;
const detune = 1 + (params.stereoWidth * 0.001);
const leftFreq = params.baseFreq / detune;
const rightFreq = params.baseFreq * detune;
let osc1PhaseL = 0;
let osc1PhaseR = 0;
let osc2PhaseL = 0;
let osc2PhaseR = 0;
let lfoPhaseL = 0;
let lfoPhaseR = Math.PI * params.stereoWidth * 0.3;
let lfoSampleHoldValue = Math.random() * 2 - 1;
let lfoSampleHoldCounter = 0;
const lfoSampleHoldInterval = Math.max(1, Math.floor(sampleRate / (params.lfo.rate * 4)));
let lfoRandomWalkCurrent = Math.random() * 2 - 1;
let lfoRandomWalkTarget = Math.random() * 2 - 1;
let lfoRandomWalkCounter = 0;
const lfoRandomWalkInterval = Math.max(1, Math.floor(sampleRate / (params.lfo.rate * 2)));
let dcBlockerPrevL = 0;
let dcBlockerAccL = 0;
let dcBlockerPrevR = 0;
let dcBlockerAccR = 0;
const dcBlockerCutoff = 0.995;
for (let i = 0; i < numSamples; i++) {
const t = i / sampleRate;
const env1 = this.calculateEnvelope(t, duration, params.oscillators[0]);
const env2 = this.calculateEnvelope(t, duration, params.oscillators[1]);
const dcw1 = this.calculateDCWEnvelope(t, duration, params.oscillators[0]);
const dcw2 = this.calculateDCWEnvelope(t, duration, params.oscillators[1]);
// Update LFO states
lfoSampleHoldCounter++;
if (lfoSampleHoldCounter >= lfoSampleHoldInterval) {
lfoSampleHoldValue = Math.random() * 2 - 1;
lfoSampleHoldCounter = 0;
}
lfoRandomWalkCounter++;
if (lfoRandomWalkCounter >= lfoRandomWalkInterval) {
lfoRandomWalkTarget = Math.random() * 2 - 1;
lfoRandomWalkCounter = 0;
}
const walkSpeed = 0.01;
lfoRandomWalkCurrent += (lfoRandomWalkTarget - lfoRandomWalkCurrent) * walkSpeed;
const lfoL = this.generateLFO(lfoPhaseL, params.lfo.waveform, lfoSampleHoldValue, lfoRandomWalkCurrent);
const lfoR = this.generateLFO(lfoPhaseR, params.lfo.waveform, lfoSampleHoldValue, lfoRandomWalkCurrent);
const lfoModL = lfoL * params.lfo.depth;
const lfoModR = lfoR * params.lfo.depth;
let pitchModL = 0, pitchModR = 0;
let dcwMod1 = 0, dcwMod2 = 0;
if (params.lfo.target === 'pitch') {
pitchModL = lfoModL * 0.02;
pitchModR = lfoModR * 0.02;
} else {
dcwMod1 = lfoModL * 0.3;
dcwMod2 = lfoModL * 0.3;
}
const [sampleL, sampleR] = this.processAlgorithm(
params.algorithm,
params.oscillators,
[osc1PhaseL, osc2PhaseL],
[osc1PhaseR, osc2PhaseR],
[env1, env2],
[Math.max(0, Math.min(1, dcw1 + dcwMod1)), Math.max(0, Math.min(1, dcw2 + dcwMod2))]
);
let outL = sampleL;
let outR = sampleR;
outL = this.softClip(outL);
outR = this.softClip(outR);
// DC blocker
const dcFilteredL = outL - dcBlockerPrevL + dcBlockerCutoff * dcBlockerAccL;
dcBlockerPrevL = outL;
dcBlockerAccL = dcFilteredL;
const dcFilteredR = outR - dcBlockerPrevR + dcBlockerCutoff * dcBlockerAccR;
dcBlockerPrevR = outR;
dcBlockerAccR = dcFilteredR;
leftBuffer[i] = dcFilteredL;
rightBuffer[i] = dcFilteredR;
// Advance oscillator phases
const detune1 = Math.pow(2, params.oscillators[0].detune / 1200);
const detune2 = Math.pow(2, params.oscillators[1].detune / 1200);
const osc1FreqL = leftFreq * detune1 * (1 + pitchModL);
const osc1FreqR = rightFreq * detune1 * (1 + pitchModR);
const osc2FreqL = leftFreq * detune2 * (1 + pitchModL);
const osc2FreqR = rightFreq * detune2 * (1 + pitchModR);
osc1PhaseL += (TAU * osc1FreqL) / sampleRate;
osc1PhaseR += (TAU * osc1FreqR) / sampleRate;
osc2PhaseL += (TAU * osc2FreqL) / sampleRate;
osc2PhaseR += (TAU * osc2FreqR) / sampleRate;
while (osc1PhaseL >= TAU) osc1PhaseL -= TAU;
while (osc1PhaseR >= TAU) osc1PhaseR -= TAU;
while (osc2PhaseL >= TAU) osc2PhaseL -= TAU;
while (osc2PhaseR >= TAU) osc2PhaseR -= TAU;
lfoPhaseL += (TAU * params.lfo.rate) / sampleRate;
lfoPhaseR += (TAU * params.lfo.rate) / sampleRate;
while (lfoPhaseL >= TAU) lfoPhaseL -= TAU;
while (lfoPhaseR >= TAU) lfoPhaseR -= TAU;
}
// Normalize
let peakL = 0;
let peakR = 0;
for (let i = 0; i < numSamples; i++) {
peakL = Math.max(peakL, Math.abs(leftBuffer[i]));
peakR = Math.max(peakR, Math.abs(rightBuffer[i]));
}
const peak = Math.max(peakL, peakR);
if (peak > 0.001) {
const normalizeGain = 0.85 / peak;
for (let i = 0; i < numSamples; i++) {
leftBuffer[i] *= normalizeGain;
rightBuffer[i] *= normalizeGain;
}
}
return [leftBuffer, rightBuffer];
}
private processAlgorithm(
algorithm: PDAlgorithm,
oscillators: [PDOscillatorParams, PDOscillatorParams],
phasesL: number[],
phasesR: number[],
envelopes: number[],
dcws: number[]
): [number, number] {
switch (algorithm) {
case PDAlgorithm.Single: {
// Single oscillator
const outL = this.generatePDWaveform(phasesL[0], oscillators[0].waveform, dcws[0], oscillators[0].pulseWidth)
* envelopes[0] * oscillators[0].level;
const outR = this.generatePDWaveform(phasesR[0], oscillators[0].waveform, dcws[0], oscillators[0].pulseWidth)
* envelopes[0] * oscillators[0].level;
return [outL, outR];
}
case PDAlgorithm.Dual: {
// Two oscillators with different waveforms
const osc1L = this.generatePDWaveform(phasesL[0], oscillators[0].waveform, dcws[0], oscillators[0].pulseWidth)
* envelopes[0] * oscillators[0].level;
const osc1R = this.generatePDWaveform(phasesR[0], oscillators[0].waveform, dcws[0], oscillators[0].pulseWidth)
* envelopes[0] * oscillators[0].level;
const osc2L = this.generatePDWaveform(phasesL[1], oscillators[1].waveform, dcws[1], oscillators[1].pulseWidth)
* envelopes[1] * oscillators[1].level;
const osc2R = this.generatePDWaveform(phasesR[1], oscillators[1].waveform, dcws[1], oscillators[1].pulseWidth)
* envelopes[1] * oscillators[1].level;
return [(osc1L + osc2L) * 0.707, (osc1R + osc2R) * 0.707];
}
case PDAlgorithm.Detune: {
// Two slightly detuned oscillators (chorus effect)
const osc1L = this.generatePDWaveform(phasesL[0], oscillators[0].waveform, dcws[0], oscillators[0].pulseWidth)
* envelopes[0] * oscillators[0].level;
const osc1R = this.generatePDWaveform(phasesR[0], oscillators[0].waveform, dcws[0], oscillators[0].pulseWidth)
* envelopes[0] * oscillators[0].level;
const osc2L = this.generatePDWaveform(phasesL[1], oscillators[0].waveform, dcws[0], oscillators[0].pulseWidth)
* envelopes[0] * oscillators[1].level;
const osc2R = this.generatePDWaveform(phasesR[1], oscillators[0].waveform, dcws[0], oscillators[0].pulseWidth)
* envelopes[0] * oscillators[1].level;
return [(osc1L + osc2L) * 0.707, (osc1R + osc2R) * 0.707];
}
case PDAlgorithm.Octave: {
// Oscillator + octave up
const osc1L = this.generatePDWaveform(phasesL[0], oscillators[0].waveform, dcws[0], oscillators[0].pulseWidth)
* envelopes[0] * oscillators[0].level;
const osc1R = this.generatePDWaveform(phasesR[0], oscillators[0].waveform, dcws[0], oscillators[0].pulseWidth)
* envelopes[0] * oscillators[0].level;
const osc2L = this.generatePDWaveform(phasesL[1] * 2, oscillators[1].waveform, dcws[1], oscillators[1].pulseWidth)
* envelopes[1] * oscillators[1].level;
const osc2R = this.generatePDWaveform(phasesR[1] * 2, oscillators[1].waveform, dcws[1], oscillators[1].pulseWidth)
* envelopes[1] * oscillators[1].level;
return [(osc1L + osc2L) * 0.707, (osc1R + osc2R) * 0.707];
}
case PDAlgorithm.Fifth: {
// Base + fifth up (1.5x frequency)
const osc1L = this.generatePDWaveform(phasesL[0], oscillators[0].waveform, dcws[0], oscillators[0].pulseWidth)
* envelopes[0] * oscillators[0].level;
const osc1R = this.generatePDWaveform(phasesR[0], oscillators[0].waveform, dcws[0], oscillators[0].pulseWidth)
* envelopes[0] * oscillators[0].level;
const osc2L = this.generatePDWaveform(phasesL[1] * 1.5, oscillators[1].waveform, dcws[1], oscillators[1].pulseWidth)
* envelopes[1] * oscillators[1].level;
const osc2R = this.generatePDWaveform(phasesR[1] * 1.5, oscillators[1].waveform, dcws[1], oscillators[1].pulseWidth)
* envelopes[1] * oscillators[1].level;
return [(osc1L + osc2L) * 0.707, (osc1R + osc2R) * 0.707];
}
case PDAlgorithm.Sub: {
// Base + octave down
const osc1L = this.generatePDWaveform(phasesL[0], oscillators[0].waveform, dcws[0], oscillators[0].pulseWidth)
* envelopes[0] * oscillators[0].level;
const osc1R = this.generatePDWaveform(phasesR[0], oscillators[0].waveform, dcws[0], oscillators[0].pulseWidth)
* envelopes[0] * oscillators[0].level;
const osc2L = this.generatePDWaveform(phasesL[1] * 0.5, oscillators[1].waveform, dcws[1], oscillators[1].pulseWidth)
* envelopes[1] * oscillators[1].level;
const osc2R = this.generatePDWaveform(phasesR[1] * 0.5, oscillators[1].waveform, dcws[1], oscillators[1].pulseWidth)
* envelopes[1] * oscillators[1].level;
return [(osc1L + osc2L) * 0.707, (osc1R + osc2R) * 0.707];
}
case PDAlgorithm.Stack: {
// Base + fifth + octave - three oscillators!
const osc1L = this.generatePDWaveform(phasesL[0], oscillators[0].waveform, dcws[0], oscillators[0].pulseWidth)
* envelopes[0] * oscillators[0].level;
const osc1R = this.generatePDWaveform(phasesR[0], oscillators[0].waveform, dcws[0], oscillators[0].pulseWidth)
* envelopes[0] * oscillators[0].level;
const osc2L = this.generatePDWaveform(phasesL[1] * 1.5, oscillators[1].waveform, dcws[1], oscillators[1].pulseWidth)
* envelopes[1] * oscillators[1].level;
const osc2R = this.generatePDWaveform(phasesR[1] * 1.5, oscillators[1].waveform, dcws[1], oscillators[1].pulseWidth)
* envelopes[1] * oscillators[1].level;
const osc3L = this.generatePDWaveform(phasesL[1] * 2, oscillators[1].waveform, dcws[1], oscillators[1].pulseWidth)
* envelopes[1] * oscillators[1].level * 0.7;
const osc3R = this.generatePDWaveform(phasesR[1] * 2, oscillators[1].waveform, dcws[1], oscillators[1].pulseWidth)
* envelopes[1] * oscillators[1].level * 0.7;
return [(osc1L + osc2L + osc3L) * 0.577, (osc1R + osc2R + osc3R) * 0.577];
}
case PDAlgorithm.Wide: {
// Heavily detuned oscillators for super wide chorus
const osc1L = this.generatePDWaveform(phasesL[0], oscillators[0].waveform, dcws[0], oscillators[0].pulseWidth)
* envelopes[0] * oscillators[0].level;
const osc1R = this.generatePDWaveform(phasesR[0], oscillators[0].waveform, dcws[0], oscillators[0].pulseWidth)
* envelopes[0] * oscillators[0].level;
const osc2L = this.generatePDWaveform(phasesL[1], oscillators[1].waveform, dcws[1], oscillators[1].pulseWidth)
* envelopes[1] * oscillators[1].level;
const osc2R = this.generatePDWaveform(phasesR[1], oscillators[1].waveform, dcws[1], oscillators[1].pulseWidth)
* envelopes[1] * oscillators[1].level;
return [(osc1L + osc2L) * 0.707, (osc1R + osc2R) * 0.707];
}
default:
return [0, 0];
}
}
private generatePDWaveform(phase: number, waveform: PDWaveform, dcw: number, pulseWidth: number): number {
const TAU = Math.PI * 2;
let normalizedPhase = phase / TAU;
normalizedPhase = normalizedPhase - Math.floor(normalizedPhase);
// Apply phase distortion based on DCW
const distortedPhase = this.applyPhaseDistortion(normalizedPhase, dcw);
switch (waveform) {
case PDWaveform.Sine:
return Math.sin(distortedPhase * TAU);
case PDWaveform.SawUp:
return distortedPhase * 2 - 1;
case PDWaveform.SawDown:
return 1 - distortedPhase * 2;
case PDWaveform.Pulse:
return distortedPhase < pulseWidth ? 1 : -1;
case PDWaveform.DoubleSine:
return Math.sin(distortedPhase * TAU * 2);
case PDWaveform.ResonantSaw:
return this.resonantSaw(distortedPhase, dcw);
default:
return Math.sin(distortedPhase * TAU);
}
}
private applyPhaseDistortion(phase: number, dcw: number): number {
// Casio CZ-style phase distortion
// DCW = 0: no distortion (linear phase)
// DCW = 1: maximum distortion (transforms waveform)
if (dcw < 0.01) return phase;
// Add a triangle wave to the phase - this is the classic CZ method
// The triangle modulates how fast we read through the waveform
const trianglePhase = phase < 0.5 ? phase * 2 : 2 - phase * 2;
// Mix between linear and distorted phase
return phase + (trianglePhase - 0.5) * dcw;
}
private resonantSaw(phase: number, brightness: number): number {
// Band-limited sawtooth with fewer harmonics for cleaner sound
const maxHarmonics = Math.min(4, Math.floor(1 + brightness * 3));
let sum = 0;
let normalization = 0;
for (let n = 1; n <= maxHarmonics; n++) {
sum += Math.sin(Math.PI * 2 * phase * n) / n;
normalization += 1 / n;
}
return normalization > 0 ? (sum / normalization) * 0.7 : 0;
}
private calculateEnvelope(t: number, duration: number, osc: PDOscillatorParams): number {
const attackTime = Math.max(0.0001, osc.attack * duration);
const decayTime = Math.max(0.0001, osc.decay * duration);
const releaseTime = Math.max(0.0001, osc.release * duration);
const sustainStart = attackTime + decayTime;
const releaseStart = duration - releaseTime;
if (t < attackTime) {
return t / attackTime;
} else if (t < sustainStart) {
const progress = (t - attackTime) / decayTime;
return 1 - progress * (1 - osc.sustain);
} else if (t < releaseStart) {
return osc.sustain;
} else {
const progress = (t - releaseStart) / releaseTime;
return osc.sustain * (1 - progress);
}
}
private calculateDCWEnvelope(t: number, duration: number, osc: PDOscillatorParams): number {
const attackTime = Math.max(0.0001, osc.dcwAttack * duration);
const decayTime = Math.max(0.0001, osc.dcwDecay * duration);
const releaseTime = Math.max(0.0001, osc.dcwRelease * duration);
const sustainStart = attackTime + decayTime;
const releaseStart = duration - releaseTime;
if (t < attackTime) {
return (t / attackTime) * osc.dcw;
} else if (t < sustainStart) {
const progress = (t - attackTime) / decayTime;
return osc.dcw * (1 - progress * (1 - osc.dcwSustain));
} else if (t < releaseStart) {
return osc.dcw * osc.dcwSustain;
} else {
const progress = (t - releaseStart) / releaseTime;
return osc.dcw * osc.dcwSustain * (1 - progress);
}
}
private generateLFO(phase: number, waveform: LFOWaveform, sampleHoldValue: number, randomWalkValue: number): number {
const TAU = Math.PI * 2;
let normalizedPhase = phase / TAU;
normalizedPhase = normalizedPhase - Math.floor(normalizedPhase);
switch (waveform) {
case LFOWaveform.Sine:
return Math.sin(phase);
case LFOWaveform.Triangle:
return normalizedPhase < 0.5
? normalizedPhase * 4 - 1
: 3 - normalizedPhase * 4;
case LFOWaveform.Square:
return normalizedPhase < 0.5 ? 1 : -1;
case LFOWaveform.Saw:
return normalizedPhase * 2 - 1;
case LFOWaveform.SampleHold:
return sampleHoldValue;
case LFOWaveform.RandomWalk:
return randomWalkValue;
default:
return 0;
}
}
private softClip(x: number): number {
const absX = Math.abs(x);
if (absX < 0.7) return x;
if (absX > 3) return Math.sign(x) * 0.98;
const x2 = x * x;
return x * (27 + x2) / (27 + 9 * x2);
}
randomParams(pitchLock?: PitchLock): PhaseDistortionFMParams {
const algorithm = this.randomInt(0, 7) as PDAlgorithm;
let baseFreq: number;
if (pitchLock?.enabled) {
baseFreq = pitchLock.frequency;
} else {
const baseFreqChoices = [55, 82.4, 110, 146.8, 220, 293.7, 440, 587.3, 880];
baseFreq = this.randomChoice(baseFreqChoices);
}
return {
baseFreq,
algorithm,
oscillators: [
this.randomOscillator(algorithm),
this.randomOscillator(algorithm),
],
lfo: {
rate: this.randomRange(0.5, 8),
depth: this.randomRange(0, 0.4),
waveform: this.randomInt(0, 5) as LFOWaveform,
target: this.randomChoice(['pitch', 'dcw'] as const),
},
stereoWidth: this.randomRange(0.1, 0.5),
};
}
private randomOscillator(algorithm: PDAlgorithm): PDOscillatorParams {
const waveform = this.randomInt(0, 5) as PDWaveform;
let detune = 0;
if (algorithm === PDAlgorithm.Detune) {
detune = this.randomRange(-10, 10);
} else if (algorithm === PDAlgorithm.Wide) {
detune = this.randomRange(-30, 30);
}
return {
waveform,
level: this.randomRange(0.5, 0.9),
detune,
dcw: this.randomRange(0.2, 0.8),
pulseWidth: this.randomRange(0.2, 0.8),
attack: this.randomRange(0.001, 0.1),
decay: this.randomRange(0.02, 0.2),
sustain: this.randomRange(0.3, 0.8),
release: this.randomRange(0.05, 0.3),
dcwAttack: this.randomRange(0.001, 0.08),
dcwDecay: this.randomRange(0.02, 0.15),
dcwSustain: this.randomRange(0.2, 0.7),
dcwRelease: this.randomRange(0.05, 0.25),
};
}
mutateParams(params: PhaseDistortionFMParams, mutationAmount: number = 0.15, pitchLock?: PitchLock): PhaseDistortionFMParams {
const baseFreq = pitchLock?.enabled ? pitchLock.frequency : params.baseFreq;
return {
baseFreq,
algorithm: Math.random() < 0.1 ? this.randomInt(0, 7) as PDAlgorithm : params.algorithm,
oscillators: params.oscillators.map(osc =>
this.mutateOscillator(osc, mutationAmount)
) as [PDOscillatorParams, PDOscillatorParams],
lfo: {
rate: this.mutateValue(params.lfo.rate, mutationAmount, 0.1, 15),
depth: this.mutateValue(params.lfo.depth, mutationAmount, 0, 0.6),
waveform: Math.random() < 0.08 ? this.randomInt(0, 5) as LFOWaveform : params.lfo.waveform,
target: Math.random() < 0.08 ? this.randomChoice(['pitch', 'dcw'] as const) : params.lfo.target,
},
stereoWidth: this.mutateValue(params.stereoWidth, mutationAmount, 0, 0.7),
};
}
private mutateOscillator(osc: PDOscillatorParams, amount: number): PDOscillatorParams {
return {
waveform: Math.random() < 0.1 ? this.randomInt(0, 5) as PDWaveform : osc.waveform,
level: this.mutateValue(osc.level, amount, 0.3, 1.0),
detune: this.mutateValue(osc.detune, amount, -20, 20),
dcw: this.mutateValue(osc.dcw, amount, 0, 1),
pulseWidth: this.mutateValue(osc.pulseWidth, amount, 0.1, 0.9),
attack: this.mutateValue(osc.attack, amount, 0.001, 0.2),
decay: this.mutateValue(osc.decay, amount, 0.01, 0.3),
sustain: this.mutateValue(osc.sustain, amount, 0.1, 0.95),
release: this.mutateValue(osc.release, amount, 0.02, 0.5),
dcwAttack: this.mutateValue(osc.dcwAttack, amount, 0.001, 0.15),
dcwDecay: this.mutateValue(osc.dcwDecay, amount, 0.01, 0.25),
dcwSustain: this.mutateValue(osc.dcwSustain, amount, 0.1, 0.9),
dcwRelease: this.mutateValue(osc.dcwRelease, amount, 0.02, 0.4),
};
}
private randomRange(min: number, max: number): number {
return min + Math.random() * (max - min);
}
private randomInt(min: number, max: number): number {
return Math.floor(this.randomRange(min, max + 1));
}
private randomChoice<T>(choices: readonly T[]): T {
return choices[Math.floor(Math.random() * choices.length)];
}
private mutateValue(value: number, amount: number, min: number, max: number): number {
const variation = value * amount * (Math.random() * 2 - 1);
return Math.max(min, Math.min(max, value + variation));
}
}

View File

@ -1,4 +1,4 @@
import type { SynthEngine, PitchLock } from './SynthEngine';
import type { SynthEngine, PitchLock } from './base/SynthEngine';
enum LFOWaveform {
Sine,
@ -82,6 +82,10 @@ export class Ring implements SynthEngine<RingParams> {
return 'generative' as const;
}
getCategory() {
return 'Modulation' as const;
}
generate(params: RingParams, sampleRate: number, duration: number): [Float32Array, Float32Array] {
const numSamples = Math.floor(sampleRate * duration);
const leftBuffer = new Float32Array(numSamples);
@ -248,6 +252,22 @@ export class Ring implements SynthEngine<RingParams> {
if (secondModPhaseR > TAU * 1000) secondModPhaseR -= TAU * 1000;
}
let peakL = 0;
let peakR = 0;
for (let i = 0; i < numSamples; i++) {
peakL = Math.max(peakL, Math.abs(leftBuffer[i]));
peakR = Math.max(peakR, Math.abs(rightBuffer[i]));
}
const peak = Math.max(peakL, peakR);
if (peak > 0.001) {
const normalizeGain = 0.85 / peak;
for (let i = 0; i < numSamples; i++) {
leftBuffer[i] *= normalizeGain;
rightBuffer[i] *= normalizeGain;
}
}
return [leftBuffer, rightBuffer];
}

View File

@ -0,0 +1,178 @@
import { CsoundEngine, type CsoundParameter } from './base/CsoundEngine';
import type { PitchLock } from './base/SynthEngine';
interface RingCymbalParams {
baseFreq: number;
overtone1Freq: number;
overtone2Freq: number;
overtone3Freq: number;
overtone1Vol: number;
overtone2Vol: number;
overtone3Vol: number;
filterCutoff: number;
resonance: number;
decay: number;
attack: number;
noise: number;
brightness: number;
spread: number;
}
export class RingCymbal extends CsoundEngine<RingCymbalParams> {
getName(): string {
return 'Ring Cymbal';
}
getDescription(): string {
return 'Metallic cymbal using ring modulation with noise and multiple oscillators';
}
getType() {
return 'generative' as const;
}
getCategory() {
return 'Percussion' as const;
}
protected getOrchestra(): string {
return `
instr 1
iBaseFreq chnget "baseFreq"
iOvertone1Freq chnget "overtone1Freq"
iOvertone2Freq chnget "overtone2Freq"
iOvertone3Freq chnget "overtone3Freq"
iOvertone1Vol chnget "overtone1Vol"
iOvertone2Vol chnget "overtone2Vol"
iOvertone3Vol chnget "overtone3Vol"
iFilterCutoff chnget "filterCutoff"
iResonance chnget "resonance"
iDecay chnget "decay"
iAttack chnget "attack"
iNoise chnget "noise"
iBrightness chnget "brightness"
iSpread chnget "spread"
idur = p3
iDecayTime = iDecay * idur
iAttackTime = iAttack * idur
; Exponential decay envelope with attack
kEnv linseg 0, iAttackTime, 1, iDecayTime - iAttackTime, 0.001, 0.001, 0
kEnv = kEnv * kEnv
; Generate white noise source
aNoise noise 1, 0
; Generate impulse oscillators at different frequencies
aOsc1 oscili 1, iBaseFreq
aOsc2 oscili iOvertone1Vol, iOvertone1Freq
aOsc3 oscili iOvertone2Vol, iOvertone2Freq
aOsc4 oscili iOvertone3Vol, iOvertone3Freq
; Ring modulation: multiply noise with oscillators
aRing1 = aNoise * aOsc1
aRing2 = aNoise * aOsc2
aRing3 = aNoise * aOsc3
aRing4 = aNoise * aOsc4
; Mix ring modulated signals
aMix = (aRing1 + aRing2 + aRing3 + aRing4) * 0.25
; Add raw noise for character
aMix = aMix * (1 - iNoise) + aNoise * iNoise * 0.3
; Apply resonant high-pass filter for metallic character
aFiltered butterhp aMix, iFilterCutoff, iResonance
; Additional high-pass for brightness
if iBrightness > 0.3 then
aFiltered butterhp aFiltered, iFilterCutoff * (1 + iBrightness), iResonance * 0.5
endif
; Apply envelope
aOut = aFiltered * kEnv * 0.4
; Stereo spread using slightly different filter parameters
iFilterCutoffR = iFilterCutoff * (1 + iSpread * 0.1)
aFilteredR butterhp aMix, iFilterCutoffR, iResonance
if iBrightness > 0.3 then
aFilteredR butterhp aFilteredR, iFilterCutoffR * (1 + iBrightness), iResonance * 0.5
endif
aOutR = aFilteredR * kEnv * 0.4
outs aOut, aOutR
endin
`;
}
protected getParametersForCsound(params: RingCymbalParams): CsoundParameter[] {
return [
{ channelName: 'baseFreq', value: params.baseFreq },
{ channelName: 'overtone1Freq', value: params.overtone1Freq },
{ channelName: 'overtone2Freq', value: params.overtone2Freq },
{ channelName: 'overtone3Freq', value: params.overtone3Freq },
{ channelName: 'overtone1Vol', value: params.overtone1Vol },
{ channelName: 'overtone2Vol', value: params.overtone2Vol },
{ channelName: 'overtone3Vol', value: params.overtone3Vol },
{ channelName: 'filterCutoff', value: params.filterCutoff },
{ channelName: 'resonance', value: params.resonance },
{ channelName: 'decay', value: params.decay },
{ channelName: 'attack', value: params.attack },
{ channelName: 'noise', value: params.noise },
{ channelName: 'brightness', value: params.brightness },
{ channelName: 'spread', value: params.spread },
];
}
randomParams(pitchLock?: PitchLock): RingCymbalParams {
const baseFreq = pitchLock?.enabled ? pitchLock.frequency : this.randomRange(800, 1800);
const inharmonicRatios = [1.4, 1.7, 2.1, 2.3, 2.9, 3.1, 3.7, 4.3];
return {
baseFreq,
overtone1Freq: baseFreq * this.randomChoice(inharmonicRatios),
overtone2Freq: baseFreq * this.randomChoice(inharmonicRatios),
overtone3Freq: baseFreq * this.randomChoice(inharmonicRatios),
overtone1Vol: this.randomRange(0.4, 1.0),
overtone2Vol: this.randomRange(0.3, 0.9),
overtone3Vol: this.randomRange(0.2, 0.7),
filterCutoff: this.randomRange(3000, 8000),
resonance: this.randomRange(2, 8),
decay: this.randomRange(0.3, 0.9),
attack: this.randomRange(0.001, 0.02),
noise: this.randomRange(0.1, 0.5),
brightness: this.randomRange(0, 0.8),
spread: this.randomRange(0.01, 0.15),
};
}
mutateParams(
params: RingCymbalParams,
mutationAmount: number = 0.15,
pitchLock?: PitchLock
): RingCymbalParams {
const baseFreq = pitchLock?.enabled ? pitchLock.frequency : params.baseFreq;
const freqRatio = baseFreq / params.baseFreq;
return {
baseFreq,
overtone1Freq: this.mutateValue(params.overtone1Freq * freqRatio, mutationAmount, baseFreq * 1.2, baseFreq * 5),
overtone2Freq: this.mutateValue(params.overtone2Freq * freqRatio, mutationAmount, baseFreq * 1.2, baseFreq * 5),
overtone3Freq: this.mutateValue(params.overtone3Freq * freqRatio, mutationAmount, baseFreq * 1.2, baseFreq * 5),
overtone1Vol: this.mutateValue(params.overtone1Vol, mutationAmount, 0.2, 1.0),
overtone2Vol: this.mutateValue(params.overtone2Vol, mutationAmount, 0.1, 1.0),
overtone3Vol: this.mutateValue(params.overtone3Vol, mutationAmount, 0.1, 0.9),
filterCutoff: this.mutateValue(params.filterCutoff, mutationAmount, 2000, 10000),
resonance: this.mutateValue(params.resonance, mutationAmount, 1, 12),
decay: this.mutateValue(params.decay, mutationAmount, 0.2, 1.0),
attack: this.mutateValue(params.attack, mutationAmount, 0.001, 0.05),
noise: this.mutateValue(params.noise, mutationAmount, 0, 0.7),
brightness: this.mutateValue(params.brightness, mutationAmount, 0, 1),
spread: this.mutateValue(params.spread, mutationAmount, 0.005, 0.25),
};
}
}

View File

@ -1,4 +1,4 @@
import type { SynthEngine, PitchLock } from './SynthEngine';
import type { SynthEngine, PitchLock } from './base/SynthEngine';
interface SampleParams {
loaded: boolean;
@ -21,6 +21,10 @@ export class Sample implements SynthEngine<SampleParams> {
return 'sample' as const;
}
getCategory() {
return 'Utility' as const;
}
async loadFile(file: File): Promise<void> {
const arrayBuffer = await file.arrayBuffer();
const audioContext = new AudioContext();

View File

@ -0,0 +1,238 @@
import type { PitchLock, SynthEngine } from './base/SynthEngine';
interface SnareParams {
// Core frequency (base pitch of the snare)
baseFreq: number;
// Tone control (0 = more tonal, 1 = more noise)
tone: number;
// Snap amount (pitch modulation intensity)
snap: number;
// Decay times
tonalDecay: number;
noiseDecay: number;
// Accent (volume boost)
accent: number;
// Tuning offset
tuning: number;
}
export class Snare implements SynthEngine {
getName(): string {
return 'Noise Snare';
}
getDescription(): string {
return 'Classic snare drum with triangle wave and noise';
}
getType() {
return 'generative' as const;
}
getCategory() {
return 'Percussion' as const;
}
randomParams(pitchLock?: PitchLock): SnareParams {
return {
baseFreq: pitchLock ? this.freqToParam(pitchLock.frequency) : 0.3 + Math.random() * 0.4,
tone: 0.4 + Math.random() * 0.4,
snap: 0.5 + Math.random() * 0.5,
tonalDecay: 0.15 + Math.random() * 0.35,
noiseDecay: 0.4 + Math.random() * 0.4,
accent: 0.5 + Math.random() * 0.5,
tuning: pitchLock ? 0.5 : 0.4 + Math.random() * 0.2,
};
}
mutateParams(params: SnareParams, mutationAmount: number = 0.15, pitchLock?: PitchLock): SnareParams {
const mutate = (value: number, amount: number = mutationAmount): number => {
return Math.max(0, Math.min(1, value + (Math.random() - 0.5) * amount));
};
return {
baseFreq: pitchLock ? params.baseFreq : mutate(params.baseFreq, 0.2),
tone: mutate(params.tone, 0.25),
snap: mutate(params.snap, 0.2),
tonalDecay: mutate(params.tonalDecay, 0.2),
noiseDecay: mutate(params.noiseDecay, 0.2),
accent: mutate(params.accent, 0.2),
tuning: pitchLock ? params.tuning : mutate(params.tuning, 0.15),
};
}
generate(
params: SnareParams,
sampleRate: number,
duration: number,
pitchLock?: PitchLock
): [Float32Array, Float32Array] {
const numSamples = Math.floor(sampleRate * duration);
const left = new Float32Array(numSamples);
const right = new Float32Array(numSamples);
// Base frequency: 100Hz to 400Hz
const baseFreq = pitchLock ? pitchLock.frequency : 100 + params.baseFreq * 300;
// Tuning offset: -12% to +12%
const tuningFactor = 0.88 + params.tuning * 0.24;
const tunedFreq = baseFreq * tuningFactor;
// Pitch modulation parameters (starts 4x higher, drops over 10ms)
const pitchMultiplier = 1 + params.snap * 3; // 1x to 4x
const modDuration = 0.008 + params.snap * 0.007; // 8ms to 15ms
// Decay times scaled by duration
const tonalDecayTime = (0.05 + params.tonalDecay * 0.15) * duration; // 50ms to 200ms
const noiseDecayTime = (0.15 + params.noiseDecay * 0.35) * duration; // 150ms to 500ms
// Volume and gain staging
const baseVolume = 0.5;
const accentGain = baseVolume * (1 + params.accent);
// Attack time (5ms ramp)
const attackTime = 0.005;
// Notch filter parameters for noise (fixed at 1000Hz)
const notchFreq = 1000;
const notchQ = 5;
for (let channel = 0; channel < 2; channel++) {
const output = channel === 0 ? left : right;
// Triangle oscillator phase
let phase = Math.random() * Math.PI * 2;
// Notch filter state variables
let notchState1 = 0;
let notchState2 = 0;
// Stereo variation (slight detune and phase offset)
const stereoDetune = channel === 0 ? 0.998 : 1.002;
const stereoPhaseOffset = channel === 0 ? 0 : 0.1;
for (let i = 0; i < numSamples; i++) {
const t = i / sampleRate;
const normPhase = i / numSamples;
// Pitch envelope: exponential ramp from pitchMultiplier*freq down to base freq
const pitchEnv = Math.exp(-t / modDuration);
const currentFreq = tunedFreq * stereoDetune * (1 + (pitchMultiplier - 1) * pitchEnv);
// Generate triangle wave
phase += (2 * Math.PI * currentFreq) / sampleRate;
if (phase > 2 * Math.PI) phase -= 2 * Math.PI;
// Triangle wave from phase
const triangle = phase < Math.PI
? -1 + (2 * phase) / Math.PI
: 3 - (2 * phase) / Math.PI;
// Attack envelope (5ms linear ramp)
const attackEnv = t < attackTime ? t / attackTime : 1.0;
// Tonal envelope: exponential decay
const tonalEnv = Math.exp(-t / tonalDecayTime) * attackEnv;
const tonalSignal = triangle * tonalEnv;
// Generate white noise
const noise = Math.random() * 2 - 1;
// Apply notch filter to noise (removes 1000Hz component)
const notchFiltered = this.notchFilter(
noise,
notchFreq,
notchQ,
sampleRate,
notchState1,
notchState2
);
notchState1 = notchFiltered.state1;
notchState2 = notchFiltered.state2;
// Noise envelope: exponential decay (longer than tonal)
const noiseEnv = Math.exp(-t / noiseDecayTime);
const noiseSignal = notchFiltered.output * noiseEnv;
// Mix tonal and noise
const tonalGain = baseVolume * (1 - params.tone * 0.5); // 0.5 to 0.25
const noiseGain = 0.15 + params.tone * 0.2; // 0.15 to 0.35
let sample = tonalSignal * tonalGain + noiseSignal * noiseGain;
// Apply accent
sample *= (0.5 + params.accent * 0.5);
// Soft clipping
sample = this.softClip(sample);
output[i] = sample;
}
}
// Normalize output to consistent level
let peak = 0;
for (let i = 0; i < numSamples; i++) {
peak = Math.max(peak, Math.abs(left[i]), Math.abs(right[i]));
}
if (peak > 0.001) {
const normGain = 0.5 / peak; // Normalize to 0.5 peak for more headroom
for (let i = 0; i < numSamples; i++) {
left[i] *= normGain;
right[i] *= normGain;
}
}
return [left, right];
}
private notchFilter(
input: number,
frequency: number,
q: number,
sampleRate: number,
state1: number,
state2: number
): { output: number; state1: number; state2: number } {
// State variable filter configured as notch
const normalizedFreq = Math.min(frequency / sampleRate, 0.48);
const f = 2 * Math.sin(Math.PI * normalizedFreq);
const qRecip = 1 / Math.max(q, 0.5);
const lowpass = state2 + f * state1;
const highpass = input - lowpass - qRecip * state1;
const bandpass = f * highpass + state1;
// Notch = input - bandpass
const notch = input - bandpass;
// Update states with clamping
const newState1 = Math.max(-2, Math.min(2, bandpass));
const newState2 = Math.max(-2, Math.min(2, lowpass));
return {
output: notch,
state1: newState1,
state2: newState2,
};
}
private softClip(x: number): number {
if (x > 1) return 1;
if (x < -1) return -1;
if (x > 0.66) return (3 - (2 - 3 * x) ** 2) / 3;
if (x < -0.66) return -(3 - (2 - 3 * -x) ** 2) / 3;
return x;
}
private freqToParam(freq: number): number {
// Map frequency to 0-1 range (100-400 Hz)
return Math.max(0, Math.min(1, (freq - 100) / 300));
}
}

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

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

View File

@ -0,0 +1,342 @@
import { CsoundEngine, type CsoundParameter } from './base/CsoundEngine';
import type { PitchLock } from './base/SynthEngine';
enum Waveform {
Sine = 0,
Saw = 1,
Square = 2,
Triangle = 3,
}
interface OscillatorParams {
waveform: Waveform;
ratio: number;
level: number;
attack: number;
decay: number;
sustain: number;
release: number;
}
interface FilterParams {
cutoff: number;
resonance: number;
envAmount: number;
attack: number;
decay: number;
sustain: number;
release: number;
}
export interface SubtractiveThreeOscParams {
baseFreq: number;
osc1: OscillatorParams;
osc2: OscillatorParams;
osc3: OscillatorParams;
filter: FilterParams;
stereoWidth: number;
}
export class SubtractiveThreeOsc extends CsoundEngine<SubtractiveThreeOscParams> {
getName(): string {
return '3OSC';
}
getDescription(): string {
return 'Three-oscillator subtractive synthesis with resonant filter';
}
getType() {
return 'generative' as const;
}
getCategory() {
return 'Subtractive' as const;
}
protected getOrchestra(): string {
return `
instr 1
; Get base frequency
ibasefreq chnget "basefreq"
istereo chnget "stereowidth"
; Oscillator 1 parameters
iosc1wave chnget "osc1_wave"
iosc1ratio chnget "osc1_ratio"
iosc1level chnget "osc1_level"
iosc1attack chnget "osc1_attack"
iosc1decay chnget "osc1_decay"
iosc1sustain chnget "osc1_sustain"
iosc1release chnget "osc1_release"
; Oscillator 2 parameters
iosc2wave chnget "osc2_wave"
iosc2ratio chnget "osc2_ratio"
iosc2level chnget "osc2_level"
iosc2attack chnget "osc2_attack"
iosc2decay chnget "osc2_decay"
iosc2sustain chnget "osc2_sustain"
iosc2release chnget "osc2_release"
; Oscillator 3 parameters
iosc3wave chnget "osc3_wave"
iosc3ratio chnget "osc3_ratio"
iosc3level chnget "osc3_level"
iosc3attack chnget "osc3_attack"
iosc3decay chnget "osc3_decay"
iosc3sustain chnget "osc3_sustain"
iosc3release chnget "osc3_release"
; Filter parameters
ifiltcutoff chnget "filt_cutoff"
ifiltres chnget "filt_resonance"
ifiltenvamt chnget "filt_envamt"
ifiltattack chnget "filt_attack"
ifiltdecay chnget "filt_decay"
ifiltsustain chnget "filt_sustain"
ifiltrelease chnget "filt_release"
idur = p3
; Convert ratios to time values
iosc1att = iosc1attack * idur
iosc1dec = iosc1decay * idur
iosc1rel = iosc1release * idur
iosc2att = iosc2attack * idur
iosc2dec = iosc2decay * idur
iosc2rel = iosc2release * idur
iosc3att = iosc3attack * idur
iosc3dec = iosc3decay * idur
iosc3rel = iosc3release * idur
ifiltatt = ifiltattack * idur
ifiltdec = ifiltdecay * idur
ifiltrel = ifiltrelease * idur
; Stereo detuning
idetune = 1 + (istereo * 0.001)
ifreqL = ibasefreq / idetune
ifreqR = ibasefreq * idetune
; Oscillator 1 envelopes
kenv1 madsr iosc1att, iosc1dec, iosc1sustain, iosc1rel
; Oscillator 1 - Left
if iosc1wave == 0 then
aosc1L oscili kenv1 * iosc1level, ifreqL * iosc1ratio
elseif iosc1wave == 1 then
aosc1L vco2 kenv1 * iosc1level, ifreqL * iosc1ratio, 0
elseif iosc1wave == 2 then
aosc1L vco2 kenv1 * iosc1level, ifreqL * iosc1ratio, 10
else
aosc1L vco2 kenv1 * iosc1level, ifreqL * iosc1ratio, 12
endif
; Oscillator 1 - Right
if iosc1wave == 0 then
aosc1R oscili kenv1 * iosc1level, ifreqR * iosc1ratio
elseif iosc1wave == 1 then
aosc1R vco2 kenv1 * iosc1level, ifreqR * iosc1ratio, 0
elseif iosc1wave == 2 then
aosc1R vco2 kenv1 * iosc1level, ifreqR * iosc1ratio, 10
else
aosc1R vco2 kenv1 * iosc1level, ifreqR * iosc1ratio, 12
endif
; Oscillator 2 envelopes
kenv2 madsr iosc2att, iosc2dec, iosc2sustain, iosc2rel
; Oscillator 2 - Left
if iosc2wave == 0 then
aosc2L oscili kenv2 * iosc2level, ifreqL * iosc2ratio
elseif iosc2wave == 1 then
aosc2L vco2 kenv2 * iosc2level, ifreqL * iosc2ratio, 0
elseif iosc2wave == 2 then
aosc2L vco2 kenv2 * iosc2level, ifreqL * iosc2ratio, 10
else
aosc2L vco2 kenv2 * iosc2level, ifreqL * iosc2ratio, 12
endif
; Oscillator 2 - Right
if iosc2wave == 0 then
aosc2R oscili kenv2 * iosc2level, ifreqR * iosc2ratio
elseif iosc2wave == 1 then
aosc2R vco2 kenv2 * iosc2level, ifreqR * iosc2ratio, 0
elseif iosc2wave == 2 then
aosc2R vco2 kenv2 * iosc2level, ifreqR * iosc2ratio, 10
else
aosc2R vco2 kenv2 * iosc2level, ifreqR * iosc2ratio, 12
endif
; Oscillator 3 envelopes
kenv3 madsr iosc3att, iosc3dec, iosc3sustain, iosc3rel
; Oscillator 3 - Left
if iosc3wave == 0 then
aosc3L oscili kenv3 * iosc3level, ifreqL * iosc3ratio
elseif iosc3wave == 1 then
aosc3L vco2 kenv3 * iosc3level, ifreqL * iosc3ratio, 0
elseif iosc3wave == 2 then
aosc3L vco2 kenv3 * iosc3level, ifreqL * iosc3ratio, 10
else
aosc3L vco2 kenv3 * iosc3level, ifreqL * iosc3ratio, 12
endif
; Oscillator 3 - Right
if iosc3wave == 0 then
aosc3R oscili kenv3 * iosc3level, ifreqR * iosc3ratio
elseif iosc3wave == 1 then
aosc3R vco2 kenv3 * iosc3level, ifreqR * iosc3ratio, 0
elseif iosc3wave == 2 then
aosc3R vco2 kenv3 * iosc3level, ifreqR * iosc3ratio, 10
else
aosc3R vco2 kenv3 * iosc3level, ifreqR * iosc3ratio, 12
endif
; Mix oscillators
amixL = aosc1L + aosc2L + aosc3L
amixR = aosc1R + aosc2R + aosc3R
; Filter envelope
kfiltenv madsr ifiltatt, ifiltdec, ifiltsustain, ifiltrel
kcutoff = ifiltcutoff + (kfiltenv * ifiltenvamt * 10000)
kcutoff = limit(kcutoff, 20, 20000)
; Apply moogladder filter
afiltL moogladder amixL, kcutoff, ifiltres
afiltR moogladder amixR, kcutoff, ifiltres
outs afiltL, afiltR
endin
`;
}
protected getParametersForCsound(params: SubtractiveThreeOscParams): CsoundParameter[] {
return [
{ channelName: 'basefreq', value: params.baseFreq },
{ channelName: 'stereowidth', value: params.stereoWidth },
{ channelName: 'osc1_wave', value: params.osc1.waveform },
{ channelName: 'osc1_ratio', value: params.osc1.ratio },
{ channelName: 'osc1_level', value: params.osc1.level },
{ channelName: 'osc1_attack', value: params.osc1.attack },
{ channelName: 'osc1_decay', value: params.osc1.decay },
{ channelName: 'osc1_sustain', value: params.osc1.sustain },
{ channelName: 'osc1_release', value: params.osc1.release },
{ channelName: 'osc2_wave', value: params.osc2.waveform },
{ channelName: 'osc2_ratio', value: params.osc2.ratio },
{ channelName: 'osc2_level', value: params.osc2.level },
{ channelName: 'osc2_attack', value: params.osc2.attack },
{ channelName: 'osc2_decay', value: params.osc2.decay },
{ channelName: 'osc2_sustain', value: params.osc2.sustain },
{ channelName: 'osc2_release', value: params.osc2.release },
{ channelName: 'osc3_wave', value: params.osc3.waveform },
{ channelName: 'osc3_ratio', value: params.osc3.ratio },
{ channelName: 'osc3_level', value: params.osc3.level },
{ channelName: 'osc3_attack', value: params.osc3.attack },
{ channelName: 'osc3_decay', value: params.osc3.decay },
{ channelName: 'osc3_sustain', value: params.osc3.sustain },
{ channelName: 'osc3_release', value: params.osc3.release },
{ channelName: 'filt_cutoff', value: params.filter.cutoff },
{ channelName: 'filt_resonance', value: params.filter.resonance },
{ channelName: 'filt_envamt', value: params.filter.envAmount },
{ channelName: 'filt_attack', value: params.filter.attack },
{ channelName: 'filt_decay', value: params.filter.decay },
{ channelName: 'filt_sustain', value: params.filter.sustain },
{ channelName: 'filt_release', value: params.filter.release },
];
}
randomParams(pitchLock?: PitchLock): SubtractiveThreeOscParams {
let baseFreq: number;
if (pitchLock?.enabled) {
baseFreq = pitchLock.frequency;
} else {
const baseFreqChoices = [55, 82.4, 110, 146.8, 220, 293.7, 440];
baseFreq = this.randomChoice(baseFreqChoices) * this.randomRange(0.98, 1.02);
}
const harmonicRatios = [0.5, 1, 2, 3, 4];
const detuneRatios = [0.99, 1.0, 1.01, 1.02, 0.98];
return {
baseFreq,
osc1: this.randomOscillator(harmonicRatios),
osc2: this.randomOscillator(detuneRatios),
osc3: this.randomOscillator(harmonicRatios),
filter: this.randomFilter(),
stereoWidth: this.randomRange(0.2, 0.8),
};
}
private randomOscillator(ratios: number[]): OscillatorParams {
return {
waveform: this.randomInt(0, 3) as Waveform,
ratio: this.randomChoice(ratios),
level: this.randomRange(0.2, 0.5),
attack: this.randomRange(0.001, 0.15),
decay: this.randomRange(0.02, 0.25),
sustain: this.randomRange(0.3, 0.8),
release: this.randomRange(0.05, 0.4),
};
}
private randomFilter(): FilterParams {
return {
cutoff: this.randomRange(200, 5000),
resonance: this.randomRange(0.1, 0.8),
envAmount: this.randomRange(0.2, 1.2),
attack: this.randomRange(0.001, 0.15),
decay: this.randomRange(0.05, 0.3),
sustain: this.randomRange(0.2, 0.7),
release: this.randomRange(0.05, 0.4),
};
}
mutateParams(
params: SubtractiveThreeOscParams,
mutationAmount: number = 0.15,
pitchLock?: PitchLock
): SubtractiveThreeOscParams {
const baseFreq = pitchLock?.enabled ? pitchLock.frequency : params.baseFreq;
return {
baseFreq,
osc1: this.mutateOscillator(params.osc1, mutationAmount),
osc2: this.mutateOscillator(params.osc2, mutationAmount),
osc3: this.mutateOscillator(params.osc3, mutationAmount),
filter: this.mutateFilter(params.filter, mutationAmount),
stereoWidth: this.mutateValue(params.stereoWidth, mutationAmount, 0, 1),
};
}
private mutateOscillator(osc: OscillatorParams, amount: number): OscillatorParams {
return {
waveform: Math.random() < 0.1 ? (this.randomInt(0, 3) as Waveform) : osc.waveform,
ratio: Math.random() < 0.1 ? this.randomChoice([0.5, 0.98, 0.99, 1, 1.01, 1.02, 2, 3, 4]) : osc.ratio,
level: this.mutateValue(osc.level, amount, 0.1, 0.7),
attack: this.mutateValue(osc.attack, amount, 0.001, 0.3),
decay: this.mutateValue(osc.decay, amount, 0.01, 0.4),
sustain: this.mutateValue(osc.sustain, amount, 0.1, 0.9),
release: this.mutateValue(osc.release, amount, 0.02, 0.6),
};
}
private mutateFilter(filter: FilterParams, amount: number): FilterParams {
return {
cutoff: this.mutateValue(filter.cutoff, amount, 100, 8000),
resonance: this.mutateValue(filter.resonance, amount, 0, 0.95),
envAmount: this.mutateValue(filter.envAmount, amount, 0, 1.5),
attack: this.mutateValue(filter.attack, amount, 0.001, 0.3),
decay: this.mutateValue(filter.decay, amount, 0.01, 0.4),
sustain: this.mutateValue(filter.sustain, amount, 0.1, 0.9),
release: this.mutateValue(filter.release, amount, 0.02, 0.6),
};
}
}

View File

@ -0,0 +1,187 @@
import { CsoundEngine, type CsoundParameter } from './base/CsoundEngine';
import type { PitchLock } from './base/SynthEngine';
interface TechnoKickParams {
startFreq: number;
endFreq: number;
freqDecay: number;
resonance: number;
cutoffStart: number;
cutoffEnd: number;
cutoffDecay: number;
ampAttack: number;
ampDecay: number;
noiseMix: number;
punch: number;
stereoWidth: number;
}
export class TechnoKick extends CsoundEngine<TechnoKickParams> {
getName(): string {
return 'Techno Kick';
}
getDescription(): string {
return 'Noise through resonant low-pass filter with frequency sweep and RMS compression for punchy electronic kicks';
}
getType() {
return 'generative' as const;
}
getCategory() {
return 'Percussion' as const;
}
protected getOrchestra(): string {
return `
instr 1
iStartFreq chnget "startFreq"
iEndFreq chnget "endFreq"
iFreqDecay chnget "freqDecay"
iResonance chnget "resonance"
iCutoffStart chnget "cutoffStart"
iCutoffEnd chnget "cutoffEnd"
iCutoffDecay chnget "cutoffDecay"
iAmpAttack chnget "ampAttack"
iAmpDecay chnget "ampDecay"
iNoiseMix chnget "noiseMix"
iPunch chnget "punch"
iStereoWidth chnget "stereoWidth"
idur = p3
iFreqDecayTime = iFreqDecay * idur
iCutoffDecayTime = iCutoffDecay * idur
iAmpAttackTime = iAmpAttack * idur
iAmpDecayTime = iAmpDecay * idur
; Generate random noise
aNoise noise 1, 0
; Frequency envelope for the filter cutoff (exponential sweep)
kFreqEnv expseg iStartFreq, iFreqDecayTime, iEndFreq, idur - iFreqDecayTime, iEndFreq
; Cutoff modulation envelope
kCutoffEnv expseg iCutoffStart, iCutoffDecayTime, iCutoffEnd, idur - iCutoffDecayTime, iCutoffEnd
; Apply resonant low-pass filter (rezzy)
aFiltered rezzy aNoise * (1 + iNoiseMix), kCutoffEnv, iResonance
; Add sine sub-bass component for more weight
kSubFreqEnv expseg iStartFreq * 0.5, iFreqDecayTime, iEndFreq * 0.5, idur - iFreqDecayTime, iEndFreq * 0.5
aSubBass oscili 0.6, kSubFreqEnv
; Mix filtered noise and sub-bass
aMix = aFiltered * 0.5 + aSubBass
; Amplitude envelope (exponential)
kAmpEnv expseg 0.001, iAmpAttackTime, 1, iAmpDecayTime, 0.001, idur - iAmpAttackTime - iAmpDecayTime, 0.001
; Apply amplitude envelope
aEnveloped = aMix * kAmpEnv
; RMS compression for punch
; Calculate RMS of signal
kRMS rms aEnveloped
if kRMS < 0.01 then
kCompGain = 1
else
kCompGain = 1 + iPunch * (0.3 / kRMS - 1)
endif
kCompGain limit kCompGain, 0.5, 3
aOut = aEnveloped * kCompGain
; Right channel with stereo width
iStartFreqR = iStartFreq * (1 + iStereoWidth * 0.01)
iEndFreqR = iEndFreq * (1 + iStereoWidth * 0.01)
kFreqEnvR expseg iStartFreqR, iFreqDecayTime, iEndFreqR, idur - iFreqDecayTime, iEndFreqR
kCutoffEnvR expseg iCutoffStart * (1 + iStereoWidth * 0.02), iCutoffDecayTime, iCutoffEnd * (1 + iStereoWidth * 0.02), idur - iCutoffDecayTime, iCutoffEnd * (1 + iStereoWidth * 0.02)
aNoiseR noise 1, 0
aFilteredR rezzy aNoiseR * (1 + iNoiseMix), kCutoffEnvR, iResonance
kSubFreqEnvR expseg iStartFreqR * 0.5, iFreqDecayTime, iEndFreqR * 0.5, idur - iFreqDecayTime, iEndFreqR * 0.5
aSubBassR oscili 0.6, kSubFreqEnvR
aMixR = aFilteredR * 0.5 + aSubBassR
aEnvelopedR = aMixR * kAmpEnv
kRMSR rms aEnvelopedR
if kRMSR < 0.01 then
kCompGainR = 1
else
kCompGainR = 1 + iPunch * (0.3 / kRMSR - 1)
endif
kCompGainR limit kCompGainR, 0.5, 3
aOutR = aEnvelopedR * kCompGainR
outs aOut, aOutR
endin
`;
}
protected getParametersForCsound(params: TechnoKickParams): CsoundParameter[] {
return [
{ channelName: 'startFreq', value: params.startFreq },
{ channelName: 'endFreq', value: params.endFreq },
{ channelName: 'freqDecay', value: params.freqDecay },
{ channelName: 'resonance', value: params.resonance },
{ channelName: 'cutoffStart', value: params.cutoffStart },
{ channelName: 'cutoffEnd', value: params.cutoffEnd },
{ channelName: 'cutoffDecay', value: params.cutoffDecay },
{ channelName: 'ampAttack', value: params.ampAttack },
{ channelName: 'ampDecay', value: params.ampDecay },
{ channelName: 'noiseMix', value: params.noiseMix },
{ channelName: 'punch', value: params.punch },
{ channelName: 'stereoWidth', value: params.stereoWidth },
];
}
randomParams(pitchLock?: PitchLock): TechnoKickParams {
const endFreqChoices = [40, 45, 50, 55, 60, 70, 80];
const endFreq = pitchLock?.enabled
? pitchLock.frequency
: this.randomChoice(endFreqChoices) * this.randomRange(0.95, 1.05);
return {
startFreq: this.randomRange(800, 1200),
endFreq,
freqDecay: this.randomRange(0.05, 0.25),
resonance: this.randomRange(5, 40),
cutoffStart: this.randomRange(300, 800),
cutoffEnd: this.randomRange(80, 200),
cutoffDecay: this.randomRange(0.1, 0.4),
ampAttack: this.randomRange(0.001, 0.005),
ampDecay: this.randomRange(0.2, 0.6),
noiseMix: this.randomRange(0.1, 0.8),
punch: this.randomRange(0.3, 0.9),
stereoWidth: this.randomRange(0, 0.3),
};
}
mutateParams(
params: TechnoKickParams,
mutationAmount: number = 0.15,
pitchLock?: PitchLock
): TechnoKickParams {
const endFreq = pitchLock?.enabled ? pitchLock.frequency : params.endFreq;
return {
startFreq: this.mutateValue(params.startFreq, mutationAmount, 600, 1500),
endFreq,
freqDecay: this.mutateValue(params.freqDecay, mutationAmount, 0.02, 0.4),
resonance: this.mutateValue(params.resonance, mutationAmount, 3, 50),
cutoffStart: this.mutateValue(params.cutoffStart, mutationAmount, 200, 1000),
cutoffEnd: this.mutateValue(params.cutoffEnd, mutationAmount, 60, 300),
cutoffDecay: this.mutateValue(params.cutoffDecay, mutationAmount, 0.05, 0.6),
ampAttack: this.mutateValue(params.ampAttack, mutationAmount, 0.001, 0.01),
ampDecay: this.mutateValue(params.ampDecay, mutationAmount, 0.1, 0.8),
noiseMix: this.mutateValue(params.noiseMix, mutationAmount, 0, 1),
punch: this.mutateValue(params.punch, mutationAmount, 0.1, 1),
stereoWidth: this.mutateValue(params.stereoWidth, mutationAmount, 0, 0.5),
};
}
}

View File

@ -1,4 +1,4 @@
import type { SynthEngine, PitchLock } from './SynthEngine';
import type { SynthEngine, PitchLock } from './base/SynthEngine';
enum EnvCurve {
Linear,
@ -73,6 +73,10 @@ export class TwoOpFM implements SynthEngine<TwoOpFMParams> {
return 'generative' as const;
}
getCategory() {
return 'FM' as const;
}
generate(params: TwoOpFMParams, sampleRate: number, duration: number): [Float32Array, Float32Array] {
const numSamples = Math.floor(sampleRate * duration);
const leftBuffer = new Float32Array(numSamples);

View File

@ -1,4 +1,4 @@
import type { SynthEngine, PitchLock } from './SynthEngine';
import type { SynthEngine, PitchLock } from './base/SynthEngine';
interface WavetableParams {
bankIndex: number;

View File

@ -1,4 +1,4 @@
import type { SynthEngine, PitchLock } from './SynthEngine';
import type { SynthEngine, PitchLock } from './base/SynthEngine';
// @ts-ignore
import { ZZFX } from 'zzfx';
@ -38,6 +38,10 @@ export class ZzfxEngine implements SynthEngine<ZzfxParams> {
return 'generative' as const;
}
getCategory() {
return 'Experimental' as const;
}
generate(params: ZzfxParams, sampleRate: number, duration: number): [Float32Array, Float32Array] {
// ZZFX uses 44100 sample rate internally
const zzfxSampleRate = 44100;

View File

@ -0,0 +1,332 @@
import { Csound } from '@csound/browser';
import type { SynthEngine, PitchLock } from './SynthEngine';
export interface CsoundParameter {
channelName: string;
value: number;
}
export abstract class CsoundEngine<T = any> implements SynthEngine<T> {
abstract getName(): string;
abstract getDescription(): string;
abstract getType(): 'generative' | 'sample' | 'input';
protected abstract getOrchestra(): string;
protected abstract getParametersForCsound(params: T): CsoundParameter[];
abstract randomParams(pitchLock?: PitchLock): T;
abstract mutateParams(params: T, mutationAmount?: number, pitchLock?: PitchLock): T;
async generate(
params: T,
sampleRate: number,
duration: number,
pitchLock?: PitchLock
): Promise<[Float32Array, Float32Array]> {
const orchestra = this.getOrchestra();
const csoundParams = this.getParametersForCsound(params);
const outputFile = '/output.wav';
const csd = this.buildCSD(orchestra, duration, sampleRate, csoundParams, outputFile);
try {
const csound = await Csound();
if (!csound) {
throw new Error('Failed to initialize Csound');
}
await csound.compileCSD(csd);
await csound.start();
await csound.perform();
await csound.cleanup();
const wavData = await csound.fs.readFile(outputFile);
const audioBuffer = await this.parseWavManually(wavData, sampleRate);
await csound.terminateInstance();
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;
this.applyFadeIn(leftChannel, rightChannel, sampleRate);
this.applyFadeOut(leftChannel, rightChannel, sampleRate);
const peak = this.findPeak(leftChannel, rightChannel);
if (peak > 0.001) {
const normalizeGain = 0.85 / peak;
this.applyGain(leftChannel, rightChannel, normalizeGain);
}
return [leftChannel, rightChannel];
} catch (error) {
console.error('Csound generation failed:', error);
const numSamples = Math.floor(sampleRate * duration);
return [new Float32Array(numSamples), new Float32Array(numSamples)];
}
}
private parseWavManually(
wavData: Uint8Array,
expectedSampleRate: number
): { leftChannel: Float32Array; rightChannel: Float32Array } {
const view = new DataView(wavData.buffer);
// Check RIFF header
const riff = String.fromCharCode(view.getUint8(0), view.getUint8(1), view.getUint8(2), view.getUint8(3));
if (riff !== 'RIFF') {
throw new Error('Invalid WAV file: no RIFF header');
}
// Check WAVE format
const wave = String.fromCharCode(view.getUint8(8), view.getUint8(9), view.getUint8(10), view.getUint8(11));
if (wave !== 'WAVE') {
throw new Error('Invalid WAV file: no WAVE format');
}
// Find fmt chunk
let offset = 12;
while (offset < wavData.length) {
const chunkId = String.fromCharCode(
view.getUint8(offset),
view.getUint8(offset + 1),
view.getUint8(offset + 2),
view.getUint8(offset + 3)
);
const chunkSize = view.getUint32(offset + 4, true);
if (chunkId === 'fmt ') {
const audioFormat = view.getUint16(offset + 8, true);
const numChannels = view.getUint16(offset + 10, true);
const sampleRate = view.getUint32(offset + 12, true);
const bitsPerSample = view.getUint16(offset + 22, true);
// Find data chunk
let dataOffset = offset + 8 + chunkSize;
while (dataOffset < wavData.length) {
const dataChunkId = String.fromCharCode(
view.getUint8(dataOffset),
view.getUint8(dataOffset + 1),
view.getUint8(dataOffset + 2),
view.getUint8(dataOffset + 3)
);
const dataChunkSize = view.getUint32(dataOffset + 4, true);
if (dataChunkId === 'data') {
const bytesPerSample = bitsPerSample / 8;
const numSamples = Math.floor(dataChunkSize / bytesPerSample / numChannels);
const leftChannel = new Float32Array(numSamples);
const rightChannel = new Float32Array(numSamples);
let audioDataOffset = dataOffset + 8;
if (bitsPerSample === 16) {
// 16-bit PCM
for (let i = 0; i < numSamples; i++) {
const leftSample = view.getInt16(audioDataOffset, true);
leftChannel[i] = leftSample / 32768.0;
audioDataOffset += 2;
if (numChannels > 1) {
const rightSample = view.getInt16(audioDataOffset, true);
rightChannel[i] = rightSample / 32768.0;
audioDataOffset += 2;
} else {
rightChannel[i] = leftChannel[i];
}
}
} else if (bitsPerSample === 32 && audioFormat === 3) {
// 32-bit float
for (let i = 0; i < numSamples; i++) {
leftChannel[i] = view.getFloat32(audioDataOffset, true);
audioDataOffset += 4;
if (numChannels > 1) {
rightChannel[i] = view.getFloat32(audioDataOffset, true);
audioDataOffset += 4;
} else {
rightChannel[i] = leftChannel[i];
}
}
} else {
throw new Error(`Unsupported WAV format: ${bitsPerSample}-bit, format ${audioFormat}`);
}
return { leftChannel, rightChannel };
}
dataOffset += 8 + dataChunkSize;
}
throw new Error('No data chunk found in WAV file');
}
offset += 8 + chunkSize;
}
throw new Error('No fmt chunk found in WAV file');
}
private buildCSD(
orchestra: string,
duration: number,
sampleRate: number,
parameters: CsoundParameter[],
outputFile: string
): string {
const paramInit = parameters
.map(p => `chnset ${p.value}, "${p.channelName}"`)
.join('\n');
return `<CsoundSynthesizer>
<CsOptions>
-W -d -m0 -o ${outputFile}
</CsOptions>
<CsInstruments>
sr = ${sampleRate}
ksmps = 64
nchnls = 2
0dbfs = 1.0
${paramInit}
${orchestra}
</CsInstruments>
<CsScore>
i 1 0 ${duration}
e
</CsScore>
</CsoundSynthesizer>`;
}
private findPeak(leftChannel: Float32Array, rightChannel: Float32Array): number {
let peak = 0;
for (let i = 0; i < leftChannel.length; i++) {
peak = Math.max(peak, Math.abs(leftChannel[i]), Math.abs(rightChannel[i]));
}
return peak;
}
private applyGain(
leftChannel: Float32Array,
rightChannel: Float32Array,
gain: number
): void {
for (let i = 0; i < leftChannel.length; i++) {
leftChannel[i] *= gain;
rightChannel[i] *= gain;
}
}
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;
const fadeSamples = Math.floor((fadeInMs / 1000) * sampleRate);
const actualFadeSamples = Math.min(fadeSamples, leftChannel.length);
for (let i = 0; i < actualFadeSamples; i++) {
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);
}
protected randomInt(min: number, max: number): number {
return Math.floor(this.randomRange(min, max + 1));
}
protected randomChoice<U>(choices: readonly U[]): U {
return choices[Math.floor(Math.random() * choices.length)];
}
protected mutateValue(value: number, amount: number, min: number, max: number): number {
const variation = value * amount * (Math.random() * 2 - 1);
return Math.max(min, Math.min(max, value + variation));
}
}

View File

@ -6,6 +6,17 @@
export type EngineType = 'generative' | 'sample' | 'input';
export type EngineCategory =
| 'Additive'
| 'Subtractive'
| 'FM'
| 'Percussion'
| 'Noise'
| 'Physical'
| 'Modulation'
| 'Experimental'
| 'Utility';
export interface PitchLock {
enabled: boolean;
frequency: number; // Frequency in Hz
@ -15,7 +26,8 @@ export interface SynthEngine<T = any> {
getName(): string;
getDescription(): string;
getType(): EngineType;
generate(params: T, sampleRate: number, duration: number, pitchLock?: PitchLock): [Float32Array, Float32Array];
getCategory(): EngineCategory;
generate(params: T, sampleRate: number, duration: number, pitchLock?: PitchLock): [Float32Array, Float32Array] | Promise<[Float32Array, Float32Array]>;
randomParams(pitchLock?: PitchLock): T;
mutateParams(params: T, mutationAmount?: number, pitchLock?: PitchLock): T;
}

View File

@ -1,6 +1,8 @@
import type { SynthEngine } from './SynthEngine';
import type { SynthEngine } from './base/SynthEngine';
import { FourOpFM } from './FourOpFM';
import { TwoOpFM } from './TwoOpFM';
import { PhaseDistortionFM } from './PhaseDistortionFM';
import { FormantFM } from './FormantFM';
import { DubSiren } from './DubSiren';
import { Benjolin } from './Benjolin';
import { ZzfxEngine } from './ZzfxEngine';
@ -10,17 +12,57 @@ import { Sample } from './Sample';
import { Input } from './Input';
import { KarplusStrong } from './KarplusStrong';
import { AdditiveEngine } from './AdditiveEngine';
import { Snare } from './Snare';
import { BassDrum } from './BassDrum';
import { HiHat } from './HiHat';
import { ParticleNoise } from './ParticleNoise';
import { DustNoise } from './DustNoise';
import { SubtractiveThreeOsc } from './SubtractiveThreeOsc';
import { MassiveAdditive } from './MassiveAdditive';
import { FormantPopDrum } from './FormantPopDrum';
import { TechnoKick } from './TechnoKick';
import { FMTomTom } from './FMTomTom';
import { RingCymbal } from './RingCymbal';
import { AdditiveBass } from './AdditiveBass';
import { FeedbackSnare } from './FeedbackSnare';
import { CombResonator } from './CombResonator';
import { LaserSweep } from './LaserSweep';
import { Dripwater } from './Dripwater';
import { Sub8 } from './Sub8';
import { Form1 } from './Form1';
import { Squine1 } from './Squine1';
export const engines: SynthEngine[] = [
new Sample(),
new Input(),
new FourOpFM(),
new TwoOpFM(),
new PhaseDistortionFM(),
new FormantFM(),
new DubSiren(),
new Benjolin(),
new ZzfxEngine(),
new NoiseDrum(),
new Snare(),
new BassDrum(),
new HiHat(),
new FormantPopDrum(),
new TechnoKick(),
new FMTomTom(),
new RingCymbal(),
new AdditiveBass(),
new FeedbackSnare(),
new Ring(),
new KarplusStrong(),
new AdditiveEngine(),
new ParticleNoise(),
new DustNoise(),
new SubtractiveThreeOsc(),
new CombResonator(),
new MassiveAdditive(),
new LaserSweep(),
new Dripwater(),
new Sub8(),
new Form1(),
new Squine1(),
];

View File

@ -0,0 +1,449 @@
// Phase Distortion FM AudioWorklet Processor
// 3-operator FM synthesis with phase distortion and DCW envelopes
const PDWaveform = {
Sawtooth: 0,
Square: 1,
Resonant1: 2,
Resonant2: 3,
Resonant3: 4,
};
const PDAlgorithm = {
Stack: 0, // 1→2→3
Split: 1, // 1→(2+3)
Ring: 2, // (1×2)→3
Parallel: 3, // 1+2+3
HarmonicStack: 4 // 1→2, 1→3
};
const LFOWaveform = {
Sine: 0,
Triangle: 1,
Square: 2,
Saw: 3,
SampleHold: 4,
RandomWalk: 5,
};
class PhaseDistortionFMProcessor extends AudioWorkletProcessor {
constructor() {
super();
this.opPhasesL = [0, 0, 0];
this.opPhasesR = [0, 0, 0];
this.lfoPhaseL = 0;
this.lfoPhaseR = 0;
this.lfoSampleHoldValue = 0;
this.lfoSampleHoldPhase = 0;
this.lfoRandomWalkCurrent = 0;
this.lfoRandomWalkTarget = 0;
this.dcBlockerL = 0;
this.dcBlockerR = 0;
this.dcBlockerCutoff = 0.995;
this.sampleCount = 0;
this.totalSamples = 0;
this.port.onmessage = (e) => {
if (e.data.type === 'init') {
this.params = e.data.params;
this.duration = e.data.duration;
this.totalSamples = Math.floor(sampleRate * this.duration);
this.lfoSampleHoldValue = Math.random() * 2 - 1;
this.lfoSampleHoldPhase = 0;
this.lfoRandomWalkCurrent = Math.random() * 2 - 1;
this.lfoRandomWalkTarget = Math.random() * 2 - 1;
const stereoOffset = this.params.stereoWidth * 0.1;
this.opPhasesL = [0, Math.PI * stereoOffset, 0];
this.opPhasesR = [0, Math.PI * stereoOffset * 1.5, 0];
this.lfoPhaseL = 0;
this.lfoPhaseR = Math.PI * this.params.stereoWidth * 0.3;
}
};
}
process(inputs, outputs, parameters) {
if (!this.params || this.sampleCount >= this.totalSamples) {
return false;
}
const output = outputs[0];
const leftChannel = output[0];
const rightChannel = output[1];
const TAU = Math.PI * 2;
const detune = 1 + (this.params.stereoWidth * 0.001);
const leftFreq = this.params.baseFreq / detune;
const rightFreq = this.params.baseFreq * detune;
for (let i = 0; i < leftChannel.length; i++) {
if (this.sampleCount >= this.totalSamples) {
leftChannel[i] = 0;
rightChannel[i] = 0;
continue;
}
const t = this.sampleCount / sampleRate;
const env1 = this.calculateEnvelope(t, this.params.operators[0]);
const env2 = this.calculateEnvelope(t, this.params.operators[1]);
const env3 = this.calculateEnvelope(t, this.params.operators[2]);
const dcw1 = this.calculateDCWEnvelope(t, this.params.operators[0]);
const dcw2 = this.calculateDCWEnvelope(t, this.params.operators[1]);
const dcw3 = this.calculateDCWEnvelope(t, this.params.operators[2]);
const lfoL = this.generateLFO(this.lfoPhaseL, this.params.lfo.waveform, this.params.lfo.rate);
const lfoR = this.generateLFO(this.lfoPhaseR, this.params.lfo.waveform, this.params.lfo.rate);
const lfoModL = lfoL * this.params.lfo.depth;
const lfoModR = lfoR * this.params.lfo.depth;
let pitchModL = 0, pitchModR = 0;
let ampModL = 1, ampModR = 1;
let modIndexMod = 0;
if (this.params.lfo.target === 'pitch') {
pitchModL = lfoModL * 0.02;
pitchModR = lfoModR * 0.02;
} else if (this.params.lfo.target === 'amplitude') {
ampModL = 1 + lfoModL * 0.5;
ampModR = 1 + lfoModR * 0.5;
} else {
modIndexMod = lfoModL;
}
const [sampleL, sampleR] = this.processAlgorithm(
this.params.algorithm,
this.opPhasesL,
this.opPhasesR,
[env1, env2, env3],
[dcw1, dcw2, dcw3],
modIndexMod
);
const gainCompensation = this.getAlgorithmGainCompensation(this.params.algorithm);
let outL = sampleL * gainCompensation * ampModL;
let outR = sampleR * gainCompensation * ampModR;
outL = this.softClip(outL);
outR = this.softClip(outR);
const dcFilteredL = outL - this.dcBlockerL;
this.dcBlockerL += (1 - this.dcBlockerCutoff) * dcFilteredL;
const dcFilteredR = outR - this.dcBlockerR;
this.dcBlockerR += (1 - this.dcBlockerCutoff) * dcFilteredR;
leftChannel[i] = dcFilteredL * 0.85;
rightChannel[i] = dcFilteredR * 0.85;
for (let op = 0; op < 3; op++) {
const opFreqL = leftFreq * this.params.operators[op].ratio * (1 + pitchModL);
const opFreqR = rightFreq * this.params.operators[op].ratio * (1 + pitchModR);
this.opPhasesL[op] += (TAU * opFreqL) / sampleRate;
this.opPhasesR[op] += (TAU * opFreqR) / sampleRate;
if (this.opPhasesL[op] > TAU * 1000) this.opPhasesL[op] -= TAU * 1000;
if (this.opPhasesR[op] > TAU * 1000) this.opPhasesR[op] -= TAU * 1000;
}
this.lfoPhaseL += (TAU * this.params.lfo.rate) / sampleRate;
this.lfoPhaseR += (TAU * this.params.lfo.rate) / sampleRate;
this.sampleCount++;
}
return true;
}
processAlgorithm(algorithm, phasesL, phasesR, envelopes, dcwEnvs, modIndexMod) {
const baseModIndex = 3.0;
const modScale = baseModIndex * (1 + modIndexMod * 2);
const ops = this.params.operators;
switch (algorithm) {
case PDAlgorithm.Stack: {
// 1→2→3
const mod1L = this.generateWaveform(phasesL[0], ops[0].waveform, ops[0].distortion, dcwEnvs[0])
* envelopes[0] * ops[0].level;
const mod1R = this.generateWaveform(phasesR[0], ops[0].waveform, ops[0].distortion, dcwEnvs[0])
* envelopes[0] * ops[0].level;
const mod2PhaseL = phasesL[1] + modScale * mod1L;
const mod2PhaseR = phasesR[1] + modScale * mod1R;
const mod2L = this.generateWaveform(mod2PhaseL, ops[1].waveform, ops[1].distortion, dcwEnvs[1])
* envelopes[1] * ops[1].level;
const mod2R = this.generateWaveform(mod2PhaseR, ops[1].waveform, ops[1].distortion, dcwEnvs[1])
* envelopes[1] * ops[1].level;
const outPhaseL = phasesL[2] + modScale * 0.7 * mod2L;
const outPhaseR = phasesR[2] + modScale * 0.7 * mod2R;
const outL = this.generateWaveform(outPhaseL, ops[2].waveform, ops[2].distortion, dcwEnvs[2])
* envelopes[2] * ops[2].level;
const outR = this.generateWaveform(outPhaseR, ops[2].waveform, ops[2].distortion, dcwEnvs[2])
* envelopes[2] * ops[2].level;
return [outL, outR];
}
case PDAlgorithm.Split: {
// 1→(2+3)
const mod1L = this.generateWaveform(phasesL[0], ops[0].waveform, ops[0].distortion, dcwEnvs[0])
* envelopes[0] * ops[0].level;
const mod1R = this.generateWaveform(phasesR[0], ops[0].waveform, ops[0].distortion, dcwEnvs[0])
* envelopes[0] * ops[0].level;
const car1PhaseL = phasesL[1] + modScale * mod1L;
const car1PhaseR = phasesR[1] + modScale * mod1R;
const car1L = this.generateWaveform(car1PhaseL, ops[1].waveform, ops[1].distortion, dcwEnvs[1])
* envelopes[1] * ops[1].level;
const car1R = this.generateWaveform(car1PhaseR, ops[1].waveform, ops[1].distortion, dcwEnvs[1])
* envelopes[1] * ops[1].level;
const car2PhaseL = phasesL[2] + modScale * mod1L;
const car2PhaseR = phasesR[2] + modScale * mod1R;
const car2L = this.generateWaveform(car2PhaseL, ops[2].waveform, ops[2].distortion, dcwEnvs[2])
* envelopes[2] * ops[2].level;
const car2R = this.generateWaveform(car2PhaseR, ops[2].waveform, ops[2].distortion, dcwEnvs[2])
* envelopes[2] * ops[2].level;
return [(car1L + car2L) * 0.707, (car1R + car2R) * 0.707];
}
case PDAlgorithm.Ring: {
// (1×2)→3
const osc1L = this.generateWaveform(phasesL[0], ops[0].waveform, ops[0].distortion, dcwEnvs[0])
* envelopes[0] * ops[0].level;
const osc1R = this.generateWaveform(phasesR[0], ops[0].waveform, ops[0].distortion, dcwEnvs[0])
* envelopes[0] * ops[0].level;
const osc2L = this.generateWaveform(phasesL[1], ops[1].waveform, ops[1].distortion, dcwEnvs[1])
* envelopes[1] * ops[1].level;
const osc2R = this.generateWaveform(phasesR[1], ops[1].waveform, ops[1].distortion, dcwEnvs[1])
* envelopes[1] * ops[1].level;
const ringModL = osc1L * osc2L * this.params.ringModAmount;
const ringModR = osc1R * osc2R * this.params.ringModAmount;
const carPhaseL = phasesL[2] + modScale * ringModL;
const carPhaseR = phasesR[2] + modScale * ringModR;
const outL = this.generateWaveform(carPhaseL, ops[2].waveform, ops[2].distortion, dcwEnvs[2])
* envelopes[2] * ops[2].level;
const outR = this.generateWaveform(carPhaseR, ops[2].waveform, ops[2].distortion, dcwEnvs[2])
* envelopes[2] * ops[2].level;
return [outL, outR];
}
case PDAlgorithm.Parallel: {
// 1+2+3
let sumL = 0, sumR = 0;
for (let i = 0; i < 3; i++) {
sumL += this.generateWaveform(phasesL[i], ops[i].waveform, ops[i].distortion, dcwEnvs[i])
* envelopes[i] * ops[i].level;
sumR += this.generateWaveform(phasesR[i], ops[i].waveform, ops[i].distortion, dcwEnvs[i])
* envelopes[i] * ops[i].level;
}
return [sumL * 0.577, sumR * 0.577];
}
case PDAlgorithm.HarmonicStack: {
// 1→2, 1→3
const mod1L = this.generateWaveform(phasesL[0], ops[0].waveform, ops[0].distortion, dcwEnvs[0])
* envelopes[0] * ops[0].level;
const mod1R = this.generateWaveform(phasesR[0], ops[0].waveform, ops[0].distortion, dcwEnvs[0])
* envelopes[0] * ops[0].level;
const car1PhaseL = phasesL[1] + modScale * mod1L;
const car1PhaseR = phasesR[1] + modScale * mod1R;
const car1L = this.generateWaveform(car1PhaseL, ops[1].waveform, ops[1].distortion, dcwEnvs[1])
* envelopes[1] * ops[1].level;
const car1R = this.generateWaveform(car1PhaseR, ops[1].waveform, ops[1].distortion, dcwEnvs[1])
* envelopes[1] * ops[1].level;
const car2PhaseL = phasesL[2] + modScale * 0.8 * mod1L;
const car2PhaseR = phasesR[2] + modScale * 0.8 * mod1R;
const car2L = this.generateWaveform(car2PhaseL, ops[2].waveform, ops[2].distortion, dcwEnvs[2])
* envelopes[2] * ops[2].level;
const car2R = this.generateWaveform(car2PhaseR, ops[2].waveform, ops[2].distortion, dcwEnvs[2])
* envelopes[2] * ops[2].level;
return [(car1L + car2L) * 0.707, (car1R + car2R) * 0.707];
}
default:
return [0, 0];
}
}
generateWaveform(phase, waveform, distortion, dcw) {
const normalizedPhase = (phase % (Math.PI * 2)) / (Math.PI * 2);
const warpedPhase = this.applyPhaseDistortion(normalizedPhase, distortion, dcw);
switch (waveform) {
case PDWaveform.Sawtooth:
return warpedPhase * 2 - 1;
case PDWaveform.Square:
return warpedPhase < 0.5 ? 1 : -1;
case PDWaveform.Resonant1:
return this.resonantSaw(warpedPhase, dcw);
case PDWaveform.Resonant2:
return this.resonantPulse(warpedPhase, dcw);
case PDWaveform.Resonant3:
return this.doubleResonant(warpedPhase, dcw);
default:
return warpedPhase * 2 - 1;
}
}
applyPhaseDistortion(phase, distortion, dcw) {
const warpAmount = distortion * dcw;
if (phase < 0.5) {
return phase * (1 + warpAmount * (1 - 2 * phase));
} else {
return 0.5 + (phase - 0.5) * (1 + warpAmount * (2 * phase - 1));
}
}
resonantSaw(phase, brightness) {
const harmonics = Math.floor(1 + brightness * 8);
let sum = 0;
for (let n = 1; n <= harmonics; n++) {
sum += Math.sin(Math.PI * 2 * phase * n) / n;
}
return sum * 0.5;
}
resonantPulse(phase, brightness) {
const pulseWidth = 0.5 - brightness * 0.3;
const harmonics = Math.floor(1 + brightness * 6);
let sum = 0;
for (let n = 1; n <= harmonics; n += 2) {
sum += Math.sin(Math.PI * 2 * phase * n) * Math.cos(Math.PI * n * pulseWidth) / n;
}
return sum * 0.6;
}
doubleResonant(phase, brightness) {
const harm1 = this.resonantSaw(phase, brightness);
const harm2 = this.resonantSaw((phase + 0.5) % 1, brightness * 0.7);
return (harm1 + harm2 * 0.5) * 0.6;
}
calculateEnvelope(t, op) {
const attackTime = op.attack * this.duration;
const decayTime = op.decay * this.duration;
const releaseTime = op.release * this.duration;
const sustainStart = attackTime + decayTime;
const releaseStart = this.duration - releaseTime;
if (t < attackTime) {
return t / attackTime;
} else if (t < sustainStart) {
const progress = (t - attackTime) / decayTime;
return 1 - progress * (1 - op.sustain);
} else if (t < releaseStart) {
return op.sustain;
} else {
const progress = (t - releaseStart) / releaseTime;
return op.sustain * (1 - progress);
}
}
calculateDCWEnvelope(t, op) {
const attackTime = op.dcwAttack * this.duration;
const decayTime = op.dcwDecay * this.duration;
const releaseTime = op.dcwRelease * this.duration;
const sustainStart = attackTime + decayTime;
const releaseStart = this.duration - releaseTime;
if (t < attackTime) {
return t / attackTime;
} else if (t < sustainStart) {
const progress = (t - attackTime) / decayTime;
return 1 - progress * (1 - op.dcwSustain);
} else if (t < releaseStart) {
return op.dcwSustain;
} else {
const progress = (t - releaseStart) / releaseTime;
return op.dcwSustain * (1 - progress);
}
}
generateLFO(phase, waveform, rate) {
const normalizedPhase = (phase % (Math.PI * 2)) / (Math.PI * 2);
switch (waveform) {
case LFOWaveform.Sine:
return Math.sin(phase);
case LFOWaveform.Triangle:
return normalizedPhase < 0.5
? normalizedPhase * 4 - 1
: 3 - normalizedPhase * 4;
case LFOWaveform.Square:
return normalizedPhase < 0.5 ? 1 : -1;
case LFOWaveform.Saw:
return normalizedPhase * 2 - 1;
case LFOWaveform.SampleHold: {
const cyclesSinceLastHold = phase - this.lfoSampleHoldPhase;
if (cyclesSinceLastHold >= Math.PI * 2) {
this.lfoSampleHoldValue = Math.random() * 2 - 1;
this.lfoSampleHoldPhase = phase;
}
return this.lfoSampleHoldValue;
}
case LFOWaveform.RandomWalk: {
const interpolationSpeed = rate / sampleRate * 20;
const diff = this.lfoRandomWalkTarget - this.lfoRandomWalkCurrent;
this.lfoRandomWalkCurrent += diff * interpolationSpeed;
if (Math.abs(diff) < 0.01) {
this.lfoRandomWalkTarget = Math.random() * 2 - 1;
}
return this.lfoRandomWalkCurrent;
}
default:
return 0;
}
}
softClip(x) {
const absX = Math.abs(x);
if (absX < 0.7) return x;
if (absX > 3) return Math.sign(x) * 0.98;
const x2 = x * x;
return x * (27 + x2) / (27 + 9 * x2);
}
getAlgorithmGainCompensation(algorithm) {
switch (algorithm) {
case PDAlgorithm.Stack:
return 0.75;
case PDAlgorithm.Split:
case PDAlgorithm.HarmonicStack:
return 0.8;
case PDAlgorithm.Ring:
return 0.7;
case PDAlgorithm.Parallel:
return 0.65;
default:
return 0.75;
}
}
}
registerProcessor('phase-distortion-fm-processor', PhaseDistortionFMProcessor);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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];
}
}

View 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];
}
}

View File

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

View 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];
}
}

View 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];
}
}

View 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];
}
}

View 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];
}
}

View 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];
}
}

View 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];
}
}

View File

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

View File

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

View File

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

View File

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

View 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];
}
}

View 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];
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,209 @@
import type { AudioProcessor, ProcessorCategory } from './AudioProcessor';
export class Resonator implements AudioProcessor {
private readonly sampleRate = 44100;
getName(): string {
return 'Resonator';
}
getDescription(): string {
return 'Multi-band resonant filter bank that adds tonal character through resonance';
}
getCategory(): ProcessorCategory {
return 'Filter';
}
process(
leftChannel: Float32Array,
rightChannel: Float32Array
): [Float32Array, Float32Array] {
const length = leftChannel.length;
const numResonators = Math.floor(Math.random() * 3) + 2; // 2-4 resonators
const baseFreq = Math.random() * 200 + 100; // 100-300 Hz base frequency
const spread = Math.random() * 0.6 + 0.4; // 0.4-1.0 harmonic spread
const resonance = Math.random() * 8 + 4; // Q factor 4-12
const mix = Math.random() * 0.6 + 0.3; // 30-90% wet
const stereoSpread = Math.random() * 0.2; // 0-20% stereo detuning
const modulationRate = Math.random() * 0.8 + 0.1; // 0.1-0.9 Hz modulation
const modulationDepth = Math.random() * 0.3 + 0.1; // 10-40% pitch modulation
const drive = Math.random() * 0.5; // 0-50% input drive
const leftOut = new Float32Array(length);
const rightOut = new Float32Array(length);
const leftResonators: Array<{
freq: number;
state1: number;
state2: number;
}> = [];
const rightResonators: Array<{
freq: number;
state1: number;
state2: number;
}> = [];
// Create resonator banks with harmonic or inharmonic relationships
const isHarmonic = Math.random() < 0.6;
for (let i = 0; i < numResonators; i++) {
let freqMultiplier: number;
if (isHarmonic) {
// Harmonic series
freqMultiplier = Math.pow(2, i * spread);
} else {
// Inharmonic/stretched partials
freqMultiplier = Math.pow(2, i * spread * (1 + Math.random() * 0.4));
}
const leftFreq = baseFreq * freqMultiplier;
const rightFreq = leftFreq * (1 + (Math.random() - 0.5) * stereoSpread);
leftResonators.push({
freq: leftFreq,
state1: 0,
state2: 0
});
rightResonators.push({
freq: rightFreq,
state1: 0,
state2: 0
});
}
for (let i = 0; i < length; i++) {
const t = i / this.sampleRate;
// LFO for frequency modulation
const lfo = Math.sin(2 * Math.PI * modulationRate * t);
// Apply input drive
let leftInput = leftChannel[i];
let rightInput = rightChannel[i];
if (drive > 0.1) {
const driveAmount = 1 + drive * 2;
leftInput = this.softSaturation(leftInput * driveAmount);
rightInput = this.softSaturation(rightInput * driveAmount);
}
// Process through all resonators
let leftResonant = 0;
let rightResonant = 0;
for (let r = 0; r < numResonators; r++) {
const leftRes = leftResonators[r];
const rightRes = rightResonators[r];
// Modulate frequency
const freqMod = 1 + lfo * modulationDepth;
const leftModFreq = Math.min(leftRes.freq * freqMod, this.sampleRate * 0.45);
const rightModFreq = Math.min(rightRes.freq * freqMod, this.sampleRate * 0.45);
// Apply resonant filter
const leftFiltered = this.stateVariableFilter(
leftInput,
leftModFreq,
resonance,
leftRes.state1,
leftRes.state2
);
leftRes.state1 = leftFiltered.state1;
leftRes.state2 = leftFiltered.state2;
const rightFiltered = this.stateVariableFilter(
rightInput,
rightModFreq,
resonance,
rightRes.state1,
rightRes.state2
);
rightRes.state1 = rightFiltered.state1;
rightRes.state2 = rightFiltered.state2;
// Amplitude compensation for number of resonators
const ampScale = 1 / Math.sqrt(numResonators);
leftResonant += leftFiltered.output * ampScale;
rightResonant += rightFiltered.output * ampScale;
}
// Mix dry and wet signals
const dryGain = Math.sqrt(1 - mix);
const wetGain = Math.sqrt(mix);
leftOut[i] = leftChannel[i] * dryGain + leftResonant * wetGain;
rightOut[i] = rightChannel[i] * dryGain + rightResonant * wetGain;
// Soft clipping
leftOut[i] = this.softClip(leftOut[i]);
rightOut[i] = this.softClip(rightOut[i]);
}
this.normalizeOutput(leftOut, rightOut);
return [leftOut, rightOut];
}
private stateVariableFilter(
input: number,
cutoff: number,
resonance: number,
state1: number,
state2: number
): { output: number; state1: number; state2: number } {
const normalizedFreq = Math.min(cutoff / this.sampleRate, 0.48);
const f = 2 * Math.sin(Math.PI * normalizedFreq);
const q = Math.max(1 / Math.min(resonance, 20), 0.01);
const lowpass = state2 + f * state1;
const highpass = input - lowpass - q * state1;
const bandpass = f * highpass + state1;
// Clamp states to prevent instability
const newState1 = Math.max(-3, Math.min(3, Math.abs(bandpass) > 1e-10 ? bandpass : 0));
const newState2 = Math.max(-3, Math.min(3, Math.abs(lowpass) > 1e-10 ? lowpass : 0));
return {
output: bandpass,
state1: newState1,
state2: newState2
};
}
private softSaturation(x: number): number {
return x / (1 + Math.abs(x));
}
private softClip(sample: number): number {
const threshold = 0.95;
if (Math.abs(sample) < threshold) {
return sample;
}
const sign = sample < 0 ? -1 : 1;
const abs = Math.abs(sample);
return sign * (threshold + (1 - threshold) * Math.tanh((abs - threshold) / (1 - threshold)));
}
private normalizeOutput(leftOut: Float32Array, rightOut: Float32Array): void {
let maxPeak = 0;
for (let i = 0; i < leftOut.length; i++) {
maxPeak = Math.max(maxPeak, Math.abs(leftOut[i]), Math.abs(rightOut[i]));
}
if (maxPeak > 0.01) {
const targetPeak = 0.95;
const normalizeGain = Math.min(1.0, targetPeak / maxPeak);
for (let i = 0; i < leftOut.length; i++) {
leftOut[i] *= normalizeGain;
rightOut[i] *= normalizeGain;
}
}
}
}

View File

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

View File

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

View File

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

View 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];
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View 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];
}
}

View 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];
}
}

View 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];
}
}

View File

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

View File

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

View 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];
}
}

View File

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

View File

@ -24,6 +24,22 @@ import { RingModulator } from './RingModulator';
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(),
@ -51,6 +67,22 @@ const processors: AudioProcessor[] = [
new Waveshaper(),
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 {

View File

@ -5,6 +5,7 @@ export class AudioService {
private currentSource: AudioBufferSourceNode | null = null;
private gainNode: GainNode | null = null;
private startTime = 0;
private currentOffset = 0;
private isPlaying = false;
private onPlaybackUpdate: ((position: number) => void) | null = null;
private animationFrameId: number | null = null;
@ -48,7 +49,7 @@ export class AudioService {
return buffer;
}
play(buffer: AudioBuffer): void {
play(buffer: AudioBuffer, offset: number = 0): void {
this.stop();
const ctx = this.getContext();
@ -57,6 +58,7 @@ export class AudioService {
source.connect(this.gainNode!);
this.startTime = ctx.currentTime;
this.currentOffset = offset;
this.isPlaying = true;
this.currentSource = source;
@ -74,7 +76,7 @@ export class AudioService {
}
};
source.start();
source.start(0, offset);
this.updatePlaybackPosition();
}
@ -84,7 +86,7 @@ export class AudioService {
}
const elapsed = this.context.currentTime - this.startTime;
this.onPlaybackUpdate(elapsed);
this.onPlaybackUpdate(elapsed + this.currentOffset);
this.animationFrameId = requestAnimationFrame(() => this.updatePlaybackPosition());
}
@ -99,6 +101,7 @@ export class AudioService {
this.currentSource = null;
}
this.isPlaying = false;
this.currentOffset = 0;
if (this.animationFrameId !== null) {
cancelAnimationFrame(this.animationFrameId);
this.animationFrameId = null;

View File

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

View File

@ -5,16 +5,18 @@
buffer: AudioBuffer | null;
color?: string;
playbackPosition?: number;
cuePoint?: number;
selectionStart?: number | null;
selectionEnd?: number | null;
onselectionchange?: (start: number | null, end: number | null) => void;
onclick?: () => void;
onclick?: (timeOffset: number) => void;
}
let {
buffer,
color = '#646cff',
playbackPosition = 0,
cuePoint = 0,
selectionStart = null,
selectionEnd = null,
onselectionchange,
@ -43,6 +45,7 @@
buffer;
color;
playbackPosition;
cuePoint;
selectionStart;
selectionEnd;
draw();
@ -75,8 +78,9 @@
}
}
if (onclick) {
onclick();
if (onclick && buffer) {
const timeOffset = (x / canvas.width) * (buffer.length / buffer.sampleRate);
onclick(timeOffset);
}
}
@ -250,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();

View File

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

View File

@ -0,0 +1,64 @@
export interface AudioState {
leftChannel: Float32Array;
rightChannel: Float32Array;
params: any;
isProcessed: boolean;
waveformColor: string;
engineIndex: number;
}
export class UndoManager {
private undoStack: AudioState[] = [];
private readonly maxHistorySize: number;
constructor(maxHistorySize: number = 20) {
this.maxHistorySize = maxHistorySize;
}
pushState(state: AudioState): void {
this.undoStack.push({
leftChannel: state.leftChannel.slice(),
rightChannel: state.rightChannel.slice(),
params: state.params,
isProcessed: state.isProcessed,
waveformColor: state.waveformColor,
engineIndex: state.engineIndex,
});
if (this.undoStack.length > this.maxHistorySize) {
this.undoStack.shift();
}
}
undo(): AudioState | null {
const previousState = this.undoStack.pop();
return previousState || null;
}
canUndo(): boolean {
return this.undoStack.length > 0;
}
clear(): void {
this.undoStack = [];
}
static captureState(
buffer: AudioBuffer | null,
params: any,
isProcessed: boolean,
waveformColor: string,
engineIndex: number
): AudioState | null {
if (!buffer) return null;
return {
leftChannel: buffer.getChannelData(0).slice(),
rightChannel: buffer.getChannelData(1).slice(),
params,
isProcessed,
waveformColor,
engineIndex,
};
}
}

View File

@ -8,6 +8,8 @@ export interface KeyboardActions {
onVolumeDecrease?: (large: boolean) => void;
onVolumeIncrease?: (large: boolean) => void;
onEscape?: () => void;
onUndo?: () => void;
onPlayFromStart?: () => void;
}
export function createKeyboardHandler(actions: KeyboardActions) {
@ -18,6 +20,9 @@ export function createKeyboardHandler(actions: KeyboardActions) {
const isLargeAdjustment = event.shiftKey;
switch (key) {
case 'z':
actions.onUndo?.();
break;
case 'm':
actions.onMutate?.();
break;
@ -27,9 +32,13 @@ export function createKeyboardHandler(actions: KeyboardActions) {
case 'p':
actions.onProcess?.();
break;
case 's':
case 'd':
actions.onDownload?.();
break;
case ' ':
event.preventDefault();
actions.onPlayFromStart?.();
break;
case 'arrowleft':
event.preventDefault();
actions.onDurationDecrease?.(isLargeAdjustment);

View File

@ -8,6 +8,8 @@ const STORAGE_KEYS = {
DURATION: 'duration',
PITCH_LOCK_ENABLED: 'pitchLockEnabled',
PITCH_LOCK_FREQUENCY: 'pitchLockFrequency',
EXPANDED_CATEGORIES: 'expandedCategories',
SELECTED_PROCESSOR_CATEGORY: 'selectedProcessorCategory',
} as const;
export function loadVolume(): number {
@ -45,3 +47,29 @@ export function loadPitchLockFrequency(): number {
export function savePitchLockFrequency(frequency: number): void {
localStorage.setItem(STORAGE_KEYS.PITCH_LOCK_FREQUENCY, frequency.toString());
}
export function loadExpandedCategories(): Set<string> {
const stored = localStorage.getItem(STORAGE_KEYS.EXPANDED_CATEGORIES);
if (stored) {
try {
const parsed = JSON.parse(stored);
return new Set(Array.isArray(parsed) ? parsed : []);
} catch {
return new Set();
}
}
return new Set();
}
export function saveExpandedCategories(categories: Set<string>): void {
localStorage.setItem(STORAGE_KEYS.EXPANDED_CATEGORIES, JSON.stringify(Array.from(categories)));
}
export function loadSelectedProcessorCategory(): string {
const stored = localStorage.getItem(STORAGE_KEYS.SELECTED_PROCESSOR_CATEGORY);
return stored || 'All';
}
export function saveSelectedProcessorCategory(category: string): void {
localStorage.setItem(STORAGE_KEYS.SELECTED_PROCESSOR_CATEGORY, category);
}