mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-17 08:06:50 -04:00
Compare commits
92 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9782795761 | |||
| 2d2b238da9 | |||
| f0839e02eb | |||
| a9f7d825c8 | |||
| 709b654ed3 | |||
| d81a214c74 | |||
| e57f240e70 | |||
| e18296e760 | |||
| c145355ccc | |||
| a46617f3f0 | |||
| d164b5e7f5 | |||
| c0bf86d11e | |||
| 6f06683d70 | |||
| 637d714ab2 | |||
| c42d00f048 | |||
| 702e558d1a | |||
| bcc2e8f759 | |||
| 27339715b7 | |||
| 5e2e6411e5 | |||
| 0fac16e769 | |||
| c199b51645 | |||
| 8d2a368da9 | |||
| 052d09e892 | |||
| 439a7dc5e6 | |||
| b8e1b8d7ac | |||
| a26559fd3d | |||
| 5448fc6077 | |||
| 06fafbd8eb | |||
| 10156f0067 | |||
| 5632d83afb | |||
| a32473d570 | |||
| e87bb4376f | |||
| 0722cf7ded | |||
| 37fde8f3e8 | |||
| 8655c5b73c | |||
| 31c1450edc | |||
| cbb4207bab | |||
| 5efcd14f40 | |||
| 8051e56d13 | |||
| 239987af0b | |||
| 8363ba5a41 | |||
| 30f7127a92 | |||
| 14193abe03 | |||
| 3caf08e199 | |||
| 5cbd38a3e8 | |||
| bbd7ed0f27 | |||
| 43504d16b6 | |||
| 7e8206dcbc | |||
| 3963b807eb | |||
| e99adffc56 | |||
| c9f494d865 | |||
| e8254735bb | |||
| 3d30007756 | |||
| aeaed50446 | |||
| 3ac0fdf619 | |||
| d1b6b6be85 | |||
| 1081fc69e9 | |||
| 73ef43dd2c | |||
| 181e51ef0e | |||
| 1167fdb910 | |||
| ec9109dd89 | |||
| 55aef18851 | |||
| 356d4360ec | |||
| 0f2aa9569b | |||
| ac7ee376e5 | |||
| ab052ce1ac | |||
| 5988e3d3b4 | |||
| 2f2bf5bf29 | |||
| b000f2297d | |||
| 77463a199a | |||
| e54449f5f7 | |||
| d743570546 | |||
| 1dac69635d | |||
| 49f38aeddb | |||
| 43c05cb504 | |||
| ccc90d547c | |||
| 998a010247 | |||
| e4cd5dc552 | |||
| 6cf0b6f651 | |||
| 794bf86904 | |||
| 9ed690ce45 | |||
| 623bf93368 | |||
| 0cc18a6468 | |||
| 87598e10d1 | |||
| ccec7e725a | |||
| f0e6c5483c | |||
| 2cbd35f6d2 | |||
| 930dabe4d6 | |||
| ff4b7ca171 | |||
| 5129fa6677 | |||
| 5336569c25 | |||
| 483843bd0a |
+21
@@ -0,0 +1,21 @@
|
||||
FROM node:24
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN npm install pnpm --global
|
||||
|
||||
COPY pnpm-workspace.yaml ./
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
COPY packages/ ./packages/
|
||||
COPY examples/ ./examples/
|
||||
RUN mkdir -p website/public
|
||||
COPY website/package.json ./website/
|
||||
|
||||
RUN pnpm install
|
||||
|
||||
|
||||
COPY . .
|
||||
|
||||
EXPOSE 4321
|
||||
|
||||
CMD ["pnpm", "dev"]
|
||||
@@ -42,6 +42,7 @@ export default [
|
||||
'**/hydra.mjs',
|
||||
'**/jsdoc-synonyms.js',
|
||||
'packages/hs2js/src/hs2js.mjs',
|
||||
'packages/supradough/dough-export.mjs',
|
||||
'**/samples',
|
||||
],
|
||||
},
|
||||
|
||||
@@ -54,11 +54,12 @@ const buildExamples = (examples) =>
|
||||
`
|
||||
: '';
|
||||
|
||||
export const Autocomplete = ({ doc, label }) =>
|
||||
export const Autocomplete = (doc) =>
|
||||
h`
|
||||
<div class="autocomplete-info-container">
|
||||
<div class="autocomplete-info-tooltip">
|
||||
<h3 class="autocomplete-info-function-name">${label || getDocLabel(doc)}</h3>
|
||||
<h3 class="autocomplete-info-function-name">${getDocLabel(doc)}</h3>
|
||||
${doc.synonyms_text ? `<div class="autocomplete-info-function-synonyms">Synonyms: ${doc.synonyms_text}</div>` : ''}
|
||||
${doc.description ? `<div class="autocomplete-info-function-description">${doc.description}</div>` : ''}
|
||||
${buildParamsList(doc.params)}
|
||||
${buildExamples(doc.examples)}
|
||||
@@ -74,15 +75,43 @@ const isValidDoc = (doc) => {
|
||||
const hasExcludedTags = (doc) =>
|
||||
['superdirtOnly', 'noAutocomplete'].some((tag) => doc.tags?.find((t) => t.originalTitle === tag));
|
||||
|
||||
const jsdocCompletions = jsdoc.docs
|
||||
.filter((doc) => isValidDoc(doc) && !hasExcludedTags(doc))
|
||||
// https://codemirror.net/docs/ref/#autocomplete.Completion
|
||||
.map((doc) => ({
|
||||
label: getDocLabel(doc),
|
||||
// detail: 'xxx', // An optional short piece of information to show (with a different style) after the label.
|
||||
info: () => Autocomplete({ doc }),
|
||||
type: 'function', // https://codemirror.net/docs/ref/#autocomplete.Completion.type
|
||||
}));
|
||||
export const getSynonymDoc = (doc, synonym) => {
|
||||
const synonyms = doc.synonyms || [];
|
||||
const docLabel = getDocLabel(doc);
|
||||
// Swap `doc.name` in for `s` in the list of synonyms
|
||||
const synonymsWithDoc = [docLabel, ...synonyms].filter((x) => x && x !== synonym);
|
||||
return {
|
||||
...doc,
|
||||
name: synonym,
|
||||
longname: synonym,
|
||||
synonyms: synonymsWithDoc,
|
||||
synonyms_text: synonymsWithDoc.join(', '),
|
||||
};
|
||||
};
|
||||
|
||||
const jsdocCompletions = (() => {
|
||||
const seen = new Set(); // avoid repetition
|
||||
const completions = [];
|
||||
for (const doc of jsdoc.docs) {
|
||||
if (!isValidDoc(doc) || hasExcludedTags(doc)) continue;
|
||||
const docLabel = getDocLabel(doc);
|
||||
// Remove duplicates
|
||||
const synonyms = doc.synonyms || [];
|
||||
let labels = [docLabel, ...synonyms];
|
||||
for (const label of labels) {
|
||||
// https://codemirror.net/docs/ref/#autocomplete.Completion
|
||||
if (label && !seen.has(label)) {
|
||||
seen.add(label);
|
||||
completions.push({
|
||||
label,
|
||||
info: () => Autocomplete(getSynonymDoc(doc, label)),
|
||||
type: 'function', // https://codemirror.net/docs/ref/#autocomplete.Completion.type
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return completions;
|
||||
})();
|
||||
|
||||
export const strudelAutocomplete = (context) => {
|
||||
const word = context.matchBefore(/\w*/);
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import {
|
||||
keymap,
|
||||
highlightSpecialChars,
|
||||
drawSelection,
|
||||
highlightActiveLine,
|
||||
dropCursor,
|
||||
rectangularSelection,
|
||||
crosshairCursor,
|
||||
lineNumbers,
|
||||
highlightActiveLineGutter,
|
||||
} from '@codemirror/view';
|
||||
import {
|
||||
defaultHighlightStyle,
|
||||
syntaxHighlighting,
|
||||
bracketMatching,
|
||||
foldGutter,
|
||||
foldKeymap,
|
||||
} from '@codemirror/language';
|
||||
import { defaultKeymap, history, historyKeymap } from '@codemirror/commands';
|
||||
import { searchKeymap, highlightSelectionMatches } from '@codemirror/search';
|
||||
import { completionKeymap, closeBracketsKeymap } from '@codemirror/autocomplete';
|
||||
|
||||
// Taken + slightly modified from https://github.com/codemirror/basic-setup/blob/main/src/codemirror.ts
|
||||
|
||||
export const basicSetup = (() => [
|
||||
// lineNumbers(),
|
||||
// highlightActiveLineGutter(),
|
||||
highlightSpecialChars(),
|
||||
history(),
|
||||
// foldGutter(),
|
||||
// drawSelection(),
|
||||
dropCursor(),
|
||||
// EditorState.allowMultipleSelections.of(true),
|
||||
// indentOnInput(),
|
||||
// syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
|
||||
// autocompletion(),
|
||||
rectangularSelection(),
|
||||
crosshairCursor(),
|
||||
// highlightActiveLine(),
|
||||
// highlightSelectionMatches(),
|
||||
keymap.of([
|
||||
...closeBracketsKeymap,
|
||||
...defaultKeymap,
|
||||
// ...searchKeymap,
|
||||
...historyKeymap,
|
||||
// ...foldKeymap,
|
||||
// ...completionKeymap,
|
||||
]),
|
||||
])();
|
||||
|
||||
/// A minimal set of extensions to create a functional editor. Only
|
||||
/// includes [the default keymap](#commands.defaultKeymap), [undo
|
||||
/// history](#commands.history), [special character
|
||||
/// highlighting](#view.highlightSpecialChars), [custom selection
|
||||
/// drawing](#view.drawSelection), and [default highlight
|
||||
/// style](#language.defaultHighlightStyle).
|
||||
export const minimalSetup = (() => [
|
||||
highlightSpecialChars(),
|
||||
history(),
|
||||
drawSelection(),
|
||||
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
|
||||
keymap.of([...defaultKeymap, ...historyKeymap]),
|
||||
])();
|
||||
@@ -1,8 +1,8 @@
|
||||
import { closeBrackets } from '@codemirror/autocomplete';
|
||||
export { toggleComment, toggleBlockComment, toggleLineComment, toggleBlockCommentByLine } from '@codemirror/commands';
|
||||
// import { search, highlightSelectionMatches } from '@codemirror/search';
|
||||
import { history, indentWithTab } from '@codemirror/commands';
|
||||
import { javascript } from '@codemirror/lang-javascript';
|
||||
import { indentWithTab } from '@codemirror/commands';
|
||||
import { javascript, javascriptLanguage } from '@codemirror/lang-javascript';
|
||||
import { defaultHighlightStyle, syntaxHighlighting, bracketMatching } from '@codemirror/language';
|
||||
import { Compartment, EditorState, Prec } from '@codemirror/state';
|
||||
import {
|
||||
@@ -24,6 +24,7 @@ import { initTheme, activateTheme, theme } from './themes.mjs';
|
||||
import { sliderPlugin, updateSliderWidgets } from './slider.mjs';
|
||||
import { widgetPlugin, updateWidgets } from './widget.mjs';
|
||||
import { persistentAtom } from '@nanostores/persistent';
|
||||
import { basicSetup } from './basicSetup.mjs';
|
||||
|
||||
const extensions = {
|
||||
isLineWrappingEnabled: (on) => (on ? EditorView.lineWrapping : []),
|
||||
@@ -85,13 +86,17 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo
|
||||
/* search(),
|
||||
highlightSelectionMatches(), */
|
||||
...initialSettings,
|
||||
basicSetup,
|
||||
mondo ? [] : javascript(),
|
||||
javascriptLanguage.data.of({
|
||||
closeBrackets: { brackets: ['(', '[', '{', "'", '"', '<'] },
|
||||
bracketMatching: { brackets: ['(', '[', '{', "'", '"', '<'] },
|
||||
}),
|
||||
sliderPlugin,
|
||||
widgetPlugin,
|
||||
// indentOnInput(), // works without. already brought with javascript extension?
|
||||
// bracketMatching(), // does not do anything
|
||||
syntaxHighlighting(defaultHighlightStyle),
|
||||
history(),
|
||||
EditorView.updateListener.of((v) => onChange(v)),
|
||||
drawSelection({ cursorBlinkRate: 0 }),
|
||||
Prec.highest(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const parser = typeof DOMParser !== 'undefined' ? new DOMParser() : null;
|
||||
export let html = (string) => {
|
||||
return parser?.parseFromString(string, 'text/html').querySelectorAll('*');
|
||||
const template = document.createElement('template');
|
||||
template.innerHTML = string.trim();
|
||||
return template.content.childNodes;
|
||||
};
|
||||
let parseChunk = (chunk) => {
|
||||
if (Array.isArray(chunk)) return chunk.flat().join('');
|
||||
|
||||
@@ -3,8 +3,9 @@ import { keymap, ViewPlugin } from '@codemirror/view';
|
||||
// import { searchKeymap } from '@codemirror/search';
|
||||
import { emacs } from '@replit/codemirror-emacs';
|
||||
import { vim } from '@replit/codemirror-vim';
|
||||
// import { vim } from './vim_test.mjs';
|
||||
import { vscodeKeymap } from '@replit/codemirror-vscode-keymap';
|
||||
import { defaultKeymap, historyKeymap } from '@codemirror/commands';
|
||||
import { defaultKeymap } from '@codemirror/commands';
|
||||
|
||||
const vscodePlugin = ViewPlugin.fromClass(
|
||||
class {
|
||||
@@ -27,5 +28,5 @@ const keymaps = {
|
||||
|
||||
export function keybindings(name) {
|
||||
const active = keymaps[name];
|
||||
return [active ? active() : [], keymap.of(historyKeymap)];
|
||||
return [active ? Prec.high(active()) : []];
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/codemirror",
|
||||
"version": "1.2.4",
|
||||
"version": "1.2.5",
|
||||
"description": "Codemirror Extensions for Strudel",
|
||||
"main": "index.mjs",
|
||||
"publishConfig": {
|
||||
@@ -42,7 +42,7 @@
|
||||
"@lezer/highlight": "^1.2.1",
|
||||
"@nanostores/persistent": "^0.10.2",
|
||||
"@replit/codemirror-emacs": "^6.1.0",
|
||||
"@replit/codemirror-vim": "^6.2.1",
|
||||
"@replit/codemirror-vim": "^6.3.0",
|
||||
"@replit/codemirror-vscode-keymap": "^6.0.2",
|
||||
"@strudel/core": "workspace:*",
|
||||
"@strudel/draw": "workspace:*",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { hoverTooltip } from '@codemirror/view';
|
||||
import jsdoc from '../../doc.json';
|
||||
import { Autocomplete } from './autocomplete.mjs';
|
||||
import { Autocomplete, getSynonymDoc } from './autocomplete.mjs';
|
||||
|
||||
const getDocLabel = (doc) => doc.name || doc.longname;
|
||||
|
||||
@@ -52,10 +52,11 @@ export const strudelTooltip = hoverTooltip(
|
||||
let entry = jsdoc.docs.filter((doc) => getDocLabel(doc) === word)[0];
|
||||
if (!entry) {
|
||||
// Try for synonyms
|
||||
entry = jsdoc.docs.filter((doc) => doc.synonyms && doc.synonyms.includes(word))[0];
|
||||
if (!entry) {
|
||||
const doc = jsdoc.docs.filter((doc) => doc.synonyms && doc.synonyms.includes(word))[0];
|
||||
if (!doc) {
|
||||
return null;
|
||||
}
|
||||
entry = getSynonymDoc(doc, word);
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -66,7 +67,7 @@ export const strudelTooltip = hoverTooltip(
|
||||
create(view) {
|
||||
let dom = document.createElement('div');
|
||||
dom.className = 'strudel-tooltip';
|
||||
const ac = Autocomplete({ doc: entry, label: word });
|
||||
const ac = Autocomplete(entry);
|
||||
dom.appendChild(ac);
|
||||
return { dom };
|
||||
},
|
||||
|
||||
@@ -91,6 +91,7 @@ export const { s, sound } = registerControl(['s', 'n', 'gain'], 'sound');
|
||||
* Define a custom webaudio node to use as a sound source.
|
||||
*
|
||||
* @name source
|
||||
* @synonyms src
|
||||
* @param {function} getSource
|
||||
* @synonyms src
|
||||
*
|
||||
@@ -309,6 +310,17 @@ export const { fmvelocity } = registerControl('fmvelocity');
|
||||
*/
|
||||
export const { bank } = registerControl('bank');
|
||||
|
||||
/**
|
||||
* mix control for the chorus effect
|
||||
*
|
||||
* @name chorus
|
||||
* @param {string | Pattern} chorus mix amount between 0 and 1
|
||||
* @example
|
||||
* note("d d a# a").s("sawtooth").chorus(.5)
|
||||
*
|
||||
*/
|
||||
export const { chorus } = registerControl('chorus');
|
||||
|
||||
// analyser node send amount 0 - 1 (used by scope)
|
||||
export const { analyze } = registerControl('analyze');
|
||||
// fftSize of analyser
|
||||
@@ -525,6 +537,7 @@ export const { tremolophase } = registerControl('tremolophase', 'tremphase');
|
||||
* shape of amplitude modulation
|
||||
*
|
||||
* @name tremoloshape
|
||||
* @synonyms tremshape
|
||||
* @param {number | Pattern} shape tri | square | sine | saw | ramp
|
||||
* @example
|
||||
* note("{f g c d}%16").tremsync(4).tremoloshape("<sine tri square>").s("sawtooth")
|
||||
@@ -540,11 +553,13 @@ export const { tremoloshape } = registerControl('tremoloshape', 'tremshape');
|
||||
* note("{f g g c d a a#}%16".sub(17)).s("supersaw").lpenv(8).lpf(150).lpq(.8).ftype('ladder').drive("<.5 4>")
|
||||
*
|
||||
*/
|
||||
export const { drive } = registerControl('drive');
|
||||
|
||||
/**
|
||||
* modulate the amplitude of an orbit to create a "sidechain" like effect
|
||||
*
|
||||
* @name duckorbit
|
||||
* @synonyms duck
|
||||
* @param {number | Pattern} orbit target orbit
|
||||
* @example
|
||||
* $: n(run(16)).scale("c:minor:pentatonic").s("sawtooth").delay(.7).orbit(2)
|
||||
@@ -569,6 +584,7 @@ export const { duckdepth } = registerControl('duckdepth');
|
||||
* the attack time of the duck effect
|
||||
*
|
||||
* @name duckattack
|
||||
* @synonyms duckatt
|
||||
* @param {number | Pattern} time
|
||||
* @example
|
||||
* stack( n(run(8)).scale("c:minor").s("sawtooth").delay(.7).orbit(2), s("bd:4!4").beat("0,4,8,11,14",16).duckorbit(2).duckattack("<0.2 0 0.4>").duckdepth(1))
|
||||
@@ -576,8 +592,6 @@ export const { duckdepth } = registerControl('duckdepth');
|
||||
*/
|
||||
export const { duckattack } = registerControl('duckattack', 'duckatt');
|
||||
|
||||
export const { drive } = registerControl('drive');
|
||||
|
||||
/**
|
||||
* Create byte beats with custom expressions
|
||||
*
|
||||
@@ -700,7 +714,7 @@ export const { phasercenter, phc } = registerControl('phasercenter', 'phc');
|
||||
* The amount the signal is affected by the phaser effect. Defaults to 0.75
|
||||
*
|
||||
* @name phaserdepth
|
||||
* @synonyms phd
|
||||
* @synonyms phd, phasdp
|
||||
* @param {number | Pattern} depth number between 0 and 1
|
||||
* @example
|
||||
* n(run(8)).scale("D:pentatonic").s("sawtooth").release(0.5)
|
||||
@@ -1097,14 +1111,27 @@ export const { delay } = registerControl(['delay', 'delaytime', 'delayfeedback']
|
||||
*
|
||||
*/
|
||||
export const { delayfeedback, delayfb, dfb } = registerControl('delayfeedback', 'delayfb', 'dfb');
|
||||
|
||||
/**
|
||||
* Sets the level of the signal that is fed back into the delay.
|
||||
* Caution: Values >= 1 will result in a signal that gets louder and louder! Don't do it
|
||||
*
|
||||
* @name delayfeedback
|
||||
* @param {number | Pattern} feedback between 0 and 1
|
||||
* @synonyms delayfb, dfb
|
||||
* @example
|
||||
* s("bd").delay(.25).delayfeedback("<.25 .5 .75 1>")
|
||||
*
|
||||
*/
|
||||
export const { delayspeed } = registerControl('delayspeed');
|
||||
/**
|
||||
* Sets the time of the delay effect.
|
||||
*
|
||||
* @name delaytime
|
||||
* @param {number | Pattern} seconds between 0 and Infinity
|
||||
* @name delayspeed
|
||||
* @param {number | Pattern} delayspeed controls the pitch of the delay feedback
|
||||
* @synonyms delayt, dt
|
||||
* @example
|
||||
* s("bd bd").delay(.25).delaytime("<.125 .25 .5 1>")
|
||||
* note("d d a# a".fast(2)).s("sawtooth").delay(.8).delaytime(1/2).delayspeed("<2 .5 -1 -2>")
|
||||
*
|
||||
*/
|
||||
export const { delaytime, delayt, dt } = registerControl('delaytime', 'delayt', 'dt');
|
||||
@@ -1182,6 +1209,7 @@ export const { dry } = registerControl('dry');
|
||||
* Used when using `begin`/`end` or `chop`/`striate` and friends, to change the fade out time of the 'grain' envelope.
|
||||
*
|
||||
* @name fadeTime
|
||||
* @synonyms fadeOutTime
|
||||
* @param {number | Pattern} time between 0 and 1
|
||||
* @example
|
||||
* s("oh*4").end(.1).fadeTime("<0 .2 .4 .8>").osc()
|
||||
|
||||
@@ -1246,7 +1246,8 @@ export const silence = gap(1);
|
||||
/* Like silence, but with a 'steps' (relative duration) of 0 */
|
||||
export const nothing = gap(0);
|
||||
|
||||
/** A discrete value that repeats once per cycle.
|
||||
/**
|
||||
* A discrete value that repeats once per cycle.
|
||||
*
|
||||
* @returns {Pattern}
|
||||
* @example
|
||||
@@ -1299,7 +1300,8 @@ export function sequenceP(pats) {
|
||||
return result;
|
||||
}
|
||||
|
||||
/** The given items are played at the same time at the same length.
|
||||
/**
|
||||
* The given items are played at the same time at the same length.
|
||||
*
|
||||
* @return {Pattern}
|
||||
* @synonyms polyrhythm, pr
|
||||
@@ -1382,11 +1384,11 @@ export function stackBy(by, ...pats) {
|
||||
.setSteps(steps);
|
||||
}
|
||||
|
||||
/** Concatenation: combines a list of patterns, switching between them successively, one per cycle:
|
||||
*
|
||||
* synonyms: `cat`
|
||||
/**
|
||||
* Concatenation: combines a list of patterns, switching between them successively, one per cycle.
|
||||
*
|
||||
* @return {Pattern}
|
||||
* @synonyms cat
|
||||
* @example
|
||||
* slowcat("e5", "b4", ["d5", "c5"])
|
||||
*
|
||||
|
||||
@@ -108,7 +108,7 @@ export function mondo(code, offset = 0) {
|
||||
return pat.markcss('color: var(--caret,--foreground);text-decoration:underline');
|
||||
}
|
||||
|
||||
let getLocations = (code, offset) => runner.parser.get_locations(code, offset);
|
||||
export let getLocations = (code, offset) => runner.parser.get_locations(code, offset);
|
||||
|
||||
export const mondi = (str, offset) => {
|
||||
const code = `[${str}]`;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/mondo",
|
||||
"version": "1.1.2",
|
||||
"version": "1.1.4",
|
||||
"description": "mondo notation for strudel",
|
||||
"main": "mondough.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { defineConfig } from 'vite';
|
||||
//import { dependencies } from './package.json';
|
||||
import { dependencies } from './package.json';
|
||||
import { resolve } from 'path';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
@@ -12,7 +12,7 @@ export default defineConfig({
|
||||
fileName: (ext) => ({ es: 'mondough.mjs' })[ext],
|
||||
},
|
||||
rollupOptions: {
|
||||
// external: [...Object.keys(dependencies)],
|
||||
external: [...Object.keys(dependencies)],
|
||||
},
|
||||
target: 'esnext',
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/repl",
|
||||
"version": "1.2.5",
|
||||
"version": "1.2.6",
|
||||
"description": "Strudel REPL as a Web Component",
|
||||
"module": "index.mjs",
|
||||
"publishConfig": {
|
||||
|
||||
@@ -196,6 +196,52 @@ function getSamplesPrefixHandler(url) {
|
||||
return;
|
||||
}
|
||||
|
||||
export async function fetchSampleMap(url) {
|
||||
// check if custom prefix handler
|
||||
const handler = getSamplesPrefixHandler(url);
|
||||
if (handler) {
|
||||
return handler(url);
|
||||
}
|
||||
url = resolveSpecialPaths(url);
|
||||
if (url.startsWith('github:')) {
|
||||
url = githubPath(url, 'strudel.json');
|
||||
}
|
||||
if (url.startsWith('local:')) {
|
||||
url = `http://localhost:5432`;
|
||||
}
|
||||
if (url.startsWith('shabda:')) {
|
||||
let [_, path] = url.split('shabda:');
|
||||
url = `https://shabda.ndre.gr/${path}.json?strudel=1`;
|
||||
}
|
||||
if (url.startsWith('shabda/speech')) {
|
||||
let [_, path] = url.split('shabda/speech');
|
||||
path = path.startsWith('/') ? path.substring(1) : path;
|
||||
let [params, words] = path.split(':');
|
||||
let gender = 'f';
|
||||
let language = 'en-GB';
|
||||
if (params) {
|
||||
[language, gender] = params.split('/');
|
||||
}
|
||||
url = `https://shabda.ndre.gr/speech/${words}.json?gender=${gender}&language=${language}&strudel=1'`;
|
||||
}
|
||||
if (typeof fetch !== 'function') {
|
||||
// not a browser
|
||||
return;
|
||||
}
|
||||
const base = url.split('/').slice(0, -1).join('/');
|
||||
if (typeof fetch === 'undefined') {
|
||||
// skip fetch when in node / testing
|
||||
return;
|
||||
}
|
||||
const json = await fetch(url)
|
||||
.then((res) => res.json())
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
throw new Error(`error loading "${url}"`);
|
||||
});
|
||||
return [json, json._base || base];
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a collection of samples to use with `s`
|
||||
* @example
|
||||
@@ -217,49 +263,8 @@ function getSamplesPrefixHandler(url) {
|
||||
|
||||
export const samples = async (sampleMap, baseUrl = sampleMap._base || '', options = {}) => {
|
||||
if (typeof sampleMap === 'string') {
|
||||
// check if custom prefix handler
|
||||
const handler = getSamplesPrefixHandler(sampleMap);
|
||||
if (handler) {
|
||||
return handler(sampleMap);
|
||||
}
|
||||
sampleMap = resolveSpecialPaths(sampleMap);
|
||||
if (sampleMap.startsWith('github:')) {
|
||||
sampleMap = githubPath(sampleMap, 'strudel.json');
|
||||
}
|
||||
if (sampleMap.startsWith('local:')) {
|
||||
sampleMap = `http://localhost:5432`;
|
||||
}
|
||||
if (sampleMap.startsWith('shabda:')) {
|
||||
let [_, path] = sampleMap.split('shabda:');
|
||||
sampleMap = `https://shabda.ndre.gr/${path}.json?strudel=1`;
|
||||
}
|
||||
if (sampleMap.startsWith('shabda/speech')) {
|
||||
let [_, path] = sampleMap.split('shabda/speech');
|
||||
path = path.startsWith('/') ? path.substring(1) : path;
|
||||
let [params, words] = path.split(':');
|
||||
let gender = 'f';
|
||||
let language = 'en-GB';
|
||||
if (params) {
|
||||
[language, gender] = params.split('/');
|
||||
}
|
||||
sampleMap = `https://shabda.ndre.gr/speech/${words}.json?gender=${gender}&language=${language}&strudel=1'`;
|
||||
}
|
||||
if (typeof fetch !== 'function') {
|
||||
// not a browser
|
||||
return;
|
||||
}
|
||||
const base = sampleMap.split('/').slice(0, -1).join('/');
|
||||
if (typeof fetch === 'undefined') {
|
||||
// skip fetch when in node / testing
|
||||
return;
|
||||
}
|
||||
return fetch(sampleMap)
|
||||
.then((res) => res.json())
|
||||
.then((json) => samples(json, baseUrl || json._base || base, options))
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
throw new Error(`error loading "${sampleMap}"`);
|
||||
});
|
||||
const [json, base] = await fetchSampleMap(sampleMap);
|
||||
return samples(json, baseUrl || base, options);
|
||||
}
|
||||
const { prebake, tag } = options;
|
||||
processSampleMap(
|
||||
|
||||
@@ -196,7 +196,7 @@ export const resetLoadedSounds = () => soundMap.set({});
|
||||
let audioContext;
|
||||
|
||||
export const setDefaultAudioContext = () => {
|
||||
audioContext = new AudioContext();
|
||||
audioContext = new AudioContext({ latencyHint: 'playback' });
|
||||
return audioContext;
|
||||
};
|
||||
|
||||
@@ -212,11 +212,17 @@ export function getAudioContextCurrentTime() {
|
||||
return getAudioContext().currentTime;
|
||||
}
|
||||
|
||||
let externalWorklets = [];
|
||||
export function registerWorklet(url) {
|
||||
externalWorklets.push(url);
|
||||
}
|
||||
|
||||
let workletsLoading;
|
||||
function loadWorklets() {
|
||||
if (!workletsLoading) {
|
||||
const audioCtx = getAudioContext();
|
||||
workletsLoading = audioCtx.audioWorklet.addModule(workletsUrl);
|
||||
const allWorkletURLs = externalWorklets.concat([workletsUrl]);
|
||||
workletsLoading = Promise.all(allWorkletURLs.map((workletURL) => audioCtx.audioWorklet.addModule(workletURL)));
|
||||
}
|
||||
|
||||
return workletsLoading;
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
pattern.wav
|
||||
@@ -0,0 +1,3 @@
|
||||
# supradough
|
||||
|
||||
platform agnostic synth and sampler intended for live coding. a reimplementation of superdough.
|
||||
@@ -0,0 +1,123 @@
|
||||
// this is a poc of how a pattern can be rendered as a wav file using node
|
||||
// run via: node dough-export.mjs
|
||||
import fs from 'node:fs';
|
||||
import WavEncoder from 'wav-encoder';
|
||||
import { evalScope } from '@strudel/core';
|
||||
import { miniAllStrings } from '@strudel/mini';
|
||||
import { Dough } from './dough.mjs';
|
||||
|
||||
await evalScope(
|
||||
import('@strudel/core'),
|
||||
import('@strudel/mini'),
|
||||
import('@strudel/tonal'),
|
||||
// import('@strudel/tonal'),
|
||||
);
|
||||
|
||||
miniAllStrings(); // allows using single quotes for mini notation / skip transpilation
|
||||
|
||||
let sampleRate = 48000,
|
||||
cps = 0.4;
|
||||
|
||||
/* await doughsamples('github:eddyflux/crate');
|
||||
await doughsamples('github:eddyflux/wax'); */
|
||||
|
||||
let pat = note('c,eb,g,<bb c4 d4 eb4>')
|
||||
.s('sine')
|
||||
.press()
|
||||
.add(note(24))
|
||||
.fmi(3)
|
||||
.fmh(5.01)
|
||||
.dec(0.4)
|
||||
.delay('.6:<.12 .22>:.8')
|
||||
.jux(press)
|
||||
.rarely(add(note('12')))
|
||||
.lpf(400)
|
||||
.lpq(0.2)
|
||||
.lpd(0.4)
|
||||
.lpenv(3)
|
||||
.fmdecay(0.4)
|
||||
.fmenv(1)
|
||||
.postgain(0.6)
|
||||
.stack(s('<pink white>*8').dec(0.07).rarely(ply('2')).delay(0.5).hpf(sine.range(200, 2000).slow(4)).hpq(0.2))
|
||||
.stack(
|
||||
s('[- white@3]*2')
|
||||
.dec(0.4)
|
||||
.hpf('<2000!3 <4000 8000>>*4')
|
||||
.hpq(0.6)
|
||||
.ply('<1 2>*4')
|
||||
.postgain(0.5)
|
||||
.delay(0.5)
|
||||
.jux(rev)
|
||||
.lpf(5000),
|
||||
)
|
||||
.stack(
|
||||
note('<c2 - [- f1] ->*2')
|
||||
.s('square')
|
||||
.lpf(sine.range(100, 300).slow(4))
|
||||
.lpe(1)
|
||||
.segment(8)
|
||||
.lpd(0.3)
|
||||
.lpq(0.2)
|
||||
.dec(0.2)
|
||||
.speed('<1 2>')
|
||||
.ply('<1 2>')
|
||||
.postgain(1),
|
||||
)
|
||||
.stack(
|
||||
chord('<Cm Cm7 Cm9 Cm11 Fm Fm7 Fm9 Fm11>')
|
||||
.voicing()
|
||||
.s('<sine>')
|
||||
.clip(1)
|
||||
.rel(0.4)
|
||||
.vib('4:.2')
|
||||
.gain(0.7)
|
||||
.hpf(1200)
|
||||
.fm(0.5)
|
||||
.att(1)
|
||||
.lpa(0.5)
|
||||
.lpf(200)
|
||||
.lpenv(4)
|
||||
.chorus(0.8),
|
||||
)
|
||||
.slow(1 / cps);
|
||||
|
||||
let cycles = 30;
|
||||
let seconds = cycles + 1; // 1s release tail
|
||||
const haps = pat.queryArc(0, cycles);
|
||||
|
||||
const dough = new Dough(sampleRate);
|
||||
|
||||
console.log('spawn voices...');
|
||||
haps.forEach((hap) => {
|
||||
hap.value._begin = Number(hap.whole.begin);
|
||||
hap.value._duration = hap.duration /* / cps */;
|
||||
dough.scheduleSpawn(hap.value);
|
||||
});
|
||||
console.log(`render ${seconds}s long buffer, each dot is 1 second:`);
|
||||
const buffers = [new Float32Array(seconds * sampleRate), new Float32Array(seconds * sampleRate)];
|
||||
let t = performance.now();
|
||||
while (dough.t <= buffers[0].length) {
|
||||
dough.update();
|
||||
buffers[0][dough.t] = dough.out[0];
|
||||
buffers[1][dough.t] = dough.out[1];
|
||||
if (dough.t % sampleRate === 0) {
|
||||
process.stdout.write('.');
|
||||
}
|
||||
}
|
||||
const took = (performance.now() - t) / 1000;
|
||||
const load = (took / seconds) * 100;
|
||||
const speed = (seconds / took).toFixed(2);
|
||||
console.log('');
|
||||
console.log(`done!
|
||||
rendered ${seconds}s in ${took.toFixed(2)}s
|
||||
speed: ${speed}x
|
||||
load: ${load.toFixed(2)}%`);
|
||||
|
||||
const patternAudio = {
|
||||
sampleRate,
|
||||
channelData: buffers,
|
||||
};
|
||||
|
||||
WavEncoder.encode(patternAudio).then((buffer) => {
|
||||
fs.writeFileSync('pattern.wav', new Float32Array(buffer));
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Dough } from './dough.mjs';
|
||||
|
||||
const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
|
||||
|
||||
class DoughProcessor extends AudioWorkletProcessor {
|
||||
constructor() {
|
||||
super();
|
||||
this.dough = new Dough(sampleRate, currentTime);
|
||||
this.port.onmessage = (event) => {
|
||||
if (event.data.spawn) {
|
||||
this.dough.scheduleSpawn(event.data.spawn);
|
||||
} else if (event.data.sample) {
|
||||
this.dough.loadSample(event.data.sample, event.data.channels, event.data.sampleRate);
|
||||
} else if (event.data.samples) {
|
||||
event.data.samples.forEach(([name, channels, sampleRate]) => {
|
||||
this.dough.loadSample(name, channels, sampleRate);
|
||||
});
|
||||
} else {
|
||||
console.log('unrecognized event type', event.data);
|
||||
}
|
||||
};
|
||||
}
|
||||
process(inputs, outputs, params) {
|
||||
if (this.disconnected) {
|
||||
return false;
|
||||
}
|
||||
const output = outputs[0];
|
||||
for (let i = 0; i < output[0].length; i++) {
|
||||
this.dough.update();
|
||||
for (let c = 0; c < output.length; c++) {
|
||||
//prevent speaker blowout via clipping if threshold exceeds
|
||||
output[c][i] = clamp(this.dough.out[c], -1, 1);
|
||||
}
|
||||
}
|
||||
return true; // keep the audio processing going
|
||||
}
|
||||
}
|
||||
|
||||
registerProcessor('dough-processor', DoughProcessor);
|
||||
@@ -0,0 +1,976 @@
|
||||
// this is dough, the superdough without dependencies
|
||||
const SAMPLE_RATE = typeof sampleRate !== 'undefined' ? sampleRate : 48000;
|
||||
const PI_DIV_SR = Math.PI / SAMPLE_RATE;
|
||||
const ISR = 1 / SAMPLE_RATE;
|
||||
|
||||
let gainCurveFunc = (val) => Math.pow(val, 2);
|
||||
|
||||
function applyGainCurve(val) {
|
||||
return gainCurveFunc(val);
|
||||
}
|
||||
|
||||
/**
|
||||
* Equal Power Crossfade function.
|
||||
* Smoothly transitions between signals A and B, maintaining consistent perceived loudness.
|
||||
*
|
||||
* @param {number} a - Signal A (can be a single value or an array value in buffer processing).
|
||||
* @param {number} b - Signal B (can be a single value or an array value in buffer processing).
|
||||
* @param {number} m - Crossfade parameter (0.0 = all A, 1.0 = all B, 0.5 = equal mix).
|
||||
* @returns {number} Crossfaded output value.
|
||||
*/
|
||||
function crossfade(a, b, m) {
|
||||
const aGain = Math.sin((1 - m) * 0.5 * Math.PI);
|
||||
const bGain = Math.sin(m * 0.5 * Math.PI);
|
||||
return a * aGain + b * bGain;
|
||||
}
|
||||
|
||||
// function setGainCurve(newGainCurveFunc) {
|
||||
// gainCurveFunc = newGainCurveFunc;
|
||||
// }
|
||||
// https://garten.salat.dev/audio-DSP/oscillators.html
|
||||
export class SineOsc {
|
||||
phase = 0;
|
||||
update(freq) {
|
||||
const value = Math.sin(this.phase * 2 * Math.PI);
|
||||
this.phase = (this.phase + freq / SAMPLE_RATE) % 1;
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
export class ZawOsc {
|
||||
phase = 0;
|
||||
update(freq) {
|
||||
this.phase += ISR * freq;
|
||||
return (this.phase % 1) * 2 - 1;
|
||||
}
|
||||
}
|
||||
|
||||
function polyBlep(t, dt) {
|
||||
// 0 <= t < 1
|
||||
if (t < dt) {
|
||||
t /= dt;
|
||||
// 2 * (t - t^2/2 - 0.5)
|
||||
return t + t - t * t - 1;
|
||||
}
|
||||
// -1 < t < 0
|
||||
if (t > 1 - dt) {
|
||||
t = (t - 1) / dt;
|
||||
// 2 * (t^2/2 + t + 0.5)
|
||||
return t * t + t + t + 1;
|
||||
}
|
||||
// 0 otherwise
|
||||
return 0;
|
||||
}
|
||||
|
||||
export class SawOsc {
|
||||
constructor(props = {}) {
|
||||
this.phase = props.phase ?? 0;
|
||||
}
|
||||
update(freq) {
|
||||
const dt = freq / SAMPLE_RATE;
|
||||
let p = polyBlep(this.phase, dt);
|
||||
let s = 2 * this.phase - 1 - p;
|
||||
this.phase += dt;
|
||||
if (this.phase > 1) {
|
||||
this.phase -= 1;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
function getUnisonDetune(unison, detune, voiceIndex) {
|
||||
if (unison < 2) {
|
||||
return 0;
|
||||
}
|
||||
const lerp = (a, b, n) => {
|
||||
return n * (b - a) + a;
|
||||
};
|
||||
return lerp(-detune * 0.5, detune * 0.5, voiceIndex / (unison - 1));
|
||||
}
|
||||
function applySemitoneDetuneToFrequency(frequency, detune) {
|
||||
return frequency * Math.pow(2, detune / 12);
|
||||
}
|
||||
export class SupersawOsc {
|
||||
constructor(props = {}) {
|
||||
//TODO: figure out a good way to pass in these params
|
||||
this.voices = props.voices ?? 5;
|
||||
this.freqspread = props.freqspread ?? 0.2;
|
||||
this.panspread = props.panspread ?? 0.4;
|
||||
this.phase = new Float32Array(this.voices).map(() => Math.random());
|
||||
}
|
||||
update(freq) {
|
||||
const gain1 = Math.sqrt(1 - this.panspread);
|
||||
const gain2 = Math.sqrt(this.panspread);
|
||||
let sl = 0;
|
||||
let sr = 0;
|
||||
for (let n = 0; n < this.voices; n++) {
|
||||
const freqAdjusted = applySemitoneDetuneToFrequency(freq, getUnisonDetune(this.voices, this.freqspread, n));
|
||||
const dt = freqAdjusted / SAMPLE_RATE;
|
||||
const isOdd = (n & 1) == 1;
|
||||
let gainL = gain1;
|
||||
let gainR = gain2;
|
||||
// invert right and left gain
|
||||
if (isOdd) {
|
||||
gainL = gain2;
|
||||
gainR = gain1;
|
||||
}
|
||||
let p = polyBlep(this.phase[n], dt);
|
||||
let s = 2 * this.phase[n] - 1 - p;
|
||||
sl = sl + s * gainL;
|
||||
sr = sr + s * gainL;
|
||||
|
||||
this.phase[n] += dt;
|
||||
if (this.phase[n] > 1) {
|
||||
this.phase[n] -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
return sl + sr;
|
||||
//TODO: make stereo
|
||||
// return [sl, sr];
|
||||
}
|
||||
}
|
||||
|
||||
export class TriOsc {
|
||||
phase = 0;
|
||||
update(freq) {
|
||||
this.phase += ISR * freq;
|
||||
let phase = this.phase % 1;
|
||||
let value = phase < 0.5 ? 2 * phase : 1 - 2 * (phase - 0.5);
|
||||
return value * 2 - 1;
|
||||
}
|
||||
}
|
||||
|
||||
export class TwoPoleFilter {
|
||||
s0 = 0;
|
||||
s1 = 0;
|
||||
update(s, cutoff, resonance = 0) {
|
||||
// Out of bound values can produce NaNs
|
||||
resonance = Math.max(resonance, 0);
|
||||
|
||||
cutoff = Math.min(cutoff, 20000);
|
||||
const c = 2 * Math.sin(cutoff * PI_DIV_SR);
|
||||
|
||||
const r = Math.pow(0.5, (resonance + 0.125) / 0.125);
|
||||
const mrc = 1 - r * c;
|
||||
|
||||
this.s0 = mrc * this.s0 - c * this.s1 + c * s; // bpf
|
||||
this.s1 = mrc * this.s1 + c * this.s0; // lpf
|
||||
return this.s1; // return lpf by default
|
||||
}
|
||||
}
|
||||
|
||||
class PulseOsc {
|
||||
constructor(phase = 0) {
|
||||
this.phase = phase;
|
||||
}
|
||||
saw(offset, dt) {
|
||||
let phase = (this.phase + offset) % 1;
|
||||
let p = polyBlep(phase, dt);
|
||||
return 2 * phase - 1 - p;
|
||||
}
|
||||
update(freq, pw = 0.5) {
|
||||
const dt = freq / SAMPLE_RATE;
|
||||
let pulse = this.saw(0, dt) - this.saw(pw, dt);
|
||||
this.phase = (this.phase + dt) % 1;
|
||||
return pulse + pw * 2 - 1;
|
||||
}
|
||||
}
|
||||
|
||||
// non bandlimited (has aliasing)
|
||||
export class PulzeOsc {
|
||||
phase = 0;
|
||||
update(freq, duty = 0.5) {
|
||||
this.phase += ISR * freq;
|
||||
let cyclePos = this.phase % 1;
|
||||
return cyclePos < duty ? 1 : -1;
|
||||
}
|
||||
}
|
||||
|
||||
export class Dust {
|
||||
update = (density) => (Math.random() < density * ISR ? Math.random() : 0);
|
||||
}
|
||||
|
||||
export class WhiteNoise {
|
||||
update() {
|
||||
return Math.random() * 2 - 1;
|
||||
}
|
||||
}
|
||||
|
||||
export class BrownNoise {
|
||||
constructor() {
|
||||
this.out = 0;
|
||||
}
|
||||
update() {
|
||||
let white = Math.random() * 2 - 1;
|
||||
this.out = (this.out + 0.02 * white) / 1.02;
|
||||
return this.out;
|
||||
}
|
||||
}
|
||||
|
||||
export class PinkNoise {
|
||||
constructor() {
|
||||
this.b0 = 0;
|
||||
this.b1 = 0;
|
||||
this.b2 = 0;
|
||||
this.b3 = 0;
|
||||
this.b4 = 0;
|
||||
this.b5 = 0;
|
||||
this.b6 = 0;
|
||||
}
|
||||
|
||||
update() {
|
||||
const white = Math.random() * 2 - 1;
|
||||
|
||||
this.b0 = 0.99886 * this.b0 + white * 0.0555179;
|
||||
this.b1 = 0.99332 * this.b1 + white * 0.0750759;
|
||||
this.b2 = 0.969 * this.b2 + white * 0.153852;
|
||||
this.b3 = 0.8665 * this.b3 + white * 0.3104856;
|
||||
this.b4 = 0.55 * this.b4 + white * 0.5329522;
|
||||
this.b5 = -0.7616 * this.b5 - white * 0.016898;
|
||||
|
||||
const pink = this.b0 + this.b1 + this.b2 + this.b3 + this.b4 + this.b5 + this.b6 + white * 0.5362;
|
||||
this.b6 = white * 0.115926;
|
||||
|
||||
return pink * 0.11;
|
||||
}
|
||||
}
|
||||
|
||||
export class Impulse {
|
||||
phase = 1;
|
||||
update(freq) {
|
||||
this.phase += ISR * freq;
|
||||
let v = this.phase >= 1 ? 1 : 0;
|
||||
this.phase = this.phase % 1;
|
||||
return v;
|
||||
}
|
||||
}
|
||||
|
||||
export class ClockDiv {
|
||||
inSgn = true;
|
||||
outSgn = true;
|
||||
clockCnt = 0;
|
||||
update(clock, factor) {
|
||||
let curSgn = clock > 0;
|
||||
if (this.inSgn != curSgn) {
|
||||
this.clockCnt++;
|
||||
if (this.clockCnt >= factor) {
|
||||
this.clockCnt = 0;
|
||||
this.outSgn = !this.outSgn;
|
||||
}
|
||||
}
|
||||
|
||||
this.inSgn = curSgn;
|
||||
return this.outSgn ? 1 : -1;
|
||||
}
|
||||
}
|
||||
|
||||
export class Hold {
|
||||
value = 0;
|
||||
trigSgn = false;
|
||||
update(input, trig) {
|
||||
if (!this.trigSgn && trig > 0) this.value = input;
|
||||
this.trigSgn = trig > 0;
|
||||
return this.value;
|
||||
}
|
||||
}
|
||||
|
||||
function lerp(x, y0, y1, exponent = 1) {
|
||||
if (x <= 0) return y0;
|
||||
if (x >= 1) return y1;
|
||||
|
||||
let curvedX;
|
||||
|
||||
if (exponent === 0) {
|
||||
curvedX = x; // linear
|
||||
} else if (exponent > 0) {
|
||||
curvedX = Math.pow(x, exponent); // ease-in
|
||||
} else {
|
||||
curvedX = 1 - Math.pow(1 - x, -exponent); // ease-out
|
||||
}
|
||||
|
||||
return y0 + (y1 - y0) * curvedX;
|
||||
}
|
||||
|
||||
export class ADSR {
|
||||
constructor(props = {}) {
|
||||
this.state = 'off';
|
||||
this.startTime = 0;
|
||||
this.startVal = 0;
|
||||
this.decayCurve = props.decayCurve ?? 1;
|
||||
}
|
||||
|
||||
update(curTime, gate, attack, decay, susVal, release) {
|
||||
switch (this.state) {
|
||||
case 'off': {
|
||||
if (gate > 0) {
|
||||
this.state = 'attack';
|
||||
this.startTime = curTime;
|
||||
this.startVal = 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
case 'attack': {
|
||||
let time = curTime - this.startTime;
|
||||
if (time > attack) {
|
||||
this.state = 'decay';
|
||||
this.startTime = curTime;
|
||||
return 1;
|
||||
}
|
||||
return lerp(time / attack, this.startVal, 1, 1);
|
||||
}
|
||||
case 'decay': {
|
||||
let time = curTime - this.startTime;
|
||||
let curVal = lerp(time / decay, 1, susVal, -this.decayCurve);
|
||||
if (gate <= 0) {
|
||||
this.state = 'release';
|
||||
this.startTime = curTime;
|
||||
this.startVal = curVal;
|
||||
return curVal;
|
||||
}
|
||||
if (time > decay) {
|
||||
this.state = 'sustain';
|
||||
this.startTime = curTime;
|
||||
return susVal;
|
||||
}
|
||||
return curVal;
|
||||
}
|
||||
case 'sustain': {
|
||||
if (gate <= 0) {
|
||||
this.state = 'release';
|
||||
this.startTime = curTime;
|
||||
this.startVal = susVal;
|
||||
}
|
||||
return susVal;
|
||||
}
|
||||
case 'release': {
|
||||
let time = curTime - this.startTime;
|
||||
|
||||
if (time > release) {
|
||||
this.state = 'off';
|
||||
return 0;
|
||||
}
|
||||
let curVal = lerp(time / release, this.startVal, 0, -this.decayCurve);
|
||||
if (gate > 0) {
|
||||
this.state = 'attack';
|
||||
this.startTime = curTime;
|
||||
this.startVal = curVal;
|
||||
}
|
||||
return curVal;
|
||||
}
|
||||
}
|
||||
throw 'invalid envelope state';
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
impulse(1).ad(.1).mul(sine(200))
|
||||
.add(x=>x.delay(.1).mul(.8))
|
||||
.out()*/
|
||||
const MAX_DELAY_TIME = 10;
|
||||
export class PitchDelay {
|
||||
lpf = new TwoPoleFilter();
|
||||
constructor(_props = {}) {
|
||||
this.buffer = new Float32Array(MAX_DELAY_TIME * SAMPLE_RATE);
|
||||
this.writeIdx = 0;
|
||||
this.readIdx = 0;
|
||||
this.numSamples = 0;
|
||||
}
|
||||
write(s, delayTime) {
|
||||
// Calculate how far in the past to read
|
||||
this.numSamples = Math.min(Math.floor(SAMPLE_RATE * delayTime), this.buffer.length - 1);
|
||||
this.writeIdx = (this.writeIdx + 1) % this.numSamples;
|
||||
this.buffer[this.writeIdx] = s;
|
||||
this.readIdx = this.writeIdx - this.numSamples + 1;
|
||||
|
||||
// If past the start of the buffer, wrap around (Q: is this possible?)
|
||||
if (this.readIdx < 0) this.readIdx += this.numSamples;
|
||||
}
|
||||
update(input, delayTime, speed = 1) {
|
||||
this.write(input, delayTime);
|
||||
let index = this.readIdx;
|
||||
if (speed < 0) {
|
||||
index = this.numSamples - Math.floor(Math.abs(this.readIdx * speed) % this.numSamples);
|
||||
} else {
|
||||
index = Math.floor(this.readIdx * speed) % this.numSamples;
|
||||
}
|
||||
const s = this.lpf.update(this.buffer[index], 0.9, 0);
|
||||
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
export class Delay {
|
||||
writeIdx = 0;
|
||||
readIdx = 0;
|
||||
buffer = new Float32Array(MAX_DELAY_TIME * SAMPLE_RATE); //.fill(0)
|
||||
write(s, delayTime) {
|
||||
this.writeIdx = (this.writeIdx + 1) % this.buffer.length;
|
||||
this.buffer[this.writeIdx] = s;
|
||||
// Calculate how far in the past to read
|
||||
let numSamples = Math.min(Math.floor(SAMPLE_RATE * delayTime), this.buffer.length - 1);
|
||||
this.readIdx = this.writeIdx - numSamples;
|
||||
// If past the start of the buffer, wrap around
|
||||
if (this.readIdx < 0) this.readIdx += this.buffer.length;
|
||||
}
|
||||
update(input, delayTime) {
|
||||
this.write(input, delayTime);
|
||||
return this.buffer[this.readIdx];
|
||||
}
|
||||
}
|
||||
//TODO: Figure out why clicking at the start off the buffer
|
||||
export class Chorus {
|
||||
delay = new Delay();
|
||||
modulator = new TriOsc();
|
||||
update(input, mix, delayTime, modulationFreq, modulationDepth) {
|
||||
const m = this.modulator.update(modulationFreq) * modulationDepth;
|
||||
const c = this.delay.update(input, delayTime * (1 + m));
|
||||
return crossfade(input, c, mix);
|
||||
}
|
||||
}
|
||||
|
||||
export class Fold {
|
||||
update(input = 0, rate = 0) {
|
||||
if (rate < 0) rate = 0;
|
||||
rate = rate + 1;
|
||||
input = input * rate;
|
||||
return 4 * (Math.abs(0.25 * input + 0.25 - Math.round(0.25 * input + 0.25)) - 0.25);
|
||||
}
|
||||
}
|
||||
|
||||
export class Lag {
|
||||
lagUnit = 4410;
|
||||
s = 0;
|
||||
update(input, rate) {
|
||||
// Remap so the useful range is around [0, 1]
|
||||
rate = rate * this.lagUnit;
|
||||
if (rate < 1) rate = 1;
|
||||
this.s += (1 / rate) * (input - this.s);
|
||||
return this.s;
|
||||
}
|
||||
}
|
||||
|
||||
export class Slew {
|
||||
last = 0;
|
||||
update(input, up, dn) {
|
||||
const upStep = up * ISR;
|
||||
const downStep = dn * ISR;
|
||||
let delta = input - this.last;
|
||||
if (delta > upStep) {
|
||||
delta = upStep;
|
||||
} else if (delta < -downStep) {
|
||||
delta = -downStep;
|
||||
}
|
||||
this.last += delta;
|
||||
return this.last;
|
||||
}
|
||||
}
|
||||
|
||||
// overdrive style distortion (adapted from noisecraft) currently unused
|
||||
export function applyDistortion(x, amount) {
|
||||
amount = Math.min(Math.max(amount, 0), 1);
|
||||
amount -= 0.01;
|
||||
var k = (2 * amount) / (1 - amount);
|
||||
var y = ((1 + k) * x) / (1 + k * Math.abs(x));
|
||||
return y;
|
||||
}
|
||||
|
||||
export class Sequence {
|
||||
clockSgn = true;
|
||||
step = 0;
|
||||
first = true;
|
||||
update(clock, ...ins) {
|
||||
if (!this.clockSgn && clock > 0) {
|
||||
this.step = (this.step + 1) % ins.length;
|
||||
this.clockSgn = clock > 0;
|
||||
return 0; // set first sample to zero to retrigger gates on step change...
|
||||
}
|
||||
this.clockSgn = clock > 0;
|
||||
return ins[this.step];
|
||||
}
|
||||
}
|
||||
|
||||
// sample rate bit crusher
|
||||
export class Coarse {
|
||||
hold = 0;
|
||||
t = 0;
|
||||
update(input, coarse) {
|
||||
if (this.t++ % coarse === 0) {
|
||||
this.t = 0;
|
||||
this.hold = input;
|
||||
}
|
||||
return this.hold;
|
||||
}
|
||||
}
|
||||
|
||||
// amplitude bit crusher
|
||||
export class Crush {
|
||||
update(input, crush) {
|
||||
crush = Math.max(1, crush);
|
||||
const x = Math.pow(2, crush - 1);
|
||||
return Math.round(input * x) / x;
|
||||
}
|
||||
}
|
||||
|
||||
// this is the distort from superdough
|
||||
export class Distort {
|
||||
update(input, distort = 0, postgain = 1) {
|
||||
postgain = Math.max(0.001, Math.min(1, postgain));
|
||||
const shape = Math.expm1(distort);
|
||||
return (((1 + shape) * input) / (1 + shape * Math.abs(input))) * postgain;
|
||||
}
|
||||
}
|
||||
// distortion could be expressed as a function, because it's stateless
|
||||
|
||||
export class BufferPlayer {
|
||||
static samples = new Map(); // string -> { channels, sampleRate }
|
||||
buffer; // Float32Array
|
||||
sampleRate;
|
||||
pos = 0;
|
||||
sampleFreq = note2freq();
|
||||
constructor(buffer, sampleRate, normalize) {
|
||||
this.buffer = buffer;
|
||||
this.sampleRate = sampleRate;
|
||||
this.duration = this.buffer.length / this.sampleRate;
|
||||
this.speed = SAMPLE_RATE / this.sampleRate;
|
||||
if (normalize) {
|
||||
// this will make the buffer last 1s if freq = sampleFreq
|
||||
// it's useful to loop samples (e.g. fit function)
|
||||
this.speed *= this.duration;
|
||||
}
|
||||
}
|
||||
update(freq) {
|
||||
if (this.pos >= this.buffer.length) {
|
||||
return 0;
|
||||
}
|
||||
const speed = (freq / this.sampleFreq) * this.speed;
|
||||
let s = this.buffer[Math.floor(this.pos)];
|
||||
this.pos = this.pos + speed;
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
export function _rangex(sig, min, max) {
|
||||
let logmin = Math.log(min);
|
||||
let range = Math.log(max) - logmin;
|
||||
const unipolar = (sig + 1) / 2;
|
||||
return Math.exp(unipolar * range + logmin);
|
||||
}
|
||||
|
||||
// duplicate
|
||||
export const getADSR = (params, curve = 'linear', defaultValues) => {
|
||||
const envmin = curve === 'exponential' ? 0.001 : 0.001;
|
||||
const releaseMin = 0.01;
|
||||
const envmax = 1;
|
||||
const [a, d, s, r] = params;
|
||||
if (a == null && d == null && s == null && r == null) {
|
||||
return defaultValues ?? [envmin, envmin, envmax, releaseMin];
|
||||
}
|
||||
const sustain = s != null ? s : (a != null && d == null) || (a == null && d == null) ? envmax : envmin;
|
||||
return [Math.max(a ?? 0, envmin), Math.max(d ?? 0, envmin), Math.min(sustain, envmax), Math.max(r ?? 0, releaseMin)];
|
||||
};
|
||||
|
||||
let shapes = {
|
||||
sine: SineOsc,
|
||||
saw: SawOsc,
|
||||
zaw: ZawOsc,
|
||||
sawtooth: SawOsc,
|
||||
zawtooth: ZawOsc,
|
||||
supersaw: SupersawOsc,
|
||||
tri: TriOsc,
|
||||
triangle: TriOsc,
|
||||
pulse: PulseOsc,
|
||||
square: PulseOsc,
|
||||
pulze: PulzeOsc,
|
||||
dust: Dust,
|
||||
crackle: Dust,
|
||||
impulse: Impulse,
|
||||
white: WhiteNoise,
|
||||
brown: BrownNoise,
|
||||
pink: PinkNoise,
|
||||
};
|
||||
|
||||
const defaultDefaultValues = {
|
||||
chorus: 0,
|
||||
note: 48,
|
||||
s: 'triangle',
|
||||
bank: '',
|
||||
gain: 1,
|
||||
postgain: 1,
|
||||
velocity: 1,
|
||||
density: '.03',
|
||||
ftype: '12db',
|
||||
fanchor: 0,
|
||||
//resonance: 1, // superdough resonance is scaled differently
|
||||
resonance: 0,
|
||||
//hresonance: 1, // superdough resonance is scaled differently
|
||||
hresonance: 0,
|
||||
// bandq: 1, // superdough resonance is scaled differently
|
||||
bandq: 0,
|
||||
channels: [1, 2],
|
||||
phaserdepth: 0.75,
|
||||
shapevol: 1,
|
||||
distortvol: 1,
|
||||
delay: 0,
|
||||
byteBeatExpression: '0',
|
||||
delayfeedback: 0.5,
|
||||
delayspeed: 1,
|
||||
delaytime: 0.25,
|
||||
orbit: 1,
|
||||
i: 1,
|
||||
fft: 8,
|
||||
z: 'triangle',
|
||||
pan: 0.5,
|
||||
fmh: 1,
|
||||
fmenv: 0, // differs from superdough
|
||||
speed: 1,
|
||||
pw: 0.5,
|
||||
};
|
||||
|
||||
let getDefaultValue = (key) => defaultDefaultValues[key];
|
||||
|
||||
const chromas = { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 };
|
||||
const accs = { '#': 1, b: -1, s: 1, f: -1 };
|
||||
const note2midi = (note, defaultOctave = 3) => {
|
||||
let [pc, acc = '', oct = ''] =
|
||||
String(note)
|
||||
.match(/^([a-gA-G])([#bsf]*)([0-9]*)$/)
|
||||
?.slice(1) || [];
|
||||
if (!pc) {
|
||||
throw new Error('not a note: "' + note + '"');
|
||||
}
|
||||
const chroma = chromas[pc.toLowerCase()];
|
||||
const offset = acc?.split('').reduce((o, char) => o + accs[char], 0) || 0;
|
||||
oct = Number(oct || defaultOctave);
|
||||
return (oct + 1) * 12 + chroma + offset;
|
||||
};
|
||||
const midi2freq = (midi) => Math.pow(2, (midi - 69) / 12) * 440;
|
||||
const note2freq = (note) => {
|
||||
note = note || getDefaultValue('note');
|
||||
if (typeof note === 'string') {
|
||||
note = note2midi(note, 3); // e.g. c3 => 48
|
||||
}
|
||||
return midi2freq(note);
|
||||
};
|
||||
|
||||
export class DoughVoice {
|
||||
out = [0, 0];
|
||||
constructor(value) {
|
||||
value.freq ??= note2freq(value.note);
|
||||
let $ = this;
|
||||
Object.assign($, value);
|
||||
$.s = $.s ?? getDefaultValue('s');
|
||||
$.gain = applyGainCurve($.gain ?? getDefaultValue('gain'));
|
||||
$.velocity = applyGainCurve($.velocity ?? getDefaultValue('velocity'));
|
||||
$.postgain = applyGainCurve($.postgain ?? getDefaultValue('postgain'));
|
||||
$.density = $.density ?? getDefaultValue('density');
|
||||
$.fanchor = $.fanchor ?? getDefaultValue('fanchor');
|
||||
$.drive = $.drive ?? 0.69;
|
||||
$.phaserdepth = $.phaserdepth ?? getDefaultValue('phaserdepth');
|
||||
$.shapevol = applyGainCurve($.shapevol ?? getDefaultValue('shapevol'));
|
||||
$.distortvol = applyGainCurve($.distortvol ?? getDefaultValue('distortvol'));
|
||||
$.i = $.i ?? getDefaultValue('i');
|
||||
$.chorus = $.chorus ?? getDefaultValue('chorus');
|
||||
$.fft = $.fft ?? getDefaultValue('fft');
|
||||
$.pan = $.pan ?? getDefaultValue('pan');
|
||||
$.orbit = $.orbit ?? getDefaultValue('orbit');
|
||||
$.fmenv = $.fmenv ?? getDefaultValue('fmenv');
|
||||
$.resonance = $.resonance ?? getDefaultValue('resonance');
|
||||
$.hresonance = $.hresonance ?? getDefaultValue('hresonance');
|
||||
$.bandq = $.bandq ?? getDefaultValue('bandq');
|
||||
$.speed = $.speed ?? getDefaultValue('speed');
|
||||
$.pw = $.pw ?? getDefaultValue('pw');
|
||||
|
||||
[$.attack, $.decay, $.sustain, $.release] = getADSR([$.attack, $.decay, $.sustain, $.release]);
|
||||
|
||||
$._holdEnd = $._begin + $._duration; // needed for gate
|
||||
$._end = $._holdEnd + $.release + 0.01; // needed for despawn
|
||||
|
||||
if ($.fmi && ($.s === 'saw' || $.s === 'sawtooth')) {
|
||||
$.s = 'zaw'; // polyblepped saw when fm is applied
|
||||
}
|
||||
|
||||
if (shapes[$.s]) {
|
||||
const SourceClass = shapes[$.s];
|
||||
$._sound = new SourceClass();
|
||||
$._channels = 1;
|
||||
} else if (BufferPlayer.samples.has($.s)) {
|
||||
const sample = BufferPlayer.samples.get($.s);
|
||||
$._buffers = [];
|
||||
$._channels = sample.channels.length;
|
||||
for (let i = 0; i < $._channels; i++) {
|
||||
$._buffers.push(new BufferPlayer(sample.channels[i], sample.sampleRate, $.unit === 'c')); // tbd unit === 'c'
|
||||
}
|
||||
} else {
|
||||
console.warn('sound not loaded', $.s);
|
||||
}
|
||||
|
||||
if ($.penv) {
|
||||
$._penv = new ADSR({ decayCurve: 4 });
|
||||
[$.pattack, $.pdecay, $.psustain, $.prelease] = getADSR([$.pattack, $.pdecay, $.psustain, $.prelease]);
|
||||
}
|
||||
|
||||
if ($.vib) {
|
||||
$._vib = new SineOsc();
|
||||
$.vibmod = $.vibmod ?? getDefaultValue('vibmod');
|
||||
}
|
||||
|
||||
if ($.fmi) {
|
||||
$._fm = new SineOsc();
|
||||
$.fmh = $.fmh ?? getDefaultValue('fmh');
|
||||
if ($.fmenv) {
|
||||
$._fmenv = new ADSR({ decayCurve: 2 });
|
||||
[$.fmattack, $.fmdecay, $.fmsustain, $.fmrelease] = getADSR([$.fmattack, $.fmdecay, $.fmsustain, $.fmrelease]);
|
||||
}
|
||||
}
|
||||
|
||||
// gain envelope
|
||||
$._adsr = new ADSR({ decayCurve: 2 });
|
||||
// delay
|
||||
$.delay = applyGainCurve($.delay ?? getDefaultValue('delay'));
|
||||
$.delayfeedback = $.delayfeedback ?? getDefaultValue('delayfeedback');
|
||||
$.delayspeed = $.delayspeed ?? getDefaultValue('delayspeed');
|
||||
$.delaytime = $.delaytime ?? getDefaultValue('delaytime');
|
||||
|
||||
// filter setup
|
||||
if ($.lpenv) {
|
||||
$._lpenv = new ADSR({ decayCurve: 4 });
|
||||
[$.lpattack, $.lpdecay, $.lpsustain, $.lprelease] = getADSR([$.lpattack, $.lpdecay, $.lpsustain, $.lprelease]);
|
||||
}
|
||||
if ($.hpenv) {
|
||||
$._hpenv = new ADSR({ decayCurve: 4 });
|
||||
[$.hpattack, $.hpdecay, $.hpsustain, $.hprelease] = getADSR([$.hpattack, $.hpdecay, $.hpsustain, $.hprelease]);
|
||||
}
|
||||
if ($.bpenv) {
|
||||
$._bpenv = new ADSR({ decayCurve: 4 });
|
||||
[$.bpattack, $.bpdecay, $.bpsustain, $.bprelease] = getADSR([$.bpattack, $.bpdecay, $.bpsustain, $.bprelease]);
|
||||
}
|
||||
|
||||
// channelwise effects setup
|
||||
$._chorus = $.chorus ? [] : null;
|
||||
$._lpf = $.cutoff ? [] : null;
|
||||
$._hpf = $.hcutoff ? [] : null;
|
||||
$._bpf = $.bandf ? [] : null;
|
||||
$._coarse = $.coarse ? [] : null;
|
||||
$._crush = $.crush ? [] : null;
|
||||
$._distort = $.distort ? [] : null;
|
||||
for (let i = 0; i < this._channels; i++) {
|
||||
$._lpf?.push(new TwoPoleFilter());
|
||||
$._hpf?.push(new TwoPoleFilter());
|
||||
$._bpf?.push(new TwoPoleFilter());
|
||||
$._chorus?.push(new Chorus());
|
||||
$._coarse?.push(new Coarse());
|
||||
$._crush?.push(new Crush());
|
||||
$._distort?.push(new Distort());
|
||||
}
|
||||
}
|
||||
update(t) {
|
||||
if (!this._sound && !this._buffers) {
|
||||
return 0;
|
||||
}
|
||||
let gate = Number(t >= this._begin && t <= this._holdEnd);
|
||||
|
||||
let freq = this.freq * this.speed;
|
||||
|
||||
// frequency modulation
|
||||
if (this._fm) {
|
||||
let fmi = this.fmi;
|
||||
if (this._fmenv) {
|
||||
const env = this._fmenv.update(t, gate, this.fmattack, this.fmdecay, this.fmsustain, this.fmrelease);
|
||||
fmi = this.fmenv * env * fmi;
|
||||
}
|
||||
const modfreq = freq * this.fmh;
|
||||
const modgain = modfreq * fmi;
|
||||
freq = freq + this._fm.update(modfreq) * modgain;
|
||||
}
|
||||
|
||||
// vibrato
|
||||
if (this._vib) {
|
||||
freq = freq * 2 ** ((this._vib.update(this.vib) * this.vibmod) / 12);
|
||||
}
|
||||
|
||||
// pitch envelope
|
||||
if (this._penv) {
|
||||
const env = this._penv.update(t, gate, this.pattack, this.pdecay, this.psustain, this.prelease);
|
||||
freq = freq + env * this.penv;
|
||||
}
|
||||
|
||||
// filters
|
||||
let lpf = this.cutoff;
|
||||
if (this._lpf) {
|
||||
if (this._lpenv) {
|
||||
const env = this._lpenv.update(t, gate, this.lpattack, this.lpdecay, this.lpsustain, this.lprelease);
|
||||
lpf = this.lpenv * env * lpf + lpf;
|
||||
}
|
||||
}
|
||||
let hpf = this.hcutoff;
|
||||
if (this._hpf) {
|
||||
if (this._hpenv) {
|
||||
const env = this._hpenv.update(t, gate, this.hpattack, this.hpdecay, this.hpsustain, this.hprelease);
|
||||
hpf = 2 ** this.hpenv * env * hpf + hpf;
|
||||
}
|
||||
}
|
||||
let bpf = this.bandf;
|
||||
if (this._bpf) {
|
||||
if (this._bpenv) {
|
||||
const env = this._bpenv.update(t, gate, this.bpattack, this.bpdecay, this.bpsustain, this.bprelease);
|
||||
bpf = 2 ** this.bpenv * env * bpf + bpf;
|
||||
}
|
||||
}
|
||||
// gain envelope
|
||||
const env = this._adsr.update(t, gate, this.attack, this.decay, this.sustain, this.release);
|
||||
|
||||
// channelwise dsp
|
||||
for (let i = 0; i < this._channels; i++) {
|
||||
// sound source
|
||||
if (this._sound && this.s === 'pulse') {
|
||||
this.out[i] = this._sound.update(freq, this.pw);
|
||||
} else if (this._sound) {
|
||||
this.out[i] = this._sound.update(freq);
|
||||
} else if (this._buffers) {
|
||||
this.out[i] = this._buffers[i].update(freq);
|
||||
}
|
||||
this.out[i] = this.out[i] * this.gain * this.velocity;
|
||||
if (this._chorus) {
|
||||
const c = this._chorus[i].update(this.out[i], this.chorus, 0.03 + 0.05 * i, 1, 0.11);
|
||||
this.out[i] = c + this.out[i];
|
||||
}
|
||||
|
||||
if (this._lpf) {
|
||||
this._lpf[i].update(this.out[i], lpf, this.resonance);
|
||||
this.out[i] = this._lpf[i].s1;
|
||||
}
|
||||
if (this._hpf) {
|
||||
this._hpf[i].update(this.out[i], hpf, this.hresonance);
|
||||
this.out[i] = this.out[i] - this._hpf[i].s1;
|
||||
}
|
||||
if (this._bpf) {
|
||||
this._bpf[i].update(this.out[i], bpf, this.bandq);
|
||||
this.out[i] = this._bpf[i].s0;
|
||||
}
|
||||
if (this._coarse) {
|
||||
this.out[i] = this._coarse[i].update(this.out[i], this.coarse);
|
||||
}
|
||||
if (this._crush) {
|
||||
this.out[i] = this._crush[i].update(this.out[i], this.crush);
|
||||
}
|
||||
if (this._distort) {
|
||||
this.out[i] = this._distort[i].update(this.out[i], this.distort, this.distortvol);
|
||||
}
|
||||
this.out[i] = this.out[i] * env;
|
||||
this.out[i] = this.out[i] * this.postgain;
|
||||
if (!this._buffers) {
|
||||
this.out[i] = this.out[i] * 0.2; // turn down waveform
|
||||
}
|
||||
}
|
||||
if (this._channels === 1) {
|
||||
this.out[1] = this.out[0];
|
||||
}
|
||||
if (this.pan !== 0.5) {
|
||||
const panpos = (this.pan * Math.PI) / 2;
|
||||
this.out[0] = this.out[0] * Math.cos(panpos);
|
||||
this.out[1] = this.out[1] * Math.sin(panpos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// this class is the interface to the "outer world"
|
||||
// it handles spawning and despawning of DoughVoice's
|
||||
export class Dough {
|
||||
voices = []; // DoughVoice[]
|
||||
vid = 0;
|
||||
q = [];
|
||||
out = [0, 0];
|
||||
delaysend = [0, 0];
|
||||
delaytime = getDefaultValue('delaytime');
|
||||
delayfeedback = getDefaultValue('delayfeedback');
|
||||
delayspeed = getDefaultValue('delayspeed');
|
||||
t = 0;
|
||||
// sampleRate: number, currentTime: number (seconds)
|
||||
constructor(sampleRate = 48000, currentTime = 0) {
|
||||
this.sampleRate = sampleRate;
|
||||
this.t = Math.floor(currentTime * sampleRate); // samples
|
||||
// console.log('init dough', this.sampleRate, this.t);
|
||||
this._delayL = new PitchDelay();
|
||||
this._delayR = new PitchDelay();
|
||||
}
|
||||
loadSample(name, channels, sampleRate) {
|
||||
BufferPlayer.samples.set(name, { channels, sampleRate });
|
||||
}
|
||||
scheduleSpawn(value) {
|
||||
if (value._begin === undefined) {
|
||||
throw new Error('[dough]: scheduleSpawn expected _begin to be set');
|
||||
}
|
||||
if (value._duration === undefined) {
|
||||
throw new Error('[dough]: scheduleSpawn expected _duration to be set');
|
||||
}
|
||||
value.sampleRate = this.sampleRate;
|
||||
// convert seconds to samples
|
||||
const time = Math.floor(value._begin * this.sampleRate); // set from supradough.mjs
|
||||
this.schedule({ time, type: 'spawn', arg: value });
|
||||
}
|
||||
spawn(value) {
|
||||
value.id = this.vid++;
|
||||
const voice = new DoughVoice(value);
|
||||
this.voices.push(voice);
|
||||
// console.log('spawn', voice.id, 'voices:', this.voices.length);
|
||||
// schedule removal
|
||||
const endTime = Math.ceil(voice._end * this.sampleRate);
|
||||
this.schedule({ time: endTime /* + 48000 */, type: 'despawn', arg: voice.id });
|
||||
}
|
||||
despawn(vid) {
|
||||
this.voices = this.voices.filter((v) => v.id !== vid);
|
||||
// console.log('despawn', vid, 'voices:', this.voices.length);
|
||||
}
|
||||
// schedules a function call with a single argument
|
||||
// msg = {time:number,type:string, arg: any}
|
||||
// the Dough method "type" will be called with "arg" at "time"
|
||||
schedule(msg) {
|
||||
if (!this.q.length) {
|
||||
// if empty, just push
|
||||
this.q.push(msg);
|
||||
return;
|
||||
}
|
||||
// not empty
|
||||
// find index where msg.time fits in
|
||||
let i = 0;
|
||||
while (i < this.q.length && this.q[i].time < msg.time) {
|
||||
i++;
|
||||
}
|
||||
// this ensures q stays sorted by time, so we only need to check q[0]
|
||||
this.q.splice(i, 0, msg);
|
||||
}
|
||||
// maybe update should be called once per block instead for perf reasons?
|
||||
update() {
|
||||
// go over q
|
||||
while (this.q.length > 0 && this.q[0].time <= this.t) {
|
||||
// console.log('schedule', this.q[0]);
|
||||
// trigger due messages. q is sorted, so we only need to check q[0]
|
||||
this[this.q[0].type](this.q[0].arg); // type is expected to be a Dough method
|
||||
this.q.shift();
|
||||
}
|
||||
// add active voices
|
||||
this.out[0] = 0;
|
||||
this.out[1] = 0;
|
||||
for (let v = 0; v < this.voices.length; v++) {
|
||||
this.voices[v].update(this.t / this.sampleRate);
|
||||
this.out[0] += this.voices[v].out[0];
|
||||
this.out[1] += this.voices[v].out[1];
|
||||
if (this.voices[v].delay) {
|
||||
this.delaysend[0] += this.voices[v].out[0] * this.voices[v].delay;
|
||||
this.delaysend[1] += this.voices[v].out[1] * this.voices[v].delay;
|
||||
this.delaytime = this.voices[v].delaytime; // we trust that these are initialized in the voice
|
||||
this.delayspeed = this.voices[v].delayspeed; // we trust that these are initialized in the voice
|
||||
this.delayfeedback = this.voices[v].delayfeedback;
|
||||
}
|
||||
}
|
||||
// todo: how to change delaytime / delayfeedback from a voice?
|
||||
const delayL = this._delayL.update(this.delaysend[0], this.delaytime, this.delayspeed);
|
||||
const delayR = this._delayR.update(this.delaysend[1], this.delaytime, this.delayspeed);
|
||||
this.delaysend[0] = delayL * this.delayfeedback;
|
||||
this.delaysend[1] = delayR * this.delayfeedback;
|
||||
this.out[0] += delayL;
|
||||
this.out[1] += delayR;
|
||||
this.t++;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
// import _workletUrl from './dough-worklet.mjs?url'; // only for dev (breaks for production build)
|
||||
import _workletUrl from './dough-worklet.mjs?audioworklet'; // only for prod (breaks in development?!)
|
||||
|
||||
export * from './dough.mjs';
|
||||
export const workletUrl = _workletUrl;
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "supradough",
|
||||
"version": "1.2.3",
|
||||
"description": "platform agnostic synth and sampler intended for live coding. a reimplementation of superdough.",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
"publishConfig": {
|
||||
"main": "dist/index.mjs"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tidalcycles/strudel.git"
|
||||
},
|
||||
"keywords": [
|
||||
"tidalcycles",
|
||||
"strudel",
|
||||
"pattern",
|
||||
"livecoding",
|
||||
"algorave"
|
||||
],
|
||||
"author": "Felix Roos <flix91@gmail.com>",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"bugs": {
|
||||
"url": "https://github.com/tidalcycles/strudel/issues"
|
||||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"devDependencies": {
|
||||
"vite": "^6.0.11",
|
||||
"vite-plugin-bundle-audioworklet": "workspace:*",
|
||||
"wav-encoder": "^1.3.0"
|
||||
},
|
||||
"dependencies": {}
|
||||
}
|
||||
@@ -7,4 +7,5 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
export * from './webaudio.mjs';
|
||||
export * from './scope.mjs';
|
||||
export * from './spectrum.mjs';
|
||||
export * from './supradough.mjs';
|
||||
export * from 'superdough';
|
||||
|
||||
@@ -35,7 +35,8 @@
|
||||
"dependencies": {
|
||||
"@strudel/core": "workspace:*",
|
||||
"@strudel/draw": "workspace:*",
|
||||
"superdough": "workspace:*"
|
||||
"superdough": "workspace:*",
|
||||
"supradough": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^6.0.11"
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
import { Pattern } from '@strudel/core';
|
||||
import { connectToDestination, getAudioContext, getWorklet } from 'superdough';
|
||||
|
||||
let doughWorklet;
|
||||
|
||||
function initDoughWorklet() {
|
||||
const ac = getAudioContext();
|
||||
doughWorklet = getWorklet(
|
||||
ac,
|
||||
'dough-processor',
|
||||
{},
|
||||
{
|
||||
outputChannelCount: [2],
|
||||
},
|
||||
);
|
||||
connectToDestination(doughWorklet); // channels?
|
||||
}
|
||||
|
||||
const soundMap = new Map();
|
||||
const loadedSounds = new Map();
|
||||
|
||||
Pattern.prototype.supradough = function () {
|
||||
return this.onTrigger((hap, __, cps, begin) => {
|
||||
hap.value._begin = begin;
|
||||
hap.value._duration = hap.duration / cps;
|
||||
!doughWorklet && initDoughWorklet();
|
||||
const s = (hap.value.bank ? hap.value.bank + '_' : '') + hap.value.s;
|
||||
const n = hap.value.n ?? 0;
|
||||
const soundKey = `${s}:${n}`;
|
||||
if (soundMap.has(s)) {
|
||||
hap.value.s = soundKey; // dough.mjs is unaware of bank and n (only maps keys to buffers)
|
||||
}
|
||||
if (soundMap.has(s) && !loadedSounds.has(soundKey)) {
|
||||
const urls = soundMap.get(s);
|
||||
const url = urls[n % urls.length];
|
||||
console.log(`load ${soundKey} from ${url}`);
|
||||
const loadSample = fetchSample(url);
|
||||
loadedSounds.set(soundKey, loadSample);
|
||||
loadSample.then(({ channels, sampleRate }) =>
|
||||
doughWorklet.port.postMessage({
|
||||
sample: soundKey,
|
||||
channels,
|
||||
sampleRate,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
doughWorklet.port.postMessage({ spawn: hap.value });
|
||||
}, 1);
|
||||
};
|
||||
|
||||
function githubPath(base, subpath = '') {
|
||||
if (!base.startsWith('github:')) {
|
||||
throw new Error('expected "github:" at the start of pseudoUrl');
|
||||
}
|
||||
let [_, path] = base.split('github:');
|
||||
path = path.endsWith('/') ? path.slice(0, -1) : path;
|
||||
if (path.split('/').length === 2) {
|
||||
// assume main as default branch if none set
|
||||
path += '/main';
|
||||
}
|
||||
return `https://raw.githubusercontent.com/${path}/${subpath}`;
|
||||
}
|
||||
export async function fetchSampleMap(url) {
|
||||
if (url.startsWith('github:')) {
|
||||
url = githubPath(url, 'strudel.json');
|
||||
}
|
||||
if (url.startsWith('local:')) {
|
||||
url = `http://localhost:5432`;
|
||||
}
|
||||
if (url.startsWith('shabda:')) {
|
||||
let [_, path] = url.split('shabda:');
|
||||
url = `https://shabda.ndre.gr/${path}.json?strudel=1`;
|
||||
}
|
||||
if (url.startsWith('shabda/speech')) {
|
||||
let [_, path] = url.split('shabda/speech');
|
||||
path = path.startsWith('/') ? path.substring(1) : path;
|
||||
let [params, words] = path.split(':');
|
||||
let gender = 'f';
|
||||
let language = 'en-GB';
|
||||
if (params) {
|
||||
[language, gender] = params.split('/');
|
||||
}
|
||||
url = `https://shabda.ndre.gr/speech/${words}.json?gender=${gender}&language=${language}&strudel=1'`;
|
||||
}
|
||||
if (typeof fetch !== 'function') {
|
||||
// not a browser
|
||||
return;
|
||||
}
|
||||
const base = url.split('/').slice(0, -1).join('/');
|
||||
if (typeof fetch === 'undefined') {
|
||||
// skip fetch when in node / testing
|
||||
return;
|
||||
}
|
||||
const json = await fetch(url)
|
||||
.then((res) => res.json())
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
throw new Error(`error loading "${url}"`);
|
||||
});
|
||||
return [json, json._base || base];
|
||||
}
|
||||
|
||||
// for some reason, only piano and flute work.. is it because mp3??
|
||||
|
||||
async function fetchSample(url) {
|
||||
const buffer = await fetch(url)
|
||||
.then((res) => res.arrayBuffer())
|
||||
.then((buf) => getAudioContext().decodeAudioData(buf));
|
||||
let channels = [];
|
||||
for (let i = 0; i < buffer.numberOfChannels; i++) {
|
||||
channels.push(buffer.getChannelData(i));
|
||||
}
|
||||
return { channels, sampleRate: buffer.sampleRate };
|
||||
}
|
||||
|
||||
export async function doughsamples(sampleMap, baseUrl) {
|
||||
if (typeof sampleMap === 'string') {
|
||||
const [json, base] = await fetchSampleMap(sampleMap);
|
||||
// console.log('json', json, 'base', base);
|
||||
return doughsamples(json, base);
|
||||
}
|
||||
Object.entries(sampleMap).map(async ([key, urls]) => {
|
||||
if (key !== '_base') {
|
||||
urls = urls.map((url) => baseUrl + url);
|
||||
// console.log('set', key, urls);
|
||||
soundMap.set(key, urls);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -5,7 +5,12 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
*/
|
||||
|
||||
import * as strudel from '@strudel/core';
|
||||
import { superdough, getAudioContext, setLogger, doughTrigger } from 'superdough';
|
||||
import { superdough, getAudioContext, setLogger, doughTrigger, registerWorklet } from 'superdough';
|
||||
import './supradough.mjs';
|
||||
import { workletUrl } from 'supradough';
|
||||
|
||||
registerWorklet(workletUrl);
|
||||
|
||||
const { Pattern, logger, repl } = strudel;
|
||||
|
||||
setLogger(logger);
|
||||
|
||||
@@ -139,10 +139,10 @@ Tune.prototype.MIDI = function(stepIn,octaveIn) {
|
||||
|
||||
/* Load a new scale */
|
||||
|
||||
Tune.prototype.loadScale = function(name){
|
||||
Tune.prototype.loadScale = function(scale){
|
||||
|
||||
/* load the scale */
|
||||
var freqs = TuningList[name].frequencies
|
||||
var freqs = isArrayOfNumbers(scale) ? scale : TuningList[scale].frequencies
|
||||
this.scale = []
|
||||
for (var i=0;i<freqs.length-1;i++) {
|
||||
this.scale.push(freqs[i]/freqs[0])
|
||||
@@ -207,8 +207,13 @@ Tune.prototype.search = function(letters) {
|
||||
return possible
|
||||
}
|
||||
|
||||
Tune.prototype.isValidScale = function(name) {
|
||||
return !!TuningList[name];
|
||||
function isArrayOfNumbers(arg) {
|
||||
return Array.isArray(arg) && arg.length > 0 && arg.every(item => typeof item === 'number' && !isNaN(item));
|
||||
}
|
||||
|
||||
/* allow an array of values too */
|
||||
Tune.prototype.isValidScale = function(scale) {
|
||||
return !!TuningList[scale] || isArrayOfNumbers(scale) ;
|
||||
}
|
||||
|
||||
/* Return a collection of notes as an array */
|
||||
|
||||
Generated
+33
-10
@@ -201,8 +201,8 @@ importers:
|
||||
specifier: ^6.1.0
|
||||
version: 6.1.0(@codemirror/autocomplete@6.18.4)(@codemirror/commands@6.8.0)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)
|
||||
'@replit/codemirror-vim':
|
||||
specifier: ^6.2.1
|
||||
version: 6.2.1(@codemirror/commands@6.8.0)(@codemirror/language@6.10.8)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)
|
||||
specifier: ^6.3.0
|
||||
version: 6.3.0(@codemirror/commands@6.8.0)(@codemirror/language@6.10.8)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)
|
||||
'@replit/codemirror-vscode-keymap':
|
||||
specifier: ^6.0.2
|
||||
version: 6.0.2(@codemirror/autocomplete@6.18.4)(@codemirror/commands@6.8.0)(@codemirror/language@6.10.8)(@codemirror/lint@6.8.4)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)
|
||||
@@ -517,6 +517,18 @@ importers:
|
||||
specifier: workspace:*
|
||||
version: link:../vite-plugin-bundle-audioworklet
|
||||
|
||||
packages/supradough:
|
||||
devDependencies:
|
||||
vite:
|
||||
specifier: ^6.0.11
|
||||
version: 6.0.11(@types/node@22.10.10)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0)
|
||||
vite-plugin-bundle-audioworklet:
|
||||
specifier: workspace:*
|
||||
version: link:../vite-plugin-bundle-audioworklet
|
||||
wav-encoder:
|
||||
specifier: ^1.3.0
|
||||
version: 1.3.0
|
||||
|
||||
packages/tidal:
|
||||
dependencies:
|
||||
'@strudel/core':
|
||||
@@ -625,6 +637,9 @@ importers:
|
||||
superdough:
|
||||
specifier: workspace:*
|
||||
version: link:../superdough
|
||||
supradough:
|
||||
specifier: workspace:*
|
||||
version: link:../supradough
|
||||
devDependencies:
|
||||
vite:
|
||||
specifier: ^6.0.11
|
||||
@@ -2232,14 +2247,14 @@ packages:
|
||||
'@codemirror/state': ^6.0.1
|
||||
'@codemirror/view': ^6.3.0
|
||||
|
||||
'@replit/codemirror-vim@6.2.1':
|
||||
resolution: {integrity: sha512-qDAcGSHBYU5RrdO//qCmD8K9t6vbP327iCj/iqrkVnjbrpFhrjOt92weGXGHmTNRh16cUtkUZ7Xq7rZf+8HVow==}
|
||||
'@replit/codemirror-vim@6.3.0':
|
||||
resolution: {integrity: sha512-aTx931ULAMuJx6xLf7KQDOL7CxD+Sa05FktTDrtLaSy53uj01ll3Zf17JdKsriER248oS55GBzg0CfCTjEneAQ==}
|
||||
peerDependencies:
|
||||
'@codemirror/commands': ^6.0.0
|
||||
'@codemirror/language': ^6.1.0
|
||||
'@codemirror/search': ^6.2.0
|
||||
'@codemirror/state': ^6.0.1
|
||||
'@codemirror/view': ^6.0.3
|
||||
'@codemirror/commands': 6.x.x
|
||||
'@codemirror/language': 6.x.x
|
||||
'@codemirror/search': 6.x.x
|
||||
'@codemirror/state': 6.x.x
|
||||
'@codemirror/view': 6.x.x
|
||||
|
||||
'@replit/codemirror-vscode-keymap@6.0.2':
|
||||
resolution: {integrity: sha512-j45qTwGxzpsv82lMD/NreGDORFKSctMDVkGRopaP+OrzSzv+pXDQuU3LnFvKpasyjVT0lf+PKG1v2DSCn/vxxg==}
|
||||
@@ -5714,6 +5729,7 @@ packages:
|
||||
node-domexception@1.0.0:
|
||||
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
|
||||
engines: {node: '>=10.5.0'}
|
||||
deprecated: Use your platform's native DOMException instead
|
||||
|
||||
node-fetch-native@1.6.6:
|
||||
resolution: {integrity: sha512-8Mc2HhqPdlIfedsuZoc3yioPuzp6b+L5jRCRY1QzuWZh2EGJVQrGppC6V6cF0bLdbW0+O2YpqCA25aF/1lvipQ==}
|
||||
@@ -6803,6 +6819,7 @@ packages:
|
||||
source-map@0.8.0-beta.0:
|
||||
resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==}
|
||||
engines: {node: '>= 8'}
|
||||
deprecated: The work that was done in this beta branch won't be included in future versions
|
||||
|
||||
sourcemap-codec@1.4.8:
|
||||
resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==}
|
||||
@@ -7535,6 +7552,9 @@ packages:
|
||||
walk-up-path@3.0.1:
|
||||
resolution: {integrity: sha512-9YlCL/ynK3CTlrSRrDxZvUauLzAswPCrsaCgilqFevUYpeEW0/3ScEjaa3kbW/T0ghhkEr7mv+fpjqn1Y1YuTA==}
|
||||
|
||||
wav-encoder@1.3.0:
|
||||
resolution: {integrity: sha512-FXJdEu2qDOI+wbVYZpu21CS1vPEg5NaxNskBr4SaULpOJMrLE6xkH8dECa7PiS+ZoeyvP7GllWUAxPN3AvFSEw==}
|
||||
|
||||
wav@1.0.2:
|
||||
resolution: {integrity: sha512-viHtz3cDd/Tcr/HbNqzQCofKdF6kWUymH9LGDdskfWFoIy/HJ+RTihgjEcHfnsy1PO4e9B+y4HwgTwMrByquhg==}
|
||||
|
||||
@@ -7657,6 +7677,7 @@ packages:
|
||||
|
||||
workbox-google-analytics@7.0.0:
|
||||
resolution: {integrity: sha512-MEYM1JTn/qiC3DbpvP2BVhyIH+dV/5BjHk756u9VbwuAhu0QHyKscTnisQuz21lfRpOwiS9z4XdqeVAKol0bzg==}
|
||||
deprecated: It is not compatible with newer versions of GA starting with v4, as long as you are using GAv3 it should be ok, but the package is not longer being maintained
|
||||
|
||||
workbox-navigation-preload@7.0.0:
|
||||
resolution: {integrity: sha512-juWCSrxo/fiMz3RsvDspeSLGmbgC0U9tKqcUPZBCf35s64wlaLXyn2KdHHXVQrb2cqF7I0Hc9siQalainmnXJA==}
|
||||
@@ -9595,7 +9616,7 @@ snapshots:
|
||||
'@codemirror/state': 6.5.1
|
||||
'@codemirror/view': 6.36.2
|
||||
|
||||
'@replit/codemirror-vim@6.2.1(@codemirror/commands@6.8.0)(@codemirror/language@6.10.8)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)':
|
||||
'@replit/codemirror-vim@6.3.0(@codemirror/commands@6.8.0)(@codemirror/language@6.10.8)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)':
|
||||
dependencies:
|
||||
'@codemirror/commands': 6.8.0
|
||||
'@codemirror/language': 6.10.8
|
||||
@@ -15955,6 +15976,8 @@ snapshots:
|
||||
|
||||
walk-up-path@3.0.1: {}
|
||||
|
||||
wav-encoder@1.3.0: {}
|
||||
|
||||
wav@1.0.2:
|
||||
dependencies:
|
||||
buffer-alloc: 1.2.0
|
||||
|
||||
@@ -1847,6 +1847,27 @@ exports[`runs examples > example "chop" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "chorus" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/4 | note:d s:sawtooth chorus:0.5 ]",
|
||||
"[ 1/4 → 1/2 | note:d s:sawtooth chorus:0.5 ]",
|
||||
"[ 1/2 → 3/4 | note:a# s:sawtooth chorus:0.5 ]",
|
||||
"[ 3/4 → 1/1 | note:a s:sawtooth chorus:0.5 ]",
|
||||
"[ 1/1 → 5/4 | note:d s:sawtooth chorus:0.5 ]",
|
||||
"[ 5/4 → 3/2 | note:d s:sawtooth chorus:0.5 ]",
|
||||
"[ 3/2 → 7/4 | note:a# s:sawtooth chorus:0.5 ]",
|
||||
"[ 7/4 → 2/1 | note:a s:sawtooth chorus:0.5 ]",
|
||||
"[ 2/1 → 9/4 | note:d s:sawtooth chorus:0.5 ]",
|
||||
"[ 9/4 → 5/2 | note:d s:sawtooth chorus:0.5 ]",
|
||||
"[ 5/2 → 11/4 | note:a# s:sawtooth chorus:0.5 ]",
|
||||
"[ 11/4 → 3/1 | note:a s:sawtooth chorus:0.5 ]",
|
||||
"[ 3/1 → 13/4 | note:d s:sawtooth chorus:0.5 ]",
|
||||
"[ 13/4 → 7/2 | note:d s:sawtooth chorus:0.5 ]",
|
||||
"[ 7/2 → 15/4 | note:a# s:sawtooth chorus:0.5 ]",
|
||||
"[ 15/4 → 4/1 | note:a s:sawtooth chorus:0.5 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "chunk" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/4 | note:A4 ]",
|
||||
@@ -2585,6 +2606,52 @@ exports[`runs examples > example "delayfeedback" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "delayfeedback" example index 0 2`] = `
|
||||
[
|
||||
"[ 0/1 → 1/1 | s:bd delay:0.25 delayfeedback:0.25 ]",
|
||||
"[ 1/1 → 2/1 | s:bd delay:0.25 delayfeedback:0.5 ]",
|
||||
"[ 2/1 → 3/1 | s:bd delay:0.25 delayfeedback:0.75 ]",
|
||||
"[ 3/1 → 4/1 | s:bd delay:0.25 delayfeedback:1 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "delayspeed" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]",
|
||||
"[ 1/8 → 1/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]",
|
||||
"[ 1/4 → 3/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]",
|
||||
"[ 3/8 → 1/2 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]",
|
||||
"[ 1/2 → 5/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]",
|
||||
"[ 5/8 → 3/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]",
|
||||
"[ 3/4 → 7/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]",
|
||||
"[ 7/8 → 1/1 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]",
|
||||
"[ 1/1 → 9/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]",
|
||||
"[ 9/8 → 5/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]",
|
||||
"[ 5/4 → 11/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]",
|
||||
"[ 11/8 → 3/2 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]",
|
||||
"[ 3/2 → 13/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]",
|
||||
"[ 13/8 → 7/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]",
|
||||
"[ 7/4 → 15/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]",
|
||||
"[ 15/8 → 2/1 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]",
|
||||
"[ 2/1 → 17/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]",
|
||||
"[ 17/8 → 9/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]",
|
||||
"[ 9/4 → 19/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]",
|
||||
"[ 19/8 → 5/2 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]",
|
||||
"[ 5/2 → 21/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]",
|
||||
"[ 21/8 → 11/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]",
|
||||
"[ 11/4 → 23/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]",
|
||||
"[ 23/8 → 3/1 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]",
|
||||
"[ 3/1 → 25/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]",
|
||||
"[ 25/8 → 13/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]",
|
||||
"[ 13/4 → 27/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]",
|
||||
"[ 27/8 → 7/2 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]",
|
||||
"[ 7/2 → 29/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]",
|
||||
"[ 29/8 → 15/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]",
|
||||
"[ 15/4 → 31/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]",
|
||||
"[ 31/8 → 4/1 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "delaysync" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/2 | s:bd delay:0.25 delaysync:0.125 ]",
|
||||
@@ -2598,19 +2665,6 @@ exports[`runs examples > example "delaysync" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "delaytime" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/2 | s:bd delay:0.25 delaytime:0.125 ]",
|
||||
"[ 1/2 → 1/1 | s:bd delay:0.25 delaytime:0.125 ]",
|
||||
"[ 1/1 → 3/2 | s:bd delay:0.25 delaytime:0.25 ]",
|
||||
"[ 3/2 → 2/1 | s:bd delay:0.25 delaytime:0.25 ]",
|
||||
"[ 2/1 → 5/2 | s:bd delay:0.25 delaytime:0.5 ]",
|
||||
"[ 5/2 → 3/1 | s:bd delay:0.25 delaytime:0.5 ]",
|
||||
"[ 3/1 → 7/2 | s:bd delay:0.25 delaytime:1 ]",
|
||||
"[ 7/2 → 4/1 | s:bd delay:0.25 delaytime:1 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "density" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/4 | s:crackle density:0.01 ]",
|
||||
|
||||
@@ -103,6 +103,7 @@ export const SIDEBAR: Sidebar = {
|
||||
Understand: [
|
||||
{ text: 'Coding syntax', link: 'learn/code' },
|
||||
{ text: 'Pitch', link: 'understand/pitch' },
|
||||
{ text: 'Xen Harmonic Functions', link: 'learn/xen' },
|
||||
{ text: 'Cycles', link: 'understand/cycles' },
|
||||
{ text: 'Voicings', link: 'understand/voicings' },
|
||||
{ text: 'Pattern Alignment', link: 'technical-manual/alignment' },
|
||||
|
||||
@@ -44,7 +44,7 @@ xxx("foo").yyy("bar")
|
||||
|
||||
Generally, `xxx` and `yyy` are called [_functions_](<https://en.wikipedia.org/wiki/Function_(computer_programming)>), while `foo` and `bar` are called function [_arguments_ or _parameters_](<https://en.wikipedia.org/wiki/Parameter_(computer_programming)>).
|
||||
So far, we've used the functions to declare which aspect of the sound we want to control, and their arguments for the actual data.
|
||||
The `yyy` function is called a [_chained_ function](https://en.wikipedia.org/wiki/Method_chaining), because it is appended with a dot (`.`).
|
||||
The `yyy` function is called a [_chained_ function](https://en.wikipedia.org/wiki/Method_chaining), because it is preceded with a dot (`.`).
|
||||
|
||||
Generally, the idea with chaining is that code such as `a("this").b("that").c("other")` allows `a`, `b` and `c` functions to happen in a specified order, without needing to write them as three separate lines of code.
|
||||
You can think of this as being similar to chaining audio effects together using guitar pedals or digital audio effects.
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
---
|
||||
title: Xen Harmonic Functions
|
||||
layout: ../../layouts/MainLayout.astro
|
||||
---
|
||||
|
||||
import { MiniRepl } from '../../docs/MiniRepl';
|
||||
import { JsDoc } from '../../docs/JsDoc';
|
||||
|
||||
# Xen Harmonic Functions
|
||||
|
||||
These functions allow the use of scales other than your typical chromatic 12 based ones.
|
||||
|
||||
### tune(scale)
|
||||
|
||||
<JsDoc client:idle name="tune" h={0} />
|
||||
|
||||
Here's an example of how to configure a basic hexany scale:
|
||||
|
||||
<MiniRepl client:idle tune={`"0 1 2 3 4 5".tune("hexany15").mul("220").freq()`} />
|
||||
|
||||
Try other scales like `hexany1`, `iraq`, `gumbeng`, `gunkali`, or `tranh3`
|
||||
|
||||
For a full list of available scales from tunejs, see http://abbernie.github.io/tune/scales.html
|
||||
|
||||
You can set your root to be a particular note with `getFreq`
|
||||
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`"4 8 9 10 - - 5 7 9 11 - -".tune("tranh3")
|
||||
.mul(getFreq('c3'))
|
||||
.freq().clip(.5).room(1)`}
|
||||
/>
|
||||
|
||||
Some tunings become more pronounced with a longer reverb decay:
|
||||
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`"<[5 6 8 10] - [5 7 9 12] -> -".tune("gumbeng")
|
||||
.mul(getFreq('c3'))
|
||||
.freq().clip(.8).room("3:10").rdim(10000).rfade(5)`}
|
||||
/>
|
||||
|
||||
Additionally, you can combo this with `fmap` so that the base note changes:
|
||||
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`"9 11 12 10 - - -".tune("gunkali")
|
||||
.mul("<c3 c3 a3 d#3>".fmap(getFreq))
|
||||
.freq().legato("2 .7").room("1:15").rdim(8500).rlp(14000).rfade(8)`}
|
||||
/>
|
||||
|
||||
Combining this with various polyrhythm tricks can become very evocative:
|
||||
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`"<[0 3 1 -] [-1 4 2 8]> ~ ~,<-4 -5>"
|
||||
.transpose(4)
|
||||
.tune("iraq")
|
||||
.mul("<c3 d3 c#3>".fmap(getFreq))
|
||||
.freq().clip(.5).room(1).rfade(9)`}
|
||||
/>
|
||||
|
||||
Another helpful trick when exploring new tunings is to strum them.
|
||||
Many have a much more enchanting sound that was chosen over many generations of musicians for being strummed.
|
||||
|
||||
Take the `sanza` tuning:
|
||||
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`"4 5 6 7 8 9".tune("sanza")
|
||||
.mul(getFreq('c3'))
|
||||
.freq()`}
|
||||
/>
|
||||
|
||||
Notes 7 and 9 will clash quite a bit if you arp them normally. Many tunings will have this sort of sound, and it can feel distracting on its own.
|
||||
See how close they are on the pitch wheel?
|
||||
|
||||
<MiniRepl client:idle tune={`"[7 9]!3".tune("sanza").mul(getFreq('c3')).freq()._pitchwheel()`} />
|
||||
|
||||
This quality is often due to how the tunings were formed with instruments that were played differently than a piano.
|
||||
As such, some tunings are much better strummed, with the subtle clash of the detuned notes actually making the sound much more magical:
|
||||
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`"[0 1 2 3 4 5 6]@0.3 -"
|
||||
.transpose("<2 5 8 1>")
|
||||
.tune("sanza")
|
||||
.mul(getFreq('c3')).freq()
|
||||
.legato("3").room(1).rfade(5)`}
|
||||
/>
|
||||
|
||||
Note the legato and reverb effects make sure the sound of the strumming gets to wash together. Alternating the direction of the strum can make the
|
||||
tones sound even more alive, too.
|
||||
|
||||
The `tranh3` tuning has a similar set of notes, with two clashing. You might trying plugging that in above and see if you find a favorite strumming pattern.
|
||||
@@ -80,6 +80,8 @@
|
||||
min-width: 300px !important;
|
||||
max-height: 400px !important;
|
||||
background-color: var(--lineHighlight) !important;
|
||||
overflow: auto;
|
||||
background: var(--background) !important;
|
||||
}
|
||||
|
||||
/* Main tooltip container */
|
||||
@@ -91,7 +93,7 @@
|
||||
font-size: var(--font-size, 13px);
|
||||
line-height: 1.4;
|
||||
max-width: 600px;
|
||||
max-height: 400px;
|
||||
height: 100%;
|
||||
min-width: 400px;
|
||||
white-space: normal !important;
|
||||
overflow-y: auto !important;
|
||||
@@ -112,6 +114,13 @@
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.autocomplete-info-function-synonyms {
|
||||
margin: 0 0 12px 0;
|
||||
color: var(--foreground);
|
||||
line-height: 1.5;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.autocomplete-info-function-description {
|
||||
margin: 0 0 12px 0;
|
||||
color: var(--foreground);
|
||||
|
||||
@@ -2,9 +2,32 @@ import { useMemo, useState } from 'react';
|
||||
|
||||
import jsdocJson from '../../../../../doc.json';
|
||||
import { Textbox } from '../textbox/Textbox';
|
||||
const availableFunctions = jsdocJson.docs
|
||||
.filter(({ name, description }) => name && !name.startsWith('_') && !!description)
|
||||
.sort((a, b) => /* a.meta.filename.localeCompare(b.meta.filename) + */ a.name.localeCompare(b.name));
|
||||
|
||||
const isValid = ({ name, description }) => name && !name.startsWith('_') && !!description;
|
||||
|
||||
const availableFunctions = (() => {
|
||||
const seen = new Set(); // avoid repetition
|
||||
const functions = [];
|
||||
for (const doc of jsdocJson.docs) {
|
||||
if (!isValid(doc)) continue;
|
||||
functions.push(doc);
|
||||
const synonyms = doc.synonyms || [];
|
||||
for (const s of synonyms) {
|
||||
if (!s || seen.has(s)) continue;
|
||||
seen.add(s);
|
||||
// Swap `doc.name` in for `s` in the list of synonyms
|
||||
const synonymsWithDoc = [doc.name, ...synonyms].filter((x) => x && x !== s);
|
||||
functions.push({
|
||||
...doc,
|
||||
name: s, // update names for the synonym
|
||||
longname: s,
|
||||
synonyms: synonymsWithDoc,
|
||||
synonyms_text: synonymsWithDoc.join(', '),
|
||||
});
|
||||
}
|
||||
}
|
||||
return functions.sort((a, b) => /* a.meta.filename.localeCompare(b.meta.filename) + */ a.name.localeCompare(b.name));
|
||||
})();
|
||||
|
||||
const getInnerText = (html) => {
|
||||
var div = document.createElement('div');
|
||||
|
||||
Reference in New Issue
Block a user