mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-21 20:55:12 -04:00
Compare commits
226 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bf21c4cf02 | |||
| 741ff3f3f0 | |||
| 4476921069 | |||
| 8c5e4a5780 | |||
| 34ae1bb305 | |||
| 0cf11adc2c | |||
| 7a5020aa71 | |||
| a205f7d873 | |||
| 0e9c50bf0a | |||
| e837c516c5 | |||
| 3b2e9fa0b4 | |||
| 6a47139e0d | |||
| f7f8c56c0a | |||
| c7e88e1ff2 | |||
| f6a43f1b10 | |||
| a3b183d304 | |||
| f7175c0505 | |||
| e26040d154 | |||
| 17a1b75e5b | |||
| 7df9688bb9 | |||
| d557bb59f0 | |||
| 679e81eb0f | |||
| 37dd834398 | |||
| 959e8ac7bc | |||
| 16524ca805 | |||
| b4b0194455 | |||
| fd472a64d9 | |||
| 58094973b0 | |||
| db49b37780 | |||
| bf243c04b9 | |||
| bc05cffdbc | |||
| 7b1e02e8cf | |||
| 257f09da4f | |||
| ecd58fa501 | |||
| c4ece4932e | |||
| 5c9855bf12 | |||
| e54b9c3a32 | |||
| 6afe0dc714 | |||
| ebaf7624f9 | |||
| 882bcc513f | |||
| aec33710b7 | |||
| d41ebf190b | |||
| 7953fa61e4 | |||
| 6051b922c0 | |||
| 367e07da0c | |||
| 0edede106a | |||
| b6264a1513 | |||
| 680b7d78ec | |||
| e79b4b8ccb | |||
| 98367868ac | |||
| 616bc64e70 | |||
| c0f4f47150 | |||
| b4496fcc24 | |||
| 48f6a41683 | |||
| 7bce739068 | |||
| 06f9c2dfb5 | |||
| 30fe5b47e3 | |||
| 1823993e7c | |||
| 1935195e9a | |||
| 300a1bbfd5 | |||
| 2f43dcab0c | |||
| 125eece223 | |||
| 53e49952d8 | |||
| e286ab2830 | |||
| e4e2d60b28 | |||
| 99bec835f8 | |||
| d349d2a0cd | |||
| c7a2c2bb9c | |||
| 5a51b4ec71 | |||
| 3922defab9 | |||
| a9752fb2a4 | |||
| 0a2f2eaa3d | |||
| 0de212c12f | |||
| b376b2616a | |||
| 56ecb006fa | |||
| 2748189f18 | |||
| 63d254daf4 | |||
| 34991e9602 | |||
| 8df5c80d82 | |||
| e0344dbd67 | |||
| 931d684b19 | |||
| 4a7344595d | |||
| 18b3c66eb8 | |||
| 63de46ae96 | |||
| c598059dc7 | |||
| dec037bed5 | |||
| 29f7f5c1f8 | |||
| bae5571df0 | |||
| 4b08dd8447 | |||
| 11e3da0552 | |||
| ef2ee0969a | |||
| 12f9af227c | |||
| dc5f6827f9 | |||
| 8baecf78b9 | |||
| 8eeebf1e4f | |||
| c7789a09e3 | |||
| a53385aa11 | |||
| 0a5fb8ca1d | |||
| 99e4999e6a | |||
| cd071cb7c0 | |||
| 8e1fba0436 | |||
| f4df4f679f | |||
| 1d2a3bdb9c | |||
| c6e0f0533e | |||
| 415b6d5f1d | |||
| 4169b764c3 | |||
| e7d1613bfb | |||
| 45aa1f5384 | |||
| 3a17aeabf3 | |||
| 2aeba2e53f | |||
| 3608f1ca25 | |||
| 37990b459f | |||
| a34e58aa09 | |||
| 8cbe4b945c | |||
| 6f9a1470e7 | |||
| 8a3719752f | |||
| a79491dd16 | |||
| 53c101005c | |||
| 82ca77be9d | |||
| 173e359a65 | |||
| ad43259353 | |||
| 32924b3fcf | |||
| 4c99f4866b | |||
| d50af71ba1 | |||
| 0664f90178 | |||
| 4321814d36 | |||
| 6610713018 | |||
| 600ab0a83e | |||
| d7e240e2dc | |||
| 25c20296a1 | |||
| 2db30a710b | |||
| 71d1fe99e4 | |||
| 05ed16e158 | |||
| 24b69ad1bc | |||
| 5bddb6f48d | |||
| c5250d7794 | |||
| 7d890156f4 | |||
| 8f9967c76d | |||
| 91ebdd5ea6 | |||
| fc74dcdb1a | |||
| d84db1e663 | |||
| 8529516c66 | |||
| 0e3840aff4 | |||
| a929b36770 | |||
| 2794541dad | |||
| 3e118ce0a8 | |||
| c3ad0d1789 | |||
| 19b710c401 | |||
| 0dbca05de0 | |||
| f8796d039f | |||
| 442f487973 | |||
| 7ccdefe1c6 | |||
| b73f624058 | |||
| e4466082eb | |||
| 29b6729246 | |||
| b3df7f438f | |||
| 3b1c75d3de | |||
| 48407c309c | |||
| 17af92b2df | |||
| e90aa5202d | |||
| fb7f686519 | |||
| aca500ba73 | |||
| 03d2f363f1 | |||
| ff370cf86f | |||
| 9cbeaef88d | |||
| ca936aada7 | |||
| fe70a2df43 | |||
| 6baca67cd3 | |||
| 85f6454340 | |||
| 16f885c010 | |||
| 5a70599aac | |||
| 7b74a902a2 | |||
| 95700e1196 | |||
| e7578be6c5 | |||
| 28df4259b8 | |||
| 5e27358fe8 | |||
| daea207aff | |||
| 92d3fe496d | |||
| 98031676d6 | |||
| 498624f6fe | |||
| bf702c3b2a | |||
| 6e4dae4dfa | |||
| a8ea9a1b63 | |||
| a2b76bde14 | |||
| 0f652e8b5b | |||
| 2a0635879c | |||
| f5fc0ff9d1 | |||
| 7774364ac7 | |||
| 329289fc35 | |||
| 8b3c0d2a4a | |||
| 120b46c4a5 | |||
| 4bdaf577c0 | |||
| 428a4b3dd7 | |||
| ff83994733 | |||
| 418bca79b2 | |||
| b3715dc5df | |||
| 9a47e0f11e | |||
| 908c0e44ff | |||
| 920776fdfe | |||
| 0fbde61b2d | |||
| 92a82ce4bd | |||
| b2ce4591d9 | |||
| 1be9f40574 | |||
| 23aa8ef509 | |||
| 945b325336 | |||
| 502f0b5c23 | |||
| 2ccbb0596d | |||
| 862b88b6c4 | |||
| cd7865a66f | |||
| 9c8e39d83d | |||
| 0a4b6a01b3 | |||
| b3e433291c | |||
| 969c9663ef | |||
| 5e4e678800 | |||
| b8423f0ed1 | |||
| 5d5159a08f | |||
| 1e96d59ac8 | |||
| 6a9739bed5 | |||
| d8f0d70908 | |||
| e25eab11ad | |||
| efb337677c | |||
| 4a70ac5a99 | |||
| 554d6118c6 | |||
| 03271ec5fb | |||
| a0760f9468 | |||
| 058d950121 |
@@ -1,6 +1,9 @@
|
|||||||
name: Build and Deploy to beta (warm.strudel.cc)
|
name: Build and Deploy to beta (warm.strudel.cc)
|
||||||
|
|
||||||
on: [workflow_dispatch]
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
# Allow one concurrent deployment
|
# Allow one concurrent deployment
|
||||||
concurrency:
|
concurrency:
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
# strudel
|
# strudel
|
||||||
|
|
||||||
Live coding patterns on the web
|
Live coding patterns on the web
|
||||||
https://strudel.cc/
|
|
||||||
|
|
||||||
- Try it here: <https://strudel.cc>
|
- Try it here: <https://strudel.cc>
|
||||||
- Docs: <https://strudel.cc/learn>
|
- Docs: <https://strudel.cc/learn>
|
||||||
|
- Source: https://codeberg.org/uzu/strudel/
|
||||||
|
* Along with many other live coding projects, we have moved from Microsoft's Github platform to Codeberg for ethical reasons. **Please don't fork the project back to github**.
|
||||||
- Technical Blog Post: <https://loophole-letters.vercel.app/strudel>
|
- Technical Blog Post: <https://loophole-letters.vercel.app/strudel>
|
||||||
- 1 Year of Strudel Blog Post: <https://loophole-letters.vercel.app/strudel1year>
|
- 1 Year of Strudel Blog Post: <https://loophole-letters.vercel.app/strudel1year>
|
||||||
- 2 Years of Strudel Blog Post: <https://strudel.cc/blog/#year-2>
|
- 2 Years of Strudel Blog Post: <https://strudel.cc/blog/#year-2>
|
||||||
|
|
||||||
|
|
||||||
## Running Locally
|
## Running Locally
|
||||||
|
|
||||||
After cloning the project, you can run the REPL locally:
|
After cloning the project, you can run the REPL locally:
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import { updateWidgets, widgetPlugin } from './widget.mjs';
|
|||||||
|
|
||||||
export { toggleBlockComment, toggleBlockCommentByLine, toggleComment, toggleLineComment } from '@codemirror/commands';
|
export { toggleBlockComment, toggleBlockCommentByLine, toggleComment, toggleLineComment } from '@codemirror/commands';
|
||||||
|
|
||||||
const extensions = {
|
export const extensions = {
|
||||||
isLineWrappingEnabled: (on) => (on ? EditorView.lineWrapping : []),
|
isLineWrappingEnabled: (on) => (on ? EditorView.lineWrapping : []),
|
||||||
isBracketMatchingEnabled: (on) => (on ? bracketMatching({ brackets: '()[]{}<>' }) : []),
|
isBracketMatchingEnabled: (on) => (on ? bracketMatching({ brackets: '()[]{}<>' }) : []),
|
||||||
isBracketClosingEnabled: (on) => (on ? closeBrackets() : []),
|
isBracketClosingEnabled: (on) => (on ? closeBrackets() : []),
|
||||||
@@ -48,7 +48,7 @@ const extensions = {
|
|||||||
]
|
]
|
||||||
: [],
|
: [],
|
||||||
};
|
};
|
||||||
const compartments = Object.fromEntries(Object.keys(extensions).map((key) => [key, new Compartment()]));
|
export const compartments = Object.fromEntries(Object.keys(extensions).map((key) => [key, new Compartment()]));
|
||||||
|
|
||||||
export const defaultSettings = {
|
export const defaultSettings = {
|
||||||
keybindings: 'codemirror',
|
keybindings: 'codemirror',
|
||||||
@@ -293,9 +293,9 @@ export class StrudelMirror {
|
|||||||
console.warn('first frame could not be painted');
|
console.warn('first frame could not be painted');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async evaluate() {
|
async evaluate(autostart = true) {
|
||||||
this.flash();
|
this.flash();
|
||||||
await this.repl.evaluate(this.code);
|
await this.repl.evaluate(this.code, autostart);
|
||||||
}
|
}
|
||||||
async stop() {
|
async stop() {
|
||||||
this.repl.scheduler.stop();
|
this.repl.scheduler.stop();
|
||||||
@@ -411,7 +411,7 @@ export class StrudelMirror {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseBooleans(value) {
|
export function parseBooleans(value) {
|
||||||
return { true: true, false: false }[value] ?? value;
|
return { true: true, false: false }[value] ?? value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,3 +4,4 @@ export * from './flash.mjs';
|
|||||||
export * from './slider.mjs';
|
export * from './slider.mjs';
|
||||||
export * from './themes.mjs';
|
export * from './themes.mjs';
|
||||||
export * from './widget.mjs';
|
export * from './widget.mjs';
|
||||||
|
export { Vim } from './keybindings.mjs';
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { defaultKeymap } from '@codemirror/commands';
|
import { defaultKeymap } from '@codemirror/commands';
|
||||||
import { Prec } from '@codemirror/state';
|
import { Prec, EditorState } from '@codemirror/state';
|
||||||
import { keymap, ViewPlugin } from '@codemirror/view';
|
import { keymap, ViewPlugin } from '@codemirror/view';
|
||||||
// import { searchKeymap } from '@codemirror/search';
|
// import { searchKeymap } from '@codemirror/search';
|
||||||
import { emacs } from '@replit/codemirror-emacs';
|
import { emacs } from '@replit/codemirror-emacs';
|
||||||
@@ -131,7 +131,13 @@ const keymaps = {
|
|||||||
vscode: vscodeExtension,
|
vscode: vscodeExtension,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export { Vim } from '@replit/codemirror-vim';
|
||||||
|
|
||||||
export function keybindings(name) {
|
export function keybindings(name) {
|
||||||
const active = keymaps[name];
|
const active = keymaps[name];
|
||||||
return [active ? Prec.high(active()) : []];
|
const extensions = active ? [Prec.high(active())] : [];
|
||||||
|
if (name === 'vim') {
|
||||||
|
extensions.push(EditorState.allowMultipleSelections.of(true));
|
||||||
|
}
|
||||||
|
return extensions;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { describe, bench } from 'vitest';
|
||||||
|
|
||||||
|
import { calculateSteps, rand, useRNG } from '../index.mjs';
|
||||||
|
|
||||||
|
const testingResolution = 128;
|
||||||
|
|
||||||
|
const _generateRandomPattern = () => rand.iter(testingResolution).fast(testingResolution).firstCycle();
|
||||||
|
|
||||||
|
describe('old random', () => {
|
||||||
|
calculateSteps(true);
|
||||||
|
bench(
|
||||||
|
'+tactus',
|
||||||
|
() => {
|
||||||
|
useRNG('legacy');
|
||||||
|
_generateRandomPattern();
|
||||||
|
},
|
||||||
|
{
|
||||||
|
time: 1000,
|
||||||
|
teardown() {
|
||||||
|
useRNG('legacy');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
calculateSteps(false);
|
||||||
|
bench(
|
||||||
|
'-tactus',
|
||||||
|
() => {
|
||||||
|
useRNG('precise');
|
||||||
|
_generateRandomPattern();
|
||||||
|
},
|
||||||
|
{
|
||||||
|
time: 1000,
|
||||||
|
teardown() {
|
||||||
|
useRNG('legacy');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('random', () => {
|
||||||
|
calculateSteps(true);
|
||||||
|
bench('+tactus', _generateRandomPattern, { time: 1000 });
|
||||||
|
|
||||||
|
calculateSteps(false);
|
||||||
|
bench('-tactus', _generateRandomPattern, { time: 1000 });
|
||||||
|
});
|
||||||
|
|
||||||
|
calculateSteps(true);
|
||||||
+275
-4
@@ -4,7 +4,8 @@ Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/
|
|||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Pattern, register, reify } from './pattern.mjs';
|
import { logger } from './logger.mjs';
|
||||||
|
import { Pattern, pure, register, reify } from './pattern.mjs';
|
||||||
|
|
||||||
export function createParam(names) {
|
export function createParam(names) {
|
||||||
let isMulti = Array.isArray(names);
|
let isMulti = Array.isArray(names);
|
||||||
@@ -2044,6 +2045,28 @@ export const { octave, oct } = registerControl('octave', 'oct');
|
|||||||
* )
|
* )
|
||||||
*/
|
*/
|
||||||
export const { orbit } = registerControl('orbit', 'o');
|
export const { orbit } = registerControl('orbit', 'o');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A `bus` is a send which can be used for mixing patterns. It combines with..
|
||||||
|
* s("bus") to play that bus through another pattern (for, say, applying non-linear
|
||||||
|
* effects like distortion to multiple signals)
|
||||||
|
*
|
||||||
|
* otherPat.bmod(..) (to modulate another pattern with the bus)
|
||||||
|
*
|
||||||
|
* @name bus
|
||||||
|
* @param {number | Pattern} number
|
||||||
|
*/
|
||||||
|
export const { bus } = registerControl('bus');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Postgain multiplier prior to sending the signal to the audio bus.
|
||||||
|
*
|
||||||
|
* @name busgain
|
||||||
|
* @synonyms bgain
|
||||||
|
* @param {number | Pattern} number
|
||||||
|
*/
|
||||||
|
export const { busgain, bgain } = registerControl('busgain', 'bgain');
|
||||||
|
|
||||||
// TODO: what is this? not found in tidal doc Answer: gain is limited to maximum of 2. This allows you to go over that
|
// TODO: what is this? not found in tidal doc Answer: gain is limited to maximum of 2. This allows you to go over that
|
||||||
export const { overgain } = registerControl('overgain');
|
export const { overgain } = registerControl('overgain');
|
||||||
// TODO: what is this? not found in tidal doc. Similar to above, but limited to 1
|
// TODO: what is this? not found in tidal doc. Similar to above, but limited to 1
|
||||||
@@ -2088,8 +2111,7 @@ export const { panorient } = registerControl('panorient');
|
|||||||
// ['pitch2'],
|
// ['pitch2'],
|
||||||
// ['pitch3'],
|
// ['pitch3'],
|
||||||
// ['portamento'],
|
// ['portamento'],
|
||||||
// TODO: LFO rate see https://tidalcycles.org/docs/patternlib/tutorials/synthesizers/#supersquare
|
|
||||||
export const { rate } = registerControl('rate');
|
|
||||||
// TODO: slide param for certain synths
|
// TODO: slide param for certain synths
|
||||||
export const { slide } = registerControl('slide');
|
export const { slide } = registerControl('slide');
|
||||||
// TODO: detune? https://tidalcycles.org/docs/patternlib/tutorials/synthesizers/#supersquare
|
// TODO: detune? https://tidalcycles.org/docs/patternlib/tutorials/synthesizers/#supersquare
|
||||||
@@ -2515,7 +2537,6 @@ export const { curve } = registerControl('curve');
|
|||||||
export const { deltaSlide } = registerControl('deltaSlide');
|
export const { deltaSlide } = registerControl('deltaSlide');
|
||||||
export const { pitchJump } = registerControl('pitchJump');
|
export const { pitchJump } = registerControl('pitchJump');
|
||||||
export const { pitchJumpTime } = registerControl('pitchJumpTime');
|
export const { pitchJumpTime } = registerControl('pitchJumpTime');
|
||||||
export const { lfo, repeatTime } = registerControl('lfo', 'repeatTime');
|
|
||||||
// noise on the frequency or as bubo calls it "frequency fog" :)
|
// noise on the frequency or as bubo calls it "frequency fog" :)
|
||||||
export const { znoise } = registerControl('znoise');
|
export const { znoise } = registerControl('znoise');
|
||||||
export const { zmod } = registerControl('zmod');
|
export const { zmod } = registerControl('zmod');
|
||||||
@@ -2792,6 +2813,254 @@ export const scrub = register(
|
|||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const subControlAliases = new Map();
|
||||||
|
const registerSubControl = (control, subControl, ...aliases) => {
|
||||||
|
const aliasMap = subControlAliases.get(control) ?? new Map();
|
||||||
|
const allKeys = new Set([subControl, ...aliases]);
|
||||||
|
for (const alias of allKeys) {
|
||||||
|
aliasMap.set(String(alias).toLowerCase(), subControl);
|
||||||
|
}
|
||||||
|
subControlAliases.set(control, aliasMap);
|
||||||
|
};
|
||||||
|
|
||||||
|
const registerSubControls = (control, subControlAliases = []) => {
|
||||||
|
for (const [subControl, ...aliases] of subControlAliases) {
|
||||||
|
registerSubControl(control, subControl, ...aliases);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getMainSubcontrolName = (control, subKey) => {
|
||||||
|
const aliasMap = subControlAliases.get(control);
|
||||||
|
if (!aliasMap) return subKey;
|
||||||
|
return aliasMap.get(String(subKey).toLowerCase()) ?? subKey;
|
||||||
|
};
|
||||||
|
|
||||||
|
registerSubControls('lfo', [
|
||||||
|
['control', 'c'],
|
||||||
|
['subControl', 'sc'],
|
||||||
|
['rate', 'r'],
|
||||||
|
['depth', 'dep', 'dr'],
|
||||||
|
['depthabs', 'da'],
|
||||||
|
['dcoffset', 'dc'],
|
||||||
|
['shape', 'sh'],
|
||||||
|
['skew', 'sk'],
|
||||||
|
['curve', 'cu'],
|
||||||
|
['sync', 's'],
|
||||||
|
['fxi'],
|
||||||
|
]);
|
||||||
|
registerSubControls('env', [
|
||||||
|
['control', 'c'],
|
||||||
|
['subControl', 'sc'],
|
||||||
|
['attack', 'att', 'a'],
|
||||||
|
['decay', 'dec', 'd'],
|
||||||
|
['sustain', 'sus', 's'],
|
||||||
|
['release', 'rel', 'r'],
|
||||||
|
['depth', 'dep', 'dr'],
|
||||||
|
['depthabs', 'da'],
|
||||||
|
['acurve', 'ac'],
|
||||||
|
['dcurve', 'dc'],
|
||||||
|
['rcurve', 'rc'],
|
||||||
|
['fxi'],
|
||||||
|
]);
|
||||||
|
registerSubControls('bmod', [
|
||||||
|
['bus', 'b'],
|
||||||
|
['control', 'c'],
|
||||||
|
['subControl', 'sc'],
|
||||||
|
['depth', 'dep', 'dr'],
|
||||||
|
['depthabs', 'da'],
|
||||||
|
['dc'],
|
||||||
|
['fxi'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
Pattern.prototype.modulate = function (type, config, idPat) {
|
||||||
|
config = { control: undefined, ...config };
|
||||||
|
const modulatorKeys = ['lfo', 'env', 'bmod'];
|
||||||
|
if (!modulatorKeys.includes(type)) {
|
||||||
|
logger(`[core] Modulation type ${type} not found. Please use one of 'lfo', 'env', 'bmod'`);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
let output = this;
|
||||||
|
let defaultValue = undefined;
|
||||||
|
// Copy value into a temporary `v` container and attach a single `id` (to be shared across
|
||||||
|
// each config entry). At the output we destructure and throw away the id
|
||||||
|
output = output.fmap((v) => (id) => ({ v, id })).appLeft(reify(idPat));
|
||||||
|
for (const [rawKey, value] of Object.entries(config)) {
|
||||||
|
const key = getMainSubcontrolName(type, rawKey);
|
||||||
|
const valuePat = reify(value);
|
||||||
|
output = output
|
||||||
|
.fmap(({ v, id }) => (c) => {
|
||||||
|
if (defaultValue === undefined) {
|
||||||
|
// default control to the control set just before this in the chain
|
||||||
|
// e.g. pat.gain(0.5).lfo({..}) will be a gain-LFO
|
||||||
|
let control = getControlName(Object.keys(v).at(-1));
|
||||||
|
if (modulatorKeys.includes(control)) {
|
||||||
|
control = `${control}_${[...v[control].__ids].at(-1)}`;
|
||||||
|
}
|
||||||
|
defaultValue = control;
|
||||||
|
}
|
||||||
|
v[type] ??= { __ids: new Set() };
|
||||||
|
const t = v[type];
|
||||||
|
id ??= t.__ids.size;
|
||||||
|
t[id] ??= { control: defaultValue };
|
||||||
|
t.__ids.add(id); // keeps track of insertion order
|
||||||
|
if (c === undefined) return { v, id };
|
||||||
|
if (key === 'control' || key === 'subControl') {
|
||||||
|
t[id][key] = getControlName(c);
|
||||||
|
} else {
|
||||||
|
t[id][key] = c;
|
||||||
|
}
|
||||||
|
return { v, id };
|
||||||
|
})
|
||||||
|
.appLeft(valuePat);
|
||||||
|
}
|
||||||
|
return output.fmap(({ v }) => v);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures an LFO. Can be called in sequence like pat.lfo(...).lfo(...) to set up multiple LFOs.
|
||||||
|
* There are two ways to declare which control will be modulated:
|
||||||
|
* 1. Explicitly put `control` in the config (e.g. `lfo({ c: "lpf" })`)
|
||||||
|
* 2. If the control parameter is absent, the control _immediately before_ the `lfo` call will be used
|
||||||
|
* (e.g. `s("saw").lpf(500).lfo()` to modulate `lpf`)
|
||||||
|
*
|
||||||
|
* Modulators can be referred to by `id` so that they can be updated later e.g. inside
|
||||||
|
* a `sometimes`. See example below.
|
||||||
|
*
|
||||||
|
* @name lfo
|
||||||
|
* @param {Object} config LFO configuration.
|
||||||
|
* @param {string | Pattern} [config.control] Node to modulate. Aliases: c
|
||||||
|
* @param {string | Pattern} [config.subControl] Sub-control name to append to the control key. Aliases: sc
|
||||||
|
* @param {number | Pattern} [config.rate] Modulation rate. Aliases: r
|
||||||
|
* @param {number | Pattern} [config.depth] Relative modulation depth. Aliases: dep, dr
|
||||||
|
* @param {number | Pattern} [config.depthabs] Absolute modulation depth. Aliases: da
|
||||||
|
* @param {number | Pattern} [config.dcoffset] DC offset / bias for the waveform. Aliases: dc
|
||||||
|
* @param {number | Pattern} [config.shape] Shape index. Aliases: sh
|
||||||
|
* @param {number | Pattern} [config.skew] Skew amount. Aliases: sk
|
||||||
|
* @param {number | Pattern} [config.curve] Exponential curve amount. Aliases: cu
|
||||||
|
* @param {number | Pattern} [config.sync] Tempo-synced modulation rate. Aliases: s
|
||||||
|
* @param {number | Pattern} [config.fxi] FX index to target
|
||||||
|
* @param {string | Pattern} id ID to use for this modulator
|
||||||
|
* @returns Pattern
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* s("saw").note("F1").lpf(500).lfo()
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* s("saw").lfo().lpf(500).lfo({ s: 0.3 })
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* s("saw").lpf(500).diode(0.3)
|
||||||
|
* .lfo({ c: "lpf" })
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* s("pulse").lpf(500).lfo()
|
||||||
|
* .lfo({ c: "s" })
|
||||||
|
* .diode(0.3)
|
||||||
|
* .sometimes(x => x.lfo({ s: "8" }, 1)) // lfo #1 (0-indexed)
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* s("pulse").lpf(500).lfo({ depth: 4 }, 'lpf_mod')
|
||||||
|
* .lfo({ c: "s" })
|
||||||
|
* .diode(0.3)
|
||||||
|
* .sometimes(x => x.lfo({ s: "8" }, 'lpf_mod'))
|
||||||
|
*/
|
||||||
|
Pattern.prototype.lfo = function (config, id) {
|
||||||
|
return this.modulate('lfo', config, id);
|
||||||
|
};
|
||||||
|
export const lfo = (config) => pure({}).lfo(config);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures an envelope. Can be called in sequence like pat.env(...).env(...) to set up multiple envelopes
|
||||||
|
* There are two ways to declare which control will be modulated:
|
||||||
|
* 1. Explicitly put `control` in the config (e.g. `env({ c: "lpf" })`)
|
||||||
|
* 2. If the control parameter is absent, the control _immediately before_ the `env` call will be used
|
||||||
|
* (e.g. `s("saw").lpf(500).env({ a: 1 })` to modulate `lpf`)
|
||||||
|
*
|
||||||
|
* Modulators can be referred to by `id` so that they can be updated later e.g. inside
|
||||||
|
* a `sometimes`. See example below.
|
||||||
|
*
|
||||||
|
* @name env
|
||||||
|
* @param {Object} config Envelope configuration.
|
||||||
|
* @param {string | Pattern} [config.control] Node to modulate. Aliases: c
|
||||||
|
* @param {string | Pattern} [config.subControl] Sub-control name to append to the control key. Aliases: sc
|
||||||
|
* @param {number | Pattern} [config.depth] Relative modulation depth. Aliases: dep, dr
|
||||||
|
* @param {number | Pattern} [config.depthabs] Absolute modulation depth. Aliases: da
|
||||||
|
* @param {number | Pattern} [config.attack] Time to reach depth. Aliases: att, a
|
||||||
|
* @param {number | Pattern} [config.decay] Time to reach sustain. Aliases: dec, d
|
||||||
|
* @param {number | Pattern} [config.sustain] Sustain depth. Aliases: sus, s
|
||||||
|
* @param {number | Pattern} [config.release] Time to return to nominal value. Aliases: rel, r
|
||||||
|
* @param {number | Pattern} [config.acurve] Snappiness of attack curve (-1 = relaxed, 1 = snappy). Aliases: ac
|
||||||
|
* @param {number | Pattern} [config.dcurve] Snappiness of decay curve (-1 = relaxed, 1 = snappy). Aliases: dc
|
||||||
|
* @param {number | Pattern} [config.rcurve] Snappiness of release curve (-1 = relaxed, 1 = snappy). Aliases: rc
|
||||||
|
* @param {number | Pattern} [config.fxi] FX index to target
|
||||||
|
* @param {string | Pattern} id ID to use for this modulator
|
||||||
|
* @returns Pattern
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* s("saw").note("F1").lpf(500).env({ a: 1 })
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* s("saw").env({ d: 1 }).note("F1")
|
||||||
|
* .lpq(4).lpf(50)
|
||||||
|
* .env({ a: 0.1, d: 1, ac: 0.8, dc: 0.3, depth: 50 })
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* s("saw").lpf(500).diode(0.3)
|
||||||
|
* .env({ c: "lpf", a: 0.5, d: 0.5 })
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* s("pulse").lpf(500).env({ a: 1 })
|
||||||
|
* .env({ c: "s", a: 1 })
|
||||||
|
* .diode(0.3)
|
||||||
|
* .sometimes(x => x.env({ a: "0.5" }, 1)) // envelope #1 (0-indexed)
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* s("pulse").lpf(500).env({ a: 1 }, 'lpf_mod')
|
||||||
|
* .env({ c: "s", a: 1 })
|
||||||
|
* .diode(0.3)
|
||||||
|
* .sometimes(x => x.env({ a: "0.5" }, 'lpf_mod'))
|
||||||
|
*/
|
||||||
|
Pattern.prototype.env = function (config, id) {
|
||||||
|
return this.modulate('env', config, id);
|
||||||
|
};
|
||||||
|
export const env = (config) => pure({}).env(config);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modulates with the output from a given `bus`.
|
||||||
|
* Can be called in sequence like pat.bmod(...).bmod(...) to set up multiple modulators
|
||||||
|
*
|
||||||
|
* Send to an audio bus with `otherPat.bus(..)`.
|
||||||
|
*
|
||||||
|
* There are two ways to declare which control will be modulated:
|
||||||
|
* 1. Explicitly put `control` in the config (e.g. `bmod({ id: 2, c: "lpf" })`)
|
||||||
|
* 2. If the control parameter is absent, the control _immediately before_ the `bmod` call will be used
|
||||||
|
* (e.g. `s("saw").lpf(500).bmod({ id: 2 })` to modulate `lpf`)
|
||||||
|
*
|
||||||
|
* Modulators can be referred to by `id` so that they can be updated later e.g. inside
|
||||||
|
* a `sometimes`. See example below.
|
||||||
|
*
|
||||||
|
* @name bmod
|
||||||
|
* @param {Object} config Bus modulation configuration.
|
||||||
|
* @param {string | Pattern} [config.bus] Bus to get modulation signal from
|
||||||
|
* @param {string | Pattern} [config.control] Node to modulate. Aliases: c
|
||||||
|
* @param {string | Pattern} [config.subControl] Sub-control name to append to the control key. Aliases: sc
|
||||||
|
* @param {number | Pattern} [config.depth] Relative modulation depth. Aliases: dep, dr
|
||||||
|
* @param {number | Pattern} [config.depthabs] Absolute modulation depth. Aliases: da
|
||||||
|
* @param {number | Pattern} [config.dc] DC offset prior to application
|
||||||
|
* @param {number | Pattern} [config.fxi] FX index to target
|
||||||
|
* @param {string | Pattern} id ID to use for this modulator
|
||||||
|
* @returns Pattern
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* modulator: s("one").seg(64).gain(slider(0, 0, 1)).bus(1).dry(0)
|
||||||
|
* carrier: s("saw").bmod({ b: 1 })
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
Pattern.prototype.bmod = function (config, id) {
|
||||||
|
return this.modulate('bmod', config, id);
|
||||||
|
};
|
||||||
|
export const bmod = (config) => pure({}).bmod(config);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transient shaper. Gives independent control over the emphasis on transients
|
* Transient shaper. Gives independent control over the emphasis on transients
|
||||||
* and sustains
|
* and sustains
|
||||||
@@ -2805,3 +3074,5 @@ export const scrub = register(
|
|||||||
* s("hh*16").bank("tr909").transient("<-1:1 1:-1>")
|
* s("hh*16").bank("tr909").transient("<-1:1 1:-1>")
|
||||||
*/
|
*/
|
||||||
export const { transient } = registerControl(['transient', 'transsustain']);
|
export const { transient } = registerControl(['transient', 'transsustain']);
|
||||||
|
|
||||||
|
export const { FXrelease, FXrel, FXr, fxr } = registerControl('FXrelease', 'FXrel', 'FXr', 'fxr');
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
/*
|
||||||
|
stateful.mjs - File of shame for stateful, impure and otherwise illegal pattern methods
|
||||||
|
Copyright (C) 2025 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/core/index.mjs>
|
||||||
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { register, reify, Pattern } from './pattern.mjs';
|
||||||
|
|
||||||
|
let timelines = {};
|
||||||
|
|
||||||
|
export const reset_state = function () {
|
||||||
|
reset_timelines();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const reset_timelines = function () {
|
||||||
|
timelines = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
/***
|
||||||
|
* Allows you to switch a pattern between different 'timelines'. This is particularly useful when
|
||||||
|
* live coding, for example when you want to cue a pattern up to play from its start.
|
||||||
|
*
|
||||||
|
* Timelines are specified by number, so that if you had a pattern like
|
||||||
|
* `n("<0 1 2 3>").s("num").timeline(1)` playing, then changed the '1'
|
||||||
|
* to '2', it would always align '0' to the nearest cycle. You will likely want to trigger
|
||||||
|
* an evaluation a little bit before the cycle starts, to avoid missing events.
|
||||||
|
*
|
||||||
|
* After the first use, a timeline will continue with the same 'offset'. That is, if you change
|
||||||
|
* a pattern without changing its timeline number, it will stay on that timeline without resetting.
|
||||||
|
*
|
||||||
|
* Rather than incrementing a timeline to reset it, it's easier to negate it, e.g. by switching between `-2`
|
||||||
|
* and `2`. This is because when you negate a timeline it will always reset.
|
||||||
|
*
|
||||||
|
* You can also pattern the timeline if you want, to create strange resetting patterns.
|
||||||
|
* @param {number | Pattern} timeline The timeline that the pattern should play on.
|
||||||
|
* @example
|
||||||
|
* n("<0 1 2 3>(3,8)")
|
||||||
|
* .sound("num")
|
||||||
|
* // resets the timeline every two cycles, by negating the timeline.
|
||||||
|
* // in a lot of cases this will be edited by a human live coder
|
||||||
|
* // rather than patterned!
|
||||||
|
* .timeline("<2 -2>".slow(2))
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const timeline = register(
|
||||||
|
'timeline',
|
||||||
|
function (tpat, pat) {
|
||||||
|
tpat = reify(tpat);
|
||||||
|
const f = function (state) {
|
||||||
|
// Is this called from the scheduler? (rather than from e.g. the visualiser)
|
||||||
|
const scheduler = !!state.controls.cyclist;
|
||||||
|
const timehaps = tpat.query(state);
|
||||||
|
const result = [];
|
||||||
|
for (const timehap of timehaps) {
|
||||||
|
const tlid = timehap.value;
|
||||||
|
let offset;
|
||||||
|
if (tlid === 0) {
|
||||||
|
offset = 0;
|
||||||
|
} else if (tlid in timelines) {
|
||||||
|
offset = timelines[tlid];
|
||||||
|
} else {
|
||||||
|
const timearc = timehap.wholeOrPart();
|
||||||
|
if (!scheduler || state.span.begin.lt(timearc.midpoint())) {
|
||||||
|
offset = timearc.begin;
|
||||||
|
} else {
|
||||||
|
// Sync to end of timearc if we first see it over halfway into its
|
||||||
|
// timespan. Allows 'cuing up' next timeline when live coding.
|
||||||
|
offset = timearc.end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (scheduler) {
|
||||||
|
// update state
|
||||||
|
timelines[tlid] = offset;
|
||||||
|
if (tlid !== 0) {
|
||||||
|
delete timelines[-tlid];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const pathaps = pat
|
||||||
|
.late(offset)
|
||||||
|
.query(state.setSpan(timehap.part))
|
||||||
|
.map((h) => h.setContext(h.combineContext(timehap)));
|
||||||
|
result.push(...pathaps);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
return new Pattern(f, pat._steps);
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
);
|
||||||
+12
-11
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
index.mjs - <short description TODO>
|
index.mjs - <short description TODO>
|
||||||
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/core/index.mjs>
|
Copyright (C) 2025 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/core/index.mjs>
|
||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -11,20 +11,21 @@ import createClock from './zyklus.mjs';
|
|||||||
import { logger } from './logger.mjs';
|
import { logger } from './logger.mjs';
|
||||||
export { Fraction, controls, createClock };
|
export { Fraction, controls, createClock };
|
||||||
export * from './controls.mjs';
|
export * from './controls.mjs';
|
||||||
export * from './hap.mjs';
|
|
||||||
export * from './pattern.mjs';
|
|
||||||
export * from './signal.mjs';
|
|
||||||
export * from './pick.mjs';
|
|
||||||
export * from './state.mjs';
|
|
||||||
export * from './timespan.mjs';
|
|
||||||
export * from './util.mjs';
|
|
||||||
export * from './speak.mjs';
|
|
||||||
export * from './evaluate.mjs';
|
|
||||||
export * from './repl.mjs';
|
|
||||||
export * from './cyclist.mjs';
|
export * from './cyclist.mjs';
|
||||||
|
export * from './evaluate.mjs';
|
||||||
|
export * from './hap.mjs';
|
||||||
|
export * from './impure.mjs';
|
||||||
export * from './logger.mjs';
|
export * from './logger.mjs';
|
||||||
|
export * from './pattern.mjs';
|
||||||
|
export * from './pick.mjs';
|
||||||
|
export * from './repl.mjs';
|
||||||
|
export * from './signal.mjs';
|
||||||
|
export * from './speak.mjs';
|
||||||
|
export * from './state.mjs';
|
||||||
export * from './time.mjs';
|
export * from './time.mjs';
|
||||||
|
export * from './timespan.mjs';
|
||||||
export * from './ui.mjs';
|
export * from './ui.mjs';
|
||||||
|
export * from './util.mjs';
|
||||||
export { default as drawLine } from './drawLine.mjs';
|
export { default as drawLine } from './drawLine.mjs';
|
||||||
// below won't work with runtime.mjs (json import fails)
|
// below won't work with runtime.mjs (json import fails)
|
||||||
/* import * as p from './package.json';
|
/* import * as p from './package.json';
|
||||||
|
|||||||
@@ -3586,6 +3586,20 @@ export const morph = (frompat, topat, bypat) => {
|
|||||||
return frompat.innerBind((from) => topat.innerBind((to) => bypat.innerBind((by) => _morph(from, to, by))));
|
return frompat.innerBind((from) => topat.innerBind((to) => bypat.innerBind((by) => _morph(from, to, by))));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const _distortWithAlg = function (name) {
|
||||||
|
const func = function (args, pat) {
|
||||||
|
const argsPat = reify(args).fmap((v) => (Array.isArray(v) ? [...v, name] : [v, 1, name]));
|
||||||
|
if (!pat) {
|
||||||
|
return pure({}).distort(argsPat);
|
||||||
|
}
|
||||||
|
return pat.distort(argsPat);
|
||||||
|
};
|
||||||
|
Pattern.prototype[name] = function (args) {
|
||||||
|
return func(args, this);
|
||||||
|
};
|
||||||
|
return func;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Soft-clipping distortion
|
* Soft-clipping distortion
|
||||||
*
|
*
|
||||||
@@ -3594,6 +3608,8 @@ export const morph = (frompat, topat, bypat) => {
|
|||||||
* @param {number | Pattern} volume linear postgain of the distortion
|
* @param {number | Pattern} volume linear postgain of the distortion
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
export const soft = _distortWithAlg('soft');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hard-clipping distortion
|
* Hard-clipping distortion
|
||||||
*
|
*
|
||||||
@@ -3602,6 +3618,8 @@ export const morph = (frompat, topat, bypat) => {
|
|||||||
* @param {number | Pattern} volume linear postgain of the distortion
|
* @param {number | Pattern} volume linear postgain of the distortion
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
export const hard = _distortWithAlg('hard');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cubic polynomial distortion
|
* Cubic polynomial distortion
|
||||||
*
|
*
|
||||||
@@ -3610,6 +3628,8 @@ export const morph = (frompat, topat, bypat) => {
|
|||||||
* @param {number | Pattern} volume linear postgain of the distortion
|
* @param {number | Pattern} volume linear postgain of the distortion
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
export const cubic = _distortWithAlg('cubic');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Diode-emulating distortion
|
* Diode-emulating distortion
|
||||||
*
|
*
|
||||||
@@ -3618,6 +3638,8 @@ export const morph = (frompat, topat, bypat) => {
|
|||||||
* @param {number | Pattern} volume linear postgain of the distortion
|
* @param {number | Pattern} volume linear postgain of the distortion
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
export const diode = _distortWithAlg('diode');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Asymmetrical diode distortion
|
* Asymmetrical diode distortion
|
||||||
*
|
*
|
||||||
@@ -3626,6 +3648,8 @@ export const morph = (frompat, topat, bypat) => {
|
|||||||
* @param {number | Pattern} volume linear postgain of the distortion
|
* @param {number | Pattern} volume linear postgain of the distortion
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
export const asym = _distortWithAlg('asym');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wavefolding distortion
|
* Wavefolding distortion
|
||||||
*
|
*
|
||||||
@@ -3634,6 +3658,8 @@ export const morph = (frompat, topat, bypat) => {
|
|||||||
* @param {number | Pattern} volume linear postgain of the distortion
|
* @param {number | Pattern} volume linear postgain of the distortion
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
export const fold = _distortWithAlg('fold');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wavefolding distortion composed with sinusoid
|
* Wavefolding distortion composed with sinusoid
|
||||||
*
|
*
|
||||||
@@ -3642,6 +3668,8 @@ export const morph = (frompat, topat, bypat) => {
|
|||||||
* @param {number | Pattern} volume linear postgain of the distortion
|
* @param {number | Pattern} volume linear postgain of the distortion
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
export const sinefold = _distortWithAlg('sinefold');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Distortion via Chebyshev polynomials
|
* Distortion via Chebyshev polynomials
|
||||||
*
|
*
|
||||||
@@ -3650,14 +3678,7 @@ export const morph = (frompat, topat, bypat) => {
|
|||||||
* @param {number | Pattern} volume linear postgain of the distortion
|
* @param {number | Pattern} volume linear postgain of the distortion
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
const distAlgoNames = ['scurve', 'soft', 'hard', 'cubic', 'diode', 'asym', 'fold', 'sinefold', 'chebyshev'];
|
export const chebyshev = _distortWithAlg('chebyshev');
|
||||||
for (const name of distAlgoNames) {
|
|
||||||
// Add aliases for distortion algorithms
|
|
||||||
Pattern.prototype[name] = function (args) {
|
|
||||||
const argsPat = reify(args).fmap((v) => (Array.isArray(v) ? [...v, name] : [v, 1, name]));
|
|
||||||
return this.distort(argsPat);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Turns a list of patterns into a single pattern which outputs list-values
|
* Turns a list of patterns into a single pattern which outputs list-values
|
||||||
@@ -3721,3 +3742,35 @@ Pattern.prototype.phases = function (list) {
|
|||||||
export const phases = (list) => {
|
export const phases = (list) => {
|
||||||
return _ensureListPattern(list).as('phases');
|
return _ensureListPattern(list).as('phases');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Establishes an FX chain. Can be called by chaining .FX(fx1).FX(fx2)..
|
||||||
|
* calls and/or in a single .FX(fx1, fx2, ..) call. The fx1, .. are _patterns_ which
|
||||||
|
* establish the controls of the given effect. See examples.
|
||||||
|
* @name FX
|
||||||
|
* @memberof Pattern
|
||||||
|
* @returns Pattern
|
||||||
|
* @example
|
||||||
|
* $: s("[sbd <hh [bd | lt | oh]>]*4").dec(.4)
|
||||||
|
* .FX(
|
||||||
|
* phaser(0.5).gain(2),
|
||||||
|
* bpf(800),
|
||||||
|
* distort(1.3),
|
||||||
|
* room(0.2),
|
||||||
|
* delay(0.5).gain(1.25),
|
||||||
|
* distort(0.3),
|
||||||
|
* ).fxr(1.7) // sets release time of effects (like delay)
|
||||||
|
* @example
|
||||||
|
* $: s("saw").fm(0.5)
|
||||||
|
* .delay(0.3) // outer effects are applied *last*
|
||||||
|
* .FX(coarse(4)) // first coarse
|
||||||
|
* .FX(lpf(500).lpe(4).lpa(1).lpd(2)) // then lpf
|
||||||
|
* .FX(distort(1)) // then distort
|
||||||
|
*/
|
||||||
|
Pattern.prototype.FX = function (...effects) {
|
||||||
|
effects = effects.map(reify);
|
||||||
|
return this.withValue((v) => (vEff) => {
|
||||||
|
const currFX = v.FX ?? [];
|
||||||
|
return { ...v, FX: currFX.concat(vEff) };
|
||||||
|
}).appLeft(parray(effects));
|
||||||
|
};
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { errorLogger, logger } from './logger.mjs';
|
|||||||
import { setTime } from './time.mjs';
|
import { setTime } from './time.mjs';
|
||||||
import { evalScope } from './evaluate.mjs';
|
import { evalScope } from './evaluate.mjs';
|
||||||
import { register, Pattern, isPattern, silence, stack } from './pattern.mjs';
|
import { register, Pattern, isPattern, silence, stack } from './pattern.mjs';
|
||||||
|
import { reset_state } from './impure.mjs';
|
||||||
|
|
||||||
export function repl({
|
export function repl({
|
||||||
defaultOutput,
|
defaultOutput,
|
||||||
@@ -52,6 +53,9 @@ export function repl({
|
|||||||
onToggle: (started) => {
|
onToggle: (started) => {
|
||||||
updateState({ started });
|
updateState({ started });
|
||||||
onToggle?.(started);
|
onToggle?.(started);
|
||||||
|
if (!started) {
|
||||||
|
reset_state();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
setInterval,
|
setInterval,
|
||||||
clearInterval,
|
clearInterval,
|
||||||
|
|||||||
+171
-38
@@ -16,7 +16,7 @@ export function steady(value) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const signal = (func) => {
|
export const signal = (func) => {
|
||||||
const query = (state) => [new Hap(undefined, state.span, func(state.span.begin))];
|
const query = (state) => [new Hap(undefined, state.span, func(state.span.begin, state.controls))];
|
||||||
return new Pattern(query);
|
return new Pattern(query);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -186,38 +186,97 @@ export const mouseY = signal(() => _mouseY);
|
|||||||
export const mousex = signal(() => _mouseX);
|
export const mousex = signal(() => _mouseX);
|
||||||
export const mouseX = signal(() => _mouseX);
|
export const mouseX = signal(() => _mouseX);
|
||||||
|
|
||||||
// random signals
|
// Random number generators
|
||||||
|
|
||||||
const xorwise = (x) => {
|
// Produce "Avalanche effect" where flipping a single bit of x
|
||||||
|
// results in all output bits flipping with probability 0.5
|
||||||
|
// See e.g. https://github.com/aappleby/smhasher/blob/0ff96f7835817a27d0487325b6c16033e2992eb5/src/MurmurHash3.cpp#L68-L77
|
||||||
|
const _murmurHashFinalizer = (x) => {
|
||||||
|
x |= 0;
|
||||||
|
x ^= x >>> 16;
|
||||||
|
x = Math.imul(x, 0x85ebca6b);
|
||||||
|
x ^= x >>> 13;
|
||||||
|
x = Math.imul(x, 0xc2b2ae35);
|
||||||
|
x ^= x >>> 16;
|
||||||
|
return x >>> 0; // unsigned
|
||||||
|
};
|
||||||
|
|
||||||
|
// Convert t to a 32 bit integer, preserving temporal resolution down to 1/2^29
|
||||||
|
const _tToT = (t) => {
|
||||||
|
return Math.floor(t * 536870912);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Used to decorrelate nearby T, i, and seed prior to hashing
|
||||||
|
const _decorrelate = (T, i = 0, seed = 0) => {
|
||||||
|
const lowBits = (T >>> 0) >>> 0;
|
||||||
|
const highBits = Math.floor(T / 4294967296) >>> 0; // 2^32
|
||||||
|
let key = lowBits ^ Math.imul(highBits ^ 0x85ebca6b, 0xc2b2ae35);
|
||||||
|
key ^= Math.imul(i ^ 0x7f4a7c15, 0x9e3779b9);
|
||||||
|
key ^= Math.imul(seed ^ 0x165667b1, 0x27d4eb2d);
|
||||||
|
return key >>> 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const randAt = (T, i = 0, seed = 0) => {
|
||||||
|
return _murmurHashFinalizer(_decorrelate(T, i, seed)) / 4294967296; // 2^32
|
||||||
|
};
|
||||||
|
|
||||||
|
// n samples at time t
|
||||||
|
const timeToRands = (t, n, seed = 0) => {
|
||||||
|
const T = _tToT(t);
|
||||||
|
if (n === 1) {
|
||||||
|
return randAt(T, 0, seed);
|
||||||
|
}
|
||||||
|
const out = new Array(n);
|
||||||
|
for (let i = 0; i < n; i++) out[i] = randAt(T, i, seed);
|
||||||
|
return out;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Old random signals. Currently the default, but can also be chosen via
|
||||||
|
// `useRNG('legacy')`
|
||||||
|
|
||||||
|
// stretch 300 cycles over the range of [0,2**29 == 536870912) then apply the xorshift algorithm
|
||||||
|
const __xorwise = (x) => {
|
||||||
const a = (x << 13) ^ x;
|
const a = (x << 13) ^ x;
|
||||||
const b = (a >> 17) ^ a;
|
const b = (a >> 17) ^ a;
|
||||||
return (b << 5) ^ b;
|
return (b << 5) ^ b;
|
||||||
};
|
};
|
||||||
|
const __frac = (x) => x - Math.trunc(x);
|
||||||
// stretch 300 cycles over the range of [0,2**29 == 536870912) then apply the xorshift algorithm
|
const __timeToIntSeed = (x) => __xorwise(Math.trunc(__frac(x / 300) * 536870912));
|
||||||
const _frac = (x) => x - Math.trunc(x);
|
const __intSeedToRand = (x) => (x % 536870912) / 536870912;
|
||||||
|
const __timeToRandsPrime = (seed, n) => {
|
||||||
const timeToIntSeed = (x) => xorwise(Math.trunc(_frac(x / 300) * 536870912));
|
if (n === 1) {
|
||||||
|
return Math.abs(__intSeedToRand(seed));
|
||||||
const intSeedToRand = (x) => (x % 536870912) / 536870912;
|
}
|
||||||
|
|
||||||
const timeToRand = (x) => Math.abs(intSeedToRand(timeToIntSeed(x)));
|
|
||||||
|
|
||||||
const timeToRandsPrime = (seed, n) => {
|
|
||||||
const result = [];
|
const result = [];
|
||||||
// eslint-disable-next-line
|
for (let i = 0; i < n; i++) {
|
||||||
for (let i = 0; i < n; ++i) {
|
result.push(__intSeedToRand(seed));
|
||||||
result.push(intSeedToRand(seed));
|
seed = __xorwise(seed);
|
||||||
seed = xorwise(seed);
|
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
const __timeToRands = (t, n) => __timeToRandsPrime(__timeToIntSeed(t), n);
|
||||||
|
|
||||||
const timeToRands = (t, n) => timeToRandsPrime(timeToIntSeed(t), n);
|
// End old random
|
||||||
|
|
||||||
|
let RNG_MODE = 'legacy';
|
||||||
|
export const getRandsAtTime = (t, n = 1, seed = 0) => {
|
||||||
|
return RNG_MODE === 'legacy' ? __timeToRands(t + seed, n) : timeToRands(t, n, seed);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Sets which random number generator to use. Historically Strudel would
|
||||||
|
* use `useRNG('legacy')`, which remains the default. To use a new more statistically
|
||||||
|
* precise RNG, try `useRNG('precise')`.
|
||||||
*
|
*
|
||||||
|
* @name useRNG
|
||||||
|
* @param {string} mod - Mode. One of 'legacy', 'precise'
|
||||||
|
* @example
|
||||||
|
* useRNG('legacy')
|
||||||
|
* // Repeats every 300 cycles
|
||||||
|
* $: n(irand(50)).seg(16).scale("C:minor").ribbon(88, 32)
|
||||||
|
* $: n(irand(50)).seg(16).scale("C:minor").ribbon(388, 32)
|
||||||
*/
|
*/
|
||||||
|
export const useRNG = (mode = 'legacy') => (RNG_MODE = mode);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A discrete pattern of numbers from 0 to n-1
|
* A discrete pattern of numbers from 0 to n-1
|
||||||
@@ -300,13 +359,13 @@ export const binaryNL = (n, nBits = 16) => {
|
|||||||
* .partials(randL(8))
|
* .partials(randL(8))
|
||||||
*/
|
*/
|
||||||
export const randL = (n) => {
|
export const randL = (n) => {
|
||||||
return signal((t) => (nVal) => timeToRands(t, nVal).map(Math.abs)).appLeft(reify(n));
|
return signal((t) => (nVal) => getRandsAtTime(t, nVal).map(Math.abs)).appLeft(reify(n));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const randrun = (n) => {
|
export const randrun = (n) => {
|
||||||
return signal((t) => {
|
return signal((t, controls) => {
|
||||||
// Without adding 0.5, the first cycle is always 0,1,2,3,...
|
// Without adding 0.5, the first cycle is always 0,1,2,3,...
|
||||||
const rands = timeToRands(t.floor().add(0.5), n);
|
const rands = getRandsAtTime(t.floor().add(0.5), n, controls.randSeed);
|
||||||
const nums = rands
|
const nums = rands
|
||||||
.map((n, i) => [n, i])
|
.map((n, i) => [n, i])
|
||||||
.sort((a, b) => (a[0] > b[0]) - (a[0] < b[0]))
|
.sort((a, b) => (a[0] > b[0]) - (a[0] < b[0]))
|
||||||
@@ -347,6 +406,37 @@ export const scramble = register('scramble', (n, pat) => {
|
|||||||
return _rearrangeWith(_irand(n)._segment(n), n, pat);
|
return _rearrangeWith(_irand(n)._segment(n), n, pat);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modify a pattern by applying a function to the `randomSeed` control if present
|
||||||
|
*
|
||||||
|
* @param {Function} func Function from seed (or undefined) to seed (or undefined)
|
||||||
|
* @param {Pattern} pat Pattern to update
|
||||||
|
* @returns Pattern
|
||||||
|
*/
|
||||||
|
export const withSeed = (func, pat) => {
|
||||||
|
return new Pattern((state) => {
|
||||||
|
let { randSeed, ...controls } = state.controls;
|
||||||
|
randSeed = func(randSeed);
|
||||||
|
return pat.query(state.setControls({ ...controls, randSeed }));
|
||||||
|
}, pat._steps);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change the seed for random signals. Normally, random signals depend on time,
|
||||||
|
* so two patterns at the same time will have the same random values. Specifying
|
||||||
|
* a new seed changes the signal output by `rand`. This also affects other functions
|
||||||
|
* that use randomness, like `shuffle` and `sometimes`.
|
||||||
|
*
|
||||||
|
* @name seed
|
||||||
|
* @param {number} n A new seed. Can be any number.
|
||||||
|
* @example
|
||||||
|
* $: s("hh*4").degrade();
|
||||||
|
* $: s("bd*4").degrade().seed(1); // Will degrade different events from the hi-hat
|
||||||
|
*/
|
||||||
|
export const seed = register('seed', (n, pat) => {
|
||||||
|
return withSeed(() => n, pat);
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A continuous pattern of random numbers, between 0 and 1.
|
* A continuous pattern of random numbers, between 0 and 1.
|
||||||
*
|
*
|
||||||
@@ -356,7 +446,7 @@ export const scramble = register('scramble', (n, pat) => {
|
|||||||
* s("bd*4,hh*8").cutoff(rand.range(500,8000))
|
* s("bd*4,hh*8").cutoff(rand.range(500,8000))
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export const rand = signal(timeToRand);
|
export const rand = signal((t, controls) => getRandsAtTime(t, 1, controls.randSeed));
|
||||||
/**
|
/**
|
||||||
* A continuous pattern of random numbers, between -1 and 1
|
* A continuous pattern of random numbers, between -1 and 1
|
||||||
*/
|
*/
|
||||||
@@ -533,36 +623,32 @@ export const wchooseCycles = (...pairs) => _wchooseWith(rand.segment(1), ...pair
|
|||||||
|
|
||||||
export const wrandcat = wchooseCycles;
|
export const wrandcat = wchooseCycles;
|
||||||
|
|
||||||
function _perlin(t) {
|
function _perlin(t, seed = 0) {
|
||||||
let ta = Math.floor(t);
|
let ta = Math.floor(t);
|
||||||
let tb = ta + 1;
|
let tb = ta + 1;
|
||||||
const smootherStep = (x) => 6.0 * x ** 5 - 15.0 * x ** 4 + 10.0 * x ** 3;
|
const smootherStep = (x) => 6.0 * x ** 5 - 15.0 * x ** 4 + 10.0 * x ** 3;
|
||||||
const interp = (x) => (a) => (b) => a + smootherStep(x) * (b - a);
|
const interp = (x) => (a) => (b) => a + smootherStep(x) * (b - a);
|
||||||
const v = interp(t - ta)(timeToRand(ta))(timeToRand(tb));
|
const ra = getRandsAtTime(ta, 1, seed);
|
||||||
|
const rb = getRandsAtTime(tb, 1, seed);
|
||||||
|
const v = interp(t - ta)(ra)(rb);
|
||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
export const perlinWith = (tpat) => {
|
|
||||||
return tpat.fmap(_perlin);
|
|
||||||
};
|
|
||||||
|
|
||||||
function _berlin(t) {
|
function _berlin(t, seed = 0) {
|
||||||
const prevRidgeStartIndex = Math.floor(t);
|
const prevRidgeStartIndex = Math.floor(t);
|
||||||
const nextRidgeStartIndex = prevRidgeStartIndex + 1;
|
const nextRidgeStartIndex = prevRidgeStartIndex + 1;
|
||||||
|
|
||||||
const prevRidgeBottomPoint = timeToRand(prevRidgeStartIndex);
|
const prevRidgeBottomPoint = getRandsAtTime(prevRidgeStartIndex, 1, seed);
|
||||||
const nextRidgeTopPoint = timeToRand(nextRidgeStartIndex) + prevRidgeBottomPoint;
|
const height = getRandsAtTime(nextRidgeStartIndex, 1, seed);
|
||||||
|
const nextRidgeTopPoint = prevRidgeBottomPoint + height;
|
||||||
|
|
||||||
const currentPercent = (t - prevRidgeStartIndex) / (nextRidgeStartIndex - prevRidgeStartIndex);
|
const currentPercent = (t - prevRidgeStartIndex) / (nextRidgeStartIndex - prevRidgeStartIndex);
|
||||||
const interp = (a, b, t) => {
|
const interp = (a, b, t) => {
|
||||||
return a + (b - a) * t;
|
return a + t * (b - a);
|
||||||
};
|
};
|
||||||
return interp(prevRidgeBottomPoint, nextRidgeTopPoint, currentPercent) / 2;
|
return interp(prevRidgeBottomPoint, nextRidgeTopPoint, currentPercent) / 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const berlinWith = (tpat) => {
|
|
||||||
return tpat.fmap(_berlin);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a continuous pattern of [perlin noise](https://en.wikipedia.org/wiki/Perlin_noise), in the range 0..1.
|
* Generates a continuous pattern of [perlin noise](https://en.wikipedia.org/wiki/Perlin_noise), in the range 0..1.
|
||||||
*
|
*
|
||||||
@@ -572,7 +658,7 @@ export const berlinWith = (tpat) => {
|
|||||||
* s("bd*4,hh*8").cutoff(perlin.range(500,8000))
|
* s("bd*4,hh*8").cutoff(perlin.range(500,8000))
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export const perlin = perlinWith(time.fmap((v) => Number(v)));
|
export const perlin = signal((t, controls) => _perlin(t, controls.randSeed));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a continuous pattern of [berlin noise](conceived by Jame Coyne and Jade Rowland as a joke but turned out to be surprisingly cool and useful,
|
* Generates a continuous pattern of [berlin noise](conceived by Jame Coyne and Jade Rowland as a joke but turned out to be surprisingly cool and useful,
|
||||||
@@ -584,7 +670,7 @@ export const perlin = perlinWith(time.fmap((v) => Number(v)));
|
|||||||
* n("0!16".add(berlin.fast(4).mul(14))).scale("d:minor")
|
* n("0!16".add(berlin.fast(4).mul(14))).scale("d:minor")
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export const berlin = berlinWith(time.fmap((v) => Number(v)));
|
export const berlin = signal((t, controls) => _berlin(t, controls.randSeed));
|
||||||
|
|
||||||
export const degradeByWith = register(
|
export const degradeByWith = register(
|
||||||
'degradeByWith',
|
'degradeByWith',
|
||||||
@@ -882,3 +968,50 @@ export const whenKey = register('whenKey', function (input, func, pat) {
|
|||||||
export const keyDown = register('keyDown', function (pat) {
|
export const keyDown = register('keyDown', function (pat) {
|
||||||
return pat.fmap(_keyDown);
|
return pat.fmap(_keyDown);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A pattern measuring the duration of events,
|
||||||
|
* in cycles per event. `cyclesPer` doesn't have structure itself, but takes structure, and therefore
|
||||||
|
* event durations, from the pattern that it is combined with.
|
||||||
|
* For example `cyclesPer.struct("1 1 [1 1] 1")` would give the same as `"0.25 0.25 [0.125 0.125] 0.25"`.
|
||||||
|
* See also its reciprocal, `per`, also known as `perCycle`.
|
||||||
|
* @example
|
||||||
|
* // Shorter events are lower in pitch
|
||||||
|
* sound("saw saw [saw saw] saw")
|
||||||
|
* .note(cyclesPer.range(50, 100))
|
||||||
|
* @example
|
||||||
|
* sound("bd sd [bd bd] sd*4 [- sd] [bd [bd bd]]")
|
||||||
|
* .note(cyclesPer.add(20))
|
||||||
|
*/
|
||||||
|
export const cyclesPer = new Pattern(function (state) {
|
||||||
|
return [new Hap(undefined, state.span, state.span.duration)];
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A pattern measuring the 'shortness' of events, or in other words, the duration of pattern events,
|
||||||
|
* in events per cycle. `per` doesn't have structure itself, but takes structure, and therefore
|
||||||
|
* event durations, from the pattern that it is combined with.
|
||||||
|
* For example `per.struct("1 1 [1 1] 1")` would give the same as `"4 4 [8 8] 4"`.
|
||||||
|
* See also its reciprocal, `cyclesPer`.
|
||||||
|
* @synonyms perCycle
|
||||||
|
* @example
|
||||||
|
* // Shorter events are more distorted
|
||||||
|
* n("0 0*2 0 0*2 0 [0 0 0]@2").sound("bd")
|
||||||
|
* .distort(per.div(2))
|
||||||
|
*/
|
||||||
|
export const per = new Pattern(function (state) {
|
||||||
|
return [new Hap(undefined, state.span, Fraction(1).div(state.span.duration))];
|
||||||
|
});
|
||||||
|
|
||||||
|
export const perCycle = per;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Like `per` but measures the shortness of events according to an exponential curve. In
|
||||||
|
* particular, where the event duration halves, the
|
||||||
|
* returned value increases by one. `perx.struct("1 1 [1 [1 1]] 1")` would therefore be
|
||||||
|
* the same as `"3 3 [4 [5 5]] 3"`.
|
||||||
|
*/
|
||||||
|
export const perx = new Pattern(function (state) {
|
||||||
|
const n = Fraction(1).div(state.span.duration);
|
||||||
|
return [new Hap(undefined, state.span, Math.log(n) / Math.log(2) + 1)];
|
||||||
|
});
|
||||||
|
|||||||
@@ -740,20 +740,6 @@ describe('Pattern', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('signal()', () => {
|
|
||||||
it('Can make saw/saw2', () => {
|
|
||||||
expect(saw.struct(true, true, true, true).firstCycle()).toStrictEqual(
|
|
||||||
sequence(0, 1 / 4, 1 / 2, 3 / 4).firstCycle(),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(saw2.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(-1, -0.5, 0, 0.5).firstCycle());
|
|
||||||
});
|
|
||||||
it('Can make isaw/isaw2', () => {
|
|
||||||
expect(isaw.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(1, 0.75, 0.5, 0.25).firstCycle());
|
|
||||||
|
|
||||||
expect(isaw2.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(1, 0.5, 0, -0.5).firstCycle());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('_setContext()', () => {
|
describe('_setContext()', () => {
|
||||||
it('Can set the hap context', () => {
|
it('Can set the hap context', () => {
|
||||||
expect(
|
expect(
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
/*
|
||||||
|
signal.test.mjs - <short description TODO>
|
||||||
|
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/core/test/pattern.test.mjs>
|
||||||
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Fraction from 'fraction.js';
|
||||||
|
|
||||||
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
|
|
||||||
|
import { saw, saw2, isaw, isaw2, per, perx, cyclesPer } from '../signal.mjs';
|
||||||
|
import { fastcat, sequence, State, TimeSpan, Hap } from '../index.mjs';
|
||||||
|
|
||||||
|
const st = (begin, end) => new State(ts(begin, end));
|
||||||
|
const ts = (begin, end) => new TimeSpan(Fraction(begin), Fraction(end));
|
||||||
|
const hap = (whole, part, value, context = {}) => new Hap(whole, part, value, context);
|
||||||
|
|
||||||
|
const third = Fraction(1, 3);
|
||||||
|
const twothirds = Fraction(2, 3);
|
||||||
|
|
||||||
|
const sameFirst = (a, b) => {
|
||||||
|
return expect(a.sortHapsByPart().firstCycle()).toStrictEqual(b.sortHapsByPart().firstCycle());
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('signal()', () => {
|
||||||
|
it('Can make saw/saw2', () => {
|
||||||
|
expect(saw.struct(true, true, true, true).firstCycle()).toStrictEqual(
|
||||||
|
sequence(0, 1 / 4, 1 / 2, 3 / 4).firstCycle(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(saw2.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(-1, -0.5, 0, 0.5).firstCycle());
|
||||||
|
});
|
||||||
|
it('Can make isaw/isaw2', () => {
|
||||||
|
expect(isaw.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(1, 0.75, 0.5, 0.25).firstCycle());
|
||||||
|
|
||||||
|
expect(isaw2.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(1, 0.5, 0, -0.5).firstCycle());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('cyclesPer', () => {
|
||||||
|
it('gives cycles per hap', () => {
|
||||||
|
sameFirst(
|
||||||
|
cyclesPer.struct(true, true, true, fastcat(true, true)),
|
||||||
|
sequence(0.25, 0.25, 0.25, fastcat(0.125, 0.125)).fmap(Fraction),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('per', () => {
|
||||||
|
it('gives haps per cycle', () => {
|
||||||
|
sameFirst(per.struct(true, true, true, fastcat(true, true)), sequence(4, 4, 4, fastcat(8, 8)).fmap(Fraction));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('perx', () => {
|
||||||
|
it('gives exponential haps per cycle', () => {
|
||||||
|
sameFirst(
|
||||||
|
perx.struct(true, true, true, fastcat(true, fastcat(true, true))),
|
||||||
|
sequence(3, 3, 3, fastcat(4, fastcat(5, 5))),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -46,4 +46,4 @@ all(osc)
|
|||||||
|
|
||||||
[open in repl](https://strudel.cc/#JDogcygiYmQqNCIpCgphbGwob3NjKQ%3D%3D)
|
[open in repl](https://strudel.cc/#JDogcygiYmQqNCIpCgphbGwob3NjKQ%3D%3D)
|
||||||
|
|
||||||
You can read more about [how to use Superdirt with Strudel the Tutorial](https://strudel.cc/learn/input-output/#superdirt-api)
|
You can read more about [how to use Superdirt with Strudel](https://strudel.cc/learn/input-output/#oscsuperdirtstrudeldirt) in the tutorial.
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ export function registerSoundfonts() {
|
|||||||
let envEnd = holdEnd + release + 0.01;
|
let envEnd = holdEnd + release + 0.01;
|
||||||
|
|
||||||
// vibrato
|
// vibrato
|
||||||
let vibratoOscillator = getVibratoOscillator(bufferSource.detune, value, time);
|
const vibratoHandle = getVibratoOscillator(bufferSource.detune, value, time);
|
||||||
// pitch envelope
|
// pitch envelope
|
||||||
getPitchEnvelope(bufferSource.detune, value, time, holdEnd);
|
getPitchEnvelope(bufferSource.detune, value, time, holdEnd);
|
||||||
|
|
||||||
@@ -174,10 +174,10 @@ export function registerSoundfonts() {
|
|||||||
const stop = (releaseTime) => {};
|
const stop = (releaseTime) => {};
|
||||||
onceEnded(bufferSource, () => {
|
onceEnded(bufferSource, () => {
|
||||||
releaseAudioNode(bufferSource);
|
releaseAudioNode(bufferSource);
|
||||||
releaseAudioNode(vibratoOscillator);
|
vibratoHandle?.stop();
|
||||||
onended();
|
onended();
|
||||||
});
|
});
|
||||||
return { node, stop };
|
return { node, stop, nodes: { source: [bufferSource], ...vibratoHandle?.nodes } };
|
||||||
},
|
},
|
||||||
{ type: 'soundfont', prebake: true, fonts },
|
{ type: 'soundfont', prebake: true, fonts },
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
/*
|
||||||
|
audioContext.mjs - Audio Context manager
|
||||||
|
|
||||||
|
Sets up a common and accessible audio context for all superdough operations
|
||||||
|
|
||||||
|
Copyright (C) 2025 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/superdough/audiocontext.mjs>
|
||||||
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
let audioContext;
|
let audioContext;
|
||||||
|
|
||||||
export const setDefaultAudioContext = () => {
|
export const setDefaultAudioContext = () => {
|
||||||
@@ -5,6 +14,11 @@ export const setDefaultAudioContext = () => {
|
|||||||
return audioContext;
|
return audioContext;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const setAudioContext = (context) => {
|
||||||
|
audioContext = context;
|
||||||
|
return audioContext;
|
||||||
|
};
|
||||||
|
|
||||||
export const getAudioContext = () => {
|
export const getAudioContext = () => {
|
||||||
if (!audioContext) {
|
if (!audioContext) {
|
||||||
return setDefaultAudioContext();
|
return setDefaultAudioContext();
|
||||||
|
|||||||
@@ -5,19 +5,18 @@ if (typeof DelayNode !== 'undefined') {
|
|||||||
wet = Math.abs(wet);
|
wet = Math.abs(wet);
|
||||||
this.delayTime.value = time;
|
this.delayTime.value = time;
|
||||||
|
|
||||||
const feedbackGain = ac.createGain();
|
this.feedbackGain = ac.createGain();
|
||||||
feedbackGain.gain.value = Math.min(Math.abs(feedback), 0.995);
|
this.feedbackGain.gain.value = Math.min(Math.abs(feedback), 0.995);
|
||||||
this.feedback = feedbackGain.gain;
|
this.feedback = this.feedbackGain.gain;
|
||||||
|
|
||||||
const delayGain = ac.createGain();
|
this.delayGain = ac.createGain();
|
||||||
delayGain.gain.value = wet;
|
this.delayGain.gain.value = wet;
|
||||||
this.delayGain = delayGain;
|
|
||||||
|
|
||||||
this.connect(feedbackGain);
|
this.connect(this.feedbackGain);
|
||||||
this.connect(delayGain);
|
this.connect(this.delayGain);
|
||||||
feedbackGain.connect(this);
|
this.feedbackGain.connect(this);
|
||||||
|
|
||||||
this.connect = (target) => delayGain.connect(target);
|
this.connect = (target) => this.delayGain.connect(target);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
start(t) {
|
start(t) {
|
||||||
@@ -25,7 +24,7 @@ if (typeof DelayNode !== 'undefined') {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AudioContext.prototype.createFeedbackDelay = function (wet, time, feedback) {
|
BaseAudioContext.prototype.createFeedbackDelay = function (wet, time, feedback) {
|
||||||
return new FeedbackDelayNode(this, wet, time, feedback);
|
return new FeedbackDelayNode(this, wet, time, feedback);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { getAudioContext } from './audioContext.mjs';
|
import { getAudioContext } from './audioContext.mjs';
|
||||||
import { logger } from './logger.mjs';
|
import { logger } from './logger.mjs';
|
||||||
import { getNoiseBuffer } from './noise.mjs';
|
import { getNoiseBuffer } from './noise.mjs';
|
||||||
|
import { getNodeFromPool } from './nodePools.mjs';
|
||||||
import { clamp, nanFallback, midiToFreq, noteToMidi } from './util.mjs';
|
import { clamp, nanFallback, midiToFreq, noteToMidi } from './util.mjs';
|
||||||
|
|
||||||
export const noises = ['pink', 'white', 'brown', 'crackle'];
|
export const noises = ['pink', 'white', 'brown', 'crackle'];
|
||||||
@@ -42,6 +43,7 @@ export const getParamADSR = (
|
|||||||
decay,
|
decay,
|
||||||
sustain,
|
sustain,
|
||||||
release,
|
release,
|
||||||
|
// min = value at start of attack, max = value at end of attack; it is possible that max < min
|
||||||
min,
|
min,
|
||||||
max,
|
max,
|
||||||
begin,
|
begin,
|
||||||
@@ -59,17 +61,15 @@ export const getParamADSR = (
|
|||||||
max = max === 0 ? 0.001 : max;
|
max = max === 0 ? 0.001 : max;
|
||||||
}
|
}
|
||||||
const range = max - min;
|
const range = max - min;
|
||||||
const peak = max;
|
|
||||||
const sustainVal = min + sustain * range;
|
const sustainVal = min + sustain * range;
|
||||||
const duration = end - begin;
|
const duration = end - begin;
|
||||||
|
|
||||||
const envValAtTime = (time) => {
|
const envValAtTime = (time) => {
|
||||||
let val;
|
let val;
|
||||||
if (attack > time) {
|
if (attack > time) {
|
||||||
let slope = getSlope(min, peak, 0, attack);
|
val = time * getSlope(min, max, 0, attack) + min;
|
||||||
val = time * slope + (min > peak ? min : 0);
|
|
||||||
} else {
|
} else {
|
||||||
val = (time - attack) * getSlope(peak, sustainVal, 0, decay) + peak;
|
val = (time - attack) * getSlope(max, sustainVal, 0, decay) + max;
|
||||||
}
|
}
|
||||||
if (curve === 'exponential') {
|
if (curve === 'exponential') {
|
||||||
val = val || 0.001;
|
val = val || 0.001;
|
||||||
@@ -105,22 +105,40 @@ function getModulationShapeInput(val) {
|
|||||||
return { tri: 0, triangle: 0, sine: 1, ramp: 2, saw: 3, square: 4 }[val] ?? 0;
|
return { tri: 0, triangle: 0, sine: 1, ramp: 2, saw: 3, square: 4 }[val] ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getLfo(audioContext, begin, end, properties = {}) {
|
export function getEnvelope(audioContext, properties = {}) {
|
||||||
const { shape = 0, ...props } = properties;
|
return getWorklet(audioContext, 'envelope-processor', properties);
|
||||||
const { dcoffset = -0.5, depth = 1 } = properties;
|
}
|
||||||
|
|
||||||
|
export function getLfo(audioContext, properties = {}) {
|
||||||
|
const {
|
||||||
|
shape = 0,
|
||||||
|
begin = 0,
|
||||||
|
end = 0,
|
||||||
|
time,
|
||||||
|
depth = 1,
|
||||||
|
dcoffset = -0.5,
|
||||||
|
frequency = 1,
|
||||||
|
skew = 0.5,
|
||||||
|
phaseoffset = 0,
|
||||||
|
curve = 1,
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
...props
|
||||||
|
} = properties;
|
||||||
|
|
||||||
const lfoprops = {
|
const lfoprops = {
|
||||||
frequency: 1,
|
|
||||||
depth,
|
|
||||||
skew: 0.5,
|
|
||||||
phaseoffset: 0,
|
|
||||||
time: begin,
|
|
||||||
begin,
|
begin,
|
||||||
end,
|
end,
|
||||||
shape: getModulationShapeInput(shape),
|
time: time ?? begin,
|
||||||
|
depth,
|
||||||
dcoffset,
|
dcoffset,
|
||||||
min: dcoffset * depth,
|
frequency,
|
||||||
max: dcoffset * depth + depth,
|
skew,
|
||||||
curve: 1,
|
phaseoffset,
|
||||||
|
curve,
|
||||||
|
shape: getModulationShapeInput(shape),
|
||||||
|
min: min ?? dcoffset * depth,
|
||||||
|
max: max ?? dcoffset * depth + depth,
|
||||||
...props,
|
...props,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -128,6 +146,7 @@ export function getLfo(audioContext, begin, end, properties = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getCompressor(ac, threshold, ratio, knee, attack, release) {
|
export function getCompressor(ac, threshold, ratio, knee, attack, release) {
|
||||||
|
const node = getNodeFromPool('compressor', () => new DynamicsCompressorNode(ac, {}));
|
||||||
const options = {
|
const options = {
|
||||||
threshold: threshold ?? -3,
|
threshold: threshold ?? -3,
|
||||||
ratio: ratio ?? 10,
|
ratio: ratio ?? 10,
|
||||||
@@ -135,7 +154,11 @@ export function getCompressor(ac, threshold, ratio, knee, attack, release) {
|
|||||||
attack: attack ?? 0.005,
|
attack: attack ?? 0.005,
|
||||||
release: release ?? 0.05,
|
release: release ?? 0.05,
|
||||||
};
|
};
|
||||||
return new DynamicsCompressorNode(ac, options);
|
const now = ac.currentTime;
|
||||||
|
Object.entries(options).forEach(([key, value]) => {
|
||||||
|
node[key].setValueAtTime(value, now);
|
||||||
|
});
|
||||||
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
// changes the default values of the envelope based on what parameters the user has defined
|
// changes the default values of the envelope based on what parameters the user has defined
|
||||||
@@ -162,7 +185,9 @@ export function getParamLfo(audioContext, param, start, end, lfoValues) {
|
|||||||
}
|
}
|
||||||
let lfo;
|
let lfo;
|
||||||
if (depth) {
|
if (depth) {
|
||||||
lfo = getLfo(audioContext, start, end, {
|
lfo = getLfo(audioContext, {
|
||||||
|
begin: start,
|
||||||
|
end,
|
||||||
depth,
|
depth,
|
||||||
dcoffset,
|
dcoffset,
|
||||||
...getLfoInputs,
|
...getLfoInputs,
|
||||||
@@ -214,10 +239,13 @@ export function createFilter(context, start, end, params, cps, cycle) {
|
|||||||
filter = getWorklet(context, 'ladder-processor', { frequency, q, drive });
|
filter = getWorklet(context, 'ladder-processor', { frequency, q, drive });
|
||||||
frequencyParam = filter.parameters.get('frequency');
|
frequencyParam = filter.parameters.get('frequency');
|
||||||
} else {
|
} else {
|
||||||
filter = context.createBiquadFilter();
|
const factory = () => context.createBiquadFilter();
|
||||||
|
filter = getNodeFromPool('filter', factory);
|
||||||
filter.type = type;
|
filter.type = type;
|
||||||
filter.Q.value = q;
|
const now = context.currentTime;
|
||||||
filter.frequency.value = frequency;
|
Object.entries({ Q: q, frequency }).forEach(([key, value]) => {
|
||||||
|
filter[key].setValueAtTime(value, now);
|
||||||
|
});
|
||||||
frequencyParam = filter.frequency;
|
frequencyParam = filter.frequency;
|
||||||
}
|
}
|
||||||
const envelopeValues = [params.attack, params.decay, params.sustain, params.release];
|
const envelopeValues = [params.attack, params.decay, params.sustain, params.release];
|
||||||
@@ -332,7 +360,7 @@ export function getVibratoOscillator(param, value, t) {
|
|||||||
releaseAudioNode(vibratoOscillator);
|
releaseAudioNode(vibratoOscillator);
|
||||||
});
|
});
|
||||||
vibratoOscillator.start(t);
|
vibratoOscillator.start(t);
|
||||||
return vibratoOscillator;
|
return { stop: (t) => vibratoOscillator.stop(t), nodes: { vib: [vibratoOscillator], vib_gain: [gain] } };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -388,6 +416,7 @@ export function applyFM(param, value, begin) {
|
|||||||
const ac = getAudioContext();
|
const ac = getAudioContext();
|
||||||
const toStop = []; // fm oscillators we will expose `stop` for
|
const toStop = []; // fm oscillators we will expose `stop` for
|
||||||
const fms = {};
|
const fms = {};
|
||||||
|
const nodes = {};
|
||||||
// Matrix
|
// Matrix
|
||||||
for (let i = 1; i <= 8; i++) {
|
for (let i = 1; i <= 8; i++) {
|
||||||
for (let j = 0; j <= 8; j++) {
|
for (let j = 0; j <= 8; j++) {
|
||||||
@@ -438,11 +467,14 @@ export function applyFM(param, value, begin) {
|
|||||||
output = osc.connect(envGain);
|
output = osc.connect(envGain);
|
||||||
}
|
}
|
||||||
fms[idx] = { input: osc.frequency, output, freq, osc, toCleanup };
|
fms[idx] = { input: osc.frequency, output, freq, osc, toCleanup };
|
||||||
|
nodes[`fm_${idx}`] = [osc];
|
||||||
}
|
}
|
||||||
const { input, output, freq, osc, toCleanup } = fms[idx];
|
const { input, output, freq, osc, toCleanup } = fms[idx];
|
||||||
const g = gainNode(amt * freq);
|
const gAmt = gainNode(amt);
|
||||||
io.push(isMod ? output.connect(g) : input);
|
const gFreq = gainNode(freq);
|
||||||
cleanupOnEnd(osc, [...toCleanup, g]);
|
io.push(isMod ? output.connect(gAmt).connect(gFreq) : input);
|
||||||
|
cleanupOnEnd(osc, [...toCleanup, gAmt, gFreq]);
|
||||||
|
nodes[`fm_${idx}_gain`] = [gAmt];
|
||||||
}
|
}
|
||||||
if (!io[1]) {
|
if (!io[1]) {
|
||||||
logger(
|
logger(
|
||||||
@@ -455,6 +487,7 @@ export function applyFM(param, value, begin) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
nodes,
|
||||||
stop: (t) => toStop.forEach((m) => m?.stop(t)),
|
stop: (t) => toStop.forEach((m) => m?.stop(t)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -593,7 +626,7 @@ export const releaseAudioNode = (node) => {
|
|||||||
// make sure all AudioScheduledSourceNodes are in a stopped state
|
// make sure all AudioScheduledSourceNodes are in a stopped state
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/AudioScheduledSourceNode
|
// https://developer.mozilla.org/en-US/docs/Web/API/AudioScheduledSourceNode
|
||||||
if (node instanceof AudioScheduledSourceNode) {
|
if (node instanceof AudioScheduledSourceNode) {
|
||||||
if (node.onended && node.onended.name !== 'cleanup') {
|
if (process.env.NODE_ENV === 'development' && node.onended && node.onended.name !== 'cleanup') {
|
||||||
logger(
|
logger(
|
||||||
`[superdough] Deprecation warning: it seems your code path is setting 'node.onended = callback' instead of using the onceEnded helper`,
|
`[superdough] Deprecation warning: it seems your code path is setting 'node.onended = callback' instead of using the onceEnded helper`,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export * from './helpers.mjs';
|
|||||||
export * from './synth.mjs';
|
export * from './synth.mjs';
|
||||||
export * from './zzfx.mjs';
|
export * from './zzfx.mjs';
|
||||||
export * from './logger.mjs';
|
export * from './logger.mjs';
|
||||||
|
export * from './modulators.mjs';
|
||||||
export * from './dspworklet.mjs';
|
export * from './dspworklet.mjs';
|
||||||
export * from './audioContext.mjs';
|
export * from './audioContext.mjs';
|
||||||
export * from './wavetable.mjs';
|
export * from './wavetable.mjs';
|
||||||
|
|||||||
@@ -0,0 +1,178 @@
|
|||||||
|
/*
|
||||||
|
modulators.mjs - Helpers for constructing modulators (envelopes, LFOs, etc.)
|
||||||
|
Copyright (C) 2025 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/superdough/modulators.mjs>
|
||||||
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { getAudioContext } from './audioContext.mjs';
|
||||||
|
import { gainNode, getEnvelope, getLfo, webAudioTimeout } from './helpers.mjs';
|
||||||
|
import { errorLogger } from './logger.mjs';
|
||||||
|
import { getSuperdoughControlTargets } from './superdoughdata.mjs';
|
||||||
|
import { clamp } from './util.mjs';
|
||||||
|
|
||||||
|
const getNodeParam = (node, name) => {
|
||||||
|
// Worklet case
|
||||||
|
if (node?.parameters) {
|
||||||
|
const p = node.parameters.get(name);
|
||||||
|
if (p instanceof AudioParam) {
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Built-in node case
|
||||||
|
let p = node?.[name];
|
||||||
|
if (p === undefined && name === 'frequency') {
|
||||||
|
// Fallbacks for source nodes without 'frequency' params (e.g. soundfonts)
|
||||||
|
p = node?.['detune'] ?? node?.['playbackRate'];
|
||||||
|
}
|
||||||
|
if (p instanceof AudioParam) {
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const controlTargets = getSuperdoughControlTargets();
|
||||||
|
|
||||||
|
const getControlData = (control, subControl) => {
|
||||||
|
const controlNoIdx = control.split('_')[0];
|
||||||
|
return controlTargets[`${controlNoIdx}_${subControl}`] ?? controlTargets[controlNoIdx];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRangeForParam = (paramName, currentValue) => {
|
||||||
|
// We clamp the frequency to a reasonable range unless the currentValue
|
||||||
|
// is low, which indicates this may be an LFO
|
||||||
|
if (paramName === 'frequency' && currentValue >= 30) {
|
||||||
|
return { min: 20 - currentValue, max: 24000 - currentValue };
|
||||||
|
}
|
||||||
|
return { min: undefined, max: undefined };
|
||||||
|
};
|
||||||
|
|
||||||
|
const clampWithWaveShaper = (modulator, min, max) => {
|
||||||
|
const ac = getAudioContext();
|
||||||
|
const curve = new Float32Array(256);
|
||||||
|
for (let i = 0; i < curve.length; i++) {
|
||||||
|
const x = (i / (curve.length - 1)) * 2 - 1;
|
||||||
|
curve[i] = clamp(x * max, min, max);
|
||||||
|
}
|
||||||
|
const shaper = new WaveShaperNode(ac, { curve });
|
||||||
|
const scaleGain = gainNode(1 / max);
|
||||||
|
modulator.connect(scaleGain).connect(shaper);
|
||||||
|
return { modulator, toCleanup: [shaper, scaleGain] };
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTargetParamsForControl = (control, nodes, subControl) => {
|
||||||
|
const targetInfo = getControlData(control, subControl);
|
||||||
|
if (!targetInfo) {
|
||||||
|
errorLogger(
|
||||||
|
new Error(`Could not find control data for target '${control}'. It may not be modulatable.`),
|
||||||
|
'superdough',
|
||||||
|
);
|
||||||
|
return { targetParams: [], paramName: control };
|
||||||
|
}
|
||||||
|
const paramName = targetInfo.param;
|
||||||
|
const nodeKey = nodes[targetInfo.node] ? targetInfo.node : control;
|
||||||
|
const targetNodes = nodes[nodeKey];
|
||||||
|
if (!targetNodes) {
|
||||||
|
const keys = Object.keys(nodes);
|
||||||
|
errorLogger(
|
||||||
|
new Error(`Could not connect to target '${nodeKey}' — it does not exist. Available targets: ${keys.join(', ')}`),
|
||||||
|
'superdough',
|
||||||
|
);
|
||||||
|
return { targetParams: [], paramName };
|
||||||
|
}
|
||||||
|
const audioParams = [];
|
||||||
|
targetNodes.forEach((targetNode) => {
|
||||||
|
const targetParam = getNodeParam(targetNode, paramName);
|
||||||
|
audioParams.push(targetParam);
|
||||||
|
});
|
||||||
|
return { targetParams: audioParams, paramName };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const connectLFO = (id, params, nodeTracker) => {
|
||||||
|
const {
|
||||||
|
rate = 1,
|
||||||
|
sync,
|
||||||
|
cps,
|
||||||
|
cycle,
|
||||||
|
control = 'lfo',
|
||||||
|
subControl,
|
||||||
|
fxi = 'main',
|
||||||
|
depth = 1,
|
||||||
|
depthabs,
|
||||||
|
...filteredParams
|
||||||
|
} = params;
|
||||||
|
const { targetParams, paramName } = getTargetParamsForControl(control, nodeTracker[fxi], subControl);
|
||||||
|
if (!targetParams.length) return;
|
||||||
|
let currentValue = targetParams[0].value;
|
||||||
|
currentValue = currentValue === 0 ? 1 : currentValue;
|
||||||
|
const { min, max } = getRangeForParam(paramName, currentValue);
|
||||||
|
const depthValue = depthabs != null ? depthabs : depth * currentValue;
|
||||||
|
const modParams = {
|
||||||
|
...filteredParams,
|
||||||
|
frequency: sync !== undefined ? sync * cps : rate,
|
||||||
|
time: cycle / cps,
|
||||||
|
depth: depthValue,
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
};
|
||||||
|
const lfoNode = getLfo(getAudioContext(), modParams);
|
||||||
|
nodeTracker.main[`lfo_${id}`] = [lfoNode];
|
||||||
|
targetParams.forEach((t) => lfoNode.connect(t));
|
||||||
|
return lfoNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const connectEnvelope = (id, params, nodeTracker) => {
|
||||||
|
const { control, subControl, acurve, dcurve, rcurve, depth = 1, depthabs, fxi = 'main', ...filteredParams } = params;
|
||||||
|
const { targetParams, paramName } = getTargetParamsForControl(control, nodeTracker[fxi], subControl);
|
||||||
|
if (!targetParams.length) return;
|
||||||
|
let currentValue = targetParams[0].value;
|
||||||
|
currentValue = currentValue === 0 ? 1 : currentValue;
|
||||||
|
const { min, max } = getRangeForParam(paramName, currentValue);
|
||||||
|
const depthValue = depthabs != null ? depthabs : depth * currentValue;
|
||||||
|
const envNode = getEnvelope(getAudioContext(), {
|
||||||
|
...filteredParams,
|
||||||
|
depth: depthValue,
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
attackCurve: acurve,
|
||||||
|
decayCurve: dcurve,
|
||||||
|
releaseCurve: rcurve,
|
||||||
|
});
|
||||||
|
nodeTracker.main[`env_${id}`] = [envNode];
|
||||||
|
targetParams.forEach((t) => envNode.connect(t));
|
||||||
|
return envNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const connectBusModulator = (params, nodeTracker, controller) => {
|
||||||
|
const ac = getAudioContext();
|
||||||
|
const { control, subControl, depth = 1, depthabs, fxi = 'main' } = params;
|
||||||
|
const { targetParams, paramName } = getTargetParamsForControl(control, nodeTracker[fxi], subControl);
|
||||||
|
if (!targetParams.length) return { toCleanup: [] };
|
||||||
|
const signal = controller.getBus(params.bus);
|
||||||
|
const dc = new ConstantSourceNode(ac, { offset: params.dc ?? 0 });
|
||||||
|
dc.start(params.begin);
|
||||||
|
const shifted = dc.connect(gainNode(1));
|
||||||
|
signal.connect(shifted);
|
||||||
|
let currentValue = targetParams[0].value;
|
||||||
|
currentValue = currentValue === 0 ? 1 : currentValue;
|
||||||
|
const { min, max } = getRangeForParam(paramName, currentValue);
|
||||||
|
const depthValue = depthabs != null ? depthabs : depth * currentValue;
|
||||||
|
const depthGain = gainNode((Math.sign(depthValue) * Math.abs(depthValue)) / 0.3);
|
||||||
|
const unClamped = shifted.connect(depthGain);
|
||||||
|
const toCleanup = [];
|
||||||
|
let modulator = unClamped;
|
||||||
|
if (min !== undefined && max !== undefined) {
|
||||||
|
const wsData = clampWithWaveShaper(unClamped, min, max);
|
||||||
|
modulator = wsData.modulator;
|
||||||
|
toCleanup.push(...wsData.toCleanup);
|
||||||
|
}
|
||||||
|
webAudioTimeout(
|
||||||
|
ac,
|
||||||
|
() => {
|
||||||
|
targetParams.forEach((t) => modulator.connect(t));
|
||||||
|
},
|
||||||
|
0,
|
||||||
|
params.begin,
|
||||||
|
);
|
||||||
|
toCleanup.push(dc, shifted, depthGain);
|
||||||
|
return { modulator, toCleanup };
|
||||||
|
};
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
/*
|
||||||
|
nodePools.mjs - Helper functions related to pooling and re-using audio nodes
|
||||||
|
|
||||||
|
Copyright (C) 2025 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/superdough/nodePools.mjs>
|
||||||
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const nodePools = new Map();
|
||||||
|
const POOL_KEY = Symbol('nodePoolKey');
|
||||||
|
const IS_WORKLET_DEAD = Symbol('nodePoolIsWorkletDead');
|
||||||
|
const MAX_POOL_SIZE = 64;
|
||||||
|
|
||||||
|
export const isPoolable = (node) => !!node[POOL_KEY];
|
||||||
|
|
||||||
|
const getParams = (node) => {
|
||||||
|
const params = new Set();
|
||||||
|
node.parameters?.forEach((param) => params.add(param));
|
||||||
|
const visited = new Set(); // prioritize deepest definition
|
||||||
|
let proto = node;
|
||||||
|
// Move up the prototype chain
|
||||||
|
while (proto !== Object.prototype) {
|
||||||
|
for (const key of Object.getOwnPropertyNames(proto)) {
|
||||||
|
if (visited.has(key)) continue;
|
||||||
|
visited.add(key);
|
||||||
|
const value = node[key];
|
||||||
|
if (value instanceof AudioParam) {
|
||||||
|
params.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
proto = Object.getPrototypeOf(proto);
|
||||||
|
}
|
||||||
|
return params;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const releaseNodeToPool = (node) => {
|
||||||
|
node.disconnect();
|
||||||
|
if (node instanceof AudioScheduledSourceNode) {
|
||||||
|
// not reusable
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (node[IS_WORKLET_DEAD]) {
|
||||||
|
// Worklet already terminated, don't pool it
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const key = node[POOL_KEY];
|
||||||
|
if (key == null) return;
|
||||||
|
const now = node.context?.currentTime ?? 0;
|
||||||
|
getParams(node).forEach((param) => param.cancelScheduledValues(now));
|
||||||
|
const pool = nodePools.get(key) ?? [];
|
||||||
|
if (pool.length < MAX_POOL_SIZE) {
|
||||||
|
pool.push(new WeakRef(node));
|
||||||
|
nodePools.set(key, pool);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const markWorkletAsDead = (worklet) => (worklet[IS_WORKLET_DEAD] = true);
|
||||||
|
|
||||||
|
// Attempt to get node from the pool. If this fails, fall back
|
||||||
|
// to building it with the factory
|
||||||
|
export const getNodeFromPool = (key, factory) => {
|
||||||
|
const pool = nodePools.get(key) ?? [];
|
||||||
|
let node;
|
||||||
|
while (pool.length) {
|
||||||
|
const ref = pool.pop();
|
||||||
|
node = ref?.deref();
|
||||||
|
if (node != null && !node[IS_WORKLET_DEAD]) break;
|
||||||
|
}
|
||||||
|
if (node == null || node[IS_WORKLET_DEAD]) {
|
||||||
|
node = factory();
|
||||||
|
}
|
||||||
|
node[POOL_KEY] = key;
|
||||||
|
return node;
|
||||||
|
};
|
||||||
@@ -2,7 +2,7 @@ import reverbGen from './reverbGen.mjs';
|
|||||||
import { clamp } from './util.mjs';
|
import { clamp } from './util.mjs';
|
||||||
|
|
||||||
if (typeof AudioContext !== 'undefined') {
|
if (typeof AudioContext !== 'undefined') {
|
||||||
AudioContext.prototype.adjustLength = function (duration, buffer, speed = 1, offsetAmount = 0) {
|
BaseAudioContext.prototype.adjustLength = function (duration, buffer, speed = 1, offsetAmount = 0) {
|
||||||
const sampleOffset = Math.floor(clamp(offsetAmount, 0, 1) * buffer.length);
|
const sampleOffset = Math.floor(clamp(offsetAmount, 0, 1) * buffer.length);
|
||||||
const newLength = buffer.sampleRate * duration;
|
const newLength = buffer.sampleRate * duration;
|
||||||
const newBuffer = this.createBuffer(buffer.numberOfChannels, buffer.length, buffer.sampleRate);
|
const newBuffer = this.createBuffer(buffer.numberOfChannels, buffer.length, buffer.sampleRate);
|
||||||
@@ -23,7 +23,7 @@ if (typeof AudioContext !== 'undefined') {
|
|||||||
return newBuffer;
|
return newBuffer;
|
||||||
};
|
};
|
||||||
|
|
||||||
AudioContext.prototype.createReverb = function (duration, fade, lp, dim, ir, irspeed, irbegin) {
|
BaseAudioContext.prototype.createReverb = function (duration, fade, lp, dim, ir, irspeed, irbegin) {
|
||||||
const convolver = this.createConvolver();
|
const convolver = this.createConvolver();
|
||||||
convolver.generate = (d = 2, fade = 0.1, lp = 15000, dim = 1000, ir, irspeed, irbegin) => {
|
convolver.generate = (d = 2, fade = 0.1, lp = 15000, dim = 1000, ir, irspeed, irbegin) => {
|
||||||
convolver.duration = d;
|
convolver.duration = d;
|
||||||
|
|||||||
@@ -12,6 +12,8 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
|
import { releaseAudioNode } from './helpers.mjs';
|
||||||
|
|
||||||
var reverbGen = {};
|
var reverbGen = {};
|
||||||
|
|
||||||
/** Generates a reverb impulse response.
|
/** Generates a reverb impulse response.
|
||||||
@@ -104,8 +106,8 @@ var applyGradualLowpass = function (input, lpFreqStart, lpFreqEnd, lpFreqEndAt,
|
|||||||
player.start();
|
player.start();
|
||||||
context.oncomplete = function (event) {
|
context.oncomplete = function (event) {
|
||||||
callback(event.renderedBuffer);
|
callback(event.renderedBuffer);
|
||||||
filter.disconnect();
|
releaseAudioNode(filter);
|
||||||
player.disconnect();
|
releaseAudioNode(player);
|
||||||
};
|
};
|
||||||
context.startRendering();
|
context.startRendering();
|
||||||
|
|
||||||
|
|||||||
@@ -65,21 +65,22 @@ export const getSampleBufferSource = async (hapValue, bank, resolveUrl) => {
|
|||||||
bufferSource.playbackRate.value = playbackRate;
|
bufferSource.playbackRate.value = playbackRate;
|
||||||
|
|
||||||
const { loopBegin = 0, loopEnd = 1, begin = 0, end = 1 } = hapValue;
|
const { loopBegin = 0, loopEnd = 1, begin = 0, end = 1 } = hapValue;
|
||||||
|
const bufferDuration = bufferSource.buffer.duration;
|
||||||
|
|
||||||
// "The computation of the offset into the sound is performed using the sound buffer's natural sample rate,
|
// The computation of the offset into the sound is performed using the sound buffer's natural duration,
|
||||||
// rather than the current playback rate, so even if the sound is playing at twice its normal speed,
|
// rather than the playback duration, so that even if the sound is playing at twice its normal speed,
|
||||||
// the midway point through a 10-second audio buffer is still 5."
|
// the midway point through a 10-second audio buffer is still 5.
|
||||||
const offset = begin * bufferSource.buffer.duration;
|
const offset = begin * bufferDuration;
|
||||||
|
|
||||||
const loop = hapValue.loop;
|
const loop = hapValue.loop;
|
||||||
if (loop) {
|
if (loop) {
|
||||||
bufferSource.loop = true;
|
bufferSource.loop = true;
|
||||||
bufferSource.loopStart = loopBegin * bufferSource.buffer.duration - offset;
|
bufferSource.loopStart = loopBegin * bufferDuration;
|
||||||
bufferSource.loopEnd = loopEnd * bufferSource.buffer.duration - offset;
|
bufferSource.loopEnd = loopEnd * bufferDuration;
|
||||||
}
|
}
|
||||||
const bufferDuration = bufferSource.buffer.duration / bufferSource.playbackRate.value;
|
const playbackDuration = bufferDuration / bufferSource.playbackRate.value;
|
||||||
const sliceDuration = (end - begin) * bufferDuration;
|
const sliceDuration = (end - begin) * playbackDuration;
|
||||||
return { bufferSource, offset, bufferDuration, sliceDuration };
|
return { bufferSource, offset, bufferDuration, playbackDuration, sliceDuration };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const loadBuffer = (url, ac, s, n = 0) => {
|
export const loadBuffer = (url, ac, s, n = 0) => {
|
||||||
@@ -301,7 +302,7 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// vibrato
|
// vibrato
|
||||||
let vibratoOscillator = getVibratoOscillator(bufferSource.detune, value, t);
|
const vibratoHandle = getVibratoOscillator(bufferSource.detune, value, t);
|
||||||
|
|
||||||
const time = t + nudge;
|
const time = t + nudge;
|
||||||
bufferSource.start(time, offset);
|
bufferSource.start(time, offset);
|
||||||
@@ -324,7 +325,7 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
|
|||||||
node.connect(out);
|
node.connect(out);
|
||||||
onceEnded(bufferSource, function () {
|
onceEnded(bufferSource, function () {
|
||||||
releaseAudioNode(bufferSource);
|
releaseAudioNode(bufferSource);
|
||||||
releaseAudioNode(vibratoOscillator);
|
vibratoHandle?.stop();
|
||||||
releaseAudioNode(node);
|
releaseAudioNode(node);
|
||||||
releaseAudioNode(out);
|
releaseAudioNode(out);
|
||||||
onended();
|
onended();
|
||||||
@@ -334,7 +335,7 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
|
|||||||
const stop = (endTime) => {
|
const stop = (endTime) => {
|
||||||
bufferSource.stop(endTime);
|
bufferSource.stop(endTime);
|
||||||
};
|
};
|
||||||
const handle = { node: out, bufferSource, stop };
|
const handle = { node: out, nodes: { source: [bufferSource], ...vibratoHandle?.nodes }, stop };
|
||||||
|
|
||||||
// cut groups
|
// cut groups
|
||||||
if (cut !== undefined) {
|
if (cut !== undefined) {
|
||||||
|
|||||||
+481
-249
@@ -7,34 +7,49 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
import './feedbackdelay.mjs';
|
import './feedbackdelay.mjs';
|
||||||
import './reverb.mjs';
|
import './reverb.mjs';
|
||||||
import './vowel.mjs';
|
import './vowel.mjs';
|
||||||
import { nanFallback, _mod, cycleToSeconds, pickAndRename } from './util.mjs';
|
import { clamp, nanFallback, _mod, cycleToSeconds, pickAndRename } from './util.mjs';
|
||||||
import workletsUrl from './worklets.mjs?audioworklet';
|
import workletsUrl from './worklets.mjs?audioworklet';
|
||||||
|
import { getNodeFromPool, isPoolable, releaseNodeToPool } from './nodePools.mjs';
|
||||||
import {
|
import {
|
||||||
createFilter,
|
createFilter,
|
||||||
|
effectSend,
|
||||||
gainNode,
|
gainNode,
|
||||||
getCompressor,
|
getCompressor,
|
||||||
getDistortion,
|
getDistortion,
|
||||||
getLfo,
|
getLfo,
|
||||||
getWorklet,
|
getWorklet,
|
||||||
effectSend,
|
|
||||||
releaseAudioNode,
|
releaseAudioNode,
|
||||||
|
webAudioTimeout,
|
||||||
} from './helpers.mjs';
|
} from './helpers.mjs';
|
||||||
import { map } from 'nanostores';
|
import { map } from 'nanostores';
|
||||||
import { logger } from './logger.mjs';
|
import { logger } from './logger.mjs';
|
||||||
|
import { connectLFO, connectEnvelope, connectBusModulator } from './modulators.mjs';
|
||||||
import { loadBuffer } from './sampler.mjs';
|
import { loadBuffer } from './sampler.mjs';
|
||||||
import { getAudioContext } from './audioContext.mjs';
|
import { getAudioContext } from './audioContext.mjs';
|
||||||
import { SuperdoughAudioController } from './superdoughoutput.mjs';
|
import { SuperdoughAudioController } from './superdoughoutput.mjs';
|
||||||
|
import { resetSeenKeys } from './wavetable.mjs';
|
||||||
|
|
||||||
export const DEFAULT_MAX_POLYPHONY = 128;
|
export const DEFAULT_MAX_POLYPHONY = 128;
|
||||||
const DEFAULT_AUDIO_DEVICE_NAME = 'System Standard';
|
const DEFAULT_AUDIO_DEVICE_NAME = 'System Standard';
|
||||||
|
|
||||||
let maxPolyphony = DEFAULT_MAX_POLYPHONY;
|
export let maxPolyphony = DEFAULT_MAX_POLYPHONY;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the max polyphony. If notes are ringing out via `release` then they will
|
||||||
|
* start to die out in first-in-first-out order once the max polyphony has been hit
|
||||||
|
*
|
||||||
|
* @name setMaxPolyphony
|
||||||
|
* @param {number} Max polyphony. Defaults to 128
|
||||||
|
* @example
|
||||||
|
* setMaxPolyphony(4)
|
||||||
|
* n(irand(24).seg(8)).scale("C#3:minor").room(1).release(4).gain(0.5)
|
||||||
|
*
|
||||||
|
*/
|
||||||
export function setMaxPolyphony(polyphony) {
|
export function setMaxPolyphony(polyphony) {
|
||||||
maxPolyphony = parseInt(polyphony) ?? DEFAULT_MAX_POLYPHONY;
|
maxPolyphony = parseInt(polyphony) ?? DEFAULT_MAX_POLYPHONY;
|
||||||
}
|
}
|
||||||
|
|
||||||
let multiChannelOrbits = false;
|
export let multiChannelOrbits = false;
|
||||||
export function setMultiChannelOrbits(bool) {
|
export function setMultiChannelOrbits(bool) {
|
||||||
multiChannelOrbits = bool == true;
|
multiChannelOrbits = bool == true;
|
||||||
}
|
}
|
||||||
@@ -52,6 +67,17 @@ export function applyGainCurve(val) {
|
|||||||
return gainCurveFunc(val);
|
return gainCurveFunc(val);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply a function to all gains provided in patterns. Can be used to rescale gain to be
|
||||||
|
* quadratic, exponential, etc. rather than linear
|
||||||
|
*
|
||||||
|
* @name setGainCurve
|
||||||
|
* @param {Function} function to apply to all gain values
|
||||||
|
* @example
|
||||||
|
* setGainCurve((x) => x * x) // quadratic gain
|
||||||
|
* s("bd*4").gain(0.5) // equivalent to 0.25 gain normally
|
||||||
|
*
|
||||||
|
*/
|
||||||
export function setGainCurve(newGainCurveFunc) {
|
export function setGainCurve(newGainCurveFunc) {
|
||||||
gainCurveFunc = newGainCurveFunc;
|
gainCurveFunc = newGainCurveFunc;
|
||||||
}
|
}
|
||||||
@@ -161,6 +187,7 @@ let defaultDefaultValues = {
|
|||||||
distortvol: 1,
|
distortvol: 1,
|
||||||
distorttype: 0,
|
distorttype: 0,
|
||||||
delay: 0,
|
delay: 0,
|
||||||
|
busgain: 1,
|
||||||
byteBeatExpression: '0',
|
byteBeatExpression: '0',
|
||||||
delayfeedback: 0.5,
|
delayfeedback: 0.5,
|
||||||
delaysync: 3 / 16,
|
delaysync: 3 / 16,
|
||||||
@@ -168,6 +195,9 @@ let defaultDefaultValues = {
|
|||||||
i: 1,
|
i: 1,
|
||||||
velocity: 1,
|
velocity: 1,
|
||||||
fft: 8,
|
fft: 8,
|
||||||
|
tremolodepth: 1,
|
||||||
|
tremolophase: 0,
|
||||||
|
release: 0.01,
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultDefaultDefaultValues = Object.freeze({ ...defaultDefaultValues });
|
const defaultDefaultDefaultValues = Object.freeze({ ...defaultDefaultValues });
|
||||||
@@ -212,11 +242,13 @@ export function registerWorklet(url) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let workletsLoading;
|
let workletsLoading;
|
||||||
function loadWorklets() {
|
export function loadWorklets() {
|
||||||
if (!workletsLoading) {
|
if (!workletsLoading) {
|
||||||
const audioCtx = getAudioContext();
|
const audioCtx = getAudioContext();
|
||||||
const allWorkletURLs = externalWorklets.concat([workletsUrl]);
|
const allWorkletURLs = externalWorklets.concat([workletsUrl]);
|
||||||
workletsLoading = Promise.all(allWorkletURLs.map((workletURL) => audioCtx.audioWorklet.addModule(workletURL)));
|
workletsLoading = Promise.all(allWorkletURLs.map((workletURL) => audioCtx.audioWorklet.addModule(workletURL))).then(
|
||||||
|
() => (workletsLoading = undefined),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return workletsLoading;
|
return workletsLoading;
|
||||||
@@ -233,6 +265,7 @@ export async function initAudio(options = {}) {
|
|||||||
|
|
||||||
setMaxPolyphony(maxPolyphony);
|
setMaxPolyphony(maxPolyphony);
|
||||||
setMultiChannelOrbits(multiChannelOrbits);
|
setMultiChannelOrbits(multiChannelOrbits);
|
||||||
|
resetSeenKeys();
|
||||||
if (typeof window === 'undefined') {
|
if (typeof window === 'undefined') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -254,8 +287,9 @@ export async function initAudio(options = {}) {
|
|||||||
logger('[superdough] failed to set audio interface', 'warning');
|
logger('[superdough] failed to set audio interface', 'warning');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if ((!audioCtx) instanceof OfflineAudioContext) {
|
||||||
await audioCtx.resume();
|
await audioCtx.resume();
|
||||||
|
}
|
||||||
if (disableWorklets) {
|
if (disableWorklets) {
|
||||||
logger('[superdough]: AudioWorklets disabled with disableWorklets');
|
logger('[superdough]: AudioWorklets disabled with disableWorklets');
|
||||||
return;
|
return;
|
||||||
@@ -289,21 +323,27 @@ export function getSuperdoughAudioController() {
|
|||||||
}
|
}
|
||||||
return controller;
|
return controller;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function setSuperdoughAudioController(newController) {
|
||||||
|
controller = newController;
|
||||||
|
return controller;
|
||||||
|
}
|
||||||
|
|
||||||
export function connectToDestination(input, channels) {
|
export function connectToDestination(input, channels) {
|
||||||
const controller = getSuperdoughAudioController();
|
const controller = getSuperdoughAudioController();
|
||||||
controller.output.connectToDestination(input, channels);
|
controller.output.connectToDestination(input, channels);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPhaser(time, end, frequency = 1, depth = 0.5, centerFrequency = 1000, sweep = 2000) {
|
function getPhaser(begin, end, frequency = 1, depth = 0.5, centerFrequency = 1000, sweep = 2000) {
|
||||||
const ac = getAudioContext();
|
const ac = getAudioContext();
|
||||||
const lfo = getLfo(ac, time, end, { frequency, depth: sweep * 2 });
|
const lfo = getLfo(ac, { frequency, depth: sweep * 2, begin, end });
|
||||||
|
|
||||||
//filters
|
//filters
|
||||||
const numStages = 1; //num of filters in series
|
const numStages = 1; //num of filters in series
|
||||||
let fOffset = 282; //for backward compat in #1800
|
let fOffset = 282; //for backward compat in #1800
|
||||||
const filterChain = [];
|
const filterChain = [];
|
||||||
for (let i = 0; i < numStages; i++) {
|
for (let i = 0; i < numStages; i++) {
|
||||||
const filter = ac.createBiquadFilter();
|
const filter = getNodeFromPool('filter', () => ac.createBiquadFilter());
|
||||||
filter.type = 'notch';
|
filter.type = 'notch';
|
||||||
filter.gain.value = 1;
|
filter.gain.value = 1;
|
||||||
filter.frequency.value = centerFrequency + fOffset;
|
filter.frequency.value = centerFrequency + fOffset;
|
||||||
@@ -326,7 +366,7 @@ export let analysers = {},
|
|||||||
analysersData = {};
|
analysersData = {};
|
||||||
|
|
||||||
export function getAnalyserById(id, fftSize = 1024, smoothingTimeConstant = 0.5) {
|
export function getAnalyserById(id, fftSize = 1024, smoothingTimeConstant = 0.5) {
|
||||||
if (!analysers[id]) {
|
if (!analysers[id] || analysers[id].context != getAudioContext()) {
|
||||||
// make sure this doesn't happen too often as it piles up garbage
|
// make sure this doesn't happen too often as it piles up garbage
|
||||||
const analyserNode = getAudioContext().createAnalyser();
|
const analyserNode = getAudioContext().createAnalyser();
|
||||||
analyserNode.fftSize = fftSize;
|
analyserNode.fftSize = fftSize;
|
||||||
@@ -367,7 +407,37 @@ function mapChannelNumbers(channels) {
|
|||||||
return (Array.isArray(channels) ? channels : [channels]).map((ch) => ch - 1);
|
return (Array.isArray(channels) ? channels : [channels]).map((ch) => ch - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class Chain {
|
||||||
|
constructor(head) {
|
||||||
|
this.audioNodes = [head];
|
||||||
|
this.tails = [head];
|
||||||
|
}
|
||||||
|
connect(...nodes) {
|
||||||
|
nodes.forEach((node) => {
|
||||||
|
this.tails.forEach((tail) => {
|
||||||
|
tail.connect(node);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
this.tails = nodes;
|
||||||
|
this.audioNodes.push(...nodes);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
connectOne(idx, node) {
|
||||||
|
this.tails[idx].connect(node);
|
||||||
|
this.tails[idx] = node;
|
||||||
|
this.audioNodes.push(node);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
releaseNodes() {
|
||||||
|
this.audioNodes.forEach((n) => (isPoolable(n) ? releaseNodeToPool(n) : releaseAudioNode(n)));
|
||||||
|
this.audioNodes = [];
|
||||||
|
this.tails = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) => {
|
export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) => {
|
||||||
|
// mapping from main FX and numbered FX chains to nodes
|
||||||
|
const nodes = { main: {} };
|
||||||
// new: t is always expected to be the absolute target onset time
|
// new: t is always expected to be the absolute target onset time
|
||||||
const ac = getAudioContext();
|
const ac = getAudioContext();
|
||||||
const audioController = getSuperdoughAudioController();
|
const audioController = getSuperdoughAudioController();
|
||||||
@@ -388,7 +458,6 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
|||||||
// duration is passed as value too..
|
// duration is passed as value too..
|
||||||
value.duration = hapDuration;
|
value.duration = hapDuration;
|
||||||
// calculate absolute time
|
// calculate absolute time
|
||||||
|
|
||||||
if (t < ac.currentTime) {
|
if (t < ac.currentTime) {
|
||||||
console.warn(
|
console.warn(
|
||||||
`[superdough]: cannot schedule sounds in the past (target: ${t.toFixed(2)}, now: ${ac.currentTime.toFixed(2)})`,
|
`[superdough]: cannot schedule sounds in the past (target: ${t.toFixed(2)}, now: ${ac.currentTime.toFixed(2)})`,
|
||||||
@@ -397,49 +466,24 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
|||||||
}
|
}
|
||||||
// destructure
|
// destructure
|
||||||
let {
|
let {
|
||||||
tremolo,
|
|
||||||
tremolosync,
|
|
||||||
tremolodepth = 1,
|
|
||||||
tremoloskew,
|
|
||||||
tremolophase = 0,
|
|
||||||
tremoloshape,
|
|
||||||
s = getDefaultValue('s'),
|
s = getDefaultValue('s'),
|
||||||
bank,
|
bank,
|
||||||
source,
|
source,
|
||||||
gain = getDefaultValue('gain'),
|
|
||||||
postgain = getDefaultValue('postgain'),
|
postgain = getDefaultValue('postgain'),
|
||||||
density = getDefaultValue('density'),
|
|
||||||
duckorbit,
|
duckorbit,
|
||||||
duckonset,
|
duckonset,
|
||||||
duckattack,
|
duckattack,
|
||||||
duckdepth,
|
duckdepth,
|
||||||
djf,
|
djf,
|
||||||
// filters
|
release = getDefaultValue('release'),
|
||||||
fanchor = getDefaultValue('fanchor'),
|
|
||||||
release = 0,
|
|
||||||
|
|
||||||
//phaser
|
|
||||||
phaserrate,
|
|
||||||
phaserdepth = getDefaultValue('phaserdepth'),
|
|
||||||
phasersweep,
|
|
||||||
phasercenter,
|
|
||||||
//
|
|
||||||
coarse,
|
|
||||||
|
|
||||||
crush,
|
|
||||||
dry,
|
dry,
|
||||||
shape,
|
|
||||||
shapevol = getDefaultValue('shapevol'),
|
|
||||||
distort,
|
|
||||||
distortvol = getDefaultValue('distortvol'),
|
|
||||||
distorttype = getDefaultValue('distorttype'),
|
|
||||||
pan,
|
|
||||||
vowel,
|
|
||||||
delay = getDefaultValue('delay'),
|
delay = getDefaultValue('delay'),
|
||||||
delayfeedback = getDefaultValue('delayfeedback'),
|
delayfeedback = getDefaultValue('delayfeedback'),
|
||||||
delaysync = getDefaultValue('delaysync'),
|
delaysync = getDefaultValue('delaysync'),
|
||||||
delaytime,
|
delaytime,
|
||||||
orbit = getDefaultValue('orbit'),
|
orbit = getDefaultValue('orbit'),
|
||||||
|
bus,
|
||||||
|
busgain = getDefaultValue('busgain'),
|
||||||
room,
|
room,
|
||||||
roomfade,
|
roomfade,
|
||||||
roomlp,
|
roomlp,
|
||||||
@@ -449,16 +493,10 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
|||||||
irspeed,
|
irspeed,
|
||||||
irbegin,
|
irbegin,
|
||||||
i = getDefaultValue('i'),
|
i = getDefaultValue('i'),
|
||||||
velocity = getDefaultValue('velocity'),
|
|
||||||
analyze, // analyser wet
|
analyze, // analyser wet
|
||||||
fft = getDefaultValue('fft'), // fftSize 0 - 10
|
fft = getDefaultValue('fft'), // fftSize 0 - 10
|
||||||
compressor: compressorThreshold,
|
FX = [],
|
||||||
compressorRatio,
|
FXrelease,
|
||||||
compressorKnee,
|
|
||||||
compressorAttack,
|
|
||||||
compressorRelease,
|
|
||||||
transient,
|
|
||||||
transsustain,
|
|
||||||
} = value;
|
} = value;
|
||||||
|
|
||||||
delaytime = delaytime ?? cycleToSeconds(delaysync, cps);
|
delaytime = delaytime ?? cycleToSeconds(delaysync, cps);
|
||||||
@@ -473,17 +511,13 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
|||||||
audioController.duck(duckorbit, t, duckonset, duckattack, duckdepth);
|
audioController.duck(duckorbit, t, duckonset, duckattack, duckdepth);
|
||||||
}
|
}
|
||||||
|
|
||||||
gain = applyGainCurve(nanFallback(gain, 1));
|
|
||||||
postgain = applyGainCurve(postgain);
|
postgain = applyGainCurve(postgain);
|
||||||
shapevol = applyGainCurve(shapevol);
|
|
||||||
distortvol = applyGainCurve(distortvol);
|
|
||||||
delay = applyGainCurve(delay);
|
delay = applyGainCurve(delay);
|
||||||
velocity = applyGainCurve(velocity);
|
busgain = applyGainCurve(busgain);
|
||||||
tremolodepth = applyGainCurve(tremolodepth);
|
|
||||||
gain *= velocity; // velocity currently only multiplies with gain. it might do other things in the future
|
|
||||||
|
|
||||||
const end = t + hapDuration;
|
const end = t + hapDuration;
|
||||||
const endWithRelease = end + release;
|
const fullRelease = Math.max(release, FXrelease ?? 0);
|
||||||
|
const endWithRelease = end + fullRelease;
|
||||||
const chainID = Math.round(Math.random() * 1000000);
|
const chainID = Math.round(Math.random() * 1000000);
|
||||||
|
|
||||||
// oldest audio nodes will be destroyed if maximum polyphony is exceeded
|
// oldest audio nodes will be destroyed if maximum polyphony is exceeded
|
||||||
@@ -497,8 +531,6 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
|||||||
activeSoundSources.delete(chainID);
|
activeSoundSources.delete(chainID);
|
||||||
}
|
}
|
||||||
|
|
||||||
let audioNodes = [];
|
|
||||||
|
|
||||||
if (['-', '~', '_'].includes(s)) {
|
if (['-', '~', '_'].includes(s)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -511,17 +543,27 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
|||||||
let sourceNode;
|
let sourceNode;
|
||||||
if (source) {
|
if (source) {
|
||||||
sourceNode = source(t, value, hapDuration, cps);
|
sourceNode = source(t, value, hapDuration, cps);
|
||||||
|
nodes.main['source'] = [sourceNode];
|
||||||
} else if (getSound(s)) {
|
} else if (getSound(s)) {
|
||||||
const { onTrigger } = getSound(s);
|
const { onTrigger } = getSound(s);
|
||||||
const onEnded = () => {
|
|
||||||
audioNodes.forEach((n) => releaseAudioNode(n));
|
const onEnded = () =>
|
||||||
activeSoundSources.delete(chainID);
|
webAudioTimeout(
|
||||||
};
|
ac,
|
||||||
|
() => {
|
||||||
|
chain.releaseNodes();
|
||||||
|
activeSoundSources.delete(chainID);
|
||||||
|
},
|
||||||
|
0,
|
||||||
|
endWithRelease,
|
||||||
|
);
|
||||||
|
|
||||||
const soundHandle = await onTrigger(t, value, onEnded, cps);
|
const soundHandle = await onTrigger(t, value, onEnded, cps);
|
||||||
|
|
||||||
if (soundHandle) {
|
if (soundHandle) {
|
||||||
sourceNode = soundHandle.node;
|
sourceNode = soundHandle.node;
|
||||||
activeSoundSources.set(chainID, new WeakRef(soundHandle)); // allow GC
|
activeSoundSources.set(chainID, new WeakRef(soundHandle)); // allow GC
|
||||||
|
nodes.main = { ...nodes.main, ...soundHandle.nodes };
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`sound ${s} not found! Is it loaded?`);
|
throw new Error(`sound ${s} not found! Is it loaded?`);
|
||||||
@@ -536,206 +578,345 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
|||||||
logger('[webaudio] skip hap: still loading', ac.currentTime - t);
|
logger('[webaudio] skip hap: still loading', ac.currentTime - t);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const chain = []; // audio nodes that will be connected to each other sequentially
|
|
||||||
chain.push(sourceNode);
|
|
||||||
stretch !== undefined && chain.push(getWorklet(ac, 'phase-vocoder-processor', { pitchFactor: stretch }));
|
|
||||||
|
|
||||||
transient !== undefined &&
|
const chain = new Chain(sourceNode); // connection manager which tracks audio nodes for releasing
|
||||||
chain.push(
|
FX = [...FX, value]; // run through the FX chain and then run through all FX outside of it as well
|
||||||
getWorklet(
|
for (let [idx, fx] of Object.entries(FX)) {
|
||||||
|
const key = idx == FX.length - 1 ? 'main' : idx;
|
||||||
|
nodes[key] ??= {};
|
||||||
|
const fxNodes = nodes[key];
|
||||||
|
let {
|
||||||
|
gain = getDefaultValue('gain'),
|
||||||
|
velocity = getDefaultValue('velocity'),
|
||||||
|
shapevol = getDefaultValue('shapevol'),
|
||||||
|
distorttype = getDefaultValue('distorttype'),
|
||||||
|
distortvol = getDefaultValue('distortvol'),
|
||||||
|
tremolodepth = getDefaultValue('tremolodepth'),
|
||||||
|
phaserdepth = getDefaultValue('phaserdepth'),
|
||||||
|
delay = getDefaultValue('delay'),
|
||||||
|
delayfeedback = getDefaultValue('delayfeedback'),
|
||||||
|
delaysync = getDefaultValue('delaysync'),
|
||||||
|
delaytime,
|
||||||
|
stretch = getDefaultValue('stretch'),
|
||||||
|
i = getDefaultValue('i'),
|
||||||
|
} = fx;
|
||||||
|
gain = applyGainCurve(nanFallback(gain, 1));
|
||||||
|
shapevol = applyGainCurve(shapevol);
|
||||||
|
distortvol = applyGainCurve(distortvol);
|
||||||
|
velocity = applyGainCurve(velocity);
|
||||||
|
tremolodepth = applyGainCurve(tremolodepth);
|
||||||
|
gain *= velocity; // velocity currently only multiplies with gain. it might do other things in the future
|
||||||
|
delaytime = delaytime ?? cycleToSeconds(delaysync, cps);
|
||||||
|
|
||||||
|
if (stretch !== undefined) {
|
||||||
|
const phaseVocoder = getWorklet(ac, 'phase-vocoder-processor', { pitchFactor: stretch });
|
||||||
|
chain.connect(phaseVocoder);
|
||||||
|
fxNodes['stretch'] = [phaseVocoder];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fx.transient !== undefined) {
|
||||||
|
const transProcessor = getWorklet(
|
||||||
ac,
|
ac,
|
||||||
'transient-processor',
|
'transient-processor',
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
processorOptions: {
|
processorOptions: {
|
||||||
attack: transient,
|
attack: fx.transient,
|
||||||
sustain: transsustain,
|
sustain: fx.transsustain,
|
||||||
begin: t,
|
begin: t,
|
||||||
end: endWithRelease,
|
end: endWithRelease,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
),
|
);
|
||||||
);
|
chain.connect(transProcessor);
|
||||||
|
fxNodes['transient'] = transProcessor;
|
||||||
|
}
|
||||||
|
|
||||||
// gain stage
|
// gain stage
|
||||||
chain.push(gainNode(gain));
|
const initialGain = gainNode(gain);
|
||||||
|
fxNodes['gain'] = [initialGain];
|
||||||
|
chain.connect(initialGain);
|
||||||
|
|
||||||
// filter
|
// filter
|
||||||
const ftype = getFilterType(value.ftype);
|
const ftype = getFilterType(value.ftype);
|
||||||
|
|
||||||
if (value.cutoff !== undefined) {
|
const filt = (params) => createFilter(ac, t, end, params, cps, cycle);
|
||||||
const lpMap = {
|
if (fx.cutoff !== undefined) {
|
||||||
frequency: 'cutoff',
|
const lpMap = {
|
||||||
q: 'resonance',
|
frequency: 'cutoff',
|
||||||
attack: 'lpattack',
|
q: 'resonance',
|
||||||
decay: 'lpdecay',
|
attack: 'lpattack',
|
||||||
sustain: 'lpsustain',
|
decay: 'lpdecay',
|
||||||
release: 'lprelease',
|
sustain: 'lpsustain',
|
||||||
env: 'lpenv',
|
release: 'lprelease',
|
||||||
anchor: 'fanchor',
|
env: 'lpenv',
|
||||||
model: 'ftype',
|
anchor: 'fanchor',
|
||||||
drive: 'drive',
|
model: 'ftype',
|
||||||
rate: 'lprate',
|
drive: 'drive',
|
||||||
sync: 'lpsync',
|
rate: 'lprate',
|
||||||
depth: 'lpdepth',
|
sync: 'lpsync',
|
||||||
depthfrequency: 'lpdepthfrequency',
|
depth: 'lpdepth',
|
||||||
shape: 'lpshape',
|
depthfrequency: 'lpdepthfrequency',
|
||||||
dcoffset: 'lpdc',
|
shape: 'lpshape',
|
||||||
skew: 'lpskew',
|
dcoffset: 'lpdc',
|
||||||
};
|
skew: 'lpskew',
|
||||||
const lpParams = pickAndRename(value, lpMap);
|
};
|
||||||
lpParams.type = 'lowpass';
|
const lpParams = pickAndRename(fx, lpMap);
|
||||||
const lp = () => createFilter(ac, t, end, lpParams, cps, cycle);
|
lpParams.type = 'lowpass';
|
||||||
const { filter: lpf1, lfo: lfo1 } = lp();
|
const { filter: lpf1, lfo: lfo1 } = filt(lpParams);
|
||||||
chain.push(lpf1);
|
fxNodes['lpf'] = [lpf1];
|
||||||
lfo1 && audioNodes.push(lfo1);
|
fxNodes['lpf_lfo'] = [lfo1];
|
||||||
if (ftype === '24db') {
|
chain.connect(lpf1);
|
||||||
const { filter: lpf2, lfo: lfo2 } = lp();
|
lfo1 && chain.audioNodes.push(lfo1);
|
||||||
chain.push(lpf2);
|
if (ftype === '24db') {
|
||||||
lfo2 && audioNodes.push(lfo2);
|
const { filter: lpf2, lfo: lfo2 } = filt(lpParams);
|
||||||
|
fxNodes['lpf'].push(lpf2);
|
||||||
|
fxNodes['lpf_lfo'].push(lfo2);
|
||||||
|
chain.connect(lpf2);
|
||||||
|
lfo2 && chain.audioNodes.push(lfo2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fx.hcutoff !== undefined) {
|
||||||
|
const hpMap = {
|
||||||
|
frequency: 'hcutoff',
|
||||||
|
q: 'hresonance',
|
||||||
|
attack: 'hpattack',
|
||||||
|
decay: 'hpdecay',
|
||||||
|
sustain: 'hpsustain',
|
||||||
|
release: 'hprelease',
|
||||||
|
env: 'hpenv',
|
||||||
|
anchor: 'fanchor',
|
||||||
|
model: 'ftype',
|
||||||
|
drive: 'drive',
|
||||||
|
rate: 'hprate',
|
||||||
|
sync: 'hpsync',
|
||||||
|
depth: 'hpdepth',
|
||||||
|
depthfrequency: 'hpdepthfrequency',
|
||||||
|
shape: 'hpshape',
|
||||||
|
dcoffset: 'hpdc',
|
||||||
|
skew: 'hpskew',
|
||||||
|
};
|
||||||
|
const hpParams = pickAndRename(fx, hpMap);
|
||||||
|
hpParams.type = 'highpass';
|
||||||
|
const { filter: hpf1, lfo: lfo1 } = filt(hpParams);
|
||||||
|
fxNodes['hpf'] = [hpf1];
|
||||||
|
fxNodes['hpf_lfo'] = [lfo1];
|
||||||
|
lfo1 && chain.audioNodes.push(lfo1);
|
||||||
|
chain.connect(hpf1);
|
||||||
|
if (ftype === '24db') {
|
||||||
|
const { filter: hpf2, lfo: lfo2 } = filt(hpParams);
|
||||||
|
fxNodes['hpf'].push(hpf2);
|
||||||
|
fxNodes['hpf_lfo'].push(lfo2);
|
||||||
|
chain.connect(hpf2);
|
||||||
|
lfo2 && chain.audioNodes.push(lfo2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fx.bandf !== undefined) {
|
||||||
|
const bpMap = {
|
||||||
|
frequency: 'bandf',
|
||||||
|
q: 'bandq',
|
||||||
|
attack: 'bpattack',
|
||||||
|
decay: 'bpdecay',
|
||||||
|
sustain: 'bpsustain',
|
||||||
|
release: 'bprelease',
|
||||||
|
env: 'bpenv',
|
||||||
|
anchor: 'fanchor',
|
||||||
|
model: 'ftype',
|
||||||
|
drive: 'drive',
|
||||||
|
rate: 'bprate',
|
||||||
|
sync: 'bpsync',
|
||||||
|
depth: 'bpdepth',
|
||||||
|
depthfrequency: 'bpdepthfrequency',
|
||||||
|
shape: 'bpshape',
|
||||||
|
dcoffset: 'bpdc',
|
||||||
|
skew: 'bpskew',
|
||||||
|
};
|
||||||
|
const bpParams = pickAndRename(fx, bpMap);
|
||||||
|
bpParams.type = 'bandpass';
|
||||||
|
const { filter: bpf1, lfo: lfo1 } = filt(bpParams);
|
||||||
|
fxNodes['bpf'] = [bpf1];
|
||||||
|
fxNodes['bpf_lfo'] = [lfo1];
|
||||||
|
chain.connect(bpf1);
|
||||||
|
lfo1 && chain.audioNodes.push(lfo1);
|
||||||
|
if (ftype === '24db') {
|
||||||
|
const { filter: bpf2, lfo: lfo2 } = filt(bpParams);
|
||||||
|
fxNodes['bpf'].push(bpf2);
|
||||||
|
fxNodes['bpf_lfo'].push(lfo2);
|
||||||
|
chain.connect(bpf2);
|
||||||
|
lfo2 && chain.audioNodes.push(lfo2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fx.vowel !== undefined) {
|
||||||
|
const vowelNode = ac.createVowelFilter(fx.vowel);
|
||||||
|
fxNodes['vowel'] = vowelNode.filters;
|
||||||
|
chain.connect(vowelNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
// effects
|
||||||
|
if (fx.coarse !== undefined) {
|
||||||
|
const coarseNode = getWorklet(ac, 'coarse-processor', { coarse: fx.coarse });
|
||||||
|
fxNodes['coarse'] = [coarseNode];
|
||||||
|
chain.connect(coarseNode);
|
||||||
|
}
|
||||||
|
if (fx.crush !== undefined) {
|
||||||
|
const crushNode = getWorklet(ac, 'crush-processor', { crush: fx.crush });
|
||||||
|
fxNodes['crush'] = [crushNode];
|
||||||
|
chain.connect(crushNode);
|
||||||
|
}
|
||||||
|
if (fx.shape !== undefined) {
|
||||||
|
const shapeNode = getWorklet(ac, 'shape-processor', { shape: fx.shape, postgain: shapevol });
|
||||||
|
fxNodes['shape'] = [shapeNode];
|
||||||
|
chain.connect(shapeNode);
|
||||||
|
}
|
||||||
|
if (fx.distort !== undefined) {
|
||||||
|
const distortNode = getDistortion(fx.distort, distortvol, distorttype);
|
||||||
|
fxNodes['distort'] = [distortNode];
|
||||||
|
chain.connect(distortNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
let tremolo = fx.tremolo;
|
||||||
|
if (fx.tremolosync != null) {
|
||||||
|
tremolo = cps * fx.tremolosync;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tremolo !== undefined) {
|
||||||
|
// Allow clipping of modulator for more dynamic possiblities, and to prevent speaker overload
|
||||||
|
// EX: a triangle waveform will clip like this /-\ when the depth is above 1
|
||||||
|
const gain = Math.max(1 - tremolodepth, 0);
|
||||||
|
const amGain = new GainNode(ac, { gain });
|
||||||
|
|
||||||
|
const time = cycle / cps;
|
||||||
|
const lfo = getLfo(ac, {
|
||||||
|
skew: fx.tremoloskew ?? (fx.tremoloshape != null ? 0.5 : 1),
|
||||||
|
frequency: tremolo,
|
||||||
|
depth: tremolodepth,
|
||||||
|
time,
|
||||||
|
dcoffset: 0,
|
||||||
|
shape: fx.tremoloshape,
|
||||||
|
phaseoffset: fx.tremolophase,
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
curve: 1.5,
|
||||||
|
begin: t,
|
||||||
|
end: endWithRelease,
|
||||||
|
});
|
||||||
|
fxNodes['tremolo'] = [lfo];
|
||||||
|
fxNodes['tremolo_gain'] = [amGain];
|
||||||
|
lfo.connect(amGain.gain);
|
||||||
|
chain.audioNodes.push(lfo);
|
||||||
|
chain.connect(amGain);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fx.compressor !== undefined) {
|
||||||
|
const compressorNode = getCompressor(
|
||||||
|
ac,
|
||||||
|
fx.compressor,
|
||||||
|
fx.compressorRatio,
|
||||||
|
fx.compressorKnee,
|
||||||
|
fx.compressorAttack,
|
||||||
|
fx.compressorRelease,
|
||||||
|
);
|
||||||
|
fxNodes['compressor'] = [compressorNode];
|
||||||
|
chain.connect(compressorNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
// panning
|
||||||
|
if (fx.pan !== undefined) {
|
||||||
|
const panner = ac.createStereoPanner();
|
||||||
|
fxNodes['pan'] = [panner];
|
||||||
|
panner.pan.value = 2 * fx.pan - 1;
|
||||||
|
chain.connect(panner);
|
||||||
|
}
|
||||||
|
// phaser
|
||||||
|
if (fx.phaserrate !== undefined && phaserdepth > 0) {
|
||||||
|
const { filterChain, lfo } = getPhaser(
|
||||||
|
t,
|
||||||
|
endWithRelease,
|
||||||
|
fx.phaserrate,
|
||||||
|
phaserdepth,
|
||||||
|
fx.phasercenter,
|
||||||
|
fx.phasersweep,
|
||||||
|
);
|
||||||
|
fxNodes['phaser'] = [...filterChain];
|
||||||
|
fxNodes['phaser_lfo'] = [lfo];
|
||||||
|
filterChain.forEach((f) => chain.connect(f));
|
||||||
|
chain.audioNodes.push(lfo);
|
||||||
|
}
|
||||||
|
// delay
|
||||||
|
if (key !== 'main' && delay > 0 && delaytime > 0 && delayfeedback > 0) {
|
||||||
|
const dry = gainNode(1);
|
||||||
|
delayfeedback = clamp(delayfeedback, 0, 0.98);
|
||||||
|
const delayNode = ac.createFeedbackDelay(1, delaytime, delayfeedback);
|
||||||
|
const wetDelay = gainNode(delay);
|
||||||
|
const dryDelay = gainNode(fx.dry ?? 1);
|
||||||
|
const sum = new GainNode(ac, { gain: 1, channelCount: 2, channelCountMode: 'explicit' });
|
||||||
|
chain
|
||||||
|
.connect(dry)
|
||||||
|
.connect(dryDelay, delayNode)
|
||||||
|
.connectOne(1, wetDelay) // connect delayNode -> wetDelay
|
||||||
|
.connect(sum);
|
||||||
|
chain.audioNodes.push(delayNode.feedbackGain, delayNode.delayGain);
|
||||||
|
fxNodes['delay'] = [delayNode];
|
||||||
|
fxNodes['delay_mix'] = [wetDelay];
|
||||||
|
}
|
||||||
|
// reverb
|
||||||
|
if (key !== 'main' && fx.room > 0) {
|
||||||
|
let roomIR;
|
||||||
|
if (fx.ir !== undefined) {
|
||||||
|
let url;
|
||||||
|
let sample = getSound(fx.ir);
|
||||||
|
if (Array.isArray(sample)) {
|
||||||
|
url = sample.data.samples[fx.i % sample.data.samples.length];
|
||||||
|
} else if (typeof sample === 'object') {
|
||||||
|
url = Object.values(sample.data.samples).flat()[i % Object.values(sample.data.samples).length];
|
||||||
|
}
|
||||||
|
roomIR = await loadBuffer(url, ac, fx.ir, 0);
|
||||||
|
}
|
||||||
|
const dry = gainNode(1);
|
||||||
|
const reverbNode = ac.createReverb(
|
||||||
|
fx.roomsize,
|
||||||
|
fx.roomfade,
|
||||||
|
fx.roomlp,
|
||||||
|
fx.roomdim,
|
||||||
|
roomIR,
|
||||||
|
fx.irspeed,
|
||||||
|
fx.irbegin,
|
||||||
|
);
|
||||||
|
const wetReverb = gainNode(fx.room);
|
||||||
|
const dryReverb = gainNode(fx.dry ?? 1);
|
||||||
|
const sum = new GainNode(ac, { gain: 1, channelCount: 2, channelCountMode: 'explicit' });
|
||||||
|
chain
|
||||||
|
.connect(dry)
|
||||||
|
.connect(dryReverb, reverbNode)
|
||||||
|
.connectOne(1, wetReverb) // connect reverbNode -> wetReverb
|
||||||
|
.connect(sum);
|
||||||
|
fxNodes['room'] = [reverbNode];
|
||||||
|
fxNodes['room_mix'] = [wetReverb];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (value.hcutoff !== undefined) {
|
if (FXrelease !== undefined && FXrelease > release) {
|
||||||
const hpMap = {
|
const releaseNode = gainNode(1);
|
||||||
frequency: 'hcutoff',
|
releaseNode.gain.setValueAtTime(1, end + release);
|
||||||
q: 'hresonance',
|
releaseNode.gain.linearRampToValueAtTime(0, endWithRelease);
|
||||||
attack: 'hpattack',
|
chain.connect(releaseNode);
|
||||||
decay: 'hpdecay',
|
|
||||||
sustain: 'hpsustain',
|
|
||||||
release: 'hprelease',
|
|
||||||
env: 'hpenv',
|
|
||||||
anchor: 'fanchor',
|
|
||||||
model: 'ftype',
|
|
||||||
drive: 'drive',
|
|
||||||
rate: 'hprate',
|
|
||||||
sync: 'hpsync',
|
|
||||||
depth: 'hpdepth',
|
|
||||||
depthfrequency: 'hpdepthfrequency',
|
|
||||||
shape: 'hpshape',
|
|
||||||
dcoffset: 'hpdc',
|
|
||||||
skew: 'hpskew',
|
|
||||||
};
|
|
||||||
const hpParams = pickAndRename(value, hpMap);
|
|
||||||
hpParams.type = 'highpass';
|
|
||||||
const hp = () => createFilter(ac, t, end, hpParams, cps, cycle);
|
|
||||||
const { filter: hpf1, lfo: lfo1 } = hp();
|
|
||||||
chain.push(hpf1);
|
|
||||||
lfo1 && audioNodes.push(lfo1);
|
|
||||||
if (ftype === '24db') {
|
|
||||||
const { filter: hpf2, lfo: lfo2 } = hp();
|
|
||||||
chain.push(hpf2);
|
|
||||||
lfo2 && audioNodes.push(lfo2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value.bandf !== undefined) {
|
|
||||||
const bpMap = {
|
|
||||||
frequency: 'bandf',
|
|
||||||
q: 'bandq',
|
|
||||||
attack: 'bpattack',
|
|
||||||
decay: 'bpdecay',
|
|
||||||
sustain: 'bpsustain',
|
|
||||||
release: 'bprelease',
|
|
||||||
env: 'bpenv',
|
|
||||||
anchor: 'fanchor',
|
|
||||||
model: 'ftype',
|
|
||||||
drive: 'drive',
|
|
||||||
rate: 'bprate',
|
|
||||||
sync: 'bpsync',
|
|
||||||
depth: 'bpdepth',
|
|
||||||
depthfrequency: 'bpdepthfrequency',
|
|
||||||
shape: 'bpshape',
|
|
||||||
dcoffset: 'bpdc',
|
|
||||||
skew: 'bpskew',
|
|
||||||
};
|
|
||||||
const bpParams = pickAndRename(value, bpMap);
|
|
||||||
bpParams.type = 'bandpass';
|
|
||||||
const bp = () => createFilter(ac, t, end, bpParams, cps, cycle);
|
|
||||||
const { filter: bpf1, lfo: lfo1 } = bp();
|
|
||||||
chain.push(bpf1);
|
|
||||||
lfo1 && audioNodes.push(lfo1);
|
|
||||||
if (ftype === '24db') {
|
|
||||||
const { filter: bpf2, lfo: lfo2 } = bp();
|
|
||||||
chain.push(bpf2);
|
|
||||||
lfo2 && audioNodes.push(lfo2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (vowel !== undefined) {
|
|
||||||
const vowelFilter = ac.createVowelFilter(vowel);
|
|
||||||
chain.push(vowelFilter);
|
|
||||||
}
|
|
||||||
|
|
||||||
// effects
|
|
||||||
coarse !== undefined && chain.push(getWorklet(ac, 'coarse-processor', { coarse }));
|
|
||||||
crush !== undefined && chain.push(getWorklet(ac, 'crush-processor', { crush }));
|
|
||||||
shape !== undefined && chain.push(getWorklet(ac, 'shape-processor', { shape, postgain: shapevol }));
|
|
||||||
distort !== undefined && chain.push(getDistortion(distort, distortvol, distorttype));
|
|
||||||
|
|
||||||
if (tremolosync != null) {
|
|
||||||
tremolo = cps * tremolosync;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value.wtPosSynced != null) {
|
|
||||||
value.wtPosRate /= cps;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value.wtWarpSynced != null) {
|
|
||||||
value.wtWarpRate /= cps;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tremolo !== undefined) {
|
|
||||||
// Allow clipping of modulator for more dynamic possiblities, and to prevent speaker overload
|
|
||||||
// EX: a triangle waveform will clip like this /-\ when the depth is above 1
|
|
||||||
const gain = Math.max(1 - tremolodepth, 0);
|
|
||||||
const amGain = new GainNode(ac, { gain });
|
|
||||||
|
|
||||||
const time = cycle / cps;
|
|
||||||
const lfo = getLfo(ac, t, endWithRelease, {
|
|
||||||
skew: tremoloskew ?? (tremoloshape != null ? 0.5 : 1),
|
|
||||||
frequency: tremolo,
|
|
||||||
depth: tremolodepth,
|
|
||||||
time,
|
|
||||||
dcoffset: 0,
|
|
||||||
shape: tremoloshape,
|
|
||||||
phaseoffset: tremolophase,
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
curve: 1.5,
|
|
||||||
});
|
|
||||||
lfo.connect(amGain.gain);
|
|
||||||
audioNodes.push(lfo);
|
|
||||||
chain.push(amGain);
|
|
||||||
}
|
|
||||||
|
|
||||||
compressorThreshold !== undefined &&
|
|
||||||
chain.push(
|
|
||||||
getCompressor(ac, compressorThreshold, compressorRatio, compressorKnee, compressorAttack, compressorRelease),
|
|
||||||
);
|
|
||||||
|
|
||||||
// panning
|
|
||||||
if (pan !== undefined) {
|
|
||||||
const panner = ac.createStereoPanner();
|
|
||||||
panner.pan.value = 2 * pan - 1;
|
|
||||||
chain.push(panner);
|
|
||||||
}
|
|
||||||
// phaser
|
|
||||||
if (phaserrate !== undefined && phaserdepth > 0) {
|
|
||||||
const { filterChain, lfo } = getPhaser(t, endWithRelease, phaserrate, phaserdepth, phasercenter, phasersweep);
|
|
||||||
audioNodes.push(lfo);
|
|
||||||
chain.push(...filterChain);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// last gain
|
// last gain
|
||||||
const post = new GainNode(ac, { gain: postgain });
|
const post = new GainNode(ac, { gain: postgain });
|
||||||
chain.push(post);
|
nodes.main['post'] = [post];
|
||||||
|
chain.connect(post);
|
||||||
|
|
||||||
// delay
|
// delay
|
||||||
if (delay > 0 && delaytime > 0 && delayfeedback > 0) {
|
if (delay > 0 && delaytime > 0 && delayfeedback > 0) {
|
||||||
orbitBus.getDelay(delaytime, delayfeedback, t);
|
const delayNode = orbitBus.getDelay(delaytime, delayfeedback, t);
|
||||||
const send = orbitBus.sendDelay(post, delay);
|
nodes.main['delay'] = [delayNode];
|
||||||
audioNodes.push(send);
|
const delaySend = orbitBus.sendDelay(post, delay);
|
||||||
|
nodes.main['delay_mix'] = [delaySend];
|
||||||
|
chain.audioNodes.push(delaySend);
|
||||||
}
|
}
|
||||||
// reverb
|
// reverb
|
||||||
if (room > 0) {
|
if (room > 0) {
|
||||||
@@ -750,33 +931,84 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
|||||||
}
|
}
|
||||||
roomIR = await loadBuffer(url, ac, ir, 0);
|
roomIR = await loadBuffer(url, ac, ir, 0);
|
||||||
}
|
}
|
||||||
orbitBus.getReverb(roomsize, roomfade, roomlp, roomdim, roomIR, irspeed, irbegin);
|
const roomNode = orbitBus.getReverb(roomsize, roomfade, roomlp, roomdim, roomIR, irspeed, irbegin);
|
||||||
const send = orbitBus.sendReverb(post, room);
|
nodes.main['room'] = [roomNode];
|
||||||
audioNodes.push(send);
|
const reverbSend = orbitBus.sendReverb(post, room);
|
||||||
|
nodes.main['room_mix'] = [reverbSend];
|
||||||
|
chain.audioNodes.push(reverbSend);
|
||||||
|
}
|
||||||
|
if (bus != null) {
|
||||||
|
const busNode = audioController.getBus(bus);
|
||||||
|
const busSend = effectSend(post, busNode, busgain);
|
||||||
|
chain.audioNodes.push(busSend);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (djf != null) {
|
if (djf != null) {
|
||||||
orbitBus.getDjf(djf, t);
|
const djfNode = orbitBus.getDjf(djf, t);
|
||||||
|
nodes.main['djf'] = [djfNode];
|
||||||
}
|
}
|
||||||
|
|
||||||
// analyser
|
// analyser
|
||||||
if (analyze) {
|
if (analyze && !(ac instanceof OfflineAudioContext)) {
|
||||||
const analyserNode = getAnalyserById(analyze, 2 ** (fft + 5));
|
const analyserNode = getAnalyserById(analyze, 2 ** (fft + 5));
|
||||||
const analyserSend = effectSend(post, analyserNode, 1);
|
const analyserSend = effectSend(post, analyserNode, 1);
|
||||||
audioNodes.push(analyserSend);
|
chain.audioNodes.push(analyserSend);
|
||||||
}
|
}
|
||||||
if (dry != null) {
|
if (dry != null) {
|
||||||
dry = applyGainCurve(dry);
|
dry = applyGainCurve(dry);
|
||||||
const dryGain = new GainNode(ac, { gain: dry });
|
const dryGain = new GainNode(ac, { gain: dry });
|
||||||
chain.push(dryGain);
|
chain.connect(dryGain);
|
||||||
orbitBus.connectToOutput(dryGain);
|
orbitBus.connectToOutput(dryGain);
|
||||||
} else {
|
} else {
|
||||||
orbitBus.connectToOutput(post);
|
orbitBus.connectToOutput(post);
|
||||||
}
|
}
|
||||||
|
|
||||||
// connect chain elements together
|
// finally, now that `nodes` is populated, set up modulators
|
||||||
chain.slice(1).reduce((last, current) => last.connect(current), chain[0]);
|
FX.forEach((fx, idx) => {
|
||||||
audioNodes = audioNodes.concat(chain);
|
const key = idx === FX.length - 1 ? 'main' : idx;
|
||||||
|
if (fx.lfo) {
|
||||||
|
for (const id of fx.lfo.__ids) {
|
||||||
|
const params = fx.lfo[id];
|
||||||
|
params.fxi ??= key;
|
||||||
|
const lfo = connectLFO(
|
||||||
|
id,
|
||||||
|
{
|
||||||
|
...params,
|
||||||
|
cps,
|
||||||
|
cycle,
|
||||||
|
begin: t,
|
||||||
|
end: endWithRelease,
|
||||||
|
},
|
||||||
|
nodes,
|
||||||
|
);
|
||||||
|
lfo && chain.audioNodes.push(lfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (fx.env) {
|
||||||
|
for (const id of fx.env.__ids) {
|
||||||
|
const params = fx.env[id];
|
||||||
|
params.fxi ??= key;
|
||||||
|
const env = connectEnvelope(
|
||||||
|
id,
|
||||||
|
{
|
||||||
|
...params,
|
||||||
|
begin: t,
|
||||||
|
end: endWithRelease,
|
||||||
|
},
|
||||||
|
nodes,
|
||||||
|
);
|
||||||
|
env && chain.audioNodes.push(env);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (fx.bmod) {
|
||||||
|
for (const id of fx.bmod.__ids) {
|
||||||
|
const params = fx.bmod[id];
|
||||||
|
params.fxi ??= key;
|
||||||
|
const { toCleanup } = connectBusModulator({ ...params, begin: t, end: endWithRelease }, nodes, controller);
|
||||||
|
chain.audioNodes.push(...toCleanup);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const superdoughTrigger = (t, hap, ct, cps) => {
|
export const superdoughTrigger = (t, hap, ct, cps) => {
|
||||||
|
|||||||
@@ -0,0 +1,150 @@
|
|||||||
|
/*
|
||||||
|
superdoughdata.mjs - Data needed for running superdough (defaults, mappings, etc.)
|
||||||
|
Copyright (C) 2025 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/superdough/superdoughdata.mjs>
|
||||||
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Mapping from control name to webaudio node and parameter
|
||||||
|
const CONTROL_TARGETS = {
|
||||||
|
stretch: { node: 'stretch', param: 'pitchFactor' },
|
||||||
|
gain: { node: 'gain', param: 'gain' },
|
||||||
|
postgain: { node: 'post', param: 'gain' },
|
||||||
|
pan: { node: 'pan', param: 'pan' },
|
||||||
|
tremolo: { node: 'tremolo', param: 'frequency' },
|
||||||
|
tremolosync: { node: 'tremolo', param: 'frequency' },
|
||||||
|
tremolodepth: { node: 'tremolo_gain', param: 'gain' },
|
||||||
|
tremoloskew: { node: 'tremolo', param: 'skew' },
|
||||||
|
tremolophase: { node: 'tremolo', param: 'phase' },
|
||||||
|
tremoloshape: { node: 'tremolo', param: 'shape' },
|
||||||
|
|
||||||
|
// MODULATORS
|
||||||
|
lfo: { node: 'lfo', param: 'frequency' },
|
||||||
|
lfo_rate: { node: 'lfo', param: 'frequency' },
|
||||||
|
lfo_sync: { node: 'lfo', param: 'frequency' },
|
||||||
|
lfo_depth: { node: 'lfo', param: 'depth' },
|
||||||
|
lfo_depthabs: { node: 'lfo', param: 'depth' },
|
||||||
|
lfo_skew: { node: 'lfo', param: 'skew' },
|
||||||
|
lfo_curve: { node: 'lfo', param: 'curve' },
|
||||||
|
lfo_dcoffset: { node: 'lfo', param: 'dcoffset' },
|
||||||
|
env: { node: 'env', param: 'depth' },
|
||||||
|
env_attack: { node: 'env', param: 'attack' },
|
||||||
|
env_decay: { node: 'env', param: 'decay' },
|
||||||
|
env_sustain: { node: 'env', param: 'sustain' },
|
||||||
|
env_release: { node: 'env', param: 'release' },
|
||||||
|
bmod: { node: 'bmod', param: 'depth' },
|
||||||
|
bmod_depth: { node: 'bmod', param: 'depth' },
|
||||||
|
bmod_depthabs: { node: 'bmod', param: 'depth' },
|
||||||
|
|
||||||
|
// LPF
|
||||||
|
cutoff: { node: 'lpf', param: 'frequency' },
|
||||||
|
resonance: { node: 'lpf', param: 'Q' },
|
||||||
|
lprate: { node: 'lpf_lfo', param: 'rate' },
|
||||||
|
lpsync: { node: 'lpf_lfo', param: 'sync' },
|
||||||
|
lpdepth: { node: 'lpf_lfo', param: 'depth' },
|
||||||
|
lpdepthfrequency: { node: 'lpf_lfo', param: 'depth' },
|
||||||
|
lpshape: { node: 'lpf_lfo', param: 'shape' },
|
||||||
|
lpdc: { node: 'lpf_lfo', param: 'dcoffset' },
|
||||||
|
lpskew: { node: 'lpf_lfo', param: 'skew' },
|
||||||
|
|
||||||
|
// HPF
|
||||||
|
hcutoff: { node: 'hpf', param: 'frequency' },
|
||||||
|
hresonance: { node: 'hpf', param: 'Q' },
|
||||||
|
hprate: { node: 'hpf_lfo', param: 'rate' },
|
||||||
|
hpsync: { node: 'hpf_lfo', param: 'sync' },
|
||||||
|
hpdepth: { node: 'hpf_lfo', param: 'depth' },
|
||||||
|
hpdepthfrequency: { node: 'hpf_lfo', param: 'depth' },
|
||||||
|
hpshape: { node: 'hpf_lfo', param: 'shape' },
|
||||||
|
hpdc: { node: 'hpf_lfo', param: 'dcoffset' },
|
||||||
|
hpskew: { node: 'hpf_lfo', param: 'skew' },
|
||||||
|
|
||||||
|
// BPF
|
||||||
|
bandf: { node: 'bpf', param: 'frequency' },
|
||||||
|
bandq: { node: 'bpf', param: 'Q' },
|
||||||
|
bprate: { node: 'bpf_lfo', param: 'rate' },
|
||||||
|
bpsync: { node: 'bpf_lfo', param: 'sync' },
|
||||||
|
bpdepth: { node: 'bpf_lfo', param: 'depth' },
|
||||||
|
bpdepthfrequency: { node: 'bpf_lfo', param: 'depth' },
|
||||||
|
bpshape: { node: 'bpf_lfo', param: 'shape' },
|
||||||
|
bpdc: { node: 'bpf_lfo', param: 'dcoffset' },
|
||||||
|
bpskew: { node: 'bpf_lfo', param: 'skew' },
|
||||||
|
|
||||||
|
vowel: { node: 'vowel', param: 'frequency' },
|
||||||
|
|
||||||
|
// DISTORTION
|
||||||
|
coarse: { node: 'coarse', param: 'coarse' },
|
||||||
|
crush: { node: 'crush', param: 'crush' },
|
||||||
|
shape: { node: 'shape', param: 'shape' },
|
||||||
|
shapevol: { node: 'shape', param: 'postgain' },
|
||||||
|
distort: { node: 'distort', param: 'distort' },
|
||||||
|
distortvol: { node: 'distort', param: 'postgain' },
|
||||||
|
distorttype: { node: 'distort', param: 'distort' },
|
||||||
|
|
||||||
|
// COMPRESSOR
|
||||||
|
compressor: { node: 'compressor', param: 'threshold' },
|
||||||
|
compressorRatio: { node: 'compressor', param: 'ratio' },
|
||||||
|
compressorKnee: { node: 'compressor', param: 'knee' },
|
||||||
|
compressorAttack: { node: 'compressor', param: 'attack' },
|
||||||
|
compressorRelease: { node: 'compressor', param: 'release' },
|
||||||
|
|
||||||
|
// PHASER
|
||||||
|
phaserrate: { node: 'phaser_lfo', param: 'frequency' },
|
||||||
|
phasersweep: { node: 'phaser_lfo', param: 'depth' },
|
||||||
|
phasercenter: { node: 'phaser', param: 'frequency' },
|
||||||
|
phaserdepth: { node: 'phaser', param: 'Q' },
|
||||||
|
|
||||||
|
// ORBIT EFFECTS
|
||||||
|
delay: { node: 'delay_mix', param: 'gain' },
|
||||||
|
delaytime: { node: 'delay', param: 'delayTime' },
|
||||||
|
delayfeedback: { node: 'delay', param: 'feedback' },
|
||||||
|
delaysync: { node: 'delay', param: 'delayTime' },
|
||||||
|
dry: { node: 'dry', param: 'gain' },
|
||||||
|
room: { node: 'room_mix', param: 'gain' },
|
||||||
|
djf: { node: 'djf', param: 'value' },
|
||||||
|
busgain: { node: 'bus', param: 'gain' },
|
||||||
|
|
||||||
|
// SYNTHS
|
||||||
|
s: { node: 'source', param: 'frequency' },
|
||||||
|
detune: { node: 'source', param: 'freqspread' },
|
||||||
|
wt: { node: 'source', param: 'position' },
|
||||||
|
warp: { node: 'source', param: 'warp' },
|
||||||
|
freq: { node: 'source', param: 'frequency' },
|
||||||
|
note: { node: 'source', param: 'frequency' },
|
||||||
|
wtdc: { node: 'wt_lfo', param: 'dc' },
|
||||||
|
wtskew: { node: 'wt_lfo', param: 'skew' },
|
||||||
|
wtrate: { node: 'wt_lfo', param: 'frequency' },
|
||||||
|
wtsync: { node: 'wt_lfo', param: 'frequency' },
|
||||||
|
wtdepth: { node: 'wt_lfo', param: 'depth' },
|
||||||
|
warpdc: { node: 'warp_lfo', param: 'dc' },
|
||||||
|
warpskew: { node: 'warp_lfo', param: 'skew' },
|
||||||
|
warprate: { node: 'warp_lfo', param: 'frequency' },
|
||||||
|
warpsync: { node: 'warp_lfo', param: 'frequency' },
|
||||||
|
warpdepth: { node: 'warp_lfo', param: 'depth' },
|
||||||
|
fmi: { node: 'fm_1_gain', param: 'gain' },
|
||||||
|
fmi2: { node: 'fm_2_gain', param: 'gain' },
|
||||||
|
fmi3: { node: 'fm_3_gain', param: 'gain' },
|
||||||
|
fmi4: { node: 'fm_4_gain', param: 'gain' },
|
||||||
|
fmi5: { node: 'fm_5_gain', param: 'gain' },
|
||||||
|
fmi6: { node: 'fm_6_gain', param: 'gain' },
|
||||||
|
fmi7: { node: 'fm_7_gain', param: 'gain' },
|
||||||
|
fmi8: { node: 'fm_8_gain', param: 'gain' },
|
||||||
|
fmh: { node: 'fm_1', param: 'frequency' },
|
||||||
|
fmh2: { node: 'fm_2', param: 'frequency' },
|
||||||
|
fmh3: { node: 'fm_3', param: 'frequency' },
|
||||||
|
fmh4: { node: 'fm_4', param: 'frequency' },
|
||||||
|
fmh5: { node: 'fm_5', param: 'frequency' },
|
||||||
|
fmh6: { node: 'fm_6', param: 'frequency' },
|
||||||
|
fmh7: { node: 'fm_7', param: 'frequency' },
|
||||||
|
fmh8: { node: 'fm_8', param: 'frequency' },
|
||||||
|
pw: { node: 'source', param: 'pulsewidth' },
|
||||||
|
pwrate: { node: 'pw_lfo', param: 'frequency' },
|
||||||
|
pwsweep: { node: 'pw_lfo', param: 'depth' },
|
||||||
|
vib: { node: 'vib', param: 'frequency' },
|
||||||
|
vibmod: { node: 'vib_gain', param: 'gain' },
|
||||||
|
byteBeatStartTime: { node: 'source', param: 'byteBeatStartTime' },
|
||||||
|
spread: { node: 'source', param: 'panspread' },
|
||||||
|
transient: { node: 'transient', param: 'attack' },
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getSuperdoughControlTargets() {
|
||||||
|
return CONTROL_TARGETS;
|
||||||
|
}
|
||||||
@@ -1,8 +1,20 @@
|
|||||||
|
/*
|
||||||
|
superdoughoutput.mjs - Output controller for superdough
|
||||||
|
|
||||||
|
Handles setting up and mixing to the outputs as well as all global (orbit) effects
|
||||||
|
|
||||||
|
Copyright (C) 2025 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/superdough/superdoughoutput.mjs>
|
||||||
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
import { effectSend, getWorklet, webAudioTimeout } from './helpers.mjs';
|
import { effectSend, getWorklet, webAudioTimeout } from './helpers.mjs';
|
||||||
import { errorLogger } from './logger.mjs';
|
import { errorLogger } from './logger.mjs';
|
||||||
import { clamp } from './util.mjs';
|
import { clamp } from './util.mjs';
|
||||||
|
|
||||||
let hasChanged = (now, before) => now !== undefined && now !== before;
|
const hasChanged = (now, before) => now !== undefined && now !== before;
|
||||||
|
// Node with fixed stereo channel count to prevent clicking when the input signal
|
||||||
|
// switches from mono to stereo
|
||||||
|
const getStereoNode = (ac) => new GainNode(ac, { gain: 1, channelCount: 2, channelCountMode: 'explicit' });
|
||||||
|
|
||||||
export class Orbit {
|
export class Orbit {
|
||||||
reverbNode;
|
reverbNode;
|
||||||
@@ -11,10 +23,11 @@ export class Orbit {
|
|||||||
summingNode;
|
summingNode;
|
||||||
djfNode;
|
djfNode;
|
||||||
audioContext;
|
audioContext;
|
||||||
|
|
||||||
constructor(audioContext) {
|
constructor(audioContext) {
|
||||||
this.audioContext = audioContext;
|
this.audioContext = audioContext;
|
||||||
this.output = new GainNode(audioContext, { gain: 1, channelCount: 2, channelCountMode: 'explicit' });
|
this.output = getStereoNode(audioContext);
|
||||||
this.summingNode = new GainNode(audioContext, { gain: 1, channelCount: 2, channelCountMode: 'explicit' });
|
this.summingNode = getStereoNode(audioContext);
|
||||||
this.summingNode.connect(this.output);
|
this.summingNode.connect(this.output);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,6 +47,7 @@ export class Orbit {
|
|||||||
}
|
}
|
||||||
const val = this.djfNode.parameters.get('value');
|
const val = this.djfNode.parameters.get('value');
|
||||||
val.setValueAtTime(value, t);
|
val.setValueAtTime(value, t);
|
||||||
|
return this.djfNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
getDelay(delaytime = 0, feedback = 0.5, t) {
|
getDelay(delaytime = 0, feedback = 0.5, t) {
|
||||||
@@ -164,6 +178,7 @@ export class SuperdoughAudioController {
|
|||||||
audioContext;
|
audioContext;
|
||||||
output;
|
output;
|
||||||
nodes = {};
|
nodes = {};
|
||||||
|
buses = {};
|
||||||
|
|
||||||
constructor(audioContext) {
|
constructor(audioContext) {
|
||||||
this.audioContext = audioContext;
|
this.audioContext = audioContext;
|
||||||
@@ -171,10 +186,14 @@ export class SuperdoughAudioController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
reset() {
|
reset() {
|
||||||
Array.from(this.nodes).forEach((node) => {
|
Object.values(this.nodes).forEach((node) => {
|
||||||
node.disconnect();
|
node.disconnect();
|
||||||
});
|
});
|
||||||
|
Object.values(this.buses).forEach((bus) => {
|
||||||
|
bus.disconnect();
|
||||||
|
});
|
||||||
this.nodes = {};
|
this.nodes = {};
|
||||||
|
this.buses = {};
|
||||||
this.output.reset();
|
this.output.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,4 +225,11 @@ export class SuperdoughAudioController {
|
|||||||
}
|
}
|
||||||
return this.nodes[orbitNum];
|
return this.nodes[orbitNum];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getBus(busNum) {
|
||||||
|
if (this.buses[busNum] == null) {
|
||||||
|
this.buses[busNum] = getStereoNode(this.audioContext);
|
||||||
|
}
|
||||||
|
return this.buses[busNum];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { clamp } from './util.mjs';
|
import { clamp } from './util.mjs';
|
||||||
import { registerSound, soundMap } from './superdough.mjs';
|
import { getSuperdoughAudioController, registerSound, soundMap } from './superdough.mjs';
|
||||||
import { getAudioContext } from './audioContext.mjs';
|
import { getAudioContext } from './audioContext.mjs';
|
||||||
import {
|
import {
|
||||||
applyFM,
|
applyFM,
|
||||||
@@ -18,8 +18,9 @@ import {
|
|||||||
} from './helpers.mjs';
|
} from './helpers.mjs';
|
||||||
import { logger } from './logger.mjs';
|
import { logger } from './logger.mjs';
|
||||||
import { getNoiseMix, getNoiseOscillator } from './noise.mjs';
|
import { getNoiseMix, getNoiseOscillator } from './noise.mjs';
|
||||||
|
import { getNodeFromPool, markWorkletAsDead, releaseNodeToPool } from './nodePools.mjs';
|
||||||
|
|
||||||
const waveforms = ['triangle', 'square', 'sawtooth', 'sine', 'user'];
|
const waveforms = ['triangle', 'square', 'sawtooth', 'sine', 'user', 'one'];
|
||||||
const waveformAliases = [
|
const waveformAliases = [
|
||||||
['tri', 'triangle'],
|
['tri', 'triangle'],
|
||||||
['sqr', 'square'],
|
['sqr', 'square'],
|
||||||
@@ -52,17 +53,17 @@ export function registerSynthSounds() {
|
|||||||
// turn down
|
// turn down
|
||||||
const g = gainNode(0.3);
|
const g = gainNode(0.3);
|
||||||
|
|
||||||
let sound = getOscillator(s, t, value, () => {
|
const sound = getOscillator(s, t, value, () => {
|
||||||
releaseAudioNode(g);
|
releaseAudioNode(g);
|
||||||
onended();
|
onended();
|
||||||
});
|
});
|
||||||
|
|
||||||
let { node: o, stop, triggerRelease } = sound;
|
const { node: o, nodes, stop, triggerRelease } = sound;
|
||||||
|
|
||||||
const { duration } = value;
|
const { duration } = value;
|
||||||
|
|
||||||
const envGain = gainNode(1);
|
const envGain = gainNode(1);
|
||||||
let node = o.connect(g).connect(envGain);
|
const node = o.connect(g).connect(envGain);
|
||||||
const holdEnd = t + duration;
|
const holdEnd = t + duration;
|
||||||
getParamADSR(node.gain, attack, decay, sustain, release, 0, 1, t, holdEnd, 'linear');
|
getParamADSR(node.gain, attack, decay, sustain, release, 0, 1, t, holdEnd, 'linear');
|
||||||
const envEnd = holdEnd + release + 0.01;
|
const envEnd = holdEnd + release + 0.01;
|
||||||
@@ -70,6 +71,7 @@ export function registerSynthSounds() {
|
|||||||
stop(envEnd);
|
stop(envEnd);
|
||||||
return {
|
return {
|
||||||
node,
|
node,
|
||||||
|
nodes,
|
||||||
stop: (endTime) => {
|
stop: (endTime) => {
|
||||||
stop(endTime);
|
stop(endTime);
|
||||||
},
|
},
|
||||||
@@ -139,6 +141,7 @@ export function registerSynthSounds() {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
node,
|
node,
|
||||||
|
nodes: { source: [o] },
|
||||||
stop: (endTime) => {
|
stop: (endTime) => {
|
||||||
o.stop(endTime);
|
o.stop(endTime);
|
||||||
},
|
},
|
||||||
@@ -165,26 +168,31 @@ export function registerSynthSounds() {
|
|||||||
const end = holdend + release + 0.01;
|
const end = holdend + release + 0.01;
|
||||||
const voices = clamp(unison, 1, 100);
|
const voices = clamp(unison, 1, 100);
|
||||||
let panspread = voices > 1 ? clamp(spread, 0, 1) : 0;
|
let panspread = voices > 1 ? clamp(spread, 0, 1) : 0;
|
||||||
let o = getWorklet(
|
const params = {
|
||||||
ac,
|
frequency,
|
||||||
'supersaw-oscillator',
|
begin,
|
||||||
{
|
end,
|
||||||
frequency,
|
freqspread: detune,
|
||||||
begin,
|
voices,
|
||||||
end,
|
panspread,
|
||||||
freqspread: detune,
|
};
|
||||||
voices,
|
const factory = () => new AudioWorkletNode(ac, 'supersaw-oscillator', { outputChannelCount: [2] });
|
||||||
panspread,
|
const o = getNodeFromPool('supersaw', factory);
|
||||||
},
|
const now = ac.currentTime;
|
||||||
{
|
Object.entries(params).forEach(([key, value]) => {
|
||||||
outputChannelCount: [2],
|
const param = o.parameters.get(key);
|
||||||
},
|
const target = value !== undefined ? value : param.defaultValue;
|
||||||
);
|
param.setValueAtTime(target, now);
|
||||||
|
});
|
||||||
|
o.port.postMessage({ type: 'initialize' });
|
||||||
|
o.port.onmessage = (e) => {
|
||||||
|
if (e.data.type === 'died') markWorkletAsDead(o);
|
||||||
|
o.port.onmessage = null;
|
||||||
|
};
|
||||||
const gainAdjustment = 1 / Math.sqrt(voices);
|
const gainAdjustment = 1 / Math.sqrt(voices);
|
||||||
getPitchEnvelope(o.parameters.get('detune'), value, begin, holdend);
|
getPitchEnvelope(o.parameters.get('detune'), value, begin, holdend);
|
||||||
const vibratoOscillator = getVibratoOscillator(o.parameters.get('detune'), value, begin);
|
const vibratoHandle = getVibratoOscillator(o.parameters.get('detune'), value, begin);
|
||||||
const fm = applyFM(o.parameters.get('frequency'), value, begin);
|
const fmHandle = applyFM(o.parameters.get('frequency'), value, begin);
|
||||||
let envGain = gainNode(1);
|
let envGain = gainNode(1);
|
||||||
envGain = o.connect(envGain);
|
envGain = o.connect(envGain);
|
||||||
|
|
||||||
@@ -193,10 +201,10 @@ export function registerSynthSounds() {
|
|||||||
let timeoutNode = webAudioTimeout(
|
let timeoutNode = webAudioTimeout(
|
||||||
ac,
|
ac,
|
||||||
() => {
|
() => {
|
||||||
releaseAudioNode(o);
|
releaseNodeToPool(o);
|
||||||
onended();
|
onended();
|
||||||
fm?.stop();
|
fmHandle?.stop();
|
||||||
vibratoOscillator?.stop();
|
vibratoHandle?.stop();
|
||||||
},
|
},
|
||||||
begin,
|
begin,
|
||||||
end,
|
end,
|
||||||
@@ -204,6 +212,7 @@ export function registerSynthSounds() {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
node: envGain,
|
node: envGain,
|
||||||
|
nodes: { source: [o], ...fmHandle?.nodes, ...vibratoHandle?.nodes },
|
||||||
stop: (time) => {
|
stop: (time) => {
|
||||||
timeoutNode.stop(time);
|
timeoutNode.stop(time);
|
||||||
},
|
},
|
||||||
@@ -279,6 +288,7 @@ export function registerSynthSounds() {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
node: envGain,
|
node: envGain,
|
||||||
|
source: o,
|
||||||
stop: (time) => {
|
stop: (time) => {
|
||||||
timeoutNode.stop(time);
|
timeoutNode.stop(time);
|
||||||
},
|
},
|
||||||
@@ -329,25 +339,25 @@ export function registerSynthSounds() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
getPitchEnvelope(o.parameters.get('detune'), value, begin, holdend);
|
getPitchEnvelope(o.parameters.get('detune'), value, begin, holdend);
|
||||||
const vibratoOscillator = getVibratoOscillator(o.parameters.get('detune'), value, begin);
|
const vibratoHandle = getVibratoOscillator(o.parameters.get('detune'), value, begin);
|
||||||
const fm = applyFM(o.parameters.get('frequency'), value, begin);
|
const fmHandle = applyFM(o.parameters.get('frequency'), value, begin);
|
||||||
let envGain = gainNode(1);
|
let envGain = gainNode(1);
|
||||||
envGain = o.connect(envGain);
|
envGain = o.connect(envGain);
|
||||||
|
|
||||||
getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 1, begin, holdend, 'linear');
|
getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 1, begin, holdend, 'linear');
|
||||||
let lfo;
|
let pw_lfo;
|
||||||
if (pwsweep != 0) {
|
if (pwsweep != 0) {
|
||||||
lfo = getLfo(ac, begin, end, { frequency: pwrate, depth: pwsweep });
|
pw_lfo = getLfo(ac, { frequency: pwrate, depth: pwsweep, begin, end });
|
||||||
lfo.connect(o.parameters.get('pulsewidth'));
|
pw_lfo.connect(o.parameters.get('pulsewidth'));
|
||||||
}
|
}
|
||||||
let timeoutNode = webAudioTimeout(
|
let timeoutNode = webAudioTimeout(
|
||||||
ac,
|
ac,
|
||||||
() => {
|
() => {
|
||||||
releaseAudioNode(o);
|
releaseAudioNode(o);
|
||||||
releaseAudioNode(lfo);
|
releaseAudioNode(pw_lfo);
|
||||||
onended();
|
onended();
|
||||||
fm?.stop();
|
fmHandle?.stop();
|
||||||
vibratoOscillator?.stop();
|
vibratoHandle?.stop();
|
||||||
},
|
},
|
||||||
begin,
|
begin,
|
||||||
end,
|
end,
|
||||||
@@ -355,6 +365,7 @@ export function registerSynthSounds() {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
node: envGain,
|
node: envGain,
|
||||||
|
nodes: { source: [o], pw_lfo: [pw_lfo], ...fmHandle?.nodes, ...vibratoHandle?.nodes },
|
||||||
stop: (time) => {
|
stop: (time) => {
|
||||||
timeoutNode.stop(time);
|
timeoutNode.stop(time);
|
||||||
},
|
},
|
||||||
@@ -363,6 +374,41 @@ export function registerSynthSounds() {
|
|||||||
{ prebake: true, type: 'synth' },
|
{ prebake: true, type: 'synth' },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
registerSound(
|
||||||
|
'bus',
|
||||||
|
(begin, value, onended) => {
|
||||||
|
const ac = getAudioContext();
|
||||||
|
const [attack, decay, sustain, release] = getADSRValues(
|
||||||
|
[value.attack, value.decay, value.sustain, value.release],
|
||||||
|
'linear',
|
||||||
|
[0.001, 0.05, 1, 0.01],
|
||||||
|
);
|
||||||
|
const holdend = begin + value.duration;
|
||||||
|
const end = holdend + release + 0.01;
|
||||||
|
const bus = getSuperdoughAudioController().getBus(value.n ?? 0);
|
||||||
|
const envGain = bus.connect(gainNode(0));
|
||||||
|
getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 1, begin, holdend, 'linear');
|
||||||
|
const timeoutNode = webAudioTimeout(
|
||||||
|
ac,
|
||||||
|
() => {
|
||||||
|
bus.disconnect(envGain);
|
||||||
|
onended();
|
||||||
|
},
|
||||||
|
begin,
|
||||||
|
end,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
node: envGain,
|
||||||
|
nodes: { source: [bus] },
|
||||||
|
stop: (time) => {
|
||||||
|
timeoutNode.stop(time);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{ prebake: true, type: 'input' },
|
||||||
|
);
|
||||||
|
|
||||||
[...noises].forEach((s) => {
|
[...noises].forEach((s) => {
|
||||||
registerSound(
|
registerSound(
|
||||||
s,
|
s,
|
||||||
@@ -400,6 +446,7 @@ export function registerSynthSounds() {
|
|||||||
stop(envEnd);
|
stop(envEnd);
|
||||||
return {
|
return {
|
||||||
node,
|
node,
|
||||||
|
nodes: { source: [o] },
|
||||||
stop: (endTime) => {
|
stop: (endTime) => {
|
||||||
stop(endTime);
|
stop(endTime);
|
||||||
},
|
},
|
||||||
@@ -467,8 +514,17 @@ export function getOscillator(s, t, value, onended) {
|
|||||||
s = 'triangle';
|
s = 'triangle';
|
||||||
}
|
}
|
||||||
s = s === 'user' && !partials ? 'triangle' : s;
|
s = s === 'user' && !partials ? 'triangle' : s;
|
||||||
// If no partials are given, use stock waveforms
|
if (s === 'one') {
|
||||||
if (!partials || partials?.length === 0 || s === 'sine') {
|
// Constant 1 oscillator (used for modulation)
|
||||||
|
o = new ConstantSourceNode(getAudioContext(), { offset: 1 });
|
||||||
|
o.start(t);
|
||||||
|
return {
|
||||||
|
node: o,
|
||||||
|
nodes: { source: o },
|
||||||
|
stop: (time) => o?.stop(time),
|
||||||
|
};
|
||||||
|
} else if (!partials || partials?.length === 0 || s === 'sine') {
|
||||||
|
// If no partials are given, use stock waveforms
|
||||||
o = getAudioContext().createOscillator();
|
o = getAudioContext().createOscillator();
|
||||||
o.type = s || 'triangle';
|
o.type = s || 'triangle';
|
||||||
}
|
}
|
||||||
@@ -479,11 +535,11 @@ export function getOscillator(s, t, value, onended) {
|
|||||||
// set frequency
|
// set frequency
|
||||||
o.frequency.value = getFrequencyFromValue(value);
|
o.frequency.value = getFrequencyFromValue(value);
|
||||||
|
|
||||||
let vibratoOscillator = getVibratoOscillator(o.detune, value, t);
|
const vibratoHandle = getVibratoOscillator(o.detune, value, t);
|
||||||
|
|
||||||
// pitch envelope
|
// pitch envelope
|
||||||
getPitchEnvelope(o.detune, value, t, t + duration);
|
getPitchEnvelope(o.detune, value, t, t + duration);
|
||||||
const fmModulator = applyFM(o.frequency, value, t);
|
const fmHandle = applyFM(o.frequency, value, t);
|
||||||
|
|
||||||
let noiseMix;
|
let noiseMix;
|
||||||
if (noise) {
|
if (noise) {
|
||||||
@@ -500,9 +556,10 @@ export function getOscillator(s, t, value, onended) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
node: noiseMix?.node || o,
|
node: noiseMix?.node || o,
|
||||||
|
nodes: { source: [o], ...vibratoHandle?.nodes, ...fmHandle?.nodes },
|
||||||
stop: (time) => {
|
stop: (time) => {
|
||||||
fmModulator.stop(time);
|
fmHandle.stop(time);
|
||||||
vibratoOscillator?.stop(time);
|
vibratoHandle?.stop(time);
|
||||||
noiseMix?.stop(time);
|
noiseMix?.stop(time);
|
||||||
o.stop(time);
|
o.stop(time);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { releaseAudioNode } from './helpers.mjs';
|
||||||
|
|
||||||
// credits to webdirt: https://github.com/dktr0/WebDirt/blob/41342e81d6ad694a2310d491fef7b7e8b0929efe/js-src/Graph.js#L597
|
// credits to webdirt: https://github.com/dktr0/WebDirt/blob/41342e81d6ad694a2310d491fef7b7e8b0929efe/js-src/Graph.js#L597
|
||||||
export var vowelFormant = {
|
export var vowelFormant = {
|
||||||
a: { freqs: [660, 1120, 2750, 3000, 3350], gains: [1, 0.5012, 0.0708, 0.0631, 0.0126], qs: [80, 90, 120, 130, 140] },
|
a: { freqs: [660, 1120, 2750, 3000, 3350], gains: [1, 0.5012, 0.0708, 0.0631, 0.0126], qs: [80, 90, 120, 130, 140] },
|
||||||
@@ -46,7 +48,8 @@ if (typeof GainNode !== 'undefined') {
|
|||||||
}
|
}
|
||||||
const { gains, qs, freqs } = vowelFormant[letter];
|
const { gains, qs, freqs } = vowelFormant[letter];
|
||||||
this.makeupGain = ac.createGain();
|
this.makeupGain = ac.createGain();
|
||||||
this.audioNodes = [];
|
this.filters = [];
|
||||||
|
this.gains = [];
|
||||||
for (let i = 0; i < 5; i++) {
|
for (let i = 0; i < 5; i++) {
|
||||||
const gain = ac.createGain();
|
const gain = ac.createGain();
|
||||||
gain.gain.value = gains[i];
|
gain.gain.value = gains[i];
|
||||||
@@ -56,9 +59,9 @@ if (typeof GainNode !== 'undefined') {
|
|||||||
filter.frequency.value = freqs[i];
|
filter.frequency.value = freqs[i];
|
||||||
super.connect(filter);
|
super.connect(filter);
|
||||||
filter.connect(gain);
|
filter.connect(gain);
|
||||||
this.audioNodes.push(filter);
|
this.filters.push(filter);
|
||||||
gain.connect(this.makeupGain);
|
gain.connect(this.makeupGain);
|
||||||
this.audioNodes.push(gain);
|
this.gains.push(gain);
|
||||||
}
|
}
|
||||||
this.makeupGain.gain.value = 8; // how much makeup gain to add?
|
this.makeupGain.gain.value = 8; // how much makeup gain to add?
|
||||||
return this;
|
return this;
|
||||||
@@ -67,15 +70,17 @@ if (typeof GainNode !== 'undefined') {
|
|||||||
this.makeupGain.connect(target);
|
this.makeupGain.connect(target);
|
||||||
}
|
}
|
||||||
disconnect() {
|
disconnect() {
|
||||||
this.makeupGain.disconnect();
|
releaseAudioNode(this.makeupGain);
|
||||||
this.audioNodes.forEach((n) => n.disconnect());
|
this.filters.forEach(releaseAudioNode);
|
||||||
|
this.gains.forEach(releaseAudioNode);
|
||||||
super.disconnect();
|
super.disconnect();
|
||||||
this.makeupGain = null;
|
this.makeupGain = null;
|
||||||
this.audioNodes = null;
|
this.filters = null;
|
||||||
|
this.gains = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AudioContext.prototype.createVowelFilter = function (letter) {
|
BaseAudioContext.prototype.createVowelFilter = function (letter) {
|
||||||
return new VowelNode(this, letter);
|
return new VowelNode(this, letter);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ import {
|
|||||||
getParamADSR,
|
getParamADSR,
|
||||||
getPitchEnvelope,
|
getPitchEnvelope,
|
||||||
getVibratoOscillator,
|
getVibratoOscillator,
|
||||||
getWorklet,
|
|
||||||
releaseAudioNode,
|
|
||||||
webAudioTimeout,
|
webAudioTimeout,
|
||||||
|
releaseAudioNode,
|
||||||
} from './helpers.mjs';
|
} from './helpers.mjs';
|
||||||
|
import { getNodeFromPool, markWorkletAsDead, releaseNodeToPool } from './nodePools.mjs';
|
||||||
import { logger } from './logger.mjs';
|
import { logger } from './logger.mjs';
|
||||||
|
|
||||||
export const Warpmode = Object.freeze({
|
export const Warpmode = Object.freeze({
|
||||||
@@ -40,6 +40,11 @@ export const Warpmode = Object.freeze({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const seenKeys = new Set();
|
const seenKeys = new Set();
|
||||||
|
|
||||||
|
export function resetSeenKeys() {
|
||||||
|
seenKeys.clear();
|
||||||
|
}
|
||||||
|
|
||||||
async function getPayload(url, label, frameLen = 2048) {
|
async function getPayload(url, label, frameLen = 2048) {
|
||||||
const key = `${url},${frameLen}`;
|
const key = `${url},${frameLen}`;
|
||||||
if (!seenKeys.has(key)) {
|
if (!seenKeys.has(key)) {
|
||||||
@@ -225,24 +230,31 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) {
|
|||||||
}
|
}
|
||||||
const endWithRelease = holdEnd + release;
|
const endWithRelease = holdEnd + release;
|
||||||
const envEnd = endWithRelease + 0.01;
|
const envEnd = endWithRelease + 0.01;
|
||||||
const source = getWorklet(
|
const params = {
|
||||||
ac,
|
begin: t,
|
||||||
'wavetable-oscillator-processor',
|
end: envEnd,
|
||||||
{
|
frequency,
|
||||||
begin: t,
|
freqspread: value.detune,
|
||||||
end: envEnd,
|
position: value.wt,
|
||||||
frequency,
|
warp: value.warp,
|
||||||
freqspread: value.detune,
|
warpMode: warpmode,
|
||||||
position: value.wt,
|
voices: Math.max(value.unison ?? 1, 1),
|
||||||
warp: value.warp,
|
panspread: value.spread,
|
||||||
warpMode: warpmode,
|
phaserand: (value.wtphaserand ?? value.unison > 1) ? 1 : 0,
|
||||||
voices: Math.max(value.unison ?? 1, 1),
|
};
|
||||||
panspread: value.spread,
|
const factory = () => new AudioWorkletNode(ac, 'wavetable-oscillator-processor', { outputChannelCount: [2] });
|
||||||
phaserand: (value.wtphaserand ?? value.unison > 1) ? 1 : 0,
|
const source = getNodeFromPool('wavetable', factory);
|
||||||
},
|
const now = ac.currentTime;
|
||||||
{ outputChannelCount: [2] },
|
Object.entries(params).forEach(([key, value]) => {
|
||||||
);
|
const param = source.parameters.get(key);
|
||||||
source.port.postMessage({ type: 'table', payload });
|
const target = value !== undefined ? value : param.defaultValue;
|
||||||
|
param.setValueAtTime(target, now);
|
||||||
|
});
|
||||||
|
source.port.postMessage({ type: 'initialize', payload });
|
||||||
|
source.port.onmessage = (e) => {
|
||||||
|
if (e.data.type === 'died') markWorkletAsDead(source);
|
||||||
|
source.port.onmessage = null;
|
||||||
|
};
|
||||||
if (ac.currentTime > t) {
|
if (ac.currentTime > t) {
|
||||||
logger(`[wavetable] still loading sound "${s}:${n}"`, 'highlight');
|
logger(`[wavetable] still loading sound "${s}:${n}"`, 'highlight');
|
||||||
return;
|
return;
|
||||||
@@ -309,19 +321,28 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) {
|
|||||||
dcoffset: value.warpdc ?? 0,
|
dcoffset: value.warpdc ?? 0,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const vibratoOscillator = getVibratoOscillator(source.parameters.get('detune'), value, t);
|
const vibratoHandle = getVibratoOscillator(source.parameters.get('detune'), value, t);
|
||||||
const fm = applyFM(source.parameters.get('frequency'), value, t);
|
const fmHandle = applyFM(source.parameters.get('frequency'), value, t);
|
||||||
const envGain = ac.createGain();
|
const envGain = ac.createGain();
|
||||||
const node = source.connect(envGain);
|
const node = source.connect(envGain);
|
||||||
getParamADSR(node.gain, attack, decay, sustain, release, 0, 0.3, t, holdEnd, 'linear');
|
getParamADSR(node.gain, attack, decay, sustain, release, 0, 0.3, t, holdEnd, 'linear');
|
||||||
getPitchEnvelope(source.parameters.get('detune'), value, t, holdEnd);
|
getPitchEnvelope(source.parameters.get('detune'), value, t, holdEnd);
|
||||||
const handle = { node, source };
|
const handle = {
|
||||||
|
node,
|
||||||
|
nodes: {
|
||||||
|
source: [source],
|
||||||
|
wt_lfo: [wtPosModulators],
|
||||||
|
warp_lfo: [wtWarpModulators],
|
||||||
|
...fmHandle?.nodes,
|
||||||
|
...vibratoHandle?.nodes,
|
||||||
|
},
|
||||||
|
};
|
||||||
const timeoutNode = webAudioTimeout(
|
const timeoutNode = webAudioTimeout(
|
||||||
ac,
|
ac,
|
||||||
() => {
|
() => {
|
||||||
releaseAudioNode(source);
|
releaseNodeToPool(source);
|
||||||
releaseAudioNode(vibratoOscillator);
|
vibratoHandle?.stop();
|
||||||
fm?.stop();
|
fmHandle?.stop();
|
||||||
releaseAudioNode(wtPosModulators);
|
releaseAudioNode(wtPosModulators);
|
||||||
releaseAudioNode(wtWarpModulators);
|
releaseAudioNode(wtWarpModulators);
|
||||||
onended();
|
onended();
|
||||||
|
|||||||
@@ -124,8 +124,8 @@ class LFOProcessor extends AudioWorkletProcessor {
|
|||||||
{ name: 'shape', defaultValue: 0 },
|
{ name: 'shape', defaultValue: 0 },
|
||||||
{ name: 'curve', defaultValue: 1 },
|
{ name: 'curve', defaultValue: 1 },
|
||||||
{ name: 'dcoffset', defaultValue: 0 },
|
{ name: 'dcoffset', defaultValue: 0 },
|
||||||
{ name: 'min', defaultValue: 0 },
|
{ name: 'min', defaultValue: -1e9 },
|
||||||
{ name: 'max', defaultValue: 1 },
|
{ name: 'max', defaultValue: 1e9 },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,7 +143,8 @@ class LFOProcessor extends AudioWorkletProcessor {
|
|||||||
|
|
||||||
process(_inputs, outputs, parameters) {
|
process(_inputs, outputs, parameters) {
|
||||||
const begin = parameters['begin'][0];
|
const begin = parameters['begin'][0];
|
||||||
if (currentTime >= parameters.end[0]) {
|
const end = parameters['end'][0];
|
||||||
|
if (currentTime >= end) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (currentTime <= begin) {
|
if (currentTime <= begin) {
|
||||||
@@ -161,6 +162,7 @@ class LFOProcessor extends AudioWorkletProcessor {
|
|||||||
const curve = parameters['curve'][0];
|
const curve = parameters['curve'][0];
|
||||||
|
|
||||||
const dcoffset = parameters['dcoffset'][0];
|
const dcoffset = parameters['dcoffset'][0];
|
||||||
|
|
||||||
const min = parameters['min'][0];
|
const min = parameters['min'][0];
|
||||||
const max = parameters['max'][0];
|
const max = parameters['max'][0];
|
||||||
const shape = waveShapeNames[parameters['shape'][0]];
|
const shape = waveShapeNames[parameters['shape'][0]];
|
||||||
@@ -461,6 +463,16 @@ registerProcessor('distort-processor', DistortProcessor);
|
|||||||
class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
|
class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
this.isAlive = true; // used internally to prevent multiple death messages
|
||||||
|
this.port.onmessage = (e) => {
|
||||||
|
const { type, payload } = e.data || {};
|
||||||
|
if (type === 'initialize') {
|
||||||
|
this.initialize(payload);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this.initialize();
|
||||||
|
}
|
||||||
|
initialize(_options) {
|
||||||
this.phase = [];
|
this.phase = [];
|
||||||
}
|
}
|
||||||
static get parameterDescriptors() {
|
static get parameterDescriptors() {
|
||||||
@@ -511,12 +523,16 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
process(_input, outputs, params) {
|
process(_input, outputs, params) {
|
||||||
if (currentTime >= params.end[0]) {
|
if (currentTime >= params.end[0] + 0.5) {
|
||||||
// should terminate
|
// Outside of grace period - should terminate
|
||||||
|
if (this.isAlive) {
|
||||||
|
this.port.postMessage({ type: 'died' });
|
||||||
|
this.isAlive = false;
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (currentTime <= params.begin[0]) {
|
if (currentTime >= params.end[0] || currentTime <= params.begin[0]) {
|
||||||
// keep alive
|
// Inside of grace period or not yet started
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const output = outputs[0];
|
const output = outputs[0];
|
||||||
@@ -967,7 +983,9 @@ class EnvelopeProcessor extends AudioWorkletProcessor {
|
|||||||
{ name: 'attackCurve', defaultValue: 0, minValue: -1, maxValue: 1 },
|
{ name: 'attackCurve', defaultValue: 0, minValue: -1, maxValue: 1 },
|
||||||
{ name: 'decayCurve', defaultValue: 0, minValue: -1, maxValue: 1 },
|
{ name: 'decayCurve', defaultValue: 0, minValue: -1, maxValue: 1 },
|
||||||
{ name: 'releaseCurve', defaultValue: 0, minValue: -1, maxValue: 1 },
|
{ name: 'releaseCurve', defaultValue: 0, minValue: -1, maxValue: 1 },
|
||||||
{ name: 'peak', defaultValue: 1 },
|
{ name: 'depth', defaultValue: 1 },
|
||||||
|
{ name: 'min', defaultValue: -1e9 },
|
||||||
|
{ name: 'max', defaultValue: 1e9 },
|
||||||
{ name: 'retrigger', defaultValue: 1, minValue: 0, maxValue: 1 },
|
{ name: 'retrigger', defaultValue: 1, minValue: 0, maxValue: 1 },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -1009,9 +1027,15 @@ class EnvelopeProcessor extends AudioWorkletProcessor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
process(_inputs, outputs, params) {
|
process(_inputs, outputs, params) {
|
||||||
|
const begin = params['begin'][0];
|
||||||
|
const end = params['end'][0];
|
||||||
|
if (currentTime >= end) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (currentTime <= begin) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
const out = outputs[0][0];
|
const out = outputs[0][0];
|
||||||
if (!out) return true;
|
|
||||||
const begin = pv(params.begin, 0);
|
|
||||||
const retrigger = pv(params.retrigger, 0) >= 0.5; // convert to bool
|
const retrigger = pv(params.retrigger, 0) >= 0.5; // convert to bool
|
||||||
if (begin !== this.beginTime && (this.state === 0 || retrigger)) {
|
if (begin !== this.beginTime && (this.state === 0 || retrigger)) {
|
||||||
// triggered
|
// triggered
|
||||||
@@ -1029,7 +1053,9 @@ class EnvelopeProcessor extends AudioWorkletProcessor {
|
|||||||
const aCurve = pv(params.attackCurve, i);
|
const aCurve = pv(params.attackCurve, i);
|
||||||
const dCurve = pv(params.decayCurve, i);
|
const dCurve = pv(params.decayCurve, i);
|
||||||
const rCurve = pv(params.releaseCurve, i);
|
const rCurve = pv(params.releaseCurve, i);
|
||||||
const peak = pv(params.peak, i);
|
const depth = pv(params.depth, i);
|
||||||
|
const min = pv(params.min, i);
|
||||||
|
const max = pv(params.max, i);
|
||||||
const states = [
|
const states = [
|
||||||
{ time: Number.POSITIVE_INFINITY, start: 0, target: 0 }, // idle
|
{ time: Number.POSITIVE_INFINITY, start: 0, target: 0 }, // idle
|
||||||
{ time: attack, start: this.attackStart, target: 1, curve: aCurve },
|
{ time: attack, start: this.attackStart, target: 1, curve: aCurve },
|
||||||
@@ -1043,7 +1069,7 @@ class EnvelopeProcessor extends AudioWorkletProcessor {
|
|||||||
this.state = (this.state + 1) % states.length;
|
this.state = (this.state + 1) % states.length;
|
||||||
time = states[this.state].time;
|
time = states[this.state].time;
|
||||||
}
|
}
|
||||||
out[i] = this.val * peak;
|
out[i] = clamp(this.val * depth, min, max);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1138,37 +1164,29 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
|||||||
|
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
super(options);
|
super(options);
|
||||||
this.frameLen = 0;
|
this.isAlive = true; // used internally to prevent multiple death messages
|
||||||
this.numFrames = 0;
|
|
||||||
this.phase = [];
|
|
||||||
|
|
||||||
this.port.onmessage = (e) => {
|
this.port.onmessage = (e) => {
|
||||||
const { type, payload } = e.data || {};
|
const { type, payload } = e.data || {};
|
||||||
if (type === 'table') {
|
if (type === 'initialize') {
|
||||||
const key = payload.key;
|
this.initialize(payload);
|
||||||
this.frameLen = payload.frameLen;
|
|
||||||
if (!tablesCache[key]) {
|
|
||||||
const tables = [payload.frames];
|
|
||||||
let table = tables[0];
|
|
||||||
for (let level = 1; level < 1; level++) {
|
|
||||||
const nextLen = table.length >> 1;
|
|
||||||
const nextTable = table.map((frame) => {
|
|
||||||
const avg = new Float32Array(nextLen);
|
|
||||||
for (let i = 0; i < nextLen; i++) {
|
|
||||||
avg[i] = (frame[2 * i] + frame[2 * i + 1]) / 2;
|
|
||||||
}
|
|
||||||
return avg;
|
|
||||||
});
|
|
||||||
tables.push(nextTable);
|
|
||||||
table = nextTable;
|
|
||||||
if (nextLen <= 32) break;
|
|
||||||
}
|
|
||||||
tablesCache[key] = tables;
|
|
||||||
}
|
|
||||||
this.tables = tablesCache[key];
|
|
||||||
this.numFrames = this.tables[0].length;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
this.initialize();
|
||||||
|
}
|
||||||
|
initialize(options) {
|
||||||
|
this.table = null;
|
||||||
|
this.frameLen = null;
|
||||||
|
this.numFrames = null;
|
||||||
|
this.phase = [];
|
||||||
|
if (options?.key) {
|
||||||
|
const key = options.key;
|
||||||
|
this.frameLen = options.frameLen;
|
||||||
|
if (!tablesCache[key]) {
|
||||||
|
tablesCache[key] = options.frames;
|
||||||
|
}
|
||||||
|
this.table = tablesCache[key];
|
||||||
|
this.numFrames = this.table.length;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_mirror(x) {
|
_mirror(x) {
|
||||||
@@ -1313,25 +1331,22 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
|||||||
return a + (b - a) * frac;
|
return a + (b - a) * frac;
|
||||||
}
|
}
|
||||||
|
|
||||||
_chooseMip(dphi) {
|
|
||||||
const approxHarm = clamp(dphi, 1e-6, 64);
|
|
||||||
let level = 0;
|
|
||||||
while (level + 1 < (this.tables?.length || 1) && approxHarm < this.tables[level][0].length / 8) {
|
|
||||||
level++;
|
|
||||||
}
|
|
||||||
return level;
|
|
||||||
}
|
|
||||||
|
|
||||||
process(_inputs, outputs, parameters) {
|
process(_inputs, outputs, parameters) {
|
||||||
if (currentTime >= parameters.end[0]) {
|
if (currentTime >= parameters.end[0] + 0.5) {
|
||||||
|
// Outside of grace period - should terminate
|
||||||
|
if (this.isAlive) {
|
||||||
|
this.port.postMessage({ type: 'died' });
|
||||||
|
this.isAlive = false;
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (currentTime <= parameters.begin[0]) {
|
if (currentTime >= parameters.end[0] || currentTime <= parameters.begin[0]) {
|
||||||
|
// Inside of grace period or not yet started
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const outL = outputs[0][0];
|
const outL = outputs[0][0];
|
||||||
const outR = outputs[0][1] || outputs[0][0];
|
const outR = outputs[0][1] || outputs[0][0];
|
||||||
if (!this.tables) {
|
if (!this.table) {
|
||||||
outL.fill(0);
|
outL.fill(0);
|
||||||
if (outR !== outL) outR.set(outL);
|
if (outR !== outL) outR.set(outL);
|
||||||
return true;
|
return true;
|
||||||
@@ -1365,14 +1380,12 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
|||||||
}
|
}
|
||||||
const fVoice = applySemitoneDetuneToFrequency(f, detuner(n)); // voice detune
|
const fVoice = applySemitoneDetuneToFrequency(f, detuner(n)); // voice detune
|
||||||
const dPhase = fVoice * INVSR;
|
const dPhase = fVoice * INVSR;
|
||||||
const level = this._chooseMip(dPhase);
|
|
||||||
const table = this.tables[level];
|
|
||||||
|
|
||||||
// warp phase then sample
|
// warp phase then sample
|
||||||
this.phase[n] = this.phase[n] ?? Math.random() * phaseRand;
|
this.phase[n] = this.phase[n] ?? Math.random() * phaseRand;
|
||||||
const ph = this._warpPhase(this.phase[n], warpAmount, warpMode);
|
const ph = this._warpPhase(this.phase[n], warpAmount, warpMode);
|
||||||
const s0 = this._sampleFrame(table[fIdx], ph);
|
const s0 = this._sampleFrame(this.table[fIdx], ph);
|
||||||
const s1 = this._sampleFrame(table[Math.min(this.numFrames - 1, fIdx + 1)], ph);
|
const s1 = this._sampleFrame(this.table[Math.min(this.numFrames - 1, fIdx + 1)], ph);
|
||||||
let s = lerp(s0, s1, interpT);
|
let s = lerp(s0, s1, interpT);
|
||||||
if (warpMode === WarpMode.FLIP && this.phase[n] < warpAmount) {
|
if (warpMode === WarpMode.FLIP && this.phase[n] < warpAmount) {
|
||||||
s = -s;
|
s = -s;
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ export function registerZZFXSounds() {
|
|||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
node: o,
|
node: o,
|
||||||
|
nodes: { source: [o] },
|
||||||
stop: () => {},
|
stop: () => {},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -42,4 +42,18 @@ describe('transpiler', () => {
|
|||||||
[12, 14],
|
[12, 14],
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
it('allows disabling mini', () => {
|
||||||
|
const code = `/* mini-off */
|
||||||
|
const randPrefix = Math.random() > 0.5 ? "b" : "s";
|
||||||
|
const drumPat = \`\${randPrefix}d\`;
|
||||||
|
// mini-on
|
||||||
|
s(drumPat).lpf("5000 10000") // make sure mini still runs;
|
||||||
|
`;
|
||||||
|
const { output, miniLocations } = transpiler(code, { ...simple, emitMiniLocations: true });
|
||||||
|
expect(output).not.toContain("m('b'");
|
||||||
|
expect(output).not.toContain("m('s'");
|
||||||
|
const cutoffIdx = code.indexOf('5000 10000');
|
||||||
|
expect(miniLocations).toHaveLength(2);
|
||||||
|
expect(miniLocations[0][0]).toEqual(cutoffIdx);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -21,12 +21,15 @@ export function registerLanguage(type, config) {
|
|||||||
export function transpiler(input, options = {}) {
|
export function transpiler(input, options = {}) {
|
||||||
const { wrapAsync = false, addReturn = true, emitMiniLocations = true, emitWidgets = true } = options;
|
const { wrapAsync = false, addReturn = true, emitMiniLocations = true, emitWidgets = true } = options;
|
||||||
|
|
||||||
|
const comments = [];
|
||||||
let ast = parse(input, {
|
let ast = parse(input, {
|
||||||
ecmaVersion: 2022,
|
ecmaVersion: 2022,
|
||||||
allowAwaitOutsideFunction: true,
|
allowAwaitOutsideFunction: true,
|
||||||
locations: true,
|
locations: true,
|
||||||
|
onComment: comments,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const miniDisableRanges = findMiniDisableRanges(comments, input.length);
|
||||||
let miniLocations = [];
|
let miniLocations = [];
|
||||||
const collectMiniLocations = (value, node) => {
|
const collectMiniLocations = (value, node) => {
|
||||||
const minilang = languages.get('minilang');
|
const minilang = languages.get('minilang');
|
||||||
@@ -66,6 +69,9 @@ export function transpiler(input, options = {}) {
|
|||||||
return this.replace(tidalWithLocation(raw, offset));
|
return this.replace(tidalWithLocation(raw, offset));
|
||||||
}
|
}
|
||||||
if (isBackTickString(node, parent)) {
|
if (isBackTickString(node, parent)) {
|
||||||
|
if (isMiniDisabled(node.start, miniDisableRanges)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const { quasis } = node;
|
const { quasis } = node;
|
||||||
const { raw } = quasis[0].value;
|
const { raw } = quasis[0].value;
|
||||||
this.skip();
|
this.skip();
|
||||||
@@ -73,6 +79,9 @@ export function transpiler(input, options = {}) {
|
|||||||
return this.replace(miniWithLocation(raw, node));
|
return this.replace(miniWithLocation(raw, node));
|
||||||
}
|
}
|
||||||
if (isStringWithDoubleQuotes(node)) {
|
if (isStringWithDoubleQuotes(node)) {
|
||||||
|
if (isMiniDisabled(node.start, miniDisableRanges)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const { value } = node;
|
const { value } = node;
|
||||||
this.skip();
|
this.skip();
|
||||||
emitMiniLocations && collectMiniLocations(value, node);
|
emitMiniLocations && collectMiniLocations(value, node);
|
||||||
@@ -327,3 +336,32 @@ function languageWithLocation(name, value, offset) {
|
|||||||
optional: false,
|
optional: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function findMiniDisableRanges(comments, codeEnd) {
|
||||||
|
const ranges = [];
|
||||||
|
const stack = []; // used to track on/off pairs
|
||||||
|
for (const comment of comments) {
|
||||||
|
const value = comment.value.trim();
|
||||||
|
if (value.startsWith('mini-off')) {
|
||||||
|
stack.push(comment.start);
|
||||||
|
} else if (value.startsWith('mini-on')) {
|
||||||
|
const start = stack.pop();
|
||||||
|
ranges.push([start, comment.end]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (stack.length) {
|
||||||
|
// If no closing mini-on is found, just turn it off until `codeEnd`
|
||||||
|
const start = stack.pop();
|
||||||
|
ranges.push([start, codeEnd]);
|
||||||
|
}
|
||||||
|
return ranges;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isMiniDisabled(offset, miniDisableRanges) {
|
||||||
|
for (const [start, end] of miniDisableRanges) {
|
||||||
|
if (offset >= start && offset < end) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,10 +5,21 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as strudel from '@strudel/core';
|
import * as strudel from '@strudel/core';
|
||||||
import { superdough, getAudioContext, setLogger, doughTrigger, registerWorklet } from 'superdough';
|
import {
|
||||||
|
superdough,
|
||||||
|
getAudioContext,
|
||||||
|
setLogger,
|
||||||
|
doughTrigger,
|
||||||
|
registerWorklet,
|
||||||
|
setAudioContext,
|
||||||
|
initAudio,
|
||||||
|
setSuperdoughAudioController,
|
||||||
|
resetGlobalEffects,
|
||||||
|
errorLogger,
|
||||||
|
} from 'superdough';
|
||||||
import './supradough.mjs';
|
import './supradough.mjs';
|
||||||
import { workletUrl } from 'supradough';
|
import { workletUrl } from 'supradough';
|
||||||
|
import { SuperdoughAudioController } from 'superdough/superdoughoutput.mjs';
|
||||||
registerWorklet(workletUrl);
|
registerWorklet(workletUrl);
|
||||||
|
|
||||||
const { Pattern, logger, repl } = strudel;
|
const { Pattern, logger, repl } = strudel;
|
||||||
@@ -26,6 +37,71 @@ export const webaudioOutput = (hap, _deadline, hapDuration, cps, t) => {
|
|||||||
return superdough(hap2value(hap), t, hapDuration, cps, hap.whole?.begin.valueOf());
|
return superdough(hap2value(hap), t, hapDuration, cps, hap.whole?.begin.valueOf());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export async function renderPatternAudio(
|
||||||
|
pattern,
|
||||||
|
cps,
|
||||||
|
begin,
|
||||||
|
end,
|
||||||
|
sampleRate,
|
||||||
|
maxPolyphony,
|
||||||
|
multiChannelOrbits,
|
||||||
|
downloadName = undefined,
|
||||||
|
) {
|
||||||
|
let audioContext = getAudioContext();
|
||||||
|
await audioContext.close();
|
||||||
|
audioContext = new OfflineAudioContext(2, ((end - begin) / cps) * sampleRate, sampleRate);
|
||||||
|
setAudioContext(audioContext);
|
||||||
|
setSuperdoughAudioController(new SuperdoughAudioController(audioContext));
|
||||||
|
await initAudio({
|
||||||
|
maxPolyphony,
|
||||||
|
multiChannelOrbits,
|
||||||
|
});
|
||||||
|
logger('[webaudio] preloading');
|
||||||
|
|
||||||
|
// Calling superdough(...) in ascending onset time order is important
|
||||||
|
// for controls that depend on the audio graph state like `cut`
|
||||||
|
let haps = pattern
|
||||||
|
.queryArc(begin, end, { _cps: cps })
|
||||||
|
.sort((a, b) => a.whole.begin.valueOf() - b.whole.begin.valueOf());
|
||||||
|
for (const hap of haps) {
|
||||||
|
if (hap.hasOnset()) {
|
||||||
|
try {
|
||||||
|
await superdough(
|
||||||
|
hap2value(hap),
|
||||||
|
(hap.whole.begin.valueOf() - begin) / cps,
|
||||||
|
hap.duration / cps,
|
||||||
|
cps,
|
||||||
|
(hap.whole?.begin.valueOf() - begin) / cps,
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
errorLogger(err, 'webaudio');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger('[webaudio] start rendering');
|
||||||
|
|
||||||
|
return audioContext
|
||||||
|
.startRendering()
|
||||||
|
.then((renderedBuffer) => {
|
||||||
|
const wavBuffer = audioBufferToWav(renderedBuffer);
|
||||||
|
const blob = new Blob([wavBuffer], { type: 'audio/wav' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
downloadName = downloadName ? `${downloadName}.wav` : `${new Date().toISOString()}.wav`;
|
||||||
|
a.download = `${downloadName}`;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
})
|
||||||
|
.finally(async () => {
|
||||||
|
setAudioContext(null);
|
||||||
|
setSuperdoughAudioController(null);
|
||||||
|
resetGlobalEffects();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function webaudioRepl(options = {}) {
|
export function webaudioRepl(options = {}) {
|
||||||
options = {
|
options = {
|
||||||
getTime: () => getAudioContext().currentTime,
|
getTime: () => getAudioContext().currentTime,
|
||||||
@@ -38,3 +114,98 @@ export function webaudioRepl(options = {}) {
|
|||||||
Pattern.prototype.dough = function () {
|
Pattern.prototype.dough = function () {
|
||||||
return this.onTrigger(doughTrigger, 1);
|
return this.onTrigger(doughTrigger, 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function audioBufferToWav(buffer, opt) {
|
||||||
|
opt = opt || {};
|
||||||
|
|
||||||
|
var numChannels = buffer.numberOfChannels;
|
||||||
|
var sampleRate = buffer.sampleRate;
|
||||||
|
var format = opt.float32 ? 3 : 1;
|
||||||
|
var bitDepth = format === 3 ? 32 : 16;
|
||||||
|
|
||||||
|
var result;
|
||||||
|
if (numChannels === 2) {
|
||||||
|
result = interleave(buffer.getChannelData(0), buffer.getChannelData(1));
|
||||||
|
} else {
|
||||||
|
result = buffer.getChannelData(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return encodeWAV(result, format, sampleRate, numChannels, bitDepth);
|
||||||
|
}
|
||||||
|
|
||||||
|
function encodeWAV(samples, format, sampleRate, numChannels, bitDepth) {
|
||||||
|
var bytesPerSample = bitDepth / 8;
|
||||||
|
var blockAlign = numChannels * bytesPerSample;
|
||||||
|
|
||||||
|
var buffer = new ArrayBuffer(44 + samples.length * bytesPerSample);
|
||||||
|
var view = new DataView(buffer);
|
||||||
|
|
||||||
|
/* RIFF identifier */
|
||||||
|
writeString(view, 0, 'RIFF');
|
||||||
|
/* RIFF chunk length */
|
||||||
|
view.setUint32(4, 36 + samples.length * bytesPerSample, true);
|
||||||
|
/* RIFF type */
|
||||||
|
writeString(view, 8, 'WAVE');
|
||||||
|
/* format chunk identifier */
|
||||||
|
writeString(view, 12, 'fmt ');
|
||||||
|
/* format chunk length */
|
||||||
|
view.setUint32(16, 16, true);
|
||||||
|
/* sample format (raw) */
|
||||||
|
view.setUint16(20, format, true);
|
||||||
|
/* channel count */
|
||||||
|
view.setUint16(22, numChannels, true);
|
||||||
|
/* sample rate */
|
||||||
|
view.setUint32(24, sampleRate, true);
|
||||||
|
/* byte rate (sample rate * block align) */
|
||||||
|
view.setUint32(28, sampleRate * blockAlign, true);
|
||||||
|
/* block align (channel count * bytes per sample) */
|
||||||
|
view.setUint16(32, blockAlign, true);
|
||||||
|
/* bits per sample */
|
||||||
|
view.setUint16(34, bitDepth, true);
|
||||||
|
/* data chunk identifier */
|
||||||
|
writeString(view, 36, 'data');
|
||||||
|
/* data chunk length */
|
||||||
|
view.setUint32(40, samples.length * bytesPerSample, true);
|
||||||
|
if (format === 1) {
|
||||||
|
// Raw PCM
|
||||||
|
floatTo16BitPCM(view, 44, samples);
|
||||||
|
} else {
|
||||||
|
writeFloat32(view, 44, samples);
|
||||||
|
}
|
||||||
|
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
function interleave(inputL, inputR) {
|
||||||
|
var length = inputL.length + inputR.length;
|
||||||
|
var result = new Float32Array(length);
|
||||||
|
|
||||||
|
var index = 0;
|
||||||
|
var inputIndex = 0;
|
||||||
|
|
||||||
|
while (index < length) {
|
||||||
|
result[index++] = inputL[inputIndex];
|
||||||
|
result[index++] = inputR[inputIndex];
|
||||||
|
inputIndex++;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeFloat32(output, offset, input) {
|
||||||
|
for (var i = 0; i < input.length; i++, offset += 4) {
|
||||||
|
output.setFloat32(offset, input[i], true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function floatTo16BitPCM(output, offset, input) {
|
||||||
|
for (var i = 0; i < input.length; i++, offset += 2) {
|
||||||
|
var s = Math.max(-1, Math.min(1, input[i]));
|
||||||
|
output.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7fff, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeString(view, offset, string) {
|
||||||
|
for (var i = 0; i < string.length; i++) {
|
||||||
|
view.setUint8(offset + i, string.charCodeAt(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -565,6 +565,52 @@ exports[`runs examples > example "_euclidRot" example index 20 1`] = `
|
|||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "FX" example index 0 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/8 | s:sbd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
|
||||||
|
"[ 1/8 → 1/4 | s:hh decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
|
||||||
|
"[ 1/4 → 3/8 | s:sbd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
|
||||||
|
"[ 3/8 → 1/2 | s:bd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
|
||||||
|
"[ 1/2 → 5/8 | s:sbd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
|
||||||
|
"[ 5/8 → 3/4 | s:hh decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
|
||||||
|
"[ 3/4 → 7/8 | s:sbd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
|
||||||
|
"[ 7/8 → 1/1 | s:lt decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
|
||||||
|
"[ 1/1 → 9/8 | s:sbd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
|
||||||
|
"[ 9/8 → 5/4 | s:hh decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
|
||||||
|
"[ 5/4 → 11/8 | s:sbd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
|
||||||
|
"[ 11/8 → 3/2 | s:oh decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
|
||||||
|
"[ 3/2 → 13/8 | s:sbd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
|
||||||
|
"[ 13/8 → 7/4 | s:hh decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
|
||||||
|
"[ 7/4 → 15/8 | s:sbd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
|
||||||
|
"[ 15/8 → 2/1 | s:bd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
|
||||||
|
"[ 2/1 → 17/8 | s:sbd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
|
||||||
|
"[ 17/8 → 9/4 | s:hh decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
|
||||||
|
"[ 9/4 → 19/8 | s:sbd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
|
||||||
|
"[ 19/8 → 5/2 | s:bd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
|
||||||
|
"[ 5/2 → 21/8 | s:sbd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
|
||||||
|
"[ 21/8 → 11/4 | s:hh decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
|
||||||
|
"[ 11/4 → 23/8 | s:sbd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
|
||||||
|
"[ 23/8 → 3/1 | s:bd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
|
||||||
|
"[ 3/1 → 25/8 | s:sbd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
|
||||||
|
"[ 25/8 → 13/4 | s:hh decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
|
||||||
|
"[ 13/4 → 27/8 | s:sbd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
|
||||||
|
"[ 27/8 → 7/2 | s:lt decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
|
||||||
|
"[ 7/2 → 29/8 | s:sbd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
|
||||||
|
"[ 29/8 → 15/4 | s:hh decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
|
||||||
|
"[ 15/4 → 31/8 | s:sbd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
|
||||||
|
"[ 31/8 → 4/1 | s:bd decay:0.4 FX:[{phaserrate:0.5 gain:2} {bandf:800} {distort:1.3} {room:0.2} {delay:0.5 gain:1.25} {distort:0.3}] FXrelease:1.7 ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "FX" example index 1 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/1 | s:saw fmi:0.5 delay:0.3 FX:[{coarse:4} {cutoff:500 lpenv:4 lpattack:1 lpdecay:2} {distort:1}] ]",
|
||||||
|
"[ 1/1 → 2/1 | s:saw fmi:0.5 delay:0.3 FX:[{coarse:4} {cutoff:500 lpenv:4 lpattack:1 lpdecay:2} {distort:1}] ]",
|
||||||
|
"[ 2/1 → 3/1 | s:saw fmi:0.5 delay:0.3 FX:[{coarse:4} {cutoff:500 lpenv:4 lpattack:1 lpdecay:2} {distort:1}] ]",
|
||||||
|
"[ 3/1 → 4/1 | s:saw fmi:0.5 delay:0.3 FX:[{coarse:4} {cutoff:500 lpenv:4 lpattack:1 lpdecay:2} {distort:1}] ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`runs examples > example "accelerate" example index 0 1`] = `
|
exports[`runs examples > example "accelerate" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 2/1 | s:sax accelerate:0 ]",
|
"[ 0/1 → 2/1 | s:sax accelerate:0 ]",
|
||||||
@@ -2539,6 +2585,84 @@ exports[`runs examples > example "cut" example index 0 1`] = `
|
|||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "cyclesPer" example index 0 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/4 | s:saw note:62.5 ]",
|
||||||
|
"[ 1/4 → 1/2 | s:saw note:62.5 ]",
|
||||||
|
"[ 1/2 → 5/8 | s:saw note:56.25 ]",
|
||||||
|
"[ 5/8 → 3/4 | s:saw note:56.25 ]",
|
||||||
|
"[ 3/4 → 1/1 | s:saw note:62.5 ]",
|
||||||
|
"[ 1/1 → 5/4 | s:saw note:62.5 ]",
|
||||||
|
"[ 5/4 → 3/2 | s:saw note:62.5 ]",
|
||||||
|
"[ 3/2 → 13/8 | s:saw note:56.25 ]",
|
||||||
|
"[ 13/8 → 7/4 | s:saw note:56.25 ]",
|
||||||
|
"[ 7/4 → 2/1 | s:saw note:62.5 ]",
|
||||||
|
"[ 2/1 → 9/4 | s:saw note:62.5 ]",
|
||||||
|
"[ 9/4 → 5/2 | s:saw note:62.5 ]",
|
||||||
|
"[ 5/2 → 21/8 | s:saw note:56.25 ]",
|
||||||
|
"[ 21/8 → 11/4 | s:saw note:56.25 ]",
|
||||||
|
"[ 11/4 → 3/1 | s:saw note:62.5 ]",
|
||||||
|
"[ 3/1 → 13/4 | s:saw note:62.5 ]",
|
||||||
|
"[ 13/4 → 7/2 | s:saw note:62.5 ]",
|
||||||
|
"[ 7/2 → 29/8 | s:saw note:56.25 ]",
|
||||||
|
"[ 29/8 → 15/4 | s:saw note:56.25 ]",
|
||||||
|
"[ 15/4 → 4/1 | s:saw note:62.5 ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "cyclesPer" example index 1 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/6 | s:bd note:20.166666666666668 ]",
|
||||||
|
"[ 1/6 → 1/3 | s:sd note:20.166666666666668 ]",
|
||||||
|
"[ 1/3 → 5/12 | s:bd note:20.083333333333332 ]",
|
||||||
|
"[ 5/12 → 1/2 | s:bd note:20.083333333333332 ]",
|
||||||
|
"[ 1/2 → 13/24 | s:sd note:20.041666666666668 ]",
|
||||||
|
"[ 13/24 → 7/12 | s:sd note:20.041666666666668 ]",
|
||||||
|
"[ 7/12 → 5/8 | s:sd note:20.041666666666668 ]",
|
||||||
|
"[ 5/8 → 2/3 | s:sd note:20.041666666666668 ]",
|
||||||
|
"[ 3/4 → 5/6 | s:sd note:20.083333333333332 ]",
|
||||||
|
"[ 5/6 → 11/12 | s:bd note:20.083333333333332 ]",
|
||||||
|
"[ 11/12 → 23/24 | s:bd note:20.041666666666668 ]",
|
||||||
|
"[ 23/24 → 1/1 | s:bd note:20.041666666666668 ]",
|
||||||
|
"[ 1/1 → 7/6 | s:bd note:20.166666666666668 ]",
|
||||||
|
"[ 7/6 → 4/3 | s:sd note:20.166666666666668 ]",
|
||||||
|
"[ 4/3 → 17/12 | s:bd note:20.083333333333332 ]",
|
||||||
|
"[ 17/12 → 3/2 | s:bd note:20.083333333333332 ]",
|
||||||
|
"[ 3/2 → 37/24 | s:sd note:20.041666666666668 ]",
|
||||||
|
"[ 37/24 → 19/12 | s:sd note:20.041666666666668 ]",
|
||||||
|
"[ 19/12 → 13/8 | s:sd note:20.041666666666668 ]",
|
||||||
|
"[ 13/8 → 5/3 | s:sd note:20.041666666666668 ]",
|
||||||
|
"[ 7/4 → 11/6 | s:sd note:20.083333333333332 ]",
|
||||||
|
"[ 11/6 → 23/12 | s:bd note:20.083333333333332 ]",
|
||||||
|
"[ 23/12 → 47/24 | s:bd note:20.041666666666668 ]",
|
||||||
|
"[ 47/24 → 2/1 | s:bd note:20.041666666666668 ]",
|
||||||
|
"[ 2/1 → 13/6 | s:bd note:20.166666666666668 ]",
|
||||||
|
"[ 13/6 → 7/3 | s:sd note:20.166666666666668 ]",
|
||||||
|
"[ 7/3 → 29/12 | s:bd note:20.083333333333332 ]",
|
||||||
|
"[ 29/12 → 5/2 | s:bd note:20.083333333333332 ]",
|
||||||
|
"[ 5/2 → 61/24 | s:sd note:20.041666666666668 ]",
|
||||||
|
"[ 61/24 → 31/12 | s:sd note:20.041666666666668 ]",
|
||||||
|
"[ 31/12 → 21/8 | s:sd note:20.041666666666668 ]",
|
||||||
|
"[ 21/8 → 8/3 | s:sd note:20.041666666666668 ]",
|
||||||
|
"[ 11/4 → 17/6 | s:sd note:20.083333333333332 ]",
|
||||||
|
"[ 17/6 → 35/12 | s:bd note:20.083333333333332 ]",
|
||||||
|
"[ 35/12 → 71/24 | s:bd note:20.041666666666668 ]",
|
||||||
|
"[ 71/24 → 3/1 | s:bd note:20.041666666666668 ]",
|
||||||
|
"[ 3/1 → 19/6 | s:bd note:20.166666666666668 ]",
|
||||||
|
"[ 19/6 → 10/3 | s:sd note:20.166666666666668 ]",
|
||||||
|
"[ 10/3 → 41/12 | s:bd note:20.083333333333332 ]",
|
||||||
|
"[ 41/12 → 7/2 | s:bd note:20.083333333333332 ]",
|
||||||
|
"[ 7/2 → 85/24 | s:sd note:20.041666666666668 ]",
|
||||||
|
"[ 85/24 → 43/12 | s:sd note:20.041666666666668 ]",
|
||||||
|
"[ 43/12 → 29/8 | s:sd note:20.041666666666668 ]",
|
||||||
|
"[ 29/8 → 11/3 | s:sd note:20.041666666666668 ]",
|
||||||
|
"[ 15/4 → 23/6 | s:sd note:20.083333333333332 ]",
|
||||||
|
"[ 23/6 → 47/12 | s:bd note:20.083333333333332 ]",
|
||||||
|
"[ 47/12 → 95/24 | s:bd note:20.041666666666668 ]",
|
||||||
|
"[ 95/24 → 4/1 | s:bd note:20.041666666666668 ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`runs examples > example "decay" example index 0 1`] = `
|
exports[`runs examples > example "decay" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/4 | note:c3 decay:0.1 sustain:0 ]",
|
"[ 0/1 → 1/4 | note:c3 decay:0.1 sustain:0 ]",
|
||||||
@@ -3725,6 +3849,51 @@ exports[`runs examples > example "end" example index 0 1`] = `
|
|||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "env" example index 0 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/1 | s:saw note:F1 cutoff:500 env:{0:{control:cutoff attack:1} __ids:{}} ]",
|
||||||
|
"[ 1/1 → 2/1 | s:saw note:F1 cutoff:500 env:{0:{control:cutoff attack:1} __ids:{}} ]",
|
||||||
|
"[ 2/1 → 3/1 | s:saw note:F1 cutoff:500 env:{0:{control:cutoff attack:1} __ids:{}} ]",
|
||||||
|
"[ 3/1 → 4/1 | s:saw note:F1 cutoff:500 env:{0:{control:cutoff attack:1} __ids:{}} ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "env" example index 1 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/1 | s:saw env:{0:{control:s decay:1} 1:{control:cutoff attack:0.1 decay:1 acurve:0.8 dcurve:0.3 depth:50} __ids:{}} note:F1 resonance:4 cutoff:50 ]",
|
||||||
|
"[ 1/1 → 2/1 | s:saw env:{0:{control:s decay:1} 1:{control:cutoff attack:0.1 decay:1 acurve:0.8 dcurve:0.3 depth:50} __ids:{}} note:F1 resonance:4 cutoff:50 ]",
|
||||||
|
"[ 2/1 → 3/1 | s:saw env:{0:{control:s decay:1} 1:{control:cutoff attack:0.1 decay:1 acurve:0.8 dcurve:0.3 depth:50} __ids:{}} note:F1 resonance:4 cutoff:50 ]",
|
||||||
|
"[ 3/1 → 4/1 | s:saw env:{0:{control:s decay:1} 1:{control:cutoff attack:0.1 decay:1 acurve:0.8 dcurve:0.3 depth:50} __ids:{}} note:F1 resonance:4 cutoff:50 ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "env" example index 2 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/1 | s:saw cutoff:500 distort:0.3 distortvol:1 distorttype:diode env:{0:{control:cutoff attack:0.5 decay:0.5} __ids:{}} ]",
|
||||||
|
"[ 1/1 → 2/1 | s:saw cutoff:500 distort:0.3 distortvol:1 distorttype:diode env:{0:{control:cutoff attack:0.5 decay:0.5} __ids:{}} ]",
|
||||||
|
"[ 2/1 → 3/1 | s:saw cutoff:500 distort:0.3 distortvol:1 distorttype:diode env:{0:{control:cutoff attack:0.5 decay:0.5} __ids:{}} ]",
|
||||||
|
"[ 3/1 → 4/1 | s:saw cutoff:500 distort:0.3 distortvol:1 distorttype:diode env:{0:{control:cutoff attack:0.5 decay:0.5} __ids:{}} ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "env" example index 3 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/1 | s:pulse cutoff:500 env:{0:{control:cutoff attack:1} 1:{control:s attack:0.5} __ids:{}} distort:0.3 distortvol:1 distorttype:diode ]",
|
||||||
|
"[ 1/1 → 2/1 | s:pulse cutoff:500 env:{0:{control:cutoff attack:1} 1:{control:s attack:1} __ids:{}} distort:0.3 distortvol:1 distorttype:diode ]",
|
||||||
|
"[ 2/1 → 3/1 | s:pulse cutoff:500 env:{0:{control:cutoff attack:1} 1:{control:s attack:1} __ids:{}} distort:0.3 distortvol:1 distorttype:diode ]",
|
||||||
|
"[ 3/1 → 4/1 | s:pulse cutoff:500 env:{0:{control:cutoff attack:1} 1:{control:s attack:0.5} __ids:{}} distort:0.3 distortvol:1 distorttype:diode ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "env" example index 4 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/1 | s:pulse cutoff:500 env:{1:{control:s attack:1} __ids:{} lpf_mod:{control:cutoff attack:0.5}} distort:0.3 distortvol:1 distorttype:diode ]",
|
||||||
|
"[ 1/1 → 2/1 | s:pulse cutoff:500 env:{1:{control:s attack:1} __ids:{} lpf_mod:{control:cutoff attack:1}} distort:0.3 distortvol:1 distorttype:diode ]",
|
||||||
|
"[ 2/1 → 3/1 | s:pulse cutoff:500 env:{1:{control:s attack:1} __ids:{} lpf_mod:{control:cutoff attack:1}} distort:0.3 distortvol:1 distorttype:diode ]",
|
||||||
|
"[ 3/1 → 4/1 | s:pulse cutoff:500 env:{1:{control:s attack:1} __ids:{} lpf_mod:{control:cutoff attack:0.5}} distort:0.3 distortvol:1 distorttype:diode ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`runs examples > example "euclid" example index 0 1`] = `
|
exports[`runs examples > example "euclid" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/8 | note:c3 ]",
|
"[ 0/1 → 1/8 | note:c3 ]",
|
||||||
@@ -6035,6 +6204,51 @@ exports[`runs examples > example "leslie" example index 0 1`] = `
|
|||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "lfo" example index 0 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/1 | s:saw note:F1 cutoff:500 lfo:{0:{control:cutoff} __ids:{}} ]",
|
||||||
|
"[ 1/1 → 2/1 | s:saw note:F1 cutoff:500 lfo:{0:{control:cutoff} __ids:{}} ]",
|
||||||
|
"[ 2/1 → 3/1 | s:saw note:F1 cutoff:500 lfo:{0:{control:cutoff} __ids:{}} ]",
|
||||||
|
"[ 3/1 → 4/1 | s:saw note:F1 cutoff:500 lfo:{0:{control:cutoff} __ids:{}} ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "lfo" example index 1 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/1 | s:saw lfo:{0:{control:s} 1:{control:cutoff sync:0.3} __ids:{}} cutoff:500 ]",
|
||||||
|
"[ 1/1 → 2/1 | s:saw lfo:{0:{control:s} 1:{control:cutoff sync:0.3} __ids:{}} cutoff:500 ]",
|
||||||
|
"[ 2/1 → 3/1 | s:saw lfo:{0:{control:s} 1:{control:cutoff sync:0.3} __ids:{}} cutoff:500 ]",
|
||||||
|
"[ 3/1 → 4/1 | s:saw lfo:{0:{control:s} 1:{control:cutoff sync:0.3} __ids:{}} cutoff:500 ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "lfo" example index 2 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/1 | s:saw cutoff:500 distort:0.3 distortvol:1 distorttype:diode lfo:{0:{control:cutoff} __ids:{}} ]",
|
||||||
|
"[ 1/1 → 2/1 | s:saw cutoff:500 distort:0.3 distortvol:1 distorttype:diode lfo:{0:{control:cutoff} __ids:{}} ]",
|
||||||
|
"[ 2/1 → 3/1 | s:saw cutoff:500 distort:0.3 distortvol:1 distorttype:diode lfo:{0:{control:cutoff} __ids:{}} ]",
|
||||||
|
"[ 3/1 → 4/1 | s:saw cutoff:500 distort:0.3 distortvol:1 distorttype:diode lfo:{0:{control:cutoff} __ids:{}} ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "lfo" example index 3 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/1 | s:pulse cutoff:500 lfo:{0:{control:cutoff} 1:{control:s sync:8} __ids:{}} distort:0.3 distortvol:1 distorttype:diode ]",
|
||||||
|
"[ 1/1 → 2/1 | s:pulse cutoff:500 lfo:{0:{control:cutoff} 1:{control:s} __ids:{}} distort:0.3 distortvol:1 distorttype:diode ]",
|
||||||
|
"[ 2/1 → 3/1 | s:pulse cutoff:500 lfo:{0:{control:cutoff} 1:{control:s} __ids:{}} distort:0.3 distortvol:1 distorttype:diode ]",
|
||||||
|
"[ 3/1 → 4/1 | s:pulse cutoff:500 lfo:{0:{control:cutoff} 1:{control:s sync:8} __ids:{}} distort:0.3 distortvol:1 distorttype:diode ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "lfo" example index 4 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/1 | s:pulse cutoff:500 lfo:{1:{control:s} __ids:{} lpf_mod:{control:cutoff depth:4 sync:8}} distort:0.3 distortvol:1 distorttype:diode ]",
|
||||||
|
"[ 1/1 → 2/1 | s:pulse cutoff:500 lfo:{1:{control:s} __ids:{} lpf_mod:{control:cutoff depth:4}} distort:0.3 distortvol:1 distorttype:diode ]",
|
||||||
|
"[ 2/1 → 3/1 | s:pulse cutoff:500 lfo:{1:{control:s} __ids:{} lpf_mod:{control:cutoff depth:4}} distort:0.3 distortvol:1 distorttype:diode ]",
|
||||||
|
"[ 3/1 → 4/1 | s:pulse cutoff:500 lfo:{1:{control:s} __ids:{} lpf_mod:{control:cutoff depth:4 sync:8}} distort:0.3 distortvol:1 distorttype:diode ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`runs examples > example "linger" example index 0 1`] = `
|
exports[`runs examples > example "linger" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/4 | s:lt ]",
|
"[ 0/1 → 1/4 | s:lt ]",
|
||||||
@@ -7913,6 +8127,51 @@ exports[`runs examples > example "penv" example index 0 1`] = `
|
|||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "per" example index 0 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/7 | n:0 s:bd distort:3.5 ]",
|
||||||
|
"[ 1/7 → 3/14 | n:0 s:bd distort:7 ]",
|
||||||
|
"[ 3/14 → 2/7 | n:0 s:bd distort:7 ]",
|
||||||
|
"[ 2/7 → 3/7 | n:0 s:bd distort:3.5 ]",
|
||||||
|
"[ 3/7 → 1/2 | n:0 s:bd distort:7 ]",
|
||||||
|
"[ 1/2 → 4/7 | n:0 s:bd distort:7 ]",
|
||||||
|
"[ 4/7 → 5/7 | n:0 s:bd distort:3.5 ]",
|
||||||
|
"[ 5/7 → 17/21 | n:0 s:bd distort:5.25 ]",
|
||||||
|
"[ 17/21 → 19/21 | n:0 s:bd distort:5.25 ]",
|
||||||
|
"[ 19/21 → 1/1 | n:0 s:bd distort:5.25 ]",
|
||||||
|
"[ 1/1 → 8/7 | n:0 s:bd distort:3.5 ]",
|
||||||
|
"[ 8/7 → 17/14 | n:0 s:bd distort:7 ]",
|
||||||
|
"[ 17/14 → 9/7 | n:0 s:bd distort:7 ]",
|
||||||
|
"[ 9/7 → 10/7 | n:0 s:bd distort:3.5 ]",
|
||||||
|
"[ 10/7 → 3/2 | n:0 s:bd distort:7 ]",
|
||||||
|
"[ 3/2 → 11/7 | n:0 s:bd distort:7 ]",
|
||||||
|
"[ 11/7 → 12/7 | n:0 s:bd distort:3.5 ]",
|
||||||
|
"[ 12/7 → 38/21 | n:0 s:bd distort:5.25 ]",
|
||||||
|
"[ 38/21 → 40/21 | n:0 s:bd distort:5.25 ]",
|
||||||
|
"[ 40/21 → 2/1 | n:0 s:bd distort:5.25 ]",
|
||||||
|
"[ 2/1 → 15/7 | n:0 s:bd distort:3.5 ]",
|
||||||
|
"[ 15/7 → 31/14 | n:0 s:bd distort:7 ]",
|
||||||
|
"[ 31/14 → 16/7 | n:0 s:bd distort:7 ]",
|
||||||
|
"[ 16/7 → 17/7 | n:0 s:bd distort:3.5 ]",
|
||||||
|
"[ 17/7 → 5/2 | n:0 s:bd distort:7 ]",
|
||||||
|
"[ 5/2 → 18/7 | n:0 s:bd distort:7 ]",
|
||||||
|
"[ 18/7 → 19/7 | n:0 s:bd distort:3.5 ]",
|
||||||
|
"[ 19/7 → 59/21 | n:0 s:bd distort:5.25 ]",
|
||||||
|
"[ 59/21 → 61/21 | n:0 s:bd distort:5.25 ]",
|
||||||
|
"[ 61/21 → 3/1 | n:0 s:bd distort:5.25 ]",
|
||||||
|
"[ 3/1 → 22/7 | n:0 s:bd distort:3.5 ]",
|
||||||
|
"[ 22/7 → 45/14 | n:0 s:bd distort:7 ]",
|
||||||
|
"[ 45/14 → 23/7 | n:0 s:bd distort:7 ]",
|
||||||
|
"[ 23/7 → 24/7 | n:0 s:bd distort:3.5 ]",
|
||||||
|
"[ 24/7 → 7/2 | n:0 s:bd distort:7 ]",
|
||||||
|
"[ 7/2 → 25/7 | n:0 s:bd distort:7 ]",
|
||||||
|
"[ 25/7 → 26/7 | n:0 s:bd distort:3.5 ]",
|
||||||
|
"[ 26/7 → 80/21 | n:0 s:bd distort:5.25 ]",
|
||||||
|
"[ 80/21 → 82/21 | n:0 s:bd distort:5.25 ]",
|
||||||
|
"[ 82/21 → 4/1 | n:0 s:bd distort:5.25 ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`runs examples > example "perlin" example index 0 1`] = `
|
exports[`runs examples > example "perlin" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/8 | s:hh cutoff:500 ]",
|
"[ 0/1 → 1/8 | s:hh cutoff:500 ]",
|
||||||
@@ -10290,6 +10549,18 @@ exports[`runs examples > example "scrub" example index 1 1`] = `
|
|||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "seed" example index 0 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/4 | s:bd ]",
|
||||||
|
"[ 1/4 → 1/2 | s:bd ]",
|
||||||
|
"[ 1/2 → 3/4 | s:bd ]",
|
||||||
|
"[ 1/1 → 5/4 | s:bd ]",
|
||||||
|
"[ 7/4 → 2/1 | s:bd ]",
|
||||||
|
"[ 9/4 → 5/2 | s:bd ]",
|
||||||
|
"[ 11/4 → 3/1 | s:bd ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`runs examples > example "segment" example index 0 1`] = `
|
exports[`runs examples > example "segment" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/24 | note:40 ]",
|
"[ 0/1 → 1/24 | note:40 ]",
|
||||||
@@ -10473,6 +10744,64 @@ exports[`runs examples > example "seqPLoop" example index 0 1`] = `
|
|||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "setGainCurve" example index 0 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/4 | s:bd gain:0.5 ]",
|
||||||
|
"[ 1/4 → 1/2 | s:bd gain:0.5 ]",
|
||||||
|
"[ 1/2 → 3/4 | s:bd gain:0.5 ]",
|
||||||
|
"[ 3/4 → 1/1 | s:bd gain:0.5 ]",
|
||||||
|
"[ 1/1 → 5/4 | s:bd gain:0.5 ]",
|
||||||
|
"[ 5/4 → 3/2 | s:bd gain:0.5 ]",
|
||||||
|
"[ 3/2 → 7/4 | s:bd gain:0.5 ]",
|
||||||
|
"[ 7/4 → 2/1 | s:bd gain:0.5 ]",
|
||||||
|
"[ 2/1 → 9/4 | s:bd gain:0.5 ]",
|
||||||
|
"[ 9/4 → 5/2 | s:bd gain:0.5 ]",
|
||||||
|
"[ 5/2 → 11/4 | s:bd gain:0.5 ]",
|
||||||
|
"[ 11/4 → 3/1 | s:bd gain:0.5 ]",
|
||||||
|
"[ 3/1 → 13/4 | s:bd gain:0.5 ]",
|
||||||
|
"[ 13/4 → 7/2 | s:bd gain:0.5 ]",
|
||||||
|
"[ 7/2 → 15/4 | s:bd gain:0.5 ]",
|
||||||
|
"[ 15/4 → 4/1 | s:bd gain:0.5 ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "setMaxPolyphony" example index 0 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/8 | note:C#3 room:1 release:4 gain:0.5 ]",
|
||||||
|
"[ 1/8 → 1/4 | note:E5 room:1 release:4 gain:0.5 ]",
|
||||||
|
"[ 1/4 → 3/8 | note:D#4 room:1 release:4 gain:0.5 ]",
|
||||||
|
"[ 3/8 → 1/2 | note:E4 room:1 release:4 gain:0.5 ]",
|
||||||
|
"[ 1/2 → 5/8 | note:B3 room:1 release:4 gain:0.5 ]",
|
||||||
|
"[ 5/8 → 3/4 | note:F#3 room:1 release:4 gain:0.5 ]",
|
||||||
|
"[ 3/4 → 7/8 | note:G#3 room:1 release:4 gain:0.5 ]",
|
||||||
|
"[ 7/8 → 1/1 | note:E4 room:1 release:4 gain:0.5 ]",
|
||||||
|
"[ 1/1 → 9/8 | note:A4 room:1 release:4 gain:0.5 ]",
|
||||||
|
"[ 9/8 → 5/4 | note:E5 room:1 release:4 gain:0.5 ]",
|
||||||
|
"[ 5/4 → 11/8 | note:F#5 room:1 release:4 gain:0.5 ]",
|
||||||
|
"[ 11/8 → 3/2 | note:G#3 room:1 release:4 gain:0.5 ]",
|
||||||
|
"[ 3/2 → 13/8 | note:C#5 room:1 release:4 gain:0.5 ]",
|
||||||
|
"[ 13/8 → 7/4 | note:G#4 room:1 release:4 gain:0.5 ]",
|
||||||
|
"[ 7/4 → 15/8 | note:G#3 room:1 release:4 gain:0.5 ]",
|
||||||
|
"[ 15/8 → 2/1 | note:C#5 room:1 release:4 gain:0.5 ]",
|
||||||
|
"[ 2/1 → 17/8 | note:E6 room:1 release:4 gain:0.5 ]",
|
||||||
|
"[ 17/8 → 9/4 | note:C#6 room:1 release:4 gain:0.5 ]",
|
||||||
|
"[ 9/4 → 19/8 | note:D#4 room:1 release:4 gain:0.5 ]",
|
||||||
|
"[ 19/8 → 5/2 | note:B5 room:1 release:4 gain:0.5 ]",
|
||||||
|
"[ 5/2 → 21/8 | note:G#4 room:1 release:4 gain:0.5 ]",
|
||||||
|
"[ 21/8 → 11/4 | note:F#3 room:1 release:4 gain:0.5 ]",
|
||||||
|
"[ 11/4 → 23/8 | note:D#5 room:1 release:4 gain:0.5 ]",
|
||||||
|
"[ 23/8 → 3/1 | note:C#3 room:1 release:4 gain:0.5 ]",
|
||||||
|
"[ 3/1 → 25/8 | note:A3 room:1 release:4 gain:0.5 ]",
|
||||||
|
"[ 25/8 → 13/4 | note:D#4 room:1 release:4 gain:0.5 ]",
|
||||||
|
"[ 13/4 → 27/8 | note:E6 room:1 release:4 gain:0.5 ]",
|
||||||
|
"[ 27/8 → 7/2 | note:E4 room:1 release:4 gain:0.5 ]",
|
||||||
|
"[ 7/2 → 29/8 | note:E4 room:1 release:4 gain:0.5 ]",
|
||||||
|
"[ 29/8 → 15/4 | note:D#6 room:1 release:4 gain:0.5 ]",
|
||||||
|
"[ 15/4 → 31/8 | note:A5 room:1 release:4 gain:0.5 ]",
|
||||||
|
"[ 31/8 → 4/1 | note:F#5 room:1 release:4 gain:0.5 ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`runs examples > example "setcpm" example index 0 1`] = `
|
exports[`runs examples > example "setcpm" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/4 | s:bd bank:tr707 ]",
|
"[ 0/1 → 1/4 | s:bd bank:tr707 ]",
|
||||||
@@ -12818,6 +13147,75 @@ exports[`runs examples > example "unit" example index 0 1`] = `
|
|||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "useRNG" example index 0 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/16 | note:D8 ]",
|
||||||
|
"[ 1/16 → 1/8 | note:Bb7 ]",
|
||||||
|
"[ 1/8 → 3/16 | note:Ab6 ]",
|
||||||
|
"[ 3/16 → 1/4 | note:D5 ]",
|
||||||
|
"[ 1/4 → 5/16 | note:Ab9 ]",
|
||||||
|
"[ 5/16 → 3/8 | note:D3 ]",
|
||||||
|
"[ 3/8 → 7/16 | note:G5 ]",
|
||||||
|
"[ 7/16 → 1/2 | note:G3 ]",
|
||||||
|
"[ 1/2 → 9/16 | note:Ab3 ]",
|
||||||
|
"[ 9/16 → 5/8 | note:Eb6 ]",
|
||||||
|
"[ 5/8 → 11/16 | note:Eb6 ]",
|
||||||
|
"[ 11/16 → 3/4 | note:Eb5 ]",
|
||||||
|
"[ 3/4 → 13/16 | note:G7 ]",
|
||||||
|
"[ 13/16 → 7/8 | note:Bb5 ]",
|
||||||
|
"[ 7/8 → 15/16 | note:D3 ]",
|
||||||
|
"[ 15/16 → 1/1 | note:Eb7 ]",
|
||||||
|
"[ 1/1 → 17/16 | note:Eb9 ]",
|
||||||
|
"[ 17/16 → 9/8 | note:G8 ]",
|
||||||
|
"[ 9/8 → 19/16 | note:Ab3 ]",
|
||||||
|
"[ 19/16 → 5/4 | note:Ab5 ]",
|
||||||
|
"[ 5/4 → 21/16 | note:C10 ]",
|
||||||
|
"[ 21/16 → 11/8 | note:C7 ]",
|
||||||
|
"[ 11/8 → 23/16 | note:G5 ]",
|
||||||
|
"[ 23/16 → 3/2 | note:Ab7 ]",
|
||||||
|
"[ 3/2 → 25/16 | note:G6 ]",
|
||||||
|
"[ 25/16 → 13/8 | note:Bb6 ]",
|
||||||
|
"[ 13/8 → 27/16 | note:Eb9 ]",
|
||||||
|
"[ 27/16 → 7/4 | note:G9 ]",
|
||||||
|
"[ 7/4 → 29/16 | note:G7 ]",
|
||||||
|
"[ 29/16 → 15/8 | note:C10 ]",
|
||||||
|
"[ 15/8 → 31/16 | note:Eb3 ]",
|
||||||
|
"[ 31/16 → 2/1 | note:Ab7 ]",
|
||||||
|
"[ 2/1 → 33/16 | note:F6 ]",
|
||||||
|
"[ 33/16 → 17/8 | note:C5 ]",
|
||||||
|
"[ 17/8 → 35/16 | note:Ab4 ]",
|
||||||
|
"[ 35/16 → 9/4 | note:G5 ]",
|
||||||
|
"[ 9/4 → 37/16 | note:C9 ]",
|
||||||
|
"[ 37/16 → 19/8 | note:Eb6 ]",
|
||||||
|
"[ 19/8 → 39/16 | note:C9 ]",
|
||||||
|
"[ 39/16 → 5/2 | note:Eb9 ]",
|
||||||
|
"[ 5/2 → 41/16 | note:D5 ]",
|
||||||
|
"[ 41/16 → 21/8 | note:F5 ]",
|
||||||
|
"[ 21/8 → 43/16 | note:F9 ]",
|
||||||
|
"[ 43/16 → 11/4 | note:Bb7 ]",
|
||||||
|
"[ 11/4 → 45/16 | note:Ab6 ]",
|
||||||
|
"[ 45/16 → 23/8 | note:Bb9 ]",
|
||||||
|
"[ 23/8 → 47/16 | note:C8 ]",
|
||||||
|
"[ 47/16 → 3/1 | note:Eb5 ]",
|
||||||
|
"[ 3/1 → 49/16 | note:F3 ]",
|
||||||
|
"[ 49/16 → 25/8 | note:G4 ]",
|
||||||
|
"[ 25/8 → 51/16 | note:D7 ]",
|
||||||
|
"[ 51/16 → 13/4 | note:D4 ]",
|
||||||
|
"[ 13/4 → 53/16 | note:F8 ]",
|
||||||
|
"[ 53/16 → 27/8 | note:C7 ]",
|
||||||
|
"[ 27/8 → 55/16 | note:Ab5 ]",
|
||||||
|
"[ 55/16 → 7/2 | note:Ab3 ]",
|
||||||
|
"[ 7/2 → 57/16 | note:F9 ]",
|
||||||
|
"[ 57/16 → 29/8 | note:D8 ]",
|
||||||
|
"[ 29/8 → 59/16 | note:F5 ]",
|
||||||
|
"[ 59/16 → 15/4 | note:Eb9 ]",
|
||||||
|
"[ 15/4 → 61/16 | note:Bb4 ]",
|
||||||
|
"[ 61/16 → 31/8 | note:C6 ]",
|
||||||
|
"[ 31/8 → 63/16 | note:C4 ]",
|
||||||
|
"[ 63/16 → 4/1 | note:G8 ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`runs examples > example "velocity" example index 0 1`] = `
|
exports[`runs examples > example "velocity" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/8 | s:hh gain:0.4 velocity:0.4 ]",
|
"[ 0/1 → 1/8 | s:hh gain:0.4 velocity:0.4 ]",
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ const skippedExamples = [
|
|||||||
'accelerationX',
|
'accelerationX',
|
||||||
'defaultmidimap',
|
'defaultmidimap',
|
||||||
'midimaps',
|
'midimaps',
|
||||||
|
'bmod',
|
||||||
];
|
];
|
||||||
|
|
||||||
describe('runs examples', () => {
|
describe('runs examples', () => {
|
||||||
|
|||||||
@@ -16,5 +16,6 @@ export default defineConfig({
|
|||||||
'**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress}.config.*',
|
'**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress}.config.*',
|
||||||
'**/shared.test.mjs',
|
'**/shared.test.mjs',
|
||||||
],
|
],
|
||||||
|
setupFiles: './vitest.setup.mjs',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { afterEach } from 'vitest';
|
||||||
|
import { useRNG } from './packages/core/signal.mjs';
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// Avoid bleed between tests
|
||||||
|
useRNG('legacy');
|
||||||
|
});
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
import '../../repl/Repl.css';
|
||||||
|
---
|
||||||
|
|
||||||
|
<div id="code"></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { DoughMirror } from './dough-mirror.mjs';
|
||||||
|
const root = document.getElementById('code');
|
||||||
|
const doughmirror = new DoughMirror({ root });
|
||||||
|
globalThis.settings = doughmirror.settings.bind(doughmirror);
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
#code {
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
import {
|
||||||
|
initEditor,
|
||||||
|
codemirrorSettings,
|
||||||
|
flash,
|
||||||
|
compartments,
|
||||||
|
extensions,
|
||||||
|
parseBooleans,
|
||||||
|
activateTheme,
|
||||||
|
updateMiniLocations,
|
||||||
|
highlightMiniLocations,
|
||||||
|
} from '@strudel/codemirror';
|
||||||
|
import { evalScope, hash2code, code2hash } from '@strudel/core';
|
||||||
|
import { Framer } from '@strudel/draw';
|
||||||
|
import { persistentAtom } from '@nanostores/persistent';
|
||||||
|
import { DoughRepl } from './dough-repl.mjs';
|
||||||
|
|
||||||
|
let initialCode = '$: note("c a f e")';
|
||||||
|
|
||||||
|
export const code = persistentAtom('vanilla-repl-code', initialCode, {
|
||||||
|
encode: JSON.stringify,
|
||||||
|
decode: JSON.parse,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
try {
|
||||||
|
const codeParam = window.location.href.split('#')[1] || '';
|
||||||
|
if (codeParam) {
|
||||||
|
const codeFromHash = hash2code(codeParam);
|
||||||
|
code.set(codeFromHash);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('could not init code from url');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DoughMirror {
|
||||||
|
constructor(options) {
|
||||||
|
const { root, initialCode = code.get(), bgFill = true } = options;
|
||||||
|
this.root = root;
|
||||||
|
this.code = initialCode;
|
||||||
|
this.repl = new DoughRepl();
|
||||||
|
this.prebaked = this.prebake();
|
||||||
|
|
||||||
|
// init codemirror
|
||||||
|
this.editor = initEditor({
|
||||||
|
root,
|
||||||
|
initialCode: this.code,
|
||||||
|
onChange: (v) => {
|
||||||
|
if (v.docChanged) {
|
||||||
|
this.code = v.state.doc.toString();
|
||||||
|
code.set(this.code);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onEvaluate: this.evaluate.bind(this),
|
||||||
|
onStop: () => this.stop(),
|
||||||
|
mondo: false,
|
||||||
|
});
|
||||||
|
const settings = codemirrorSettings.get();
|
||||||
|
this.setFontSize(settings.fontSize);
|
||||||
|
this.setFontFamily(settings.fontFamily);
|
||||||
|
|
||||||
|
// init event highlighting
|
||||||
|
this.framer = new Framer(
|
||||||
|
(time) => {
|
||||||
|
const frameHaps = this.repl.processHaps();
|
||||||
|
highlightMiniLocations(this.editor, time, frameHaps);
|
||||||
|
},
|
||||||
|
(err) => console.log('Framer error', err),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
prebake() {
|
||||||
|
const modulesLoading = evalScope(import('@strudel/core'), import('@strudel/tonal'), import('@strudel/mini'));
|
||||||
|
return Promise.all([modulesLoading, this.repl.prebake()]);
|
||||||
|
}
|
||||||
|
async evaluate() {
|
||||||
|
this.framer.start();
|
||||||
|
this.flash();
|
||||||
|
await this.prebaked;
|
||||||
|
const { miniLocations } = await this.repl.evaluate(this.code);
|
||||||
|
window.location.hash = '#' + code2hash(this.code);
|
||||||
|
updateMiniLocations(this.editor, miniLocations);
|
||||||
|
}
|
||||||
|
stop() {
|
||||||
|
this.repl.stop();
|
||||||
|
this.framer.stop();
|
||||||
|
highlightMiniLocations(this.editor, 0, []);
|
||||||
|
}
|
||||||
|
// added synonym (compared to StrudelMirror)
|
||||||
|
settings(settings = {}) {
|
||||||
|
this.updateSettings(settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
// the rest is copy pasted from StrudelMirror:
|
||||||
|
|
||||||
|
updateSettings(settings = {}) {
|
||||||
|
settings.fontSize && this.setFontSize(settings.fontSize);
|
||||||
|
settings.fontFamily && this.setFontFamily(settings.fontFamily);
|
||||||
|
for (let key in extensions) {
|
||||||
|
if (key in settings) {
|
||||||
|
this.reconfigureExtension(key, settings[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const updated = { ...codemirrorSettings.get(), ...settings };
|
||||||
|
// console.log(updated);
|
||||||
|
codemirrorSettings.set(updated);
|
||||||
|
}
|
||||||
|
reconfigureExtension(key, value) {
|
||||||
|
if (!extensions[key]) {
|
||||||
|
console.warn(`extension ${key} is not known`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
value = parseBooleans(value);
|
||||||
|
const newValue = extensions[key](value, this);
|
||||||
|
this.editor.dispatch({
|
||||||
|
effects: compartments[key].reconfigure(newValue),
|
||||||
|
});
|
||||||
|
if (key === 'theme') {
|
||||||
|
activateTheme(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
flash(ms) {
|
||||||
|
flash(this.editor, ms);
|
||||||
|
}
|
||||||
|
setFontSize(size) {
|
||||||
|
this.root.style.fontSize = size + 'px';
|
||||||
|
}
|
||||||
|
setFontFamily(family) {
|
||||||
|
this.root.style.fontFamily = family;
|
||||||
|
const scroller = this.root.querySelector('.cm-scroller');
|
||||||
|
if (scroller) {
|
||||||
|
scroller.style.fontFamily = family;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setCode(code, offset = 0) {
|
||||||
|
const changes = {
|
||||||
|
from: 0,
|
||||||
|
to: this.editor.state.doc.length + offset,
|
||||||
|
insert: code,
|
||||||
|
};
|
||||||
|
this.editor.dispatch({ changes });
|
||||||
|
}
|
||||||
|
getCursorLocation() {
|
||||||
|
return this.editor.state.selection.main.head;
|
||||||
|
}
|
||||||
|
setCursorLocation(col) {
|
||||||
|
return this.editor.dispatch({ selection: { anchor: col } });
|
||||||
|
}
|
||||||
|
appendCode(code) {
|
||||||
|
const cursor = this.getCursorLocation();
|
||||||
|
this.setCode(this.code + code);
|
||||||
|
this.setCursorLocation(cursor);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
// import { Dough, doughsamples } from 'dough-synth';
|
||||||
|
import { Dough, doughsamples } from 'https://unpkg.com/dough-synth@0.1.9/dough.js';
|
||||||
|
import { Pattern, noteToMidi, evaluate, stack } from '@strudel/core';
|
||||||
|
// import doughUrl from 'dough-synth?url';
|
||||||
|
import { transpiler } from '@strudel/transpiler';
|
||||||
|
//const doughBaseUrl = doughUrl.split('/').slice(0, -1).join('/') + '/';
|
||||||
|
const doughBaseUrl = 'https://unpkg.com/dough-synth@0.1.9/';
|
||||||
|
|
||||||
|
Object.assign(globalThis, { doughsamples });
|
||||||
|
|
||||||
|
export class DoughRepl {
|
||||||
|
pattern;
|
||||||
|
latency = 0.1;
|
||||||
|
cps = 0.5;
|
||||||
|
origin;
|
||||||
|
t0;
|
||||||
|
lasttime;
|
||||||
|
strudel;
|
||||||
|
q = [];
|
||||||
|
constructor() {
|
||||||
|
this.ready = this.init();
|
||||||
|
}
|
||||||
|
async init() {
|
||||||
|
// init dough immediately, so that it can attach the document click event to initAudio immediately
|
||||||
|
this.dough = new Dough({
|
||||||
|
base: doughBaseUrl,
|
||||||
|
//base: "../", // local dev
|
||||||
|
onTick: ({ t0, t1 }) => {
|
||||||
|
if (!this.pattern) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.origin ??= t0;
|
||||||
|
this.t0 = t0;
|
||||||
|
this.lasttime = performance.now();
|
||||||
|
const a = (t0 - this.origin) * this.cps;
|
||||||
|
const b = (t1 - this.origin) * this.cps;
|
||||||
|
|
||||||
|
const haps = this.pattern.queryArc(a, b).filter((hap) => hap.hasOnset());
|
||||||
|
if (!haps.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
haps.forEach((hap) => {
|
||||||
|
const time = hap.whole.begin.valueOf() / this.cps + this.origin + this.latency;
|
||||||
|
const duration = hap.duration.valueOf() / this.cps;
|
||||||
|
const event = {
|
||||||
|
dough: 'play',
|
||||||
|
...hap.value,
|
||||||
|
time,
|
||||||
|
duration,
|
||||||
|
};
|
||||||
|
if (event.note && typeof event.note === 'string') {
|
||||||
|
event.note = noteToMidi(event.note);
|
||||||
|
}
|
||||||
|
//console.log("event", JSON.stringify(Object.entries(event)));
|
||||||
|
this.dough.evaluate(event);
|
||||||
|
this.q.push({ event, hap });
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
// miniAllStrings();
|
||||||
|
const setcps = (cps) => (this.cps = cps);
|
||||||
|
const setcpm = (cpm) => setcps(cpm / 60);
|
||||||
|
const replScope = { setcps, setcpm };
|
||||||
|
Object.assign(globalThis, replScope);
|
||||||
|
}
|
||||||
|
async evaluate(code) {
|
||||||
|
await this.ready;
|
||||||
|
let patterns = [];
|
||||||
|
Pattern.prototype.p = function (id) {
|
||||||
|
if (!id.startsWith('_')) {
|
||||||
|
patterns.push(this);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let { meta } = await evaluate(code, transpiler, { addReturn: false, wrapAsync: true, emitWidgets: false });
|
||||||
|
const { miniLocations } = meta;
|
||||||
|
|
||||||
|
this.pattern = stack(...patterns);
|
||||||
|
return { miniLocations, pattern: this.pattern };
|
||||||
|
}
|
||||||
|
stop() {
|
||||||
|
this.pattern = undefined;
|
||||||
|
this.origin = undefined;
|
||||||
|
}
|
||||||
|
prebake() {
|
||||||
|
return Promise.all([
|
||||||
|
// doughsamples('github:eddyflux/crate')
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
// tbd: move this to dough-synth
|
||||||
|
get time() {
|
||||||
|
return this.t0 + (performance.now() - this.lasttime) / 1000 + this.latency;
|
||||||
|
}
|
||||||
|
processHaps() {
|
||||||
|
const currentHaps = [];
|
||||||
|
const time = this.time;
|
||||||
|
this.q = this.q.filter(({ event, hap }) => {
|
||||||
|
const end = event.time + event.duration;
|
||||||
|
const isActive = time >= event.time && time <= end;
|
||||||
|
if (isActive) {
|
||||||
|
currentHaps.push(hap);
|
||||||
|
}
|
||||||
|
return end > time; // delete old events
|
||||||
|
// we do NOT return !isActive, because a frame might miss an event, which would cause a leak
|
||||||
|
});
|
||||||
|
// console.log(this.q.length); // to check for leaks
|
||||||
|
return currentHaps;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
---
|
||||||
|
import HeadCommon from '../../components/HeadCommon.astro';
|
||||||
|
import Dough from '../../components/Dough/Dough.astro';
|
||||||
|
---
|
||||||
|
|
||||||
|
<html lang="en" class="m-0 dark">
|
||||||
|
<head>
|
||||||
|
<HeadCommon />
|
||||||
|
<title>Strudel Dough REPL</title>
|
||||||
|
</head>
|
||||||
|
<body class="h-app-height bg-background m-0">
|
||||||
|
<Dough />
|
||||||
|
<a rel="me" href="https://social.toplap.org/@strudel" target="_blank" class="hidden">mastodon</a>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -11,9 +11,9 @@ import { JsDoc } from '../../docs/JsDoc';
|
|||||||
You can optionally add some music metadata in your Strudel code, by using tags in code comments:
|
You can optionally add some music metadata in your Strudel code, by using tags in code comments:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
// @title Hey Hoo
|
// @title My Cool Song
|
||||||
// @by Sam Tagada
|
// @by John Doe
|
||||||
// @license CC BY-NC-SA
|
// @license CC-BY-SA-4.0
|
||||||
```
|
```
|
||||||
|
|
||||||
Like other comments, those are ignored by Strudel, but it can be used by other tools to retrieve some information about the music.
|
Like other comments, those are ignored by Strudel, but it can be used by other tools to retrieve some information about the music.
|
||||||
@@ -24,22 +24,22 @@ You can also use comment blocks:
|
|||||||
|
|
||||||
```js
|
```js
|
||||||
/*
|
/*
|
||||||
@title Hey Hoo
|
@title My Cool Song
|
||||||
@by Sam Tagada
|
@by John Doe
|
||||||
@license CC BY-NC-SA
|
@license CC-BY-SA-4.0
|
||||||
*/
|
*/
|
||||||
```
|
```
|
||||||
|
|
||||||
Or define multiple tags in one line:
|
Or define multiple tags in one line:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
// @title Hey Hoo @by Sam Tagada @license CC BY-NC-SA
|
// @title My Cool Song @by John Doe @license CC-BY-SA-4.0
|
||||||
```
|
```
|
||||||
|
|
||||||
The `title` tag has an alternative syntax using quotes (must be defined at the very begining):
|
The `title` tag has an alternative syntax using quotes (must be defined at the very begining):
|
||||||
|
|
||||||
```js
|
```js
|
||||||
// "Hey Hoo" @by Sam Tagada
|
// "My Cool Song" @by John Doe
|
||||||
```
|
```
|
||||||
|
|
||||||
## Tags list
|
## Tags list
|
||||||
@@ -48,20 +48,22 @@ Available tags are:
|
|||||||
|
|
||||||
- `@title`: music title
|
- `@title`: music title
|
||||||
- `@by`: music author(s), separated by comma, eventually followed with a link in `<>` (ex: `@by John Doe <https://example.com>`)
|
- `@by`: music author(s), separated by comma, eventually followed with a link in `<>` (ex: `@by John Doe <https://example.com>`)
|
||||||
- `@license`: music license(s), e.g. CC BY-NC-SA. Unsure? [Choose a creative commons license here](https://creativecommons.org/choose/)
|
- `@license`: music license(s), separated by comma. Each license should be specified by using the correct identifier in the [https://spdx.org/licenses/](SPDX License List). Example: CC-BY-SA-4.0. Unsure? [Choose a Creative Commons license here](https://creativecommons.org/choose/).
|
||||||
- `@details`: some additional information about the music
|
- `@details`: some additional information about the music
|
||||||
- `@url`: web page(s) related to the music (git repo, soundcloud link, etc.)
|
- `@url`: web page(s) related to the music (git repository, Soundcloud link, etc.)
|
||||||
- `@genre`: music genre(s) (pop, jazz, etc)
|
- `@genre`: music genre(s) (pop, jazz, etc.)
|
||||||
- `@album`: music album name
|
- `@album`: music album name
|
||||||
|
|
||||||
|
Note to tool authors: _Never_ trust that a song has filled those fields with syntactically correct values; make sure your software is robust enough it doesn't break if it encounters bad values
|
||||||
|
|
||||||
## Multiple values
|
## Multiple values
|
||||||
|
|
||||||
Some of them accepts several values, using the comma or new line separator, or duplicating the tag:
|
Some of them accepts several values, using the comma or new line separator, or duplicating the tag:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
/*
|
/*
|
||||||
@by Sam Tagada
|
@by John Doe
|
||||||
Jimmy
|
Jane Doe
|
||||||
@genre pop, jazz
|
@genre pop, jazz
|
||||||
@url https://example.com
|
@url https://example.com
|
||||||
@url https://example.org
|
@url https://example.org
|
||||||
@@ -72,11 +74,11 @@ You can also add optional prefixes and use tags where you want:
|
|||||||
|
|
||||||
```js
|
```js
|
||||||
/*
|
/*
|
||||||
song @by Sam Tagada
|
song @by John Doe
|
||||||
samples @by Jimmy
|
samples @by Jane Doe
|
||||||
*/
|
*/
|
||||||
...
|
...
|
||||||
note("a3 c#4 e4 a4") // @by Sandy
|
note("a3 c#4 e4 a4") // @by Sandy Sue
|
||||||
```
|
```
|
||||||
|
|
||||||
## Multiline
|
## Multiline
|
||||||
|
|||||||
@@ -142,6 +142,8 @@ The "~" represents a rest, and will create silence between other events:
|
|||||||
|
|
||||||
<MiniRepl client:idle tune={`note("[b4 [~ c5] d5 e5]")`} punchcard />
|
<MiniRepl client:idle tune={`note("[b4 [~ c5] d5 e5]")`} punchcard />
|
||||||
|
|
||||||
|
Alternatively, "-" can be used instead of "~". It means the same thing.
|
||||||
|
|
||||||
## Parallel / polyphony
|
## Parallel / polyphony
|
||||||
|
|
||||||
Using commas, we can play chords.
|
Using commas, we can play chords.
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ note("c3 [e3 g3]*2")
|
|||||||
is transpiled to:
|
is transpiled to:
|
||||||
|
|
||||||
```strudel
|
```strudel
|
||||||
note(m('c3 [e3 g3]', 5))
|
note(m('c3 [e3 g3]*2', 5))
|
||||||
```
|
```
|
||||||
|
|
||||||
Here, the string is wrapped in `m`, which will create a pattern from a mini-notation string. As the second parameter, it gets passed source code location of the string, which enables highlighting active events later.
|
Here, the string is wrapped in `m`, which will create a pattern from a mini-notation string. As the second parameter, it gets passed source code location of the string, which enables highlighting active events later.
|
||||||
|
|||||||
@@ -35,3 +35,24 @@ Troubleshooting
|
|||||||
|
|
||||||
- If :w logs but evaluation doesn't apply, ensure Vim keybindings are active and try again. You can also use Ctrl+Enter as a fallback.
|
- If :w logs but evaluation doesn't apply, ensure Vim keybindings are active and try again. You can also use Ctrl+Enter as a fallback.
|
||||||
- For :q / gc, ensure focus is inside the editor. If an error occurs, reload the page to reset editor state and try again.
|
- For :q / gc, ensure focus is inside the editor. If an error occurs, reload the page to reset editor state and try again.
|
||||||
|
|
||||||
|
## Adding custom keybindings
|
||||||
|
|
||||||
|
To add custom keybindings the `Vim` object can be used (either from within a pattern or in a prebake script)
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Map 'jk' to Escape in normal mode
|
||||||
|
Vim.map('jk', '<Esc>', 'insert');
|
||||||
|
// Map 'U' to :w (Evaluate the current code)
|
||||||
|
Vim.map('U', ':w<CR>', 'normal');
|
||||||
|
// Map 'Q' to :q — Stop/pause playback
|
||||||
|
Vim.map('Q', ':q<CR>', 'normal');
|
||||||
|
// Map 'J' to find next '$' (jump to next label)
|
||||||
|
Vim.map('J', '/\\$<CR>', 'normal');
|
||||||
|
// Map 'K' to find previous '$' (jump to previous label)
|
||||||
|
Vim.map('K', '?\\$<CR>', 'normal');
|
||||||
|
```
|
||||||
|
|
||||||
|
For more information on how to use the `Vim` object see [CodeMirror Vim](https://github.com/replit/codemirror-vim)
|
||||||
|
|||||||
@@ -377,4 +377,4 @@ insect [crow metal] - -,
|
|||||||
punchcard
|
punchcard
|
||||||
/>
|
/>
|
||||||
|
|
||||||
Now that we know the basics of how to make beats, let's look at how we can play [notes](/workshop/first-notes)
|
Now that we know the basics of how to make beats, let's look at how we can play [notes](/workshop/first-notes).
|
||||||
|
|||||||
@@ -0,0 +1,651 @@
|
|||||||
|
/*
|
||||||
|
audiograph.mjs - show a svg view of the web audio API graph built during a playback
|
||||||
|
Copyright (C) 2025 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/website/src/repl/audiograph.mjs>
|
||||||
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// main entry point is `debugAudiograph`
|
||||||
|
import { logger } from '@strudel/core';
|
||||||
|
import { getAudioContext, getSuperdoughAudioController, webaudioOutput } from '@strudel/webaudio';
|
||||||
|
|
||||||
|
let mermaid = null;
|
||||||
|
let svgPanZoom = null;
|
||||||
|
|
||||||
|
let running = false;
|
||||||
|
let hap_count = 0;
|
||||||
|
|
||||||
|
let cache = new Map();
|
||||||
|
const initCache = JSON.stringify({
|
||||||
|
connect: [],
|
||||||
|
where: [],
|
||||||
|
disconnectAll: 0,
|
||||||
|
disconnectOne: 0,
|
||||||
|
hasStop: false,
|
||||||
|
stopCount: 0,
|
||||||
|
ac: null,
|
||||||
|
creation: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
let toggleOrig;
|
||||||
|
|
||||||
|
function stackTrace() {
|
||||||
|
var err = new Error();
|
||||||
|
const stacktrace = err.stack;
|
||||||
|
const lines = stacktrace.split('\n');
|
||||||
|
let lineIndex = lines.findIndex((line) => line !== 'Error' && !line.includes('audiograph.mjs'));
|
||||||
|
if (lines[lineIndex].includes('gainNode')) lineIndex++;
|
||||||
|
if (lines[lineIndex].includes('getWorklet')) lineIndex++;
|
||||||
|
const line = lines[lineIndex].replace(/\s*at\s/, '').replace('http', '@');
|
||||||
|
let match;
|
||||||
|
match = line.match(/([^@]*)@.*packages(\/[^:]+:\d+:\d+)/);
|
||||||
|
if (match) {
|
||||||
|
return match[1].replace(/[^.:/a-zA-Z0-9]/g, '') + '@' + match[2].replace(/[^.:/a-zA-Z0-9]/g, '');
|
||||||
|
}
|
||||||
|
return '@';
|
||||||
|
}
|
||||||
|
|
||||||
|
// This captures all AudioNodes lazily
|
||||||
|
// when an `.audioid` property is called
|
||||||
|
// no solution was found to hook
|
||||||
|
// AudioNode's constructor directly
|
||||||
|
let audioid = 0;
|
||||||
|
const lazyRegister = (o) => {
|
||||||
|
Object.defineProperty(o.prototype, 'audioid', {
|
||||||
|
get: function () {
|
||||||
|
if (!this._audioid) {
|
||||||
|
this._audioid = ++audioid;
|
||||||
|
const s = JSON.parse(initCache);
|
||||||
|
s.type = this.constructor.name === 'AudiographNode' ? this.constructor._parentClassName : this.constructor.name;
|
||||||
|
// special case for subclassed AudioNodes
|
||||||
|
// they are implemented in superdough but hard to get a reference on here
|
||||||
|
// they are not AudioScheduledSourceNodes anyway
|
||||||
|
if (['FeedbackDelayNode', 'VowelNode'].indexOf(s.type) === -1) {
|
||||||
|
s.hasStop = window[s.type].prototype instanceof AudioScheduledSourceNode;
|
||||||
|
}
|
||||||
|
s.ac = this.context?.constructor.name || 'AudioParam';
|
||||||
|
s.creation = s.creation || stackTrace();
|
||||||
|
cache.set(this._audioid, s);
|
||||||
|
}
|
||||||
|
return this._audioid;
|
||||||
|
},
|
||||||
|
enumerable: false,
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// extend a specific AudioNode's constructor
|
||||||
|
// necessary when creation is done direclty by
|
||||||
|
// calling the constructor
|
||||||
|
// eg: new GainNode(...)
|
||||||
|
const audioNodeHook = (node) => {
|
||||||
|
const name = node.prototype.constructor.name;
|
||||||
|
const PatchedNode = class AudiographNode extends node {
|
||||||
|
constructor(...args) {
|
||||||
|
super(...args);
|
||||||
|
// trigger the lazy register
|
||||||
|
this._audioid = this.audioid;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
PatchedNode._parentClassName = name;
|
||||||
|
window[name] = PatchedNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const drawMessage = async function (message) {
|
||||||
|
const element = document.querySelector('.strudel-mermaid');
|
||||||
|
let gd = '';
|
||||||
|
gd += '---\n';
|
||||||
|
gd += 'config:\n';
|
||||||
|
gd += ' flowchart:\n';
|
||||||
|
gd += ' wrappingWidth: 600\n';
|
||||||
|
gd += '---\n';
|
||||||
|
gd += 'flowchart LR\n';
|
||||||
|
gd += 'id[' + message.replaceAll(' ', ' ') + ']\n';
|
||||||
|
|
||||||
|
let { svg } = await mermaid.render('strudelSvgId', gd);
|
||||||
|
svg = svg.replace(/max-width:\s[0-9.]*px;/i, 'height: 100%');
|
||||||
|
svg = svg.replaceAll('&nbsp;', ' ');
|
||||||
|
element.innerHTML = svg;
|
||||||
|
};
|
||||||
|
|
||||||
|
const drawDiagram = async function () {
|
||||||
|
const element = document.querySelector('.strudel-mermaid');
|
||||||
|
let code = window.strudelMirror.code;
|
||||||
|
code = code.replace(/^await debugAudiograph.*\n?/gm, '');
|
||||||
|
code = '// date: ' + new Date().toISOString() + '\n\n' + code;
|
||||||
|
code = '// host: ' + document.location.hostname + '\n' + code;
|
||||||
|
const codeLines = code.split(/(?:\n|\r\n?)/);
|
||||||
|
const maxLineLength = codeLines.reduce((memo, line) => Math.max(memo, line.length), 0);
|
||||||
|
|
||||||
|
// https://mermaid.js.org/syntax/flowchart.html
|
||||||
|
let gd = '';
|
||||||
|
gd += '---\n';
|
||||||
|
gd += 'config:\n';
|
||||||
|
gd += ' flowchart:\n';
|
||||||
|
gd += ' wrappingWidth: ' + 14 * maxLineLength + '\n';
|
||||||
|
gd += '---\n';
|
||||||
|
gd += 'flowchart TB\n';
|
||||||
|
gd += '\tsubgraph AG[STRUDEL AUDIOGRAPH]\n';
|
||||||
|
|
||||||
|
// seed graph builder with all
|
||||||
|
// unconnected nodes
|
||||||
|
let lookup = [];
|
||||||
|
cache.forEach((v, k) => {
|
||||||
|
if (v.connect.length === 0) lookup.push(k);
|
||||||
|
});
|
||||||
|
const relations = [];
|
||||||
|
let curRelations;
|
||||||
|
const zombieCount = 0;
|
||||||
|
const sourceLoc = (stack) => {
|
||||||
|
if (stack === '@') return stack;
|
||||||
|
return stack.replace('@', '\n').replace('/superdough/', '/');
|
||||||
|
};
|
||||||
|
const label = (s) => {
|
||||||
|
const source = s.creation ? '\n' + sourceLoc(s.creation) : '';
|
||||||
|
let lb = '[' + '**' + s.type + '**' + source + ']';
|
||||||
|
if (s.ac === 'OfflineAudioContext') lb = '[' + lb + ']';
|
||||||
|
return lb;
|
||||||
|
};
|
||||||
|
const isConnectLeak = (s) => {
|
||||||
|
return (
|
||||||
|
['AudioDestinationNode', 'AudioParam'].indexOf(s.type) === -1 &&
|
||||||
|
s.disconnectAll === 0 &&
|
||||||
|
s.connect.length > s.disconnectOne
|
||||||
|
);
|
||||||
|
};
|
||||||
|
const isStopLeak = (s) => {
|
||||||
|
return s.hasStop && s.stopCount === 0;
|
||||||
|
};
|
||||||
|
do {
|
||||||
|
curRelations = relations.length;
|
||||||
|
lookup.slice().forEach((n) => {
|
||||||
|
cache.forEach((v, k) => {
|
||||||
|
if (v.connect.indexOf(n) !== -1) {
|
||||||
|
if (lookup.indexOf(k) === -1) lookup.push(k);
|
||||||
|
gd += v.connect
|
||||||
|
.map((i) => {
|
||||||
|
if (lookup.indexOf(i) === -1) lookup.push(i);
|
||||||
|
if (relations.indexOf(k + '-' + i) === -1) {
|
||||||
|
relations.push(k + '-' + i);
|
||||||
|
return (
|
||||||
|
'\t\tnode' +
|
||||||
|
k +
|
||||||
|
label(v) +
|
||||||
|
' -- ' +
|
||||||
|
sourceLoc(v.where[0]) +
|
||||||
|
' --> node' +
|
||||||
|
i +
|
||||||
|
label(cache.get(i)) +
|
||||||
|
'\n'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.join('');
|
||||||
|
}
|
||||||
|
if (k === n) {
|
||||||
|
gd += v.connect
|
||||||
|
.map((i) => {
|
||||||
|
if (lookup.indexOf(i) === -1) lookup.push(i);
|
||||||
|
if (relations.indexOf(k + '-' + i) === -1) {
|
||||||
|
relations.push(k + '-' + i);
|
||||||
|
return (
|
||||||
|
'\t\tnode' +
|
||||||
|
k +
|
||||||
|
label(v) +
|
||||||
|
' -- ' +
|
||||||
|
sourceLoc(v.where[0]) +
|
||||||
|
' --> node' +
|
||||||
|
i +
|
||||||
|
label(cache.get(i)) +
|
||||||
|
'\n'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.join('');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} while (relations.length > curRelations /*&& lookup.length < 100*/);
|
||||||
|
// add orphan nodes
|
||||||
|
const inRelation = '-' + relations.join('-') + '-';
|
||||||
|
cache.forEach((v, k) => {
|
||||||
|
if (!inRelation.includes('-' + k + '-')) {
|
||||||
|
gd += '\t\tnode' + k + label(v) + '\n';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const codePlaceholder = 'm'.repeat(maxLineLength);
|
||||||
|
gd += '\tsubgraph LEGEND\n';
|
||||||
|
gd += '\t\tlegend1[in AudioContext]\n';
|
||||||
|
gd += '\t\tlegend2[[in OfflineAudioContext]]\n';
|
||||||
|
gd += '\t\tlegend3[not disconnected]\n';
|
||||||
|
gd += '\t\tlegend4[AudioParam]\n';
|
||||||
|
gd += '\t\tlegend5[AudioDestinationNode]\n';
|
||||||
|
gd += '\t\tlegend6[not stopped]\n';
|
||||||
|
gd += '\tend\n';
|
||||||
|
gd += '\tsubgraph CODE[Strudel Code]\n';
|
||||||
|
// we use a codePlaceholder to
|
||||||
|
// - avoid problems with special chars
|
||||||
|
// - stop mermaid to split lines on space with multiple tspans
|
||||||
|
// - force mermaid to prepare a sufficiently sized zone
|
||||||
|
gd += '\ncode[' + (codePlaceholder + '<br>').repeat(codeLines.length) + ']\n';
|
||||||
|
gd += '\tend\n';
|
||||||
|
gd += '\tend\n';
|
||||||
|
gd += '\tclassDef audioparam fill:#6f6;\n';
|
||||||
|
gd += '\tclassDef destination fill:#99f;\n';
|
||||||
|
gd += '\tclassDef connectleak fill:#f96,stroke:#f00,stroke-width:2px;\n';
|
||||||
|
gd += '\tclassDef stopleak fill:#f55,stroke:#f00,stroke-width:2px;\n';
|
||||||
|
gd += '\tclass legend3 connectleak;\n';
|
||||||
|
gd += '\tclass legend4 audioparam;\n';
|
||||||
|
gd += '\tclass legend5 destination;\n';
|
||||||
|
gd += '\tclass legend6 stopleak;\n';
|
||||||
|
cache.forEach((v, k) => {
|
||||||
|
if (isConnectLeak(v)) {
|
||||||
|
gd += '\tclass node' + k + ' connectleak;\n';
|
||||||
|
} else if (isStopLeak(v)) {
|
||||||
|
gd += '\tclass node' + k + ' stopleak;\n';
|
||||||
|
}
|
||||||
|
if (v.type === 'AudioParam') {
|
||||||
|
gd += '\tclass node' + k + ' audioparam;\n';
|
||||||
|
}
|
||||||
|
if (v.type === 'AudioDestinationNode') {
|
||||||
|
gd += '\tclass node' + k + ' destination;\n';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let { svg } = await mermaid.render('strudelSvgId', gd);
|
||||||
|
|
||||||
|
// put real code in code zone
|
||||||
|
let idx = 0;
|
||||||
|
const escapeHtml = (unsafe) => {
|
||||||
|
return unsafe
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
};
|
||||||
|
svg = svg.replaceAll(codePlaceholder, () => escapeHtml(codeLines[idx++]));
|
||||||
|
|
||||||
|
// improve sizing on web page
|
||||||
|
svg = svg.replace(/max-width:\s[0-9.]*px;/i, 'height: 100%');
|
||||||
|
element.innerHTML = svg;
|
||||||
|
|
||||||
|
// align the code lines
|
||||||
|
let svgText = document.querySelector('[id^=flowchart-code] text');
|
||||||
|
svgText.setAttributeNS(null, 'style', 'text-anchor: start;');
|
||||||
|
const svgElement = document.querySelector('svg');
|
||||||
|
const svgLabel = document.querySelector('svg [id^=flowchart-code] .label');
|
||||||
|
const transformList = svgLabel.transform.baseVal;
|
||||||
|
const svgTransform = svgElement.createSVGTransform();
|
||||||
|
const tspans = Array.from(document.querySelectorAll('[id^=flowchart-code] tspan.text-inner-tspan'));
|
||||||
|
let tspansMaxLength = tspans.reduce((memo, tspan) => Math.max(memo, tspan.getComputedTextLength()), 0);
|
||||||
|
svgTransform.setTranslate(-tspansMaxLength / 2, 0);
|
||||||
|
transformList.appendItem(svgTransform);
|
||||||
|
|
||||||
|
let doPan = false;
|
||||||
|
let eventsHandler;
|
||||||
|
let panZoom;
|
||||||
|
let mousepos;
|
||||||
|
|
||||||
|
eventsHandler = {
|
||||||
|
haltEventListeners: ['mousedown', 'mousemove', 'mouseup'],
|
||||||
|
mouseDownHandler: function (ev) {
|
||||||
|
if (event.target.className == '[object SVGAnimatedString]') {
|
||||||
|
doPan = true;
|
||||||
|
mousepos = {
|
||||||
|
x: ev.clientX,
|
||||||
|
y: ev.clientY,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mouseMoveHandler: function (ev) {
|
||||||
|
if (doPan) {
|
||||||
|
panZoom.panBy({
|
||||||
|
x: ev.clientX - mousepos.x,
|
||||||
|
y: ev.clientY - mousepos.y,
|
||||||
|
});
|
||||||
|
mousepos = {
|
||||||
|
x: ev.clientX,
|
||||||
|
y: ev.clientY,
|
||||||
|
};
|
||||||
|
window.getSelection().removeAllRanges();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mouseUpHandler: function (ev) {
|
||||||
|
doPan = false;
|
||||||
|
},
|
||||||
|
init: function (options) {
|
||||||
|
options.svgElement.addEventListener('mousedown', this.mouseDownHandler, false);
|
||||||
|
options.svgElement.addEventListener('mousemove', this.mouseMoveHandler, false);
|
||||||
|
options.svgElement.addEventListener('mouseup', this.mouseUpHandler, false);
|
||||||
|
},
|
||||||
|
destroy: function (options) {
|
||||||
|
options.svgElement.removeEventListener('mousedown', this.mouseDownHandler, false);
|
||||||
|
options.svgElement.removeEventListener('mousemove', this.mouseMoveHandler, false);
|
||||||
|
options.svgElement.removeEventListener('mouseup', this.mouseUpHandler, false);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
panZoom = svgPanZoom('#strudelSvgId', {
|
||||||
|
zoomEnabled: true,
|
||||||
|
controlIconsEnabled: true,
|
||||||
|
fit: 1,
|
||||||
|
center: 1,
|
||||||
|
zoomScaleSensitivity: 0.4,
|
||||||
|
customEventsHandler: eventsHandler,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const svgExport = async () => {
|
||||||
|
const a = document.createElement('a');
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.style = 'display: none';
|
||||||
|
|
||||||
|
const selector = '.strudel-mermaid';
|
||||||
|
const bbox = document.querySelector('svg g').getBBox();
|
||||||
|
let transform, style;
|
||||||
|
// clean pan-zoom viewport
|
||||||
|
const pzViewport = document.querySelector('.svg-pan-zoom_viewport');
|
||||||
|
if (pzViewport) {
|
||||||
|
transform = pzViewport.transform;
|
||||||
|
style = pzViewport.style;
|
||||||
|
pzViewport.setAttribute('transform', '');
|
||||||
|
pzViewport.style = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const spzMin = await fetch('https://cdn.jsdelivr.net/npm/svg-pan-zoom@3.6.2/dist/svg-pan-zoom.min.js').then((res) =>
|
||||||
|
res.text(),
|
||||||
|
);
|
||||||
|
|
||||||
|
const scriptContent = '<![CDATA[' + spzMin + ';svgPanZoom("svg");]]>';
|
||||||
|
// prepare svg
|
||||||
|
const content = document
|
||||||
|
.querySelector(selector)
|
||||||
|
.innerHTML.replaceAll('<br>', '<br/>')
|
||||||
|
// remove useless tags
|
||||||
|
.replace(/<g id="svg-pan-zoom.*<\/g>/, '<script>' + scriptContent + '</script>')
|
||||||
|
.replace(/<defs>.*<\/defs>/, '')
|
||||||
|
// give inkscape true sizes
|
||||||
|
.replace('width="100%"', 'width="' + bbox.width + '" height="' + bbox.height + '"');
|
||||||
|
|
||||||
|
// restore pan-zoom viewport
|
||||||
|
if (pzViewport) {
|
||||||
|
pzViewport.setAttribute('transform', transform);
|
||||||
|
pzViewport.style = style;
|
||||||
|
}
|
||||||
|
|
||||||
|
// trigger download
|
||||||
|
var blob = new Blob([content], { type: 'image/svg+xml' }),
|
||||||
|
url = window.URL.createObjectURL(blob);
|
||||||
|
a.href = url;
|
||||||
|
a.download = 'audiograph.svg';
|
||||||
|
a.click();
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetAudioOutput = function (audioid) {
|
||||||
|
// calling reset on SuperdoughAudioController
|
||||||
|
// will discard output nodes AND recreate them
|
||||||
|
// so we keep the same `cache` to handle the
|
||||||
|
// `disconnects` knowing that new nodes will be
|
||||||
|
// stricly after the current audioid.
|
||||||
|
// then we purge the old nodes from the `cache`
|
||||||
|
// to have a clean state
|
||||||
|
|
||||||
|
// make sure destination will be recreated in the
|
||||||
|
// cache
|
||||||
|
const destination = getAudioContext().destination;
|
||||||
|
if (destination._audioid) delete destination._audioid;
|
||||||
|
|
||||||
|
const sac = getSuperdoughAudioController();
|
||||||
|
sac.reset();
|
||||||
|
Array.from(cache.keys()).map((k) => {
|
||||||
|
if (k <= audioid) cache.delete(k);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const postProcessing = async function () {
|
||||||
|
hap_count = 0;
|
||||||
|
await drawDiagram();
|
||||||
|
|
||||||
|
resetAudioOutput(audioid);
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultOptions = {
|
||||||
|
StopAfterHapCount: 10,
|
||||||
|
hapsBatch: 0,
|
||||||
|
maxEdges: 10000,
|
||||||
|
maxTextSize: 200000,
|
||||||
|
audioAPIBreathingRoomSec: 5,
|
||||||
|
};
|
||||||
|
|
||||||
|
// `StopAfterHapCount` :
|
||||||
|
// The player will auto-stop after hap count have
|
||||||
|
// been played. when StopAfterHapCount = 0, it will
|
||||||
|
// continue playing until 'stop' is clicked
|
||||||
|
// `audioAPIBreathingRoomSec` :
|
||||||
|
// how much time should we wait after 'stop' to let
|
||||||
|
// the audioAPI finish its tail of ondended calls
|
||||||
|
// `hapsBatch` :
|
||||||
|
// the AudioGraph will be displayed every hapsBatch haps
|
||||||
|
// when hapsBatch = 0, AudioGraph will only be displayed
|
||||||
|
// after and auto-stop or after 'stop' is clicked
|
||||||
|
// In hapsBatch mode you will probably see a trailing of
|
||||||
|
// non disconnected notes on the graph because the audio
|
||||||
|
// API may have some lag disconnecting them
|
||||||
|
// cf also audioAPIBreathingRoomSec
|
||||||
|
// `maxEdges`
|
||||||
|
// This is a mermaid.js config that forces a hard limit
|
||||||
|
// on the maximum number of Edges of a graph
|
||||||
|
// needs a reload to be taken into account
|
||||||
|
// `maxTextSize`
|
||||||
|
// This is a mermaid.js config that forces a hard limit
|
||||||
|
// on the maximum text size of a graph definition
|
||||||
|
// needs a reload to be taken into account
|
||||||
|
|
||||||
|
export const debugAudiograph = async (argOptions = {}) => {
|
||||||
|
const options = Object.assign({}, defaultOptions, argOptions);
|
||||||
|
const { StopAfterHapCount, hapsBatch, maxEdges, maxTextSize, audioAPIBreathingRoomSec } = options;
|
||||||
|
const sm = window.strudelMirror;
|
||||||
|
const code = sm.code;
|
||||||
|
if (!code.match(/await\s+debugAudiograph/)) {
|
||||||
|
throw new Error('you need to call `await debugAudiograph()` for audiograph to work');
|
||||||
|
}
|
||||||
|
const emptyOptions = /await\s+debugAudiograph\(\)/.exec(code);
|
||||||
|
if (emptyOptions) {
|
||||||
|
const cutCode = emptyOptions.index + emptyOptions[0].length - 1;
|
||||||
|
const codeOptions = JSON.stringify({ StopAfterHapCount: StopAfterHapCount }).replaceAll('"', '');
|
||||||
|
sm.setCode(code.slice(0, cutCode) + codeOptions + code.slice(cutCode));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.audiograph === undefined) {
|
||||||
|
const ag = (window.audiograph = {});
|
||||||
|
|
||||||
|
toggleOrig = sm.toggle;
|
||||||
|
|
||||||
|
////////////////////////////////////////
|
||||||
|
// step 1: web audio api instrumentation
|
||||||
|
////////////////////////////////////////
|
||||||
|
|
||||||
|
// path AudioNode & AudioParam
|
||||||
|
// to give them lazy ids
|
||||||
|
// this captures both `ac.createGain`
|
||||||
|
// and `new GainNode(..)` patterns
|
||||||
|
lazyRegister(AudioNode);
|
||||||
|
lazyRegister(AudioParam);
|
||||||
|
lazyRegister(PeriodicWave);
|
||||||
|
|
||||||
|
const audioNodes = [
|
||||||
|
AudioBufferSourceNode,
|
||||||
|
AudioWorkletNode,
|
||||||
|
AnalyserNode,
|
||||||
|
BiquadFilterNode,
|
||||||
|
ChannelMergerNode,
|
||||||
|
ChannelSplitterNode,
|
||||||
|
ConstantSourceNode,
|
||||||
|
ConvolverNode,
|
||||||
|
DelayNode,
|
||||||
|
DynamicsCompressorNode,
|
||||||
|
GainNode,
|
||||||
|
IIRFilterNode,
|
||||||
|
OscillatorNode,
|
||||||
|
PannerNode,
|
||||||
|
StereoPannerNode,
|
||||||
|
WaveShaperNode,
|
||||||
|
];
|
||||||
|
audioNodes.map((n) => {
|
||||||
|
if (n.prototype instanceof AudioScheduledSourceNode) {
|
||||||
|
const stopOrig = n.prototype.stop;
|
||||||
|
n.prototype.stop = function (...args) {
|
||||||
|
// stop called
|
||||||
|
const result = stopOrig.call(this, ...args);
|
||||||
|
const s = cache.get(this.audioid);
|
||||||
|
s.stopCount++;
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
audioNodeHook(n);
|
||||||
|
});
|
||||||
|
|
||||||
|
// patch BaseAudioContext factory methods
|
||||||
|
// to capture the source reference
|
||||||
|
Object.getOwnPropertyNames(BaseAudioContext.prototype)
|
||||||
|
.filter((n) => n.startsWith('create') && ['createBuffer'].indexOf(n) === -1)
|
||||||
|
.map((name) => {
|
||||||
|
const orig = BaseAudioContext.prototype[name];
|
||||||
|
BaseAudioContext.prototype[name] = function (...args) {
|
||||||
|
const result = orig.call(this, ...args);
|
||||||
|
const s = cache.get(result.audioid);
|
||||||
|
s.creation = stackTrace();
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const connectOrig = AudioNode.prototype.connect;
|
||||||
|
AudioNode.prototype.connect = function (destination, ...args) {
|
||||||
|
const result = connectOrig.call(this, destination, ...args);
|
||||||
|
const s = cache.get(this.audioid);
|
||||||
|
s.connect.push(destination.audioid);
|
||||||
|
s.where.push(stackTrace());
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const disconnectOrig = AudioNode.prototype.disconnect;
|
||||||
|
AudioNode.prototype.disconnect = function (destination, ...args) {
|
||||||
|
const result = disconnectOrig.call(this, destination, ...args);
|
||||||
|
const s = cache.get(this.audioid);
|
||||||
|
if (s.connect.length) {
|
||||||
|
if (destination) {
|
||||||
|
s.disconnectOne++;
|
||||||
|
} else {
|
||||||
|
s.disconnectAll++;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger('WEIRD: node ' + this.audioid + 'called disconnect before any call to connect !');
|
||||||
|
//logger(new Error().stack);
|
||||||
|
console.log(cache);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
// call reset 2 times to handle reload + 'play'
|
||||||
|
// the first reset's disconnect adds audioid tags on previous outputs
|
||||||
|
// that were not tagged (wrong cutoff)
|
||||||
|
resetAudioOutput(audioid);
|
||||||
|
// the second reset has the correct audioid cutoff
|
||||||
|
resetAudioOutput(audioid);
|
||||||
|
|
||||||
|
////////////////////////////////////////
|
||||||
|
// step 2: Load external modules
|
||||||
|
////////////////////////////////////////
|
||||||
|
|
||||||
|
const { default: mermaidModule } = await import(
|
||||||
|
'https://cdn.jsdelivr.net/npm/mermaid@11.12.1/dist/mermaid.esm.mjs'
|
||||||
|
);
|
||||||
|
mermaid = mermaidModule;
|
||||||
|
|
||||||
|
mermaid.initialize({
|
||||||
|
startOnLoad: false,
|
||||||
|
themeCSS: '.flowchart { height: 100%; }',
|
||||||
|
maxEdges: maxEdges,
|
||||||
|
maxTextSize: maxTextSize,
|
||||||
|
htmlLabels: false,
|
||||||
|
flowchart: {
|
||||||
|
htmlLabels: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { default: svgPanZoomModule } = await import('https://esm.sh/svg-pan-zoom');
|
||||||
|
svgPanZoom = svgPanZoomModule;
|
||||||
|
|
||||||
|
//////////////////////////////////////////
|
||||||
|
// step 3: UI modifications
|
||||||
|
//////////////////////////////////////////
|
||||||
|
|
||||||
|
// add audiograph panel
|
||||||
|
if (!document.querySelector('.strudel-mermaid')) {
|
||||||
|
const mermaidDiv = document.createElement('div');
|
||||||
|
mermaidDiv.className = 'strudel-mermaid';
|
||||||
|
mermaidDiv.style = 'min-height: 600px; width: 60%';
|
||||||
|
const referenceNode = document.querySelector('#code');
|
||||||
|
referenceNode.parentNode.insertBefore(mermaidDiv, referenceNode.nextSibling);
|
||||||
|
}
|
||||||
|
|
||||||
|
// add svg export button
|
||||||
|
if (!document.querySelector('button[title=svg]')) {
|
||||||
|
const exportButton = document.createElement('button');
|
||||||
|
exportButton.innerHTML = '<span>ExportDiagram</span>';
|
||||||
|
exportButton.title = 'svg';
|
||||||
|
exportButton.onclick = svgExport;
|
||||||
|
const updateButton = document.querySelector('button[title=update]');
|
||||||
|
updateButton.parentNode.insertBefore(exportButton, updateButton);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!running) {
|
||||||
|
running = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hapsBatch === 0 || hap_count < hapsBatch) {
|
||||||
|
let msg = '';
|
||||||
|
msg += 'Recording activity...';
|
||||||
|
msg += '\npress stop to build diagram';
|
||||||
|
if (StopAfterHapCount) {
|
||||||
|
msg += '\nwill stop automatically in ' + Math.max(StopAfterHapCount - hap_count, 0) + ' haps';
|
||||||
|
}
|
||||||
|
await drawMessage(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
sm.toggle = async () => {
|
||||||
|
running = false;
|
||||||
|
sm.toggle = toggleOrig;
|
||||||
|
// schedule `toggle` on the js main loop
|
||||||
|
// to avoid interfering with any on-flight onTick
|
||||||
|
// not doing this can lead to a phase > 0 which will
|
||||||
|
// break the next start
|
||||||
|
setTimeout(sm.toggle.bind(sm), 0);
|
||||||
|
await drawMessage('please wait ' + audioAPIBreathingRoomSec + ' seconds\n' + 'the audio API is finishing its work');
|
||||||
|
setTimeout(postProcessing, audioAPIBreathingRoomSec * 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
/*global all*/
|
||||||
|
all((pat) =>
|
||||||
|
pat.onTrigger(async (hap, duration, cps, t) => {
|
||||||
|
hap_count++;
|
||||||
|
const key = Object.entries(hap.value)
|
||||||
|
.map((param) => param.join('/'))
|
||||||
|
.join('/');
|
||||||
|
|
||||||
|
// if we reached StopAfterHapCount, click 'stop'
|
||||||
|
if (StopAfterHapCount && hap_count > StopAfterHapCount) {
|
||||||
|
if (running) {
|
||||||
|
await sm.toggle();
|
||||||
|
}
|
||||||
|
// stop sending haps to superdough(...)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await webaudioOutput(hap, t, hap.duration / cps, cps, t);
|
||||||
|
|
||||||
|
if (hapsBatch && hap_count % hapsBatch === 0) drawDiagram();
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
import PlayCircleIcon from '@heroicons/react/20/solid/PlayCircleIcon';
|
||||||
|
import cx from '@src/cx.mjs';
|
||||||
|
import NumberInput from '@src/repl/components/NumberInput';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { Textbox } from '../textbox/Textbox';
|
||||||
|
import { getAudioContext } from '@strudel/webaudio';
|
||||||
|
import XMarkIcon from '@heroicons/react/24/outline/XMarkIcon';
|
||||||
|
|
||||||
|
function Checkbox({ label, value, onChange, disabled = false }) {
|
||||||
|
return (
|
||||||
|
<label className={cx(disabled && 'opacity-50')}>
|
||||||
|
<input disabled={disabled} type="checkbox" checked={value} onChange={onChange} />
|
||||||
|
{' ' + label}
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function FormItem({ label, children, disabled }) {
|
||||||
|
return (
|
||||||
|
<div className="grid gap-2 w-full">
|
||||||
|
<label className={cx(disabled && 'opacity-50')}>{label}</label>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ExportTab(Props) {
|
||||||
|
const { handleExport } = Props;
|
||||||
|
|
||||||
|
const [downloadName, setDownloadName] = useState('');
|
||||||
|
const [startCycle, setStartCycle] = useState(0);
|
||||||
|
const [endCycle, setEndCycle] = useState(1);
|
||||||
|
const [sampleRate, setSampleRate] = useState(48000);
|
||||||
|
const [multiChannelOrbits, setMultiChannelOrbits] = useState(true);
|
||||||
|
const [maxPolyphony, setMaxPolyphony] = useState(1024);
|
||||||
|
const [exporting, setExporting] = useState(false);
|
||||||
|
const [progress, setProgress] = useState(0);
|
||||||
|
const [length, setLength] = useState(1);
|
||||||
|
|
||||||
|
const refreshProgress = () => {
|
||||||
|
const audioContext = getAudioContext();
|
||||||
|
if (audioContext instanceof OfflineAudioContext) {
|
||||||
|
setProgress(audioContext.currentTime);
|
||||||
|
setLength(audioContext.length / sampleRate);
|
||||||
|
setTimeout(refreshProgress, 100);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="text-foreground w-full p-4 space-y-4">
|
||||||
|
<FormItem label="File name" disabled={exporting}>
|
||||||
|
<Textbox
|
||||||
|
onBlur={(e) => {
|
||||||
|
setDownloadName(e.target.value);
|
||||||
|
}}
|
||||||
|
onChange={(v) => {
|
||||||
|
setDownloadName(v);
|
||||||
|
}}
|
||||||
|
disabled={exporting}
|
||||||
|
placeholder="Leave empty to use current date"
|
||||||
|
className={cx('placeholder:opacity-50', exporting && 'opacity-50 border-opacity-50')}
|
||||||
|
value={downloadName ?? ''}
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
<div className="flex flex-row gap-4 w-full">
|
||||||
|
<FormItem label="Start cycle" disabled={exporting}>
|
||||||
|
<Textbox
|
||||||
|
min={1}
|
||||||
|
max={Infinity}
|
||||||
|
onBlur={(e) => {
|
||||||
|
let v = parseInt(e.target.value);
|
||||||
|
v = isNaN(v) ? 0 : Math.max(0, v);
|
||||||
|
setStartCycle(v);
|
||||||
|
}}
|
||||||
|
onChange={(v) => {
|
||||||
|
v = parseInt(v);
|
||||||
|
setStartCycle(v);
|
||||||
|
}}
|
||||||
|
type="number"
|
||||||
|
placeholder=""
|
||||||
|
disabled={exporting}
|
||||||
|
className={cx(exporting && 'opacity-50 border-opacity-50', 'w-full')}
|
||||||
|
value={startCycle ?? ''}
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
<FormItem label="End cycle" disabled={exporting}>
|
||||||
|
<Textbox
|
||||||
|
min={1}
|
||||||
|
max={Infinity}
|
||||||
|
onBlur={(e) => {
|
||||||
|
let v = parseInt(e.target.value);
|
||||||
|
v = isNaN(v) ? Math.max(startCycle + 1, parseInt(v)) : v;
|
||||||
|
setEndCycle(v);
|
||||||
|
}}
|
||||||
|
onChange={(v) => {
|
||||||
|
v = parseInt(v);
|
||||||
|
setEndCycle(v);
|
||||||
|
}}
|
||||||
|
type="number"
|
||||||
|
placeholder=""
|
||||||
|
disabled={exporting}
|
||||||
|
className={cx(exporting && 'opacity-50 border-opacity-50', 'w-full')}
|
||||||
|
value={endCycle ?? ''}
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row gap-4">
|
||||||
|
<FormItem label="Sample rate" disabled={exporting}>
|
||||||
|
<Textbox
|
||||||
|
min={1}
|
||||||
|
max={Infinity}
|
||||||
|
onBlur={(e) => {
|
||||||
|
let v = parseInt(e.target.value);
|
||||||
|
v = isNaN(v) ? 1 : Math.max(1, v);
|
||||||
|
setSampleRate(v);
|
||||||
|
}}
|
||||||
|
onChange={(v) => {
|
||||||
|
v = parseInt(v);
|
||||||
|
setSampleRate(v);
|
||||||
|
}}
|
||||||
|
type="number"
|
||||||
|
placeholder=""
|
||||||
|
disabled={exporting}
|
||||||
|
className={cx(exporting && 'opacity-50 border-opacity-50')}
|
||||||
|
value={sampleRate ?? ''}
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
<FormItem label="Maximum polyphony" disabled={exporting}>
|
||||||
|
<Textbox
|
||||||
|
min={1}
|
||||||
|
max={Infinity}
|
||||||
|
onBlur={(e) => {
|
||||||
|
let v = parseInt(e.target.value);
|
||||||
|
v = isNaN(v) ? Math.max(1, parseInt(v)) : v;
|
||||||
|
setMaxPolyphony(v);
|
||||||
|
}}
|
||||||
|
onChange={(v) => {
|
||||||
|
v = Math.max(1, parseInt(v));
|
||||||
|
setMaxPolyphony(v);
|
||||||
|
}}
|
||||||
|
type="number"
|
||||||
|
placeholder=""
|
||||||
|
disabled={exporting}
|
||||||
|
className={cx(exporting && 'opacity-50 border-opacity-50')}
|
||||||
|
value={maxPolyphony ?? ''}
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Checkbox
|
||||||
|
label="Multi Channel Orbits"
|
||||||
|
onChange={(cbEvent) => {
|
||||||
|
const val = cbEvent.target.checked;
|
||||||
|
setMultiChannelOrbits(val);
|
||||||
|
}}
|
||||||
|
disabled={exporting}
|
||||||
|
value={multiChannelOrbits}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className={cx('bg-background p-2 w-full rounded-md hover:opacity-75 relative', exporting && 'opacity-50')}
|
||||||
|
disabled={exporting}
|
||||||
|
onClick={async () => {
|
||||||
|
setExporting(true);
|
||||||
|
setTimeout(refreshProgress, 2000);
|
||||||
|
const modal = document.getElementById('exportProgressModal');
|
||||||
|
modal.showModal();
|
||||||
|
await handleExport(startCycle, endCycle, sampleRate, maxPolyphony, multiChannelOrbits, downloadName)
|
||||||
|
.then(() => {
|
||||||
|
const modal = document.getElementById('exportProgressModal');
|
||||||
|
modal.close();
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setExporting(false);
|
||||||
|
setProgress(0);
|
||||||
|
setLength(1);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="absolute top-0 left-0 right-0 bottom-0 backdrop-invert"
|
||||||
|
style={{
|
||||||
|
width: `${(exporting ? 1 : 0) + (progress / length) * 99}%`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span className="text-foreground">{exporting ? 'Exporting...' : 'Export to WAV'}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<dialog
|
||||||
|
closedby={exporting ? 'none' : 'closerequest'}
|
||||||
|
id="exportProgressModal"
|
||||||
|
className="text-md bg-background text-foreground rounded-lg backdrop:bg-background backdrop:opacity-25"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import { useLogger } from '../useLogger';
|
|||||||
import { WelcomeTab } from './WelcomeTab';
|
import { WelcomeTab } from './WelcomeTab';
|
||||||
import { PatternsTab } from './PatternsTab';
|
import { PatternsTab } from './PatternsTab';
|
||||||
import { ChevronLeftIcon, XMarkIcon } from '@heroicons/react/16/solid';
|
import { ChevronLeftIcon, XMarkIcon } from '@heroicons/react/16/solid';
|
||||||
|
import ExportTab from './ExportTab';
|
||||||
|
|
||||||
const TAURI = typeof window !== 'undefined' && window.__TAURI__;
|
const TAURI = typeof window !== 'undefined' && window.__TAURI__;
|
||||||
|
|
||||||
@@ -80,6 +81,7 @@ const tabNames = {
|
|||||||
patterns: 'patterns',
|
patterns: 'patterns',
|
||||||
sounds: 'sounds',
|
sounds: 'sounds',
|
||||||
reference: 'reference',
|
reference: 'reference',
|
||||||
|
export: 'export',
|
||||||
console: 'console',
|
console: 'console',
|
||||||
settings: 'settings',
|
settings: 'settings',
|
||||||
};
|
};
|
||||||
@@ -126,6 +128,8 @@ function PanelContent({ context, tab }) {
|
|||||||
return <SoundsTab />;
|
return <SoundsTab />;
|
||||||
case tabNames.reference:
|
case tabNames.reference:
|
||||||
return <Reference />;
|
return <Reference />;
|
||||||
|
case tabNames.export:
|
||||||
|
return <ExportTab handleExport={context.handleExport} />;
|
||||||
case tabNames.settings:
|
case tabNames.settings:
|
||||||
return <SettingsTab started={context.started} />;
|
return <SettingsTab started={context.started} />;
|
||||||
case tabNames.files:
|
case tabNames.files:
|
||||||
|
|||||||
@@ -393,6 +393,8 @@ samples({
|
|||||||
bass: { d2: 'https://cdn.freesound.org/previews/608/608286_13074022-lq.mp3' }
|
bass: { d2: 'https://cdn.freesound.org/previews/608/608286_13074022-lq.mp3' }
|
||||||
})
|
})
|
||||||
|
|
||||||
|
useRNG('legacy')
|
||||||
|
|
||||||
stack(
|
stack(
|
||||||
// bells
|
// bells
|
||||||
n("0").euclidLegato(3,8)
|
n("0").euclidLegato(3,8)
|
||||||
@@ -430,6 +432,7 @@ export const festivalOfFingers3 = `// "Festival of fingers 3"
|
|||||||
// @by Felix Roos
|
// @by Felix Roos
|
||||||
|
|
||||||
setcps(1)
|
setcps(1)
|
||||||
|
useRNG('legacy')
|
||||||
|
|
||||||
n("[-7*3],0,2,6,[8 7]")
|
n("[-7*3],0,2,6,[8 7]")
|
||||||
.echoWith(
|
.echoWith(
|
||||||
@@ -454,6 +457,8 @@ export const meltingsubmarine = `// "Melting submarine"
|
|||||||
// @by Felix Roos
|
// @by Felix Roos
|
||||||
|
|
||||||
samples('github:tidalcycles/dirt-samples')
|
samples('github:tidalcycles/dirt-samples')
|
||||||
|
useRNG('legacy')
|
||||||
|
|
||||||
stack(
|
stack(
|
||||||
s("bd:5,[~ <sd:1!3 sd:1(3,4,3)>],hh27(3,4,1)") // drums
|
s("bd:5,[~ <sd:1!3 sd:1(3,4,3)>],hh27(3,4,1)") // drums
|
||||||
.speed(perlin.range(.7,.9)) // random sample speed variation
|
.speed(perlin.range(.7,.9)) // random sample speed variation
|
||||||
@@ -602,6 +607,9 @@ export const belldub = `// "Belldub"
|
|||||||
|
|
||||||
samples({ bell: {b4:'https://cdn.freesound.org/previews/339/339809_5121236-lq.mp3'}})
|
samples({ bell: {b4:'https://cdn.freesound.org/previews/339/339809_5121236-lq.mp3'}})
|
||||||
// "Hand Bells, B, Single.wav" by InspectorJ (www.jshaw.co.uk) of Freesound.org
|
// "Hand Bells, B, Single.wav" by InspectorJ (www.jshaw.co.uk) of Freesound.org
|
||||||
|
|
||||||
|
useRNG('legacy')
|
||||||
|
|
||||||
stack(
|
stack(
|
||||||
// bass
|
// bass
|
||||||
note("[0 ~] [2 [0 2]] [4 4*2] [[4 ~] [2 ~] 0@2]".scale('g1 dorian').superimpose(x=>x.add(.02)))
|
note("[0 ~] [2 [0 2]] [4 4*2] [[4 ~] [2 ~] 0@2]".scale('g1 dorian').superimpose(x=>x.add(.02)))
|
||||||
@@ -638,6 +646,7 @@ export const dinofunk = `// "Dinofunk"
|
|||||||
// @by Felix Roos
|
// @by Felix Roos
|
||||||
|
|
||||||
setcps(1)
|
setcps(1)
|
||||||
|
useRNG('legacy')
|
||||||
|
|
||||||
samples({bass:'https://cdn.freesound.org/previews/614/614637_2434927-hq.mp3',
|
samples({bass:'https://cdn.freesound.org/previews/614/614637_2434927-hq.mp3',
|
||||||
dino:{b4:'https://cdn.freesound.org/previews/316/316403_5123851-hq.mp3'}})
|
dino:{b4:'https://cdn.freesound.org/previews/316/316403_5123851-hq.mp3'}})
|
||||||
@@ -666,6 +675,8 @@ export const sampleDemo = `// "Sample demo"
|
|||||||
// @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
|
// @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
|
||||||
// @by Felix Roos
|
// @by Felix Roos
|
||||||
|
|
||||||
|
useRNG('legacy')
|
||||||
|
|
||||||
stack(
|
stack(
|
||||||
// percussion
|
// percussion
|
||||||
s("[woodblock:1 woodblock:2*2] snare_rim:0,gong/8,brakedrum:1(3,8),~@3 cowbell:3")
|
s("[woodblock:1 woodblock:2*2] snare_rim:0,gong/8,brakedrum:1(3,8),~@3 cowbell:3")
|
||||||
@@ -684,6 +695,8 @@ export const holyflute = `// "Holy flute"
|
|||||||
// @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
|
// @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
|
||||||
// @by Felix Roos
|
// @by Felix Roos
|
||||||
|
|
||||||
|
useRNG('legacy')
|
||||||
|
|
||||||
"c3 eb3(3,8) c4/2 g3*2"
|
"c3 eb3(3,8) c4/2 g3*2"
|
||||||
.superimpose(
|
.superimpose(
|
||||||
x=>x.slow(2).add(12),
|
x=>x.slow(2).add(12),
|
||||||
@@ -699,6 +712,8 @@ export const flatrave = `// "Flatrave"
|
|||||||
// @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
|
// @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
|
||||||
// @by Felix Roos
|
// @by Felix Roos
|
||||||
|
|
||||||
|
useRNG('legacy')
|
||||||
|
|
||||||
stack(
|
stack(
|
||||||
s("bd*2,~ [cp,sd]").bank('RolandTR909'),
|
s("bd*2,~ [cp,sd]").bank('RolandTR909'),
|
||||||
|
|
||||||
@@ -727,6 +742,8 @@ export const amensister = `// "Amensister"
|
|||||||
|
|
||||||
samples('github:tidalcycles/dirt-samples')
|
samples('github:tidalcycles/dirt-samples')
|
||||||
|
|
||||||
|
useRNG('legacy')
|
||||||
|
|
||||||
stack(
|
stack(
|
||||||
// amen
|
// amen
|
||||||
n("0 1 2 3 4 5 6 7")
|
n("0 1 2 3 4 5 6 7")
|
||||||
@@ -834,6 +851,8 @@ export const arpoon = `// "Arpoon"
|
|||||||
// @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
|
// @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
|
||||||
// @by Felix Roos
|
// @by Felix Roos
|
||||||
|
|
||||||
|
useRNG('legacy')
|
||||||
|
|
||||||
samples('github:tidalcycles/dirt-samples')
|
samples('github:tidalcycles/dirt-samples')
|
||||||
|
|
||||||
n("[0,3] 2 [1,3] 2".fast(3).lastOf(4, fast(2))).clip(2)
|
n("[0,3] 2 [1,3] 2".fast(3).lastOf(4, fast(2))).clip(2)
|
||||||
|
|||||||
@@ -9,11 +9,13 @@ import { getDrawContext } from '@strudel/draw';
|
|||||||
import { evaluate, transpiler } from '@strudel/transpiler';
|
import { evaluate, transpiler } from '@strudel/transpiler';
|
||||||
import {
|
import {
|
||||||
getAudioContextCurrentTime,
|
getAudioContextCurrentTime,
|
||||||
|
renderPatternAudio,
|
||||||
webaudioOutput,
|
webaudioOutput,
|
||||||
resetGlobalEffects,
|
resetGlobalEffects,
|
||||||
resetLoadedSounds,
|
resetLoadedSounds,
|
||||||
initAudioOnFirstClick,
|
initAudioOnFirstClick,
|
||||||
resetDefaults,
|
resetDefaults,
|
||||||
|
initAudio,
|
||||||
} from '@strudel/webaudio';
|
} from '@strudel/webaudio';
|
||||||
import { setVersionDefaultsFrom } from './util.mjs';
|
import { setVersionDefaultsFrom } from './util.mjs';
|
||||||
import { StrudelMirror, defaultSettings } from '@strudel/codemirror';
|
import { StrudelMirror, defaultSettings } from '@strudel/codemirror';
|
||||||
@@ -36,6 +38,7 @@ import { getRandomTune, initCode, loadModules, shareCode } from './util.mjs';
|
|||||||
import './Repl.css';
|
import './Repl.css';
|
||||||
import { setInterval, clearInterval } from 'worker-timers';
|
import { setInterval, clearInterval } from 'worker-timers';
|
||||||
import { getMetadata } from '../metadata_parser';
|
import { getMetadata } from '../metadata_parser';
|
||||||
|
import { debugAudiograph } from './audiograph';
|
||||||
|
|
||||||
const { latestCode, maxPolyphony, audioDeviceName, multiChannelOrbits } = settingsMap.get();
|
const { latestCode, maxPolyphony, audioDeviceName, multiChannelOrbits } = settingsMap.get();
|
||||||
let modulesLoading, presets, drawContext, clearCanvas, audioReady;
|
let modulesLoading, presets, drawContext, clearCanvas, audioReady;
|
||||||
@@ -129,6 +132,7 @@ export function useReplContext() {
|
|||||||
bgFill: false,
|
bgFill: false,
|
||||||
});
|
});
|
||||||
window.strudelMirror = editor;
|
window.strudelMirror = editor;
|
||||||
|
window.debugAudiograph = debugAudiograph;
|
||||||
|
|
||||||
// init settings
|
// init settings
|
||||||
initCode().then(async (decoded) => {
|
initCode().then(async (decoded) => {
|
||||||
@@ -207,6 +211,30 @@ export function useReplContext() {
|
|||||||
const handleEvaluate = () => {
|
const handleEvaluate = () => {
|
||||||
editorRef.current.evaluate();
|
editorRef.current.evaluate();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleExport = async (begin, end, sampleRate, maxPolyphony, multiChannelOrbits, downloadName = undefined) => {
|
||||||
|
await editorRef.current.evaluate(false);
|
||||||
|
editorRef.current.repl.scheduler.stop();
|
||||||
|
await renderPatternAudio(
|
||||||
|
editorRef.current.repl.state.pattern,
|
||||||
|
editorRef.current.repl.scheduler.cps,
|
||||||
|
begin,
|
||||||
|
end,
|
||||||
|
sampleRate,
|
||||||
|
maxPolyphony,
|
||||||
|
multiChannelOrbits,
|
||||||
|
downloadName,
|
||||||
|
).finally(async () => {
|
||||||
|
const { latestCode, maxPolyphony, audioDeviceName, multiChannelOrbits } = settingsMap.get();
|
||||||
|
await initAudio({
|
||||||
|
latestCode,
|
||||||
|
maxPolyphony,
|
||||||
|
audioDeviceName,
|
||||||
|
multiChannelOrbits,
|
||||||
|
});
|
||||||
|
editorRef.current.repl.scheduler.stop();
|
||||||
|
});
|
||||||
|
};
|
||||||
const handleShuffle = async () => {
|
const handleShuffle = async () => {
|
||||||
const patternData = await getRandomTune();
|
const patternData = await getRandomTune();
|
||||||
const code = patternData.code;
|
const code = patternData.code;
|
||||||
@@ -235,6 +263,7 @@ export function useReplContext() {
|
|||||||
handleShuffle,
|
handleShuffle,
|
||||||
handleShare,
|
handleShare,
|
||||||
handleEvaluate,
|
handleEvaluate,
|
||||||
|
handleExport,
|
||||||
init,
|
init,
|
||||||
error,
|
error,
|
||||||
editorRef,
|
editorRef,
|
||||||
|
|||||||
Reference in New Issue
Block a user