Compare commits

..

92 Commits

Author SHA1 Message Date
Felix Roos 9782795761 fix: autocomplete container style 2025-09-14 01:04:48 +02:00
Felix Roos 2d2b238da9 fix: use template element for string to html 2025-09-14 01:03:14 +02:00
froos f0839e02eb Merge pull request 'Feature: Synonyms in autocomplete and reference' (#1535) from glossing/strudel:glossing/synonyms-in-autocomplete into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1535
2025-09-14 00:49:16 +02:00
Felix Roos a9f7d825c8 hotfix: format 2025-09-14 00:40:54 +02:00
Aria 709b654ed3 Add synonyms to autocomplete 2025-09-13 16:21:09 -05:00
froos d81a214c74 Merge pull request 'Docs: add xen package examples' (#1446) from dudymas/strudel:docs/xen/added-tune into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1446
2025-09-13 20:01:47 +02:00
froos e57f240e70 Merge branch 'main' into docs/xen/added-tune 2025-09-13 20:00:35 +02:00
Felix Roos e18296e760 hotfix: word 2025-09-13 19:52:07 +02:00
froos c145355ccc Merge pull request 'Update website/src/pages/learn/code.mdx' (#1391) from anecondev/strudel:main into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1391
2025-09-13 19:51:09 +02:00
Felix Roos a46617f3f0 hotfix: use ?audioworklet for supradough worklet import 2025-09-12 10:16:37 +02:00
Felix Roos d164b5e7f5 Revert "Revert "Merge pull request 'supradough poc' (#1362) from supradough into main""
This reverts commit c0bf86d11e.
2025-09-12 10:08:55 +02:00
Felix Roos c0bf86d11e Revert "Merge pull request 'supradough poc' (#1362) from supradough into main"
This reverts commit 6f06683d70, reversing
changes made to 637d714ab2.
2025-09-11 23:19:22 +02:00
Switch Angel AKA Jade Rose 6f06683d70 Merge pull request 'supradough poc' (#1362) from supradough into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1362
2025-09-11 22:52:29 +02:00
Felix Roos 637d714ab2 Publish
- @strudel/mondo@1.1.4
2025-09-11 19:14:00 +02:00
Felix Roos c42d00f048 hotfix: export mondo getLocations 2025-09-11 19:11:45 +02:00
Felix Roos 702e558d1a Publish
- @strudel/mondo@1.1.3
2025-09-11 17:30:38 +02:00
froos bcc2e8f759 Merge pull request 'fix: exclude mondough dependencies' (#1563) from fix-mondo-package into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1563
2025-09-11 17:29:34 +02:00
Felix Roos 27339715b7 fix: exclude mondough dependencies 2025-09-11 17:22:06 +02:00
Felix Roos 5e2e6411e5 Publish
- @strudel/codemirror@1.2.5
 - @strudel/repl@1.2.6
2025-09-11 08:26:20 +02:00
froos 0fac16e769 Merge pull request 'add basicSetup for keybindings' (#1462) from Dsm0/strudel:codemirror-keymap-fix into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1462
2025-09-11 00:27:20 +02:00
Aria c199b51645 Update autocomplete name to be the label and update ref to use main name as a synonym 2025-09-02 16:29:01 -05:00
Aria 8d2a368da9 Add some more synonyms to controls docs 2025-09-02 16:14:50 -05:00
Aria 052d09e892 Add synonyms to reference 2025-09-02 16:14:38 -05:00
Aria 439a7dc5e6 Include synonyms in autocomplete 2025-09-02 16:05:40 -05:00
Jade (Rose) Rowland b8e1b8d7ac Merge branch 'main' into supradough 2025-08-31 22:42:07 -04:00
daslyfe a26559fd3d Merge branch 'main' into supradough 2025-07-22 06:35:59 +02:00
Dsm0 5448fc6077 removed duplication and unnecessary keymappings in codemirror config 2025-07-16 15:17:34 -07:00
Dsm0 06fafbd8eb Ensure no default extensions conflict with extensions in defaultSettings 2025-07-16 15:03:55 -07:00
Dsm0 10156f0067 add basicSetup for keybindings 2025-07-15 19:13:24 -07:00
Felix Roos 5632d83afb fix: supradough onTrigger after breaking change 2025-07-16 02:21:39 +02:00
Felix Roos a32473d570 Merge remote-tracking branch 'origin/main' into supradough 2025-07-15 22:26:46 +02:00
dudymas e87bb4376f Merge branch 'main' into docs/xen/added-tune 2025-07-05 20:28:09 +02:00
dudymas 0722cf7ded feat(xen/tunejs): support sending a custom tuning 2025-07-05 13:43:42 -04:00
dudymas 37fde8f3e8 docs(website/learn/xen): discussed strumming 2025-07-05 13:43:09 -04:00
dudymas 8655c5b73c Merge branch 'main' into docs/xen/added-tune 2025-07-05 00:50:57 +02:00
dudymas 31c1450edc docs(website/learn/xen): add tune() examples 2025-07-04 18:50:21 -04:00
dudymas cbb4207bab chore(docker): basic support added 2025-07-04 18:50:21 -04:00
anecondev 5efcd14f40 Update website/src/pages/learn/code.mdx
Correct "appended" to "prepended"
2025-06-18 17:07:13 +02:00
froos 8051e56d13 Merge branch 'main' into supradough 2025-06-14 00:35:52 +02:00
Felix Roos 239987af0b snapshot 2025-06-12 10:23:06 +02:00
Felix Roos 8363ba5a41 format 2025-06-12 10:22:35 +02:00
Felix Roos 30f7127a92 Merge remote-tracking branch 'origin/main' into supradough 2025-06-12 10:22:08 +02:00
Felix Roos 14193abe03 improve export: add stereo + measure performance + add bigger pattern for testing 2025-06-12 02:26:50 +02:00
Felix Roos 3caf08e199 up latency 2025-06-12 02:26:08 +02:00
Felix Roos 5cbd38a3e8 improve filter performance making it accept hz directly 2025-06-12 02:25:46 +02:00
Felix Roos bbd7ed0f27 lazy sample loading 2025-06-12 02:25:04 +02:00
Jade (Rose) Rowland 43504d16b6 add chorus 2025-06-10 16:03:42 +02:00
Jade (Rose) Rowland 7e8206dcbc pitched delay 2025-06-10 12:31:21 +02:00
Felix Roos 3963b807eb fix: calling doughsamples multiple times 2025-06-09 22:32:41 +02:00
Felix Roos e99adffc56 vibrato 2025-06-09 22:29:35 +02:00
Felix Roos c9f494d865 fix: node export 2025-06-09 22:10:09 +02:00
Felix Roos e8254735bb apply speed to synths and samples + add normalize flag (for fit) 2025-06-09 19:35:50 +02:00
Jade (Rose) Rowland 3d30007756 supersaw 2025-06-09 15:55:44 +02:00
Jade (Rose) Rowland aeaed50446 gaincurve 2025-06-09 15:18:47 +02:00
Felix Roos 3ac0fdf619 rudimentary sample map loading (doughsamples) 2025-06-09 11:07:57 +02:00
Felix Roos d1b6b6be85 fix: hpf 2025-06-09 09:54:24 +02:00
Felix Roos 1081fc69e9 refactor: stereo sample support + channelwise fx 2025-06-09 09:52:29 +02:00
Felix Roos 73ef43dd2c move things around in init
+ shorten things
2025-06-08 22:56:55 +02:00
Jade (Rose) Rowland 181e51ef0e adjustable envelope curves 2025-06-08 20:26:20 +02:00
Jade (Rose) Rowland 1167fdb910 Merge branch 'supradough' of https://github.com/tidalcycles/strudel into supradough 2025-06-08 19:25:47 +02:00
Jade (Rose) Rowland ec9109dd89 exponential lerp 2025-06-08 19:24:55 +02:00
Felix Roos 55aef18851 buffers can now be repitched (still with aliasing) 2025-06-08 14:43:23 +02:00
Felix Roos 356d4360ec fix: scheduling + sample playback 2025-06-08 09:05:14 +02:00
Felix Roos 0f2aa9569b half working samples 2025-06-08 02:04:49 +02:00
Felix Roos ac7ee376e5 pitch envelope 2025-06-08 00:43:58 +02:00
Felix Roos ab052ce1ac fix: polyblepped sawtooth + fm = death 2025-06-07 23:15:09 +02:00
Felix Roos 5988e3d3b4 fix: hot reloading for dough.mjs (see comment) 2025-06-07 22:42:48 +02:00
Felix Roos 2f2bf5bf29 fix: note2midi for notes without octave 2025-06-07 22:36:54 +02:00
Felix Roos b000f2297d fm 2025-06-07 20:31:47 +02:00
Felix Roos 77463a199a add square alias for pulse 2025-06-07 13:26:43 +02:00
Felix Roos e54449f5f7 white brown pink noise + crackle alias for dust 2025-06-07 13:24:06 +02:00
Felix Roos d743570546 set delay time and feedback from voice 2025-06-07 13:12:35 +02:00
Felix Roos 1dac69635d feedback delay (still hard coded feedback / time values) 2025-06-07 10:40:34 +02:00
Felix Roos 49f38aeddb remove log 2025-06-06 16:26:17 +02:00
Felix Roos 43c05cb504 move supradough to separate package 2025-06-06 16:17:05 +02:00
Felix Roos ccc90d547c ignore dough-export to fix checks 2025-06-06 15:47:19 +02:00
Felix Roos 998a010247 better filter envelopes 2025-06-06 11:21:01 +02:00
Felix Roos e4cd5dc552 rough filter envelope implementation 2025-06-06 10:55:38 +02:00
Felix Roos 6cf0b6f651 hpf + bpf 2025-06-06 10:12:49 +02:00
Felix Roos 794bf86904 distort 2025-06-06 09:56:41 +02:00
Felix Roos 9ed690ce45 pulse osc + velocity 2025-06-06 09:37:40 +02:00
Felix Roos 623bf93368 panning 2025-06-06 08:50:02 +02:00
Felix Roos 0cc18a6468 crush (might be wrong) 2025-06-06 08:27:47 +02:00
Felix Roos 87598e10d1 coarse 2025-06-06 08:17:20 +02:00
Felix Roos ccec7e725a poc: render pattern audio with node script 2025-06-05 11:04:55 +02:00
Felix Roos f0e6c5483c handle invalid spawns + remove logs 2025-06-05 10:30:53 +02:00
Felix Roos 2cbd35f6d2 refactor: make webaudio glue as minimal as possible 2025-06-05 10:26:14 +02:00
Felix Roos 930dabe4d6 dough as a singleton worklet with voice allocation 2025-06-05 09:54:44 +02:00
Jade (Rose) Rowland ff4b7ca171 fix volume 2025-06-04 01:08:36 +01:00
Jade (Rose) Rowland 5129fa6677 fix click 2025-06-04 00:48:32 +01:00
Felix Roos 5336569c25 proper lpf mapping + remove redundant worklet params, 2025-06-01 21:20:28 +02:00
Felix Roos 483843bd0a begin supradough 2025-06-01 18:06:36 +02:00
36 changed files with 1813 additions and 119 deletions
+21
View File
@@ -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"]
+1
View File
@@ -42,6 +42,7 @@ export default [
'**/hydra.mjs',
'**/jsdoc-synonyms.js',
'packages/hs2js/src/hs2js.mjs',
'packages/supradough/dough-export.mjs',
'**/samples',
],
},
+40 -11
View File
@@ -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*/);
+63
View File
@@ -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]),
])();
+8 -3
View File
@@ -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(
+3 -2
View File
@@ -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 -2
View File
@@ -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()) : []];
}
+2 -2
View File
@@ -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:*",
+5 -4
View File
@@ -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 };
},
+34 -6
View File
@@ -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()
+7 -5
View File
@@ -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"])
*
+1 -1
View File
@@ -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 -1
View File
@@ -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",
+2 -2
View File
@@ -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 -1
View File
@@ -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": {
+48 -43
View File
@@ -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(
+8 -2
View File
@@ -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;
+1
View File
@@ -0,0 +1 @@
pattern.wav
+3
View File
@@ -0,0 +1,3 @@
# supradough
platform agnostic synth and sampler intended for live coding. a reimplementation of superdough.
+123
View File
@@ -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));
});
+39
View File
@@ -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);
+976
View File
@@ -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++;
}
}
+5
View File
@@ -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;
+37
View File
@@ -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": {}
}
+1
View File
@@ -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';
+2 -1
View File
@@ -35,7 +35,8 @@
"dependencies": {
"@strudel/core": "workspace:*",
"@strudel/draw": "workspace:*",
"superdough": "workspace:*"
"superdough": "workspace:*",
"supradough": "workspace:*"
},
"devDependencies": {
"vite": "^6.0.11"
+130
View File
@@ -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);
}
});
}
+6 -1
View File
@@ -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);
+9 -4
View File
@@ -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 */
+33 -10
View File
@@ -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
+67 -13
View File
@@ -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 ]",
+1
View File
@@ -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' },
+1 -1
View File
@@ -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.
+95
View File
@@ -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.
+10 -1
View File
@@ -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');