mirror of
https://codeberg.org/uzu/strudel
synced 2026-10-15 20:36:29 -04:00
Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5bc3b5e9a7 | |||
| 173a1654c8 | |||
| dedd687a1f | |||
| 6abb9f85a8 | |||
| 4f3ba01afc | |||
| f440d7869c | |||
| 2d7f918876 | |||
| d385ceba39 | |||
| 9258857e36 | |||
| ad7e301394 | |||
| 7acb9a3ac7 | |||
| 9665b233a1 | |||
| 6c67851909 | |||
| 109ea3093b | |||
| 8d991ad8ae | |||
| 0b69ce21c7 | |||
| 4f05674cb3 | |||
| feab3f5c86 | |||
| 90c05ec38a | |||
| 7e0eed87cb | |||
| 1a1197f67a | |||
| 3c5afb32f3 | |||
| 76cbd23859 | |||
| b436ae789c | |||
| 275731afc7 | |||
| 5cb2214d88 |
@@ -42,7 +42,6 @@ export default [
|
||||
'**/hydra.mjs',
|
||||
'**/jsdoc-synonyms.js',
|
||||
'packages/hs2js/src/hs2js.mjs',
|
||||
'packages/supradough/dough-export.mjs',
|
||||
'**/samples',
|
||||
],
|
||||
},
|
||||
|
||||
@@ -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?.()),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 +0,0 @@
|
||||
pattern.wav
|
||||
@@ -1,3 +0,0 @@
|
||||
# supradough
|
||||
|
||||
platform agnostic synth and sampler intended for live coding. a reimplementation of superdough.
|
||||
@@ -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));
|
||||
});
|
||||
@@ -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
@@ -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;
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
},
|
||||
});
|
||||
@@ -3,6 +3,7 @@
|
||||
"version": "0.2.0",
|
||||
"private": true,
|
||||
"description": "Experimental Tidal Code interpreter",
|
||||
"main": "tidal.mjs",
|
||||
"module": "tidal.mjs",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -35,8 +35,7 @@
|
||||
"dependencies": {
|
||||
"@strudel/core": "workspace:*",
|
||||
"@strudel/draw": "workspace:*",
|
||||
"superdough": "workspace:*",
|
||||
"supradough": "workspace:*"
|
||||
"superdough": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^6.0.11"
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
Generated
+3
-23
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user