Compare commits

..

18 Commits

Author SHA1 Message Date
yaxu 8dbf5ce74c revert 3583091e43
revert Merge pull request 'Fix Cyclist skipping haps during triggered increases in cps' (#2103) from tzwaan/fix-cyclist-cps into main

Reviewed-on: https://codeberg.org/uzu/strudel/pulls/2103
2026-10-16 09:25:24 +02:00
Alex McLean 7f02baf93b Merge pull request 'Add open and close signals, useful for making linear envelopes' (#2148) from openclose into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/2148
2026-10-16 00:52:42 +02:00
Alex McLean 2cf3ffa507 Merge pull request 'Allow randL to be seeded' (#2152) from tyow/localstrudel:randl-seed-fix into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/2152
2026-10-15 23:53:21 +02:00
Alex McLean 3583091e43 Merge pull request 'Fix Cyclist skipping haps during triggered increases in cps' (#2103) from tzwaan/fix-cyclist-cps into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/2103
2026-10-15 23:49:48 +02:00
tyow 0ecf60f751 fix #2143 2026-10-15 17:34:22 -04:00
froos 73c57f0f9d Merge pull request 'fix(website): add webaudiofontdata URL to PWA runtime caching' (#2080) from gregocham/strudel:fix/cache-soundfonts-2068 into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/2080
2026-10-15 21:56:12 +02:00
Felix Roos cc8c9f1c26 Merge remote-tracking branch 'origin/main' into fix/cache-soundfonts-2068 2026-10-15 21:53:06 +02:00
Felix Roos bcec4695a0 codeformat 2026-10-15 21:52:57 +02:00
froos e782debc15 Merge pull request 'Bug fix: Remove left-over widget id line from block-based evals' (#2150) from glossing/pianoroll-bug into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/2150
2026-10-15 21:41:44 +02:00
Aria 459768af2a Bug fix: Remove left-over widget id line from block-based evals 2026-10-15 14:01:28 -05:00
alex be3422c5e6 snapshot new examples 2026-10-15 14:10:39 +01:00
alex 382d5df933 Add open and close signals 2026-10-15 13:58:24 +01:00
Tijmen Zwaan 13d404613c Fix after rebase 2026-10-14 21:21:26 +02:00
Tijmen Zwaan f54a1f07d2 Fix the lastEnd not being correct when cps slowed down 2026-10-14 21:21:26 +02:00
Tijmen Zwaan 44e371c99d Fix cps haps sort order being reversed 2026-10-14 21:21:26 +02:00
Tijmen Zwaan c8d0342ecb Slightly more correct sorting of haps for cps changes 2026-10-14 21:21:26 +02:00
Tijmen Zwaan 096e53690f Fix Cyclist skipping haps during triggered increases in cps 2026-10-14 21:21:25 +02:00
karolinaskg e3e3cdfcfc fix(website): add webaudiofontdata URL to PWA runtime caching 2026-06-30 23:39:43 +03:00
12 changed files with 146 additions and 373 deletions
+6 -79
View File
@@ -11,7 +11,6 @@ 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';
@@ -25,12 +24,7 @@ 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 {
deleteAllInlineBeforeCharacter,
InsertCharBeforeChar,
jumpToCharacter,
jumpToNextCharacter,
} from './labelJump.mjs';
import { jumpToCharacter } from './labelJump.mjs';
export { toggleBlockComment, toggleBlockCommentByLine, toggleComment, toggleLineComment } from '@codemirror/commands';
@@ -50,9 +44,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()]));
@@ -80,10 +74,6 @@ 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();
@@ -111,7 +101,6 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo
syntaxHighlighting(defaultHighlightStyle),
EditorView.updateListener.of((v) => onChange(v)),
drawSelection({ cursorBlinkRate: 0 }),
repeatCharKeymap,
Prec.highest(
keymap.of([
{
@@ -133,74 +122,12 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo
},
{
key: 'Alt-w',
run: (view) => jumpToNextCharacter(view, ANON_LABEL, 1),
run: (view) => jumpToCharacter(view, '$', 1),
},
{
key: 'Alt-q',
run: (view) => {
return jumpToNextCharacter(view, ANON_LABEL, -1);
},
run: (view) => jumpToCharacter(view, '$', -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?.()),
+11 -133
View File
@@ -1,54 +1,18 @@
import { EditorSelection } from '@codemirror/state';
import { SearchCursor } from '@codemirror/search';
import { EditorView } from '@codemirror/view';
import { syntaxTree } from '@codemirror/language';
/**
* 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) {
export function jumpToCharacter(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;
const characterPositions = getCharacterPositions(state, character);
while (!cursor.next().done) {
characterPositions.push(cursor.value.to);
}
if (!characterPositions.length) {
return true;
return false;
}
if (direction > 0) {
jumpPos = characterPositions.find((x) => x > pos + 1) ?? characterPositions.at(0); // Loop back around for convenience
@@ -57,97 +21,11 @@ export function jumpToNextCharacter(view, character, direction = 1) {
}
if (jumpPos == null) {
return true;
return false;
}
const selection = EditorSelection.cursor(jumpPos - 1);
dispatch({
selection,
effects: EditorView.scrollIntoView(
selection.head,
{ y: "start" }
)
selection: EditorSelection.cursor(jumpPos - 1),
scrollIntoView: true,
});
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
@@ -1,55 +0,0 @@
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;
}
}
])
];
+24 -1
View File
@@ -420,7 +420,7 @@ export const binaryNL = (n, nBits = 16) => {
* .partials(randL(8))
*/
export const randL = (n) => {
return signal((t) => (nVal) => getRandsAtTime(t, nVal).map(Math.abs)).appLeft(reify(n));
return signal((t, controls) => (nVal) => getRandsAtTime(t, nVal, controls.randSeed).map(Math.abs)).appLeft(reify(n));
};
export const randrun = (n) => {
@@ -1143,3 +1143,26 @@ export const perx = new Pattern(function (state) {
const n = Fraction(1).div(state.span.duration);
return [new Hap(undefined, state.span, Math.log(n) / Math.log(2) + 1)];
});
/**
* A signal that goes from `0` to `1` over one cycle, and then stays at `1` forever.
* Mainly useful in combination with other functions that reset the cycle. See also
* `close`.
* @tags temporal
* @example
* sound("bd sd [- bd] sd".fast(2))
* .lpf(open.slow(4).range(300,3000)) // close envelope over 4 cycles
* .restart("x/4") // restart every 4 cycles
*/
export const open = signal((t) => t.min(1));
/**
* The same as `open`, but goes from `1` to `0` over one cycle, and then
* stays on `0` forever.
* @tags temporal
* @example
* sound("bd sd [- bd] sd".fast(2))
* .lpf(close.slow(4).range(300,3000)) // close envelope over 4 cycles
* .restart("x/4") // restart every 4 cycles
*/
export const close = signal((t) => Fraction(1).sub(t).max(0));
+4 -30
View File
@@ -1,4 +1,4 @@
import { BASE_MIDI_NOTE, getBaseURL, getCommonSampleInfo, noteToFreq, noteToMidi } from './util.mjs';
import { getBaseURL, getCommonSampleInfo } from './util.mjs';
import { registerSound, registerWaveTable, soundMap } from './index.mjs';
import { getAudioContext } from './audioContext.mjs';
import {
@@ -14,13 +14,6 @@ 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) {
@@ -63,9 +56,8 @@ export const getDur = getDuration;
export function getSampleInfo(hapValue, bank) {
const { speed = 1.0 } = hapValue;
const { transpose, url, index, midi, label, baseFrequency } = getCommonSampleInfo(hapValue, bank);
const playbackRate = Math.abs(speed) * Math.pow(2, transpose / 12);
const { transpose, url, index, midi, label } = getCommonSampleInfo(hapValue, bank);
let playbackRate = Math.abs(speed) * Math.pow(2, transpose / 12);
return { transpose, url, index, midi, label, playbackRate };
}
@@ -188,12 +180,7 @@ export const processSampleMap = (sampleMap, fn, baseUrl = sampleMap._base || '')
if (baseUrl.startsWith('github:')) {
baseUrl = githubPath(baseUrl, '');
}
/**
*
* @param {string} v
* @returns {SampleMetaData}
*/
const fullUrl = (v) => ({ url: baseUrl + v, midi: extractMidiNoteFromString(v) });
const fullUrl = (v) => baseUrl + v;
if (Array.isArray(value)) {
//return [key, value.map(replaceUrl)];
value = value.map(fullUrl);
@@ -404,16 +391,3 @@ 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);
}
+10 -42
View File
@@ -19,12 +19,7 @@ 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) {
@@ -34,11 +29,6 @@ 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;
};
@@ -48,17 +38,7 @@ export const freqToMidi = (freq) => {
return (12 * Math.log(freq / 440)) / Math.LN2 + 69;
};
/**
*
* @param {string} note
* @param {number} defaultOctave
* @returns {number}
*/
export const noteToFreq = (note, defaultOctave = 3) => {
return midiToFreq(noteToMidi(note, defaultOctave));
};
function __valueToMidi(value) {
export const valueToMidi = (value, fallbackValue) => {
if (typeof value !== 'object') {
throw new Error('valueToMidi: expected object value');
}
@@ -72,15 +52,10 @@ function __valueToMidi(value) {
if (typeof note === 'number') {
return note;
}
return;
}
export const valueToMidi = (value, fallbackValue) => {
const parsedValue = __valueToMidi(value) ?? fallbackValue;
if (parsedValue == null) {
if (!fallbackValue) {
throw new Error('valueToMidi: expected freq or note to be set');
}
return parsedValue;
return fallbackValue;
};
export function nanFallback(value, fallback = 0, silent) {
@@ -109,18 +84,15 @@ 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;
const maybeMidiNote = __valueToMidi(hapValue);
const midi = maybeMidiNote ?? BASE_MIDI_NOTE;
let transpose = midi - BASE_MIDI_NOTE; // C3 is middle C;
let midi = valueToMidi(hapValue, 36);
let transpose = midi - 36; // C3 is middle C;
let url;
let index = 0;
let samplemeta;
if (Array.isArray(bank)) {
index = getSoundIndex(n, bank.length);
samplemeta = bank[index];
url = bank[index];
} else {
const midiDiff = (noteA) => noteToMidi(noteA) - midi;
// object format will expect keys as notes
@@ -132,14 +104,10 @@ export function getCommonSampleInfo(hapValue, bank) {
);
transpose = -midiDiff(closest); // semitones to repitch
index = getSoundIndex(n, bank[closest].length);
samplemeta = bank[closest][index];
url = bank[closest][index];
}
const label = `${s}:${index}`;
if (maybeMidiNote != null) {
transpose = transpose + (BASE_MIDI_NOTE - samplemeta.midi);
}
return { transpose, index, midi, label, url: samplemeta.url };
return { transpose, url, index, midi, label };
}
/** Selects entries from `source` and renames them via `map`
-3
View File
@@ -94,9 +94,6 @@ export function getWidgetID(widgetConfig) {
function widgetWithLocation(node, widgetConfig) {
const id = getWidgetID(widgetConfig);
// Store the unique ID back into the config so it's available for widget management
// This is critical for block-based evaluation to match existing widgets with new ones
widgetConfig.id = id;
// add loc as identifier to first argument
// the sliderWithID function is assumed to be sliderWithID(id, value, min?, max?)
node.arguments.unshift({
+74
View File
@@ -2440,6 +2440,43 @@ exports[`runs examples > example "clip" example index 0 1`] = `
]
`;
exports[`runs examples > example "close" example index 0 1`] = `
[
"[ 0/1 → 1/8 | s:bd cutoff:3000 ]",
"[ 1/8 → 1/4 | s:sd cutoff:2915.625 ]",
"[ 5/16 → 3/8 | s:bd cutoff:2789.0625 ]",
"[ 3/8 → 1/2 | s:sd cutoff:2746.875 ]",
"[ 1/2 → 5/8 | s:bd cutoff:2662.5 ]",
"[ 5/8 → 3/4 | s:sd cutoff:2578.125 ]",
"[ 13/16 → 7/8 | s:bd cutoff:2451.5625 ]",
"[ 7/8 → 1/1 | s:sd cutoff:2409.375 ]",
"[ 1/1 → 9/8 | s:bd cutoff:2325 ]",
"[ 9/8 → 5/4 | s:sd cutoff:2240.625 ]",
"[ 21/16 → 11/8 | s:bd cutoff:2114.0625 ]",
"[ 11/8 → 3/2 | s:sd cutoff:2071.875 ]",
"[ 3/2 → 13/8 | s:bd cutoff:1987.5 ]",
"[ 13/8 → 7/4 | s:sd cutoff:1903.125 ]",
"[ 29/16 → 15/8 | s:bd cutoff:1776.5625 ]",
"[ 15/8 → 2/1 | s:sd cutoff:1734.375 ]",
"[ 2/1 → 17/8 | s:bd cutoff:1650 ]",
"[ 17/8 → 9/4 | s:sd cutoff:1565.625 ]",
"[ 37/16 → 19/8 | s:bd cutoff:1439.0625 ]",
"[ 19/8 → 5/2 | s:sd cutoff:1396.875 ]",
"[ 5/2 → 21/8 | s:bd cutoff:1312.5 ]",
"[ 21/8 → 11/4 | s:sd cutoff:1228.125 ]",
"[ 45/16 → 23/8 | s:bd cutoff:1101.5625 ]",
"[ 23/8 → 3/1 | s:sd cutoff:1059.375 ]",
"[ 3/1 → 25/8 | s:bd cutoff:975 ]",
"[ 25/8 → 13/4 | s:sd cutoff:890.625 ]",
"[ 53/16 → 27/8 | s:bd cutoff:764.0625 ]",
"[ 27/8 → 7/2 | s:sd cutoff:721.875 ]",
"[ 7/2 → 29/8 | s:bd cutoff:637.5 ]",
"[ 29/8 → 15/4 | s:sd cutoff:553.125 ]",
"[ 61/16 → 31/8 | s:bd cutoff:426.5625 ]",
"[ 31/8 → 4/1 | s:sd cutoff:384.375 ]",
]
`;
exports[`runs examples > example "coarse" example index 0 1`] = `
[
"[ 0/1 → 1/8 | s:hh coarse:1 ]",
@@ -8727,6 +8764,43 @@ exports[`runs examples > example "onTriggerTime" example index 0 1`] = `
]
`;
exports[`runs examples > example "open" example index 0 1`] = `
[
"[ 0/1 → 1/8 | s:bd cutoff:300 ]",
"[ 1/8 → 1/4 | s:sd cutoff:384.375 ]",
"[ 5/16 → 3/8 | s:bd cutoff:510.9375 ]",
"[ 3/8 → 1/2 | s:sd cutoff:553.125 ]",
"[ 1/2 → 5/8 | s:bd cutoff:637.5 ]",
"[ 5/8 → 3/4 | s:sd cutoff:721.875 ]",
"[ 13/16 → 7/8 | s:bd cutoff:848.4375 ]",
"[ 7/8 → 1/1 | s:sd cutoff:890.625 ]",
"[ 1/1 → 9/8 | s:bd cutoff:975 ]",
"[ 9/8 → 5/4 | s:sd cutoff:1059.375 ]",
"[ 21/16 → 11/8 | s:bd cutoff:1185.9375 ]",
"[ 11/8 → 3/2 | s:sd cutoff:1228.125 ]",
"[ 3/2 → 13/8 | s:bd cutoff:1312.5 ]",
"[ 13/8 → 7/4 | s:sd cutoff:1396.875 ]",
"[ 29/16 → 15/8 | s:bd cutoff:1523.4375 ]",
"[ 15/8 → 2/1 | s:sd cutoff:1565.625 ]",
"[ 2/1 → 17/8 | s:bd cutoff:1650 ]",
"[ 17/8 → 9/4 | s:sd cutoff:1734.375 ]",
"[ 37/16 → 19/8 | s:bd cutoff:1860.9375 ]",
"[ 19/8 → 5/2 | s:sd cutoff:1903.125 ]",
"[ 5/2 → 21/8 | s:bd cutoff:1987.5 ]",
"[ 21/8 → 11/4 | s:sd cutoff:2071.875 ]",
"[ 45/16 → 23/8 | s:bd cutoff:2198.4375 ]",
"[ 23/8 → 3/1 | s:sd cutoff:2240.625 ]",
"[ 3/1 → 25/8 | s:bd cutoff:2325 ]",
"[ 25/8 → 13/4 | s:sd cutoff:2409.375 ]",
"[ 53/16 → 27/8 | s:bd cutoff:2535.9375 ]",
"[ 27/8 → 7/2 | s:sd cutoff:2578.125 ]",
"[ 7/2 → 29/8 | s:bd cutoff:2662.5 ]",
"[ 29/8 → 15/4 | s:sd cutoff:2746.875 ]",
"[ 61/16 → 31/8 | s:bd cutoff:2873.4375 ]",
"[ 31/8 → 4/1 | s:sd cutoff:2915.625 ]",
]
`;
exports[`runs examples > example "orbit" example index 0 1`] = `
[
"[ 0/1 → 1/6 | s:hh delay:0.5 delaytime:0.25 orbit:1 ]",
+1
View File
@@ -80,6 +80,7 @@ export default defineConfig({
/^https:\/\/freesound\.org\/.*/i,
/^https:\/\/cdn\.freesound\.org\/.*/i,
/^https:\/\/shabda\.ndre\.gr\/.*/i,
/^https:\/\/felixroos\.github\.io\/webaudiofontdata\/.*/i,
].some((regex) => regex.test(url)),
handler: 'CacheFirst',
options: {
@@ -9,7 +9,9 @@ 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';
@@ -18,13 +20,8 @@ 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 = pattern.meta
const meta = useMemo(() => getMetadata(pattern.code), [pattern]);
let title = meta.title;
if (title == null) {
@@ -61,10 +58,6 @@ 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;
@@ -100,7 +93,7 @@ export function PatternsTab({ context }) {
}
return Object.fromEntries(
Object.entries(userPatterns).filter(([_key, pattern]) => {
const meta = pattern.meta;
const meta = getMetadata(pattern.code);
// Search for specific meta keys
const searchLowercaseTrimmed = search.trim().toLowerCase();
+12 -13
View File
@@ -1,4 +1,4 @@
import { extractMidiNoteFromString, registerSampleSource } from '@strudel/webaudio';
import { 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) => {
.map((soundFile, i) => {
const title = soundFile.title;
if (!isAudioFile(title)) {
return;
@@ -58,26 +58,25 @@ export function registerSamplesFromDB(config = userSamplesDBConfig, onComplete =
splitRelativePath[splitRelativePath.length - 2] ?? soundFile.id.split(/\W+/)[0] ?? 'user';
const blob = soundFile.blob;
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);
return blobToDataUrl(blob).then((soundPath) => {
const titlePathMap = sounds.get(parentDirectory) ?? new Map();
sounds.set(parentDirectory, sampleInfoMap);
titlePathMap.set(title, soundPath);
sounds.set(parentDirectory, titlePathMap);
return;
});
}),
)
.then(() => {
sounds.forEach((sampleInfoMap, key) => {
const bank = Array.from(sampleInfoMap.keys())
sounds.forEach((titlePathMap, key) => {
const value = Array.from(titlePathMap.keys())
.sort((a, b) => {
return a.localeCompare(b);
})
.map((title) => sampleInfoMap.get(title));
registerSampleSource(key, bank, { prebake: false });
.map((title) => titlePathMap.get(title));
registerSampleSource(key, value, { prebake: false });
});
logger('imported sounds registered!', 'success');
-6
View File
@@ -3,7 +3,6 @@ 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',
@@ -19,10 +18,6 @@ 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.
@@ -90,7 +85,6 @@ 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 {