Compare commits

...

26 Commits

Author SHA1 Message Date
Jade (Rose) Rowland 5bc3b5e9a7 Merge branch 'main' into show12 2026-10-15 14:56:24 -04:00
froos 173a1654c8 Merge pull request 'fix hs2js + tidal package' (#2149) from fix-tidal-package into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/2149
2026-10-15 17:32:01 +02:00
Felix Roos dedd687a1f Merge remote-tracking branch 'origin/main' into fix-tidal-package 2026-10-15 16:12:59 +02:00
froos 6abb9f85a8 Merge pull request 'remove supradough' (#2147) from remove-supradough into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/2147
2026-10-15 15:07:11 +02:00
Felix Roos 4f3ba01afc fix snapshots 2026-10-15 15:01:10 +02:00
Felix Roos f440d7869c remove supradough 2026-10-15 14:57:59 +02:00
Jade (Rose) Rowland 2d7f918876 Merge branch 'main' into show12 2026-10-14 20:34:22 -04:00
Felix Roos d385ceba39 fix tidal package wasm url for vite build 2026-10-12 18:28:41 +02:00
Jade (Rose) Rowland 9258857e36 Merge branch 'main' into show12
Resolves conflicts in packages/superdough/sampler.mjs (merged imports and
getSampleInfo/getDuration additions from main) and website/src/settings.mjs
(non-overlapping additions auto-merged).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01559pA7fMAMyY9H91PaewXz
2026-09-14 02:35:56 -04:00
Jade (Rose) Rowland ad7e301394 repeat charactar 2026-07-30 12:13:47 +01:00
Jade (Rose) Rowland 7acb9a3ac7 patterntaborder 2026-04-09 17:26:37 -07:00
Jade (Rose) Rowland 9665b233a1 sort by pattern string 2026-04-03 23:41:46 -04:00
Jade (Rose) Rowland 6c67851909 fix merge conflict 2026-02-16 01:42:51 -05:00
Jade (Rose) Rowland 109ea3093b Merge branch 'soloshortcut' into show5 2026-02-11 19:56:37 -05:00
Jade (Rose) Rowland 8d991ad8ae Merge branch 'sample_note' into show5 2026-02-11 19:53:20 -05:00
Jade (Rose) Rowland 0b69ce21c7 jsdoc 2026-02-08 21:11:10 -05:00
Jade (Rose) Rowland 4f05674cb3 working 2026-02-08 20:32:59 -05:00
Jade (Rose) Rowland feab3f5c86 freq 2026-02-08 19:25:10 -05:00
Jade (Rose) Rowland 90c05ec38a fix edge cases 2026-02-08 14:12:21 -05:00
Jade (Rose) Rowland 7e0eed87cb normalize shortcuts 2026-01-27 17:21:36 -05:00
Jade (Rose) Rowland 1a1197f67a Merge branch 'main' into soloshortcut 2026-01-22 21:11:31 -05:00
Jade (Rose) Rowland 3c5afb32f3 scroll to center 2026-01-22 02:04:27 -05:00
Jade (Rose) Rowland 76cbd23859 Merge branch 'soloshortcut' of ssh://codeberg.org/uzu/strudel into soloshortcut 2026-01-22 00:25:41 -05:00
Jade (Rose) Rowland b436ae789c rm dead code 2026-01-22 00:25:28 -05:00
Switch Angel AKA Jade Rose 275731afc7 Merge branch 'main' into soloshortcut 2026-01-22 06:22:17 +01:00
Jade (Rose) Rowland 5cb2214d88 working 2026-01-22 00:20:44 -05:00
28 changed files with 471 additions and 1695 deletions
-1
View File
@@ -42,7 +42,6 @@ export default [
'**/hydra.mjs',
'**/jsdoc-synonyms.js',
'packages/hs2js/src/hs2js.mjs',
'packages/supradough/dough-export.mjs',
'**/samples',
],
},
+79 -6
View File
@@ -11,6 +11,7 @@ import {
keymap,
lineNumbers,
} from '@codemirror/view';
import {repeatCharKeymap} from './repeatcharacter.mjs';
import { persistentAtom } from '@nanostores/persistent';
import { registerControl, repl } from '@strudel/core';
import { cleanupDraw, Drawer } from '@strudel/draw';
@@ -24,7 +25,12 @@ import { sliderPlugin, updateSliderWidgets } from './slider.mjs';
import { activateTheme, initTheme, theme } from './themes.mjs';
import { isTooltipEnabled } from './tooltip.mjs';
import { updateWidgets, widgetPlugin } from './widget.mjs';
import { jumpToCharacter } from './labelJump.mjs';
import {
deleteAllInlineBeforeCharacter,
InsertCharBeforeChar,
jumpToCharacter,
jumpToNextCharacter,
} from './labelJump.mjs';
export { toggleBlockComment, toggleBlockCommentByLine, toggleComment, toggleLineComment } from '@codemirror/commands';
@@ -44,9 +50,9 @@ export const extensions = {
isMultiCursorEnabled: (on) =>
on
? [
EditorState.allowMultipleSelections.of(true),
EditorView.clickAddsSelectionRange.of((ev) => ev.metaKey || ev.ctrlKey),
]
EditorState.allowMultipleSelections.of(true),
EditorView.clickAddsSelectionRange.of((ev) => ev.metaKey || ev.ctrlKey),
]
: [],
};
export const compartments = Object.fromEntries(Object.keys(extensions).map((key) => [key, new Compartment()]));
@@ -74,6 +80,10 @@ export const codemirrorSettings = persistentAtom('codemirror-settings', defaultS
decode: JSON.parse,
});
const ANON_LABEL = '$';
const SOLO_LABEL = 'S';
const MUTE_LABEL = '_';
// https://codemirror.net/docs/guide/
export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, root, mondo }) {
const settings = codemirrorSettings.get();
@@ -101,6 +111,7 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo
syntaxHighlighting(defaultHighlightStyle),
EditorView.updateListener.of((v) => onChange(v)),
drawSelection({ cursorBlinkRate: 0 }),
repeatCharKeymap,
Prec.highest(
keymap.of([
{
@@ -122,12 +133,74 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo
},
{
key: 'Alt-w',
run: (view) => jumpToCharacter(view, '$', 1),
run: (view) => jumpToNextCharacter(view, ANON_LABEL, 1),
},
{
key: 'Alt-q',
run: (view) => jumpToCharacter(view, '$', -1),
run: (view) => {
return jumpToNextCharacter(view, ANON_LABEL, -1);
},
},
// clear all muted
{
key: `Alt-Ctrl-0`,
run: (view) => {
return deleteAllInlineBeforeCharacter(view, MUTE_LABEL + ANON_LABEL);
},
},
// clear all solod
{
key: `Alt-Shift-0`,
run: (view) => {
return deleteAllInlineBeforeCharacter(view, SOLO_LABEL + ANON_LABEL);
},
},
// clear all solo and mute
{
key: `Ctrl-Shift-0`,
run: (view) => {
return deleteAllInlineBeforeCharacter(view, ANON_LABEL);
},
},
...Array.from({ length: 9 }).map((_, i) => {
let num = i + 1;
return {
key: `Alt-${num}`,
run: (view) => {
return jumpToCharacter(view, ANON_LABEL, i);
},
};
}),
// handle solo toggles 1-9
...Array.from({ length: 9 }).map((_, i) => {
let num = i + 1;
return {
key: `Alt-Shift-${num}`,
run: (view) => {
return InsertCharBeforeChar(view, ANON_LABEL, SOLO_LABEL, i);
},
};
}),
// handle mute toggles 1-9
...Array.from({ length: 9 }).map((_, i) => {
let num = i + 1;
return {
key: `Alt-Ctrl-${num}`,
run: (view) => {
return InsertCharBeforeChar(view, ANON_LABEL, MUTE_LABEL, i);
},
};
}),
// Handle clearing mutes and solos 1-9
...Array.from({ length: 9 }).map((_, i) => {
let num = i + 1;
return {
key: `Ctrl-Shift-${num}`,
run: (view) => {
return InsertCharBeforeChar(view, ANON_LABEL, '', i);
},
};
}),
/* {
key: 'Ctrl-Shift-.',
run: () => (onPanic ? onPanic() : onStop?.()),
+133 -11
View File
@@ -1,18 +1,54 @@
import { EditorSelection } from '@codemirror/state';
import { SearchCursor } from '@codemirror/search';
import { EditorView } from '@codemirror/view';
import { syntaxTree } from '@codemirror/language';
export function jumpToCharacter(view, character, direction = 1) {
/**
* gets all of the positions of a character in a document, excluding commented out lines
* @param { EditorState} state
* @param {String} character
* @returns {number[]}
*/
function getCharacterPositions(state, character) {
const cursor = new SearchCursor(state.doc, character);
const characterPositions = [];
while (!cursor.next().done) {
const linestartpos = state.doc.lineAt(cursor.value.to).from
if (!isLineCommentedOut(state, linestartpos)) {
characterPositions.push(cursor.value.to);
}
}
return characterPositions;
}
function isLineCommentedOut(state, pos) {
const line = state.doc.lineAt(pos);
// remove white space
pos = line.from + line.text.search(/\S/)
const tree = syntaxTree(state);
const node = tree.resolveInner(pos, 1)
return node.name.includes("Comment")
}
/**
* jump to the next character in a document
* @param {EditorView} view
* @param {String} character
* @param {number} direction 0 or 1
* @returns {boolean}
*/
export function jumpToNextCharacter(view, character, direction = 1) {
const { state, dispatch } = view;
const pos = state.selection.main.head;
const cursor = new SearchCursor(state.doc, character);
let characterPositions = [];
let jumpPos;
while (!cursor.next().done) {
characterPositions.push(cursor.value.to);
}
const characterPositions = getCharacterPositions(state, character);
if (!characterPositions.length) {
return false;
return true;
}
if (direction > 0) {
jumpPos = characterPositions.find((x) => x > pos + 1) ?? characterPositions.at(0); // Loop back around for convenience
@@ -21,11 +57,97 @@ export function jumpToCharacter(view, character, direction = 1) {
}
if (jumpPos == null) {
return false;
return true;
}
const selection = EditorSelection.cursor(jumpPos - 1);
dispatch({
selection: EditorSelection.cursor(jumpPos - 1),
scrollIntoView: true,
selection,
effects: EditorView.scrollIntoView(
selection.head,
{ y: "start" }
)
});
return true;
}
/**
*
* @param {EditorView} view
* @param {String} character
* @param {number} index the instance of the character
* @returns {true}
*/
export function jumpToCharacter(view, character, index) {
const { state, dispatch } = view;
const characterPositions = getCharacterPositions(state, character);
const pos = characterPositions.at(index) ?? characterPositions.at(-1);
if (pos == null) {
return true;
}
const selection = EditorSelection.cursor(pos - 1);
dispatch({
selection,
effects: EditorView.scrollIntoView(
selection.head,
{ y: "start" }
)
});
return true;
}
/**
*
* @param {EditorView} view
* @param {String} character
* @returns {true}
*/
export function deleteAllInlineBeforeCharacter(view, character) {
const { state, dispatch } = view;
const characterPositions = getCharacterPositions(state, character);
const changes = [];
characterPositions.forEach((pos) => {
const line = state.doc.lineAt(pos);
if (state.doc.sliceString(line.from, line.from + 2) === COMMENT_STRING) {
return;
}
changes.push({
from: line.from,
to: pos - 1,
insert: '',
});
});
dispatch({ changes });
return true;
}
/**
*
* @param {EditorView} view
* @param {String} character
* @param {String} character2
* @param {number} index
* @returns {true}
*/
export function InsertCharBeforeChar(view, character, character2, index) {
const { state, dispatch } = view;
const changes = [];
const characterPositions = getCharacterPositions(state, character);
const labelpos = characterPositions.at(index) ?? characterPositions.at(-1);
const line = state.doc.lineAt(labelpos);
//delete preceeding characters
changes.push({
from: line.from,
to: labelpos - 1,
insert: '',
});
changes.push({
insert: character2,
from: line.from,
});
dispatch({ changes });
return true;
}
+55
View File
@@ -0,0 +1,55 @@
import { Compartment } from "@codemirror/state";
import { keymap } from "@codemirror/view";
const repeatMode = new Compartment();
function repeatPreviousChar(times) {
return ({ state, dispatch }) => {
const { from, empty } = state.selection.main;
if (!empty || from === 0) return false;
const prevChar = state.doc.sliceString(from - 1, from);
const text = Array(times).fill(prevChar).join(" ");
dispatch(state.update({
changes: {
from,
insert: " " + text
}
}));
return true;
};
}
function exitRepeatMode(view) {
view.dispatch({
effects: repeatMode.reconfigure([])
});
}
const digitBindings = Array.from({ length: 9 }, (_, i) => ({
key: String(i + 1),
run(view) {
repeatPreviousChar(i + 1)(view);
exitRepeatMode(view);
return true;
}
}));
export const repeatCharKeymap = [
repeatMode.of([]),
keymap.of([
{
key: "Alt-r",
run(view) {
view.dispatch({
effects: repeatMode.reconfigure(keymap.of(digitBindings))
});
return true;
}
}
])
];
+52 -66
View File
@@ -446,7 +446,7 @@ export const { accelerate } = registerControl('accelerate');
* Sets the velocity from 0 to 1. Is multiplied together with gain.
*
* @name velocity
* @tags amplitude, superdough, supradough
* @tags amplitude, superdough
* @synonyms vel
* @example
* s("hh*8")
@@ -458,7 +458,7 @@ export const { velocity, vel } = registerControl('velocity', 'vel');
* Controls the gain by an exponential amount.
*
* @name gain
* @tags amplitude, superdough, supradough
* @tags amplitude, superdough
* @param {number | Pattern} amount gain.
* @example
* s("hh*8").gain(".4!2 1 .4!2 1 .4 1").fast(2)
@@ -469,7 +469,7 @@ export const { gain } = registerControl('gain');
* Gain applied after all effects have been processed.
*
* @name postgain
* @tags amplitude, superdough, supradough
* @tags amplitude, superdough
* @example
* s("bd sd [~ bd] sd,hh*8")
* .compressor("-20:20:10:.002:.02").postgain(1.5)
@@ -499,7 +499,7 @@ export const { amp } = registerControl('amp');
* any of the 8 individual FMs (e.g. `fmh2`)
*
* @name fmh
* @tags fm, superdough, supradough
* @tags fm, superdough
* @param {number | Pattern} harmonicity
* @example
* note("c e g b g e")
@@ -520,7 +520,7 @@ export const { fmh, fmh1, fmh2, fmh3, fmh4, fmh5, fmh6, fmh7, fmh8 } = registerM
* `fm3`
*
* @name fm
* @tags fm, superdough, supradough
* @tags fm, superdough
* @param {number | Pattern} brightness modulation index
* @synonyms fmi
* @example
@@ -544,7 +544,7 @@ export const { fmi, fmi1, fmi2, fmi3, fmi4, fmi5, fmi6, fmi7, fmi8, fm, fm1, fm2
* any of the 8 individual FMs (e.g. `fmenv4`)
*
* @name fmenv
* @tags fm, envelope, superdough, supradough
* @tags fm, envelope, superdough
* @param {number | Pattern} type lin | exp
* @synonyms fme
* @example
@@ -569,7 +569,7 @@ export const { fmenv, fmenv1, fmenv2, fmenv3, fmenv4, fmenv5, fmenv6, fmenv7, fm
* any of the 8 individual FMs (e.g. `fmatt5`)
*
* @name fmattack
* @tags fm, envelope, superdough, supradough
* @tags fm, envelope, superdough
* @synonyms fmatt
* @param {number | Pattern} time attack time
* @synonyms fmatt
@@ -608,7 +608,7 @@ export const {
* any of the 8 individual FMs (e.g. `fmwave6`)
*
* @name fmwave
* @tags fm, superdough, supradough
* @tags fm, superdough
* @param {number | Pattern} wave waveform
* @example
* n("0 1 2 3".fast(4)).scale("d:minor").s("sine").fmwave("<sine square sawtooth crackle>").fm(4).fmh(2.01)
@@ -628,7 +628,7 @@ export const { fmwave, fmwave1, fmwave2, fmwave3, fmwave4, fmwave5, fmwave6, fmw
* any of the 8 individual FMs (e.g. `fmdec6`)
*
* @name fmdecay
* @tags fm, envelope, superdough, supradough
* @tags fm, envelope, superdough
* @synonyms fmdec
* @param {number | Pattern} time decay time
* @synonyms fmdec
@@ -668,7 +668,7 @@ export const {
* any of the 8 individual FMs (e.g. `fmsus7`)
*
* @name fmsustain
* @tags fm, envelope, superdough, supradough
* @tags fm, envelope, superdough
* @synonyms fmsus
* @param {number | Pattern} level sustain level
* @synonyms fmsus
@@ -708,7 +708,7 @@ export const {
* any of the 8 individual FMs (e.g. `fmrel8`)
*
* @name fmrelease
* @tags fm, envelope, superdough, supradough
* @tags fm, envelope, superdough
* @synonyms fmrel
* @param {number | Pattern} time release time
*
@@ -778,7 +778,7 @@ export const { fft } = registerControl('fft');
* Amplitude envelope attack time: Specifies how long it takes for the sound to reach its peak value, relative to the onset.
*
* @name attack
* @tags amplitude, envelope, superdough, supradough
* @tags amplitude, envelope, superdough
* @param {number | Pattern} attack time in seconds.
* @synonyms att
* @example
@@ -792,7 +792,7 @@ export const { attack, att } = registerControl('attack', 'att');
* Note that the decay is only audible if the sustain value is lower than 1.
*
* @name decay
* @tags amplitude, envelope, superdough, supradough
* @tags amplitude, envelope, superdough
* @param {number | Pattern} time decay time in seconds
* @synonyms dec
* @example
@@ -804,7 +804,7 @@ export const { decay, dec } = registerControl('decay', 'dec');
* Amplitude envelope sustain level: The level which is reached after attack / decay, being sustained until the offset.
*
* @name sustain
* @tags amplitude, envelope, superdough, supradough
* @tags amplitude, envelope, superdough
* @param {number | Pattern} gain sustain level between 0 and 1
* @synonyms sus
* @example
@@ -816,7 +816,7 @@ export const { sustain, sus } = registerControl('sustain', 'sus');
* Amplitude envelope release time: The time it takes after the offset to go from sustain level to zero.
*
* @name release
* @tags amplitude, envelope, superdough, supradough
* @tags amplitude, envelope, superdough
* @param {number | Pattern} time release time in seconds
* @synonyms rel
* @example
@@ -831,7 +831,7 @@ export const { hold } = registerControl('hold');
* can also optionally supply the 'bpq' parameter separated by ':'.
*
* @name bpf
* @tags filter, superdough, supradough
* @tags filter, superdough
* @param {number | Pattern} frequency center frequency
* @synonyms bandf, bp
* @example
@@ -844,7 +844,7 @@ export const { bandf, bpf, bp } = registerControl(['bandf', 'bandq', 'bpenv'], '
* Sets the **b**and-**p**ass **q**-factor (resonance).
*
* @name bpq
* @tags filter, superdough, supradough
* @tags filter, superdough
* @param {number | Pattern} q q factor
* @synonyms bandq
* @example
@@ -923,7 +923,7 @@ export const { loopEnd, loope } = registerControl('loopEnd', 'loope');
* Bit crusher effect.
*
* @name crush
* @tags superdough, supradough
* @tags superdough
* @param {number | Pattern} depth between 1 (for drastic reduction in bit-depth) to 16 (for barely no reduction).
* @example
* s("<bd sd>,hh*3").fast(2).crush("<16 8 7 6 5 4 3 2>")
@@ -935,7 +935,7 @@ export const { crush } = registerControl('crush');
* Fake-resampling for lowering the sample rate. Caution: This effect seems to only work in chromium based browsers
*
* @name coarse
* @tags superdough, supradough
* @tags superdough
* @param {number | Pattern} factor 1 for original 2 for half, 3 for a third and so on.
* @example
* s("bd sd [~ bd] sd,hh*8").coarse("<1 4 8 16 32>")
@@ -1294,7 +1294,7 @@ export const { cut } = registerControl('cut');
* When using mininotation, you can also optionally add the 'lpq' parameter, separated by ':'.
*
* @name lpf
* @tags filter, superdough, supradough
* @tags filter, superdough
* @param {number | Pattern} frequency audible between 0 and 20000
* @synonyms cutoff, ctf, lp
* @example
@@ -1308,7 +1308,7 @@ export const { cutoff, ctf, lpf, lp } = registerControl(['cutoff', 'resonance',
/**
* Sets the lowpass filter envelope modulation depth.
* @name lpenv
* @tags filter, envelope, superdough, supradough
* @tags filter, envelope, superdough
* @param {number | Pattern} modulation depth of the lowpass filter envelope between 0 and _n_
* @synonyms lpe
* @example
@@ -1322,7 +1322,7 @@ export const { lpenv, lpe } = registerControl('lpenv', 'lpe');
/**
* Sets the highpass filter envelope modulation depth.
* @name hpenv
* @tags filter, envelope, superdough, supradough
* @tags filter, envelope, superdough
* @param {number | Pattern} modulation depth of the highpass filter envelope between 0 and _n_
* @synonyms hpe
* @example
@@ -1336,7 +1336,7 @@ export const { hpenv, hpe } = registerControl('hpenv', 'hpe');
/**
* Sets the bandpass filter envelope modulation depth.
* @name bpenv
* @tags filter, envelope, superdough, supradough
* @tags filter, envelope, superdough
* @param {number | Pattern} modulation depth of the bandpass filter envelope between 0 and _n_
* @synonyms bpe
* @example
@@ -1350,7 +1350,7 @@ export const { bpenv, bpe } = registerControl('bpenv', 'bpe');
/**
* Sets the attack duration for the lowpass filter envelope.
* @name lpattack
* @tags filter, envelope, superdough, supradough
* @tags filter, envelope, superdough
* @param {number | Pattern} attack time of the filter envelope
* @synonyms lpa
* @example
@@ -1364,7 +1364,7 @@ export const { lpattack, lpa } = registerControl('lpattack', 'lpa');
/**
* Sets the attack duration for the highpass filter envelope.
* @name hpattack
* @tags filter, envelope, superdough, supradough
* @tags filter, envelope, superdough
* @param {number | Pattern} attack time of the highpass filter envelope
* @synonyms hpa
* @example
@@ -1378,7 +1378,7 @@ export const { hpattack, hpa } = registerControl('hpattack', 'hpa');
/**
* Sets the attack duration for the bandpass filter envelope.
* @name bpattack
* @tags filter, envelope, superdough, supradough
* @tags filter, envelope, superdough
* @param {number | Pattern} attack time of the bandpass filter envelope
* @synonyms bpa
* @example
@@ -1392,7 +1392,7 @@ export const { bpattack, bpa } = registerControl('bpattack', 'bpa');
/**
* Sets the decay duration for the lowpass filter envelope.
* @name lpdecay
* @tags filter, envelope, superdough, supradough
* @tags filter, envelope, superdough
* @param {number | Pattern} decay time of the filter envelope
* @synonyms lpd
* @example
@@ -1406,7 +1406,7 @@ export const { lpdecay, lpd } = registerControl('lpdecay', 'lpd');
/**
* Sets the decay duration for the highpass filter envelope.
* @name hpdecay
* @tags filter, envelope, superdough, supradough
* @tags filter, envelope, superdough
* @param {number | Pattern} decay time of the highpass filter envelope
* @synonyms hpd
* @example
@@ -1421,7 +1421,7 @@ export const { hpdecay, hpd } = registerControl('hpdecay', 'hpd');
/**
* Sets the decay duration for the bandpass filter envelope.
* @name bpdecay
* @tags filter, envelope, superdough, supradough
* @tags filter, envelope, superdough
* @param {number | Pattern} decay time of the bandpass filter envelope
* @synonyms bpd
* @example
@@ -1436,7 +1436,7 @@ export const { bpdecay, bpd } = registerControl('bpdecay', 'bpd');
/**
* Sets the sustain amplitude for the lowpass filter envelope.
* @name lpsustain
* @tags filter, envelope, superdough, supradough
* @tags filter, envelope, superdough
* @param {number | Pattern} sustain amplitude of the lowpass filter envelope
* @synonyms lps
* @example
@@ -1451,7 +1451,7 @@ export const { lpsustain, lps } = registerControl('lpsustain', 'lps');
/**
* Sets the sustain amplitude for the highpass filter envelope.
* @name hpsustain
* @tags filter, envelope, superdough, supradough
* @tags filter, envelope, superdough
* @param {number | Pattern} sustain amplitude of the highpass filter envelope
* @synonyms hps
* @example
@@ -1466,7 +1466,7 @@ export const { hpsustain, hps } = registerControl('hpsustain', 'hps');
/**
* Sets the sustain amplitude for the bandpass filter envelope.
* @name bpsustain
* @tags filter, envelope, superdough, supradough
* @tags filter, envelope, superdough
* @param {number | Pattern} sustain amplitude of the bandpass filter envelope
* @synonyms bps
* @example
@@ -1481,7 +1481,7 @@ export const { bpsustain, bps } = registerControl('bpsustain', 'bps');
/**
* Sets the release time for the lowpass filter envelope.
* @name lprelease
* @tags filter, envelope, superdough, supradough
* @tags filter, envelope, superdough
* @param {number | Pattern} release time of the filter envelope
* @synonyms lpr
* @example
@@ -1497,7 +1497,7 @@ export const { lprelease, lpr } = registerControl('lprelease', 'lpr');
/**
* Sets the release time for the highpass filter envelope.
* @name hprelease
* @tags filter, envelope, superdough, supradough
* @tags filter, envelope, superdough
* @param {number | Pattern} release time of the highpass filter envelope
* @synonyms hpr
* @example
@@ -1513,7 +1513,7 @@ export const { hprelease, hpr } = registerControl('hprelease', 'hpr');
/**
* Sets the release time for the bandpass filter envelope.
* @name bprelease
* @tags filter, envelope, superdough, supradough
* @tags filter, envelope, superdough
* @param {number | Pattern} release time of the bandpass filter envelope
* @synonyms bpr
* @example
@@ -1558,7 +1558,7 @@ export const { fanchor } = registerControl('fanchor');
* When using mininotation, you can also optionally add the 'hpq' parameter, separated by ':'.
*
* @name hpf
* @tags filter, superdough, supradough
* @tags filter, superdough
* @param {number | Pattern} frequency audible between 0 and 20000
* @synonyms hp, hcutoff
* @example
@@ -1781,7 +1781,7 @@ export const { hpskew } = registerControl('hpskew');
* Applies a vibrato to the frequency of the oscillator.
*
* @name vib
* @tags pitch, lfo, superdough, supradough
* @tags pitch, lfo, superdough
* @synonyms vibrato, v
* @param {number | Pattern} frequency of the vibrato in hertz
* @example
@@ -1799,7 +1799,7 @@ export const { vib, vibrato, v } = registerControl(['vib', 'vibmod'], 'vibrato',
* Adds pink noise to the mix
*
* @name noise
* @tags generators, superdough, supradough
* @tags generators, superdough
* @param {number | Pattern} wet wet amount
* @example
* sound("<white pink brown>/2")
@@ -1809,7 +1809,7 @@ export const { noise } = registerControl('noise');
* Sets the vibrato depth in semitones. Only has an effect if `vibrato` | `vib` | `v` is is also set
*
* @name vibmod
* @tags pitch, lfo, superdough, supradough
* @tags pitch, lfo, superdough
* @synonyms vmod
* @param {number | Pattern} depth of vibrato (in semitones)
* @example
@@ -1828,7 +1828,7 @@ export const { hcutoff, hpf, hp } = registerControl(['hcutoff', 'hresonance', 'h
* Controls the **h**igh-**p**ass **q**-value.
*
* @name hpq
* @tags filter, superdough, supradough
* @tags filter, superdough
* @param {number | Pattern} q resonance factor between 0 and 50
* @synonyms hresonance
* @example
@@ -1840,7 +1840,7 @@ export const { hresonance, hpq } = registerControl('hresonance', 'hpq');
* Controls the **l**ow-**p**ass **q**-value.
*
* @name lpq
* @tags filter, superdough, supradough
* @tags filter, superdough
* @param {number | Pattern} q resonance factor between 0 and 50
* @synonyms resonance
* @example
@@ -1870,7 +1870,7 @@ export const { djf } = registerControl('djf');
*
*
* @name delay
* @tags orbit, superdough, supradough
* @tags orbit, superdough
* @param {number | Pattern} level between 0 and 1
* @example
* s("bd bd").delay("<0 .25 .5 1>")
@@ -1884,7 +1884,7 @@ export const { delay } = registerControl(['delay', 'delaytime', 'delayfeedback']
* Caution: Values >= 1 will result in a signal that gets louder and louder! Don't do it
*
* @name delayfeedback
* @tags orbit, superdough, supradough
* @tags orbit, superdough
* @param {number | Pattern} feedback between 0 and 1
* @synonyms delayfb, dfb
* @example
@@ -1893,24 +1893,11 @@ export const { delay } = registerControl(['delay', 'delaytime', 'delayfeedback']
*/
export const { delayfeedback, delayfb, dfb } = registerControl('delayfeedback', 'delayfb', 'dfb');
/**
* Sets the time of the delay effect.
*
* @name delayspeed
* @tags supradough
* @param {number | Pattern} delayspeed controls the pitch of the delay feedback
* @synonyms delayt, dt
* @example
* note("d d a# a".fast(2)).s("sawtooth").delay(.8).delaytime(1/2).delayspeed("<2 .5 -1 -2>")
*
*/
export const { delayspeed } = registerControl('delayspeed');
/**
* Sets the time of the delay effect in seconds.
*
* @name delaytime
* @tags orbit, superdough, supradough
* @tags orbit, superdough
* @param {number | Pattern} delay in seconds
* @synonyms delayt, dt
* @example
@@ -1918,7 +1905,6 @@ export const { delayspeed } = registerControl('delayspeed');
* .s("sawtooth")
* .delay(.8)
* .delaytime(1/2)
* .delayspeed("<2 .5 -1 -2>")
*/
export const { delaytime, delayt, dt } = registerControl('delaytime', 'delayt', 'dt');
@@ -2026,7 +2012,7 @@ export const { freq } = registerControl('freq');
* Attack time of pitch envelope.
*
* @name pattack
* @tags pitch, envelope, superdough, supradough
* @tags pitch, envelope, superdough
* @synonyms patt
* @param {number | Pattern} time time in seconds
* @example
@@ -2038,7 +2024,7 @@ export const { pattack, patt } = registerControl('pattack', 'patt');
* Decay time of pitch envelope.
*
* @name pdecay
* @tags pitch, envelope, superdough, supradough
* @tags pitch, envelope, superdough
* @synonyms pdec
* @param {number | Pattern} time time in seconds
* @example
@@ -2052,7 +2038,7 @@ export const { psustain, psus } = registerControl('psustain', 'psus');
* Release time of pitch envelope
*
* @name prelease
* @tags pitch, envelope, superdough, supradough
* @tags pitch, envelope, superdough
* @synonyms prel
* @param {number | Pattern} time time in seconds
* @example
@@ -2067,7 +2053,7 @@ export const { prelease, prel } = registerControl('prelease', 'prel');
* If you don't set other pitch envelope controls, `pattack:.2` will be the default.
*
* @name penv
* @tags pitch, envelope, superdough, supradough
* @tags pitch, envelope, superdough
* @param {number | Pattern} semitones change in semitones
* @example
* note("c")
@@ -2248,7 +2234,7 @@ export const { overshape } = registerControl('overshape');
* Sets position in stereo.
*
* @name pan
* @tags superdough, supradough
* @tags superdough
* @param {number | Pattern} pan between 0 and 1, from left to right (assuming stereo), once round a circle (assuming multichannel)
* @example
* s("[bd hh]*2").pan("<.5 1 .5 0>")
@@ -2506,7 +2492,7 @@ export const { shape } = registerControl(['shape', 'shapevol']);
* Most useful values are usually between 0 and 10 (depending on source gain). If you are feeling adventurous, you can turn it up to 11 and beyond ;)
*
* @name distort
* @tags distortion, superdough, supradough
* @tags distortion, superdough
* @synonyms dist
* @param {number | Pattern} distortion amount of distortion to apply
* @param {number | Pattern} volume linear postgain of the distortion
@@ -2526,7 +2512,7 @@ export const { distort, dist } = registerControl(['distort', 'distortvol', 'dist
*
* @name distortvol
* @synonyms distortion, distvol
* @tags superdough, supradough
* @tags superdough
* @param {number | Pattern} volume linear postgain of the distortion
* @example
* s("bd*4").bank("tr909").distort(2).distortvol(0.8)
@@ -2537,7 +2523,7 @@ export const { distortvol } = registerControl('distortvol', 'distvol');
* Type of waveshaping distortion to apply.
*
* @name distorttype
* @tags distortion, superdough, supradough
* @tags distortion, superdough
* @synonyms disttype
* @param {number | string | Pattern} type type of distortion to apply
* @example
+2 -2
View File
@@ -29,10 +29,10 @@
},
"homepage": "https://codeberg.org/uzu/strudel/",
"dependencies": {
"web-tree-sitter": "^0.24.7"
"web-tree-sitter": "^0.24.7",
"tree-sitter-haskell": "^0.23.1"
},
"devDependencies": {
"tree-sitter-haskell": "^0.23.1",
"vite": "^6.0.11"
},
"engines": {
+9 -3
View File
@@ -1,6 +1,8 @@
import Parser from 'web-tree-sitter';
import treeSitterWasm from 'web-tree-sitter/tree-sitter.wasm?url';
import grammarWasm from 'tree-sitter-haskell/tree-sitter-haskell.wasm?url';
let base = '/';
let base;
export function setBase(path) {
base = path;
}
@@ -10,11 +12,15 @@ let isReady = false,
async function _loadParser() {
await Parser.init({
locateFile(scriptName, scriptDirectory) {
return `${base}${scriptName}`;
if (base) {
return `${base}${scriptName}`;
}
return treeSitterWasm;
},
});
parser = new Parser();
const Lang = await Parser.Language.load(`${base}tree-sitter-haskell.wasm`);
let grammarUrl = base ? `${base}tree-sitter-haskell.wasm` : grammarWasm;
const Lang = await Parser.Language.load(grammarUrl);
parser.setLanguage(Lang);
isReady = true;
return parser;
+30 -4
View File
@@ -1,4 +1,4 @@
import { getBaseURL, getCommonSampleInfo } from './util.mjs';
import { BASE_MIDI_NOTE, getBaseURL, getCommonSampleInfo, noteToFreq, noteToMidi } from './util.mjs';
import { registerSound, registerWaveTable, soundMap } from './index.mjs';
import { getAudioContext } from './audioContext.mjs';
import {
@@ -14,6 +14,13 @@ import { logger } from './logger.mjs';
const bufferCache = {}; // string: Promise<ArrayBuffer>
const loadCache = {}; // string: Promise<ArrayBuffer>
/**
*
* @typedef {Object} SampleMetaData
* @property {string} url
* @property {midi} number
*/
export const getCachedBuffer = (url) => bufferCache[url];
function humanFileSize(bytes, si) {
@@ -56,8 +63,9 @@ export const getDur = getDuration;
export function getSampleInfo(hapValue, bank) {
const { speed = 1.0 } = hapValue;
const { transpose, url, index, midi, label } = getCommonSampleInfo(hapValue, bank);
let playbackRate = Math.abs(speed) * Math.pow(2, transpose / 12);
const { transpose, url, index, midi, label, baseFrequency } = getCommonSampleInfo(hapValue, bank);
const playbackRate = Math.abs(speed) * Math.pow(2, transpose / 12);
return { transpose, url, index, midi, label, playbackRate };
}
@@ -180,7 +188,12 @@ export const processSampleMap = (sampleMap, fn, baseUrl = sampleMap._base || '')
if (baseUrl.startsWith('github:')) {
baseUrl = githubPath(baseUrl, '');
}
const fullUrl = (v) => baseUrl + v;
/**
*
* @param {string} v
* @returns {SampleMetaData}
*/
const fullUrl = (v) => ({ url: baseUrl + v, midi: extractMidiNoteFromString(v) });
if (Array.isArray(value)) {
//return [key, value.map(replaceUrl)];
value = value.map(fullUrl);
@@ -391,3 +404,16 @@ export function registerSampleSource(key, bank, params) {
registerSample(key, bank, params);
}
}
export function extractMidiNoteFromString(str) {
const regex = /_([a-gA-G])([#b])?([1-9])?\b/;
const match = str.match(regex);
if (match == null) {
return BASE_MIDI_NOTE;
}
const base = match[1].toUpperCase();
const accidental = match[2] ?? '';
const octave_str = match[3] ?? '';
const parsedVal = base + accidental + octave_str;
return noteToMidi(parsedVal);
}
+42 -10
View File
@@ -19,7 +19,12 @@ const accs = { '#': 1, b: -1, s: 1, f: -1 };
export const getAccidentalsOffset = (accidentals) => {
return accidentals?.split('').reduce((o, char) => o + accs[char], 0) || 0;
};
/**
*
* @param {string} note
* @param {number} defaultOctave
* @returns {number}
*/
export const noteToMidi = (note, defaultOctave = 3) => {
const [pc, acc, oct = defaultOctave] = tokenizeNote(note);
if (!pc) {
@@ -29,6 +34,11 @@ export const noteToMidi = (note, defaultOctave = 3) => {
const offset = getAccidentalsOffset(acc);
return (Number(oct) + 1) * 12 + chroma + offset;
};
/**
*
* @param {number} n
* @returns {number}
*/
export const midiToFreq = (n) => {
return Math.pow(2, (n - 69) / 12) * 440;
};
@@ -38,7 +48,17 @@ export const freqToMidi = (freq) => {
return (12 * Math.log(freq / 440)) / Math.LN2 + 69;
};
export const valueToMidi = (value, fallbackValue) => {
/**
*
* @param {string} note
* @param {number} defaultOctave
* @returns {number}
*/
export const noteToFreq = (note, defaultOctave = 3) => {
return midiToFreq(noteToMidi(note, defaultOctave));
};
function __valueToMidi(value) {
if (typeof value !== 'object') {
throw new Error('valueToMidi: expected object value');
}
@@ -52,10 +72,15 @@ export const valueToMidi = (value, fallbackValue) => {
if (typeof note === 'number') {
return note;
}
if (!fallbackValue) {
return;
}
export const valueToMidi = (value, fallbackValue) => {
const parsedValue = __valueToMidi(value) ?? fallbackValue;
if (parsedValue == null) {
throw new Error('valueToMidi: expected freq or note to be set');
}
return fallbackValue;
return parsedValue;
};
export function nanFallback(value, fallback = 0, silent) {
@@ -84,15 +109,18 @@ export function secondsToCycle(t, cps) {
// deduces relevant info for sample loading from hap.value and sample definition
// it encapsulates the core sampler logic into a pure and synchronous function
// hapValue: Hap.value, bank: sample bank definition for sound "s" (values in strudel.json format)
export const BASE_MIDI_NOTE = 36;
export function getCommonSampleInfo(hapValue, bank) {
const { s, n = 0 } = hapValue;
let midi = valueToMidi(hapValue, 36);
let transpose = midi - 36; // C3 is middle C;
let url;
const maybeMidiNote = __valueToMidi(hapValue);
const midi = maybeMidiNote ?? BASE_MIDI_NOTE;
let transpose = midi - BASE_MIDI_NOTE; // C3 is middle C;
let index = 0;
let samplemeta;
if (Array.isArray(bank)) {
index = getSoundIndex(n, bank.length);
url = bank[index];
samplemeta = bank[index];
} else {
const midiDiff = (noteA) => noteToMidi(noteA) - midi;
// object format will expect keys as notes
@@ -104,10 +132,14 @@ export function getCommonSampleInfo(hapValue, bank) {
);
transpose = -midiDiff(closest); // semitones to repitch
index = getSoundIndex(n, bank[closest].length);
url = bank[closest][index];
samplemeta = bank[closest][index];
}
const label = `${s}:${index}`;
return { transpose, url, index, midi, label };
if (maybeMidiNote != null) {
transpose = transpose + (BASE_MIDI_NOTE - samplemeta.midi);
}
return { transpose, index, midi, label, url: samplemeta.url };
}
/** Selects entries from `source` and renames them via `map`
-1
View File
@@ -1 +0,0 @@
pattern.wav
-3
View File
@@ -1,3 +0,0 @@
# supradough
platform agnostic synth and sampler intended for live coding. a reimplementation of superdough.
-123
View File
@@ -1,123 +0,0 @@
// 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
@@ -1,39 +0,0 @@
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);
File diff suppressed because it is too large Load Diff
-5
View File
@@ -1,5 +0,0 @@
// 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;
-39
View File
@@ -1,39 +0,0 @@
{
"name": "supradough",
"version": "1.2.4",
"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://codeberg.org/uzu/strudel.git"
},
"keywords": [
"tidalcycles",
"strudel",
"pattern",
"livecoding",
"algorave"
],
"author": "Felix Roos <flix91@gmail.com>",
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://codeberg.org/uzu/strudel/issues"
},
"homepage": "https://codeberg.org/uzu/strudel#readme",
"devDependencies": {
"vite": "^6.0.11",
"vite-plugin-bundle-audioworklet": "workspace:*",
"wav-encoder": "^1.3.0"
},
"engines": {
"node": ">=18.0.0"
}
}
-16
View File
@@ -1,16 +0,0 @@
import { defineConfig } from 'vite';
import { resolve } from 'path';
import bundleAudioWorkletPlugin from 'vite-plugin-bundle-audioworklet';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [bundleAudioWorkletPlugin()],
build: {
lib: {
entry: resolve(__dirname, 'index.mjs'),
formats: ['es'],
fileName: (ext) => ({ es: 'index.mjs' })[ext],
},
target: 'esnext',
},
});
+1
View File
@@ -3,6 +3,7 @@
"version": "0.2.0",
"private": true,
"description": "Experimental Tidal Code interpreter",
"main": "tidal.mjs",
"module": "tidal.mjs",
"repository": {
"type": "git",
-1
View File
@@ -7,5 +7,4 @@ 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';
+1 -2
View File
@@ -35,8 +35,7 @@
"dependencies": {
"@strudel/core": "workspace:*",
"@strudel/draw": "workspace:*",
"superdough": "workspace:*",
"supradough": "workspace:*"
"superdough": "workspace:*"
},
"devDependencies": {
"vite": "^6.0.11"
-130
View File
@@ -1,130 +0,0 @@
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.onSchedule((hap, begin, cps) => {
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 supradoughsamples(sampleMap, baseUrl) {
if (typeof sampleMap === 'string') {
const [json, base] = await _fetchSampleMap(sampleMap);
// console.log('json', json, 'base', base);
return supradoughsamples(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);
}
});
}
-3
View File
@@ -17,10 +17,7 @@ import {
resetGlobalEffects,
errorLogger,
} from 'superdough';
import './supradough.mjs';
import { workletUrl } from 'supradough';
import { SuperdoughAudioController } from 'superdough/superdoughoutput.mjs';
registerWorklet(workletUrl);
const { Pattern, logger, repl } = strudel;
+3 -23
View File
@@ -359,13 +359,13 @@ importers:
packages/hs2js:
dependencies:
tree-sitter-haskell:
specifier: ^0.23.1
version: 0.23.1(tree-sitter@0.21.1)
web-tree-sitter:
specifier: ^0.24.7
version: 0.24.7
devDependencies:
tree-sitter-haskell:
specifier: ^0.23.1
version: 0.23.1(tree-sitter@0.21.1)
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)
@@ -606,18 +606,6 @@ 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':
@@ -729,9 +717,6 @@ importers:
superdough:
specifier: workspace:*
version: link:../superdough
supradough:
specifier: workspace:*
version: link:../supradough
devDependencies:
vite:
specifier: ^6.0.11
@@ -7782,9 +7767,6 @@ 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==}
@@ -16365,8 +16347,6 @@ snapshots:
walk-up-path@3.0.1: {}
wav-encoder@1.3.0: {}
wav@1.0.2:
dependencies:
buffer-alloc: 1.2.0
+32 -69
View File
@@ -3138,43 +3138,6 @@ exports[`runs examples > example "delayfeedback" example index 0 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 ]",
@@ -3190,38 +3153,38 @@ exports[`runs examples > example "delaysync" example index 0 1`] = `
exports[`runs examples > example "delaytime" 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 ]",
"[ 0/1 → 1/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 ]",
"[ 1/8 → 1/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 ]",
"[ 1/4 → 3/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 ]",
"[ 3/8 → 1/2 | note:a s:sawtooth delay:0.8 delaytime:0.5 ]",
"[ 1/2 → 5/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 ]",
"[ 5/8 → 3/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 ]",
"[ 3/4 → 7/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 ]",
"[ 7/8 → 1/1 | note:a s:sawtooth delay:0.8 delaytime:0.5 ]",
"[ 1/1 → 9/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 ]",
"[ 9/8 → 5/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 ]",
"[ 5/4 → 11/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 ]",
"[ 11/8 → 3/2 | note:a s:sawtooth delay:0.8 delaytime:0.5 ]",
"[ 3/2 → 13/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 ]",
"[ 13/8 → 7/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 ]",
"[ 7/4 → 15/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 ]",
"[ 15/8 → 2/1 | note:a s:sawtooth delay:0.8 delaytime:0.5 ]",
"[ 2/1 → 17/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 ]",
"[ 17/8 → 9/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 ]",
"[ 9/4 → 19/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 ]",
"[ 19/8 → 5/2 | note:a s:sawtooth delay:0.8 delaytime:0.5 ]",
"[ 5/2 → 21/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 ]",
"[ 21/8 → 11/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 ]",
"[ 11/4 → 23/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 ]",
"[ 23/8 → 3/1 | note:a s:sawtooth delay:0.8 delaytime:0.5 ]",
"[ 3/1 → 25/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 ]",
"[ 25/8 → 13/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 ]",
"[ 13/4 → 27/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 ]",
"[ 27/8 → 7/2 | note:a s:sawtooth delay:0.8 delaytime:0.5 ]",
"[ 7/2 → 29/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 ]",
"[ 29/8 → 15/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 ]",
"[ 15/4 → 31/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 ]",
"[ 31/8 → 4/1 | note:a s:sawtooth delay:0.8 delaytime:0.5 ]",
]
`;
@@ -9,9 +9,7 @@ import {
userPattern,
} from '../../../user_pattern_utils.mjs';
import { useMemo, useRef } from 'react';
import { getMetadata } from '../../../metadata_parser.js';
import { useExamplePatterns } from '../../useExamplePatterns.jsx';
import { parseJSON, isUdels } from '../../util.mjs';
import { useSettings } from '../../../settings.mjs';
import { ActionButton } from '../button/action-button.jsx';
import { Pagination } from '../pagination/Pagination.jsx';
@@ -20,8 +18,13 @@ import { useDebounce } from '../usedebounce.jsx';
import cx from '@src/cx.mjs';
import { Textbox } from '@src/repl/components/panel/SettingsTab.jsx';
const PATTERN_SORT = {
"NEWEST": "most recent",
"A-Z": "A-Z",
}
export function PatternLabel({ pattern } /* : { pattern: Tables<'code'> } */) {
const meta = useMemo(() => getMetadata(pattern.code), [pattern]);
const meta = pattern.meta
let title = meta.title;
if (title == null) {
@@ -58,6 +61,10 @@ function PatternButtons({ patterns, activePattern, onClick, started }) {
return (
<div className="p-2">
{Object.values(patterns)
.sort((a, b) => {
return (b.meta.title ?? "").localeCompare(a.meta.title ?? "")
})
.reverse()
.map((pattern) => {
const id = pattern.id;
@@ -93,7 +100,7 @@ export function PatternsTab({ context }) {
}
return Object.fromEntries(
Object.entries(userPatterns).filter(([_key, pattern]) => {
const meta = getMetadata(pattern.code);
const meta = pattern.meta;
// Search for specific meta keys
const searchLowercaseTrimmed = search.trim().toLowerCase();
@@ -5,9 +5,8 @@ import { Textbox } from '@src/repl/components/panel/SettingsTab';
import { settingsMap, useSettings } from '@src/settings.mjs';
const isValid = ({ name, description, tags = [] }) => {
const isSupradoughOnly = tags.includes('supradough') && !tags.includes('superdough');
const isSuperdirtOnly = tags.includes('superdirt') && !tags.includes('superdough');
return name && !name.startsWith('_') && !!description && !isSupradoughOnly && !isSuperdirtOnly;
return name && !name.startsWith('_') && !!description && !isSuperdirtOnly;
};
const availableFunctions = (() => {
@@ -38,7 +37,7 @@ const availableFunctions = (() => {
})();
const tagCounts = {};
const ignoredTags = ['supradough', 'superdirt'];
const ignoredTags = ['superdirt'];
// const tagOptions = { all: `all (${availableFunctions.length})` };
const tagOptions = { all: `all` };
for (const doc of availableFunctions) {
+13 -12
View File
@@ -1,4 +1,4 @@
import { registerSampleSource } from '@strudel/webaudio';
import { extractMidiNoteFromString, registerSampleSource } from '@strudel/webaudio';
import { isAudioFile } from './files.mjs';
import { logger } from '@strudel/core';
@@ -47,7 +47,7 @@ export function registerSamplesFromDB(config = userSamplesDBConfig, onComplete =
Promise.all(
[...soundFiles]
.sort((a, b) => a.title.localeCompare(b.title, undefined, { numeric: true, sensitivity: 'base' }))
.map((soundFile, i) => {
.map((soundFile) => {
const title = soundFile.title;
if (!isAudioFile(title)) {
return;
@@ -58,25 +58,26 @@ export function registerSamplesFromDB(config = userSamplesDBConfig, onComplete =
splitRelativePath[splitRelativePath.length - 2] ?? soundFile.id.split(/\W+/)[0] ?? 'user';
const blob = soundFile.blob;
return blobToDataUrl(blob).then((soundPath) => {
const titlePathMap = sounds.get(parentDirectory) ?? new Map();
return blobToDataUrl(blob).then((path) => {
const sampleInfoMap = sounds.get(parentDirectory) ?? new Map();
const midi = extractMidiNoteFromString(title);
/** @type {import('@strudel/webaudio').SampleMetaData} */
const samplemetadata = { url: path, midi };
sampleInfoMap.set(title, samplemetadata);
titlePathMap.set(title, soundPath);
sounds.set(parentDirectory, titlePathMap);
sounds.set(parentDirectory, sampleInfoMap);
return;
});
}),
)
.then(() => {
sounds.forEach((titlePathMap, key) => {
const value = Array.from(titlePathMap.keys())
sounds.forEach((sampleInfoMap, key) => {
const bank = Array.from(sampleInfoMap.keys())
.sort((a, b) => {
return a.localeCompare(b);
})
.map((title) => titlePathMap.get(title));
registerSampleSource(key, value, { prebake: false });
.map((title) => sampleInfoMap.get(title));
registerSampleSource(key, bank, { prebake: false });
});
logger('imported sounds registered!', 'success');
+6
View File
@@ -3,6 +3,7 @@ import { useStore } from '@nanostores/react';
import { register } from '@strudel/core';
import { isUdels } from './repl/util.mjs';
import { computed } from 'nanostores';
import { getMetadata } from './metadata_parser';
export const audioEngineTargets = {
webaudio: 'webaudio',
@@ -18,6 +19,10 @@ export const soundFilterType = {
ALL: 'all',
};
export const PATTERN_SORT = {
"NEWEST": "most recent",
"A-Z": "A-Z",
}
const initialPrebakeScript = `// Prebake script
//
// This is code that is loaded before your pattern is run.
@@ -85,6 +90,7 @@ export const $settings = computed(settingsMap, (state) => {
Object.keys(userPatterns).forEach((key) => {
const data = userPatterns[key];
data.id = data.id ?? key;
data.meta = getMetadata(data.code)
userPatterns[key] = data;
});
return {