Compare commits
14 Commits
c1f7cc02fd
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 3cadc23238 | |||
| aba4abb054 | |||
| dfb57a082f | |||
| 6116745795 | |||
| 65a1e16781 | |||
| b700c68b4d | |||
| fb92c3ae2a | |||
| 38479f0253 | |||
| 580aa4b96f | |||
| 467558efd2 | |||
| 51e7c44c93 | |||
| 179c52facc | |||
| 4df063f9b3 | |||
| cb730237f5 |
71
README.md
71
README.md
@ -1,4 +1,4 @@
|
||||
# poof
|
||||
# rsgp
|
||||
|
||||
Audio synthesis web application for generating random sound samples. Users generate sounds via different synthesis engines, mutate parameters, apply audio processors in multiple passes, and export as WAV. Built for musicians seeking unexpected textures and one-shots.
|
||||
|
||||
@ -39,8 +39,8 @@ pnpm build
|
||||
## Docker
|
||||
|
||||
```sh
|
||||
docker build -t poof .
|
||||
docker run -p 8080:80 poof
|
||||
docker build -t rsgp .
|
||||
docker run -p 8080:80 rsgp
|
||||
```
|
||||
|
||||
Opens on http://localhost:8080
|
||||
@ -55,6 +55,69 @@ Opens on http://localhost:8080
|
||||
4. Keep all DSP code, helpers, and types in the same file
|
||||
5. Register in `src/lib/audio/engines/registry.ts`
|
||||
|
||||
### Adding CSound-Based Synthesis Engines
|
||||
|
||||
For complex DSP algorithms, you can leverage CSound's powerful audio language:
|
||||
|
||||
1. Create a single file in `src/lib/audio/engines/` extending the `CsoundEngine<ParamsType>` abstract class
|
||||
2. Define a TypeScript interface for your parameters
|
||||
3. Implement required methods:
|
||||
- `getName()`: Engine display name
|
||||
- `getDescription()`: Brief description
|
||||
- `getType()`: Return `'generative'`, `'sample'`, or `'input'`
|
||||
- `getOrchestra()`: Return CSound orchestra code as a string
|
||||
- `getParametersForCsound(params)`: Map TypeScript params to CSound channel parameters
|
||||
- `randomParams(pitchLock?)`: Generate random parameter values
|
||||
- `mutateParams(params, mutationAmount?, pitchLock?)`: Mutate existing parameters
|
||||
4. Keep all enums, interfaces, and helper logic in the same file
|
||||
5. Register in `src/lib/audio/engines/registry.ts`
|
||||
|
||||
**CSound Orchestra Guidelines:**
|
||||
- Use `instr 1` as your main instrument
|
||||
- Read parameters via `chnget "paramName"`
|
||||
- Duration is available as `p3`
|
||||
- Time-based parameters (attack, decay, release) should be ratios (0-1) scaled by `p3`
|
||||
- Output stereo audio with `outs aLeft, aRight`
|
||||
- The base class handles WAV parsing, normalization, and fade-in
|
||||
|
||||
**Example:**
|
||||
```typescript
|
||||
import { CsoundEngine, type CsoundParameter } from './base/CsoundEngine';
|
||||
|
||||
interface MyParams {
|
||||
frequency: number;
|
||||
resonance: number;
|
||||
}
|
||||
|
||||
export class MyEngine extends CsoundEngine<MyParams> {
|
||||
getName() { return 'My Engine'; }
|
||||
getDescription() { return 'Description'; }
|
||||
getType() { return 'generative' as const; }
|
||||
|
||||
protected getOrchestra(): string {
|
||||
return `
|
||||
instr 1
|
||||
iFreq chnget "frequency"
|
||||
iRes chnget "resonance"
|
||||
aNoise noise 1, 0
|
||||
aOut butterbp aNoise, iFreq, iRes
|
||||
outs aOut, aOut
|
||||
endin
|
||||
`;
|
||||
}
|
||||
|
||||
protected getParametersForCsound(params: MyParams): CsoundParameter[] {
|
||||
return [
|
||||
{ channelName: 'frequency', value: params.frequency },
|
||||
{ channelName: 'resonance', value: params.resonance }
|
||||
];
|
||||
}
|
||||
|
||||
randomParams() { /* ... */ }
|
||||
mutateParams(params, amount = 0.15) { /* ... */ }
|
||||
}
|
||||
```
|
||||
|
||||
### Adding Audio Processors
|
||||
|
||||
1. Create a single file in `src/lib/audio/processors/` implementing the `AudioProcessor` interface
|
||||
@ -67,3 +130,5 @@ Opens on http://localhost:8080
|
||||
|
||||
- Wavetables from [Adventure Kid Waveforms](https://www.adventurekid.se/akrt/waveforms/adventure-kid-waveforms/) by Kristoffer Ekstrand
|
||||
- [Garten Salat](https://garten.salat.dev/) by Felix Roos for drum synthesis inspiration.
|
||||
- [Csound](https://csound.com/) for powerful audio synthesis capabilities.
|
||||
- Steven Yi for some synths: [csound-live-code](https://github.com/kunstmusik/csound-live-code).
|
||||
|
||||
@ -4,9 +4,9 @@
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="description" content="Poof: a sample generator" />
|
||||
<meta name="description" content="RSGP: Random Sample Generator and Processor" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<title>Poof</title>
|
||||
<title>RSGP</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "poof",
|
||||
"name": "rsgp",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
@ -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
383
pnpm-lock.yaml
generated
@ -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
BIN
public/tutorial.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 972 KiB |
867
src/App.svelte
867
src/App.svelte
File diff suppressed because it is too large
Load Diff
228
src/lib/audio/engines/AdditiveBass.ts
Normal file
228
src/lib/audio/engines/AdditiveBass.ts
Normal file
@ -0,0 +1,228 @@
|
||||
import { CsoundEngine, type CsoundParameter } from './base/CsoundEngine';
|
||||
import type { PitchLock } from './base/SynthEngine';
|
||||
|
||||
interface AdditiveBassParams {
|
||||
baseFreq: number;
|
||||
pitchSweep: number;
|
||||
pitchDecay: number;
|
||||
overtoneAmp: number;
|
||||
overtoneFreqMult: number;
|
||||
noiseAmp: number;
|
||||
noiseDecay: number;
|
||||
filterResonance: number;
|
||||
filterCutoff: number;
|
||||
attack: number;
|
||||
decay: number;
|
||||
waveshape: number;
|
||||
bodyResonance: number;
|
||||
click: number;
|
||||
harmonicSpread: number;
|
||||
}
|
||||
|
||||
export class AdditiveBass extends CsoundEngine<AdditiveBassParams> {
|
||||
getName(): string {
|
||||
return 'Additive Bass';
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return 'Deep bass drum using additive synthesis with pink noise and waveshaping';
|
||||
}
|
||||
|
||||
getType() {
|
||||
return 'generative' as const;
|
||||
}
|
||||
|
||||
getCategory() {
|
||||
return 'Percussion' as const;
|
||||
}
|
||||
|
||||
protected getOrchestra(): string {
|
||||
return `
|
||||
instr 1
|
||||
iBaseFreq chnget "baseFreq"
|
||||
iPitchSweep chnget "pitchSweep"
|
||||
iPitchDecay chnget "pitchDecay"
|
||||
iOvertoneAmp chnget "overtoneAmp"
|
||||
iOvertoneFreqMult chnget "overtoneFreqMult"
|
||||
iNoiseAmp chnget "noiseAmp"
|
||||
iNoiseDecay chnget "noiseDecay"
|
||||
iFilterResonance chnget "filterResonance"
|
||||
iFilterCutoff chnget "filterCutoff"
|
||||
iAttack chnget "attack"
|
||||
iDecay chnget "decay"
|
||||
iWaveshape chnget "waveshape"
|
||||
iBodyResonance chnget "bodyResonance"
|
||||
iClick chnget "click"
|
||||
iHarmonicSpread chnget "harmonicSpread"
|
||||
|
||||
idur = p3
|
||||
iAttackTime = iAttack * idur
|
||||
iDecayTime = iDecay * idur
|
||||
iPitchDecayTime = iPitchDecay * idur
|
||||
iNoiseDecayTime = iNoiseDecay * idur
|
||||
|
||||
; Pitch envelope: exponential sweep from high to low
|
||||
kPitchEnv expseg iBaseFreq * (1 + iPitchSweep * 3), iPitchDecayTime, iBaseFreq, idur - iPitchDecayTime, iBaseFreq * 0.95
|
||||
|
||||
; Main amplitude envelope with attack and decay
|
||||
kAmpEnv linseg 0, iAttackTime, 1, iDecayTime, 0.001, 0.001, 0
|
||||
kAmpEnv = kAmpEnv * kAmpEnv
|
||||
|
||||
; Generate fundamental sine wave
|
||||
aFund oscili 0.7, kPitchEnv
|
||||
|
||||
; Generate overtone at multiple of fundamental
|
||||
aOvertone oscili iOvertoneAmp, kPitchEnv * iOvertoneFreqMult
|
||||
|
||||
; Add harmonic spread (additional harmonics)
|
||||
aHarm2 oscili iHarmonicSpread * 0.3, kPitchEnv * 3
|
||||
aHarm3 oscili iHarmonicSpread * 0.2, kPitchEnv * 5
|
||||
|
||||
; Mix oscillators
|
||||
aMix = aFund + aOvertone + aHarm2 + aHarm3
|
||||
|
||||
; Apply waveshaping (hyperbolic tangent style)
|
||||
if iWaveshape > 0.1 then
|
||||
aMix = tanh(aMix * (1 + iWaveshape * 3))
|
||||
endif
|
||||
|
||||
; Generate pink noise
|
||||
aPink pinkish 1
|
||||
|
||||
; Noise envelope (fast decay)
|
||||
kNoiseEnv expseg 1, iNoiseDecayTime, 0.001, idur - iNoiseDecayTime, 0.001
|
||||
aPinkScaled = aPink * iNoiseAmp * kNoiseEnv
|
||||
|
||||
; Add noise to mix
|
||||
aMix = aMix + aPinkScaled
|
||||
|
||||
; Click transient (high frequency burst at start)
|
||||
if iClick > 0.1 then
|
||||
kClickEnv linseg 1, 0.005, 0, idur - 0.005, 0
|
||||
aClick oscili iClick * 0.4, kPitchEnv * 8
|
||||
aMix = aMix + aClick * kClickEnv
|
||||
endif
|
||||
|
||||
; Apply resonant low-pass filter
|
||||
kFilterFreq = iFilterCutoff * (1 + kPitchEnv / iBaseFreq * 0.5)
|
||||
aFiltered rezzy aMix, kFilterFreq, iFilterResonance
|
||||
|
||||
; Body resonance (second resonant filter at fundamental)
|
||||
if iBodyResonance > 0.1 then
|
||||
aBodyFilt butterbp aFiltered, kPitchEnv * 0.5, 20
|
||||
aFiltered = aFiltered + aBodyFilt * iBodyResonance
|
||||
endif
|
||||
|
||||
; Apply main envelope
|
||||
aOut = aFiltered * kAmpEnv * 0.5
|
||||
|
||||
; Stereo - slightly different phase and detune for right channel
|
||||
kPitchEnvR expseg iBaseFreq * 1.002 * (1 + iPitchSweep * 3), iPitchDecayTime, iBaseFreq * 1.002, idur - iPitchDecayTime, iBaseFreq * 0.952
|
||||
|
||||
aFundR oscili 0.7, kPitchEnvR
|
||||
aOvertoneR oscili iOvertoneAmp, kPitchEnvR * iOvertoneFreqMult
|
||||
aHarm2R oscili iHarmonicSpread * 0.3, kPitchEnvR * 3
|
||||
aHarm3R oscili iHarmonicSpread * 0.2, kPitchEnvR * 5
|
||||
|
||||
aMixR = aFundR + aOvertoneR + aHarm2R + aHarm3R
|
||||
|
||||
if iWaveshape > 0.1 then
|
||||
aMixR = tanh(aMixR * (1 + iWaveshape * 3))
|
||||
endif
|
||||
|
||||
aPinkR pinkish 1
|
||||
aPinkScaledR = aPinkR * iNoiseAmp * kNoiseEnv
|
||||
aMixR = aMixR + aPinkScaledR
|
||||
|
||||
if iClick > 0.1 then
|
||||
aClickR oscili iClick * 0.4, kPitchEnvR * 8
|
||||
aMixR = aMixR + aClickR * kClickEnv
|
||||
endif
|
||||
|
||||
kFilterFreqR = iFilterCutoff * (1 + kPitchEnvR / iBaseFreq * 0.5)
|
||||
aFilteredR rezzy aMixR, kFilterFreqR, iFilterResonance
|
||||
|
||||
if iBodyResonance > 0.1 then
|
||||
aBodyFiltR butterbp aFilteredR, kPitchEnvR * 0.5, 20
|
||||
aFilteredR = aFilteredR + aBodyFiltR * iBodyResonance
|
||||
endif
|
||||
|
||||
aOutR = aFilteredR * kAmpEnv * 0.5
|
||||
|
||||
outs aOut, aOutR
|
||||
endin
|
||||
`;
|
||||
}
|
||||
|
||||
protected getParametersForCsound(params: AdditiveBassParams): CsoundParameter[] {
|
||||
return [
|
||||
{ channelName: 'baseFreq', value: params.baseFreq },
|
||||
{ channelName: 'pitchSweep', value: params.pitchSweep },
|
||||
{ channelName: 'pitchDecay', value: params.pitchDecay },
|
||||
{ channelName: 'overtoneAmp', value: params.overtoneAmp },
|
||||
{ channelName: 'overtoneFreqMult', value: params.overtoneFreqMult },
|
||||
{ channelName: 'noiseAmp', value: params.noiseAmp },
|
||||
{ channelName: 'noiseDecay', value: params.noiseDecay },
|
||||
{ channelName: 'filterResonance', value: params.filterResonance },
|
||||
{ channelName: 'filterCutoff', value: params.filterCutoff },
|
||||
{ channelName: 'attack', value: params.attack },
|
||||
{ channelName: 'decay', value: params.decay },
|
||||
{ channelName: 'waveshape', value: params.waveshape },
|
||||
{ channelName: 'bodyResonance', value: params.bodyResonance },
|
||||
{ channelName: 'click', value: params.click },
|
||||
{ channelName: 'harmonicSpread', value: params.harmonicSpread },
|
||||
];
|
||||
}
|
||||
|
||||
randomParams(pitchLock?: PitchLock): AdditiveBassParams {
|
||||
const baseFreq = pitchLock?.enabled ? pitchLock.frequency : this.randomRange(35, 80);
|
||||
|
||||
const overtoneMultChoices = [1.5, 2.0, 2.5, 3.0, 4.0];
|
||||
|
||||
return {
|
||||
baseFreq,
|
||||
pitchSweep: this.randomRange(0.3, 1.0),
|
||||
pitchDecay: this.randomRange(0.02, 0.15),
|
||||
overtoneAmp: this.randomRange(0.2, 0.7),
|
||||
overtoneFreqMult: this.randomChoice(overtoneMultChoices),
|
||||
noiseAmp: this.randomRange(0.05, 0.3),
|
||||
noiseDecay: this.randomRange(0.01, 0.08),
|
||||
filterResonance: this.randomRange(5, 25),
|
||||
filterCutoff: this.randomRange(100, 800),
|
||||
attack: this.randomRange(0.001, 0.02),
|
||||
decay: this.randomRange(0.3, 0.8),
|
||||
waveshape: this.randomRange(0, 0.7),
|
||||
bodyResonance: this.randomRange(0, 0.5),
|
||||
click: this.randomRange(0, 0.6),
|
||||
harmonicSpread: this.randomRange(0, 0.5),
|
||||
};
|
||||
}
|
||||
|
||||
mutateParams(
|
||||
params: AdditiveBassParams,
|
||||
mutationAmount: number = 0.15,
|
||||
pitchLock?: PitchLock
|
||||
): AdditiveBassParams {
|
||||
const baseFreq = pitchLock?.enabled ? pitchLock.frequency : params.baseFreq;
|
||||
const overtoneMultChoices = [1.5, 2.0, 2.5, 3.0, 4.0];
|
||||
|
||||
return {
|
||||
baseFreq,
|
||||
pitchSweep: this.mutateValue(params.pitchSweep, mutationAmount, 0.1, 1.5),
|
||||
pitchDecay: this.mutateValue(params.pitchDecay, mutationAmount, 0.01, 0.25),
|
||||
overtoneAmp: this.mutateValue(params.overtoneAmp, mutationAmount, 0, 1.0),
|
||||
overtoneFreqMult:
|
||||
Math.random() < 0.1 ? this.randomChoice(overtoneMultChoices) : params.overtoneFreqMult,
|
||||
noiseAmp: this.mutateValue(params.noiseAmp, mutationAmount, 0, 0.5),
|
||||
noiseDecay: this.mutateValue(params.noiseDecay, mutationAmount, 0.005, 0.15),
|
||||
filterResonance: this.mutateValue(params.filterResonance, mutationAmount, 2, 40),
|
||||
filterCutoff: this.mutateValue(params.filterCutoff, mutationAmount, 80, 1200),
|
||||
attack: this.mutateValue(params.attack, mutationAmount, 0.001, 0.05),
|
||||
decay: this.mutateValue(params.decay, mutationAmount, 0.15, 0.95),
|
||||
waveshape: this.mutateValue(params.waveshape, mutationAmount, 0, 1),
|
||||
bodyResonance: this.mutateValue(params.bodyResonance, mutationAmount, 0, 0.8),
|
||||
click: this.mutateValue(params.click, mutationAmount, 0, 0.8),
|
||||
harmonicSpread: this.mutateValue(params.harmonicSpread, mutationAmount, 0, 0.7),
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { PitchLock, SynthEngine } from './SynthEngine';
|
||||
import type { PitchLock, SynthEngine } from './base/SynthEngine';
|
||||
|
||||
interface BassDrumParams {
|
||||
// Core frequency (base pitch of the kick)
|
||||
@ -57,7 +57,7 @@ interface BassDrumParams {
|
||||
|
||||
export class BassDrum implements SynthEngine {
|
||||
getName(): string {
|
||||
return 'Kick';
|
||||
return 'Dark Kick';
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
@ -68,6 +68,10 @@ export class BassDrum implements SynthEngine {
|
||||
return 'generative' as const;
|
||||
}
|
||||
|
||||
getCategory() {
|
||||
return 'Percussion' as const;
|
||||
}
|
||||
|
||||
randomParams(pitchLock?: PitchLock): BassDrumParams {
|
||||
// Choose a kick character/style
|
||||
const styleRoll = Math.random();
|
||||
|
||||
@ -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);
|
||||
|
||||
350
src/lib/audio/engines/CombResonator.ts
Normal file
350
src/lib/audio/engines/CombResonator.ts
Normal file
@ -0,0 +1,350 @@
|
||||
import { CsoundEngine, type CsoundParameter } from './base/CsoundEngine';
|
||||
import type { PitchLock } from './base/SynthEngine';
|
||||
|
||||
enum SourceType {
|
||||
Saw = 0,
|
||||
Pulse = 1,
|
||||
NoiseBurst = 2,
|
||||
Triangle = 3,
|
||||
}
|
||||
|
||||
interface CombParams {
|
||||
feedback: number;
|
||||
brightness: number;
|
||||
}
|
||||
|
||||
interface ResonatorParams {
|
||||
frequency: number;
|
||||
resonance: number;
|
||||
envAmount: number;
|
||||
attack: number;
|
||||
decay: number;
|
||||
sustain: number;
|
||||
release: number;
|
||||
}
|
||||
|
||||
interface DelayParams {
|
||||
time1: number;
|
||||
time2: number;
|
||||
feedback: number;
|
||||
filterFreq: number;
|
||||
filterSweep: number;
|
||||
mix: number;
|
||||
}
|
||||
|
||||
export interface CombResonatorParams {
|
||||
baseFreq: number;
|
||||
sourceType: SourceType;
|
||||
sourceDecay: number;
|
||||
comb: CombParams;
|
||||
resonator: ResonatorParams;
|
||||
delay: DelayParams;
|
||||
stereoWidth: number;
|
||||
}
|
||||
|
||||
export class CombResonator extends CsoundEngine<CombResonatorParams> {
|
||||
getName(): string {
|
||||
return 'CombRes';
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return 'Comb filter and resonator for metallic and bell-like sounds';
|
||||
}
|
||||
|
||||
getType() {
|
||||
return 'generative' as const;
|
||||
}
|
||||
|
||||
getCategory() {
|
||||
return 'Subtractive' as const;
|
||||
}
|
||||
|
||||
protected getOrchestra(): string {
|
||||
return `
|
||||
instr 1
|
||||
ibasefreq chnget "basefreq"
|
||||
isourcetype chnget "sourcetype"
|
||||
isourcedecay chnget "sourcedecay"
|
||||
icombfeedback chnget "comb_feedback"
|
||||
icombbright chnget "comb_brightness"
|
||||
iresfreq chnget "res_frequency"
|
||||
iresres chnget "res_resonance"
|
||||
iresenvamt chnget "res_envamt"
|
||||
iresattack chnget "res_attack"
|
||||
iresdecay chnget "res_decay"
|
||||
iressustain chnget "res_sustain"
|
||||
iresrelease chnget "res_release"
|
||||
ideltime1 chnget "delay_time1"
|
||||
ideltime2 chnget "delay_time2"
|
||||
idelfeedback chnget "delay_feedback"
|
||||
ifilterfreq chnget "delay_filterfreq"
|
||||
ifiltersweep chnget "delay_filtersweep"
|
||||
idelmix chnget "delay_mix"
|
||||
istereo chnget "stereowidth"
|
||||
|
||||
idur = p3
|
||||
|
||||
; Convert ratios to time values
|
||||
iresatt = iresattack * idur
|
||||
iresdec = iresdecay * idur
|
||||
iresrel = iresrelease * idur
|
||||
isrcdec = isourcedecay * idur
|
||||
idt1 = ideltime1 * idur
|
||||
idt2 = ideltime2 * idur
|
||||
|
||||
; Stereo detuning
|
||||
idetune = 1 + (istereo * 0.003)
|
||||
ifreqL = ibasefreq / idetune
|
||||
ifreqR = ibasefreq * idetune
|
||||
|
||||
; Source envelope - sharp attack, exponential decay
|
||||
ksrcenv expon 1, isrcdec, 0.001
|
||||
|
||||
; Generate source signals with more amplitude
|
||||
if isourcetype == 0 then
|
||||
; Saw wave with harmonics
|
||||
asrcL vco2 ksrcenv * 0.8, ifreqL, 0
|
||||
asrcR vco2 ksrcenv * 0.8, ifreqR, 0
|
||||
elseif isourcetype == 1 then
|
||||
; Pulse wave
|
||||
asrcL vco2 ksrcenv * 0.8, ifreqL, 2, 0.3
|
||||
asrcR vco2 ksrcenv * 0.8, ifreqR, 2, 0.3
|
||||
elseif isourcetype == 2 then
|
||||
; Noise burst with pitch
|
||||
anoise1 noise ksrcenv * 0.6, 0
|
||||
anoise2 noise ksrcenv * 0.6, 0
|
||||
asrcL butterbp anoise1, ifreqL, ifreqL * 2
|
||||
asrcR butterbp anoise2, ifreqR, ifreqR * 2
|
||||
else
|
||||
; Triangle wave
|
||||
asrcL vco2 ksrcenv * 0.8, ifreqL, 12
|
||||
asrcR vco2 ksrcenv * 0.8, ifreqR, 12
|
||||
endif
|
||||
|
||||
; Direct path for immediate sound (no delay)
|
||||
adirectL = asrcL * 0.3
|
||||
adirectR = asrcR * 0.3
|
||||
|
||||
; Comb filter using vcomb for better feedback control
|
||||
idelaytimeL = 1 / ifreqL
|
||||
idelaytimeR = 1 / ifreqR
|
||||
ilooptime = 0.1
|
||||
|
||||
acombL vcomb asrcL, idelaytimeL, ilooptime, icombfeedback
|
||||
acombR vcomb asrcR, idelaytimeR, ilooptime, icombfeedback
|
||||
|
||||
; Damping filter
|
||||
adampl tone acombL, ibasefreq * icombbright
|
||||
adampR tone acombR, ibasefreq * icombbright
|
||||
|
||||
; Resonator envelope for filter sweep
|
||||
kresenv madsr iresatt, iresdec, iressustain, iresrel
|
||||
kresfreq = iresfreq + (kresenv * iresenvamt * ibasefreq * 4)
|
||||
kresfreq = limit(kresfreq, 80, 18000)
|
||||
|
||||
; Multiple resonators for richer tone
|
||||
kbw = kresfreq / iresres
|
||||
ares1L butterbp adampl, kresfreq, kbw
|
||||
ares1R butterbp adampR, kresfreq, kbw
|
||||
|
||||
ares2L butterbp adampl, kresfreq * 1.5, kbw * 1.2
|
||||
ares2R butterbp adampR, kresfreq * 1.5, kbw * 1.2
|
||||
|
||||
; Mix resonators
|
||||
aresL = ares1L + (ares2L * 0.5)
|
||||
aresR = ares1R + (ares2R * 0.5)
|
||||
|
||||
; Dry signal (this plays immediately)
|
||||
adryL = (adampl * 0.2) + (aresL * 0.8)
|
||||
adryR = (adampR * 0.2) + (aresR * 0.8)
|
||||
|
||||
; Direct signal envelope: loud at start, fades out quickly
|
||||
kdirectenv linseg 1, idur * 0.05, 0, idur * 0.95, 0
|
||||
|
||||
; Crossfade envelope: start with dry signal, fade in delays
|
||||
kdryenv linseg 0.6, idur * 0.15, 0.3, idur * 0.85, 0.2
|
||||
kwetenv linseg 0, idur * 0.1, 1, idur * 0.9, 1
|
||||
|
||||
; Extreme filter sweep envelopes with multiple segments
|
||||
kfilter1 linseg ifilterfreq, idur * 0.2, ifilterfreq + (ifiltersweep * 12000), idur * 0.3, ifilterfreq - (ifiltersweep * 5000), idur * 0.5, ifilterfreq + (ifiltersweep * 15000)
|
||||
kfilter1 = limit(kfilter1, 250, 19000)
|
||||
|
||||
kfilter2 expseg ifilterfreq * 2, idur * 0.15, ifilterfreq * 0.3 + 200, idur * 0.35, ifilterfreq * 3 + 100, idur * 0.5, ifilterfreq + (ifiltersweep * 10000) + 100
|
||||
kfilter2 = limit(kfilter2, 200, 18000)
|
||||
|
||||
kfilter3 linseg ifilterfreq * 0.5, idur * 0.25, ifilterfreq + (ifiltersweep * 8000), idur * 0.25, ifilterfreq * 2, idur * 0.5, ifilterfreq - (ifiltersweep * 3000)
|
||||
kfilter3 = limit(kfilter3, 300, 16000)
|
||||
|
||||
kfilter4 expon ifilterfreq + 100, idur, ifilterfreq + (ifiltersweep * 14000) + 100
|
||||
kfilter4 = limit(kfilter4, 350, 17000)
|
||||
|
||||
; LFO for delay time modulation
|
||||
klfo1 lfo 0.03, 3 + (ifiltersweep * 2)
|
||||
klfo2 lfo 0.04, 5 - (ifiltersweep * 1.5)
|
||||
|
||||
; Multi-tap delay line 1 (Left -> Right) with modulation
|
||||
abuf1 delayr idt1 * 1.1
|
||||
kdt1a = (idt1 * 0.2) + klfo1
|
||||
kdt1b = (idt1 * 0.45) - klfo2
|
||||
kdt1c = (idt1 * 0.75) + (klfo1 * 0.5)
|
||||
kdt1d = idt1
|
||||
adel1a deltap3 kdt1a
|
||||
adel1b deltap3 kdt1b
|
||||
adel1c deltap3 kdt1c
|
||||
adel1d deltap3 kdt1d
|
||||
delayw adryL + (adel1d * idelfeedback * 0.95)
|
||||
|
||||
afilt1a butterbp adel1a, kfilter1, kfilter1 * 0.15
|
||||
afilt1b butterbp adel1b, kfilter2, kfilter2 * 0.2
|
||||
afilt1c butterbp adel1c, kfilter3, kfilter3 * 0.25
|
||||
afilt1d butterbp adel1d, kfilter4, kfilter4 * 0.18
|
||||
|
||||
adelR = (afilt1a * 0.6) + (afilt1b * 0.8) + (afilt1c * 0.9) + (afilt1d * 0.7)
|
||||
|
||||
; Multi-tap delay line 2 (Right -> Left) with modulation
|
||||
abuf2 delayr idt2 * 1.1
|
||||
kdt2a = (idt2 * 0.18) - klfo2
|
||||
kdt2b = (idt2 * 0.42) + klfo1
|
||||
kdt2c = (idt2 * 0.7) - (klfo2 * 0.5)
|
||||
kdt2d = idt2
|
||||
adel2a deltap3 kdt2a
|
||||
adel2b deltap3 kdt2b
|
||||
adel2c deltap3 kdt2c
|
||||
adel2d deltap3 kdt2d
|
||||
delayw adryR + (adel2d * idelfeedback * 0.95)
|
||||
|
||||
afilt2a butterbp adel2a, kfilter1 * 1.4, kfilter1 * 0.12
|
||||
afilt2b butterbp adel2b, kfilter2 * 0.7, kfilter2 * 0.22
|
||||
afilt2c butterbp adel2c, kfilter3 * 1.2, kfilter3 * 0.16
|
||||
afilt2d butterbp adel2d, kfilter4 * 0.9, kfilter4 * 0.2
|
||||
|
||||
adelL = (afilt2a * 0.7) + (afilt2b * 0.6) + (afilt2c * 0.85) + (afilt2d * 0.8)
|
||||
|
||||
; Additional chaotic resonant delays
|
||||
abuf3 delayr idt1 * 1.6
|
||||
kdt3 = (idt1 * 1.4) + (klfo1 * 2)
|
||||
adel3 deltap3 kdt3
|
||||
delayw (adryL + adryR) * 0.5 + (adel3 * idelfeedback * 0.85)
|
||||
afilt3 butterbp adel3, kfilter2 * 0.6, kfilter2 * 0.1
|
||||
|
||||
abuf4 delayr idt2 * 1.8
|
||||
kdt4 = (idt2 * 1.6) - (klfo2 * 2)
|
||||
adel4 deltap3 kdt4
|
||||
delayw (adryR + adryL) * 0.5 + (adel4 * idelfeedback * 0.8)
|
||||
afilt4 butterbp adel4, kfilter3 * 1.3, kfilter3 * 0.12
|
||||
|
||||
abuf5 delayr idt1 * 2.2
|
||||
kdt5 = (idt1 * 2.0) + klfo1 + klfo2
|
||||
adel5 deltap3 kdt5
|
||||
delayw (adelL + adelR) * 0.4 + (adel5 * idelfeedback * 0.75)
|
||||
afilt5 butterbp adel5, kfilter4 * 0.8, kfilter4 * 0.08
|
||||
|
||||
; Mix: direct signal (immediate), dry (soon after), delays (build up)
|
||||
amixL = (adirectL * kdirectenv) + (adryL * kdryenv) + ((adelL * 1.2 + afilt3 * 0.8 + afilt4 * 0.7 + afilt5 * 0.6) * idelmix * kwetenv)
|
||||
amixR = (adirectR * kdirectenv) + (adryR * kdryenv) + ((adelR * 1.2 + afilt4 * 0.8 + afilt3 * 0.7 + afilt5 * 0.6) * idelmix * kwetenv)
|
||||
|
||||
outs amixL, amixR
|
||||
endin
|
||||
`;
|
||||
}
|
||||
|
||||
protected getParametersForCsound(params: CombResonatorParams): CsoundParameter[] {
|
||||
return [
|
||||
{ channelName: 'basefreq', value: params.baseFreq },
|
||||
{ channelName: 'sourcetype', value: params.sourceType },
|
||||
{ channelName: 'sourcedecay', value: params.sourceDecay },
|
||||
{ channelName: 'comb_feedback', value: params.comb.feedback },
|
||||
{ channelName: 'comb_brightness', value: params.comb.brightness },
|
||||
{ channelName: 'res_frequency', value: params.resonator.frequency },
|
||||
{ channelName: 'res_resonance', value: params.resonator.resonance },
|
||||
{ channelName: 'res_envamt', value: params.resonator.envAmount },
|
||||
{ channelName: 'res_attack', value: params.resonator.attack },
|
||||
{ channelName: 'res_decay', value: params.resonator.decay },
|
||||
{ channelName: 'res_sustain', value: params.resonator.sustain },
|
||||
{ channelName: 'res_release', value: params.resonator.release },
|
||||
{ channelName: 'delay_time1', value: params.delay.time1 },
|
||||
{ channelName: 'delay_time2', value: params.delay.time2 },
|
||||
{ channelName: 'delay_feedback', value: params.delay.feedback },
|
||||
{ channelName: 'delay_filterfreq', value: params.delay.filterFreq },
|
||||
{ channelName: 'delay_filtersweep', value: params.delay.filterSweep },
|
||||
{ channelName: 'delay_mix', value: params.delay.mix },
|
||||
{ channelName: 'stereowidth', value: params.stereoWidth },
|
||||
];
|
||||
}
|
||||
|
||||
randomParams(pitchLock?: PitchLock): CombResonatorParams {
|
||||
let baseFreq: number;
|
||||
if (pitchLock?.enabled) {
|
||||
baseFreq = pitchLock.frequency;
|
||||
} else {
|
||||
const baseFreqChoices = [82.4, 110, 146.8, 220, 293.7, 440, 587.3, 880];
|
||||
baseFreq = this.randomChoice(baseFreqChoices) * this.randomRange(0.97, 1.03);
|
||||
}
|
||||
|
||||
return {
|
||||
baseFreq,
|
||||
sourceType: this.randomInt(0, 3) as SourceType,
|
||||
sourceDecay: this.randomRange(0.02, 0.15),
|
||||
comb: {
|
||||
feedback: this.randomRange(0.7, 0.95),
|
||||
brightness: this.randomRange(3, 20),
|
||||
},
|
||||
resonator: {
|
||||
frequency: baseFreq * this.randomChoice([1, 1.3, 1.6, 2, 2.4, 3, 3.5, 4, 5]),
|
||||
resonance: this.randomRange(8, 80),
|
||||
envAmount: this.randomRange(0.3, 2.0),
|
||||
attack: this.randomRange(0.001, 0.05),
|
||||
decay: this.randomRange(0.15, 0.6),
|
||||
sustain: this.randomRange(0.05, 0.4),
|
||||
release: this.randomRange(0.3, 0.8),
|
||||
},
|
||||
delay: {
|
||||
time1: this.randomRange(0.08, 0.35),
|
||||
time2: this.randomRange(0.1, 0.4),
|
||||
feedback: this.randomRange(0.82, 0.98),
|
||||
filterFreq: this.randomRange(400, 5000),
|
||||
filterSweep: this.randomRange(-1.2, 2.0),
|
||||
mix: this.randomRange(0.6, 1.0),
|
||||
},
|
||||
stereoWidth: this.randomRange(0.2, 0.8),
|
||||
};
|
||||
}
|
||||
|
||||
mutateParams(
|
||||
params: CombResonatorParams,
|
||||
mutationAmount: number = 0.15,
|
||||
pitchLock?: PitchLock
|
||||
): CombResonatorParams {
|
||||
const baseFreq = pitchLock?.enabled ? pitchLock.frequency : params.baseFreq;
|
||||
|
||||
return {
|
||||
baseFreq,
|
||||
sourceType: Math.random() < 0.15 ? (this.randomInt(0, 3) as SourceType) : params.sourceType,
|
||||
sourceDecay: this.mutateValue(params.sourceDecay, mutationAmount, 0.02, 0.5),
|
||||
comb: {
|
||||
feedback: this.mutateValue(params.comb.feedback, mutationAmount, 0.3, 0.95),
|
||||
brightness: this.mutateValue(params.comb.brightness, mutationAmount, 1, 20),
|
||||
},
|
||||
resonator: {
|
||||
frequency: Math.random() < 0.2
|
||||
? baseFreq * this.randomChoice([1, 1.5, 2, 2.5, 3, 3.5, 4])
|
||||
: this.mutateValue(params.resonator.frequency, mutationAmount, baseFreq * 0.5, baseFreq * 6),
|
||||
resonance: this.mutateValue(params.resonator.resonance, mutationAmount, 3, 80),
|
||||
envAmount: this.mutateValue(params.resonator.envAmount, mutationAmount, 0, 2),
|
||||
attack: this.mutateValue(params.resonator.attack, mutationAmount, 0.001, 0.2),
|
||||
decay: this.mutateValue(params.resonator.decay, mutationAmount, 0.05, 0.6),
|
||||
sustain: this.mutateValue(params.resonator.sustain, mutationAmount, 0, 0.7),
|
||||
release: this.mutateValue(params.resonator.release, mutationAmount, 0.1, 0.9),
|
||||
},
|
||||
delay: {
|
||||
time1: this.mutateValue(params.delay.time1, mutationAmount, 0.05, 0.5),
|
||||
time2: this.mutateValue(params.delay.time2, mutationAmount, 0.08, 0.55),
|
||||
feedback: this.mutateValue(params.delay.feedback, mutationAmount, 0.75, 0.99),
|
||||
filterFreq: this.mutateValue(params.delay.filterFreq, mutationAmount, 350, 7000),
|
||||
filterSweep: this.mutateValue(params.delay.filterSweep, mutationAmount, -1.5, 2.5),
|
||||
mix: this.mutateValue(params.delay.mix, mutationAmount, 0.5, 1.0),
|
||||
},
|
||||
stereoWidth: this.mutateValue(params.stereoWidth, mutationAmount, 0, 1),
|
||||
};
|
||||
}
|
||||
}
|
||||
334
src/lib/audio/engines/Dripwater.ts
Normal file
334
src/lib/audio/engines/Dripwater.ts
Normal file
@ -0,0 +1,334 @@
|
||||
import { CsoundEngine, type CsoundParameter } from './base/CsoundEngine';
|
||||
import type { PitchLock } from './base/SynthEngine';
|
||||
|
||||
interface DripwaterParams {
|
||||
baseFreq: number;
|
||||
dropRate: number;
|
||||
numElements: number;
|
||||
damping: number;
|
||||
shakeIntensity: number;
|
||||
freqRatio1: number;
|
||||
freqRatio2: number;
|
||||
freqRatio3: number;
|
||||
attack: number;
|
||||
release: number;
|
||||
amplitude: number;
|
||||
delayTime1: number;
|
||||
delayTime2: number;
|
||||
delayTime3: number;
|
||||
delayTime4: number;
|
||||
delayGain1: number;
|
||||
delayGain2: number;
|
||||
delayGain3: number;
|
||||
delayGain4: number;
|
||||
feedbackDelayTime: number;
|
||||
feedbackAmount: number;
|
||||
modDepth: number;
|
||||
modRate: number;
|
||||
stereoSpread: number;
|
||||
combDelayTime: number;
|
||||
combFeedback: number;
|
||||
reverseDelayTime: number;
|
||||
reverseGain: number;
|
||||
cascadeDelay1: number;
|
||||
cascadeDelay2: number;
|
||||
cascadeFeedback: number;
|
||||
}
|
||||
|
||||
export class Dripwater extends CsoundEngine<DripwaterParams> {
|
||||
getName(): string {
|
||||
return 'Weird Waters';
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return 'Physical model of water droplets through cascading delays, comb filters, and reverse echoes';
|
||||
}
|
||||
|
||||
getType() {
|
||||
return 'generative' as const;
|
||||
}
|
||||
|
||||
getCategory() {
|
||||
return 'Noise' as const;
|
||||
}
|
||||
|
||||
protected getOrchestra(): string {
|
||||
return `
|
||||
instr 1
|
||||
iBaseFreq chnget "baseFreq"
|
||||
iDropRate chnget "dropRate"
|
||||
iNumElements chnget "numElements"
|
||||
iDamping chnget "damping"
|
||||
iShakeIntensity chnget "shakeIntensity"
|
||||
iFreqRatio1 chnget "freqRatio1"
|
||||
iFreqRatio2 chnget "freqRatio2"
|
||||
iFreqRatio3 chnget "freqRatio3"
|
||||
iAttack chnget "attack"
|
||||
iRelease chnget "release"
|
||||
iAmplitude chnget "amplitude"
|
||||
iDelayTime1 chnget "delayTime1"
|
||||
iDelayTime2 chnget "delayTime2"
|
||||
iDelayTime3 chnget "delayTime3"
|
||||
iDelayTime4 chnget "delayTime4"
|
||||
iDelayGain1 chnget "delayGain1"
|
||||
iDelayGain2 chnget "delayGain2"
|
||||
iDelayGain3 chnget "delayGain3"
|
||||
iDelayGain4 chnget "delayGain4"
|
||||
iFeedbackDelayTime chnget "feedbackDelayTime"
|
||||
iFeedbackAmount chnget "feedbackAmount"
|
||||
iModDepth chnget "modDepth"
|
||||
iModRate chnget "modRate"
|
||||
iStereoSpread chnget "stereoSpread"
|
||||
iCombDelayTime chnget "combDelayTime"
|
||||
iCombFeedback chnget "combFeedback"
|
||||
iReverseDelayTime chnget "reverseDelayTime"
|
||||
iReverseGain chnget "reverseGain"
|
||||
iCascadeDelay1 chnget "cascadeDelay1"
|
||||
iCascadeDelay2 chnget "cascadeDelay2"
|
||||
iCascadeFeedback chnget "cascadeFeedback"
|
||||
|
||||
idur = p3
|
||||
iAttackTime = iAttack * idur
|
||||
iReleaseTime = iRelease * idur
|
||||
|
||||
; Scale delay times by duration so they fill the entire sound
|
||||
iScaledDelayTime1 = iDelayTime1 * idur
|
||||
iScaledDelayTime2 = iDelayTime2 * idur
|
||||
iScaledDelayTime3 = iDelayTime3 * idur
|
||||
iScaledDelayTime4 = iDelayTime4 * idur
|
||||
iScaledFeedbackDelay = iFeedbackDelayTime * idur
|
||||
iModDepthScaled = iModDepth * idur * 0.1
|
||||
iScaledCombDelay = iCombDelayTime * idur
|
||||
iScaledReverseDelay = iReverseDelayTime * idur
|
||||
iScaledCascade1 = iCascadeDelay1 * idur
|
||||
iScaledCascade2 = iCascadeDelay2 * idur
|
||||
|
||||
; Calculate resonant frequencies from base frequency and ratios
|
||||
iFreq1 = iBaseFreq * iFreqRatio1
|
||||
iFreq2 = iBaseFreq * iFreqRatio2
|
||||
iFreq3 = iBaseFreq * iFreqRatio3
|
||||
|
||||
; Generate dripwater sound
|
||||
; Signature: ares dripwater kamp, idettack, inum, idamp, imaxshake, ifreq, ifreq1, ifreq2
|
||||
; Reduce amplitude to prevent clipping with all the delays
|
||||
kAmp = iAmplitude * 0.08
|
||||
aDrip dripwater kAmp, iDropRate, iNumElements, iDamping, iShakeIntensity, iFreq1, iFreq2, iFreq3
|
||||
|
||||
; Apply AR envelope
|
||||
kEnv linsegr 0, iAttackTime, 1, iReleaseTime, 0
|
||||
|
||||
aDry = aDrip * kEnv
|
||||
|
||||
; Apply multitap delay with duration-scaled times
|
||||
; Signature: a1 [, a2, a3, ...] multitap asource [, itime1, igain1, itime2, igain2, ...]
|
||||
aMultitap multitap aDry, iScaledDelayTime1, iDelayGain1, iScaledDelayTime2, iDelayGain2, iScaledDelayTime3, iDelayGain3, iScaledDelayTime4, iDelayGain4
|
||||
|
||||
; Add modulated feedback delay
|
||||
; LFO modulates delay time
|
||||
kLFO oscili iModDepthScaled, iModRate
|
||||
kDelayTime = iScaledFeedbackDelay + kLFO
|
||||
kDelayTime = max(kDelayTime, 0.001)
|
||||
|
||||
; Variable delay with feedback
|
||||
aBuf delayr idur
|
||||
aTapL deltapi kDelayTime
|
||||
delayw aDry + (aTapL * iFeedbackAmount)
|
||||
|
||||
; Comb filter delay (creates resonance)
|
||||
aComb comb aDry, iScaledCombDelay, iCombFeedback
|
||||
|
||||
; Reverse delay using delayr/delayw with interpolation
|
||||
aBufRev delayr iScaledReverseDelay
|
||||
aReverseTap deltapi iScaledReverseDelay * (1 - (line:a(0, idur, 1)))
|
||||
delayw aDry
|
||||
aReverse = aReverseTap * iReverseGain
|
||||
|
||||
; Cascaded delays (two delays feeding into each other)
|
||||
aBuf1 delayr idur
|
||||
aCascade1 deltapi iScaledCascade1
|
||||
aBuf2 delayr idur
|
||||
aCascade2 deltapi iScaledCascade2
|
||||
delayw aCascade1 * iCascadeFeedback
|
||||
delayw aDry + (aCascade2 * iCascadeFeedback)
|
||||
|
||||
aOut = aDry + aMultitap + aTapL + aComb + aReverse + aCascade1 + aCascade2
|
||||
|
||||
; Create stereo width with slightly different parameters for right channel
|
||||
; Slightly detune the resonant frequencies for right channel
|
||||
iFreq1_R = iFreq1 * 1.003
|
||||
iFreq2_R = iFreq2 * 0.998
|
||||
iFreq3_R = iFreq3 * 1.002
|
||||
|
||||
; Use slightly different initial values for variation
|
||||
iDropRate_R = iDropRate * 1.02
|
||||
iShakeIntensity_R = iShakeIntensity * 0.98
|
||||
|
||||
kAmp_R = iAmplitude * 0.08
|
||||
aDrip_R dripwater kAmp_R, iDropRate_R, iNumElements, iDamping, iShakeIntensity_R, iFreq1_R, iFreq2_R, iFreq3_R
|
||||
|
||||
aDry_R = aDrip_R * kEnv
|
||||
|
||||
; Apply multitap delay to right channel with slightly different times for stereo width
|
||||
iScaledDelayTime1_R = iScaledDelayTime1 * 1.03
|
||||
iScaledDelayTime2_R = iScaledDelayTime2 * 0.97
|
||||
iScaledDelayTime3_R = iScaledDelayTime3 * 1.05
|
||||
iScaledDelayTime4_R = iScaledDelayTime4 * 0.95
|
||||
|
||||
aMultitap_R multitap aDry_R, iScaledDelayTime1_R, iDelayGain1, iScaledDelayTime2_R, iDelayGain2, iScaledDelayTime3_R, iDelayGain3, iScaledDelayTime4_R, iDelayGain4
|
||||
|
||||
; Right channel modulated delay with stereo spread
|
||||
kLFO_R oscili iModDepthScaled, iModRate * 1.07
|
||||
kDelayTime_R = (iScaledFeedbackDelay * iStereoSpread) + kLFO_R
|
||||
kDelayTime_R = max(kDelayTime_R, 0.001)
|
||||
|
||||
aBuf_R delayr idur
|
||||
aTapR deltapi kDelayTime_R
|
||||
delayw aDry_R + (aTapR * iFeedbackAmount)
|
||||
|
||||
; Comb filter for right channel
|
||||
iScaledCombDelay_R = iScaledCombDelay * 1.07
|
||||
aComb_R comb aDry_R, iScaledCombDelay_R, iCombFeedback
|
||||
|
||||
; Reverse delay for right channel
|
||||
iScaledReverseDelay_R = iScaledReverseDelay * 0.93
|
||||
aBufRev_R delayr iScaledReverseDelay_R
|
||||
aReverseTap_R deltapi iScaledReverseDelay_R * (1 - (line:a(0, idur, 1)))
|
||||
delayw aDry_R
|
||||
aReverse_R = aReverseTap_R * iReverseGain
|
||||
|
||||
; Cascaded delays for right channel
|
||||
iScaledCascade1_R = iScaledCascade1 * 1.05
|
||||
iScaledCascade2_R = iScaledCascade2 * 0.95
|
||||
aBuf1_R delayr idur
|
||||
aCascade1_R deltapi iScaledCascade1_R
|
||||
aBuf2_R delayr idur
|
||||
aCascade2_R deltapi iScaledCascade2_R
|
||||
delayw aCascade1_R * iCascadeFeedback
|
||||
delayw aDry_R + (aCascade2_R * iCascadeFeedback)
|
||||
|
||||
aOut_R = aDry_R + aMultitap_R + aTapR + aComb_R + aReverse_R + aCascade1_R + aCascade2_R
|
||||
|
||||
outs aOut, aOut_R
|
||||
endin
|
||||
`;
|
||||
}
|
||||
|
||||
protected getParametersForCsound(params: DripwaterParams): CsoundParameter[] {
|
||||
return [
|
||||
{ channelName: 'baseFreq', value: params.baseFreq },
|
||||
{ channelName: 'dropRate', value: params.dropRate },
|
||||
{ channelName: 'numElements', value: params.numElements },
|
||||
{ channelName: 'damping', value: params.damping },
|
||||
{ channelName: 'shakeIntensity', value: params.shakeIntensity },
|
||||
{ channelName: 'freqRatio1', value: params.freqRatio1 },
|
||||
{ channelName: 'freqRatio2', value: params.freqRatio2 },
|
||||
{ channelName: 'freqRatio3', value: params.freqRatio3 },
|
||||
{ channelName: 'attack', value: params.attack },
|
||||
{ channelName: 'release', value: params.release },
|
||||
{ channelName: 'amplitude', value: params.amplitude },
|
||||
{ channelName: 'delayTime1', value: params.delayTime1 },
|
||||
{ channelName: 'delayTime2', value: params.delayTime2 },
|
||||
{ channelName: 'delayTime3', value: params.delayTime3 },
|
||||
{ channelName: 'delayTime4', value: params.delayTime4 },
|
||||
{ channelName: 'delayGain1', value: params.delayGain1 },
|
||||
{ channelName: 'delayGain2', value: params.delayGain2 },
|
||||
{ channelName: 'delayGain3', value: params.delayGain3 },
|
||||
{ channelName: 'delayGain4', value: params.delayGain4 },
|
||||
{ channelName: 'feedbackDelayTime', value: params.feedbackDelayTime },
|
||||
{ channelName: 'feedbackAmount', value: params.feedbackAmount },
|
||||
{ channelName: 'modDepth', value: params.modDepth },
|
||||
{ channelName: 'modRate', value: params.modRate },
|
||||
{ channelName: 'stereoSpread', value: params.stereoSpread },
|
||||
{ channelName: 'combDelayTime', value: params.combDelayTime },
|
||||
{ channelName: 'combFeedback', value: params.combFeedback },
|
||||
{ channelName: 'reverseDelayTime', value: params.reverseDelayTime },
|
||||
{ channelName: 'reverseGain', value: params.reverseGain },
|
||||
{ channelName: 'cascadeDelay1', value: params.cascadeDelay1 },
|
||||
{ channelName: 'cascadeDelay2', value: params.cascadeDelay2 },
|
||||
{ channelName: 'cascadeFeedback', value: params.cascadeFeedback },
|
||||
];
|
||||
}
|
||||
|
||||
randomParams(pitchLock?: PitchLock): DripwaterParams {
|
||||
const baseFreqChoices = [55, 82.4, 110, 146.8, 220, 293.7, 440, 587.3, 880];
|
||||
const baseFreq = pitchLock?.enabled
|
||||
? pitchLock.frequency
|
||||
: this.randomChoice(baseFreqChoices) * this.randomRange(0.95, 1.05);
|
||||
|
||||
return {
|
||||
baseFreq,
|
||||
dropRate: this.randomRange(2, 40),
|
||||
numElements: this.randomInt(4, 48),
|
||||
damping: this.randomRange(0.1, 0.85),
|
||||
shakeIntensity: this.randomRange(0.2, 0.9),
|
||||
freqRatio1: this.randomRange(1.0, 3.5),
|
||||
freqRatio2: this.randomRange(2.5, 7.0),
|
||||
freqRatio3: this.randomRange(5.0, 14.0),
|
||||
attack: this.randomRange(0.001, 0.04),
|
||||
release: this.randomRange(0.15, 0.9),
|
||||
amplitude: this.randomRange(0.6, 0.95),
|
||||
delayTime1: this.randomRange(0.1, 0.3),
|
||||
delayTime2: this.randomRange(0.25, 0.5),
|
||||
delayTime3: this.randomRange(0.45, 0.7),
|
||||
delayTime4: this.randomRange(0.65, 0.95),
|
||||
delayGain1: this.randomRange(0.4, 0.7),
|
||||
delayGain2: this.randomRange(0.3, 0.55),
|
||||
delayGain3: this.randomRange(0.2, 0.4),
|
||||
delayGain4: this.randomRange(0.1, 0.25),
|
||||
feedbackDelayTime: this.randomRange(0.15, 0.5),
|
||||
feedbackAmount: this.randomRange(0.3, 0.7),
|
||||
modDepth: this.randomRange(0.02, 0.12),
|
||||
modRate: this.randomRange(0.1, 3.0),
|
||||
stereoSpread: this.randomRange(0.85, 1.15),
|
||||
combDelayTime: this.randomRange(0.05, 0.25),
|
||||
combFeedback: this.randomRange(0.4, 0.8),
|
||||
reverseDelayTime: this.randomRange(0.3, 0.8),
|
||||
reverseGain: this.randomRange(0.15, 0.4),
|
||||
cascadeDelay1: this.randomRange(0.2, 0.45),
|
||||
cascadeDelay2: this.randomRange(0.35, 0.65),
|
||||
cascadeFeedback: this.randomRange(0.3, 0.65),
|
||||
};
|
||||
}
|
||||
|
||||
mutateParams(
|
||||
params: DripwaterParams,
|
||||
mutationAmount: number = 0.15,
|
||||
pitchLock?: PitchLock
|
||||
): DripwaterParams {
|
||||
const baseFreq = pitchLock?.enabled ? pitchLock.frequency : params.baseFreq;
|
||||
|
||||
return {
|
||||
baseFreq,
|
||||
dropRate: this.mutateValue(params.dropRate, mutationAmount, 1, 50),
|
||||
numElements: Math.round(this.mutateValue(params.numElements, mutationAmount, 2, 64)),
|
||||
damping: this.mutateValue(params.damping, mutationAmount, 0.0, 0.95),
|
||||
shakeIntensity: this.mutateValue(params.shakeIntensity, mutationAmount, 0.1, 1.0),
|
||||
freqRatio1: this.mutateValue(params.freqRatio1, mutationAmount, 1.0, 4.0),
|
||||
freqRatio2: this.mutateValue(params.freqRatio2, mutationAmount, 2.0, 8.0),
|
||||
freqRatio3: this.mutateValue(params.freqRatio3, mutationAmount, 4.0, 16.0),
|
||||
attack: this.mutateValue(params.attack, mutationAmount, 0.001, 0.05),
|
||||
release: this.mutateValue(params.release, mutationAmount, 0.1, 0.95),
|
||||
amplitude: this.mutateValue(params.amplitude, mutationAmount, 0.5, 1.0),
|
||||
delayTime1: this.mutateValue(params.delayTime1, mutationAmount, 0.05, 0.35),
|
||||
delayTime2: this.mutateValue(params.delayTime2, mutationAmount, 0.2, 0.6),
|
||||
delayTime3: this.mutateValue(params.delayTime3, mutationAmount, 0.4, 0.8),
|
||||
delayTime4: this.mutateValue(params.delayTime4, mutationAmount, 0.6, 0.98),
|
||||
delayGain1: this.mutateValue(params.delayGain1, mutationAmount, 0.3, 0.8),
|
||||
delayGain2: this.mutateValue(params.delayGain2, mutationAmount, 0.2, 0.65),
|
||||
delayGain3: this.mutateValue(params.delayGain3, mutationAmount, 0.1, 0.5),
|
||||
delayGain4: this.mutateValue(params.delayGain4, mutationAmount, 0.05, 0.35),
|
||||
feedbackDelayTime: this.mutateValue(params.feedbackDelayTime, mutationAmount, 0.1, 0.6),
|
||||
feedbackAmount: this.mutateValue(params.feedbackAmount, mutationAmount, 0.2, 0.8),
|
||||
modDepth: this.mutateValue(params.modDepth, mutationAmount, 0.01, 0.15),
|
||||
modRate: this.mutateValue(params.modRate, mutationAmount, 0.05, 4.0),
|
||||
stereoSpread: this.mutateValue(params.stereoSpread, mutationAmount, 0.7, 1.3),
|
||||
combDelayTime: this.mutateValue(params.combDelayTime, mutationAmount, 0.03, 0.3),
|
||||
combFeedback: this.mutateValue(params.combFeedback, mutationAmount, 0.3, 0.85),
|
||||
reverseDelayTime: this.mutateValue(params.reverseDelayTime, mutationAmount, 0.2, 0.9),
|
||||
reverseGain: this.mutateValue(params.reverseGain, mutationAmount, 0.1, 0.5),
|
||||
cascadeDelay1: this.mutateValue(params.cascadeDelay1, mutationAmount, 0.15, 0.5),
|
||||
cascadeDelay2: this.mutateValue(params.cascadeDelay2, mutationAmount, 0.3, 0.7),
|
||||
cascadeFeedback: this.mutateValue(params.cascadeFeedback, mutationAmount, 0.2, 0.75),
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
|
||||
532
src/lib/audio/engines/DustNoise.ts
Normal file
532
src/lib/audio/engines/DustNoise.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
184
src/lib/audio/engines/FMTomTom.ts
Normal file
184
src/lib/audio/engines/FMTomTom.ts
Normal file
@ -0,0 +1,184 @@
|
||||
import { CsoundEngine, type CsoundParameter } from './base/CsoundEngine';
|
||||
import type { PitchLock } from './base/SynthEngine';
|
||||
|
||||
interface FMTomTomParams {
|
||||
baseFreq: number;
|
||||
pitchBendAmount: number;
|
||||
pitchBendDecay: number;
|
||||
modIndex: number;
|
||||
modRatio: number;
|
||||
noiseHPFreq: number;
|
||||
noiseResonance: number;
|
||||
noiseMix: number;
|
||||
ampAttack: number;
|
||||
ampDecay: number;
|
||||
sustain: number;
|
||||
release: number;
|
||||
tonality: number;
|
||||
stereoDetune: number;
|
||||
}
|
||||
|
||||
export class FMTomTom extends CsoundEngine<FMTomTomParams> {
|
||||
getName(): string {
|
||||
return 'FM Tom-Tom';
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return 'High-pass filtered noise modulating a sine oscillator with pitch bend envelope simulating tom-tom membrane';
|
||||
}
|
||||
|
||||
getType() {
|
||||
return 'generative' as const;
|
||||
}
|
||||
|
||||
getCategory() {
|
||||
return 'Percussion' as const;
|
||||
}
|
||||
|
||||
protected getOrchestra(): string {
|
||||
return `
|
||||
instr 1
|
||||
iBaseFreq chnget "baseFreq"
|
||||
iPitchBendAmount chnget "pitchBendAmount"
|
||||
iPitchBendDecay chnget "pitchBendDecay"
|
||||
iModIndex chnget "modIndex"
|
||||
iModRatio chnget "modRatio"
|
||||
iNoiseHPFreq chnget "noiseHPFreq"
|
||||
iNoiseResonance chnget "noiseResonance"
|
||||
iNoiseMix chnget "noiseMix"
|
||||
iAmpAttack chnget "ampAttack"
|
||||
iAmpDecay chnget "ampDecay"
|
||||
iSustain chnget "sustain"
|
||||
iRelease chnget "release"
|
||||
iTonality chnget "tonality"
|
||||
iStereoDetune chnget "stereoDetune"
|
||||
|
||||
idur = p3
|
||||
iPitchBendTime = iPitchBendDecay * idur
|
||||
iAmpAttackTime = iAmpAttack * idur
|
||||
iAmpDecayTime = iAmpDecay * idur
|
||||
iReleaseTime = iRelease * idur
|
||||
|
||||
; Pitch bend envelope (simulates drum membrane tightening)
|
||||
; Starts at higher pitch and decays to base pitch
|
||||
iPitchStart = iBaseFreq * (1 + iPitchBendAmount)
|
||||
kPitchEnv expseg iPitchStart, iPitchBendTime, iBaseFreq, idur - iPitchBendTime, iBaseFreq
|
||||
|
||||
; Generate high-pass filtered noise for modulation
|
||||
aNoise noise 1, 0
|
||||
aNoiseHP butterhp aNoise, iNoiseHPFreq
|
||||
aNoiseFiltered butterbp aNoiseHP, iNoiseHPFreq * 2, iNoiseResonance
|
||||
|
||||
; Scale noise for FM modulation
|
||||
aNoiseScaled = aNoiseFiltered * iModIndex * kPitchEnv * iTonality
|
||||
|
||||
; FM synthesis: noise modulates sine oscillator
|
||||
aModulator oscili iModIndex * kPitchEnv, kPitchEnv * iModRatio
|
||||
aCarrier oscili 0.5, kPitchEnv + aModulator + aNoiseScaled
|
||||
|
||||
; Add direct noise component for more realistic tom sound
|
||||
aNoiseDirect = aNoiseFiltered * iNoiseMix * 0.3
|
||||
|
||||
; Mix carrier and noise
|
||||
aMix = aCarrier * (1 - iNoiseMix * 0.5) + aNoiseDirect
|
||||
|
||||
; Amplitude envelope (ADSR-like with fast attack and decay)
|
||||
kAmpEnv expseg 0.001, iAmpAttackTime, 1, iAmpDecayTime, iSustain, idur - iAmpAttackTime - iAmpDecayTime - iReleaseTime, iSustain, iReleaseTime, 0.001
|
||||
|
||||
; Apply amplitude envelope
|
||||
aOut = aMix * kAmpEnv
|
||||
|
||||
; Right channel with stereo detune
|
||||
iBaseFreqR = iBaseFreq * (1 + iStereoDetune * 0.02)
|
||||
iPitchStartR = iBaseFreqR * (1 + iPitchBendAmount)
|
||||
kPitchEnvR expseg iPitchStartR, iPitchBendTime, iBaseFreqR, idur - iPitchBendTime, iBaseFreqR
|
||||
|
||||
aNoiseR noise 1, 0
|
||||
aNoiseHPR butterhp aNoiseR, iNoiseHPFreq * (1 + iStereoDetune * 0.01)
|
||||
aNoiseFilteredR butterbp aNoiseHPR, iNoiseHPFreq * 2 * (1 + iStereoDetune * 0.01), iNoiseResonance
|
||||
|
||||
aNoiseScaledR = aNoiseFilteredR * iModIndex * kPitchEnvR * iTonality
|
||||
aModulatorR oscili iModIndex * kPitchEnvR, kPitchEnvR * iModRatio
|
||||
aCarrierR oscili 0.5, kPitchEnvR + aModulatorR + aNoiseScaledR
|
||||
|
||||
aNoiseDirectR = aNoiseFilteredR * iNoiseMix * 0.3
|
||||
aMixR = aCarrierR * (1 - iNoiseMix * 0.5) + aNoiseDirectR
|
||||
aOutR = aMixR * kAmpEnv
|
||||
|
||||
outs aOut, aOutR
|
||||
endin
|
||||
`;
|
||||
}
|
||||
|
||||
protected getParametersForCsound(params: FMTomTomParams): CsoundParameter[] {
|
||||
return [
|
||||
{ channelName: 'baseFreq', value: params.baseFreq },
|
||||
{ channelName: 'pitchBendAmount', value: params.pitchBendAmount },
|
||||
{ channelName: 'pitchBendDecay', value: params.pitchBendDecay },
|
||||
{ channelName: 'modIndex', value: params.modIndex },
|
||||
{ channelName: 'modRatio', value: params.modRatio },
|
||||
{ channelName: 'noiseHPFreq', value: params.noiseHPFreq },
|
||||
{ channelName: 'noiseResonance', value: params.noiseResonance },
|
||||
{ channelName: 'noiseMix', value: params.noiseMix },
|
||||
{ channelName: 'ampAttack', value: params.ampAttack },
|
||||
{ channelName: 'ampDecay', value: params.ampDecay },
|
||||
{ channelName: 'sustain', value: params.sustain },
|
||||
{ channelName: 'release', value: params.release },
|
||||
{ channelName: 'tonality', value: params.tonality },
|
||||
{ channelName: 'stereoDetune', value: params.stereoDetune },
|
||||
];
|
||||
}
|
||||
|
||||
randomParams(pitchLock?: PitchLock): FMTomTomParams {
|
||||
const baseFreqChoices = [80, 100, 120, 150, 180, 220, 260, 300];
|
||||
const baseFreq = pitchLock?.enabled
|
||||
? pitchLock.frequency
|
||||
: this.randomChoice(baseFreqChoices) * this.randomRange(0.9, 1.1);
|
||||
|
||||
const modRatios = [0.5, 1, 1.5, 2, 2.5, 3];
|
||||
|
||||
return {
|
||||
baseFreq,
|
||||
pitchBendAmount: this.randomRange(0.2, 0.8),
|
||||
pitchBendDecay: this.randomRange(0.05, 0.2),
|
||||
modIndex: this.randomRange(1, 8),
|
||||
modRatio: this.randomChoice(modRatios),
|
||||
noiseHPFreq: this.randomRange(200, 800),
|
||||
noiseResonance: this.randomRange(20, 100),
|
||||
noiseMix: this.randomRange(0.1, 0.6),
|
||||
ampAttack: this.randomRange(0.001, 0.01),
|
||||
ampDecay: this.randomRange(0.1, 0.3),
|
||||
sustain: this.randomRange(0.2, 0.6),
|
||||
release: this.randomRange(0.2, 0.5),
|
||||
tonality: this.randomRange(0.3, 0.9),
|
||||
stereoDetune: this.randomRange(0, 0.5),
|
||||
};
|
||||
}
|
||||
|
||||
mutateParams(
|
||||
params: FMTomTomParams,
|
||||
mutationAmount: number = 0.15,
|
||||
pitchLock?: PitchLock
|
||||
): FMTomTomParams {
|
||||
const baseFreq = pitchLock?.enabled ? pitchLock.frequency : params.baseFreq;
|
||||
const modRatios = [0.5, 1, 1.5, 2, 2.5, 3];
|
||||
|
||||
return {
|
||||
baseFreq,
|
||||
pitchBendAmount: this.mutateValue(params.pitchBendAmount, mutationAmount, 0.1, 1),
|
||||
pitchBendDecay: this.mutateValue(params.pitchBendDecay, mutationAmount, 0.02, 0.4),
|
||||
modIndex: this.mutateValue(params.modIndex, mutationAmount, 0.5, 12),
|
||||
modRatio:
|
||||
Math.random() < 0.15 ? this.randomChoice(modRatios) : params.modRatio,
|
||||
noiseHPFreq: this.mutateValue(params.noiseHPFreq, mutationAmount, 100, 1200),
|
||||
noiseResonance: this.mutateValue(params.noiseResonance, mutationAmount, 15, 150),
|
||||
noiseMix: this.mutateValue(params.noiseMix, mutationAmount, 0, 0.8),
|
||||
ampAttack: this.mutateValue(params.ampAttack, mutationAmount, 0.001, 0.02),
|
||||
ampDecay: this.mutateValue(params.ampDecay, mutationAmount, 0.05, 0.5),
|
||||
sustain: this.mutateValue(params.sustain, mutationAmount, 0.1, 0.8),
|
||||
release: this.mutateValue(params.release, mutationAmount, 0.1, 0.7),
|
||||
tonality: this.mutateValue(params.tonality, mutationAmount, 0.1, 1),
|
||||
stereoDetune: this.mutateValue(params.stereoDetune, mutationAmount, 0, 1),
|
||||
};
|
||||
}
|
||||
}
|
||||
247
src/lib/audio/engines/FeedbackSnare.ts
Normal file
247
src/lib/audio/engines/FeedbackSnare.ts
Normal file
@ -0,0 +1,247 @@
|
||||
import { CsoundEngine, type CsoundParameter } from './base/CsoundEngine';
|
||||
import type { PitchLock } from './base/SynthEngine';
|
||||
|
||||
interface FeedbackSnareParams {
|
||||
baseFreq: number;
|
||||
tonalDecay: number;
|
||||
noiseDecay: number;
|
||||
toneResonance: number;
|
||||
springDecay: number;
|
||||
springTone: number;
|
||||
pitchBend: number;
|
||||
pitchBendSpeed: number;
|
||||
pulseRate: number;
|
||||
feedbackAmount: number;
|
||||
delayTime: number;
|
||||
crossFeedMix: number;
|
||||
snap: number;
|
||||
brightness: number;
|
||||
}
|
||||
|
||||
export class FeedbackSnare extends CsoundEngine<FeedbackSnareParams> {
|
||||
getName(): string {
|
||||
return 'Feedback Snare';
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return 'Complex snare using cross-feedback delay network with pulsed noise modulation';
|
||||
}
|
||||
|
||||
getType() {
|
||||
return 'generative' as const;
|
||||
}
|
||||
|
||||
getCategory() {
|
||||
return 'Percussion' as const;
|
||||
}
|
||||
|
||||
protected getOrchestra(): string {
|
||||
return `
|
||||
instr 1
|
||||
iBaseFreq chnget "baseFreq"
|
||||
iTonalDecay chnget "tonalDecay"
|
||||
iNoiseDecay chnget "noiseDecay"
|
||||
iToneResonance chnget "toneResonance"
|
||||
iSpringDecay chnget "springDecay"
|
||||
iSpringTone chnget "springTone"
|
||||
iPitchBend chnget "pitchBend"
|
||||
iPitchBendSpeed chnget "pitchBendSpeed"
|
||||
iPulseRate chnget "pulseRate"
|
||||
iFeedbackAmount chnget "feedbackAmount"
|
||||
iDelayTime chnget "delayTime"
|
||||
iCrossFeedMix chnget "crossFeedMix"
|
||||
iSnap chnget "snap"
|
||||
iBrightness chnget "brightness"
|
||||
|
||||
idur = p3
|
||||
iTonalDecayTime = iTonalDecay * idur
|
||||
iNoiseDecayTime = iNoiseDecay * idur
|
||||
iSpringDecayTime = iSpringDecay * idur
|
||||
iPitchBendTime = iPitchBendSpeed * idur
|
||||
|
||||
; Pitch envelope with bend
|
||||
kPitchEnv expseg iBaseFreq * (1 + iPitchBend * 2), iPitchBendTime, iBaseFreq, idur - iPitchBendTime, iBaseFreq * 0.95
|
||||
|
||||
; Generate square wave pulse for tonal component
|
||||
aPulse vco2 0.5, kPitchEnv, 2, 0.5
|
||||
|
||||
; Tonal envelope
|
||||
kToneEnv expseg 1, iTonalDecayTime, 0.001, idur - iTonalDecayTime, 0.001
|
||||
aTonal = aPulse * kToneEnv
|
||||
|
||||
; Apply drum tone resonant filter
|
||||
aDrumTone rezzy aTonal, kPitchEnv, iToneResonance
|
||||
|
||||
; Generate white noise
|
||||
aNoise noise 1, 0
|
||||
|
||||
; Pulse modulation of noise (creates rhythmic texture)
|
||||
kPulseMod oscili 1, iPulseRate
|
||||
kPulseMod = (kPulseMod + 1) * 0.5
|
||||
aNoiseModulated = aNoise * kPulseMod
|
||||
|
||||
; Noise envelope
|
||||
kNoiseEnv expseg 1, iNoiseDecayTime, 0.001, idur - iNoiseDecayTime, 0.001
|
||||
|
||||
; Parallel filters on noise
|
||||
; Bandpass filter 1 (body)
|
||||
aNoiseBody butterbp aNoiseModulated, iBaseFreq * 1.5, 100
|
||||
; Bandpass filter 2 (mid)
|
||||
aNoiseMid butterbp aNoiseModulated, iBaseFreq * 3, 200
|
||||
; Highpass filter (crispness)
|
||||
aNoiseHigh butterhp aNoiseModulated, 3000
|
||||
|
||||
; Mix noise components
|
||||
aNoiseMix = (aNoiseBody * 0.4 + aNoiseMid * 0.3 + aNoiseHigh * 0.3 * iBrightness) * kNoiseEnv
|
||||
|
||||
; Mix tonal and noise
|
||||
aMix = aDrumTone * 0.5 + aNoiseMix * 0.5
|
||||
|
||||
; Cross-feedback delay network (simulates spring/snare wires)
|
||||
; Create two delay lines that feed back into each other
|
||||
aDelay1Init init 0
|
||||
aDelay2Init init 0
|
||||
|
||||
; Spring tone filter (for the delayed signal)
|
||||
iSpringFreq = 800 + iSpringTone * 4000
|
||||
|
||||
; Delay line 1
|
||||
aDelayIn1 = aMix + aDelay2Init * iFeedbackAmount * iCrossFeedMix
|
||||
aDelay1 vdelay aDelayIn1, iDelayTime * 1000, 50
|
||||
aDelay1Filt butterbp aDelay1, iSpringFreq, 100
|
||||
aDelay1Out = aDelay1Filt * exp(-p3 / iSpringDecayTime)
|
||||
|
||||
; Delay line 2
|
||||
aDelayIn2 = aMix + aDelay1Out * iFeedbackAmount
|
||||
aDelay2 vdelay aDelayIn2, iDelayTime * 1.3 * 1000, 50
|
||||
aDelay2Filt butterbp aDelay2, iSpringFreq * 1.2, 120
|
||||
aDelay2Out = aDelay2Filt * exp(-p3 / iSpringDecayTime)
|
||||
|
||||
; Update feedback
|
||||
aDelay1Init = aDelay1Out
|
||||
aDelay2Init = aDelay2Out
|
||||
|
||||
; Mix dry and delay
|
||||
aOut = aMix * 0.6 + aDelay1Out * 0.2 + aDelay2Out * 0.2
|
||||
|
||||
; Add snap transient
|
||||
if iSnap > 0.1 then
|
||||
kSnapEnv linseg 1, 0.003, 0, idur - 0.003, 0
|
||||
aSnap noise iSnap * 0.5, 0
|
||||
aSnapFilt butterhp aSnap, 8000
|
||||
aOut = aOut + aSnapFilt * kSnapEnv
|
||||
endif
|
||||
|
||||
; Final output scaling
|
||||
aOut = aOut * 0.4
|
||||
|
||||
; Right channel with slightly different parameters
|
||||
aPulseR vco2 0.5, kPitchEnv * 1.002, 2, 0.5
|
||||
aTonalR = aPulseR * kToneEnv
|
||||
aDrumToneR rezzy aTonalR, kPitchEnv * 1.002, iToneResonance
|
||||
|
||||
aNoiseR noise 1, 0
|
||||
aNoiseModulatedR = aNoiseR * kPulseMod
|
||||
aNoiseBodyR butterbp aNoiseModulatedR, iBaseFreq * 1.52, 100
|
||||
aNoiseMidR butterbp aNoiseModulatedR, iBaseFreq * 3.03, 200
|
||||
aNoiseHighR butterhp aNoiseModulatedR, 3100
|
||||
|
||||
aNoiseMixR = (aNoiseBodyR * 0.4 + aNoiseMidR * 0.3 + aNoiseHighR * 0.3 * iBrightness) * kNoiseEnv
|
||||
aMixR = aDrumToneR * 0.5 + aNoiseMixR * 0.5
|
||||
|
||||
aDelay1InitR init 0
|
||||
aDelay2InitR init 0
|
||||
|
||||
aDelayIn1R = aMixR + aDelay2InitR * iFeedbackAmount * iCrossFeedMix
|
||||
aDelay1R vdelay aDelayIn1R, iDelayTime * 1.05 * 1000, 50
|
||||
aDelay1FiltR butterbp aDelay1R, iSpringFreq * 1.01, 100
|
||||
aDelay1OutR = aDelay1FiltR * exp(-p3 / iSpringDecayTime)
|
||||
|
||||
aDelayIn2R = aMixR + aDelay1OutR * iFeedbackAmount
|
||||
aDelay2R vdelay aDelayIn2R, iDelayTime * 1.35 * 1000, 50
|
||||
aDelay2FiltR butterbp aDelay2R, iSpringFreq * 1.22, 120
|
||||
aDelay2OutR = aDelay2FiltR * exp(-p3 / iSpringDecayTime)
|
||||
|
||||
aDelay1InitR = aDelay1OutR
|
||||
aDelay2InitR = aDelay2OutR
|
||||
|
||||
aOutR = aMixR * 0.6 + aDelay1OutR * 0.2 + aDelay2OutR * 0.2
|
||||
|
||||
if iSnap > 0.1 then
|
||||
aSnapR noise iSnap * 0.5, 0
|
||||
aSnapFiltR butterhp aSnapR, 8100
|
||||
aOutR = aOutR + aSnapFiltR * kSnapEnv
|
||||
endif
|
||||
|
||||
aOutR = aOutR * 0.4
|
||||
|
||||
outs aOut, aOutR
|
||||
endin
|
||||
`;
|
||||
}
|
||||
|
||||
protected getParametersForCsound(params: FeedbackSnareParams): CsoundParameter[] {
|
||||
return [
|
||||
{ channelName: 'baseFreq', value: params.baseFreq },
|
||||
{ channelName: 'tonalDecay', value: params.tonalDecay },
|
||||
{ channelName: 'noiseDecay', value: params.noiseDecay },
|
||||
{ channelName: 'toneResonance', value: params.toneResonance },
|
||||
{ channelName: 'springDecay', value: params.springDecay },
|
||||
{ channelName: 'springTone', value: params.springTone },
|
||||
{ channelName: 'pitchBend', value: params.pitchBend },
|
||||
{ channelName: 'pitchBendSpeed', value: params.pitchBendSpeed },
|
||||
{ channelName: 'pulseRate', value: params.pulseRate },
|
||||
{ channelName: 'feedbackAmount', value: params.feedbackAmount },
|
||||
{ channelName: 'delayTime', value: params.delayTime },
|
||||
{ channelName: 'crossFeedMix', value: params.crossFeedMix },
|
||||
{ channelName: 'snap', value: params.snap },
|
||||
{ channelName: 'brightness', value: params.brightness },
|
||||
];
|
||||
}
|
||||
|
||||
randomParams(pitchLock?: PitchLock): FeedbackSnareParams {
|
||||
const baseFreq = pitchLock?.enabled ? pitchLock.frequency : this.randomRange(150, 350);
|
||||
|
||||
return {
|
||||
baseFreq,
|
||||
tonalDecay: this.randomRange(0.1, 0.3),
|
||||
noiseDecay: this.randomRange(0.3, 0.7),
|
||||
toneResonance: this.randomRange(5, 25),
|
||||
springDecay: this.randomRange(0.2, 0.6),
|
||||
springTone: this.randomRange(0.2, 0.8),
|
||||
pitchBend: this.randomRange(0.3, 0.9),
|
||||
pitchBendSpeed: this.randomRange(0.01, 0.05),
|
||||
pulseRate: this.randomRange(50, 300),
|
||||
feedbackAmount: this.randomRange(0.3, 0.7),
|
||||
delayTime: this.randomRange(0.005, 0.025),
|
||||
crossFeedMix: this.randomRange(0.4, 0.9),
|
||||
snap: this.randomRange(0, 0.6),
|
||||
brightness: this.randomRange(0.3, 0.9),
|
||||
};
|
||||
}
|
||||
|
||||
mutateParams(
|
||||
params: FeedbackSnareParams,
|
||||
mutationAmount: number = 0.15,
|
||||
pitchLock?: PitchLock
|
||||
): FeedbackSnareParams {
|
||||
const baseFreq = pitchLock?.enabled ? pitchLock.frequency : params.baseFreq;
|
||||
|
||||
return {
|
||||
baseFreq,
|
||||
tonalDecay: this.mutateValue(params.tonalDecay, mutationAmount, 0.05, 0.5),
|
||||
noiseDecay: this.mutateValue(params.noiseDecay, mutationAmount, 0.2, 0.9),
|
||||
toneResonance: this.mutateValue(params.toneResonance, mutationAmount, 2, 40),
|
||||
springDecay: this.mutateValue(params.springDecay, mutationAmount, 0.1, 0.8),
|
||||
springTone: this.mutateValue(params.springTone, mutationAmount, 0, 1),
|
||||
pitchBend: this.mutateValue(params.pitchBend, mutationAmount, 0.1, 1.2),
|
||||
pitchBendSpeed: this.mutateValue(params.pitchBendSpeed, mutationAmount, 0.005, 0.1),
|
||||
pulseRate: this.mutateValue(params.pulseRate, mutationAmount, 20, 500),
|
||||
feedbackAmount: this.mutateValue(params.feedbackAmount, mutationAmount, 0.1, 0.85),
|
||||
delayTime: this.mutateValue(params.delayTime, mutationAmount, 0.003, 0.04),
|
||||
crossFeedMix: this.mutateValue(params.crossFeedMix, mutationAmount, 0.2, 1),
|
||||
snap: this.mutateValue(params.snap, mutationAmount, 0, 0.8),
|
||||
brightness: this.mutateValue(params.brightness, mutationAmount, 0, 1),
|
||||
};
|
||||
}
|
||||
}
|
||||
296
src/lib/audio/engines/Form1.ts
Normal file
296
src/lib/audio/engines/Form1.ts
Normal file
@ -0,0 +1,296 @@
|
||||
import { CsoundEngine, type CsoundParameter } from './base/CsoundEngine';
|
||||
import type { PitchLock } from './base/SynthEngine';
|
||||
|
||||
interface Form1Params {
|
||||
frequency: number;
|
||||
vibratoRate: number;
|
||||
vibratoDepth: number;
|
||||
formant1Freq: number;
|
||||
formant1BW: number;
|
||||
formant1Amp: number;
|
||||
formant2Freq: number;
|
||||
formant2BW: number;
|
||||
formant2Amp: number;
|
||||
formant3Freq: number;
|
||||
formant3BW: number;
|
||||
formant3Amp: number;
|
||||
formant4Freq: number;
|
||||
formant4BW: number;
|
||||
formant4Amp: number;
|
||||
formant5Freq: number;
|
||||
formant5BW: number;
|
||||
formant5Amp: number;
|
||||
attack: number;
|
||||
decay: number;
|
||||
sustain: number;
|
||||
release: number;
|
||||
}
|
||||
|
||||
export class Form1 extends CsoundEngine<Form1Params> {
|
||||
getName(): string {
|
||||
return 'Throat';
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return 'Multi-voice formant vocal synthesizer with deep sub-bass and wide stereo field';
|
||||
}
|
||||
|
||||
getType(): 'generative' | 'sample' | 'input' {
|
||||
return 'generative';
|
||||
}
|
||||
|
||||
getCategory(): 'Additive' | 'Subtractive' | 'FM' | 'Percussion' | 'Noise' | 'Physical' | 'Modulation' | 'Experimental' | 'Utility' {
|
||||
return 'Physical';
|
||||
}
|
||||
|
||||
getOrchestra(): string {
|
||||
return `
|
||||
instr 1
|
||||
ifreq = chnget("frequency")
|
||||
iamp = 0.3
|
||||
ivibratoRate = chnget("vibratoRate")
|
||||
ivibratoDepth = chnget("vibratoDepth")
|
||||
|
||||
if1freq = chnget("formant1Freq")
|
||||
if1bw = chnget("formant1BW")
|
||||
if1amp = chnget("formant1Amp")
|
||||
if2freq = chnget("formant2Freq")
|
||||
if2bw = chnget("formant2BW")
|
||||
if2amp = chnget("formant2Amp")
|
||||
if3freq = chnget("formant3Freq")
|
||||
if3bw = chnget("formant3BW")
|
||||
if3amp = chnget("formant3Amp")
|
||||
if4freq = chnget("formant4Freq")
|
||||
if4bw = chnget("formant4BW")
|
||||
if4amp = chnget("formant4Amp")
|
||||
if5freq = chnget("formant5Freq")
|
||||
if5bw = chnget("formant5BW")
|
||||
if5amp = chnget("formant5Amp")
|
||||
|
||||
iatt = chnget("attack") * p3
|
||||
idec = chnget("decay") * p3
|
||||
isus = chnget("sustain")
|
||||
irel = chnget("release") * p3
|
||||
|
||||
; Amplitude envelope
|
||||
aenv = madsr(iatt, idec, isus, irel)
|
||||
|
||||
; Dual vibrato LFOs for stereo width
|
||||
kvibL = lfo(ivibratoDepth, ivibratoRate)
|
||||
kvibR = lfo(ivibratoDepth, ivibratoRate * 1.03)
|
||||
|
||||
; === LEFT CHANNEL VOICES ===
|
||||
; Voice 1L: Main voice
|
||||
kfreq1L = ifreq * (1 + kvibL)
|
||||
inumharm1L = int(sr / (2 * ifreq))
|
||||
asig1L = buzz(1, kfreq1L, inumharm1L, -1)
|
||||
|
||||
; Voice 2L: Slightly detuned (0.987x for beating)
|
||||
kfreq2L = ifreq * 0.987 * (1 + kvibL * 1.02)
|
||||
inumharm2L = int(sr / (2 * ifreq * 0.987))
|
||||
asig2L = buzz(0.85, kfreq2L, inumharm2L, -1)
|
||||
|
||||
; Voice 3L: Slightly detuned up (1.013x for beating)
|
||||
kfreq3L = ifreq * 1.013 * (1 + kvibL * 0.98)
|
||||
inumharm3L = int(sr / (2 * ifreq * 1.013))
|
||||
asig3L = buzz(0.8, kfreq3L, inumharm3L, -1)
|
||||
|
||||
; Voice 4L: Octave down
|
||||
kfreq4L = ifreq * 0.5 * (1 + kvibL * 1.04)
|
||||
inumharm4L = int(sr / (2 * ifreq * 0.5))
|
||||
asig4L = buzz(0.65, kfreq4L, inumharm4L, -1)
|
||||
|
||||
; Voice 5L: Octave up
|
||||
kfreq5L = ifreq * 2 * (1 + kvibL * 0.96)
|
||||
inumharm5L = int(sr / (2 * ifreq * 2))
|
||||
asig5L = buzz(0.45, kfreq5L, inumharm5L, -1)
|
||||
|
||||
; === RIGHT CHANNEL VOICES (different detuning) ===
|
||||
; Voice 1R: Main voice slightly different
|
||||
kfreq1R = ifreq * 1.002 * (1 + kvibR)
|
||||
inumharm1R = int(sr / (2 * ifreq * 1.002))
|
||||
asig1R = buzz(1, kfreq1R, inumharm1R, -1)
|
||||
|
||||
; Voice 2R: Different detuning (0.993x)
|
||||
kfreq2R = ifreq * 0.993 * (1 + kvibR * 1.01)
|
||||
inumharm2R = int(sr / (2 * ifreq * 0.993))
|
||||
asig2R = buzz(0.85, kfreq2R, inumharm2R, -1)
|
||||
|
||||
; Voice 3R: Different detuning up (1.007x)
|
||||
kfreq3R = ifreq * 1.007 * (1 + kvibR * 0.99)
|
||||
inumharm3R = int(sr / (2 * ifreq * 1.007))
|
||||
asig3R = buzz(0.8, kfreq3R, inumharm3R, -1)
|
||||
|
||||
; Voice 4R: Octave down different phase
|
||||
kfreq4R = ifreq * 0.501 * (1 + kvibR * 1.06)
|
||||
inumharm4R = int(sr / (2 * ifreq * 0.501))
|
||||
asig4R = buzz(0.6, kfreq4R, inumharm4R, -1)
|
||||
|
||||
; Voice 5R: Octave up different phase
|
||||
kfreq5R = ifreq * 1.998 * (1 + kvibR * 0.94)
|
||||
inumharm5R = int(sr / (2 * ifreq * 1.998))
|
||||
asig5R = buzz(0.4, kfreq5R, inumharm5R, -1)
|
||||
|
||||
; Sub-bass: Buzz oscillator one octave below with harmonic content
|
||||
kfreqSubL = ifreq * 0.5 * (1 + kvibL * 0.5)
|
||||
kfreqSubR = ifreq * 0.502 * (1 + kvibR * 0.5)
|
||||
inumharmSubL = int(sr / (2 * ifreq * 0.5))
|
||||
inumharmSubR = int(sr / (2 * ifreq * 0.502))
|
||||
aSubRawL = buzz(1, kfreqSubL, inumharmSubL, -1)
|
||||
aSubRawR = buzz(1, kfreqSubR, inumharmSubR, -1)
|
||||
|
||||
; Apply very wide formants for vocal character with depth
|
||||
aSubF1L = butterbp(aSubRawL, if1freq * 0.6, if1bw * 8)
|
||||
aSubF2L = butterbp(aSubRawL, if2freq * 0.7, if2bw * 10)
|
||||
aSubF3L = butterbp(aSubRawL, if3freq * 0.65, if3bw * 9)
|
||||
aSubF1R = butterbp(aSubRawR, if1freq * 0.62, if1bw * 8.5)
|
||||
aSubF2R = butterbp(aSubRawR, if2freq * 0.68, if2bw * 10.5)
|
||||
aSubF3R = butterbp(aSubRawR, if3freq * 0.67, if3bw * 9.5)
|
||||
|
||||
; Mix sub with formants for depth and complexity
|
||||
aSubMixL = (aSubRawL * 0.3) + (aSubF1L * 0.3) + (aSubF2L * 0.25) + (aSubF3L * 0.15)
|
||||
aSubMixR = (aSubRawR * 0.3) + (aSubF1R * 0.3) + (aSubF2R * 0.25) + (aSubF3R * 0.15)
|
||||
|
||||
; Add gentle low-pass for warmth and smooth out harsh frequencies
|
||||
aSubL = butterlp(aSubMixL, 1200)
|
||||
aSubR = butterlp(aSubMixR, 1250)
|
||||
|
||||
; Scale sub-bass
|
||||
aSubL = aSubL * 0.8
|
||||
aSubR = aSubR * 0.8
|
||||
|
||||
; Mix voices per channel (sub added separately later)
|
||||
asigMixL = asig1L + asig2L + asig3L + asig4L + asig5L
|
||||
asigMixR = asig1R + asig2R + asig3R + asig4R + asig5R
|
||||
|
||||
; === LEFT CHANNEL FORMANTS ===
|
||||
; Main formants
|
||||
a1L = butterbp(asigMixL * if1amp, if1freq, if1bw)
|
||||
a2L = butterbp(asigMixL * if2amp, if2freq, if2bw)
|
||||
a3L = butterbp(asigMixL * if3amp, if3freq, if3bw)
|
||||
a4L = butterbp(asigMixL * if4amp, if4freq, if4bw)
|
||||
a5L = butterbp(asigMixL * if5amp, if5freq, if5bw)
|
||||
|
||||
; Additional formant layers
|
||||
a1bL = butterbp(asigMixL * if1amp * 0.35, if1freq * 1.025, if1bw * 1.12)
|
||||
a2bL = butterbp(asigMixL * if2amp * 0.3, if2freq * 0.975, if2bw * 1.18)
|
||||
a3bL = butterbp(asigMixL * if3amp * 0.25, if3freq * 1.035, if3bw * 1.1)
|
||||
|
||||
; === RIGHT CHANNEL FORMANTS (different shifts) ===
|
||||
; Main formants
|
||||
a1R = butterbp(asigMixR * if1amp, if1freq * 1.005, if1bw * 1.02)
|
||||
a2R = butterbp(asigMixR * if2amp, if2freq * 0.995, if2bw * 1.03)
|
||||
a3R = butterbp(asigMixR * if3amp, if3freq * 1.008, if3bw * 1.01)
|
||||
a4R = butterbp(asigMixR * if4amp, if4freq * 0.997, if4bw * 1.04)
|
||||
a5R = butterbp(asigMixR * if5amp, if5freq * 1.003, if5bw * 1.02)
|
||||
|
||||
; Additional formant layers
|
||||
a1bR = butterbp(asigMixR * if1amp * 0.3, if1freq * 0.98, if1bw * 1.15)
|
||||
a2bR = butterbp(asigMixR * if2amp * 0.28, if2freq * 1.022, if2bw * 1.2)
|
||||
a3bR = butterbp(asigMixR * if3amp * 0.22, if3freq * 0.97, if3bw * 1.12)
|
||||
|
||||
; Combine formants per channel
|
||||
asigOutL = a1L + a2L + a3L + a4L + a5L + a1bL + a2bL + a3bL
|
||||
asigOutR = a1R + a2R + a3R + a4R + a5R + a1bR + a2bR + a3bR
|
||||
|
||||
; Apply envelope and level
|
||||
asigFormantL = asigOutL * aenv * iamp * 4
|
||||
asigFormantR = asigOutR * aenv * iamp * 4
|
||||
|
||||
; Add sub-bass directly (bypassing formants)
|
||||
aL = asigFormantL + (aSubL * aenv)
|
||||
aR = asigFormantR + (aSubR * aenv)
|
||||
|
||||
outs aL, aR
|
||||
endin
|
||||
`;
|
||||
}
|
||||
|
||||
getParametersForCsound(params: Form1Params): CsoundParameter[] {
|
||||
return [
|
||||
{ channelName: 'frequency', value: params.frequency },
|
||||
{ channelName: 'vibratoRate', value: params.vibratoRate },
|
||||
{ channelName: 'vibratoDepth', value: params.vibratoDepth },
|
||||
{ channelName: 'formant1Freq', value: params.formant1Freq },
|
||||
{ channelName: 'formant1BW', value: params.formant1BW },
|
||||
{ channelName: 'formant1Amp', value: params.formant1Amp },
|
||||
{ channelName: 'formant2Freq', value: params.formant2Freq },
|
||||
{ channelName: 'formant2BW', value: params.formant2BW },
|
||||
{ channelName: 'formant2Amp', value: params.formant2Amp },
|
||||
{ channelName: 'formant3Freq', value: params.formant3Freq },
|
||||
{ channelName: 'formant3BW', value: params.formant3BW },
|
||||
{ channelName: 'formant3Amp', value: params.formant3Amp },
|
||||
{ channelName: 'formant4Freq', value: params.formant4Freq },
|
||||
{ channelName: 'formant4BW', value: params.formant4BW },
|
||||
{ channelName: 'formant4Amp', value: params.formant4Amp },
|
||||
{ channelName: 'formant5Freq', value: params.formant5Freq },
|
||||
{ channelName: 'formant5BW', value: params.formant5BW },
|
||||
{ channelName: 'formant5Amp', value: params.formant5Amp },
|
||||
{ channelName: 'attack', value: params.attack },
|
||||
{ channelName: 'decay', value: params.decay },
|
||||
{ channelName: 'sustain', value: params.sustain },
|
||||
{ channelName: 'release', value: params.release }
|
||||
];
|
||||
}
|
||||
|
||||
randomParams(pitchLock?: PitchLock): Form1Params {
|
||||
const frequency = pitchLock?.enabled ? pitchLock.frequency : 55 * Math.pow(2, Math.random() * 4);
|
||||
|
||||
return {
|
||||
frequency,
|
||||
vibratoRate: 2 + Math.random() * 6,
|
||||
vibratoDepth: 0.001 + Math.random() * 0.008,
|
||||
formant1Freq: 400 + Math.random() * 800,
|
||||
formant1BW: 40 + Math.random() * 100,
|
||||
formant1Amp: 0.8 + Math.random() * 0.2,
|
||||
formant2Freq: 800 + Math.random() * 600,
|
||||
formant2BW: 50 + Math.random() * 100,
|
||||
formant2Amp: 0.4 + Math.random() * 0.4,
|
||||
formant3Freq: 2000 + Math.random() * 1500,
|
||||
formant3BW: 80 + Math.random() * 120,
|
||||
formant3Amp: 0.05 + Math.random() * 0.15,
|
||||
formant4Freq: 3000 + Math.random() * 1500,
|
||||
formant4BW: 100 + Math.random() * 100,
|
||||
formant4Amp: 0.1 + Math.random() * 0.2,
|
||||
formant5Freq: 4000 + Math.random() * 1500,
|
||||
formant5BW: 100 + Math.random() * 150,
|
||||
formant5Amp: 0.01 + Math.random() * 0.1,
|
||||
attack: Math.random() * 0.15,
|
||||
decay: 0.05 + Math.random() * 0.3,
|
||||
sustain: 0.3 + Math.random() * 0.5,
|
||||
release: 0.05 + Math.random() * 0.4
|
||||
};
|
||||
}
|
||||
|
||||
mutateParams(params: Form1Params, mutationAmount = 0.2, pitchLock?: PitchLock): Form1Params {
|
||||
const mutate = (value: number, min: number, max: number) => {
|
||||
const change = (Math.random() - 0.5) * 2 * mutationAmount * (max - min);
|
||||
return Math.max(min, Math.min(max, value + change));
|
||||
};
|
||||
|
||||
return {
|
||||
frequency: pitchLock?.enabled ? pitchLock.frequency : mutate(params.frequency, 55, 55 * Math.pow(2, 4)),
|
||||
vibratoRate: mutate(params.vibratoRate, 2, 8),
|
||||
vibratoDepth: mutate(params.vibratoDepth, 0.001, 0.009),
|
||||
formant1Freq: mutate(params.formant1Freq, 400, 1200),
|
||||
formant1BW: mutate(params.formant1BW, 40, 140),
|
||||
formant1Amp: mutate(params.formant1Amp, 0.8, 1.0),
|
||||
formant2Freq: mutate(params.formant2Freq, 800, 1400),
|
||||
formant2BW: mutate(params.formant2BW, 50, 150),
|
||||
formant2Amp: mutate(params.formant2Amp, 0.4, 0.8),
|
||||
formant3Freq: mutate(params.formant3Freq, 2000, 3500),
|
||||
formant3BW: mutate(params.formant3BW, 80, 200),
|
||||
formant3Amp: mutate(params.formant3Amp, 0.05, 0.2),
|
||||
formant4Freq: mutate(params.formant4Freq, 3000, 4500),
|
||||
formant4BW: mutate(params.formant4BW, 100, 200),
|
||||
formant4Amp: mutate(params.formant4Amp, 0.1, 0.3),
|
||||
formant5Freq: mutate(params.formant5Freq, 4000, 5500),
|
||||
formant5BW: mutate(params.formant5BW, 100, 250),
|
||||
formant5Amp: mutate(params.formant5Amp, 0.01, 0.11),
|
||||
attack: mutate(params.attack, 0, 0.15),
|
||||
decay: mutate(params.decay, 0.05, 0.35),
|
||||
sustain: mutate(params.sustain, 0.3, 0.8),
|
||||
release: mutate(params.release, 0.05, 0.45)
|
||||
};
|
||||
}
|
||||
}
|
||||
408
src/lib/audio/engines/FormantFM.ts
Normal file
408
src/lib/audio/engines/FormantFM.ts
Normal file
@ -0,0 +1,408 @@
|
||||
import { CsoundEngine, type CsoundParameter } from './base/CsoundEngine';
|
||||
import type { PitchLock } from './base/SynthEngine';
|
||||
|
||||
enum VowelType {
|
||||
A,
|
||||
E,
|
||||
I,
|
||||
O,
|
||||
U,
|
||||
AE,
|
||||
OE,
|
||||
UE,
|
||||
}
|
||||
|
||||
enum ModulationType {
|
||||
SimpleFM,
|
||||
DoubleFM,
|
||||
RingMod,
|
||||
CrossFM,
|
||||
}
|
||||
|
||||
interface FormantBand {
|
||||
frequency: number;
|
||||
bandwidth: number;
|
||||
amplitude: number;
|
||||
}
|
||||
|
||||
interface FormantFMParams {
|
||||
baseFreq: number;
|
||||
vowel: VowelType;
|
||||
vowelMorph: number;
|
||||
modulationType: ModulationType;
|
||||
modIndex: number;
|
||||
modRatio: number;
|
||||
attack: number;
|
||||
decay: number;
|
||||
sustain: number;
|
||||
release: number;
|
||||
brightness: number;
|
||||
vibrato: number;
|
||||
vibratoRate: number;
|
||||
detune: number;
|
||||
noise: number;
|
||||
formantLFORate: number;
|
||||
formantLFODepth: number;
|
||||
modIndexLFORate: number;
|
||||
modIndexLFODepth: number;
|
||||
chaos: number;
|
||||
}
|
||||
|
||||
export class FormantFM extends CsoundEngine<FormantFMParams> {
|
||||
getName(): string {
|
||||
return 'Formant FM';
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return 'FM synthesis with formant filters creating vowel-like sounds and vocal textures';
|
||||
}
|
||||
|
||||
getType() {
|
||||
return 'generative' as const;
|
||||
}
|
||||
|
||||
getCategory() {
|
||||
return 'FM' as const;
|
||||
}
|
||||
|
||||
protected getOrchestra(): string {
|
||||
return `
|
||||
instr 1
|
||||
iBaseFreq chnget "baseFreq"
|
||||
iVowel chnget "vowel"
|
||||
iVowelMorph chnget "vowelMorph"
|
||||
iModType chnget "modulationType"
|
||||
iModIndex chnget "modIndex"
|
||||
iModRatio chnget "modRatio"
|
||||
iAttack chnget "attack"
|
||||
iDecay chnget "decay"
|
||||
iSustain chnget "sustain"
|
||||
iRelease chnget "release"
|
||||
iBrightness chnget "brightness"
|
||||
iVibrato chnget "vibrato"
|
||||
iVibratoRate chnget "vibratoRate"
|
||||
iDetune chnget "detune"
|
||||
iNoise chnget "noise"
|
||||
iFormantLFORate chnget "formantLFORate"
|
||||
iFormantLFODepth chnget "formantLFODepth"
|
||||
iModIndexLFORate chnget "modIndexLFORate"
|
||||
iModIndexLFODepth chnget "modIndexLFODepth"
|
||||
iChaos chnget "chaos"
|
||||
|
||||
idur = p3
|
||||
iAttackTime = iAttack * idur
|
||||
iDecayTime = iDecay * idur
|
||||
iReleaseTime = iRelease * idur
|
||||
|
||||
; Envelope
|
||||
kEnv madsr iAttackTime, iDecayTime, iSustain, iReleaseTime
|
||||
|
||||
; Vibrato LFO with chaos
|
||||
kVib oscili iVibrato * iBaseFreq * 0.02, iVibratoRate
|
||||
kChaosLFO1 oscili iChaos * iBaseFreq * 0.01, iVibratoRate * 1.618
|
||||
kChaosLFO2 oscili iChaos * iBaseFreq * 0.005, iVibratoRate * 2.414
|
||||
kFreq = iBaseFreq + kVib + kChaosLFO1 + kChaosLFO2
|
||||
|
||||
; Get formant parameters based on vowel type
|
||||
if iVowel == 0 then
|
||||
; A (as in "father")
|
||||
iF1 = 730
|
||||
iF2 = 1090
|
||||
iF3 = 2440
|
||||
iA1 = 1.0
|
||||
iA2 = 0.5
|
||||
iA3 = 0.25
|
||||
iBW1 = 80
|
||||
iBW2 = 90
|
||||
iBW3 = 120
|
||||
elseif iVowel == 1 then
|
||||
; E (as in "bet")
|
||||
iF1 = 530
|
||||
iF2 = 1840
|
||||
iF3 = 2480
|
||||
iA1 = 1.0
|
||||
iA2 = 0.6
|
||||
iA3 = 0.3
|
||||
iBW1 = 60
|
||||
iBW2 = 100
|
||||
iBW3 = 120
|
||||
elseif iVowel == 2 then
|
||||
; I (as in "bit")
|
||||
iF1 = 390
|
||||
iF2 = 1990
|
||||
iF3 = 2550
|
||||
iA1 = 1.0
|
||||
iA2 = 0.7
|
||||
iA3 = 0.2
|
||||
iBW1 = 50
|
||||
iBW2 = 100
|
||||
iBW3 = 120
|
||||
elseif iVowel == 3 then
|
||||
; O (as in "boat")
|
||||
iF1 = 570
|
||||
iF2 = 840
|
||||
iF3 = 2410
|
||||
iA1 = 1.0
|
||||
iA2 = 0.45
|
||||
iA3 = 0.28
|
||||
iBW1 = 70
|
||||
iBW2 = 80
|
||||
iBW3 = 100
|
||||
elseif iVowel == 4 then
|
||||
; U (as in "boot")
|
||||
iF1 = 440
|
||||
iF2 = 1020
|
||||
iF3 = 2240
|
||||
iA1 = 1.0
|
||||
iA2 = 0.4
|
||||
iA3 = 0.2
|
||||
iBW1 = 70
|
||||
iBW2 = 80
|
||||
iBW3 = 100
|
||||
elseif iVowel == 5 then
|
||||
; AE (as in "bat")
|
||||
iF1 = 660
|
||||
iF2 = 1720
|
||||
iF3 = 2410
|
||||
iA1 = 1.0
|
||||
iA2 = 0.55
|
||||
iA3 = 0.3
|
||||
iBW1 = 80
|
||||
iBW2 = 90
|
||||
iBW3 = 120
|
||||
elseif iVowel == 6 then
|
||||
; OE (as in "bird")
|
||||
iF1 = 490
|
||||
iF2 = 1350
|
||||
iF3 = 1690
|
||||
iA1 = 1.0
|
||||
iA2 = 0.5
|
||||
iA3 = 0.4
|
||||
iBW1 = 70
|
||||
iBW2 = 80
|
||||
iBW3 = 100
|
||||
else
|
||||
; UE (as in "about")
|
||||
iF1 = 520
|
||||
iF2 = 1190
|
||||
iF3 = 2390
|
||||
iA1 = 1.0
|
||||
iA2 = 0.45
|
||||
iA3 = 0.25
|
||||
iBW1 = 70
|
||||
iBW2 = 80
|
||||
iBW3 = 110
|
||||
endif
|
||||
|
||||
; Modulate formant frequencies with multiple LFOs
|
||||
kFormantShift = 1 + (iVowelMorph - 0.5) * 0.4
|
||||
kFormantLFO oscili iFormantLFODepth * 0.3, iFormantLFORate
|
||||
kFormantShiftModulated = kFormantShift + kFormantLFO
|
||||
kF1 = iF1 * kFormantShiftModulated
|
||||
kF2 = iF2 * kFormantShiftModulated
|
||||
kF3 = iF3 * kFormantShiftModulated
|
||||
|
||||
; Modulation index LFO
|
||||
kModIndexLFO oscili iModIndexLFODepth * iModIndex, iModIndexLFORate
|
||||
kModIndexDynamic = iModIndex + kModIndexLFO
|
||||
|
||||
; Generate carrier and modulator based on modulation type
|
||||
kModFreq = kFreq * iModRatio
|
||||
|
||||
if iModType == 0 then
|
||||
; Simple FM with dynamic modulation
|
||||
aMod oscili kModIndexDynamic * kModFreq, kModFreq
|
||||
aCarrier oscili 0.5, kFreq + aMod
|
||||
|
||||
elseif iModType == 1 then
|
||||
; Double FM (modulator modulates itself) with chaos
|
||||
kChaosModDepth = 1 + (iChaos * 0.5)
|
||||
aMod1 oscili kModIndexDynamic * 0.3 * kModFreq * kChaosModDepth, kModFreq * 0.5
|
||||
aMod2 oscili kModIndexDynamic * kModFreq, kModFreq + aMod1
|
||||
aCarrier oscili 0.5, kFreq + aMod2
|
||||
|
||||
elseif iModType == 2 then
|
||||
; Ring modulation with frequency wobble
|
||||
kRingModWobble oscili iChaos * 0.2, iFormantLFORate * 0.7
|
||||
aMod oscili 0.5, kModFreq * (1 + kRingModWobble)
|
||||
aCarrierTemp oscili 0.5, kFreq
|
||||
aCarrier = aCarrierTemp * aMod
|
||||
|
||||
else
|
||||
; Cross FM with multiple carriers
|
||||
aMod oscili kModIndexDynamic * kModFreq, kModFreq
|
||||
aCarrier1 oscili 0.4, kFreq + aMod
|
||||
aCarrier2 oscili 0.3, kFreq * 0.5 + aMod * 0.5
|
||||
kThirdCarrierFreq = kFreq * (1.5 + iChaos * 0.3)
|
||||
aCarrier3 oscili 0.2 * iChaos, kThirdCarrierFreq + aMod * 0.3
|
||||
aCarrier = aCarrier1 + aCarrier2 + aCarrier3
|
||||
endif
|
||||
|
||||
; Add brightness via high-frequency content
|
||||
aCarrierBright oscili 0.15 * iBrightness, kFreq * 2
|
||||
aCarrierMix = aCarrier + aCarrierBright
|
||||
|
||||
; Add subtle noise for breathiness
|
||||
aNoise noise 0.08 * iNoise, 0
|
||||
aCarrierFinal = aCarrierMix + aNoise
|
||||
|
||||
; Apply formant filters (bandpass filters at formant frequencies)
|
||||
aFormant1 butterbp aCarrierFinal, kF1, iBW1
|
||||
aFormant1Scaled = aFormant1 * iA1
|
||||
|
||||
aFormant2 butterbp aCarrierFinal, kF2, iBW2
|
||||
aFormant2Scaled = aFormant2 * iA2
|
||||
|
||||
aFormant3 butterbp aCarrierFinal, kF3, iBW3
|
||||
aFormant3Scaled = aFormant3 * iA3
|
||||
|
||||
; Mix formants
|
||||
aMix = (aFormant1Scaled + aFormant2Scaled + aFormant3Scaled) * 0.6
|
||||
|
||||
; Apply envelope
|
||||
aOut = aMix * kEnv
|
||||
|
||||
; Stereo - slightly different phase for right channel
|
||||
iDetuneFactor = 1 + (iDetune * 0.5)
|
||||
kFreqR = iBaseFreq * iDetuneFactor + kVib
|
||||
|
||||
; Regenerate right channel with detuned frequency
|
||||
kModFreqR = kFreqR * iModRatio
|
||||
|
||||
if iModType == 0 then
|
||||
aModR oscili iModIndex * kModFreqR, kModFreqR
|
||||
aCarrierR oscili 0.5, kFreqR + aModR
|
||||
elseif iModType == 1 then
|
||||
aMod1R oscili iModIndex * 0.3 * kModFreqR, kModFreqR * 0.5
|
||||
aMod2R oscili iModIndex * kModFreqR, kModFreqR + aMod1R
|
||||
aCarrierR oscili 0.5, kFreqR + aMod2R
|
||||
elseif iModType == 2 then
|
||||
aModR oscili 0.5, kModFreqR
|
||||
aCarrierTempR oscili 0.5, kFreqR
|
||||
aCarrierR = aCarrierTempR * aModR
|
||||
else
|
||||
aModR oscili iModIndex * kModFreqR, kModFreqR
|
||||
aCarrier1R oscili 0.4, kFreqR + aModR
|
||||
aCarrier2R oscili 0.3, kFreqR * 0.5 + aModR * 0.5
|
||||
aCarrierR = aCarrier1R + aCarrier2R
|
||||
endif
|
||||
|
||||
aCarrierBrightR oscili 0.15 * iBrightness, kFreqR * 2
|
||||
aCarrierMixR = aCarrierR + aCarrierBrightR
|
||||
aNoiseR noise 0.08 * iNoise, 0
|
||||
aCarrierFinalR = aCarrierMixR + aNoiseR
|
||||
|
||||
kF1R = iF1 * kFormantShift
|
||||
kF2R = iF2 * kFormantShift
|
||||
kF3R = iF3 * kFormantShift
|
||||
|
||||
aFormant1R butterbp aCarrierFinalR, kF1R, iBW1
|
||||
aFormant1ScaledR = aFormant1R * iA1
|
||||
aFormant2R butterbp aCarrierFinalR, kF2R, iBW2
|
||||
aFormant2ScaledR = aFormant2R * iA2
|
||||
aFormant3R butterbp aCarrierFinalR, kF3R, iBW3
|
||||
aFormant3ScaledR = aFormant3R * iA3
|
||||
|
||||
aMixR = (aFormant1ScaledR + aFormant2ScaledR + aFormant3ScaledR) * 0.6
|
||||
aOutR = aMixR * kEnv
|
||||
|
||||
outs aOut, aOutR
|
||||
endin
|
||||
`;
|
||||
}
|
||||
|
||||
protected getParametersForCsound(params: FormantFMParams): CsoundParameter[] {
|
||||
return [
|
||||
{ channelName: 'baseFreq', value: params.baseFreq },
|
||||
{ channelName: 'vowel', value: params.vowel },
|
||||
{ channelName: 'vowelMorph', value: params.vowelMorph },
|
||||
{ channelName: 'modulationType', value: params.modulationType },
|
||||
{ channelName: 'modIndex', value: params.modIndex },
|
||||
{ channelName: 'modRatio', value: params.modRatio },
|
||||
{ channelName: 'attack', value: params.attack },
|
||||
{ channelName: 'decay', value: params.decay },
|
||||
{ channelName: 'sustain', value: params.sustain },
|
||||
{ channelName: 'release', value: params.release },
|
||||
{ channelName: 'brightness', value: params.brightness },
|
||||
{ channelName: 'vibrato', value: params.vibrato },
|
||||
{ channelName: 'vibratoRate', value: params.vibratoRate },
|
||||
{ channelName: 'detune', value: params.detune },
|
||||
{ channelName: 'noise', value: params.noise },
|
||||
{ channelName: 'formantLFORate', value: params.formantLFORate },
|
||||
{ channelName: 'formantLFODepth', value: params.formantLFODepth },
|
||||
{ channelName: 'modIndexLFORate', value: params.modIndexLFORate },
|
||||
{ channelName: 'modIndexLFODepth', value: params.modIndexLFODepth },
|
||||
{ channelName: 'chaos', value: params.chaos },
|
||||
];
|
||||
}
|
||||
|
||||
randomParams(pitchLock?: PitchLock): FormantFMParams {
|
||||
const baseFreqChoices = [82.4, 110, 146.8, 220, 293.7, 440];
|
||||
const baseFreq = pitchLock?.enabled
|
||||
? pitchLock.frequency
|
||||
: this.randomChoice(baseFreqChoices) * this.randomRange(0.95, 1.05);
|
||||
|
||||
const modulationRatios = [0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 5, 7, 9];
|
||||
|
||||
return {
|
||||
baseFreq,
|
||||
vowel: this.randomInt(0, 7) as VowelType,
|
||||
vowelMorph: this.randomRange(0.3, 0.7),
|
||||
modulationType: this.randomInt(0, 3) as ModulationType,
|
||||
modIndex: this.randomRange(1, 10),
|
||||
modRatio: this.randomChoice(modulationRatios),
|
||||
attack: this.randomRange(0.001, 0.15),
|
||||
decay: this.randomRange(0.05, 0.25),
|
||||
sustain: this.randomRange(0.3, 0.8),
|
||||
release: this.randomRange(0.1, 0.4),
|
||||
brightness: this.randomRange(0, 0.6),
|
||||
vibrato: this.randomRange(0, 0.5),
|
||||
vibratoRate: this.randomRange(3, 8),
|
||||
detune: this.randomRange(0.001, 0.01),
|
||||
noise: this.randomRange(0, 0.3),
|
||||
formantLFORate: this.randomRange(0.2, 6),
|
||||
formantLFODepth: this.randomRange(0, 0.8),
|
||||
modIndexLFORate: this.randomRange(0.5, 12),
|
||||
modIndexLFODepth: this.randomRange(0, 0.7),
|
||||
chaos: this.randomRange(0, 0.8),
|
||||
};
|
||||
}
|
||||
|
||||
mutateParams(
|
||||
params: FormantFMParams,
|
||||
mutationAmount: number = 0.15,
|
||||
pitchLock?: PitchLock
|
||||
): FormantFMParams {
|
||||
const baseFreq = pitchLock?.enabled ? pitchLock.frequency : params.baseFreq;
|
||||
const modulationRatios = [0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 5, 7, 9];
|
||||
|
||||
return {
|
||||
baseFreq,
|
||||
vowel: Math.random() < 0.1 ? (this.randomInt(0, 7) as VowelType) : params.vowel,
|
||||
vowelMorph: this.mutateValue(params.vowelMorph, mutationAmount, 0, 1),
|
||||
modulationType:
|
||||
Math.random() < 0.08
|
||||
? (this.randomInt(0, 3) as ModulationType)
|
||||
: params.modulationType,
|
||||
modIndex: this.mutateValue(params.modIndex, mutationAmount, 0.5, 15),
|
||||
modRatio:
|
||||
Math.random() < 0.12
|
||||
? this.randomChoice(modulationRatios)
|
||||
: params.modRatio,
|
||||
attack: this.mutateValue(params.attack, mutationAmount, 0.001, 0.25),
|
||||
decay: this.mutateValue(params.decay, mutationAmount, 0.02, 0.4),
|
||||
sustain: this.mutateValue(params.sustain, mutationAmount, 0.1, 0.9),
|
||||
release: this.mutateValue(params.release, mutationAmount, 0.05, 0.6),
|
||||
brightness: this.mutateValue(params.brightness, mutationAmount, 0, 1),
|
||||
vibrato: this.mutateValue(params.vibrato, mutationAmount, 0, 0.6),
|
||||
vibratoRate: this.mutateValue(params.vibratoRate, mutationAmount, 2, 10),
|
||||
detune: this.mutateValue(params.detune, mutationAmount, 0.0005, 0.02),
|
||||
noise: this.mutateValue(params.noise, mutationAmount, 0, 0.5),
|
||||
formantLFORate: this.mutateValue(params.formantLFORate, mutationAmount, 0.1, 8),
|
||||
formantLFODepth: this.mutateValue(params.formantLFODepth, mutationAmount, 0, 1),
|
||||
modIndexLFORate: this.mutateValue(params.modIndexLFORate, mutationAmount, 0.3, 15),
|
||||
modIndexLFODepth: this.mutateValue(params.modIndexLFODepth, mutationAmount, 0, 1),
|
||||
chaos: this.mutateValue(params.chaos, mutationAmount, 0, 1),
|
||||
};
|
||||
}
|
||||
}
|
||||
142
src/lib/audio/engines/FormantPopDrum.ts
Normal file
142
src/lib/audio/engines/FormantPopDrum.ts
Normal file
@ -0,0 +1,142 @@
|
||||
import { CsoundEngine, type CsoundParameter } from './base/CsoundEngine';
|
||||
import type { PitchLock } from './base/SynthEngine';
|
||||
|
||||
interface FormantPopDrumParams {
|
||||
formant1Freq: number;
|
||||
formant1Width: number;
|
||||
formant2Freq: number;
|
||||
formant2Width: number;
|
||||
noiseDecay: number;
|
||||
ampAttack: number;
|
||||
ampDecay: number;
|
||||
brightness: number;
|
||||
stereoSpread: number;
|
||||
}
|
||||
|
||||
export class FormantPopDrum extends CsoundEngine<FormantPopDrumParams> {
|
||||
getName(): string {
|
||||
return 'Formant Pop Drum';
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return 'Short noise burst through dual bandpass filters creating marimba-like or wooden drum tones';
|
||||
}
|
||||
|
||||
getType() {
|
||||
return 'generative' as const;
|
||||
}
|
||||
|
||||
getCategory() {
|
||||
return 'Percussion' as const;
|
||||
}
|
||||
|
||||
protected getOrchestra(): string {
|
||||
return `
|
||||
instr 1
|
||||
iF1Freq chnget "formant1Freq"
|
||||
iF1Width chnget "formant1Width"
|
||||
iF2Freq chnget "formant2Freq"
|
||||
iF2Width chnget "formant2Width"
|
||||
iNoiseDecay chnget "noiseDecay"
|
||||
iAmpAttack chnget "ampAttack"
|
||||
iAmpDecay chnget "ampDecay"
|
||||
iBrightness chnget "brightness"
|
||||
iStereoSpread chnget "stereoSpread"
|
||||
|
||||
idur = p3
|
||||
iNoiseDecayTime = iNoiseDecay * idur
|
||||
iAmpAttackTime = iAmpAttack * idur
|
||||
iAmpDecayTime = iAmpDecay * idur
|
||||
|
||||
; Declick envelope for noise (very short to avoid clicks)
|
||||
kDeclickEnv linseg 0, 0.001, 1, iNoiseDecayTime, 0, idur - iNoiseDecayTime - 0.001, 0
|
||||
|
||||
; Generate random noise
|
||||
aNoise noise 1, 0
|
||||
|
||||
; Apply declick envelope to noise
|
||||
aNoiseEnv = aNoise * kDeclickEnv
|
||||
|
||||
; First bandpass filter (formant 1)
|
||||
aFormant1 butterbp aNoiseEnv, iF1Freq, iF1Width
|
||||
|
||||
; Second bandpass filter (formant 2)
|
||||
aFormant2 butterbp aNoiseEnv, iF2Freq, iF2Width
|
||||
|
||||
; Mix formants with brightness control
|
||||
aMix = aFormant1 * (1 - iBrightness * 0.5) + aFormant2 * iBrightness
|
||||
|
||||
; Amplitude envelope (exponential decay)
|
||||
kAmpEnv expseg 0.001, iAmpAttackTime, 1, iAmpDecayTime, 0.001, idur - iAmpAttackTime - iAmpDecayTime, 0.001
|
||||
|
||||
; Apply amplitude envelope
|
||||
aOut = aMix * kAmpEnv
|
||||
|
||||
; Stereo output with slight frequency offset for right channel
|
||||
iF1FreqR = iF1Freq * (1 + iStereoSpread * 0.02)
|
||||
iF2FreqR = iF2Freq * (1 + iStereoSpread * 0.02)
|
||||
|
||||
aFormant1R butterbp aNoiseEnv, iF1FreqR, iF1Width
|
||||
aFormant2R butterbp aNoiseEnv, iF2FreqR, iF2Width
|
||||
|
||||
aMixR = aFormant1R * (1 - iBrightness * 0.5) + aFormant2R * iBrightness
|
||||
aOutR = aMixR * kAmpEnv
|
||||
|
||||
outs aOut, aOutR
|
||||
endin
|
||||
`;
|
||||
}
|
||||
|
||||
protected getParametersForCsound(params: FormantPopDrumParams): CsoundParameter[] {
|
||||
return [
|
||||
{ channelName: 'formant1Freq', value: params.formant1Freq },
|
||||
{ channelName: 'formant1Width', value: params.formant1Width },
|
||||
{ channelName: 'formant2Freq', value: params.formant2Freq },
|
||||
{ channelName: 'formant2Width', value: params.formant2Width },
|
||||
{ channelName: 'noiseDecay', value: params.noiseDecay },
|
||||
{ channelName: 'ampAttack', value: params.ampAttack },
|
||||
{ channelName: 'ampDecay', value: params.ampDecay },
|
||||
{ channelName: 'brightness', value: params.brightness },
|
||||
{ channelName: 'stereoSpread', value: params.stereoSpread },
|
||||
];
|
||||
}
|
||||
|
||||
randomParams(pitchLock?: PitchLock): FormantPopDrumParams {
|
||||
const formant1FreqChoices = [200, 250, 300, 400, 500, 600, 800, 1000];
|
||||
const formant1Freq = pitchLock?.enabled
|
||||
? pitchLock.frequency
|
||||
: this.randomChoice(formant1FreqChoices) * this.randomRange(0.9, 1.1);
|
||||
|
||||
return {
|
||||
formant1Freq,
|
||||
formant1Width: this.randomRange(30, 120),
|
||||
formant2Freq: formant1Freq * this.randomRange(1.5, 3.5),
|
||||
formant2Width: this.randomRange(40, 150),
|
||||
noiseDecay: this.randomRange(0.05, 0.3),
|
||||
ampAttack: this.randomRange(0.001, 0.02),
|
||||
ampDecay: this.randomRange(0.1, 0.6),
|
||||
brightness: this.randomRange(0.2, 0.8),
|
||||
stereoSpread: this.randomRange(0, 0.5),
|
||||
};
|
||||
}
|
||||
|
||||
mutateParams(
|
||||
params: FormantPopDrumParams,
|
||||
mutationAmount: number = 0.15,
|
||||
pitchLock?: PitchLock
|
||||
): FormantPopDrumParams {
|
||||
const formant1Freq = pitchLock?.enabled ? pitchLock.frequency : params.formant1Freq;
|
||||
|
||||
return {
|
||||
formant1Freq,
|
||||
formant1Width: this.mutateValue(params.formant1Width, mutationAmount, 20, 200),
|
||||
formant2Freq: this.mutateValue(params.formant2Freq, mutationAmount, 300, 4000),
|
||||
formant2Width: this.mutateValue(params.formant2Width, mutationAmount, 30, 250),
|
||||
noiseDecay: this.mutateValue(params.noiseDecay, mutationAmount, 0.02, 0.5),
|
||||
ampAttack: this.mutateValue(params.ampAttack, mutationAmount, 0.001, 0.05),
|
||||
ampDecay: this.mutateValue(params.ampDecay, mutationAmount, 0.05, 0.8),
|
||||
brightness: this.mutateValue(params.brightness, mutationAmount, 0, 1),
|
||||
stereoSpread: this.mutateValue(params.stereoSpread, mutationAmount, 0, 1),
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { PitchLock, SynthEngine } from './SynthEngine';
|
||||
import type { PitchLock, SynthEngine } from './base/SynthEngine';
|
||||
|
||||
interface HiHatParams {
|
||||
// Decay time (0 = closed/tight, 1 = open/long)
|
||||
@ -19,7 +19,7 @@ interface HiHatParams {
|
||||
|
||||
export class HiHat implements SynthEngine {
|
||||
getName(): string {
|
||||
return 'Hi-Hat';
|
||||
return 'Noise Hi-Hat';
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
@ -30,6 +30,10 @@ export class HiHat implements SynthEngine {
|
||||
return 'generative' as const;
|
||||
}
|
||||
|
||||
getCategory() {
|
||||
return 'Percussion' as const;
|
||||
}
|
||||
|
||||
randomParams(pitchLock?: PitchLock): HiHatParams {
|
||||
return {
|
||||
decay: Math.random(),
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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);
|
||||
|
||||
244
src/lib/audio/engines/LaserSweep.ts
Normal file
244
src/lib/audio/engines/LaserSweep.ts
Normal file
@ -0,0 +1,244 @@
|
||||
import { CsoundEngine, type CsoundParameter } from './base/CsoundEngine';
|
||||
|
||||
interface LaserSweepParams {
|
||||
startFreq: number;
|
||||
endFreq: number;
|
||||
sweepCurve: number;
|
||||
resonance: number;
|
||||
brightness: number;
|
||||
delayTime: number;
|
||||
delayFeedback: number;
|
||||
reverbMix: number;
|
||||
reverbSize: number;
|
||||
noiseAmount: number;
|
||||
decayShape: number;
|
||||
waveformMix: number;
|
||||
mainWaveType: number;
|
||||
secondWaveType: number;
|
||||
pulseWidth: number;
|
||||
filterTracking: number;
|
||||
subOctave: number;
|
||||
subOctaveLevel: number;
|
||||
}
|
||||
|
||||
export class LaserSweep extends CsoundEngine<LaserSweepParams> {
|
||||
getName(): string {
|
||||
return 'Laser Sweep';
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return 'Sweeping laser beam with delay and reverb';
|
||||
}
|
||||
|
||||
getType(): 'generative' | 'sample' | 'input' {
|
||||
return 'generative';
|
||||
}
|
||||
|
||||
getCategory() {
|
||||
return 'Experimental' as const;
|
||||
}
|
||||
|
||||
getOrchestra(): string {
|
||||
return `
|
||||
sr = 44100
|
||||
ksmps = 64
|
||||
nchnls = 2
|
||||
0dbfs = 1
|
||||
|
||||
instr 1
|
||||
iDur = p3
|
||||
|
||||
iStartFreq chnget "startFreq"
|
||||
iEndFreq chnget "endFreq"
|
||||
iSweepCurve chnget "sweepCurve"
|
||||
iResonance chnget "resonance"
|
||||
iBrightness chnget "brightness"
|
||||
iDelayTime chnget "delayTime"
|
||||
iDelayFeedback chnget "delayFeedback"
|
||||
iReverbMix chnget "reverbMix"
|
||||
iReverbSize chnget "reverbSize"
|
||||
iNoiseAmount chnget "noiseAmount"
|
||||
iDecayShape chnget "decayShape"
|
||||
iWaveformMix chnget "waveformMix"
|
||||
iMainWaveType chnget "mainWaveType"
|
||||
iSecondWaveType chnget "secondWaveType"
|
||||
iPulseWidth chnget "pulseWidth"
|
||||
iFilterTracking chnget "filterTracking"
|
||||
iSubOctave chnget "subOctave"
|
||||
iSubOctaveLevel chnget "subOctaveLevel"
|
||||
|
||||
if iDecayShape < 0.33 then
|
||||
kEnv expseg 1, iDur * 0.85, 1, iDur * 0.15, 0.001
|
||||
elseif iDecayShape < 0.67 then
|
||||
kEnv linseg 1, iDur * 0.85, 1, iDur * 0.15, 0
|
||||
else
|
||||
kEnv expseg 1, iDur * 0.8, 1, iDur * 0.18, 0.001
|
||||
endif
|
||||
|
||||
kFreq expseg iStartFreq, iDur * iSweepCurve, iEndFreq
|
||||
kFreqLimited limit kFreq, 40, 16000
|
||||
|
||||
if iMainWaveType < 0.2 then
|
||||
aMain vco2 0.6, kFreqLimited, 10
|
||||
elseif iMainWaveType < 0.4 then
|
||||
aMain vco2 0.6, kFreqLimited, 0
|
||||
elseif iMainWaveType < 0.6 then
|
||||
aMain vco2 0.6, kFreqLimited, 2, iPulseWidth
|
||||
elseif iMainWaveType < 0.8 then
|
||||
aMain vco2 0.6, kFreqLimited, 12
|
||||
else
|
||||
aMain oscili 0.6, kFreqLimited
|
||||
endif
|
||||
|
||||
if iSecondWaveType < 0.2 then
|
||||
aSecond vco2 0.5, kFreqLimited, 2, iPulseWidth
|
||||
elseif iSecondWaveType < 0.4 then
|
||||
aSecond vco2 0.5, kFreqLimited, 0
|
||||
elseif iSecondWaveType < 0.6 then
|
||||
aSecond vco2 0.5, kFreqLimited, 12
|
||||
elseif iSecondWaveType < 0.8 then
|
||||
aSecond vco2 0.5, kFreqLimited, 10
|
||||
else
|
||||
aSecond oscili 0.5, kFreqLimited * 1.5
|
||||
endif
|
||||
|
||||
aNoise rand 0.3
|
||||
kNoiseFiltFreq limit kFreqLimited * 0.5, 50, 12000
|
||||
aNoise butterbp aNoise, kNoiseFiltFreq, kNoiseFiltFreq * 0.5
|
||||
|
||||
if iSubOctaveLevel > 0.01 then
|
||||
if iSubOctave < 0.5 then
|
||||
kSubFreq = kFreqLimited * 0.5
|
||||
else
|
||||
kSubFreq = kFreqLimited * 0.25
|
||||
endif
|
||||
aSub oscili iSubOctaveLevel * 0.7, kSubFreq
|
||||
else
|
||||
aSub = 0
|
||||
endif
|
||||
|
||||
aOsc = aMain + aSecond * iWaveformMix + aSecond * iBrightness * 0.3 + aNoise * iNoiseAmount + aSub
|
||||
|
||||
kFilterFreq = kFreqLimited * (1 + (1 - iFilterTracking) * 2)
|
||||
kFilterFreq limit kFilterFreq, 50, 15000
|
||||
|
||||
kBandwidth = kFilterFreq * (0.1 + iResonance * 0.3)
|
||||
aFilt butterbp aOsc, kFilterFreq, kBandwidth
|
||||
|
||||
aFilt = aFilt * kEnv * 0.8
|
||||
|
||||
if iDelayTime > 0.01 then
|
||||
aDlyL vdelay3 aFilt, iDelayTime * 1000, 2000
|
||||
aDlyR vdelay3 aFilt, iDelayTime * 1000 * 1.1, 2000
|
||||
|
||||
aDlyL = aDlyL * iDelayFeedback * kEnv
|
||||
aDlyR = aDlyR * iDelayFeedback * kEnv
|
||||
else
|
||||
aDlyL = 0
|
||||
aDlyR = 0
|
||||
endif
|
||||
|
||||
if iReverbMix > 0.01 then
|
||||
aWetL, aWetR reverbsc aFilt + aDlyL * 0.5, aFilt + aDlyR * 0.5, iReverbSize, 8000
|
||||
else
|
||||
aWetL = 0
|
||||
aWetR = 0
|
||||
endif
|
||||
|
||||
aOutL = aFilt + aDlyL * 0.4 + aWetL * iReverbMix
|
||||
aOutR = aFilt + aDlyR * 0.4 + aWetR * iReverbMix
|
||||
|
||||
outs aOutL, aOutR
|
||||
endin
|
||||
`;
|
||||
}
|
||||
|
||||
getParametersForCsound(params: LaserSweepParams): CsoundParameter[] {
|
||||
return [
|
||||
{ channelName: 'startFreq', value: params.startFreq },
|
||||
{ channelName: 'endFreq', value: params.endFreq },
|
||||
{ channelName: 'sweepCurve', value: params.sweepCurve },
|
||||
{ channelName: 'resonance', value: params.resonance },
|
||||
{ channelName: 'brightness', value: params.brightness },
|
||||
{ channelName: 'delayTime', value: params.delayTime },
|
||||
{ channelName: 'delayFeedback', value: params.delayFeedback },
|
||||
{ channelName: 'reverbMix', value: params.reverbMix },
|
||||
{ channelName: 'reverbSize', value: params.reverbSize },
|
||||
{ channelName: 'noiseAmount', value: params.noiseAmount },
|
||||
{ channelName: 'decayShape', value: params.decayShape },
|
||||
{ channelName: 'waveformMix', value: params.waveformMix },
|
||||
{ channelName: 'mainWaveType', value: params.mainWaveType },
|
||||
{ channelName: 'secondWaveType', value: params.secondWaveType },
|
||||
{ channelName: 'pulseWidth', value: params.pulseWidth },
|
||||
{ channelName: 'filterTracking', value: params.filterTracking },
|
||||
{ channelName: 'subOctave', value: params.subOctave },
|
||||
{ channelName: 'subOctaveLevel', value: params.subOctaveLevel }
|
||||
];
|
||||
}
|
||||
|
||||
randomParams(pitchLock?: number): LaserSweepParams {
|
||||
const isUpward = Math.random() > 0.3;
|
||||
|
||||
const startFreq = isUpward
|
||||
? 60 + Math.random() * 200
|
||||
: 1000 + Math.random() * 10000;
|
||||
|
||||
const endFreq = isUpward
|
||||
? 1000 + Math.random() * 10000
|
||||
: 60 + Math.random() * 200;
|
||||
|
||||
return {
|
||||
startFreq,
|
||||
endFreq,
|
||||
sweepCurve: 0.2 + Math.random() * 0.7,
|
||||
resonance: 0.2 + Math.random() * 0.7,
|
||||
brightness: 0.1 + Math.random() * 0.8,
|
||||
delayTime: Math.random() < 0.3 ? 0 : 0.05 + Math.random() * 0.5,
|
||||
delayFeedback: 0.1 + Math.random() * 0.7,
|
||||
reverbMix: Math.random() < 0.2 ? 0 : 0.1 + Math.random() * 0.5,
|
||||
reverbSize: 0.4 + Math.random() * 0.5,
|
||||
noiseAmount: 0.05 + Math.random() * 0.5,
|
||||
decayShape: Math.random(),
|
||||
waveformMix: 0.3 + Math.random() * 0.7,
|
||||
mainWaveType: Math.random(),
|
||||
secondWaveType: Math.random(),
|
||||
pulseWidth: 0.1 + Math.random() * 0.8,
|
||||
filterTracking: 0.3 + Math.random() * 0.7,
|
||||
subOctave: Math.random(),
|
||||
subOctaveLevel: Math.random() < 0.4 ? 0 : 0.3 + Math.random() * 0.7
|
||||
};
|
||||
}
|
||||
|
||||
mutateParams(
|
||||
params: LaserSweepParams,
|
||||
mutationAmount: number = 0.3,
|
||||
pitchLock?: number
|
||||
): LaserSweepParams {
|
||||
const mutate = (value: number, min: number, max: number): number => {
|
||||
const range = max - min;
|
||||
const change = (Math.random() - 0.5) * range * mutationAmount;
|
||||
return Math.max(min, Math.min(max, value + change));
|
||||
};
|
||||
|
||||
return {
|
||||
startFreq: mutate(params.startFreq, 40, 12000),
|
||||
endFreq: mutate(params.endFreq, 40, 12000),
|
||||
sweepCurve: mutate(params.sweepCurve, 0.1, 0.95),
|
||||
resonance: mutate(params.resonance, 0.1, 1.0),
|
||||
brightness: mutate(params.brightness, 0.0, 1.0),
|
||||
delayTime: mutate(params.delayTime, 0.0, 0.6),
|
||||
delayFeedback: mutate(params.delayFeedback, 0.0, 0.85),
|
||||
reverbMix: mutate(params.reverbMix, 0.0, 0.7),
|
||||
reverbSize: mutate(params.reverbSize, 0.3, 0.95),
|
||||
noiseAmount: mutate(params.noiseAmount, 0.0, 0.7),
|
||||
decayShape: mutate(params.decayShape, 0.0, 1.0),
|
||||
waveformMix: mutate(params.waveformMix, 0.0, 1.0),
|
||||
mainWaveType: mutate(params.mainWaveType, 0.0, 1.0),
|
||||
secondWaveType: mutate(params.secondWaveType, 0.0, 1.0),
|
||||
pulseWidth: mutate(params.pulseWidth, 0.05, 0.95),
|
||||
filterTracking: mutate(params.filterTracking, 0.0, 1.0),
|
||||
subOctave: mutate(params.subOctave, 0.0, 1.0),
|
||||
subOctaveLevel: mutate(params.subOctaveLevel, 0.0, 1.0)
|
||||
};
|
||||
}
|
||||
}
|
||||
317
src/lib/audio/engines/MassiveAdditive.ts
Normal file
317
src/lib/audio/engines/MassiveAdditive.ts
Normal file
@ -0,0 +1,317 @@
|
||||
import { CsoundEngine, type CsoundParameter } from './base/CsoundEngine';
|
||||
import type { PitchLock } from './base/SynthEngine';
|
||||
|
||||
interface MassiveAdditiveParams {
|
||||
baseFreq: number;
|
||||
numPartials: number;
|
||||
spectrumShape: number;
|
||||
harmonicSpread: number;
|
||||
inharmonicity: number;
|
||||
attack: number;
|
||||
decay: number;
|
||||
sustain: number;
|
||||
release: number;
|
||||
ampLFORate: number;
|
||||
ampLFODepth: number;
|
||||
ampLFOPhaseSpread: number;
|
||||
freqLFORate: number;
|
||||
freqLFODepth: number;
|
||||
partialDecayRate: number;
|
||||
partialAttackSpread: number;
|
||||
oddEvenBalance: number;
|
||||
shimmer: number;
|
||||
shimmerRate: number;
|
||||
stereoSpread: number;
|
||||
brightness: number;
|
||||
chaos: number;
|
||||
}
|
||||
|
||||
export class MassiveAdditive extends CsoundEngine<MassiveAdditiveParams> {
|
||||
getName(): string {
|
||||
return 'Spectral Add';
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return 'Spectral additive synthesis with octave doubling, micro-detuned beating, and evolving harmonic content';
|
||||
}
|
||||
|
||||
getType() {
|
||||
return 'generative' as const;
|
||||
}
|
||||
|
||||
getCategory() {
|
||||
return 'Additive' as const;
|
||||
}
|
||||
|
||||
protected getOrchestra(): string {
|
||||
return `
|
||||
; Function tables for sine wave
|
||||
gisine ftgen 0, 0, 16384, 10, 1
|
||||
|
||||
instr 1
|
||||
iBaseFreq chnget "baseFreq"
|
||||
iNumPartials chnget "numPartials"
|
||||
iSpectrumShape chnget "spectrumShape"
|
||||
iHarmonicSpread chnget "harmonicSpread"
|
||||
iInharmonicity chnget "inharmonicity"
|
||||
iAttack chnget "attack"
|
||||
iDecay chnget "decay"
|
||||
iSustain chnget "sustain"
|
||||
iRelease chnget "release"
|
||||
iAmpLFORate chnget "ampLFORate"
|
||||
iAmpLFODepth chnget "ampLFODepth"
|
||||
iAmpLFOPhaseSpread chnget "ampLFOPhaseSpread"
|
||||
iFreqLFORate chnget "freqLFORate"
|
||||
iFreqLFODepth chnget "freqLFODepth"
|
||||
iPartialDecayRate chnget "partialDecayRate"
|
||||
iPartialAttackSpread chnget "partialAttackSpread"
|
||||
iOddEvenBalance chnget "oddEvenBalance"
|
||||
iShimmer chnget "shimmer"
|
||||
iShimmerRate chnget "shimmerRate"
|
||||
iStereoSpread chnget "stereoSpread"
|
||||
iBrightness chnget "brightness"
|
||||
iChaos chnget "chaos"
|
||||
|
||||
idur = p3
|
||||
iAttackTime = iAttack * idur
|
||||
iDecayTime = iDecay * idur
|
||||
iReleaseTime = iRelease * idur
|
||||
|
||||
; Main envelope
|
||||
kEnv madsr iAttackTime, iDecayTime, iSustain, iReleaseTime
|
||||
|
||||
; Global modulation LFOs
|
||||
kGlobalAmpLFO oscili 1, iAmpLFORate * 0.5
|
||||
kShimmerLFO oscili 1, iShimmerRate
|
||||
kFreqLFO oscili iFreqLFODepth * 0.02, iFreqLFORate
|
||||
|
||||
; Chaos LFOs
|
||||
kChaosLFO1 oscili 1, iAmpLFORate * 1.618
|
||||
kChaosLFO2 oscili 1, iAmpLFORate * 2.414
|
||||
|
||||
; Create frequency and amplitude tables dynamically
|
||||
iPartialCount = min(iNumPartials, 64)
|
||||
|
||||
; Generate tables for partial frequencies and amplitudes
|
||||
gifreq ftgen 0, 0, 128, -2, 0
|
||||
giamp ftgen 0, 0, 128, -2, 0
|
||||
gifreq2 ftgen 0, 0, 128, -2, 0
|
||||
giamp2 ftgen 0, 0, 128, -2, 0
|
||||
|
||||
iPartialIndex = 0
|
||||
loop_setup:
|
||||
iN = iPartialIndex + 1
|
||||
|
||||
; Calculate frequency ratio with harmonicity and inharmonicity
|
||||
iHarmonic = iN * iHarmonicSpread
|
||||
iInharmonicShift = iInharmonicity * iN * iN * 0.001
|
||||
iFreqRatio = iHarmonic * (1 + iInharmonicShift)
|
||||
|
||||
; Calculate amplitude based on spectrum shape
|
||||
iAmpFalloff = 1 / pow(iN, 1 + iSpectrumShape * 2)
|
||||
|
||||
; Brightness boost for higher partials
|
||||
iBrightnessBoost = 1 + (iBrightness * (iN / iPartialCount) * 2)
|
||||
|
||||
; Odd/even balance
|
||||
iOddEvenFactor = 1
|
||||
if (iN % 2) == 0 then
|
||||
iOddEvenFactor = iOddEvenBalance
|
||||
else
|
||||
iOddEvenFactor = 2 - iOddEvenBalance
|
||||
endif
|
||||
|
||||
iAmp = iAmpFalloff * iBrightnessBoost * iOddEvenFactor
|
||||
|
||||
; Create alternate partial distribution for morphing
|
||||
iFreqRatio2 = iFreqRatio * (1 + (iChaos * 0.05 * sin(iN * 0.7)))
|
||||
iAmp2 = iAmp * (1 - (iN / iPartialCount) * 0.3)
|
||||
|
||||
; Write to tables
|
||||
tableiw iFreqRatio, iPartialIndex, gifreq
|
||||
tableiw iAmp, iPartialIndex, giamp
|
||||
tableiw iFreqRatio2, iPartialIndex, gifreq2
|
||||
tableiw iAmp2, iPartialIndex, giamp2
|
||||
|
||||
iPartialIndex = iPartialIndex + 1
|
||||
if iPartialIndex < iPartialCount goto loop_setup
|
||||
|
||||
; Generate additive synthesis with heavy modulation and octave doubling
|
||||
|
||||
; Slow modulation LFOs for spectral evolution
|
||||
kLFO1 oscili 1, iAmpLFORate * 0.53
|
||||
kLFO2 oscili 1, iAmpLFORate * 0.89
|
||||
kLFO3 oscili 1, iAmpLFORate * 1.37
|
||||
kShimmerLFO1 oscili 1, iShimmerRate * 0.67
|
||||
kShimmerLFO2 oscili 1, iShimmerRate * 1.13
|
||||
|
||||
; Very slow spectral morphing
|
||||
kSpectralMorph oscili 1, iAmpLFORate * 0.19
|
||||
kMorphAmount = (kSpectralMorph + 1) * 0.5
|
||||
|
||||
; Minimal frequency wobble for organic feel
|
||||
kFreqWobble = kFreqLFO * 0.5
|
||||
|
||||
; Micro-detuning for beating (0.1 to 0.5 Hz beating)
|
||||
iMicroDetune = 0.0003 + (iChaos * 0.0005)
|
||||
|
||||
; === FUNDAMENTAL OCTAVE ===
|
||||
; Main voice at fundamental frequency
|
||||
kcps = iBaseFreq * (1 + kFreqWobble)
|
||||
kAmpMain = kEnv * (0.6 + kLFO1 * iAmpLFODepth * 0.2)
|
||||
aMainVoice adsynt2 kAmpMain, kcps, gisine, gifreq, giamp, iPartialCount
|
||||
|
||||
; Slightly detuned fundamental for beating
|
||||
kcpsDetune = iBaseFreq * (1 + iMicroDetune) * (1 + kFreqWobble * 0.93)
|
||||
kAmpDetune = kEnv * (0.5 + kLFO2 * iAmpLFODepth * 0.25)
|
||||
aDetuneVoice adsynt2 kAmpDetune, kcpsDetune, gisine, gifreq, giamp, iPartialCount, 0.25
|
||||
|
||||
; Morphing spectral variant
|
||||
kAmpMorph = kEnv * kMorphAmount * (0.4 + kShimmerLFO1 * iShimmer * 0.3)
|
||||
aMorphVoice adsynt2 kAmpMorph, kcps, gisine, gifreq2, giamp2, iPartialCount, 0.5
|
||||
|
||||
; === OCTAVE UP ===
|
||||
; One octave higher with micro-detune for beating
|
||||
kcpsOctUp = (iBaseFreq * 2) * (1 + kFreqWobble * 1.07)
|
||||
kAmpOctUp = kEnv * (0.35 + kLFO3 * iAmpLFODepth * 0.2 + kShimmerLFO2 * iShimmer * 0.25)
|
||||
aOctaveUp adsynt2 kAmpOctUp, kcpsOctUp, gisine, gifreq, giamp, iPartialCount, 0.33
|
||||
|
||||
; Detuned octave up for complex beating
|
||||
kcpsOctUpDetune = (iBaseFreq * 2) * (1 - iMicroDetune * 0.8) * (1 + kFreqWobble * 1.11)
|
||||
kAmpOctUpDetune = kEnv * (0.3 + kLFO1 * iAmpLFODepth * 0.25)
|
||||
aOctaveUpDetune adsynt2 kAmpOctUpDetune, kcpsOctUpDetune, gisine, gifreq, giamp, iPartialCount, 0.67
|
||||
|
||||
; === OCTAVE DOWN ===
|
||||
; One octave lower with micro-detune
|
||||
kcpsOctDown = (iBaseFreq * 0.5) * (1 + kFreqWobble * 0.97)
|
||||
kAmpOctDown = kEnv * (0.4 + kShimmerLFO1 * iShimmer * 0.3)
|
||||
aOctaveDown adsynt2 kAmpOctDown, kcpsOctDown, gisine, gifreq, giamp, iPartialCount, 0.17
|
||||
|
||||
; Detuned octave down for sub-harmonic beating
|
||||
kcpsOctDownDetune = (iBaseFreq * 0.5) * (1 + iMicroDetune * 1.2) * (1 + kFreqWobble * 1.03)
|
||||
kAmpOctDownDetune = kEnv * (0.35 + kLFO2 * iAmpLFODepth * 0.2)
|
||||
aOctaveDownDetune adsynt2 kAmpOctDownDetune, kcpsOctDownDetune, gisine, gifreq2, giamp, iPartialCount, 0.83
|
||||
|
||||
; === STEREO MIXING ===
|
||||
; Left channel: emphasize fundamental and octave down
|
||||
aOutL = aMainVoice * 0.7 + aDetuneVoice * 0.6 + aMorphVoice * 0.5
|
||||
aOutL = aOutL + aOctaveUp * 0.4 + aOctaveUpDetune * 0.35
|
||||
aOutL = aOutL + aOctaveDown * 0.55 + aOctaveDownDetune * 0.45
|
||||
|
||||
; Right channel: emphasize octaves with different balance
|
||||
aOutR = aMainVoice * 0.65 + aDetuneVoice * 0.55 + aMorphVoice * 0.6
|
||||
aOutR = aOutR + aOctaveUp * 0.5 + aOctaveUpDetune * 0.4
|
||||
aOutR = aOutR + aOctaveDown * 0.45 + aOctaveDownDetune * 0.5
|
||||
|
||||
; Subtle stereo width from chaos
|
||||
kStereoMod = kChaosLFO1 * iChaos * 0.1
|
||||
aOutL = aOutL * (1 - kStereoMod * iStereoSpread)
|
||||
aOutR = aOutR * (1 + kStereoMod * iStereoSpread)
|
||||
|
||||
; Normalize to prevent clipping
|
||||
aOutL = aOutL * 0.28
|
||||
aOutR = aOutR * 0.28
|
||||
|
||||
outs aOutL, aOutR
|
||||
endin
|
||||
`;
|
||||
}
|
||||
|
||||
protected getParametersForCsound(params: MassiveAdditiveParams): CsoundParameter[] {
|
||||
return [
|
||||
{ channelName: 'baseFreq', value: params.baseFreq },
|
||||
{ channelName: 'numPartials', value: params.numPartials },
|
||||
{ channelName: 'spectrumShape', value: params.spectrumShape },
|
||||
{ channelName: 'harmonicSpread', value: params.harmonicSpread },
|
||||
{ channelName: 'inharmonicity', value: params.inharmonicity },
|
||||
{ channelName: 'attack', value: params.attack },
|
||||
{ channelName: 'decay', value: params.decay },
|
||||
{ channelName: 'sustain', value: params.sustain },
|
||||
{ channelName: 'release', value: params.release },
|
||||
{ channelName: 'ampLFORate', value: params.ampLFORate },
|
||||
{ channelName: 'ampLFODepth', value: params.ampLFODepth },
|
||||
{ channelName: 'ampLFOPhaseSpread', value: params.ampLFOPhaseSpread },
|
||||
{ channelName: 'freqLFORate', value: params.freqLFORate },
|
||||
{ channelName: 'freqLFODepth', value: params.freqLFODepth },
|
||||
{ channelName: 'partialDecayRate', value: params.partialDecayRate },
|
||||
{ channelName: 'partialAttackSpread', value: params.partialAttackSpread },
|
||||
{ channelName: 'oddEvenBalance', value: params.oddEvenBalance },
|
||||
{ channelName: 'shimmer', value: params.shimmer },
|
||||
{ channelName: 'shimmerRate', value: params.shimmerRate },
|
||||
{ channelName: 'stereoSpread', value: params.stereoSpread },
|
||||
{ channelName: 'brightness', value: params.brightness },
|
||||
{ channelName: 'chaos', value: params.chaos },
|
||||
];
|
||||
}
|
||||
|
||||
randomParams(pitchLock?: PitchLock): MassiveAdditiveParams {
|
||||
const baseFreqChoices = [55, 82.4, 110, 146.8, 220, 293.7, 440];
|
||||
const baseFreq = pitchLock?.enabled
|
||||
? pitchLock.frequency
|
||||
: this.randomChoice(baseFreqChoices) * this.randomRange(0.98, 1.02);
|
||||
|
||||
return {
|
||||
baseFreq,
|
||||
numPartials: this.randomInt(32, 64),
|
||||
spectrumShape: this.randomRange(0.3, 0.7),
|
||||
harmonicSpread: this.randomChoice([1, 1.5, 2, 3]),
|
||||
inharmonicity: this.randomRange(0, 0.5),
|
||||
attack: this.randomRange(0.05, 0.3),
|
||||
decay: this.randomRange(0.15, 0.5),
|
||||
sustain: this.randomRange(0.5, 0.9),
|
||||
release: this.randomRange(0.2, 0.6),
|
||||
ampLFORate: this.randomRange(0.2, 2),
|
||||
ampLFODepth: this.randomRange(0.3, 0.8),
|
||||
ampLFOPhaseSpread: this.randomRange(0.5, 1),
|
||||
freqLFORate: this.randomRange(0.1, 1.5),
|
||||
freqLFODepth: this.randomRange(0.05, 0.3),
|
||||
partialDecayRate: this.randomRange(0.3, 0.8),
|
||||
partialAttackSpread: this.randomRange(0.1, 0.5),
|
||||
oddEvenBalance: this.randomRange(0.5, 1.5),
|
||||
shimmer: this.randomRange(0.3, 0.9),
|
||||
shimmerRate: this.randomRange(0.05, 0.8),
|
||||
stereoSpread: this.randomRange(0.4, 1),
|
||||
brightness: this.randomRange(0.3, 0.9),
|
||||
chaos: this.randomRange(0.1, 0.7),
|
||||
};
|
||||
}
|
||||
|
||||
mutateParams(
|
||||
params: MassiveAdditiveParams,
|
||||
mutationAmount: number = 0.15,
|
||||
pitchLock?: PitchLock
|
||||
): MassiveAdditiveParams {
|
||||
const baseFreq = pitchLock?.enabled ? pitchLock.frequency : params.baseFreq;
|
||||
|
||||
return {
|
||||
baseFreq,
|
||||
numPartials:
|
||||
Math.random() < 0.15
|
||||
? this.randomInt(16, 64)
|
||||
: Math.round(this.mutateValue(params.numPartials, mutationAmount, 16, 64)),
|
||||
spectrumShape: this.mutateValue(params.spectrumShape, mutationAmount, 0, 1),
|
||||
harmonicSpread:
|
||||
Math.random() < 0.1
|
||||
? this.randomChoice([0.5, 1, 1.5, 2])
|
||||
: params.harmonicSpread,
|
||||
inharmonicity: this.mutateValue(params.inharmonicity, mutationAmount, 0, 0.5),
|
||||
attack: this.mutateValue(params.attack, mutationAmount, 0.001, 0.4),
|
||||
decay: this.mutateValue(params.decay, mutationAmount, 0.05, 0.6),
|
||||
sustain: this.mutateValue(params.sustain, mutationAmount, 0.2, 0.9),
|
||||
release: this.mutateValue(params.release, mutationAmount, 0.05, 0.8),
|
||||
ampLFORate: this.mutateValue(params.ampLFORate, mutationAmount, 0.1, 6),
|
||||
ampLFODepth: this.mutateValue(params.ampLFODepth, mutationAmount, 0, 1),
|
||||
ampLFOPhaseSpread: this.mutateValue(params.ampLFOPhaseSpread, mutationAmount, 0, 1),
|
||||
freqLFORate: this.mutateValue(params.freqLFORate, mutationAmount, 0.1, 5),
|
||||
freqLFODepth: this.mutateValue(params.freqLFODepth, mutationAmount, 0, 0.8),
|
||||
partialDecayRate: this.mutateValue(params.partialDecayRate, mutationAmount, 0, 1),
|
||||
partialAttackSpread: this.mutateValue(params.partialAttackSpread, mutationAmount, 0, 0.8),
|
||||
oddEvenBalance: this.mutateValue(params.oddEvenBalance, mutationAmount, 0.2, 1.8),
|
||||
shimmer: this.mutateValue(params.shimmer, mutationAmount, 0, 1),
|
||||
shimmerRate: this.mutateValue(params.shimmerRate, mutationAmount, 0.05, 2),
|
||||
stereoSpread: this.mutateValue(params.stereoSpread, mutationAmount, 0, 1),
|
||||
brightness: this.mutateValue(params.brightness, mutationAmount, 0, 1),
|
||||
chaos: this.mutateValue(params.chaos, mutationAmount, 0, 0.8),
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -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 'NPerc';
|
||||
return 'Noise Perc';
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
@ -50,6 +50,10 @@ export class NoiseDrum implements SynthEngine {
|
||||
return 'generative' as const;
|
||||
}
|
||||
|
||||
getCategory() {
|
||||
return 'Percussion' as const;
|
||||
}
|
||||
|
||||
randomParams(): NoiseDrumParams {
|
||||
// Intelligent parameter generation based on correlated characteristics
|
||||
|
||||
@ -93,16 +97,16 @@ export class NoiseDrum implements SynthEngine {
|
||||
const filterType = filterFreq < 0.3 ?
|
||||
Math.random() * 0.35 : // Low freq prefers lowpass
|
||||
filterFreq > 0.7 ?
|
||||
0.5 + Math.random() * 0.5 : // High freq prefers highpass/bandpass
|
||||
Math.random(); // Mid freq - any type
|
||||
0.5 + Math.random() * 0.5 : // High freq prefers highpass/bandpass
|
||||
Math.random(); // Mid freq - any type
|
||||
|
||||
// Decay time inversely correlates with frequency
|
||||
const decayBias = Math.random();
|
||||
const ampDecay = filterFreq < 0.3 ?
|
||||
0.25 + decayBias * 0.4 : // Low freq can be longer
|
||||
filterFreq > 0.6 ?
|
||||
0.08 + decayBias * 0.35 : // High freq shorter
|
||||
0.2 + decayBias * 0.45; // Mid range
|
||||
0.08 + decayBias * 0.35 : // High freq shorter
|
||||
0.2 + decayBias * 0.45; // Mid range
|
||||
|
||||
// Attack is generally very short for percussion
|
||||
const ampAttack = Math.random() < 0.85 ?
|
||||
@ -372,7 +376,7 @@ export class NoiseDrum implements SynthEngine {
|
||||
|
||||
// Blend body resonance - SUBTLE
|
||||
sample = sample * (1 - params.bodyAmount * 0.4) +
|
||||
bodyFiltered.output * params.bodyAmount * 0.6 * bodyEnv;
|
||||
bodyFiltered.output * params.bodyAmount * 0.6 * bodyEnv;
|
||||
}
|
||||
|
||||
// Apply amplitude envelope
|
||||
@ -434,7 +438,7 @@ export class NoiseDrum implements SynthEngine {
|
||||
pinkState[5] = -0.7616 * pinkState[5] - whiteNoise * 0.0168980;
|
||||
|
||||
const pink = pinkState[0] + pinkState[1] + pinkState[2] + pinkState[3] +
|
||||
pinkState[4] + pinkState[5] + pinkState[6] + whiteNoise * 0.5362;
|
||||
pinkState[4] + pinkState[5] + pinkState[6] + whiteNoise * 0.5362;
|
||||
pinkState[6] = whiteNoise * 0.115926;
|
||||
|
||||
return pink * 0.11;
|
||||
|
||||
394
src/lib/audio/engines/ParticleNoise.ts
Normal file
394
src/lib/audio/engines/ParticleNoise.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import type { SynthEngine, PitchLock } from './SynthEngine';
|
||||
import type { SynthEngine, PitchLock } from './base/SynthEngine';
|
||||
|
||||
enum PDWaveform {
|
||||
Sine,
|
||||
@ -65,7 +65,7 @@ export class PhaseDistortionFM implements SynthEngine<PhaseDistortionFMParams> {
|
||||
private static workletURL: string | null = null;
|
||||
|
||||
getName(): string {
|
||||
return 'PD';
|
||||
return 'Phase Dist';
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
@ -76,6 +76,10 @@ export class PhaseDistortionFM implements SynthEngine<PhaseDistortionFMParams> {
|
||||
return 'generative' as const;
|
||||
}
|
||||
|
||||
getCategory() {
|
||||
return 'FM' as const;
|
||||
}
|
||||
|
||||
generate(params: PhaseDistortionFMParams, sampleRate: number, duration: number): [Float32Array, Float32Array] {
|
||||
const numSamples = Math.floor(sampleRate * duration);
|
||||
const leftBuffer = new Float32Array(numSamples);
|
||||
|
||||
@ -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);
|
||||
|
||||
178
src/lib/audio/engines/RingCymbal.ts
Normal file
178
src/lib/audio/engines/RingCymbal.ts
Normal file
@ -0,0 +1,178 @@
|
||||
import { CsoundEngine, type CsoundParameter } from './base/CsoundEngine';
|
||||
import type { PitchLock } from './base/SynthEngine';
|
||||
|
||||
interface RingCymbalParams {
|
||||
baseFreq: number;
|
||||
overtone1Freq: number;
|
||||
overtone2Freq: number;
|
||||
overtone3Freq: number;
|
||||
overtone1Vol: number;
|
||||
overtone2Vol: number;
|
||||
overtone3Vol: number;
|
||||
filterCutoff: number;
|
||||
resonance: number;
|
||||
decay: number;
|
||||
attack: number;
|
||||
noise: number;
|
||||
brightness: number;
|
||||
spread: number;
|
||||
}
|
||||
|
||||
export class RingCymbal extends CsoundEngine<RingCymbalParams> {
|
||||
getName(): string {
|
||||
return 'Ring Cymbal';
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return 'Metallic cymbal using ring modulation with noise and multiple oscillators';
|
||||
}
|
||||
|
||||
getType() {
|
||||
return 'generative' as const;
|
||||
}
|
||||
|
||||
getCategory() {
|
||||
return 'Percussion' as const;
|
||||
}
|
||||
|
||||
protected getOrchestra(): string {
|
||||
return `
|
||||
instr 1
|
||||
iBaseFreq chnget "baseFreq"
|
||||
iOvertone1Freq chnget "overtone1Freq"
|
||||
iOvertone2Freq chnget "overtone2Freq"
|
||||
iOvertone3Freq chnget "overtone3Freq"
|
||||
iOvertone1Vol chnget "overtone1Vol"
|
||||
iOvertone2Vol chnget "overtone2Vol"
|
||||
iOvertone3Vol chnget "overtone3Vol"
|
||||
iFilterCutoff chnget "filterCutoff"
|
||||
iResonance chnget "resonance"
|
||||
iDecay chnget "decay"
|
||||
iAttack chnget "attack"
|
||||
iNoise chnget "noise"
|
||||
iBrightness chnget "brightness"
|
||||
iSpread chnget "spread"
|
||||
|
||||
idur = p3
|
||||
iDecayTime = iDecay * idur
|
||||
iAttackTime = iAttack * idur
|
||||
|
||||
; Exponential decay envelope with attack
|
||||
kEnv linseg 0, iAttackTime, 1, iDecayTime - iAttackTime, 0.001, 0.001, 0
|
||||
kEnv = kEnv * kEnv
|
||||
|
||||
; Generate white noise source
|
||||
aNoise noise 1, 0
|
||||
|
||||
; Generate impulse oscillators at different frequencies
|
||||
aOsc1 oscili 1, iBaseFreq
|
||||
aOsc2 oscili iOvertone1Vol, iOvertone1Freq
|
||||
aOsc3 oscili iOvertone2Vol, iOvertone2Freq
|
||||
aOsc4 oscili iOvertone3Vol, iOvertone3Freq
|
||||
|
||||
; Ring modulation: multiply noise with oscillators
|
||||
aRing1 = aNoise * aOsc1
|
||||
aRing2 = aNoise * aOsc2
|
||||
aRing3 = aNoise * aOsc3
|
||||
aRing4 = aNoise * aOsc4
|
||||
|
||||
; Mix ring modulated signals
|
||||
aMix = (aRing1 + aRing2 + aRing3 + aRing4) * 0.25
|
||||
|
||||
; Add raw noise for character
|
||||
aMix = aMix * (1 - iNoise) + aNoise * iNoise * 0.3
|
||||
|
||||
; Apply resonant high-pass filter for metallic character
|
||||
aFiltered butterhp aMix, iFilterCutoff, iResonance
|
||||
|
||||
; Additional high-pass for brightness
|
||||
if iBrightness > 0.3 then
|
||||
aFiltered butterhp aFiltered, iFilterCutoff * (1 + iBrightness), iResonance * 0.5
|
||||
endif
|
||||
|
||||
; Apply envelope
|
||||
aOut = aFiltered * kEnv * 0.4
|
||||
|
||||
; Stereo spread using slightly different filter parameters
|
||||
iFilterCutoffR = iFilterCutoff * (1 + iSpread * 0.1)
|
||||
aFilteredR butterhp aMix, iFilterCutoffR, iResonance
|
||||
|
||||
if iBrightness > 0.3 then
|
||||
aFilteredR butterhp aFilteredR, iFilterCutoffR * (1 + iBrightness), iResonance * 0.5
|
||||
endif
|
||||
|
||||
aOutR = aFilteredR * kEnv * 0.4
|
||||
|
||||
outs aOut, aOutR
|
||||
endin
|
||||
`;
|
||||
}
|
||||
|
||||
protected getParametersForCsound(params: RingCymbalParams): CsoundParameter[] {
|
||||
return [
|
||||
{ channelName: 'baseFreq', value: params.baseFreq },
|
||||
{ channelName: 'overtone1Freq', value: params.overtone1Freq },
|
||||
{ channelName: 'overtone2Freq', value: params.overtone2Freq },
|
||||
{ channelName: 'overtone3Freq', value: params.overtone3Freq },
|
||||
{ channelName: 'overtone1Vol', value: params.overtone1Vol },
|
||||
{ channelName: 'overtone2Vol', value: params.overtone2Vol },
|
||||
{ channelName: 'overtone3Vol', value: params.overtone3Vol },
|
||||
{ channelName: 'filterCutoff', value: params.filterCutoff },
|
||||
{ channelName: 'resonance', value: params.resonance },
|
||||
{ channelName: 'decay', value: params.decay },
|
||||
{ channelName: 'attack', value: params.attack },
|
||||
{ channelName: 'noise', value: params.noise },
|
||||
{ channelName: 'brightness', value: params.brightness },
|
||||
{ channelName: 'spread', value: params.spread },
|
||||
];
|
||||
}
|
||||
|
||||
randomParams(pitchLock?: PitchLock): RingCymbalParams {
|
||||
const baseFreq = pitchLock?.enabled ? pitchLock.frequency : this.randomRange(800, 1800);
|
||||
|
||||
const inharmonicRatios = [1.4, 1.7, 2.1, 2.3, 2.9, 3.1, 3.7, 4.3];
|
||||
|
||||
return {
|
||||
baseFreq,
|
||||
overtone1Freq: baseFreq * this.randomChoice(inharmonicRatios),
|
||||
overtone2Freq: baseFreq * this.randomChoice(inharmonicRatios),
|
||||
overtone3Freq: baseFreq * this.randomChoice(inharmonicRatios),
|
||||
overtone1Vol: this.randomRange(0.4, 1.0),
|
||||
overtone2Vol: this.randomRange(0.3, 0.9),
|
||||
overtone3Vol: this.randomRange(0.2, 0.7),
|
||||
filterCutoff: this.randomRange(3000, 8000),
|
||||
resonance: this.randomRange(2, 8),
|
||||
decay: this.randomRange(0.3, 0.9),
|
||||
attack: this.randomRange(0.001, 0.02),
|
||||
noise: this.randomRange(0.1, 0.5),
|
||||
brightness: this.randomRange(0, 0.8),
|
||||
spread: this.randomRange(0.01, 0.15),
|
||||
};
|
||||
}
|
||||
|
||||
mutateParams(
|
||||
params: RingCymbalParams,
|
||||
mutationAmount: number = 0.15,
|
||||
pitchLock?: PitchLock
|
||||
): RingCymbalParams {
|
||||
const baseFreq = pitchLock?.enabled ? pitchLock.frequency : params.baseFreq;
|
||||
const freqRatio = baseFreq / params.baseFreq;
|
||||
|
||||
return {
|
||||
baseFreq,
|
||||
overtone1Freq: this.mutateValue(params.overtone1Freq * freqRatio, mutationAmount, baseFreq * 1.2, baseFreq * 5),
|
||||
overtone2Freq: this.mutateValue(params.overtone2Freq * freqRatio, mutationAmount, baseFreq * 1.2, baseFreq * 5),
|
||||
overtone3Freq: this.mutateValue(params.overtone3Freq * freqRatio, mutationAmount, baseFreq * 1.2, baseFreq * 5),
|
||||
overtone1Vol: this.mutateValue(params.overtone1Vol, mutationAmount, 0.2, 1.0),
|
||||
overtone2Vol: this.mutateValue(params.overtone2Vol, mutationAmount, 0.1, 1.0),
|
||||
overtone3Vol: this.mutateValue(params.overtone3Vol, mutationAmount, 0.1, 0.9),
|
||||
filterCutoff: this.mutateValue(params.filterCutoff, mutationAmount, 2000, 10000),
|
||||
resonance: this.mutateValue(params.resonance, mutationAmount, 1, 12),
|
||||
decay: this.mutateValue(params.decay, mutationAmount, 0.2, 1.0),
|
||||
attack: this.mutateValue(params.attack, mutationAmount, 0.001, 0.05),
|
||||
noise: this.mutateValue(params.noise, mutationAmount, 0, 0.7),
|
||||
brightness: this.mutateValue(params.brightness, mutationAmount, 0, 1),
|
||||
spread: this.mutateValue(params.spread, mutationAmount, 0.005, 0.25),
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { PitchLock, SynthEngine } from './SynthEngine';
|
||||
import type { PitchLock, SynthEngine } from './base/SynthEngine';
|
||||
|
||||
interface SnareParams {
|
||||
// Core frequency (base pitch of the snare)
|
||||
@ -23,7 +23,7 @@ interface SnareParams {
|
||||
|
||||
export class Snare implements SynthEngine {
|
||||
getName(): string {
|
||||
return 'Snare';
|
||||
return 'Noise Snare';
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
@ -34,6 +34,10 @@ export class Snare implements SynthEngine {
|
||||
return 'generative' as const;
|
||||
}
|
||||
|
||||
getCategory() {
|
||||
return 'Percussion' as const;
|
||||
}
|
||||
|
||||
randomParams(pitchLock?: PitchLock): SnareParams {
|
||||
return {
|
||||
baseFreq: pitchLock ? this.freqToParam(pitchLock.frequency) : 0.3 + Math.random() * 0.4,
|
||||
|
||||
149
src/lib/audio/engines/Squine1.ts
Normal file
149
src/lib/audio/engines/Squine1.ts
Normal file
@ -0,0 +1,149 @@
|
||||
import { CsoundEngine, type CsoundParameter } from './base/CsoundEngine';
|
||||
import type { PitchLock } from './base/SynthEngine';
|
||||
|
||||
interface Squine1Params {
|
||||
frequency: number;
|
||||
shapeAmount1: number;
|
||||
shapeAmount2: number;
|
||||
modulationDepth1: number;
|
||||
modulationDepth2: number;
|
||||
osc2Detune: number;
|
||||
osc2Level: number;
|
||||
hpfCutoff: number;
|
||||
attack: number;
|
||||
decay: number;
|
||||
sustain: number;
|
||||
release: number;
|
||||
}
|
||||
|
||||
export class Squine1 extends CsoundEngine<Squine1Params> {
|
||||
getName(): string {
|
||||
return 'Squine1';
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return 'Squinewave synthesizer with waveshaping and dual oscillators';
|
||||
}
|
||||
|
||||
getType(): 'generative' | 'sample' | 'input' {
|
||||
return 'generative';
|
||||
}
|
||||
|
||||
getCategory(): 'Additive' | 'Subtractive' | 'FM' | 'Percussion' | 'Noise' | 'Physical' | 'Modulation' | 'Experimental' | 'Utility' {
|
||||
return 'Subtractive';
|
||||
}
|
||||
|
||||
getOrchestra(): string {
|
||||
return `
|
||||
instr 1
|
||||
ifreq = chnget("frequency")
|
||||
iamp = 0.5
|
||||
ishape1 = chnget("shapeAmount1")
|
||||
ishape2 = chnget("shapeAmount2")
|
||||
imod1 = chnget("modulationDepth1")
|
||||
imod2 = chnget("modulationDepth2")
|
||||
idetune = chnget("osc2Detune")
|
||||
iosc2Level = chnget("osc2Level")
|
||||
ihpf = chnget("hpfCutoff")
|
||||
iatt = chnget("attack") * p3
|
||||
idec = chnget("decay") * p3
|
||||
isus = chnget("sustain")
|
||||
irel = chnget("release") * p3
|
||||
|
||||
; First oscillator with waveshaping (squine approximation)
|
||||
aosc1 = poscil(1, ifreq)
|
||||
|
||||
; Create dynamic shape envelope for osc1
|
||||
kshape1 = expseg(ishape1, p3, ishape1 * imod1)
|
||||
|
||||
; Waveshaping: blend between sine and square-like
|
||||
asig1 = tanh(aosc1 * kshape1 * 5) / tanh(kshape1 * 5)
|
||||
|
||||
; Second oscillator slightly detuned
|
||||
aosc2 = poscil(1, ifreq * (1 + idetune))
|
||||
|
||||
; Create dynamic shape envelope for osc2
|
||||
kshape2 = expseg(ishape2, p3, ishape2 * imod2)
|
||||
|
||||
; Waveshaping for second oscillator
|
||||
asig2 = tanh(aosc2 * kshape2 * 5) / tanh(kshape2 * 5)
|
||||
|
||||
; Mix oscillators
|
||||
asig = asig1 + (asig2 * iosc2Level)
|
||||
|
||||
; High-pass filter to remove DC and rumble
|
||||
kcutoff = max(ihpf, 20)
|
||||
asig = butterhp(asig, kcutoff)
|
||||
|
||||
; Amplitude envelope
|
||||
aenv = madsr(iatt, idec, isus, irel)
|
||||
asig = asig * aenv * iamp
|
||||
|
||||
; DC blocker
|
||||
asig = dcblock2(asig)
|
||||
|
||||
outs asig, asig
|
||||
endin
|
||||
`;
|
||||
}
|
||||
|
||||
getParametersForCsound(params: Squine1Params): CsoundParameter[] {
|
||||
return [
|
||||
{ channelName: 'frequency', value: params.frequency },
|
||||
{ channelName: 'shapeAmount1', value: params.shapeAmount1 },
|
||||
{ channelName: 'shapeAmount2', value: params.shapeAmount2 },
|
||||
{ channelName: 'modulationDepth1', value: params.modulationDepth1 },
|
||||
{ channelName: 'modulationDepth2', value: params.modulationDepth2 },
|
||||
{ channelName: 'osc2Detune', value: params.osc2Detune },
|
||||
{ channelName: 'osc2Level', value: params.osc2Level },
|
||||
{ channelName: 'hpfCutoff', value: params.hpfCutoff },
|
||||
{ channelName: 'attack', value: params.attack },
|
||||
{ channelName: 'decay', value: params.decay },
|
||||
{ channelName: 'sustain', value: params.sustain },
|
||||
{ channelName: 'release', value: params.release }
|
||||
];
|
||||
}
|
||||
|
||||
randomParams(pitchLock?: PitchLock): Squine1Params {
|
||||
const frequency = pitchLock?.enabled ? pitchLock.frequency : 55 * Math.pow(2, Math.random() * 5);
|
||||
|
||||
return {
|
||||
frequency,
|
||||
shapeAmount1: 0.2 + Math.random() * 0.8,
|
||||
shapeAmount2: 0.2 + Math.random() * 0.8,
|
||||
modulationDepth1: 0.05 + Math.random() * 0.7,
|
||||
modulationDepth2: 0.05 + Math.random() * 0.7,
|
||||
osc2Detune: 0.0001 + Math.random() * 0.005,
|
||||
osc2Level: 0.01 + Math.random() * 0.15,
|
||||
hpfCutoff: frequency * 0.5 + Math.random() * frequency,
|
||||
attack: 0.001 + Math.random() * 0.05,
|
||||
decay: 0.05 + Math.random() * 0.3,
|
||||
sustain: 0.2 + Math.random() * 0.6,
|
||||
release: 0.01 + Math.random() * 0.3
|
||||
};
|
||||
}
|
||||
|
||||
mutateParams(params: Squine1Params, mutationAmount = 0.2, pitchLock?: PitchLock): Squine1Params {
|
||||
const mutate = (value: number, min: number, max: number) => {
|
||||
const change = (Math.random() - 0.5) * 2 * mutationAmount * (max - min);
|
||||
return Math.max(min, Math.min(max, value + change));
|
||||
};
|
||||
|
||||
const newFreq = pitchLock?.enabled ? pitchLock.frequency : mutate(params.frequency, 55, 55 * Math.pow(2, 5));
|
||||
|
||||
return {
|
||||
frequency: newFreq,
|
||||
shapeAmount1: mutate(params.shapeAmount1, 0.2, 1.0),
|
||||
shapeAmount2: mutate(params.shapeAmount2, 0.2, 1.0),
|
||||
modulationDepth1: mutate(params.modulationDepth1, 0.05, 0.75),
|
||||
modulationDepth2: mutate(params.modulationDepth2, 0.05, 0.75),
|
||||
osc2Detune: mutate(params.osc2Detune, 0.0001, 0.0051),
|
||||
osc2Level: mutate(params.osc2Level, 0.01, 0.16),
|
||||
hpfCutoff: mutate(params.hpfCutoff, newFreq * 0.5, newFreq * 1.5),
|
||||
attack: mutate(params.attack, 0.001, 0.051),
|
||||
decay: mutate(params.decay, 0.05, 0.35),
|
||||
sustain: mutate(params.sustain, 0.2, 0.8),
|
||||
release: mutate(params.release, 0.01, 0.31)
|
||||
};
|
||||
}
|
||||
}
|
||||
135
src/lib/audio/engines/Sub8.ts
Normal file
135
src/lib/audio/engines/Sub8.ts
Normal file
@ -0,0 +1,135 @@
|
||||
import { CsoundEngine, type CsoundParameter } from './base/CsoundEngine';
|
||||
import type { PitchLock } from './base/SynthEngine';
|
||||
|
||||
interface Sub8Params {
|
||||
frequency: number;
|
||||
filterCutoff: number;
|
||||
filterEnvAmount: number;
|
||||
resonance: number;
|
||||
saturation: number;
|
||||
osc1Level: number;
|
||||
osc2Level: number;
|
||||
osc3Level: number;
|
||||
attack: number;
|
||||
decay: number;
|
||||
sustain: number;
|
||||
release: number;
|
||||
}
|
||||
|
||||
export class Sub8 extends CsoundEngine<Sub8Params> {
|
||||
getName(): string {
|
||||
return 'Sub8';
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return 'Multi-oscillator subtractive synth with diode ladder filter and saturation';
|
||||
}
|
||||
|
||||
getType(): 'generative' | 'sample' | 'input' {
|
||||
return 'generative';
|
||||
}
|
||||
|
||||
getCategory(): 'Additive' | 'Subtractive' | 'FM' | 'Percussion' | 'Noise' | 'Physical' | 'Modulation' | 'Experimental' | 'Utility' {
|
||||
return 'Subtractive';
|
||||
}
|
||||
|
||||
getOrchestra(): string {
|
||||
return `
|
||||
instr 1
|
||||
ifreq = chnget("frequency")
|
||||
iamp = 0.5
|
||||
ifilterCutoff = chnget("filterCutoff")
|
||||
ifilterEnvAmount = chnget("filterEnvAmount")
|
||||
ireso = chnget("resonance")
|
||||
isat = chnget("saturation")
|
||||
iosc1Level = chnget("osc1Level")
|
||||
iosc2Level = chnget("osc2Level")
|
||||
iosc3Level = chnget("osc3Level")
|
||||
iatt = chnget("attack") * p3
|
||||
idec = chnget("decay") * p3
|
||||
isus = chnget("sustain")
|
||||
irel = chnget("release") * p3
|
||||
|
||||
; Three detuned oscillators
|
||||
asig1 = vco2(iamp * iosc1Level, ifreq, 10)
|
||||
asig2 = vco2(iamp * iosc2Level, ifreq * 2, 10)
|
||||
asig3 = vco2(iamp * iosc3Level, ifreq * 3.5, 12)
|
||||
asig = asig1 + asig2 + asig3
|
||||
|
||||
; Saturation
|
||||
asig = tanh(asig * isat) / tanh(isat)
|
||||
|
||||
; Filter envelope
|
||||
aenv = madsr(iatt, idec, isus, irel)
|
||||
kcutoff = ifilterCutoff + (aenv * ifilterEnvAmount)
|
||||
kcutoff = limit(kcutoff, 20, 18000)
|
||||
|
||||
; Diode ladder filter
|
||||
asig = diode_ladder(asig, kcutoff, ireso)
|
||||
|
||||
; Final amplitude envelope
|
||||
asig = asig * aenv * 0.7
|
||||
|
||||
outs asig, asig
|
||||
endin
|
||||
`;
|
||||
}
|
||||
|
||||
getParametersForCsound(params: Sub8Params): CsoundParameter[] {
|
||||
return [
|
||||
{ channelName: 'frequency', value: params.frequency },
|
||||
{ channelName: 'filterCutoff', value: params.filterCutoff },
|
||||
{ channelName: 'filterEnvAmount', value: params.filterEnvAmount },
|
||||
{ channelName: 'resonance', value: params.resonance },
|
||||
{ channelName: 'saturation', value: params.saturation },
|
||||
{ channelName: 'osc1Level', value: params.osc1Level },
|
||||
{ channelName: 'osc2Level', value: params.osc2Level },
|
||||
{ channelName: 'osc3Level', value: params.osc3Level },
|
||||
{ channelName: 'attack', value: params.attack },
|
||||
{ channelName: 'decay', value: params.decay },
|
||||
{ channelName: 'sustain', value: params.sustain },
|
||||
{ channelName: 'release', value: params.release }
|
||||
];
|
||||
}
|
||||
|
||||
randomParams(pitchLock?: PitchLock): Sub8Params {
|
||||
const frequency = pitchLock?.enabled ? pitchLock.frequency : 55 * Math.pow(2, Math.random() * 5);
|
||||
|
||||
return {
|
||||
frequency,
|
||||
filterCutoff: 200 + Math.random() * 3800,
|
||||
filterEnvAmount: Math.random() * 6000,
|
||||
resonance: 0.5 + Math.random() * 14.5,
|
||||
saturation: 1 + Math.random() * 9,
|
||||
osc1Level: 0.5 + Math.random() * 0.5,
|
||||
osc2Level: Math.random() * 0.5,
|
||||
osc3Level: Math.random() * 0.3,
|
||||
attack: Math.random() * 0.1,
|
||||
decay: 0.05 + Math.random() * 0.3,
|
||||
sustain: 0.2 + Math.random() * 0.6,
|
||||
release: 0.05 + Math.random() * 0.4
|
||||
};
|
||||
}
|
||||
|
||||
mutateParams(params: Sub8Params, mutationAmount = 0.2, pitchLock?: PitchLock): Sub8Params {
|
||||
const mutate = (value: number, min: number, max: number) => {
|
||||
const change = (Math.random() - 0.5) * 2 * mutationAmount * (max - min);
|
||||
return Math.max(min, Math.min(max, value + change));
|
||||
};
|
||||
|
||||
return {
|
||||
frequency: pitchLock?.enabled ? pitchLock.frequency : mutate(params.frequency, 55, 55 * Math.pow(2, 5)),
|
||||
filterCutoff: mutate(params.filterCutoff, 200, 4000),
|
||||
filterEnvAmount: mutate(params.filterEnvAmount, 0, 6000),
|
||||
resonance: mutate(params.resonance, 0.5, 15),
|
||||
saturation: mutate(params.saturation, 1, 10),
|
||||
osc1Level: mutate(params.osc1Level, 0.5, 1),
|
||||
osc2Level: mutate(params.osc2Level, 0, 0.5),
|
||||
osc3Level: mutate(params.osc3Level, 0, 0.3),
|
||||
attack: mutate(params.attack, 0, 0.1),
|
||||
decay: mutate(params.decay, 0.05, 0.35),
|
||||
sustain: mutate(params.sustain, 0.2, 0.8),
|
||||
release: mutate(params.release, 0.05, 0.45)
|
||||
};
|
||||
}
|
||||
}
|
||||
342
src/lib/audio/engines/SubtractiveThreeOsc.ts
Normal file
342
src/lib/audio/engines/SubtractiveThreeOsc.ts
Normal 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),
|
||||
};
|
||||
}
|
||||
}
|
||||
187
src/lib/audio/engines/TechnoKick.ts
Normal file
187
src/lib/audio/engines/TechnoKick.ts
Normal file
@ -0,0 +1,187 @@
|
||||
import { CsoundEngine, type CsoundParameter } from './base/CsoundEngine';
|
||||
import type { PitchLock } from './base/SynthEngine';
|
||||
|
||||
interface TechnoKickParams {
|
||||
startFreq: number;
|
||||
endFreq: number;
|
||||
freqDecay: number;
|
||||
resonance: number;
|
||||
cutoffStart: number;
|
||||
cutoffEnd: number;
|
||||
cutoffDecay: number;
|
||||
ampAttack: number;
|
||||
ampDecay: number;
|
||||
noiseMix: number;
|
||||
punch: number;
|
||||
stereoWidth: number;
|
||||
}
|
||||
|
||||
export class TechnoKick extends CsoundEngine<TechnoKickParams> {
|
||||
getName(): string {
|
||||
return 'Techno Kick';
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return 'Noise through resonant low-pass filter with frequency sweep and RMS compression for punchy electronic kicks';
|
||||
}
|
||||
|
||||
getType() {
|
||||
return 'generative' as const;
|
||||
}
|
||||
|
||||
getCategory() {
|
||||
return 'Percussion' as const;
|
||||
}
|
||||
|
||||
protected getOrchestra(): string {
|
||||
return `
|
||||
instr 1
|
||||
iStartFreq chnget "startFreq"
|
||||
iEndFreq chnget "endFreq"
|
||||
iFreqDecay chnget "freqDecay"
|
||||
iResonance chnget "resonance"
|
||||
iCutoffStart chnget "cutoffStart"
|
||||
iCutoffEnd chnget "cutoffEnd"
|
||||
iCutoffDecay chnget "cutoffDecay"
|
||||
iAmpAttack chnget "ampAttack"
|
||||
iAmpDecay chnget "ampDecay"
|
||||
iNoiseMix chnget "noiseMix"
|
||||
iPunch chnget "punch"
|
||||
iStereoWidth chnget "stereoWidth"
|
||||
|
||||
idur = p3
|
||||
iFreqDecayTime = iFreqDecay * idur
|
||||
iCutoffDecayTime = iCutoffDecay * idur
|
||||
iAmpAttackTime = iAmpAttack * idur
|
||||
iAmpDecayTime = iAmpDecay * idur
|
||||
|
||||
; Generate random noise
|
||||
aNoise noise 1, 0
|
||||
|
||||
; Frequency envelope for the filter cutoff (exponential sweep)
|
||||
kFreqEnv expseg iStartFreq, iFreqDecayTime, iEndFreq, idur - iFreqDecayTime, iEndFreq
|
||||
|
||||
; Cutoff modulation envelope
|
||||
kCutoffEnv expseg iCutoffStart, iCutoffDecayTime, iCutoffEnd, idur - iCutoffDecayTime, iCutoffEnd
|
||||
|
||||
; Apply resonant low-pass filter (rezzy)
|
||||
aFiltered rezzy aNoise * (1 + iNoiseMix), kCutoffEnv, iResonance
|
||||
|
||||
; Add sine sub-bass component for more weight
|
||||
kSubFreqEnv expseg iStartFreq * 0.5, iFreqDecayTime, iEndFreq * 0.5, idur - iFreqDecayTime, iEndFreq * 0.5
|
||||
aSubBass oscili 0.6, kSubFreqEnv
|
||||
|
||||
; Mix filtered noise and sub-bass
|
||||
aMix = aFiltered * 0.5 + aSubBass
|
||||
|
||||
; Amplitude envelope (exponential)
|
||||
kAmpEnv expseg 0.001, iAmpAttackTime, 1, iAmpDecayTime, 0.001, idur - iAmpAttackTime - iAmpDecayTime, 0.001
|
||||
|
||||
; Apply amplitude envelope
|
||||
aEnveloped = aMix * kAmpEnv
|
||||
|
||||
; RMS compression for punch
|
||||
; Calculate RMS of signal
|
||||
kRMS rms aEnveloped
|
||||
if kRMS < 0.01 then
|
||||
kCompGain = 1
|
||||
else
|
||||
kCompGain = 1 + iPunch * (0.3 / kRMS - 1)
|
||||
endif
|
||||
kCompGain limit kCompGain, 0.5, 3
|
||||
|
||||
aOut = aEnveloped * kCompGain
|
||||
|
||||
; Right channel with stereo width
|
||||
iStartFreqR = iStartFreq * (1 + iStereoWidth * 0.01)
|
||||
iEndFreqR = iEndFreq * (1 + iStereoWidth * 0.01)
|
||||
|
||||
kFreqEnvR expseg iStartFreqR, iFreqDecayTime, iEndFreqR, idur - iFreqDecayTime, iEndFreqR
|
||||
kCutoffEnvR expseg iCutoffStart * (1 + iStereoWidth * 0.02), iCutoffDecayTime, iCutoffEnd * (1 + iStereoWidth * 0.02), idur - iCutoffDecayTime, iCutoffEnd * (1 + iStereoWidth * 0.02)
|
||||
|
||||
aNoiseR noise 1, 0
|
||||
aFilteredR rezzy aNoiseR * (1 + iNoiseMix), kCutoffEnvR, iResonance
|
||||
|
||||
kSubFreqEnvR expseg iStartFreqR * 0.5, iFreqDecayTime, iEndFreqR * 0.5, idur - iFreqDecayTime, iEndFreqR * 0.5
|
||||
aSubBassR oscili 0.6, kSubFreqEnvR
|
||||
|
||||
aMixR = aFilteredR * 0.5 + aSubBassR
|
||||
aEnvelopedR = aMixR * kAmpEnv
|
||||
|
||||
kRMSR rms aEnvelopedR
|
||||
if kRMSR < 0.01 then
|
||||
kCompGainR = 1
|
||||
else
|
||||
kCompGainR = 1 + iPunch * (0.3 / kRMSR - 1)
|
||||
endif
|
||||
kCompGainR limit kCompGainR, 0.5, 3
|
||||
|
||||
aOutR = aEnvelopedR * kCompGainR
|
||||
|
||||
outs aOut, aOutR
|
||||
endin
|
||||
`;
|
||||
}
|
||||
|
||||
protected getParametersForCsound(params: TechnoKickParams): CsoundParameter[] {
|
||||
return [
|
||||
{ channelName: 'startFreq', value: params.startFreq },
|
||||
{ channelName: 'endFreq', value: params.endFreq },
|
||||
{ channelName: 'freqDecay', value: params.freqDecay },
|
||||
{ channelName: 'resonance', value: params.resonance },
|
||||
{ channelName: 'cutoffStart', value: params.cutoffStart },
|
||||
{ channelName: 'cutoffEnd', value: params.cutoffEnd },
|
||||
{ channelName: 'cutoffDecay', value: params.cutoffDecay },
|
||||
{ channelName: 'ampAttack', value: params.ampAttack },
|
||||
{ channelName: 'ampDecay', value: params.ampDecay },
|
||||
{ channelName: 'noiseMix', value: params.noiseMix },
|
||||
{ channelName: 'punch', value: params.punch },
|
||||
{ channelName: 'stereoWidth', value: params.stereoWidth },
|
||||
];
|
||||
}
|
||||
|
||||
randomParams(pitchLock?: PitchLock): TechnoKickParams {
|
||||
const endFreqChoices = [40, 45, 50, 55, 60, 70, 80];
|
||||
const endFreq = pitchLock?.enabled
|
||||
? pitchLock.frequency
|
||||
: this.randomChoice(endFreqChoices) * this.randomRange(0.95, 1.05);
|
||||
|
||||
return {
|
||||
startFreq: this.randomRange(800, 1200),
|
||||
endFreq,
|
||||
freqDecay: this.randomRange(0.05, 0.25),
|
||||
resonance: this.randomRange(5, 40),
|
||||
cutoffStart: this.randomRange(300, 800),
|
||||
cutoffEnd: this.randomRange(80, 200),
|
||||
cutoffDecay: this.randomRange(0.1, 0.4),
|
||||
ampAttack: this.randomRange(0.001, 0.005),
|
||||
ampDecay: this.randomRange(0.2, 0.6),
|
||||
noiseMix: this.randomRange(0.1, 0.8),
|
||||
punch: this.randomRange(0.3, 0.9),
|
||||
stereoWidth: this.randomRange(0, 0.3),
|
||||
};
|
||||
}
|
||||
|
||||
mutateParams(
|
||||
params: TechnoKickParams,
|
||||
mutationAmount: number = 0.15,
|
||||
pitchLock?: PitchLock
|
||||
): TechnoKickParams {
|
||||
const endFreq = pitchLock?.enabled ? pitchLock.frequency : params.endFreq;
|
||||
|
||||
return {
|
||||
startFreq: this.mutateValue(params.startFreq, mutationAmount, 600, 1500),
|
||||
endFreq,
|
||||
freqDecay: this.mutateValue(params.freqDecay, mutationAmount, 0.02, 0.4),
|
||||
resonance: this.mutateValue(params.resonance, mutationAmount, 3, 50),
|
||||
cutoffStart: this.mutateValue(params.cutoffStart, mutationAmount, 200, 1000),
|
||||
cutoffEnd: this.mutateValue(params.cutoffEnd, mutationAmount, 60, 300),
|
||||
cutoffDecay: this.mutateValue(params.cutoffDecay, mutationAmount, 0.05, 0.6),
|
||||
ampAttack: this.mutateValue(params.ampAttack, mutationAmount, 0.001, 0.01),
|
||||
ampDecay: this.mutateValue(params.ampDecay, mutationAmount, 0.1, 0.8),
|
||||
noiseMix: this.mutateValue(params.noiseMix, mutationAmount, 0, 1),
|
||||
punch: this.mutateValue(params.punch, mutationAmount, 0.1, 1),
|
||||
stereoWidth: this.mutateValue(params.stereoWidth, mutationAmount, 0, 0.5),
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { SynthEngine, PitchLock } from './SynthEngine';
|
||||
import type { SynthEngine, PitchLock } from './base/SynthEngine';
|
||||
|
||||
interface WavetableParams {
|
||||
bankIndex: number;
|
||||
|
||||
@ -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;
|
||||
|
||||
332
src/lib/audio/engines/base/CsoundEngine.ts
Normal file
332
src/lib/audio/engines/base/CsoundEngine.ts
Normal 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));
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -1,7 +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';
|
||||
@ -14,6 +15,22 @@ 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(),
|
||||
@ -21,6 +38,7 @@ export const engines: SynthEngine[] = [
|
||||
new FourOpFM(),
|
||||
new TwoOpFM(),
|
||||
new PhaseDistortionFM(),
|
||||
new FormantFM(),
|
||||
new DubSiren(),
|
||||
new Benjolin(),
|
||||
new ZzfxEngine(),
|
||||
@ -28,7 +46,23 @@ export const engines: SynthEngine[] = [
|
||||
new Snare(),
|
||||
new BassDrum(),
|
||||
new HiHat(),
|
||||
new FormantPopDrum(),
|
||||
new TechnoKick(),
|
||||
new FMTomTom(),
|
||||
new RingCymbal(),
|
||||
new AdditiveBass(),
|
||||
new FeedbackSnare(),
|
||||
new Ring(),
|
||||
new KarplusStrong(),
|
||||
new AdditiveEngine(),
|
||||
new ParticleNoise(),
|
||||
new DustNoise(),
|
||||
new SubtractiveThreeOsc(),
|
||||
new CombResonator(),
|
||||
new MassiveAdditive(),
|
||||
new LaserSweep(),
|
||||
new Dripwater(),
|
||||
new Sub8(),
|
||||
new Form1(),
|
||||
new Squine1(),
|
||||
];
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
export type ProcessorCategory = 'Amplitude' | 'Filter' | 'Time' | 'Space' | 'Pitch' | 'Modulation' | 'Distortion' | 'Spectral' | 'Utility';
|
||||
|
||||
export interface AudioProcessor {
|
||||
getName(): string;
|
||||
getDescription(): string;
|
||||
getCategory(): ProcessorCategory;
|
||||
process(
|
||||
leftChannel: Float32Array,
|
||||
rightChannel: Float32Array
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { AudioProcessor } from "./AudioProcessor";
|
||||
import type { AudioProcessor, ProcessorCategory } from "./AudioProcessor";
|
||||
|
||||
export class BitCrusher implements AudioProcessor {
|
||||
getName(): string {
|
||||
@ -9,6 +9,10 @@ export class BitCrusher implements AudioProcessor {
|
||||
return "Reduces bit depth for lo-fi digital distortion";
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Distortion';
|
||||
}
|
||||
|
||||
async process(
|
||||
leftChannel: Float32Array,
|
||||
rightChannel: Float32Array
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { AudioProcessor } from './AudioProcessor';
|
||||
import type { AudioProcessor, ProcessorCategory } from './AudioProcessor';
|
||||
|
||||
export class Chorus implements AudioProcessor {
|
||||
private readonly sampleRate = 44100;
|
||||
@ -11,6 +11,10 @@ export class Chorus implements AudioProcessor {
|
||||
return 'Multiple delayed copies with pitch modulation for thick, ensemble sounds';
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Time';
|
||||
}
|
||||
|
||||
process(
|
||||
leftChannel: Float32Array,
|
||||
rightChannel: Float32Array
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { AudioProcessor } from "./AudioProcessor";
|
||||
import type { AudioProcessor, ProcessorCategory } from "./AudioProcessor";
|
||||
|
||||
export class Compressor implements AudioProcessor {
|
||||
getName(): string {
|
||||
@ -9,6 +9,10 @@ export class Compressor implements AudioProcessor {
|
||||
return "Reduces dynamic range by taming peaks with makeup gain";
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Amplitude';
|
||||
}
|
||||
|
||||
async process(
|
||||
leftChannel: Float32Array,
|
||||
rightChannel: Float32Array
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { AudioProcessor } from './AudioProcessor';
|
||||
import type { AudioProcessor, ProcessorCategory } from './AudioProcessor';
|
||||
|
||||
type RoomType = 'small' | 'medium' | 'large' | 'hall' | 'plate' | 'chamber';
|
||||
|
||||
@ -22,6 +22,10 @@ export class ConvolutionReverb implements AudioProcessor {
|
||||
return 'Realistic room ambience using Web Audio ConvolverNode with synthetic impulse responses';
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Space';
|
||||
}
|
||||
|
||||
async process(
|
||||
leftChannel: Float32Array,
|
||||
rightChannel: Float32Array
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { AudioProcessor } from "./AudioProcessor";
|
||||
import type { AudioProcessor, ProcessorCategory } from "./AudioProcessor";
|
||||
|
||||
export class DCOffsetRemover implements AudioProcessor {
|
||||
getName(): string {
|
||||
@ -9,6 +9,10 @@ export class DCOffsetRemover implements AudioProcessor {
|
||||
return "Removes DC offset bias from the audio signal";
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Utility';
|
||||
}
|
||||
|
||||
async process(
|
||||
leftChannel: Float32Array,
|
||||
rightChannel: Float32Array
|
||||
|
||||
31
src/lib/audio/processors/ExpFadeIn.ts
Normal file
31
src/lib/audio/processors/ExpFadeIn.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import type { AudioProcessor, ProcessorCategory } from './AudioProcessor';
|
||||
|
||||
export class ExpFadeIn implements AudioProcessor {
|
||||
getName(): string {
|
||||
return 'Fade In (Exp)';
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return 'Applies an exponential fade from silence to current level';
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Amplitude';
|
||||
}
|
||||
|
||||
process(leftIn: Float32Array, rightIn: Float32Array): [Float32Array, Float32Array] {
|
||||
const length = leftIn.length;
|
||||
|
||||
const leftOut = new Float32Array(length);
|
||||
const rightOut = new Float32Array(length);
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
const t = i / length;
|
||||
const gain = 1.0 - Math.exp(-5.0 * t);
|
||||
leftOut[i] = leftIn[i] * gain;
|
||||
rightOut[i] = rightIn[i] * gain;
|
||||
}
|
||||
|
||||
return [leftOut, rightOut];
|
||||
}
|
||||
}
|
||||
31
src/lib/audio/processors/ExpFadeOut.ts
Normal file
31
src/lib/audio/processors/ExpFadeOut.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import type { AudioProcessor, ProcessorCategory } from './AudioProcessor';
|
||||
|
||||
export class ExpFadeOut implements AudioProcessor {
|
||||
getName(): string {
|
||||
return 'Fade Out (Exp)';
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return 'Applies an exponential fade from current level to silence';
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Amplitude';
|
||||
}
|
||||
|
||||
process(leftIn: Float32Array, rightIn: Float32Array): [Float32Array, Float32Array] {
|
||||
const length = leftIn.length;
|
||||
|
||||
const leftOut = new Float32Array(length);
|
||||
const rightOut = new Float32Array(length);
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
const t = i / length;
|
||||
const gain = Math.exp(-5.0 * t);
|
||||
leftOut[i] = leftIn[i] * gain;
|
||||
rightOut[i] = rightIn[i] * gain;
|
||||
}
|
||||
|
||||
return [leftOut, rightOut];
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import type { AudioProcessor } from "./AudioProcessor";
|
||||
import type { AudioProcessor, ProcessorCategory } from "./AudioProcessor";
|
||||
|
||||
export class HaasEffect implements AudioProcessor {
|
||||
getName(): string {
|
||||
@ -9,6 +9,10 @@ export class HaasEffect implements AudioProcessor {
|
||||
return "Creates stereo width with micro-delay (precedence effect)";
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Space';
|
||||
}
|
||||
|
||||
async process(
|
||||
leftChannel: Float32Array,
|
||||
rightChannel: Float32Array
|
||||
|
||||
65
src/lib/audio/processors/HighPassSweepDown.ts
Normal file
65
src/lib/audio/processors/HighPassSweepDown.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import type { AudioProcessor, ProcessorCategory } from './AudioProcessor';
|
||||
|
||||
export class HighPassSweepDown implements AudioProcessor {
|
||||
getName(): string {
|
||||
return 'HP Sweep Down';
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return 'Sweeps a high-pass filter from thin to full';
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Filter';
|
||||
}
|
||||
|
||||
process(leftIn: Float32Array, rightIn: Float32Array): [Float32Array, Float32Array] {
|
||||
const length = leftIn.length;
|
||||
const leftOut = new Float32Array(length);
|
||||
const rightOut = new Float32Array(length);
|
||||
|
||||
const sampleRate = 44100;
|
||||
const startFreq = 8000;
|
||||
const endFreq = 20;
|
||||
const Q = 1.5;
|
||||
|
||||
let lx1 = 0, lx2 = 0, ly1 = 0, ly2 = 0;
|
||||
let rx1 = 0, rx2 = 0, ry1 = 0, ry2 = 0;
|
||||
|
||||
let b0 = 0, b1 = 0, b2 = 0, a1 = 0, a2 = 0;
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
if (i % 64 === 0) {
|
||||
const t = i / length;
|
||||
const freq = startFreq * Math.pow(endFreq / startFreq, t);
|
||||
const omega = 2.0 * Math.PI * freq / sampleRate;
|
||||
const alpha = Math.sin(omega) / (2.0 * Q);
|
||||
|
||||
const a0 = 1.0 + alpha;
|
||||
b0 = ((1.0 + Math.cos(omega)) / 2.0) / a0;
|
||||
b1 = (-(1.0 + Math.cos(omega))) / a0;
|
||||
b2 = b0;
|
||||
a1 = (-2.0 * Math.cos(omega)) / a0;
|
||||
a2 = (1.0 - alpha) / a0;
|
||||
}
|
||||
|
||||
const lx0 = leftIn[i];
|
||||
const rx0 = rightIn[i];
|
||||
|
||||
leftOut[i] = b0 * lx0 + b1 * lx1 + b2 * lx2 - a1 * ly1 - a2 * ly2;
|
||||
rightOut[i] = b0 * rx0 + b1 * rx1 + b2 * rx2 - a1 * ry1 - a2 * ry2;
|
||||
|
||||
lx2 = lx1;
|
||||
lx1 = lx0;
|
||||
ly2 = ly1;
|
||||
ly1 = leftOut[i];
|
||||
|
||||
rx2 = rx1;
|
||||
rx1 = rx0;
|
||||
ry2 = ry1;
|
||||
ry1 = rightOut[i];
|
||||
}
|
||||
|
||||
return [leftOut, rightOut];
|
||||
}
|
||||
}
|
||||
65
src/lib/audio/processors/HighPassSweepUp.ts
Normal file
65
src/lib/audio/processors/HighPassSweepUp.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import type { AudioProcessor, ProcessorCategory } from './AudioProcessor';
|
||||
|
||||
export class HighPassSweepUp implements AudioProcessor {
|
||||
getName(): string {
|
||||
return 'HP Sweep Up';
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return 'Sweeps a high-pass filter from full to thin';
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Filter';
|
||||
}
|
||||
|
||||
process(leftIn: Float32Array, rightIn: Float32Array): [Float32Array, Float32Array] {
|
||||
const length = leftIn.length;
|
||||
const leftOut = new Float32Array(length);
|
||||
const rightOut = new Float32Array(length);
|
||||
|
||||
const sampleRate = 44100;
|
||||
const startFreq = 20;
|
||||
const endFreq = 8000;
|
||||
const Q = 1.5;
|
||||
|
||||
let lx1 = 0, lx2 = 0, ly1 = 0, ly2 = 0;
|
||||
let rx1 = 0, rx2 = 0, ry1 = 0, ry2 = 0;
|
||||
|
||||
let b0 = 0, b1 = 0, b2 = 0, a1 = 0, a2 = 0;
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
if (i % 64 === 0) {
|
||||
const t = i / length;
|
||||
const freq = startFreq * Math.pow(endFreq / startFreq, t);
|
||||
const omega = 2.0 * Math.PI * freq / sampleRate;
|
||||
const alpha = Math.sin(omega) / (2.0 * Q);
|
||||
|
||||
const a0 = 1.0 + alpha;
|
||||
b0 = ((1.0 + Math.cos(omega)) / 2.0) / a0;
|
||||
b1 = (-(1.0 + Math.cos(omega))) / a0;
|
||||
b2 = b0;
|
||||
a1 = (-2.0 * Math.cos(omega)) / a0;
|
||||
a2 = (1.0 - alpha) / a0;
|
||||
}
|
||||
|
||||
const lx0 = leftIn[i];
|
||||
const rx0 = rightIn[i];
|
||||
|
||||
leftOut[i] = b0 * lx0 + b1 * lx1 + b2 * lx2 - a1 * ly1 - a2 * ly2;
|
||||
rightOut[i] = b0 * rx0 + b1 * rx1 + b2 * rx2 - a1 * ry1 - a2 * ry2;
|
||||
|
||||
lx2 = lx1;
|
||||
lx1 = lx0;
|
||||
ly2 = ly1;
|
||||
ly1 = leftOut[i];
|
||||
|
||||
rx2 = rx1;
|
||||
rx1 = rx0;
|
||||
ry2 = ry1;
|
||||
ry1 = rightOut[i];
|
||||
}
|
||||
|
||||
return [leftOut, rightOut];
|
||||
}
|
||||
}
|
||||
30
src/lib/audio/processors/LinearFadeIn.ts
Normal file
30
src/lib/audio/processors/LinearFadeIn.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import type { AudioProcessor, ProcessorCategory } from './AudioProcessor';
|
||||
|
||||
export class LinearFadeIn implements AudioProcessor {
|
||||
getName(): string {
|
||||
return 'Fade In';
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return 'Applies a linear fade from silence to current level';
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Amplitude';
|
||||
}
|
||||
|
||||
process(leftIn: Float32Array, rightIn: Float32Array): [Float32Array, Float32Array] {
|
||||
const length = leftIn.length;
|
||||
|
||||
const leftOut = new Float32Array(length);
|
||||
const rightOut = new Float32Array(length);
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
const gain = i / length;
|
||||
leftOut[i] = leftIn[i] * gain;
|
||||
rightOut[i] = rightIn[i] * gain;
|
||||
}
|
||||
|
||||
return [leftOut, rightOut];
|
||||
}
|
||||
}
|
||||
30
src/lib/audio/processors/LinearFadeOut.ts
Normal file
30
src/lib/audio/processors/LinearFadeOut.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import type { AudioProcessor, ProcessorCategory } from './AudioProcessor';
|
||||
|
||||
export class LinearFadeOut implements AudioProcessor {
|
||||
getName(): string {
|
||||
return 'Fade Out';
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return 'Applies a linear fade from current level to silence';
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Amplitude';
|
||||
}
|
||||
|
||||
process(leftIn: Float32Array, rightIn: Float32Array): [Float32Array, Float32Array] {
|
||||
const length = leftIn.length;
|
||||
|
||||
const leftOut = new Float32Array(length);
|
||||
const rightOut = new Float32Array(length);
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
const gain = 1.0 - i / length;
|
||||
leftOut[i] = leftIn[i] * gain;
|
||||
rightOut[i] = rightIn[i] * gain;
|
||||
}
|
||||
|
||||
return [leftOut, rightOut];
|
||||
}
|
||||
}
|
||||
65
src/lib/audio/processors/LowPassSweepDown.ts
Normal file
65
src/lib/audio/processors/LowPassSweepDown.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import type { AudioProcessor, ProcessorCategory } from './AudioProcessor';
|
||||
|
||||
export class LowPassSweepDown implements AudioProcessor {
|
||||
getName(): string {
|
||||
return 'LP Sweep Down';
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return 'Sweeps a low-pass filter from bright to dark';
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Filter';
|
||||
}
|
||||
|
||||
process(leftIn: Float32Array, rightIn: Float32Array): [Float32Array, Float32Array] {
|
||||
const length = leftIn.length;
|
||||
const leftOut = new Float32Array(length);
|
||||
const rightOut = new Float32Array(length);
|
||||
|
||||
const sampleRate = 44100;
|
||||
const startFreq = 18000;
|
||||
const endFreq = 100;
|
||||
const Q = 1.5;
|
||||
|
||||
let lx1 = 0, lx2 = 0, ly1 = 0, ly2 = 0;
|
||||
let rx1 = 0, rx2 = 0, ry1 = 0, ry2 = 0;
|
||||
|
||||
let b0 = 0, b1 = 0, b2 = 0, a1 = 0, a2 = 0;
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
if (i % 64 === 0) {
|
||||
const t = i / length;
|
||||
const freq = startFreq * Math.pow(endFreq / startFreq, t);
|
||||
const omega = 2.0 * Math.PI * freq / sampleRate;
|
||||
const alpha = Math.sin(omega) / (2.0 * Q);
|
||||
|
||||
const a0 = 1.0 + alpha;
|
||||
b0 = ((1.0 - Math.cos(omega)) / 2.0) / a0;
|
||||
b1 = (1.0 - Math.cos(omega)) / a0;
|
||||
b2 = b0;
|
||||
a1 = (-2.0 * Math.cos(omega)) / a0;
|
||||
a2 = (1.0 - alpha) / a0;
|
||||
}
|
||||
|
||||
const lx0 = leftIn[i];
|
||||
const rx0 = rightIn[i];
|
||||
|
||||
leftOut[i] = b0 * lx0 + b1 * lx1 + b2 * lx2 - a1 * ly1 - a2 * ly2;
|
||||
rightOut[i] = b0 * rx0 + b1 * rx1 + b2 * rx2 - a1 * ry1 - a2 * ry2;
|
||||
|
||||
lx2 = lx1;
|
||||
lx1 = lx0;
|
||||
ly2 = ly1;
|
||||
ly1 = leftOut[i];
|
||||
|
||||
rx2 = rx1;
|
||||
rx1 = rx0;
|
||||
ry2 = ry1;
|
||||
ry1 = rightOut[i];
|
||||
}
|
||||
|
||||
return [leftOut, rightOut];
|
||||
}
|
||||
}
|
||||
65
src/lib/audio/processors/LowPassSweepUp.ts
Normal file
65
src/lib/audio/processors/LowPassSweepUp.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import type { AudioProcessor, ProcessorCategory } from './AudioProcessor';
|
||||
|
||||
export class LowPassSweepUp implements AudioProcessor {
|
||||
getName(): string {
|
||||
return 'LP Sweep Up';
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return 'Sweeps a low-pass filter from dark to bright';
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Filter';
|
||||
}
|
||||
|
||||
process(leftIn: Float32Array, rightIn: Float32Array): [Float32Array, Float32Array] {
|
||||
const length = leftIn.length;
|
||||
const leftOut = new Float32Array(length);
|
||||
const rightOut = new Float32Array(length);
|
||||
|
||||
const sampleRate = 44100;
|
||||
const startFreq = 100;
|
||||
const endFreq = 18000;
|
||||
const Q = 1.5;
|
||||
|
||||
let lx1 = 0, lx2 = 0, ly1 = 0, ly2 = 0;
|
||||
let rx1 = 0, rx2 = 0, ry1 = 0, ry2 = 0;
|
||||
|
||||
let b0 = 0, b1 = 0, b2 = 0, a1 = 0, a2 = 0;
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
if (i % 64 === 0) {
|
||||
const t = i / length;
|
||||
const freq = startFreq * Math.pow(endFreq / startFreq, t);
|
||||
const omega = 2.0 * Math.PI * freq / sampleRate;
|
||||
const alpha = Math.sin(omega) / (2.0 * Q);
|
||||
|
||||
const a0 = 1.0 + alpha;
|
||||
b0 = ((1.0 - Math.cos(omega)) / 2.0) / a0;
|
||||
b1 = (1.0 - Math.cos(omega)) / a0;
|
||||
b2 = b0;
|
||||
a1 = (-2.0 * Math.cos(omega)) / a0;
|
||||
a2 = (1.0 - alpha) / a0;
|
||||
}
|
||||
|
||||
const lx0 = leftIn[i];
|
||||
const rx0 = rightIn[i];
|
||||
|
||||
leftOut[i] = b0 * lx0 + b1 * lx1 + b2 * lx2 - a1 * ly1 - a2 * ly2;
|
||||
rightOut[i] = b0 * rx0 + b1 * rx1 + b2 * rx2 - a1 * ry1 - a2 * ry2;
|
||||
|
||||
lx2 = lx1;
|
||||
lx1 = lx0;
|
||||
ly2 = ly1;
|
||||
ly1 = leftOut[i];
|
||||
|
||||
rx2 = rx1;
|
||||
rx1 = rx0;
|
||||
ry2 = ry1;
|
||||
ry1 = rightOut[i];
|
||||
}
|
||||
|
||||
return [leftOut, rightOut];
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import type { AudioProcessor } from './AudioProcessor';
|
||||
import type { AudioProcessor, ProcessorCategory } from './AudioProcessor';
|
||||
|
||||
export class MicroPitch implements AudioProcessor {
|
||||
getName(): string {
|
||||
@ -9,6 +9,10 @@ export class MicroPitch implements AudioProcessor {
|
||||
return 'Applies subtle random pitch variations for analog warmth and character';
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Pitch';
|
||||
}
|
||||
|
||||
process(
|
||||
leftChannel: Float32Array,
|
||||
rightChannel: Float32Array
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { AudioProcessor } from "./AudioProcessor";
|
||||
import type { AudioProcessor, ProcessorCategory } from "./AudioProcessor";
|
||||
|
||||
export class Normalize implements AudioProcessor {
|
||||
getName(): string {
|
||||
@ -9,6 +9,10 @@ export class Normalize implements AudioProcessor {
|
||||
return "Normalizes audio to maximum amplitude without clipping";
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Amplitude';
|
||||
}
|
||||
|
||||
async process(
|
||||
leftChannel: Float32Array,
|
||||
rightChannel: Float32Array
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { AudioProcessor } from "./AudioProcessor";
|
||||
import type { AudioProcessor, ProcessorCategory } from "./AudioProcessor";
|
||||
|
||||
export class OctaveDown implements AudioProcessor {
|
||||
getName(): string {
|
||||
@ -9,6 +9,10 @@ export class OctaveDown implements AudioProcessor {
|
||||
return "Shifts pitch down one octave by halving playback rate";
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Pitch';
|
||||
}
|
||||
|
||||
async process(
|
||||
leftChannel: Float32Array,
|
||||
rightChannel: Float32Array
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { AudioProcessor } from "./AudioProcessor";
|
||||
import type { AudioProcessor, ProcessorCategory } from "./AudioProcessor";
|
||||
|
||||
export class OctaveUp implements AudioProcessor {
|
||||
getName(): string {
|
||||
@ -9,6 +9,10 @@ export class OctaveUp implements AudioProcessor {
|
||||
return "Shifts pitch up one octave by doubling playback rate";
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Pitch';
|
||||
}
|
||||
|
||||
async process(
|
||||
leftChannel: Float32Array,
|
||||
rightChannel: Float32Array
|
||||
|
||||
34
src/lib/audio/processors/PanLeftToRight.ts
Normal file
34
src/lib/audio/processors/PanLeftToRight.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import type { AudioProcessor, ProcessorCategory } from './AudioProcessor';
|
||||
|
||||
export class PanLeftToRight implements AudioProcessor {
|
||||
getName(): string {
|
||||
return 'Pan L→R';
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return 'Gradually pans the sound from left to right';
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Space';
|
||||
}
|
||||
|
||||
process(leftIn: Float32Array, rightIn: Float32Array): [Float32Array, Float32Array] {
|
||||
const length = leftIn.length;
|
||||
const leftOut = new Float32Array(length);
|
||||
const rightOut = new Float32Array(length);
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
const t = i / length;
|
||||
const panAngle = t * Math.PI * 0.5;
|
||||
const leftGain = Math.cos(panAngle);
|
||||
const rightGain = Math.sin(panAngle);
|
||||
|
||||
const mono = (leftIn[i] + rightIn[i]) * 0.5;
|
||||
leftOut[i] = mono * leftGain;
|
||||
rightOut[i] = mono * rightGain;
|
||||
}
|
||||
|
||||
return [leftOut, rightOut];
|
||||
}
|
||||
}
|
||||
34
src/lib/audio/processors/PanRightToLeft.ts
Normal file
34
src/lib/audio/processors/PanRightToLeft.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import type { AudioProcessor, ProcessorCategory } from './AudioProcessor';
|
||||
|
||||
export class PanRightToLeft implements AudioProcessor {
|
||||
getName(): string {
|
||||
return 'Pan R→L';
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return 'Gradually pans the sound from right to left';
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Space';
|
||||
}
|
||||
|
||||
process(leftIn: Float32Array, rightIn: Float32Array): [Float32Array, Float32Array] {
|
||||
const length = leftIn.length;
|
||||
const leftOut = new Float32Array(length);
|
||||
const rightOut = new Float32Array(length);
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
const t = i / length;
|
||||
const panAngle = (1.0 - t) * Math.PI * 0.5;
|
||||
const leftGain = Math.cos(panAngle);
|
||||
const rightGain = Math.sin(panAngle);
|
||||
|
||||
const mono = (leftIn[i] + rightIn[i]) * 0.5;
|
||||
leftOut[i] = mono * leftGain;
|
||||
rightOut[i] = mono * rightGain;
|
||||
}
|
||||
|
||||
return [leftOut, rightOut];
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import type { AudioProcessor } from './AudioProcessor';
|
||||
import type { AudioProcessor, ProcessorCategory } from './AudioProcessor';
|
||||
|
||||
export class PanShuffler implements AudioProcessor {
|
||||
getName(): string {
|
||||
@ -9,6 +9,10 @@ export class PanShuffler implements AudioProcessor {
|
||||
return 'Smoothly pans segments across the stereo field';
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Space';
|
||||
}
|
||||
|
||||
process(
|
||||
leftChannel: Float32Array,
|
||||
rightChannel: Float32Array
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { AudioProcessor } from "./AudioProcessor";
|
||||
import type { AudioProcessor, ProcessorCategory } from "./AudioProcessor";
|
||||
|
||||
export class PhaseInverter implements AudioProcessor {
|
||||
getName(): string {
|
||||
@ -9,6 +9,10 @@ export class PhaseInverter implements AudioProcessor {
|
||||
return "Inverts polarity of one or both channels";
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Utility';
|
||||
}
|
||||
|
||||
async process(
|
||||
leftChannel: Float32Array,
|
||||
rightChannel: Float32Array
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { AudioProcessor } from './AudioProcessor';
|
||||
import type { AudioProcessor, ProcessorCategory } from './AudioProcessor';
|
||||
|
||||
export class Phaser implements AudioProcessor {
|
||||
private readonly sampleRate = 44100;
|
||||
@ -11,6 +11,10 @@ export class Phaser implements AudioProcessor {
|
||||
return 'Classic phaser effect with sweeping all-pass filters for swirling, spacey sounds';
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Filter';
|
||||
}
|
||||
|
||||
process(
|
||||
leftChannel: Float32Array,
|
||||
rightChannel: Float32Array
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { AudioProcessor } from './AudioProcessor';
|
||||
import type { AudioProcessor, ProcessorCategory } from './AudioProcessor';
|
||||
|
||||
export class PitchShifter implements AudioProcessor {
|
||||
getName(): string {
|
||||
@ -9,6 +9,10 @@ export class PitchShifter implements AudioProcessor {
|
||||
return 'Transposes audio up or down in semitones without changing duration';
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Pitch';
|
||||
}
|
||||
|
||||
process(
|
||||
leftChannel: Float32Array,
|
||||
rightChannel: Float32Array
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { AudioProcessor } from './AudioProcessor';
|
||||
import type { AudioProcessor, ProcessorCategory } from './AudioProcessor';
|
||||
|
||||
export class PitchWobble implements AudioProcessor {
|
||||
getName(): string {
|
||||
@ -9,6 +9,10 @@ export class PitchWobble implements AudioProcessor {
|
||||
return 'Variable-rate playback with LFO modulation for tape wow/vibrato effects';
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Pitch';
|
||||
}
|
||||
|
||||
process(
|
||||
leftChannel: Float32Array,
|
||||
rightChannel: Float32Array
|
||||
|
||||
209
src/lib/audio/processors/Resonator.ts
Normal file
209
src/lib/audio/processors/Resonator.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import type { AudioProcessor } from './AudioProcessor';
|
||||
import type { AudioProcessor, ProcessorCategory } from './AudioProcessor';
|
||||
|
||||
export class Reverser implements AudioProcessor {
|
||||
getName(): string {
|
||||
@ -9,6 +9,10 @@ export class Reverser implements AudioProcessor {
|
||||
return 'Plays the sound backwards';
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Time';
|
||||
}
|
||||
|
||||
process(
|
||||
leftChannel: Float32Array,
|
||||
rightChannel: Float32Array
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { AudioProcessor } from "./AudioProcessor";
|
||||
import type { AudioProcessor, ProcessorCategory } from "./AudioProcessor";
|
||||
|
||||
export class RingModulator implements AudioProcessor {
|
||||
getName(): string {
|
||||
@ -9,6 +9,10 @@ export class RingModulator implements AudioProcessor {
|
||||
return "Frequency modulation for metallic, bell-like tones";
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Modulation';
|
||||
}
|
||||
|
||||
async process(
|
||||
leftChannel: Float32Array,
|
||||
rightChannel: Float32Array
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { AudioProcessor } from './AudioProcessor';
|
||||
import type { AudioProcessor, ProcessorCategory } from './AudioProcessor';
|
||||
|
||||
export class SegmentShuffler implements AudioProcessor {
|
||||
getName(): string {
|
||||
@ -9,6 +9,10 @@ export class SegmentShuffler implements AudioProcessor {
|
||||
return 'Randomly reorganizes and swaps parts of the sound';
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Time';
|
||||
}
|
||||
|
||||
process(
|
||||
leftChannel: Float32Array,
|
||||
rightChannel: Float32Array
|
||||
|
||||
50
src/lib/audio/processors/SlowTapeStop.ts
Normal file
50
src/lib/audio/processors/SlowTapeStop.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import type { AudioProcessor, ProcessorCategory } from './AudioProcessor';
|
||||
|
||||
export class SlowTapeStop implements AudioProcessor {
|
||||
getName(): string {
|
||||
return 'Slow Tape Stop';
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return 'Simulates a tape machine gradually slowing to a stop';
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Time';
|
||||
}
|
||||
|
||||
process(leftIn: Float32Array, rightIn: Float32Array): [Float32Array, Float32Array] {
|
||||
const length = leftIn.length;
|
||||
const leftOut = new Float32Array(length);
|
||||
const rightOut = new Float32Array(length);
|
||||
|
||||
const stopPoint = length * 0.4;
|
||||
const stopDuration = length * 0.6;
|
||||
|
||||
let readPos = 0;
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
if (i < stopPoint) {
|
||||
readPos = i;
|
||||
} else {
|
||||
const t = (i - stopPoint) / stopDuration;
|
||||
const curve = 1.0 - (t * t * t);
|
||||
const speed = Math.max(0, curve);
|
||||
readPos += speed;
|
||||
}
|
||||
|
||||
const idx = Math.floor(readPos);
|
||||
const frac = readPos - idx;
|
||||
|
||||
if (idx < length - 1) {
|
||||
leftOut[i] = leftIn[idx] * (1.0 - frac) + leftIn[idx + 1] * frac;
|
||||
rightOut[i] = rightIn[idx] * (1.0 - frac) + rightIn[idx + 1] * frac;
|
||||
} else if (idx < length) {
|
||||
leftOut[i] = leftIn[idx];
|
||||
rightOut[i] = rightIn[idx];
|
||||
}
|
||||
}
|
||||
|
||||
return [leftOut, rightOut];
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import type { AudioProcessor } from './AudioProcessor';
|
||||
import type { AudioProcessor, ProcessorCategory } from './AudioProcessor';
|
||||
|
||||
export class SpectralBlur implements AudioProcessor {
|
||||
getName(): string {
|
||||
@ -9,6 +9,10 @@ export class SpectralBlur implements AudioProcessor {
|
||||
return 'Smears frequency content across neighboring bins for dreamy, diffused textures';
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Spectral';
|
||||
}
|
||||
|
||||
process(
|
||||
leftChannel: Float32Array,
|
||||
rightChannel: Float32Array
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { AudioProcessor } from './AudioProcessor';
|
||||
import type { AudioProcessor, ProcessorCategory } from './AudioProcessor';
|
||||
|
||||
export class SpectralShift implements AudioProcessor {
|
||||
getName(): string {
|
||||
@ -9,6 +9,10 @@ export class SpectralShift implements AudioProcessor {
|
||||
return 'Shifts all frequencies by a fixed Hz amount creating inharmonic, metallic timbres';
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Spectral';
|
||||
}
|
||||
|
||||
process(
|
||||
leftChannel: Float32Array,
|
||||
rightChannel: Float32Array
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { AudioProcessor } from './AudioProcessor';
|
||||
import type { AudioProcessor, ProcessorCategory } from './AudioProcessor';
|
||||
|
||||
export class StereoSwap implements AudioProcessor {
|
||||
getName(): string {
|
||||
@ -9,6 +9,10 @@ export class StereoSwap implements AudioProcessor {
|
||||
return 'Swaps left and right channels';
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Space';
|
||||
}
|
||||
|
||||
process(
|
||||
leftChannel: Float32Array,
|
||||
rightChannel: Float32Array
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { AudioProcessor } from "./AudioProcessor";
|
||||
import type { AudioProcessor, ProcessorCategory } from "./AudioProcessor";
|
||||
|
||||
export class StereoWidener implements AudioProcessor {
|
||||
getName(): string {
|
||||
@ -9,6 +9,10 @@ export class StereoWidener implements AudioProcessor {
|
||||
return "Expands stereo field using mid-side processing";
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Space';
|
||||
}
|
||||
|
||||
async process(
|
||||
leftChannel: Float32Array,
|
||||
rightChannel: Float32Array
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { AudioProcessor } from './AudioProcessor';
|
||||
import type { AudioProcessor, ProcessorCategory } from './AudioProcessor';
|
||||
|
||||
export class Stutter implements AudioProcessor {
|
||||
getName(): string {
|
||||
@ -9,6 +9,10 @@ export class Stutter implements AudioProcessor {
|
||||
return 'Rapidly repeats small fragments with smooth crossfades';
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Time';
|
||||
}
|
||||
|
||||
process(
|
||||
leftChannel: Float32Array,
|
||||
rightChannel: Float32Array
|
||||
|
||||
49
src/lib/audio/processors/TapeSpeedUp.ts
Normal file
49
src/lib/audio/processors/TapeSpeedUp.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import type { AudioProcessor, ProcessorCategory } from './AudioProcessor';
|
||||
|
||||
export class TapeSpeedUp implements AudioProcessor {
|
||||
getName(): string {
|
||||
return 'Tape Speed Up';
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return 'Simulates a tape machine accelerating from slow to normal speed';
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Time';
|
||||
}
|
||||
|
||||
process(leftIn: Float32Array, rightIn: Float32Array): [Float32Array, Float32Array] {
|
||||
const length = leftIn.length;
|
||||
const leftOut = new Float32Array(length);
|
||||
const rightOut = new Float32Array(length);
|
||||
|
||||
const accelDuration = length * 0.3;
|
||||
|
||||
let readPos = 0;
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
if (i < accelDuration) {
|
||||
const t = i / accelDuration;
|
||||
const curve = t * t * t;
|
||||
const speed = curve;
|
||||
readPos += speed;
|
||||
} else {
|
||||
readPos += 1.0;
|
||||
}
|
||||
|
||||
const idx = Math.floor(readPos);
|
||||
const frac = readPos - idx;
|
||||
|
||||
if (idx < length - 1) {
|
||||
leftOut[i] = leftIn[idx] * (1.0 - frac) + leftIn[idx + 1] * frac;
|
||||
rightOut[i] = rightIn[idx] * (1.0 - frac) + rightIn[idx + 1] * frac;
|
||||
} else if (idx < length) {
|
||||
leftOut[i] = leftIn[idx];
|
||||
rightOut[i] = rightIn[idx];
|
||||
}
|
||||
}
|
||||
|
||||
return [leftOut, rightOut];
|
||||
}
|
||||
}
|
||||
50
src/lib/audio/processors/TapeStop.ts
Normal file
50
src/lib/audio/processors/TapeStop.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import type { AudioProcessor, ProcessorCategory } from './AudioProcessor';
|
||||
|
||||
export class TapeStop implements AudioProcessor {
|
||||
getName(): string {
|
||||
return 'Tape Stop';
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return 'Simulates a tape machine slowing to a stop';
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Time';
|
||||
}
|
||||
|
||||
process(leftIn: Float32Array, rightIn: Float32Array): [Float32Array, Float32Array] {
|
||||
const length = leftIn.length;
|
||||
const leftOut = new Float32Array(length);
|
||||
const rightOut = new Float32Array(length);
|
||||
|
||||
const stopPoint = length * 0.7;
|
||||
const stopDuration = length * 0.3;
|
||||
|
||||
let readPos = 0;
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
if (i < stopPoint) {
|
||||
readPos = i;
|
||||
} else {
|
||||
const t = (i - stopPoint) / stopDuration;
|
||||
const curve = 1.0 - (t * t * t);
|
||||
const speed = Math.max(0, curve);
|
||||
readPos += speed;
|
||||
}
|
||||
|
||||
const idx = Math.floor(readPos);
|
||||
const frac = readPos - idx;
|
||||
|
||||
if (idx < length - 1) {
|
||||
leftOut[i] = leftIn[idx] * (1.0 - frac) + leftIn[idx + 1] * frac;
|
||||
rightOut[i] = rightIn[idx] * (1.0 - frac) + rightIn[idx + 1] * frac;
|
||||
} else if (idx < length) {
|
||||
leftOut[i] = leftIn[idx];
|
||||
rightOut[i] = rightIn[idx];
|
||||
}
|
||||
}
|
||||
|
||||
return [leftOut, rightOut];
|
||||
}
|
||||
}
|
||||
48
src/lib/audio/processors/TapeWobble.ts
Normal file
48
src/lib/audio/processors/TapeWobble.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import type { AudioProcessor, ProcessorCategory } from './AudioProcessor';
|
||||
|
||||
export class TapeWobble implements AudioProcessor {
|
||||
getName(): string {
|
||||
return 'Tape Wobble';
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return 'Simulates tape machine speed instability with pitch variations';
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Time';
|
||||
}
|
||||
|
||||
process(leftIn: Float32Array, rightIn: Float32Array): [Float32Array, Float32Array] {
|
||||
const length = leftIn.length;
|
||||
const leftOut = new Float32Array(length);
|
||||
const rightOut = new Float32Array(length);
|
||||
|
||||
const sampleRate = 44100;
|
||||
const wobbleFreq = 2.0 + Math.random() * 2.0;
|
||||
const wobbleDepth = 0.015;
|
||||
|
||||
let readPos = 0;
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
const t = i / sampleRate;
|
||||
const wobble = Math.sin(2.0 * Math.PI * wobbleFreq * t) * wobbleDepth;
|
||||
const speed = 1.0 + wobble;
|
||||
|
||||
readPos += speed;
|
||||
|
||||
const idx = Math.floor(readPos);
|
||||
const frac = readPos - idx;
|
||||
|
||||
if (idx < length - 1) {
|
||||
leftOut[i] = leftIn[idx] * (1.0 - frac) + leftIn[idx + 1] * frac;
|
||||
rightOut[i] = rightIn[idx] * (1.0 - frac) + rightIn[idx + 1] * frac;
|
||||
} else if (idx < length) {
|
||||
leftOut[i] = leftIn[idx];
|
||||
rightOut[i] = rightIn[idx];
|
||||
}
|
||||
}
|
||||
|
||||
return [leftOut, rightOut];
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import type { AudioProcessor } from './AudioProcessor';
|
||||
import type { AudioProcessor, ProcessorCategory } from './AudioProcessor';
|
||||
|
||||
export class Tremolo implements AudioProcessor {
|
||||
getName(): string {
|
||||
@ -9,6 +9,10 @@ export class Tremolo implements AudioProcessor {
|
||||
return 'Applies rhythmic volume modulation';
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Amplitude';
|
||||
}
|
||||
|
||||
process(
|
||||
leftChannel: Float32Array,
|
||||
rightChannel: Float32Array
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { AudioProcessor } from "./AudioProcessor";
|
||||
import type { AudioProcessor, ProcessorCategory } from "./AudioProcessor";
|
||||
|
||||
export class TrimSilence implements AudioProcessor {
|
||||
getName(): string {
|
||||
@ -9,6 +9,10 @@ export class TrimSilence implements AudioProcessor {
|
||||
return "Removes leading and trailing silence from audio";
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Utility';
|
||||
}
|
||||
|
||||
async process(
|
||||
leftChannel: Float32Array,
|
||||
rightChannel: Float32Array
|
||||
|
||||
50
src/lib/audio/processors/VinylStop.ts
Normal file
50
src/lib/audio/processors/VinylStop.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import type { AudioProcessor, ProcessorCategory } from './AudioProcessor';
|
||||
|
||||
export class VinylStop implements AudioProcessor {
|
||||
getName(): string {
|
||||
return 'Vinyl Stop';
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return 'Simulates a turntable slowing to a stop with realistic physics';
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Time';
|
||||
}
|
||||
|
||||
process(leftIn: Float32Array, rightIn: Float32Array): [Float32Array, Float32Array] {
|
||||
const length = leftIn.length;
|
||||
const leftOut = new Float32Array(length);
|
||||
const rightOut = new Float32Array(length);
|
||||
|
||||
const stopPoint = length * 0.7;
|
||||
const stopDuration = length * 0.3;
|
||||
|
||||
let readPos = 0;
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
if (i < stopPoint) {
|
||||
readPos = i;
|
||||
} else {
|
||||
const t = (i - stopPoint) / stopDuration;
|
||||
const curve = Math.exp(-5.0 * t);
|
||||
const speed = Math.max(0, curve);
|
||||
readPos += speed;
|
||||
}
|
||||
|
||||
const idx = Math.floor(readPos);
|
||||
const frac = readPos - idx;
|
||||
|
||||
if (idx < length - 1) {
|
||||
leftOut[i] = leftIn[idx] * (1.0 - frac) + leftIn[idx + 1] * frac;
|
||||
rightOut[i] = rightIn[idx] * (1.0 - frac) + rightIn[idx + 1] * frac;
|
||||
} else if (idx < length) {
|
||||
leftOut[i] = leftIn[idx];
|
||||
rightOut[i] = rightIn[idx];
|
||||
}
|
||||
}
|
||||
|
||||
return [leftOut, rightOut];
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import type { AudioProcessor } from "./AudioProcessor";
|
||||
import type { AudioProcessor, ProcessorCategory } from "./AudioProcessor";
|
||||
|
||||
export class Waveshaper implements AudioProcessor {
|
||||
getName(): string {
|
||||
@ -9,6 +9,10 @@ export class Waveshaper implements AudioProcessor {
|
||||
return "Transfer function distortion with various curve shapes";
|
||||
}
|
||||
|
||||
getCategory(): ProcessorCategory {
|
||||
return 'Distortion';
|
||||
}
|
||||
|
||||
async process(
|
||||
leftChannel: Float32Array,
|
||||
rightChannel: Float32Array
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -1,28 +1,77 @@
|
||||
<script lang="ts">
|
||||
import { getAllProcessors } from "../audio/processors/registry";
|
||||
import type { AudioProcessor } from "../audio/processors/AudioProcessor";
|
||||
import type { AudioProcessor, ProcessorCategory } from "../audio/processors/AudioProcessor";
|
||||
|
||||
interface Props {
|
||||
onselect: (processor: AudioProcessor) => void;
|
||||
selectedCategory: ProcessorCategory | 'All';
|
||||
onselectcategory: (category: ProcessorCategory | 'All') => void;
|
||||
}
|
||||
|
||||
let { onselect }: Props = $props();
|
||||
let { onselect, selectedCategory, onselectcategory }: Props = $props();
|
||||
|
||||
const allProcessors = getAllProcessors().sort((a, b) =>
|
||||
a.getName().localeCompare(b.getName())
|
||||
const allProcessors = getAllProcessors();
|
||||
|
||||
const allCategories: ProcessorCategory[] = [
|
||||
'Amplitude',
|
||||
'Filter',
|
||||
'Time',
|
||||
'Space',
|
||||
'Pitch',
|
||||
'Modulation',
|
||||
'Distortion',
|
||||
'Spectral',
|
||||
'Utility'
|
||||
];
|
||||
|
||||
const categoryCountsMap = $derived.by(() => {
|
||||
const counts = new Map<ProcessorCategory, number>();
|
||||
for (const processor of allProcessors) {
|
||||
const category = processor.getCategory();
|
||||
counts.set(category, (counts.get(category) || 0) + 1);
|
||||
}
|
||||
return counts;
|
||||
});
|
||||
|
||||
const filteredProcessors = $derived(
|
||||
selectedCategory === 'All'
|
||||
? allProcessors.sort((a, b) => a.getName().localeCompare(b.getName()))
|
||||
: allProcessors
|
||||
.filter(p => p.getCategory() === selectedCategory)
|
||||
.sort((a, b) => a.getName().localeCompare(b.getName()))
|
||||
);
|
||||
</script>
|
||||
|
||||
<div class="processor-popup">
|
||||
{#each allProcessors as processor}
|
||||
<div class="popup-sidebar">
|
||||
<button
|
||||
class="processor-tile"
|
||||
data-description={processor.getDescription()}
|
||||
onclick={() => onselect(processor)}
|
||||
class="category-filter"
|
||||
class:active={selectedCategory === 'All'}
|
||||
onclick={() => onselectcategory('All')}
|
||||
>
|
||||
{processor.getName()}
|
||||
All ({allProcessors.length})
|
||||
</button>
|
||||
{/each}
|
||||
{#each allCategories as category}
|
||||
<button
|
||||
class="category-filter"
|
||||
class:active={selectedCategory === category}
|
||||
onclick={() => onselectcategory(category)}
|
||||
>
|
||||
{category} ({categoryCountsMap.get(category) || 0})
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="processors-grid">
|
||||
{#each filteredProcessors as processor}
|
||||
<button
|
||||
class="processor-tile"
|
||||
data-description={processor.getDescription()}
|
||||
onclick={() => onselect(processor)}
|
||||
>
|
||||
{processor.getName()}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@ -31,18 +80,74 @@
|
||||
bottom: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background-color: #000;
|
||||
background-color: #0a0a0a;
|
||||
border: 2px solid #fff;
|
||||
padding: 0.5rem;
|
||||
z-index: 1000;
|
||||
width: 90vw;
|
||||
max-width: 500px;
|
||||
margin-bottom: 0.5rem;
|
||||
max-height: 60vh;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.popup-sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #1a1a1a;
|
||||
border-right: 1px solid #333;
|
||||
min-width: 80px;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #444 transparent;
|
||||
}
|
||||
|
||||
.popup-sidebar::-webkit-scrollbar {
|
||||
width: 3px;
|
||||
}
|
||||
|
||||
.popup-sidebar::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.popup-sidebar::-webkit-scrollbar-thumb {
|
||||
background: #444;
|
||||
}
|
||||
|
||||
.category-filter {
|
||||
font-size: 0.65rem;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
padding: 0.5rem 0.4rem;
|
||||
background-color: #1a1a1a;
|
||||
border: none;
|
||||
border-bottom: 1px solid #2a2a2a;
|
||||
color: #888;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s, color 0.2s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.category-filter:hover {
|
||||
background-color: #222;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.category-filter.active {
|
||||
background-color: #0a0a0a;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.processors-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 0.4rem;
|
||||
width: 90vw;
|
||||
max-width: 400px;
|
||||
margin-bottom: 0.5rem;
|
||||
max-height: 60vh;
|
||||
padding: 0.5rem;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.processor-tile {
|
||||
@ -90,11 +195,23 @@
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.processor-popup {
|
||||
width: 550px;
|
||||
max-width: 550px;
|
||||
}
|
||||
|
||||
.popup-sidebar {
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.category-filter {
|
||||
font-size: 0.7rem;
|
||||
padding: 0.5rem 0.6rem;
|
||||
}
|
||||
|
||||
.processors-grid {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
width: 500px;
|
||||
max-width: 500px;
|
||||
padding: 0.6rem;
|
||||
gap: 0.5rem;
|
||||
padding: 0.6rem;
|
||||
}
|
||||
|
||||
.processor-tile {
|
||||
@ -114,9 +231,16 @@
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.processor-popup {
|
||||
width: 650px;
|
||||
max-width: 650px;
|
||||
}
|
||||
|
||||
.popup-sidebar {
|
||||
min-width: 110px;
|
||||
}
|
||||
|
||||
.processors-grid {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
width: 600px;
|
||||
max-width: 600px;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
|
||||
@ -5,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();
|
||||
|
||||
@ -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>
|
||||
|
||||
64
src/lib/utils/UndoManager.ts
Normal file
64
src/lib/utils/UndoManager.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
|
||||
@ -8,6 +8,8 @@ const STORAGE_KEYS = {
|
||||
DURATION: 'duration',
|
||||
PITCH_LOCK_ENABLED: 'pitchLockEnabled',
|
||||
PITCH_LOCK_FREQUENCY: 'pitchLockFrequency',
|
||||
EXPANDED_CATEGORIES: 'expandedCategories',
|
||||
SELECTED_PROCESSOR_CATEGORY: 'selectedProcessorCategory',
|
||||
} as const;
|
||||
|
||||
export function loadVolume(): number {
|
||||
@ -45,3 +47,29 @@ export function loadPitchLockFrequency(): number {
|
||||
export function savePitchLockFrequency(frequency: number): void {
|
||||
localStorage.setItem(STORAGE_KEYS.PITCH_LOCK_FREQUENCY, frequency.toString());
|
||||
}
|
||||
|
||||
export function loadExpandedCategories(): Set<string> {
|
||||
const stored = localStorage.getItem(STORAGE_KEYS.EXPANDED_CATEGORIES);
|
||||
if (stored) {
|
||||
try {
|
||||
const parsed = JSON.parse(stored);
|
||||
return new Set(Array.isArray(parsed) ? parsed : []);
|
||||
} catch {
|
||||
return new Set();
|
||||
}
|
||||
}
|
||||
return new Set();
|
||||
}
|
||||
|
||||
export function saveExpandedCategories(categories: Set<string>): void {
|
||||
localStorage.setItem(STORAGE_KEYS.EXPANDED_CATEGORIES, JSON.stringify(Array.from(categories)));
|
||||
}
|
||||
|
||||
export function loadSelectedProcessorCategory(): string {
|
||||
const stored = localStorage.getItem(STORAGE_KEYS.SELECTED_PROCESSOR_CATEGORY);
|
||||
return stored || 'All';
|
||||
}
|
||||
|
||||
export function saveSelectedProcessorCategory(category: string): void {
|
||||
localStorage.setItem(STORAGE_KEYS.SELECTED_PROCESSOR_CATEGORY, category);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user