mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-13 06:19:33 -04:00
Merge branch 'main' into feature/592-sound-alias
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
name: Strudel tests
|
||||
|
||||
on: [push]
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'pnpm'
|
||||
cache: "pnpm"
|
||||
- run: pnpm install
|
||||
- run: pnpm run format-check
|
||||
- run: pnpm run lint
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
FROM node:24
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN npm install pnpm --global
|
||||
|
||||
COPY pnpm-workspace.yaml ./
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
COPY packages/ ./packages/
|
||||
COPY examples/ ./examples/
|
||||
RUN mkdir -p website/public
|
||||
COPY website/package.json ./website/
|
||||
|
||||
RUN pnpm install
|
||||
|
||||
|
||||
COPY . .
|
||||
|
||||
EXPOSE 4321
|
||||
|
||||
CMD ["pnpm", "dev"]
|
||||
@@ -42,6 +42,7 @@ export default [
|
||||
'**/hydra.mjs',
|
||||
'**/jsdoc-synonyms.js',
|
||||
'packages/hs2js/src/hs2js.mjs',
|
||||
'packages/supradough/dough-export.mjs',
|
||||
'**/samples',
|
||||
],
|
||||
},
|
||||
|
||||
@@ -54,13 +54,16 @@ const buildExamples = (examples) =>
|
||||
`
|
||||
: '';
|
||||
|
||||
export const Autocomplete = ({ doc, label }) =>
|
||||
export const Autocomplete = (doc) =>
|
||||
h`
|
||||
<div class="autocomplete-info-tooltip">
|
||||
<h3 class="autocomplete-info-function-name">${label || getDocLabel(doc)}</h3>
|
||||
${doc.description ? `<p class="autocomplete-info-function-description">${doc.description}</p>` : ''}
|
||||
${buildParamsList(doc.params)}
|
||||
${buildExamples(doc.examples)}
|
||||
<div class="autocomplete-info-container">
|
||||
<div class="autocomplete-info-tooltip">
|
||||
<h3 class="autocomplete-info-function-name">${getDocLabel(doc)}</h3>
|
||||
${doc.synonyms_text ? `<div class="autocomplete-info-function-synonyms">Synonyms: ${doc.synonyms_text}</div>` : ''}
|
||||
${doc.description ? `<div class="autocomplete-info-function-description">${doc.description}</div>` : ''}
|
||||
${buildParamsList(doc.params)}
|
||||
${buildExamples(doc.examples)}
|
||||
</div>
|
||||
</div>
|
||||
`[0];
|
||||
|
||||
@@ -72,15 +75,43 @@ const isValidDoc = (doc) => {
|
||||
const hasExcludedTags = (doc) =>
|
||||
['superdirtOnly', 'noAutocomplete'].some((tag) => doc.tags?.find((t) => t.originalTitle === tag));
|
||||
|
||||
const jsdocCompletions = jsdoc.docs
|
||||
.filter((doc) => isValidDoc(doc) && !hasExcludedTags(doc))
|
||||
// https://codemirror.net/docs/ref/#autocomplete.Completion
|
||||
.map((doc) => ({
|
||||
label: getDocLabel(doc),
|
||||
// detail: 'xxx', // An optional short piece of information to show (with a different style) after the label.
|
||||
info: () => Autocomplete({ doc }),
|
||||
type: 'function', // https://codemirror.net/docs/ref/#autocomplete.Completion.type
|
||||
}));
|
||||
export const getSynonymDoc = (doc, synonym) => {
|
||||
const synonyms = doc.synonyms || [];
|
||||
const docLabel = getDocLabel(doc);
|
||||
// Swap `doc.name` in for `s` in the list of synonyms
|
||||
const synonymsWithDoc = [docLabel, ...synonyms].filter((x) => x && x !== synonym);
|
||||
return {
|
||||
...doc,
|
||||
name: synonym,
|
||||
longname: synonym,
|
||||
synonyms: synonymsWithDoc,
|
||||
synonyms_text: synonymsWithDoc.join(', '),
|
||||
};
|
||||
};
|
||||
|
||||
const jsdocCompletions = (() => {
|
||||
const seen = new Set(); // avoid repetition
|
||||
const completions = [];
|
||||
for (const doc of jsdoc.docs) {
|
||||
if (!isValidDoc(doc) || hasExcludedTags(doc)) continue;
|
||||
const docLabel = getDocLabel(doc);
|
||||
// Remove duplicates
|
||||
const synonyms = doc.synonyms || [];
|
||||
let labels = [docLabel, ...synonyms];
|
||||
for (const label of labels) {
|
||||
// https://codemirror.net/docs/ref/#autocomplete.Completion
|
||||
if (label && !seen.has(label)) {
|
||||
seen.add(label);
|
||||
completions.push({
|
||||
label,
|
||||
info: () => Autocomplete(getSynonymDoc(doc, label)),
|
||||
type: 'function', // https://codemirror.net/docs/ref/#autocomplete.Completion.type
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return completions;
|
||||
})();
|
||||
|
||||
export const strudelAutocomplete = (context) => {
|
||||
const word = context.matchBefore(/\w*/);
|
||||
@@ -98,4 +129,4 @@ export const strudelAutocomplete = (context) => {
|
||||
};
|
||||
|
||||
export const isAutoCompletionEnabled = (enabled) =>
|
||||
enabled ? [autocompletion({ override: [strudelAutocomplete] })] : [];
|
||||
enabled ? [autocompletion({ override: [strudelAutocomplete], closeOnBlur: false })] : [];
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import {
|
||||
keymap,
|
||||
highlightSpecialChars,
|
||||
drawSelection,
|
||||
highlightActiveLine,
|
||||
dropCursor,
|
||||
rectangularSelection,
|
||||
crosshairCursor,
|
||||
lineNumbers,
|
||||
highlightActiveLineGutter,
|
||||
} from '@codemirror/view';
|
||||
import {
|
||||
defaultHighlightStyle,
|
||||
syntaxHighlighting,
|
||||
bracketMatching,
|
||||
foldGutter,
|
||||
foldKeymap,
|
||||
} from '@codemirror/language';
|
||||
import { defaultKeymap, history, historyKeymap } from '@codemirror/commands';
|
||||
import { searchKeymap, highlightSelectionMatches } from '@codemirror/search';
|
||||
import { completionKeymap, closeBracketsKeymap } from '@codemirror/autocomplete';
|
||||
|
||||
// Taken + slightly modified from https://github.com/codemirror/basic-setup/blob/main/src/codemirror.ts
|
||||
|
||||
export const basicSetup = (() => [
|
||||
// lineNumbers(),
|
||||
// highlightActiveLineGutter(),
|
||||
highlightSpecialChars(),
|
||||
history(),
|
||||
// foldGutter(),
|
||||
// drawSelection(),
|
||||
dropCursor(),
|
||||
// EditorState.allowMultipleSelections.of(true),
|
||||
// indentOnInput(),
|
||||
// syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
|
||||
// autocompletion(),
|
||||
rectangularSelection(),
|
||||
crosshairCursor(),
|
||||
// highlightActiveLine(),
|
||||
// highlightSelectionMatches(),
|
||||
keymap.of([
|
||||
...closeBracketsKeymap,
|
||||
...defaultKeymap,
|
||||
// ...searchKeymap,
|
||||
...historyKeymap,
|
||||
// ...foldKeymap,
|
||||
// ...completionKeymap,
|
||||
]),
|
||||
])();
|
||||
|
||||
/// A minimal set of extensions to create a functional editor. Only
|
||||
/// includes [the default keymap](#commands.defaultKeymap), [undo
|
||||
/// history](#commands.history), [special character
|
||||
/// highlighting](#view.highlightSpecialChars), [custom selection
|
||||
/// drawing](#view.drawSelection), and [default highlight
|
||||
/// style](#language.defaultHighlightStyle).
|
||||
export const minimalSetup = (() => [
|
||||
highlightSpecialChars(),
|
||||
history(),
|
||||
drawSelection(),
|
||||
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
|
||||
keymap.of([...defaultKeymap, ...historyKeymap]),
|
||||
])();
|
||||
@@ -1,8 +1,8 @@
|
||||
import { closeBrackets } from '@codemirror/autocomplete';
|
||||
export { toggleComment, toggleBlockComment, toggleLineComment, toggleBlockCommentByLine } from '@codemirror/commands';
|
||||
// import { search, highlightSelectionMatches } from '@codemirror/search';
|
||||
import { history, indentWithTab } from '@codemirror/commands';
|
||||
import { javascript } from '@codemirror/lang-javascript';
|
||||
import { indentWithTab } from '@codemirror/commands';
|
||||
import { javascript, javascriptLanguage } from '@codemirror/lang-javascript';
|
||||
import { defaultHighlightStyle, syntaxHighlighting, bracketMatching } from '@codemirror/language';
|
||||
import { Compartment, EditorState, Prec } from '@codemirror/state';
|
||||
import {
|
||||
@@ -24,6 +24,7 @@ import { initTheme, activateTheme, theme } from './themes.mjs';
|
||||
import { sliderPlugin, updateSliderWidgets } from './slider.mjs';
|
||||
import { widgetPlugin, updateWidgets } from './widget.mjs';
|
||||
import { persistentAtom } from '@nanostores/persistent';
|
||||
import { basicSetup } from './basicSetup.mjs';
|
||||
|
||||
const extensions = {
|
||||
isLineWrappingEnabled: (on) => (on ? EditorView.lineWrapping : []),
|
||||
@@ -85,13 +86,17 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo
|
||||
/* search(),
|
||||
highlightSelectionMatches(), */
|
||||
...initialSettings,
|
||||
basicSetup,
|
||||
mondo ? [] : javascript(),
|
||||
javascriptLanguage.data.of({
|
||||
closeBrackets: { brackets: ['(', '[', '{', "'", '"', '<'] },
|
||||
bracketMatching: { brackets: ['(', '[', '{', "'", '"', '<'] },
|
||||
}),
|
||||
sliderPlugin,
|
||||
widgetPlugin,
|
||||
// indentOnInput(), // works without. already brought with javascript extension?
|
||||
// bracketMatching(), // does not do anything
|
||||
syntaxHighlighting(defaultHighlightStyle),
|
||||
history(),
|
||||
EditorView.updateListener.of((v) => onChange(v)),
|
||||
drawSelection({ cursorBlinkRate: 0 }),
|
||||
Prec.highest(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const parser = typeof DOMParser !== 'undefined' ? new DOMParser() : null;
|
||||
export let html = (string) => {
|
||||
return parser?.parseFromString(string, 'text/html').querySelectorAll('*');
|
||||
const template = document.createElement('template');
|
||||
template.innerHTML = string.trim();
|
||||
return template.content.childNodes;
|
||||
};
|
||||
let parseChunk = (chunk) => {
|
||||
if (Array.isArray(chunk)) return chunk.flat().join('');
|
||||
|
||||
@@ -3,8 +3,9 @@ import { keymap, ViewPlugin } from '@codemirror/view';
|
||||
// import { searchKeymap } from '@codemirror/search';
|
||||
import { emacs } from '@replit/codemirror-emacs';
|
||||
import { vim } from '@replit/codemirror-vim';
|
||||
// import { vim } from './vim_test.mjs';
|
||||
import { vscodeKeymap } from '@replit/codemirror-vscode-keymap';
|
||||
import { defaultKeymap, historyKeymap } from '@codemirror/commands';
|
||||
import { defaultKeymap } from '@codemirror/commands';
|
||||
|
||||
const vscodePlugin = ViewPlugin.fromClass(
|
||||
class {
|
||||
@@ -27,5 +28,5 @@ const keymaps = {
|
||||
|
||||
export function keybindings(name) {
|
||||
const active = keymaps[name];
|
||||
return [active ? active() : [], keymap.of(historyKeymap)];
|
||||
return [active ? Prec.high(active()) : []];
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/codemirror",
|
||||
"version": "1.2.3",
|
||||
"version": "1.2.5",
|
||||
"description": "Codemirror Extensions for Strudel",
|
||||
"main": "index.mjs",
|
||||
"publishConfig": {
|
||||
@@ -42,7 +42,7 @@
|
||||
"@lezer/highlight": "^1.2.1",
|
||||
"@nanostores/persistent": "^0.10.2",
|
||||
"@replit/codemirror-emacs": "^6.1.0",
|
||||
"@replit/codemirror-vim": "^6.2.1",
|
||||
"@replit/codemirror-vim": "^6.3.0",
|
||||
"@replit/codemirror-vscode-keymap": "^6.0.2",
|
||||
"@strudel/core": "workspace:*",
|
||||
"@strudel/draw": "workspace:*",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { hoverTooltip } from '@codemirror/view';
|
||||
import jsdoc from '../../doc.json';
|
||||
import { Autocomplete } from './autocomplete.mjs';
|
||||
import { Autocomplete, getSynonymDoc } from './autocomplete.mjs';
|
||||
|
||||
const getDocLabel = (doc) => doc.name || doc.longname;
|
||||
|
||||
@@ -52,10 +52,11 @@ export const strudelTooltip = hoverTooltip(
|
||||
let entry = jsdoc.docs.filter((doc) => getDocLabel(doc) === word)[0];
|
||||
if (!entry) {
|
||||
// Try for synonyms
|
||||
entry = jsdoc.docs.filter((doc) => doc.synonyms && doc.synonyms.includes(word))[0];
|
||||
if (!entry) {
|
||||
const doc = jsdoc.docs.filter((doc) => doc.synonyms && doc.synonyms.includes(word))[0];
|
||||
if (!doc) {
|
||||
return null;
|
||||
}
|
||||
entry = getSynonymDoc(doc, word);
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -66,7 +67,7 @@ export const strudelTooltip = hoverTooltip(
|
||||
create(view) {
|
||||
let dom = document.createElement('div');
|
||||
dom.className = 'strudel-tooltip';
|
||||
const ac = Autocomplete({ doc: entry, label: word });
|
||||
const ac = Autocomplete(entry);
|
||||
dom.appendChild(ac);
|
||||
return { dom };
|
||||
},
|
||||
|
||||
@@ -91,6 +91,7 @@ export const { s, sound } = registerControl(['s', 'n', 'gain'], 'sound');
|
||||
* Define a custom webaudio node to use as a sound source.
|
||||
*
|
||||
* @name source
|
||||
* @synonyms src
|
||||
* @param {function} getSource
|
||||
* @synonyms src
|
||||
*
|
||||
@@ -309,6 +310,17 @@ export const { fmvelocity } = registerControl('fmvelocity');
|
||||
*/
|
||||
export const { bank } = registerControl('bank');
|
||||
|
||||
/**
|
||||
* mix control for the chorus effect
|
||||
*
|
||||
* @name chorus
|
||||
* @param {string | Pattern} chorus mix amount between 0 and 1
|
||||
* @example
|
||||
* note("d d a# a").s("sawtooth").chorus(.5)
|
||||
*
|
||||
*/
|
||||
export const { chorus } = registerControl('chorus');
|
||||
|
||||
// analyser node send amount 0 - 1 (used by scope)
|
||||
export const { analyze } = registerControl('analyze');
|
||||
// fftSize of analyser
|
||||
@@ -525,6 +537,7 @@ export const { tremolophase } = registerControl('tremolophase', 'tremphase');
|
||||
* shape of amplitude modulation
|
||||
*
|
||||
* @name tremoloshape
|
||||
* @synonyms tremshape
|
||||
* @param {number | Pattern} shape tri | square | sine | saw | ramp
|
||||
* @example
|
||||
* note("{f g c d}%16").tremsync(4).tremoloshape("<sine tri square>").s("sawtooth")
|
||||
@@ -540,11 +553,13 @@ export const { tremoloshape } = registerControl('tremoloshape', 'tremshape');
|
||||
* note("{f g g c d a a#}%16".sub(17)).s("supersaw").lpenv(8).lpf(150).lpq(.8).ftype('ladder').drive("<.5 4>")
|
||||
*
|
||||
*/
|
||||
export const { drive } = registerControl('drive');
|
||||
|
||||
/**
|
||||
* modulate the amplitude of an orbit to create a "sidechain" like effect
|
||||
*
|
||||
* @name duckorbit
|
||||
* @synonyms duck
|
||||
* @param {number | Pattern} orbit target orbit
|
||||
* @example
|
||||
* $: n(run(16)).scale("c:minor:pentatonic").s("sawtooth").delay(.7).orbit(2)
|
||||
@@ -569,6 +584,7 @@ export const { duckdepth } = registerControl('duckdepth');
|
||||
* the attack time of the duck effect
|
||||
*
|
||||
* @name duckattack
|
||||
* @synonyms duckatt
|
||||
* @param {number | Pattern} time
|
||||
* @example
|
||||
* stack( n(run(8)).scale("c:minor").s("sawtooth").delay(.7).orbit(2), s("bd:4!4").beat("0,4,8,11,14",16).duckorbit(2).duckattack("<0.2 0 0.4>").duckdepth(1))
|
||||
@@ -576,8 +592,6 @@ export const { duckdepth } = registerControl('duckdepth');
|
||||
*/
|
||||
export const { duckattack } = registerControl('duckattack', 'duckatt');
|
||||
|
||||
export const { drive } = registerControl('drive');
|
||||
|
||||
/**
|
||||
* Create byte beats with custom expressions
|
||||
*
|
||||
@@ -700,7 +714,7 @@ export const { phasercenter, phc } = registerControl('phasercenter', 'phc');
|
||||
* The amount the signal is affected by the phaser effect. Defaults to 0.75
|
||||
*
|
||||
* @name phaserdepth
|
||||
* @synonyms phd
|
||||
* @synonyms phd, phasdp
|
||||
* @param {number | Pattern} depth number between 0 and 1
|
||||
* @example
|
||||
* n(run(8)).scale("D:pentatonic").s("sawtooth").release(0.5)
|
||||
@@ -1097,14 +1111,27 @@ export const { delay } = registerControl(['delay', 'delaytime', 'delayfeedback']
|
||||
*
|
||||
*/
|
||||
export const { delayfeedback, delayfb, dfb } = registerControl('delayfeedback', 'delayfb', 'dfb');
|
||||
|
||||
/**
|
||||
* Sets the level of the signal that is fed back into the delay.
|
||||
* Caution: Values >= 1 will result in a signal that gets louder and louder! Don't do it
|
||||
*
|
||||
* @name delayfeedback
|
||||
* @param {number | Pattern} feedback between 0 and 1
|
||||
* @synonyms delayfb, dfb
|
||||
* @example
|
||||
* s("bd").delay(.25).delayfeedback("<.25 .5 .75 1>")
|
||||
*
|
||||
*/
|
||||
export const { delayspeed } = registerControl('delayspeed');
|
||||
/**
|
||||
* Sets the time of the delay effect.
|
||||
*
|
||||
* @name delaytime
|
||||
* @param {number | Pattern} seconds between 0 and Infinity
|
||||
* @name delayspeed
|
||||
* @param {number | Pattern} delayspeed controls the pitch of the delay feedback
|
||||
* @synonyms delayt, dt
|
||||
* @example
|
||||
* s("bd bd").delay(.25).delaytime("<.125 .25 .5 1>")
|
||||
* note("d d a# a".fast(2)).s("sawtooth").delay(.8).delaytime(1/2).delayspeed("<2 .5 -1 -2>")
|
||||
*
|
||||
*/
|
||||
export const { delaytime, delayt, dt } = registerControl('delaytime', 'delayt', 'dt');
|
||||
@@ -1182,6 +1209,7 @@ export const { dry } = registerControl('dry');
|
||||
* Used when using `begin`/`end` or `chop`/`striate` and friends, to change the fade out time of the 'grain' envelope.
|
||||
*
|
||||
* @name fadeTime
|
||||
* @synonyms fadeOutTime
|
||||
* @param {number | Pattern} time between 0 and 1
|
||||
* @example
|
||||
* s("oh*4").end(.1).fadeTime("<0 .2 .4 .8>").osc()
|
||||
|
||||
@@ -6,7 +6,7 @@ let debounce = 1000,
|
||||
|
||||
export function errorLogger(e, origin = 'cyclist') {
|
||||
//TODO: add some kind of debug flag that enables this while in dev mode
|
||||
console.error(e);
|
||||
// console.error(e);
|
||||
logger(`[${origin}] error: ${e.message}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/core",
|
||||
"version": "1.2.3",
|
||||
"version": "1.2.4",
|
||||
"description": "Port of Tidal Cycles to JavaScript",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -1246,7 +1246,8 @@ export const silence = gap(1);
|
||||
/* Like silence, but with a 'steps' (relative duration) of 0 */
|
||||
export const nothing = gap(0);
|
||||
|
||||
/** A discrete value that repeats once per cycle.
|
||||
/**
|
||||
* A discrete value that repeats once per cycle.
|
||||
*
|
||||
* @returns {Pattern}
|
||||
* @example
|
||||
@@ -1299,7 +1300,8 @@ export function sequenceP(pats) {
|
||||
return result;
|
||||
}
|
||||
|
||||
/** The given items are played at the same time at the same length.
|
||||
/**
|
||||
* The given items are played at the same time at the same length.
|
||||
*
|
||||
* @return {Pattern}
|
||||
* @synonyms polyrhythm, pr
|
||||
@@ -1382,11 +1384,11 @@ export function stackBy(by, ...pats) {
|
||||
.setSteps(steps);
|
||||
}
|
||||
|
||||
/** Concatenation: combines a list of patterns, switching between them successively, one per cycle:
|
||||
*
|
||||
* synonyms: `cat`
|
||||
/**
|
||||
* Concatenation: combines a list of patterns, switching between them successively, one per cycle.
|
||||
*
|
||||
* @return {Pattern}
|
||||
* @synonyms cat
|
||||
* @example
|
||||
* slowcat("e5", "b4", ["d5", "c5"])
|
||||
*
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/csound",
|
||||
"version": "1.2.4",
|
||||
"version": "1.2.5",
|
||||
"description": "csound bindings for strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/draw",
|
||||
"version": "1.2.3",
|
||||
"version": "1.2.4",
|
||||
"description": "Helpers for drawing with Strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/gamepad",
|
||||
"version": "1.2.3",
|
||||
"version": "1.2.4",
|
||||
"description": "Gamepad Inputs for strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/hydra",
|
||||
"version": "1.2.3",
|
||||
"version": "1.2.4",
|
||||
"description": "Hydra integration for strudel",
|
||||
"main": "hydra.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/midi",
|
||||
"version": "1.2.4",
|
||||
"version": "1.2.5",
|
||||
"description": "Midi API for strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/mini",
|
||||
"version": "1.2.3",
|
||||
"version": "1.2.4",
|
||||
"description": "Mini notation for strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -108,7 +108,7 @@ export function mondo(code, offset = 0) {
|
||||
return pat.markcss('color: var(--caret,--foreground);text-decoration:underline');
|
||||
}
|
||||
|
||||
let getLocations = (code, offset) => runner.parser.get_locations(code, offset);
|
||||
export let getLocations = (code, offset) => runner.parser.get_locations(code, offset);
|
||||
|
||||
export const mondi = (str, offset) => {
|
||||
const code = `[${str}]`;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/mondo",
|
||||
"version": "1.1.1",
|
||||
"version": "1.1.4",
|
||||
"description": "mondo notation for strudel",
|
||||
"main": "mondough.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { defineConfig } from 'vite';
|
||||
//import { dependencies } from './package.json';
|
||||
import { dependencies } from './package.json';
|
||||
import { resolve } from 'path';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
@@ -12,7 +12,7 @@ export default defineConfig({
|
||||
fileName: (ext) => ({ es: 'mondough.mjs' })[ext],
|
||||
},
|
||||
rollupOptions: {
|
||||
// external: [...Object.keys(dependencies)],
|
||||
external: [...Object.keys(dependencies)],
|
||||
},
|
||||
target: 'esnext',
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/motion",
|
||||
"version": "1.2.3",
|
||||
"version": "1.2.4",
|
||||
"description": "DeviceMotion API for strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/mqtt",
|
||||
"version": "1.2.3",
|
||||
"version": "1.2.4",
|
||||
"description": "MQTT API for strudel",
|
||||
"main": "mqtt.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/osc",
|
||||
"version": "1.2.3",
|
||||
"version": "1.2.4",
|
||||
"description": "OSC messaging for strudel",
|
||||
"main": "osc.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { oscTriggerTauri } from '../desktopbridge/oscbridge.mjs';
|
||||
import { isTauri } from '../desktopbridge/utils.mjs';
|
||||
/* import { oscTriggerTauri } from '../desktopbridge/oscbridge.mjs';
|
||||
import { isTauri } from '../desktopbridge/utils.mjs'; */
|
||||
import { oscTrigger } from './osc.mjs';
|
||||
|
||||
const trigger = isTauri() ? oscTriggerTauri : oscTrigger;
|
||||
const trigger = /* isTauri() ? oscTriggerTauri : */ oscTrigger;
|
||||
|
||||
export const superdirtOutput = (hap, deadline, hapDuration, cps, targetTime) => {
|
||||
const currentTime = performance.now() / 1000;
|
||||
return trigger(null, hap, currentTime, cps, targetTime);
|
||||
return trigger(hap, currentTime, cps, targetTime);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/repl",
|
||||
"version": "1.2.4",
|
||||
"version": "1.2.6",
|
||||
"description": "Strudel REPL as a Web Component",
|
||||
"module": "index.mjs",
|
||||
"publishConfig": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/sampler",
|
||||
"version": "0.2.2",
|
||||
"version": "0.2.3",
|
||||
"description": "",
|
||||
"keywords": [
|
||||
"tidalcycles",
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import cowsay from 'cowsay';
|
||||
import { createReadStream, existsSync } from 'fs';
|
||||
import { createReadStream, existsSync, writeFileSync } from 'fs';
|
||||
import { readdir } from 'fs/promises';
|
||||
import http from 'http';
|
||||
import { join, sep } from 'path';
|
||||
import { join, resolve, sep } from 'path';
|
||||
import readline from 'readline';
|
||||
import os from 'os';
|
||||
|
||||
// eslint-disable-next-line
|
||||
const LOG = !!process.env.LOG || false;
|
||||
const VALID_AUDIO_EXTENSIONS = ['wav', 'mp3', 'ogg'];
|
||||
|
||||
const isAudioFile = (f) => {
|
||||
const ext = f.split('.').slice(-1)[0].toLowerCase();
|
||||
return VALID_AUDIO_EXTENSIONS.includes(ext);
|
||||
};
|
||||
|
||||
async function getFilesInDirectory(directory) {
|
||||
let files = [];
|
||||
@@ -21,32 +27,32 @@ async function getFilesInDirectory(directory) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const subFiles = (await getFilesInDirectory(fullPath)).filter((f) =>
|
||||
['wav', 'mp3', 'ogg'].includes(f.split('.').slice(-1)[0].toLowerCase()),
|
||||
);
|
||||
const subFiles = (await getFilesInDirectory(fullPath)).filter(isAudioFile);
|
||||
files = files.concat(subFiles);
|
||||
LOG && console.log(`${dirent.name} (${subFiles.length})`);
|
||||
} catch (err) {
|
||||
LOG && console.warn(`skipped due to error: ${fullPath}`);
|
||||
}
|
||||
} else {
|
||||
files.push(fullPath);
|
||||
isAudioFile(fullPath) && files.push(fullPath);
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
async function getBanks(directory) {
|
||||
async function getBanks(directory, flat = false) {
|
||||
let files = await getFilesInDirectory(directory);
|
||||
let banks = {};
|
||||
directory = directory.split(sep).join('/');
|
||||
files = files.map((path) => {
|
||||
path = path.split(sep).join('/');
|
||||
const [bank] = path.split('/').slice(-2);
|
||||
const subDir = path.replace(directory, '');
|
||||
const subDirFlat = subDir.replaceAll('/', '_').slice(1); // remove initial underscore
|
||||
const subDirFlatStem = subDirFlat.replace(/\.[^.]+$/, ''); // remove extension
|
||||
let bank = flat ? subDirFlatStem : path.split('/').slice(-2)[0];
|
||||
banks[bank] = banks[bank] || [];
|
||||
const relativeUrl = path.replace(directory, '');
|
||||
banks[bank].push(relativeUrl);
|
||||
return relativeUrl;
|
||||
banks[bank].push(subDir);
|
||||
return subDir;
|
||||
});
|
||||
banks._base = `http://localhost:5432`;
|
||||
return { banks, files };
|
||||
@@ -54,14 +60,44 @@ async function getBanks(directory) {
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
// eslint-disable-next-line
|
||||
const directory = process.cwd();
|
||||
function getArgValue(flag) {
|
||||
const i = args.indexOf(flag);
|
||||
if (i !== -1) {
|
||||
const nextIsFlag = args[i + 1]?.startsWith('--') ?? true;
|
||||
if (nextIsFlag) return true;
|
||||
return args[i + 1];
|
||||
}
|
||||
}
|
||||
|
||||
function getInput(query) {
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
|
||||
return new Promise((resolve) =>
|
||||
rl.question(query, (response) => {
|
||||
rl.close();
|
||||
resolve(response);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
let directory = getArgValue('--dir') || process.cwd();
|
||||
directory = resolve(directory);
|
||||
if (args.includes('--json')) {
|
||||
const { banks, files } = await getBanks(directory);
|
||||
const { banks } = await getBanks(directory, getArgValue('--flat'));
|
||||
const json = JSON.stringify(banks);
|
||||
console.log(json);
|
||||
process.exit(0);
|
||||
const outFile = resolve(directory, 'strudel.json');
|
||||
if (existsSync(outFile)) {
|
||||
const answer = await getInput(`Warning: File already exists at ${outFile}. Overwrite? (y/N): `);
|
||||
if (answer.toLowerCase() !== 'y') {
|
||||
console.log('Aborted.');
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
writeFileSync(outFile, json, 'utf8');
|
||||
console.log(`Wrote json to ${outFile}`);
|
||||
}
|
||||
|
||||
console.log(
|
||||
@@ -74,7 +110,7 @@ console.log(
|
||||
|
||||
const server = http.createServer(async (req, res) => {
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
const { banks, files } = await getBanks(directory);
|
||||
const { banks, files } = await getBanks(directory, getArgValue('--flat'));
|
||||
if (req.url === '/') {
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
return res.end(JSON.stringify(banks));
|
||||
@@ -82,7 +118,7 @@ const server = http.createServer(async (req, res) => {
|
||||
let subpath = decodeURIComponent(req.url);
|
||||
const filePath = join(directory, subpath.split('/').join(sep));
|
||||
|
||||
//console.log('GET:', filePath);
|
||||
// console.log('GET:', filePath);
|
||||
const isFound = existsSync(filePath);
|
||||
if (!isFound) {
|
||||
res.statusCode = 404;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/serial",
|
||||
"version": "1.2.3",
|
||||
"version": "1.2.4",
|
||||
"description": "Webserial API for strudel",
|
||||
"main": "serial.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/soundfonts",
|
||||
"version": "1.2.4",
|
||||
"version": "1.2.5",
|
||||
"description": "Soundsfont support for strudel",
|
||||
"main": "index.mjs",
|
||||
"publishConfig": {
|
||||
|
||||
@@ -174,7 +174,7 @@ let curves = ['linear', 'exponential'];
|
||||
export function getPitchEnvelope(param, value, t, holdEnd) {
|
||||
// envelope is active when any of these values is set
|
||||
const hasEnvelope = value.pattack ?? value.pdecay ?? value.psustain ?? value.prelease ?? value.penv;
|
||||
if (!hasEnvelope) {
|
||||
if (hasEnvelope === undefined) {
|
||||
return;
|
||||
}
|
||||
const penv = nanFallback(value.penv, 1, true);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "superdough",
|
||||
"version": "1.2.4",
|
||||
"version": "1.2.5",
|
||||
"description": "simple web audio synth and sampler intended for live coding. inspired by superdirt and webdirt.",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -196,6 +196,52 @@ function getSamplesPrefixHandler(url) {
|
||||
return;
|
||||
}
|
||||
|
||||
export async function fetchSampleMap(url) {
|
||||
// check if custom prefix handler
|
||||
const handler = getSamplesPrefixHandler(url);
|
||||
if (handler) {
|
||||
return handler(url);
|
||||
}
|
||||
url = resolveSpecialPaths(url);
|
||||
if (url.startsWith('github:')) {
|
||||
url = githubPath(url, 'strudel.json');
|
||||
}
|
||||
if (url.startsWith('local:')) {
|
||||
url = `http://localhost:5432`;
|
||||
}
|
||||
if (url.startsWith('shabda:')) {
|
||||
let [_, path] = url.split('shabda:');
|
||||
url = `https://shabda.ndre.gr/${path}.json?strudel=1`;
|
||||
}
|
||||
if (url.startsWith('shabda/speech')) {
|
||||
let [_, path] = url.split('shabda/speech');
|
||||
path = path.startsWith('/') ? path.substring(1) : path;
|
||||
let [params, words] = path.split(':');
|
||||
let gender = 'f';
|
||||
let language = 'en-GB';
|
||||
if (params) {
|
||||
[language, gender] = params.split('/');
|
||||
}
|
||||
url = `https://shabda.ndre.gr/speech/${words}.json?gender=${gender}&language=${language}&strudel=1'`;
|
||||
}
|
||||
if (typeof fetch !== 'function') {
|
||||
// not a browser
|
||||
return;
|
||||
}
|
||||
const base = url.split('/').slice(0, -1).join('/');
|
||||
if (typeof fetch === 'undefined') {
|
||||
// skip fetch when in node / testing
|
||||
return;
|
||||
}
|
||||
const json = await fetch(url)
|
||||
.then((res) => res.json())
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
throw new Error(`error loading "${url}"`);
|
||||
});
|
||||
return [json, json._base || base];
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a collection of samples to use with `s`
|
||||
* @example
|
||||
@@ -217,49 +263,8 @@ function getSamplesPrefixHandler(url) {
|
||||
|
||||
export const samples = async (sampleMap, baseUrl = sampleMap._base || '', options = {}) => {
|
||||
if (typeof sampleMap === 'string') {
|
||||
// check if custom prefix handler
|
||||
const handler = getSamplesPrefixHandler(sampleMap);
|
||||
if (handler) {
|
||||
return handler(sampleMap);
|
||||
}
|
||||
sampleMap = resolveSpecialPaths(sampleMap);
|
||||
if (sampleMap.startsWith('github:')) {
|
||||
sampleMap = githubPath(sampleMap, 'strudel.json');
|
||||
}
|
||||
if (sampleMap.startsWith('local:')) {
|
||||
sampleMap = `http://localhost:5432`;
|
||||
}
|
||||
if (sampleMap.startsWith('shabda:')) {
|
||||
let [_, path] = sampleMap.split('shabda:');
|
||||
sampleMap = `https://shabda.ndre.gr/${path}.json?strudel=1`;
|
||||
}
|
||||
if (sampleMap.startsWith('shabda/speech')) {
|
||||
let [_, path] = sampleMap.split('shabda/speech');
|
||||
path = path.startsWith('/') ? path.substring(1) : path;
|
||||
let [params, words] = path.split(':');
|
||||
let gender = 'f';
|
||||
let language = 'en-GB';
|
||||
if (params) {
|
||||
[language, gender] = params.split('/');
|
||||
}
|
||||
sampleMap = `https://shabda.ndre.gr/speech/${words}.json?gender=${gender}&language=${language}&strudel=1'`;
|
||||
}
|
||||
if (typeof fetch !== 'function') {
|
||||
// not a browser
|
||||
return;
|
||||
}
|
||||
const base = sampleMap.split('/').slice(0, -1).join('/');
|
||||
if (typeof fetch === 'undefined') {
|
||||
// skip fetch when in node / testing
|
||||
return;
|
||||
}
|
||||
return fetch(sampleMap)
|
||||
.then((res) => res.json())
|
||||
.then((json) => samples(json, baseUrl || json._base || base, options))
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
throw new Error(`error loading "${sampleMap}"`);
|
||||
});
|
||||
const [json, base] = await fetchSampleMap(sampleMap);
|
||||
return samples(json, baseUrl || base, options);
|
||||
}
|
||||
const { prebake, tag } = options;
|
||||
processSampleMap(
|
||||
|
||||
@@ -209,7 +209,7 @@ export const resetLoadedSounds = () => soundMap.set({});
|
||||
let audioContext;
|
||||
|
||||
export const setDefaultAudioContext = () => {
|
||||
audioContext = new AudioContext();
|
||||
audioContext = new AudioContext({ latencyHint: 'playback' });
|
||||
return audioContext;
|
||||
};
|
||||
|
||||
@@ -225,11 +225,17 @@ export function getAudioContextCurrentTime() {
|
||||
return getAudioContext().currentTime;
|
||||
}
|
||||
|
||||
let externalWorklets = [];
|
||||
export function registerWorklet(url) {
|
||||
externalWorklets.push(url);
|
||||
}
|
||||
|
||||
let workletsLoading;
|
||||
function loadWorklets() {
|
||||
if (!workletsLoading) {
|
||||
const audioCtx = getAudioContext();
|
||||
workletsLoading = audioCtx.audioWorklet.addModule(workletsUrl);
|
||||
const allWorkletURLs = externalWorklets.concat([workletsUrl]);
|
||||
workletsLoading = Promise.all(allWorkletURLs.map((workletURL) => audioCtx.audioWorklet.addModule(workletURL)));
|
||||
}
|
||||
|
||||
return workletsLoading;
|
||||
@@ -426,7 +432,7 @@ function connectToOrbit(node, orbit) {
|
||||
function setOrbit(audioContext, orbit, channels) {
|
||||
if (orbits[orbit] == null) {
|
||||
orbits[orbit] = {
|
||||
gain: new GainNode(audioContext, { gain: 1 }),
|
||||
gain: new GainNode(audioContext, { gain: 1, channelCount: 2, channelCountMode: 'explicit' }),
|
||||
};
|
||||
connectToDestination(orbits[orbit].gain, channels);
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
pattern.wav
|
||||
@@ -0,0 +1,3 @@
|
||||
# supradough
|
||||
|
||||
platform agnostic synth and sampler intended for live coding. a reimplementation of superdough.
|
||||
@@ -0,0 +1,123 @@
|
||||
// this is a poc of how a pattern can be rendered as a wav file using node
|
||||
// run via: node dough-export.mjs
|
||||
import fs from 'node:fs';
|
||||
import WavEncoder from 'wav-encoder';
|
||||
import { evalScope } from '@strudel/core';
|
||||
import { miniAllStrings } from '@strudel/mini';
|
||||
import { Dough } from './dough.mjs';
|
||||
|
||||
await evalScope(
|
||||
import('@strudel/core'),
|
||||
import('@strudel/mini'),
|
||||
import('@strudel/tonal'),
|
||||
// import('@strudel/tonal'),
|
||||
);
|
||||
|
||||
miniAllStrings(); // allows using single quotes for mini notation / skip transpilation
|
||||
|
||||
let sampleRate = 48000,
|
||||
cps = 0.4;
|
||||
|
||||
/* await doughsamples('github:eddyflux/crate');
|
||||
await doughsamples('github:eddyflux/wax'); */
|
||||
|
||||
let pat = note('c,eb,g,<bb c4 d4 eb4>')
|
||||
.s('sine')
|
||||
.press()
|
||||
.add(note(24))
|
||||
.fmi(3)
|
||||
.fmh(5.01)
|
||||
.dec(0.4)
|
||||
.delay('.6:<.12 .22>:.8')
|
||||
.jux(press)
|
||||
.rarely(add(note('12')))
|
||||
.lpf(400)
|
||||
.lpq(0.2)
|
||||
.lpd(0.4)
|
||||
.lpenv(3)
|
||||
.fmdecay(0.4)
|
||||
.fmenv(1)
|
||||
.postgain(0.6)
|
||||
.stack(s('<pink white>*8').dec(0.07).rarely(ply('2')).delay(0.5).hpf(sine.range(200, 2000).slow(4)).hpq(0.2))
|
||||
.stack(
|
||||
s('[- white@3]*2')
|
||||
.dec(0.4)
|
||||
.hpf('<2000!3 <4000 8000>>*4')
|
||||
.hpq(0.6)
|
||||
.ply('<1 2>*4')
|
||||
.postgain(0.5)
|
||||
.delay(0.5)
|
||||
.jux(rev)
|
||||
.lpf(5000),
|
||||
)
|
||||
.stack(
|
||||
note('<c2 - [- f1] ->*2')
|
||||
.s('square')
|
||||
.lpf(sine.range(100, 300).slow(4))
|
||||
.lpe(1)
|
||||
.segment(8)
|
||||
.lpd(0.3)
|
||||
.lpq(0.2)
|
||||
.dec(0.2)
|
||||
.speed('<1 2>')
|
||||
.ply('<1 2>')
|
||||
.postgain(1),
|
||||
)
|
||||
.stack(
|
||||
chord('<Cm Cm7 Cm9 Cm11 Fm Fm7 Fm9 Fm11>')
|
||||
.voicing()
|
||||
.s('<sine>')
|
||||
.clip(1)
|
||||
.rel(0.4)
|
||||
.vib('4:.2')
|
||||
.gain(0.7)
|
||||
.hpf(1200)
|
||||
.fm(0.5)
|
||||
.att(1)
|
||||
.lpa(0.5)
|
||||
.lpf(200)
|
||||
.lpenv(4)
|
||||
.chorus(0.8),
|
||||
)
|
||||
.slow(1 / cps);
|
||||
|
||||
let cycles = 30;
|
||||
let seconds = cycles + 1; // 1s release tail
|
||||
const haps = pat.queryArc(0, cycles);
|
||||
|
||||
const dough = new Dough(sampleRate);
|
||||
|
||||
console.log('spawn voices...');
|
||||
haps.forEach((hap) => {
|
||||
hap.value._begin = Number(hap.whole.begin);
|
||||
hap.value._duration = hap.duration /* / cps */;
|
||||
dough.scheduleSpawn(hap.value);
|
||||
});
|
||||
console.log(`render ${seconds}s long buffer, each dot is 1 second:`);
|
||||
const buffers = [new Float32Array(seconds * sampleRate), new Float32Array(seconds * sampleRate)];
|
||||
let t = performance.now();
|
||||
while (dough.t <= buffers[0].length) {
|
||||
dough.update();
|
||||
buffers[0][dough.t] = dough.out[0];
|
||||
buffers[1][dough.t] = dough.out[1];
|
||||
if (dough.t % sampleRate === 0) {
|
||||
process.stdout.write('.');
|
||||
}
|
||||
}
|
||||
const took = (performance.now() - t) / 1000;
|
||||
const load = (took / seconds) * 100;
|
||||
const speed = (seconds / took).toFixed(2);
|
||||
console.log('');
|
||||
console.log(`done!
|
||||
rendered ${seconds}s in ${took.toFixed(2)}s
|
||||
speed: ${speed}x
|
||||
load: ${load.toFixed(2)}%`);
|
||||
|
||||
const patternAudio = {
|
||||
sampleRate,
|
||||
channelData: buffers,
|
||||
};
|
||||
|
||||
WavEncoder.encode(patternAudio).then((buffer) => {
|
||||
fs.writeFileSync('pattern.wav', new Float32Array(buffer));
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Dough } from './dough.mjs';
|
||||
|
||||
const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
|
||||
|
||||
class DoughProcessor extends AudioWorkletProcessor {
|
||||
constructor() {
|
||||
super();
|
||||
this.dough = new Dough(sampleRate, currentTime);
|
||||
this.port.onmessage = (event) => {
|
||||
if (event.data.spawn) {
|
||||
this.dough.scheduleSpawn(event.data.spawn);
|
||||
} else if (event.data.sample) {
|
||||
this.dough.loadSample(event.data.sample, event.data.channels, event.data.sampleRate);
|
||||
} else if (event.data.samples) {
|
||||
event.data.samples.forEach(([name, channels, sampleRate]) => {
|
||||
this.dough.loadSample(name, channels, sampleRate);
|
||||
});
|
||||
} else {
|
||||
console.log('unrecognized event type', event.data);
|
||||
}
|
||||
};
|
||||
}
|
||||
process(inputs, outputs, params) {
|
||||
if (this.disconnected) {
|
||||
return false;
|
||||
}
|
||||
const output = outputs[0];
|
||||
for (let i = 0; i < output[0].length; i++) {
|
||||
this.dough.update();
|
||||
for (let c = 0; c < output.length; c++) {
|
||||
//prevent speaker blowout via clipping if threshold exceeds
|
||||
output[c][i] = clamp(this.dough.out[c], -1, 1);
|
||||
}
|
||||
}
|
||||
return true; // keep the audio processing going
|
||||
}
|
||||
}
|
||||
|
||||
registerProcessor('dough-processor', DoughProcessor);
|
||||
@@ -0,0 +1,976 @@
|
||||
// this is dough, the superdough without dependencies
|
||||
const SAMPLE_RATE = typeof sampleRate !== 'undefined' ? sampleRate : 48000;
|
||||
const PI_DIV_SR = Math.PI / SAMPLE_RATE;
|
||||
const ISR = 1 / SAMPLE_RATE;
|
||||
|
||||
let gainCurveFunc = (val) => Math.pow(val, 2);
|
||||
|
||||
function applyGainCurve(val) {
|
||||
return gainCurveFunc(val);
|
||||
}
|
||||
|
||||
/**
|
||||
* Equal Power Crossfade function.
|
||||
* Smoothly transitions between signals A and B, maintaining consistent perceived loudness.
|
||||
*
|
||||
* @param {number} a - Signal A (can be a single value or an array value in buffer processing).
|
||||
* @param {number} b - Signal B (can be a single value or an array value in buffer processing).
|
||||
* @param {number} m - Crossfade parameter (0.0 = all A, 1.0 = all B, 0.5 = equal mix).
|
||||
* @returns {number} Crossfaded output value.
|
||||
*/
|
||||
function crossfade(a, b, m) {
|
||||
const aGain = Math.sin((1 - m) * 0.5 * Math.PI);
|
||||
const bGain = Math.sin(m * 0.5 * Math.PI);
|
||||
return a * aGain + b * bGain;
|
||||
}
|
||||
|
||||
// function setGainCurve(newGainCurveFunc) {
|
||||
// gainCurveFunc = newGainCurveFunc;
|
||||
// }
|
||||
// https://garten.salat.dev/audio-DSP/oscillators.html
|
||||
export class SineOsc {
|
||||
phase = 0;
|
||||
update(freq) {
|
||||
const value = Math.sin(this.phase * 2 * Math.PI);
|
||||
this.phase = (this.phase + freq / SAMPLE_RATE) % 1;
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
export class ZawOsc {
|
||||
phase = 0;
|
||||
update(freq) {
|
||||
this.phase += ISR * freq;
|
||||
return (this.phase % 1) * 2 - 1;
|
||||
}
|
||||
}
|
||||
|
||||
function polyBlep(t, dt) {
|
||||
// 0 <= t < 1
|
||||
if (t < dt) {
|
||||
t /= dt;
|
||||
// 2 * (t - t^2/2 - 0.5)
|
||||
return t + t - t * t - 1;
|
||||
}
|
||||
// -1 < t < 0
|
||||
if (t > 1 - dt) {
|
||||
t = (t - 1) / dt;
|
||||
// 2 * (t^2/2 + t + 0.5)
|
||||
return t * t + t + t + 1;
|
||||
}
|
||||
// 0 otherwise
|
||||
return 0;
|
||||
}
|
||||
|
||||
export class SawOsc {
|
||||
constructor(props = {}) {
|
||||
this.phase = props.phase ?? 0;
|
||||
}
|
||||
update(freq) {
|
||||
const dt = freq / SAMPLE_RATE;
|
||||
let p = polyBlep(this.phase, dt);
|
||||
let s = 2 * this.phase - 1 - p;
|
||||
this.phase += dt;
|
||||
if (this.phase > 1) {
|
||||
this.phase -= 1;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
function getUnisonDetune(unison, detune, voiceIndex) {
|
||||
if (unison < 2) {
|
||||
return 0;
|
||||
}
|
||||
const lerp = (a, b, n) => {
|
||||
return n * (b - a) + a;
|
||||
};
|
||||
return lerp(-detune * 0.5, detune * 0.5, voiceIndex / (unison - 1));
|
||||
}
|
||||
function applySemitoneDetuneToFrequency(frequency, detune) {
|
||||
return frequency * Math.pow(2, detune / 12);
|
||||
}
|
||||
export class SupersawOsc {
|
||||
constructor(props = {}) {
|
||||
//TODO: figure out a good way to pass in these params
|
||||
this.voices = props.voices ?? 5;
|
||||
this.freqspread = props.freqspread ?? 0.2;
|
||||
this.panspread = props.panspread ?? 0.4;
|
||||
this.phase = new Float32Array(this.voices).map(() => Math.random());
|
||||
}
|
||||
update(freq) {
|
||||
const gain1 = Math.sqrt(1 - this.panspread);
|
||||
const gain2 = Math.sqrt(this.panspread);
|
||||
let sl = 0;
|
||||
let sr = 0;
|
||||
for (let n = 0; n < this.voices; n++) {
|
||||
const freqAdjusted = applySemitoneDetuneToFrequency(freq, getUnisonDetune(this.voices, this.freqspread, n));
|
||||
const dt = freqAdjusted / SAMPLE_RATE;
|
||||
const isOdd = (n & 1) == 1;
|
||||
let gainL = gain1;
|
||||
let gainR = gain2;
|
||||
// invert right and left gain
|
||||
if (isOdd) {
|
||||
gainL = gain2;
|
||||
gainR = gain1;
|
||||
}
|
||||
let p = polyBlep(this.phase[n], dt);
|
||||
let s = 2 * this.phase[n] - 1 - p;
|
||||
sl = sl + s * gainL;
|
||||
sr = sr + s * gainL;
|
||||
|
||||
this.phase[n] += dt;
|
||||
if (this.phase[n] > 1) {
|
||||
this.phase[n] -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
return sl + sr;
|
||||
//TODO: make stereo
|
||||
// return [sl, sr];
|
||||
}
|
||||
}
|
||||
|
||||
export class TriOsc {
|
||||
phase = 0;
|
||||
update(freq) {
|
||||
this.phase += ISR * freq;
|
||||
let phase = this.phase % 1;
|
||||
let value = phase < 0.5 ? 2 * phase : 1 - 2 * (phase - 0.5);
|
||||
return value * 2 - 1;
|
||||
}
|
||||
}
|
||||
|
||||
export class TwoPoleFilter {
|
||||
s0 = 0;
|
||||
s1 = 0;
|
||||
update(s, cutoff, resonance = 0) {
|
||||
// Out of bound values can produce NaNs
|
||||
resonance = Math.max(resonance, 0);
|
||||
|
||||
cutoff = Math.min(cutoff, 20000);
|
||||
const c = 2 * Math.sin(cutoff * PI_DIV_SR);
|
||||
|
||||
const r = Math.pow(0.5, (resonance + 0.125) / 0.125);
|
||||
const mrc = 1 - r * c;
|
||||
|
||||
this.s0 = mrc * this.s0 - c * this.s1 + c * s; // bpf
|
||||
this.s1 = mrc * this.s1 + c * this.s0; // lpf
|
||||
return this.s1; // return lpf by default
|
||||
}
|
||||
}
|
||||
|
||||
class PulseOsc {
|
||||
constructor(phase = 0) {
|
||||
this.phase = phase;
|
||||
}
|
||||
saw(offset, dt) {
|
||||
let phase = (this.phase + offset) % 1;
|
||||
let p = polyBlep(phase, dt);
|
||||
return 2 * phase - 1 - p;
|
||||
}
|
||||
update(freq, pw = 0.5) {
|
||||
const dt = freq / SAMPLE_RATE;
|
||||
let pulse = this.saw(0, dt) - this.saw(pw, dt);
|
||||
this.phase = (this.phase + dt) % 1;
|
||||
return pulse + pw * 2 - 1;
|
||||
}
|
||||
}
|
||||
|
||||
// non bandlimited (has aliasing)
|
||||
export class PulzeOsc {
|
||||
phase = 0;
|
||||
update(freq, duty = 0.5) {
|
||||
this.phase += ISR * freq;
|
||||
let cyclePos = this.phase % 1;
|
||||
return cyclePos < duty ? 1 : -1;
|
||||
}
|
||||
}
|
||||
|
||||
export class Dust {
|
||||
update = (density) => (Math.random() < density * ISR ? Math.random() : 0);
|
||||
}
|
||||
|
||||
export class WhiteNoise {
|
||||
update() {
|
||||
return Math.random() * 2 - 1;
|
||||
}
|
||||
}
|
||||
|
||||
export class BrownNoise {
|
||||
constructor() {
|
||||
this.out = 0;
|
||||
}
|
||||
update() {
|
||||
let white = Math.random() * 2 - 1;
|
||||
this.out = (this.out + 0.02 * white) / 1.02;
|
||||
return this.out;
|
||||
}
|
||||
}
|
||||
|
||||
export class PinkNoise {
|
||||
constructor() {
|
||||
this.b0 = 0;
|
||||
this.b1 = 0;
|
||||
this.b2 = 0;
|
||||
this.b3 = 0;
|
||||
this.b4 = 0;
|
||||
this.b5 = 0;
|
||||
this.b6 = 0;
|
||||
}
|
||||
|
||||
update() {
|
||||
const white = Math.random() * 2 - 1;
|
||||
|
||||
this.b0 = 0.99886 * this.b0 + white * 0.0555179;
|
||||
this.b1 = 0.99332 * this.b1 + white * 0.0750759;
|
||||
this.b2 = 0.969 * this.b2 + white * 0.153852;
|
||||
this.b3 = 0.8665 * this.b3 + white * 0.3104856;
|
||||
this.b4 = 0.55 * this.b4 + white * 0.5329522;
|
||||
this.b5 = -0.7616 * this.b5 - white * 0.016898;
|
||||
|
||||
const pink = this.b0 + this.b1 + this.b2 + this.b3 + this.b4 + this.b5 + this.b6 + white * 0.5362;
|
||||
this.b6 = white * 0.115926;
|
||||
|
||||
return pink * 0.11;
|
||||
}
|
||||
}
|
||||
|
||||
export class Impulse {
|
||||
phase = 1;
|
||||
update(freq) {
|
||||
this.phase += ISR * freq;
|
||||
let v = this.phase >= 1 ? 1 : 0;
|
||||
this.phase = this.phase % 1;
|
||||
return v;
|
||||
}
|
||||
}
|
||||
|
||||
export class ClockDiv {
|
||||
inSgn = true;
|
||||
outSgn = true;
|
||||
clockCnt = 0;
|
||||
update(clock, factor) {
|
||||
let curSgn = clock > 0;
|
||||
if (this.inSgn != curSgn) {
|
||||
this.clockCnt++;
|
||||
if (this.clockCnt >= factor) {
|
||||
this.clockCnt = 0;
|
||||
this.outSgn = !this.outSgn;
|
||||
}
|
||||
}
|
||||
|
||||
this.inSgn = curSgn;
|
||||
return this.outSgn ? 1 : -1;
|
||||
}
|
||||
}
|
||||
|
||||
export class Hold {
|
||||
value = 0;
|
||||
trigSgn = false;
|
||||
update(input, trig) {
|
||||
if (!this.trigSgn && trig > 0) this.value = input;
|
||||
this.trigSgn = trig > 0;
|
||||
return this.value;
|
||||
}
|
||||
}
|
||||
|
||||
function lerp(x, y0, y1, exponent = 1) {
|
||||
if (x <= 0) return y0;
|
||||
if (x >= 1) return y1;
|
||||
|
||||
let curvedX;
|
||||
|
||||
if (exponent === 0) {
|
||||
curvedX = x; // linear
|
||||
} else if (exponent > 0) {
|
||||
curvedX = Math.pow(x, exponent); // ease-in
|
||||
} else {
|
||||
curvedX = 1 - Math.pow(1 - x, -exponent); // ease-out
|
||||
}
|
||||
|
||||
return y0 + (y1 - y0) * curvedX;
|
||||
}
|
||||
|
||||
export class ADSR {
|
||||
constructor(props = {}) {
|
||||
this.state = 'off';
|
||||
this.startTime = 0;
|
||||
this.startVal = 0;
|
||||
this.decayCurve = props.decayCurve ?? 1;
|
||||
}
|
||||
|
||||
update(curTime, gate, attack, decay, susVal, release) {
|
||||
switch (this.state) {
|
||||
case 'off': {
|
||||
if (gate > 0) {
|
||||
this.state = 'attack';
|
||||
this.startTime = curTime;
|
||||
this.startVal = 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
case 'attack': {
|
||||
let time = curTime - this.startTime;
|
||||
if (time > attack) {
|
||||
this.state = 'decay';
|
||||
this.startTime = curTime;
|
||||
return 1;
|
||||
}
|
||||
return lerp(time / attack, this.startVal, 1, 1);
|
||||
}
|
||||
case 'decay': {
|
||||
let time = curTime - this.startTime;
|
||||
let curVal = lerp(time / decay, 1, susVal, -this.decayCurve);
|
||||
if (gate <= 0) {
|
||||
this.state = 'release';
|
||||
this.startTime = curTime;
|
||||
this.startVal = curVal;
|
||||
return curVal;
|
||||
}
|
||||
if (time > decay) {
|
||||
this.state = 'sustain';
|
||||
this.startTime = curTime;
|
||||
return susVal;
|
||||
}
|
||||
return curVal;
|
||||
}
|
||||
case 'sustain': {
|
||||
if (gate <= 0) {
|
||||
this.state = 'release';
|
||||
this.startTime = curTime;
|
||||
this.startVal = susVal;
|
||||
}
|
||||
return susVal;
|
||||
}
|
||||
case 'release': {
|
||||
let time = curTime - this.startTime;
|
||||
|
||||
if (time > release) {
|
||||
this.state = 'off';
|
||||
return 0;
|
||||
}
|
||||
let curVal = lerp(time / release, this.startVal, 0, -this.decayCurve);
|
||||
if (gate > 0) {
|
||||
this.state = 'attack';
|
||||
this.startTime = curTime;
|
||||
this.startVal = curVal;
|
||||
}
|
||||
return curVal;
|
||||
}
|
||||
}
|
||||
throw 'invalid envelope state';
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
impulse(1).ad(.1).mul(sine(200))
|
||||
.add(x=>x.delay(.1).mul(.8))
|
||||
.out()*/
|
||||
const MAX_DELAY_TIME = 10;
|
||||
export class PitchDelay {
|
||||
lpf = new TwoPoleFilter();
|
||||
constructor(_props = {}) {
|
||||
this.buffer = new Float32Array(MAX_DELAY_TIME * SAMPLE_RATE);
|
||||
this.writeIdx = 0;
|
||||
this.readIdx = 0;
|
||||
this.numSamples = 0;
|
||||
}
|
||||
write(s, delayTime) {
|
||||
// Calculate how far in the past to read
|
||||
this.numSamples = Math.min(Math.floor(SAMPLE_RATE * delayTime), this.buffer.length - 1);
|
||||
this.writeIdx = (this.writeIdx + 1) % this.numSamples;
|
||||
this.buffer[this.writeIdx] = s;
|
||||
this.readIdx = this.writeIdx - this.numSamples + 1;
|
||||
|
||||
// If past the start of the buffer, wrap around (Q: is this possible?)
|
||||
if (this.readIdx < 0) this.readIdx += this.numSamples;
|
||||
}
|
||||
update(input, delayTime, speed = 1) {
|
||||
this.write(input, delayTime);
|
||||
let index = this.readIdx;
|
||||
if (speed < 0) {
|
||||
index = this.numSamples - Math.floor(Math.abs(this.readIdx * speed) % this.numSamples);
|
||||
} else {
|
||||
index = Math.floor(this.readIdx * speed) % this.numSamples;
|
||||
}
|
||||
const s = this.lpf.update(this.buffer[index], 0.9, 0);
|
||||
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
export class Delay {
|
||||
writeIdx = 0;
|
||||
readIdx = 0;
|
||||
buffer = new Float32Array(MAX_DELAY_TIME * SAMPLE_RATE); //.fill(0)
|
||||
write(s, delayTime) {
|
||||
this.writeIdx = (this.writeIdx + 1) % this.buffer.length;
|
||||
this.buffer[this.writeIdx] = s;
|
||||
// Calculate how far in the past to read
|
||||
let numSamples = Math.min(Math.floor(SAMPLE_RATE * delayTime), this.buffer.length - 1);
|
||||
this.readIdx = this.writeIdx - numSamples;
|
||||
// If past the start of the buffer, wrap around
|
||||
if (this.readIdx < 0) this.readIdx += this.buffer.length;
|
||||
}
|
||||
update(input, delayTime) {
|
||||
this.write(input, delayTime);
|
||||
return this.buffer[this.readIdx];
|
||||
}
|
||||
}
|
||||
//TODO: Figure out why clicking at the start off the buffer
|
||||
export class Chorus {
|
||||
delay = new Delay();
|
||||
modulator = new TriOsc();
|
||||
update(input, mix, delayTime, modulationFreq, modulationDepth) {
|
||||
const m = this.modulator.update(modulationFreq) * modulationDepth;
|
||||
const c = this.delay.update(input, delayTime * (1 + m));
|
||||
return crossfade(input, c, mix);
|
||||
}
|
||||
}
|
||||
|
||||
export class Fold {
|
||||
update(input = 0, rate = 0) {
|
||||
if (rate < 0) rate = 0;
|
||||
rate = rate + 1;
|
||||
input = input * rate;
|
||||
return 4 * (Math.abs(0.25 * input + 0.25 - Math.round(0.25 * input + 0.25)) - 0.25);
|
||||
}
|
||||
}
|
||||
|
||||
export class Lag {
|
||||
lagUnit = 4410;
|
||||
s = 0;
|
||||
update(input, rate) {
|
||||
// Remap so the useful range is around [0, 1]
|
||||
rate = rate * this.lagUnit;
|
||||
if (rate < 1) rate = 1;
|
||||
this.s += (1 / rate) * (input - this.s);
|
||||
return this.s;
|
||||
}
|
||||
}
|
||||
|
||||
export class Slew {
|
||||
last = 0;
|
||||
update(input, up, dn) {
|
||||
const upStep = up * ISR;
|
||||
const downStep = dn * ISR;
|
||||
let delta = input - this.last;
|
||||
if (delta > upStep) {
|
||||
delta = upStep;
|
||||
} else if (delta < -downStep) {
|
||||
delta = -downStep;
|
||||
}
|
||||
this.last += delta;
|
||||
return this.last;
|
||||
}
|
||||
}
|
||||
|
||||
// overdrive style distortion (adapted from noisecraft) currently unused
|
||||
export function applyDistortion(x, amount) {
|
||||
amount = Math.min(Math.max(amount, 0), 1);
|
||||
amount -= 0.01;
|
||||
var k = (2 * amount) / (1 - amount);
|
||||
var y = ((1 + k) * x) / (1 + k * Math.abs(x));
|
||||
return y;
|
||||
}
|
||||
|
||||
export class Sequence {
|
||||
clockSgn = true;
|
||||
step = 0;
|
||||
first = true;
|
||||
update(clock, ...ins) {
|
||||
if (!this.clockSgn && clock > 0) {
|
||||
this.step = (this.step + 1) % ins.length;
|
||||
this.clockSgn = clock > 0;
|
||||
return 0; // set first sample to zero to retrigger gates on step change...
|
||||
}
|
||||
this.clockSgn = clock > 0;
|
||||
return ins[this.step];
|
||||
}
|
||||
}
|
||||
|
||||
// sample rate bit crusher
|
||||
export class Coarse {
|
||||
hold = 0;
|
||||
t = 0;
|
||||
update(input, coarse) {
|
||||
if (this.t++ % coarse === 0) {
|
||||
this.t = 0;
|
||||
this.hold = input;
|
||||
}
|
||||
return this.hold;
|
||||
}
|
||||
}
|
||||
|
||||
// amplitude bit crusher
|
||||
export class Crush {
|
||||
update(input, crush) {
|
||||
crush = Math.max(1, crush);
|
||||
const x = Math.pow(2, crush - 1);
|
||||
return Math.round(input * x) / x;
|
||||
}
|
||||
}
|
||||
|
||||
// this is the distort from superdough
|
||||
export class Distort {
|
||||
update(input, distort = 0, postgain = 1) {
|
||||
postgain = Math.max(0.001, Math.min(1, postgain));
|
||||
const shape = Math.expm1(distort);
|
||||
return (((1 + shape) * input) / (1 + shape * Math.abs(input))) * postgain;
|
||||
}
|
||||
}
|
||||
// distortion could be expressed as a function, because it's stateless
|
||||
|
||||
export class BufferPlayer {
|
||||
static samples = new Map(); // string -> { channels, sampleRate }
|
||||
buffer; // Float32Array
|
||||
sampleRate;
|
||||
pos = 0;
|
||||
sampleFreq = note2freq();
|
||||
constructor(buffer, sampleRate, normalize) {
|
||||
this.buffer = buffer;
|
||||
this.sampleRate = sampleRate;
|
||||
this.duration = this.buffer.length / this.sampleRate;
|
||||
this.speed = SAMPLE_RATE / this.sampleRate;
|
||||
if (normalize) {
|
||||
// this will make the buffer last 1s if freq = sampleFreq
|
||||
// it's useful to loop samples (e.g. fit function)
|
||||
this.speed *= this.duration;
|
||||
}
|
||||
}
|
||||
update(freq) {
|
||||
if (this.pos >= this.buffer.length) {
|
||||
return 0;
|
||||
}
|
||||
const speed = (freq / this.sampleFreq) * this.speed;
|
||||
let s = this.buffer[Math.floor(this.pos)];
|
||||
this.pos = this.pos + speed;
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
export function _rangex(sig, min, max) {
|
||||
let logmin = Math.log(min);
|
||||
let range = Math.log(max) - logmin;
|
||||
const unipolar = (sig + 1) / 2;
|
||||
return Math.exp(unipolar * range + logmin);
|
||||
}
|
||||
|
||||
// duplicate
|
||||
export const getADSR = (params, curve = 'linear', defaultValues) => {
|
||||
const envmin = curve === 'exponential' ? 0.001 : 0.001;
|
||||
const releaseMin = 0.01;
|
||||
const envmax = 1;
|
||||
const [a, d, s, r] = params;
|
||||
if (a == null && d == null && s == null && r == null) {
|
||||
return defaultValues ?? [envmin, envmin, envmax, releaseMin];
|
||||
}
|
||||
const sustain = s != null ? s : (a != null && d == null) || (a == null && d == null) ? envmax : envmin;
|
||||
return [Math.max(a ?? 0, envmin), Math.max(d ?? 0, envmin), Math.min(sustain, envmax), Math.max(r ?? 0, releaseMin)];
|
||||
};
|
||||
|
||||
let shapes = {
|
||||
sine: SineOsc,
|
||||
saw: SawOsc,
|
||||
zaw: ZawOsc,
|
||||
sawtooth: SawOsc,
|
||||
zawtooth: ZawOsc,
|
||||
supersaw: SupersawOsc,
|
||||
tri: TriOsc,
|
||||
triangle: TriOsc,
|
||||
pulse: PulseOsc,
|
||||
square: PulseOsc,
|
||||
pulze: PulzeOsc,
|
||||
dust: Dust,
|
||||
crackle: Dust,
|
||||
impulse: Impulse,
|
||||
white: WhiteNoise,
|
||||
brown: BrownNoise,
|
||||
pink: PinkNoise,
|
||||
};
|
||||
|
||||
const defaultDefaultValues = {
|
||||
chorus: 0,
|
||||
note: 48,
|
||||
s: 'triangle',
|
||||
bank: '',
|
||||
gain: 1,
|
||||
postgain: 1,
|
||||
velocity: 1,
|
||||
density: '.03',
|
||||
ftype: '12db',
|
||||
fanchor: 0,
|
||||
//resonance: 1, // superdough resonance is scaled differently
|
||||
resonance: 0,
|
||||
//hresonance: 1, // superdough resonance is scaled differently
|
||||
hresonance: 0,
|
||||
// bandq: 1, // superdough resonance is scaled differently
|
||||
bandq: 0,
|
||||
channels: [1, 2],
|
||||
phaserdepth: 0.75,
|
||||
shapevol: 1,
|
||||
distortvol: 1,
|
||||
delay: 0,
|
||||
byteBeatExpression: '0',
|
||||
delayfeedback: 0.5,
|
||||
delayspeed: 1,
|
||||
delaytime: 0.25,
|
||||
orbit: 1,
|
||||
i: 1,
|
||||
fft: 8,
|
||||
z: 'triangle',
|
||||
pan: 0.5,
|
||||
fmh: 1,
|
||||
fmenv: 0, // differs from superdough
|
||||
speed: 1,
|
||||
pw: 0.5,
|
||||
};
|
||||
|
||||
let getDefaultValue = (key) => defaultDefaultValues[key];
|
||||
|
||||
const chromas = { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 };
|
||||
const accs = { '#': 1, b: -1, s: 1, f: -1 };
|
||||
const note2midi = (note, defaultOctave = 3) => {
|
||||
let [pc, acc = '', oct = ''] =
|
||||
String(note)
|
||||
.match(/^([a-gA-G])([#bsf]*)([0-9]*)$/)
|
||||
?.slice(1) || [];
|
||||
if (!pc) {
|
||||
throw new Error('not a note: "' + note + '"');
|
||||
}
|
||||
const chroma = chromas[pc.toLowerCase()];
|
||||
const offset = acc?.split('').reduce((o, char) => o + accs[char], 0) || 0;
|
||||
oct = Number(oct || defaultOctave);
|
||||
return (oct + 1) * 12 + chroma + offset;
|
||||
};
|
||||
const midi2freq = (midi) => Math.pow(2, (midi - 69) / 12) * 440;
|
||||
const note2freq = (note) => {
|
||||
note = note || getDefaultValue('note');
|
||||
if (typeof note === 'string') {
|
||||
note = note2midi(note, 3); // e.g. c3 => 48
|
||||
}
|
||||
return midi2freq(note);
|
||||
};
|
||||
|
||||
export class DoughVoice {
|
||||
out = [0, 0];
|
||||
constructor(value) {
|
||||
value.freq ??= note2freq(value.note);
|
||||
let $ = this;
|
||||
Object.assign($, value);
|
||||
$.s = $.s ?? getDefaultValue('s');
|
||||
$.gain = applyGainCurve($.gain ?? getDefaultValue('gain'));
|
||||
$.velocity = applyGainCurve($.velocity ?? getDefaultValue('velocity'));
|
||||
$.postgain = applyGainCurve($.postgain ?? getDefaultValue('postgain'));
|
||||
$.density = $.density ?? getDefaultValue('density');
|
||||
$.fanchor = $.fanchor ?? getDefaultValue('fanchor');
|
||||
$.drive = $.drive ?? 0.69;
|
||||
$.phaserdepth = $.phaserdepth ?? getDefaultValue('phaserdepth');
|
||||
$.shapevol = applyGainCurve($.shapevol ?? getDefaultValue('shapevol'));
|
||||
$.distortvol = applyGainCurve($.distortvol ?? getDefaultValue('distortvol'));
|
||||
$.i = $.i ?? getDefaultValue('i');
|
||||
$.chorus = $.chorus ?? getDefaultValue('chorus');
|
||||
$.fft = $.fft ?? getDefaultValue('fft');
|
||||
$.pan = $.pan ?? getDefaultValue('pan');
|
||||
$.orbit = $.orbit ?? getDefaultValue('orbit');
|
||||
$.fmenv = $.fmenv ?? getDefaultValue('fmenv');
|
||||
$.resonance = $.resonance ?? getDefaultValue('resonance');
|
||||
$.hresonance = $.hresonance ?? getDefaultValue('hresonance');
|
||||
$.bandq = $.bandq ?? getDefaultValue('bandq');
|
||||
$.speed = $.speed ?? getDefaultValue('speed');
|
||||
$.pw = $.pw ?? getDefaultValue('pw');
|
||||
|
||||
[$.attack, $.decay, $.sustain, $.release] = getADSR([$.attack, $.decay, $.sustain, $.release]);
|
||||
|
||||
$._holdEnd = $._begin + $._duration; // needed for gate
|
||||
$._end = $._holdEnd + $.release + 0.01; // needed for despawn
|
||||
|
||||
if ($.fmi && ($.s === 'saw' || $.s === 'sawtooth')) {
|
||||
$.s = 'zaw'; // polyblepped saw when fm is applied
|
||||
}
|
||||
|
||||
if (shapes[$.s]) {
|
||||
const SourceClass = shapes[$.s];
|
||||
$._sound = new SourceClass();
|
||||
$._channels = 1;
|
||||
} else if (BufferPlayer.samples.has($.s)) {
|
||||
const sample = BufferPlayer.samples.get($.s);
|
||||
$._buffers = [];
|
||||
$._channels = sample.channels.length;
|
||||
for (let i = 0; i < $._channels; i++) {
|
||||
$._buffers.push(new BufferPlayer(sample.channels[i], sample.sampleRate, $.unit === 'c')); // tbd unit === 'c'
|
||||
}
|
||||
} else {
|
||||
console.warn('sound not loaded', $.s);
|
||||
}
|
||||
|
||||
if ($.penv) {
|
||||
$._penv = new ADSR({ decayCurve: 4 });
|
||||
[$.pattack, $.pdecay, $.psustain, $.prelease] = getADSR([$.pattack, $.pdecay, $.psustain, $.prelease]);
|
||||
}
|
||||
|
||||
if ($.vib) {
|
||||
$._vib = new SineOsc();
|
||||
$.vibmod = $.vibmod ?? getDefaultValue('vibmod');
|
||||
}
|
||||
|
||||
if ($.fmi) {
|
||||
$._fm = new SineOsc();
|
||||
$.fmh = $.fmh ?? getDefaultValue('fmh');
|
||||
if ($.fmenv) {
|
||||
$._fmenv = new ADSR({ decayCurve: 2 });
|
||||
[$.fmattack, $.fmdecay, $.fmsustain, $.fmrelease] = getADSR([$.fmattack, $.fmdecay, $.fmsustain, $.fmrelease]);
|
||||
}
|
||||
}
|
||||
|
||||
// gain envelope
|
||||
$._adsr = new ADSR({ decayCurve: 2 });
|
||||
// delay
|
||||
$.delay = applyGainCurve($.delay ?? getDefaultValue('delay'));
|
||||
$.delayfeedback = $.delayfeedback ?? getDefaultValue('delayfeedback');
|
||||
$.delayspeed = $.delayspeed ?? getDefaultValue('delayspeed');
|
||||
$.delaytime = $.delaytime ?? getDefaultValue('delaytime');
|
||||
|
||||
// filter setup
|
||||
if ($.lpenv) {
|
||||
$._lpenv = new ADSR({ decayCurve: 4 });
|
||||
[$.lpattack, $.lpdecay, $.lpsustain, $.lprelease] = getADSR([$.lpattack, $.lpdecay, $.lpsustain, $.lprelease]);
|
||||
}
|
||||
if ($.hpenv) {
|
||||
$._hpenv = new ADSR({ decayCurve: 4 });
|
||||
[$.hpattack, $.hpdecay, $.hpsustain, $.hprelease] = getADSR([$.hpattack, $.hpdecay, $.hpsustain, $.hprelease]);
|
||||
}
|
||||
if ($.bpenv) {
|
||||
$._bpenv = new ADSR({ decayCurve: 4 });
|
||||
[$.bpattack, $.bpdecay, $.bpsustain, $.bprelease] = getADSR([$.bpattack, $.bpdecay, $.bpsustain, $.bprelease]);
|
||||
}
|
||||
|
||||
// channelwise effects setup
|
||||
$._chorus = $.chorus ? [] : null;
|
||||
$._lpf = $.cutoff ? [] : null;
|
||||
$._hpf = $.hcutoff ? [] : null;
|
||||
$._bpf = $.bandf ? [] : null;
|
||||
$._coarse = $.coarse ? [] : null;
|
||||
$._crush = $.crush ? [] : null;
|
||||
$._distort = $.distort ? [] : null;
|
||||
for (let i = 0; i < this._channels; i++) {
|
||||
$._lpf?.push(new TwoPoleFilter());
|
||||
$._hpf?.push(new TwoPoleFilter());
|
||||
$._bpf?.push(new TwoPoleFilter());
|
||||
$._chorus?.push(new Chorus());
|
||||
$._coarse?.push(new Coarse());
|
||||
$._crush?.push(new Crush());
|
||||
$._distort?.push(new Distort());
|
||||
}
|
||||
}
|
||||
update(t) {
|
||||
if (!this._sound && !this._buffers) {
|
||||
return 0;
|
||||
}
|
||||
let gate = Number(t >= this._begin && t <= this._holdEnd);
|
||||
|
||||
let freq = this.freq * this.speed;
|
||||
|
||||
// frequency modulation
|
||||
if (this._fm) {
|
||||
let fmi = this.fmi;
|
||||
if (this._fmenv) {
|
||||
const env = this._fmenv.update(t, gate, this.fmattack, this.fmdecay, this.fmsustain, this.fmrelease);
|
||||
fmi = this.fmenv * env * fmi;
|
||||
}
|
||||
const modfreq = freq * this.fmh;
|
||||
const modgain = modfreq * fmi;
|
||||
freq = freq + this._fm.update(modfreq) * modgain;
|
||||
}
|
||||
|
||||
// vibrato
|
||||
if (this._vib) {
|
||||
freq = freq * 2 ** ((this._vib.update(this.vib) * this.vibmod) / 12);
|
||||
}
|
||||
|
||||
// pitch envelope
|
||||
if (this._penv) {
|
||||
const env = this._penv.update(t, gate, this.pattack, this.pdecay, this.psustain, this.prelease);
|
||||
freq = freq + env * this.penv;
|
||||
}
|
||||
|
||||
// filters
|
||||
let lpf = this.cutoff;
|
||||
if (this._lpf) {
|
||||
if (this._lpenv) {
|
||||
const env = this._lpenv.update(t, gate, this.lpattack, this.lpdecay, this.lpsustain, this.lprelease);
|
||||
lpf = this.lpenv * env * lpf + lpf;
|
||||
}
|
||||
}
|
||||
let hpf = this.hcutoff;
|
||||
if (this._hpf) {
|
||||
if (this._hpenv) {
|
||||
const env = this._hpenv.update(t, gate, this.hpattack, this.hpdecay, this.hpsustain, this.hprelease);
|
||||
hpf = 2 ** this.hpenv * env * hpf + hpf;
|
||||
}
|
||||
}
|
||||
let bpf = this.bandf;
|
||||
if (this._bpf) {
|
||||
if (this._bpenv) {
|
||||
const env = this._bpenv.update(t, gate, this.bpattack, this.bpdecay, this.bpsustain, this.bprelease);
|
||||
bpf = 2 ** this.bpenv * env * bpf + bpf;
|
||||
}
|
||||
}
|
||||
// gain envelope
|
||||
const env = this._adsr.update(t, gate, this.attack, this.decay, this.sustain, this.release);
|
||||
|
||||
// channelwise dsp
|
||||
for (let i = 0; i < this._channels; i++) {
|
||||
// sound source
|
||||
if (this._sound && this.s === 'pulse') {
|
||||
this.out[i] = this._sound.update(freq, this.pw);
|
||||
} else if (this._sound) {
|
||||
this.out[i] = this._sound.update(freq);
|
||||
} else if (this._buffers) {
|
||||
this.out[i] = this._buffers[i].update(freq);
|
||||
}
|
||||
this.out[i] = this.out[i] * this.gain * this.velocity;
|
||||
if (this._chorus) {
|
||||
const c = this._chorus[i].update(this.out[i], this.chorus, 0.03 + 0.05 * i, 1, 0.11);
|
||||
this.out[i] = c + this.out[i];
|
||||
}
|
||||
|
||||
if (this._lpf) {
|
||||
this._lpf[i].update(this.out[i], lpf, this.resonance);
|
||||
this.out[i] = this._lpf[i].s1;
|
||||
}
|
||||
if (this._hpf) {
|
||||
this._hpf[i].update(this.out[i], hpf, this.hresonance);
|
||||
this.out[i] = this.out[i] - this._hpf[i].s1;
|
||||
}
|
||||
if (this._bpf) {
|
||||
this._bpf[i].update(this.out[i], bpf, this.bandq);
|
||||
this.out[i] = this._bpf[i].s0;
|
||||
}
|
||||
if (this._coarse) {
|
||||
this.out[i] = this._coarse[i].update(this.out[i], this.coarse);
|
||||
}
|
||||
if (this._crush) {
|
||||
this.out[i] = this._crush[i].update(this.out[i], this.crush);
|
||||
}
|
||||
if (this._distort) {
|
||||
this.out[i] = this._distort[i].update(this.out[i], this.distort, this.distortvol);
|
||||
}
|
||||
this.out[i] = this.out[i] * env;
|
||||
this.out[i] = this.out[i] * this.postgain;
|
||||
if (!this._buffers) {
|
||||
this.out[i] = this.out[i] * 0.2; // turn down waveform
|
||||
}
|
||||
}
|
||||
if (this._channels === 1) {
|
||||
this.out[1] = this.out[0];
|
||||
}
|
||||
if (this.pan !== 0.5) {
|
||||
const panpos = (this.pan * Math.PI) / 2;
|
||||
this.out[0] = this.out[0] * Math.cos(panpos);
|
||||
this.out[1] = this.out[1] * Math.sin(panpos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// this class is the interface to the "outer world"
|
||||
// it handles spawning and despawning of DoughVoice's
|
||||
export class Dough {
|
||||
voices = []; // DoughVoice[]
|
||||
vid = 0;
|
||||
q = [];
|
||||
out = [0, 0];
|
||||
delaysend = [0, 0];
|
||||
delaytime = getDefaultValue('delaytime');
|
||||
delayfeedback = getDefaultValue('delayfeedback');
|
||||
delayspeed = getDefaultValue('delayspeed');
|
||||
t = 0;
|
||||
// sampleRate: number, currentTime: number (seconds)
|
||||
constructor(sampleRate = 48000, currentTime = 0) {
|
||||
this.sampleRate = sampleRate;
|
||||
this.t = Math.floor(currentTime * sampleRate); // samples
|
||||
// console.log('init dough', this.sampleRate, this.t);
|
||||
this._delayL = new PitchDelay();
|
||||
this._delayR = new PitchDelay();
|
||||
}
|
||||
loadSample(name, channels, sampleRate) {
|
||||
BufferPlayer.samples.set(name, { channels, sampleRate });
|
||||
}
|
||||
scheduleSpawn(value) {
|
||||
if (value._begin === undefined) {
|
||||
throw new Error('[dough]: scheduleSpawn expected _begin to be set');
|
||||
}
|
||||
if (value._duration === undefined) {
|
||||
throw new Error('[dough]: scheduleSpawn expected _duration to be set');
|
||||
}
|
||||
value.sampleRate = this.sampleRate;
|
||||
// convert seconds to samples
|
||||
const time = Math.floor(value._begin * this.sampleRate); // set from supradough.mjs
|
||||
this.schedule({ time, type: 'spawn', arg: value });
|
||||
}
|
||||
spawn(value) {
|
||||
value.id = this.vid++;
|
||||
const voice = new DoughVoice(value);
|
||||
this.voices.push(voice);
|
||||
// console.log('spawn', voice.id, 'voices:', this.voices.length);
|
||||
// schedule removal
|
||||
const endTime = Math.ceil(voice._end * this.sampleRate);
|
||||
this.schedule({ time: endTime /* + 48000 */, type: 'despawn', arg: voice.id });
|
||||
}
|
||||
despawn(vid) {
|
||||
this.voices = this.voices.filter((v) => v.id !== vid);
|
||||
// console.log('despawn', vid, 'voices:', this.voices.length);
|
||||
}
|
||||
// schedules a function call with a single argument
|
||||
// msg = {time:number,type:string, arg: any}
|
||||
// the Dough method "type" will be called with "arg" at "time"
|
||||
schedule(msg) {
|
||||
if (!this.q.length) {
|
||||
// if empty, just push
|
||||
this.q.push(msg);
|
||||
return;
|
||||
}
|
||||
// not empty
|
||||
// find index where msg.time fits in
|
||||
let i = 0;
|
||||
while (i < this.q.length && this.q[i].time < msg.time) {
|
||||
i++;
|
||||
}
|
||||
// this ensures q stays sorted by time, so we only need to check q[0]
|
||||
this.q.splice(i, 0, msg);
|
||||
}
|
||||
// maybe update should be called once per block instead for perf reasons?
|
||||
update() {
|
||||
// go over q
|
||||
while (this.q.length > 0 && this.q[0].time <= this.t) {
|
||||
// console.log('schedule', this.q[0]);
|
||||
// trigger due messages. q is sorted, so we only need to check q[0]
|
||||
this[this.q[0].type](this.q[0].arg); // type is expected to be a Dough method
|
||||
this.q.shift();
|
||||
}
|
||||
// add active voices
|
||||
this.out[0] = 0;
|
||||
this.out[1] = 0;
|
||||
for (let v = 0; v < this.voices.length; v++) {
|
||||
this.voices[v].update(this.t / this.sampleRate);
|
||||
this.out[0] += this.voices[v].out[0];
|
||||
this.out[1] += this.voices[v].out[1];
|
||||
if (this.voices[v].delay) {
|
||||
this.delaysend[0] += this.voices[v].out[0] * this.voices[v].delay;
|
||||
this.delaysend[1] += this.voices[v].out[1] * this.voices[v].delay;
|
||||
this.delaytime = this.voices[v].delaytime; // we trust that these are initialized in the voice
|
||||
this.delayspeed = this.voices[v].delayspeed; // we trust that these are initialized in the voice
|
||||
this.delayfeedback = this.voices[v].delayfeedback;
|
||||
}
|
||||
}
|
||||
// todo: how to change delaytime / delayfeedback from a voice?
|
||||
const delayL = this._delayL.update(this.delaysend[0], this.delaytime, this.delayspeed);
|
||||
const delayR = this._delayR.update(this.delaysend[1], this.delaytime, this.delayspeed);
|
||||
this.delaysend[0] = delayL * this.delayfeedback;
|
||||
this.delaysend[1] = delayR * this.delayfeedback;
|
||||
this.out[0] += delayL;
|
||||
this.out[1] += delayR;
|
||||
this.t++;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
// import _workletUrl from './dough-worklet.mjs?url'; // only for dev (breaks for production build)
|
||||
import _workletUrl from './dough-worklet.mjs?audioworklet'; // only for prod (breaks in development?!)
|
||||
|
||||
export * from './dough.mjs';
|
||||
export const workletUrl = _workletUrl;
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "supradough",
|
||||
"version": "1.2.3",
|
||||
"description": "platform agnostic synth and sampler intended for live coding. a reimplementation of superdough.",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
"publishConfig": {
|
||||
"main": "dist/index.mjs"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tidalcycles/strudel.git"
|
||||
},
|
||||
"keywords": [
|
||||
"tidalcycles",
|
||||
"strudel",
|
||||
"pattern",
|
||||
"livecoding",
|
||||
"algorave"
|
||||
],
|
||||
"author": "Felix Roos <flix91@gmail.com>",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"bugs": {
|
||||
"url": "https://github.com/tidalcycles/strudel/issues"
|
||||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"devDependencies": {
|
||||
"vite": "^6.0.11",
|
||||
"vite-plugin-bundle-audioworklet": "workspace:*",
|
||||
"wav-encoder": "^1.3.0"
|
||||
},
|
||||
"dependencies": {}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/tonal",
|
||||
"version": "1.2.3",
|
||||
"version": "1.2.4",
|
||||
"description": "Tonal functions for strudel",
|
||||
"main": "index.mjs",
|
||||
"publishConfig": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/transpiler",
|
||||
"version": "1.2.3",
|
||||
"version": "1.2.4",
|
||||
"description": "Transpiler for strudel user code. Converts syntactically correct but semantically meaningless JS into evaluatable strudel code.",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/web",
|
||||
"version": "1.2.4",
|
||||
"version": "1.2.5",
|
||||
"description": "Easy to setup, opiniated bundle of Strudel for the browser.",
|
||||
"module": "web.mjs",
|
||||
"publishConfig": {
|
||||
|
||||
@@ -7,4 +7,5 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
export * from './webaudio.mjs';
|
||||
export * from './scope.mjs';
|
||||
export * from './spectrum.mjs';
|
||||
export * from './supradough.mjs';
|
||||
export * from 'superdough';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/webaudio",
|
||||
"version": "1.2.4",
|
||||
"version": "1.2.5",
|
||||
"description": "Web Audio helpers for Strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
@@ -35,7 +35,8 @@
|
||||
"dependencies": {
|
||||
"@strudel/core": "workspace:*",
|
||||
"@strudel/draw": "workspace:*",
|
||||
"superdough": "workspace:*"
|
||||
"superdough": "workspace:*",
|
||||
"supradough": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^6.0.11"
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
import { Pattern } from '@strudel/core';
|
||||
import { connectToDestination, getAudioContext, getWorklet } from 'superdough';
|
||||
|
||||
let doughWorklet;
|
||||
|
||||
function initDoughWorklet() {
|
||||
const ac = getAudioContext();
|
||||
doughWorklet = getWorklet(
|
||||
ac,
|
||||
'dough-processor',
|
||||
{},
|
||||
{
|
||||
outputChannelCount: [2],
|
||||
},
|
||||
);
|
||||
connectToDestination(doughWorklet); // channels?
|
||||
}
|
||||
|
||||
const soundMap = new Map();
|
||||
const loadedSounds = new Map();
|
||||
|
||||
Pattern.prototype.supradough = function () {
|
||||
return this.onTrigger((hap, __, cps, begin) => {
|
||||
hap.value._begin = begin;
|
||||
hap.value._duration = hap.duration / cps;
|
||||
!doughWorklet && initDoughWorklet();
|
||||
const s = (hap.value.bank ? hap.value.bank + '_' : '') + hap.value.s;
|
||||
const n = hap.value.n ?? 0;
|
||||
const soundKey = `${s}:${n}`;
|
||||
if (soundMap.has(s)) {
|
||||
hap.value.s = soundKey; // dough.mjs is unaware of bank and n (only maps keys to buffers)
|
||||
}
|
||||
if (soundMap.has(s) && !loadedSounds.has(soundKey)) {
|
||||
const urls = soundMap.get(s);
|
||||
const url = urls[n % urls.length];
|
||||
console.log(`load ${soundKey} from ${url}`);
|
||||
const loadSample = fetchSample(url);
|
||||
loadedSounds.set(soundKey, loadSample);
|
||||
loadSample.then(({ channels, sampleRate }) =>
|
||||
doughWorklet.port.postMessage({
|
||||
sample: soundKey,
|
||||
channels,
|
||||
sampleRate,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
doughWorklet.port.postMessage({ spawn: hap.value });
|
||||
}, 1);
|
||||
};
|
||||
|
||||
function githubPath(base, subpath = '') {
|
||||
if (!base.startsWith('github:')) {
|
||||
throw new Error('expected "github:" at the start of pseudoUrl');
|
||||
}
|
||||
let [_, path] = base.split('github:');
|
||||
path = path.endsWith('/') ? path.slice(0, -1) : path;
|
||||
if (path.split('/').length === 2) {
|
||||
// assume main as default branch if none set
|
||||
path += '/main';
|
||||
}
|
||||
return `https://raw.githubusercontent.com/${path}/${subpath}`;
|
||||
}
|
||||
export async function fetchSampleMap(url) {
|
||||
if (url.startsWith('github:')) {
|
||||
url = githubPath(url, 'strudel.json');
|
||||
}
|
||||
if (url.startsWith('local:')) {
|
||||
url = `http://localhost:5432`;
|
||||
}
|
||||
if (url.startsWith('shabda:')) {
|
||||
let [_, path] = url.split('shabda:');
|
||||
url = `https://shabda.ndre.gr/${path}.json?strudel=1`;
|
||||
}
|
||||
if (url.startsWith('shabda/speech')) {
|
||||
let [_, path] = url.split('shabda/speech');
|
||||
path = path.startsWith('/') ? path.substring(1) : path;
|
||||
let [params, words] = path.split(':');
|
||||
let gender = 'f';
|
||||
let language = 'en-GB';
|
||||
if (params) {
|
||||
[language, gender] = params.split('/');
|
||||
}
|
||||
url = `https://shabda.ndre.gr/speech/${words}.json?gender=${gender}&language=${language}&strudel=1'`;
|
||||
}
|
||||
if (typeof fetch !== 'function') {
|
||||
// not a browser
|
||||
return;
|
||||
}
|
||||
const base = url.split('/').slice(0, -1).join('/');
|
||||
if (typeof fetch === 'undefined') {
|
||||
// skip fetch when in node / testing
|
||||
return;
|
||||
}
|
||||
const json = await fetch(url)
|
||||
.then((res) => res.json())
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
throw new Error(`error loading "${url}"`);
|
||||
});
|
||||
return [json, json._base || base];
|
||||
}
|
||||
|
||||
// for some reason, only piano and flute work.. is it because mp3??
|
||||
|
||||
async function fetchSample(url) {
|
||||
const buffer = await fetch(url)
|
||||
.then((res) => res.arrayBuffer())
|
||||
.then((buf) => getAudioContext().decodeAudioData(buf));
|
||||
let channels = [];
|
||||
for (let i = 0; i < buffer.numberOfChannels; i++) {
|
||||
channels.push(buffer.getChannelData(i));
|
||||
}
|
||||
return { channels, sampleRate: buffer.sampleRate };
|
||||
}
|
||||
|
||||
export async function doughsamples(sampleMap, baseUrl) {
|
||||
if (typeof sampleMap === 'string') {
|
||||
const [json, base] = await fetchSampleMap(sampleMap);
|
||||
// console.log('json', json, 'base', base);
|
||||
return doughsamples(json, base);
|
||||
}
|
||||
Object.entries(sampleMap).map(async ([key, urls]) => {
|
||||
if (key !== '_base') {
|
||||
urls = urls.map((url) => baseUrl + url);
|
||||
// console.log('set', key, urls);
|
||||
soundMap.set(key, urls);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -5,7 +5,12 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
*/
|
||||
|
||||
import * as strudel from '@strudel/core';
|
||||
import { superdough, getAudioContext, setLogger, doughTrigger } from 'superdough';
|
||||
import { superdough, getAudioContext, setLogger, doughTrigger, registerWorklet } from 'superdough';
|
||||
import './supradough.mjs';
|
||||
import { workletUrl } from 'supradough';
|
||||
|
||||
registerWorklet(workletUrl);
|
||||
|
||||
const { Pattern, logger, repl } = strudel;
|
||||
|
||||
setLogger(logger);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/xen",
|
||||
"version": "1.2.3",
|
||||
"version": "1.2.4",
|
||||
"description": "Xenharmonic API for strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -139,10 +139,10 @@ Tune.prototype.MIDI = function(stepIn,octaveIn) {
|
||||
|
||||
/* Load a new scale */
|
||||
|
||||
Tune.prototype.loadScale = function(name){
|
||||
Tune.prototype.loadScale = function(scale){
|
||||
|
||||
/* load the scale */
|
||||
var freqs = TuningList[name].frequencies
|
||||
var freqs = isArrayOfNumbers(scale) ? scale : TuningList[scale].frequencies
|
||||
this.scale = []
|
||||
for (var i=0;i<freqs.length-1;i++) {
|
||||
this.scale.push(freqs[i]/freqs[0])
|
||||
@@ -207,8 +207,13 @@ Tune.prototype.search = function(letters) {
|
||||
return possible
|
||||
}
|
||||
|
||||
Tune.prototype.isValidScale = function(name) {
|
||||
return !!TuningList[name];
|
||||
function isArrayOfNumbers(arg) {
|
||||
return Array.isArray(arg) && arg.length > 0 && arg.every(item => typeof item === 'number' && !isNaN(item));
|
||||
}
|
||||
|
||||
/* allow an array of values too */
|
||||
Tune.prototype.isValidScale = function(scale) {
|
||||
return !!TuningList[scale] || isArrayOfNumbers(scale) ;
|
||||
}
|
||||
|
||||
/* Return a collection of notes as an array */
|
||||
|
||||
Generated
+33
-10
@@ -201,8 +201,8 @@ importers:
|
||||
specifier: ^6.1.0
|
||||
version: 6.1.0(@codemirror/autocomplete@6.18.4)(@codemirror/commands@6.8.0)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)
|
||||
'@replit/codemirror-vim':
|
||||
specifier: ^6.2.1
|
||||
version: 6.2.1(@codemirror/commands@6.8.0)(@codemirror/language@6.10.8)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)
|
||||
specifier: ^6.3.0
|
||||
version: 6.3.0(@codemirror/commands@6.8.0)(@codemirror/language@6.10.8)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)
|
||||
'@replit/codemirror-vscode-keymap':
|
||||
specifier: ^6.0.2
|
||||
version: 6.0.2(@codemirror/autocomplete@6.18.4)(@codemirror/commands@6.8.0)(@codemirror/language@6.10.8)(@codemirror/lint@6.8.4)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)
|
||||
@@ -517,6 +517,18 @@ importers:
|
||||
specifier: workspace:*
|
||||
version: link:../vite-plugin-bundle-audioworklet
|
||||
|
||||
packages/supradough:
|
||||
devDependencies:
|
||||
vite:
|
||||
specifier: ^6.0.11
|
||||
version: 6.0.11(@types/node@22.10.10)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0)
|
||||
vite-plugin-bundle-audioworklet:
|
||||
specifier: workspace:*
|
||||
version: link:../vite-plugin-bundle-audioworklet
|
||||
wav-encoder:
|
||||
specifier: ^1.3.0
|
||||
version: 1.3.0
|
||||
|
||||
packages/tidal:
|
||||
dependencies:
|
||||
'@strudel/core':
|
||||
@@ -625,6 +637,9 @@ importers:
|
||||
superdough:
|
||||
specifier: workspace:*
|
||||
version: link:../superdough
|
||||
supradough:
|
||||
specifier: workspace:*
|
||||
version: link:../supradough
|
||||
devDependencies:
|
||||
vite:
|
||||
specifier: ^6.0.11
|
||||
@@ -2232,14 +2247,14 @@ packages:
|
||||
'@codemirror/state': ^6.0.1
|
||||
'@codemirror/view': ^6.3.0
|
||||
|
||||
'@replit/codemirror-vim@6.2.1':
|
||||
resolution: {integrity: sha512-qDAcGSHBYU5RrdO//qCmD8K9t6vbP327iCj/iqrkVnjbrpFhrjOt92weGXGHmTNRh16cUtkUZ7Xq7rZf+8HVow==}
|
||||
'@replit/codemirror-vim@6.3.0':
|
||||
resolution: {integrity: sha512-aTx931ULAMuJx6xLf7KQDOL7CxD+Sa05FktTDrtLaSy53uj01ll3Zf17JdKsriER248oS55GBzg0CfCTjEneAQ==}
|
||||
peerDependencies:
|
||||
'@codemirror/commands': ^6.0.0
|
||||
'@codemirror/language': ^6.1.0
|
||||
'@codemirror/search': ^6.2.0
|
||||
'@codemirror/state': ^6.0.1
|
||||
'@codemirror/view': ^6.0.3
|
||||
'@codemirror/commands': 6.x.x
|
||||
'@codemirror/language': 6.x.x
|
||||
'@codemirror/search': 6.x.x
|
||||
'@codemirror/state': 6.x.x
|
||||
'@codemirror/view': 6.x.x
|
||||
|
||||
'@replit/codemirror-vscode-keymap@6.0.2':
|
||||
resolution: {integrity: sha512-j45qTwGxzpsv82lMD/NreGDORFKSctMDVkGRopaP+OrzSzv+pXDQuU3LnFvKpasyjVT0lf+PKG1v2DSCn/vxxg==}
|
||||
@@ -5714,6 +5729,7 @@ packages:
|
||||
node-domexception@1.0.0:
|
||||
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
|
||||
engines: {node: '>=10.5.0'}
|
||||
deprecated: Use your platform's native DOMException instead
|
||||
|
||||
node-fetch-native@1.6.6:
|
||||
resolution: {integrity: sha512-8Mc2HhqPdlIfedsuZoc3yioPuzp6b+L5jRCRY1QzuWZh2EGJVQrGppC6V6cF0bLdbW0+O2YpqCA25aF/1lvipQ==}
|
||||
@@ -6803,6 +6819,7 @@ packages:
|
||||
source-map@0.8.0-beta.0:
|
||||
resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==}
|
||||
engines: {node: '>= 8'}
|
||||
deprecated: The work that was done in this beta branch won't be included in future versions
|
||||
|
||||
sourcemap-codec@1.4.8:
|
||||
resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==}
|
||||
@@ -7535,6 +7552,9 @@ packages:
|
||||
walk-up-path@3.0.1:
|
||||
resolution: {integrity: sha512-9YlCL/ynK3CTlrSRrDxZvUauLzAswPCrsaCgilqFevUYpeEW0/3ScEjaa3kbW/T0ghhkEr7mv+fpjqn1Y1YuTA==}
|
||||
|
||||
wav-encoder@1.3.0:
|
||||
resolution: {integrity: sha512-FXJdEu2qDOI+wbVYZpu21CS1vPEg5NaxNskBr4SaULpOJMrLE6xkH8dECa7PiS+ZoeyvP7GllWUAxPN3AvFSEw==}
|
||||
|
||||
wav@1.0.2:
|
||||
resolution: {integrity: sha512-viHtz3cDd/Tcr/HbNqzQCofKdF6kWUymH9LGDdskfWFoIy/HJ+RTihgjEcHfnsy1PO4e9B+y4HwgTwMrByquhg==}
|
||||
|
||||
@@ -7657,6 +7677,7 @@ packages:
|
||||
|
||||
workbox-google-analytics@7.0.0:
|
||||
resolution: {integrity: sha512-MEYM1JTn/qiC3DbpvP2BVhyIH+dV/5BjHk756u9VbwuAhu0QHyKscTnisQuz21lfRpOwiS9z4XdqeVAKol0bzg==}
|
||||
deprecated: It is not compatible with newer versions of GA starting with v4, as long as you are using GAv3 it should be ok, but the package is not longer being maintained
|
||||
|
||||
workbox-navigation-preload@7.0.0:
|
||||
resolution: {integrity: sha512-juWCSrxo/fiMz3RsvDspeSLGmbgC0U9tKqcUPZBCf35s64wlaLXyn2KdHHXVQrb2cqF7I0Hc9siQalainmnXJA==}
|
||||
@@ -9595,7 +9616,7 @@ snapshots:
|
||||
'@codemirror/state': 6.5.1
|
||||
'@codemirror/view': 6.36.2
|
||||
|
||||
'@replit/codemirror-vim@6.2.1(@codemirror/commands@6.8.0)(@codemirror/language@6.10.8)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)':
|
||||
'@replit/codemirror-vim@6.3.0(@codemirror/commands@6.8.0)(@codemirror/language@6.10.8)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)':
|
||||
dependencies:
|
||||
'@codemirror/commands': 6.8.0
|
||||
'@codemirror/language': 6.10.8
|
||||
@@ -15955,6 +15976,8 @@ snapshots:
|
||||
|
||||
walk-up-path@3.0.1: {}
|
||||
|
||||
wav-encoder@1.3.0: {}
|
||||
|
||||
wav@1.0.2:
|
||||
dependencies:
|
||||
buffer-alloc: 1.2.0
|
||||
|
||||
@@ -1847,6 +1847,27 @@ exports[`runs examples > example "chop" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "chorus" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/4 | note:d s:sawtooth chorus:0.5 ]",
|
||||
"[ 1/4 → 1/2 | note:d s:sawtooth chorus:0.5 ]",
|
||||
"[ 1/2 → 3/4 | note:a# s:sawtooth chorus:0.5 ]",
|
||||
"[ 3/4 → 1/1 | note:a s:sawtooth chorus:0.5 ]",
|
||||
"[ 1/1 → 5/4 | note:d s:sawtooth chorus:0.5 ]",
|
||||
"[ 5/4 → 3/2 | note:d s:sawtooth chorus:0.5 ]",
|
||||
"[ 3/2 → 7/4 | note:a# s:sawtooth chorus:0.5 ]",
|
||||
"[ 7/4 → 2/1 | note:a s:sawtooth chorus:0.5 ]",
|
||||
"[ 2/1 → 9/4 | note:d s:sawtooth chorus:0.5 ]",
|
||||
"[ 9/4 → 5/2 | note:d s:sawtooth chorus:0.5 ]",
|
||||
"[ 5/2 → 11/4 | note:a# s:sawtooth chorus:0.5 ]",
|
||||
"[ 11/4 → 3/1 | note:a s:sawtooth chorus:0.5 ]",
|
||||
"[ 3/1 → 13/4 | note:d s:sawtooth chorus:0.5 ]",
|
||||
"[ 13/4 → 7/2 | note:d s:sawtooth chorus:0.5 ]",
|
||||
"[ 7/2 → 15/4 | note:a# s:sawtooth chorus:0.5 ]",
|
||||
"[ 15/4 → 4/1 | note:a s:sawtooth chorus:0.5 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "chunk" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/4 | note:A4 ]",
|
||||
@@ -2585,6 +2606,52 @@ exports[`runs examples > example "delayfeedback" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "delayfeedback" example index 0 2`] = `
|
||||
[
|
||||
"[ 0/1 → 1/1 | s:bd delay:0.25 delayfeedback:0.25 ]",
|
||||
"[ 1/1 → 2/1 | s:bd delay:0.25 delayfeedback:0.5 ]",
|
||||
"[ 2/1 → 3/1 | s:bd delay:0.25 delayfeedback:0.75 ]",
|
||||
"[ 3/1 → 4/1 | s:bd delay:0.25 delayfeedback:1 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "delayspeed" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]",
|
||||
"[ 1/8 → 1/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]",
|
||||
"[ 1/4 → 3/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]",
|
||||
"[ 3/8 → 1/2 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]",
|
||||
"[ 1/2 → 5/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]",
|
||||
"[ 5/8 → 3/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]",
|
||||
"[ 3/4 → 7/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]",
|
||||
"[ 7/8 → 1/1 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]",
|
||||
"[ 1/1 → 9/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]",
|
||||
"[ 9/8 → 5/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]",
|
||||
"[ 5/4 → 11/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]",
|
||||
"[ 11/8 → 3/2 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]",
|
||||
"[ 3/2 → 13/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]",
|
||||
"[ 13/8 → 7/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]",
|
||||
"[ 7/4 → 15/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]",
|
||||
"[ 15/8 → 2/1 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]",
|
||||
"[ 2/1 → 17/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]",
|
||||
"[ 17/8 → 9/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]",
|
||||
"[ 9/4 → 19/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]",
|
||||
"[ 19/8 → 5/2 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]",
|
||||
"[ 5/2 → 21/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]",
|
||||
"[ 21/8 → 11/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]",
|
||||
"[ 11/4 → 23/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]",
|
||||
"[ 23/8 → 3/1 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]",
|
||||
"[ 3/1 → 25/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]",
|
||||
"[ 25/8 → 13/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]",
|
||||
"[ 13/4 → 27/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]",
|
||||
"[ 27/8 → 7/2 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]",
|
||||
"[ 7/2 → 29/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]",
|
||||
"[ 29/8 → 15/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]",
|
||||
"[ 15/4 → 31/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]",
|
||||
"[ 31/8 → 4/1 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "delaysync" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/2 | s:bd delay:0.25 delaysync:0.125 ]",
|
||||
@@ -2598,19 +2665,6 @@ exports[`runs examples > example "delaysync" example index 0 1`] = `
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "delaytime" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/2 | s:bd delay:0.25 delaytime:0.125 ]",
|
||||
"[ 1/2 → 1/1 | s:bd delay:0.25 delaytime:0.125 ]",
|
||||
"[ 1/1 → 3/2 | s:bd delay:0.25 delaytime:0.25 ]",
|
||||
"[ 3/2 → 2/1 | s:bd delay:0.25 delaytime:0.25 ]",
|
||||
"[ 2/1 → 5/2 | s:bd delay:0.25 delaytime:0.5 ]",
|
||||
"[ 5/2 → 3/1 | s:bd delay:0.25 delaytime:0.5 ]",
|
||||
"[ 3/1 → 7/2 | s:bd delay:0.25 delaytime:1 ]",
|
||||
"[ 7/2 → 4/1 | s:bd delay:0.25 delaytime:1 ]",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`runs examples > example "density" example index 0 1`] = `
|
||||
[
|
||||
"[ 0/1 → 1/4 | s:crackle density:0.01 ]",
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
@@ -103,6 +103,7 @@ export const SIDEBAR: Sidebar = {
|
||||
Understand: [
|
||||
{ text: 'Coding syntax', link: 'learn/code' },
|
||||
{ text: 'Pitch', link: 'understand/pitch' },
|
||||
{ text: 'Xen Harmonic Functions', link: 'learn/xen' },
|
||||
{ text: 'Cycles', link: 'understand/cycles' },
|
||||
{ text: 'Voicings', link: 'understand/voicings' },
|
||||
{ text: 'Pattern Alignment', link: 'technical-manual/alignment' },
|
||||
|
||||
@@ -44,7 +44,7 @@ xxx("foo").yyy("bar")
|
||||
|
||||
Generally, `xxx` and `yyy` are called [_functions_](<https://en.wikipedia.org/wiki/Function_(computer_programming)>), while `foo` and `bar` are called function [_arguments_ or _parameters_](<https://en.wikipedia.org/wiki/Parameter_(computer_programming)>).
|
||||
So far, we've used the functions to declare which aspect of the sound we want to control, and their arguments for the actual data.
|
||||
The `yyy` function is called a [_chained_ function](https://en.wikipedia.org/wiki/Method_chaining), because it is appended with a dot (`.`).
|
||||
The `yyy` function is called a [_chained_ function](https://en.wikipedia.org/wiki/Method_chaining), because it is preceded with a dot (`.`).
|
||||
|
||||
Generally, the idea with chaining is that code such as `a("this").b("that").c("other")` allows `a`, `b` and `c` functions to happen in a specified order, without needing to write them as three separate lines of code.
|
||||
You can think of this as being similar to chaining audio effects together using guitar pedals or digital audio effects.
|
||||
|
||||
@@ -11,6 +11,129 @@ import { JsDoc } from '../../docs/JsDoc';
|
||||
Whether you're using a synth or a sample, you can apply any of the following built-in audio effects.
|
||||
As you might suspect, the effects can be chained together, and they accept a pattern string as their argument.
|
||||
|
||||
# Signal chain
|
||||
|
||||
<img src="/img/strudel-signal-flow.png"></img>
|
||||
|
||||
The signal chain in Strudel is as follows:
|
||||
|
||||
- An sound-generating event is triggered by a pattern
|
||||
- This has a start time and a duration, which is usually
|
||||
controlled by the note length and ADSR parameters
|
||||
- If we exceed the max polyphony, old sounds begin to die off
|
||||
- Muted sounds (one whose `s` value is `-`, `~`, or `_`) are skipped
|
||||
- A sound is produced (through, say, a sample or an oscillator)
|
||||
- This is where detune-based effects (like `detune`, `penv`, etc. occur)
|
||||
- The following will occur _in order_ and only if they've been called in the pattern. Note that all of these are
|
||||
single use effects, meaning that multiple occurrences of them in a pattern will simply override the values
|
||||
(e.g. you can't do `s("bd").lpf(100).distort(2).lpf(800)` to lowpass, distort, and then lowpass
|
||||
again)
|
||||
- Phase vocoder (`stretch`)
|
||||
- Gain is applied (`gain`)
|
||||
- This is where the main (volume) ADSR happens
|
||||
- A lowpass filter (`lpf`)
|
||||
- A highpass filter (`hpf`)
|
||||
- A bandpass filter (`bandpass`)
|
||||
- A vowel filter (`vowel`)
|
||||
- Sample rate reduction (`coarse`)
|
||||
- Bit crushing (`crush`)
|
||||
- Waveshape distortion (`shape`)
|
||||
- Normal distortion (`distort`)
|
||||
- Tremolo (`tremolo`)
|
||||
- Compressor (`compressor`)
|
||||
- Panning (`pan`)
|
||||
- Phaser (`phaser`)
|
||||
- Postgain (`post`)
|
||||
- The sound is then split into multiple destinations
|
||||
- Dry output (amount controlled by `dry` parameter)
|
||||
- The sends
|
||||
- Analyzers
|
||||
- These are used for tooling like `scope` and `spectrum` and their setup usually happens behind the scenes
|
||||
- Delay (amount controlled by `delay` parameter)
|
||||
- Reverb (amount controlled by `room` parameter)
|
||||
- The dry output, delay, and reverb are joined into what is called the "orbit" of the pattern (see more in the section below)
|
||||
- The `duck` effect affects the volume of all signals in the orbit
|
||||
- The orbit is then sent to the mixer
|
||||
|
||||
## Orbits
|
||||
|
||||
Orbits are the way in which outputs are handled in Strudel. They also prescribe which delay and reverb to associate with the dry signal.
|
||||
By default, all orbits are mixed down to channels `1` and `2` in stereo, however with the "Multi Channel Orbits" setting
|
||||
(under Settings at the right) you can use them as individual 2 channel stereo outs (orbit `i` will be mapped to
|
||||
to channels `2i` and `2i + 1`). You can then use routers like Blackhole 16 to retrieve and record all of the channels in a DAW for later processing.
|
||||
|
||||
The default orbit is `1` and it is set with `orbit`. You may send a sound to multiple orbits via mininotation
|
||||
|
||||
<MiniRepl client:visible tune={`s("white").orbit("2,3,4").gain(0.2)`} />
|
||||
|
||||
but please be careful as this will create three copies of the sound behind the scenes, meaning that if they are mixed
|
||||
down to a single output, they will triple the volume. We've reduced the gain here to save your ears.
|
||||
|
||||
⚠️ There is only one delay and reverb per orbit, so please be aware that if you attempt to change the parameters on two
|
||||
patterns pointing to the same orbit, it can lead to unpredictable results. Compare, for example, this pretty pluck
|
||||
with a large reverb:
|
||||
|
||||
<MiniRepl
|
||||
client:visible
|
||||
tune={`
|
||||
$: s("triangle*4").decay(0.5).n(irand(12)).scale('C minor')
|
||||
.room(1).roomsize(10)`}
|
||||
/>
|
||||
|
||||
versus the same pluck with a muted kick drum coming in and overwriting the `roomsize` value:
|
||||
|
||||
<MiniRepl
|
||||
client:visible
|
||||
tune={`
|
||||
$: s("triangle*4").decay(0.5).n(irand(12)).scale('C minor')
|
||||
.room(1).roomsize(10)
|
||||
|
||||
$: s("bd\*4").room(0.01).roomsize(0.01).postgain(0)`}
|
||||
/>
|
||||
|
||||
This is due to them sharing the same orbit: the default of `1`. It can be corrected simply by updating the orbits to be
|
||||
distinct:
|
||||
|
||||
<MiniRepl
|
||||
client:visible
|
||||
tune={`
|
||||
$: s("triangle*4").decay(0.5).n(irand(12)).scale('C minor')
|
||||
.room(1).roomsize(10).orbit(2)
|
||||
|
||||
$: s("bd\*4").room(0.01).roomsize(0.01).postgain(0)`}
|
||||
/>
|
||||
|
||||
## Continuous changes
|
||||
|
||||
As all of the above is triggered by a _sound occurring_, it is often the case that parameters may not be
|
||||
modified continuously in time. For example,
|
||||
|
||||
<MiniRepl
|
||||
client:visible
|
||||
tune={`
|
||||
s("supersaw").lpf(tri.range(100, 5000).slow(2))`}
|
||||
/>
|
||||
|
||||
Will not produce a continually LFO'd low-pass filter due to the `tri` only being sampled every time the note hits
|
||||
(in this case the default of once per cycle). You can fake it by introducing more sound-generating events, e.g.:
|
||||
|
||||
<MiniRepl
|
||||
client:visible
|
||||
tune={`
|
||||
s("supersaw").seg(16).lpf(tri.range(100, 5000).slow(2))`}
|
||||
/>
|
||||
|
||||
Some parameters _do_ induce continuous variations in time, though:
|
||||
|
||||
- The ADSR curve (governed by `attack`, `sustain`, `decay`, `release`)
|
||||
- The pitch envelope curve (governed by `penv` and its associated ADSR)
|
||||
- The FM curve (`fmenv`)
|
||||
- The filter envelopes (`lpenv`, `hpenv`, `bpenv`)
|
||||
- Tremolo (`tremolo`)
|
||||
- Phaser (`phaser`)
|
||||
- Vibrato (`vib`)
|
||||
- Ducking (`duckorbit`)
|
||||
|
||||
# Filters
|
||||
|
||||
Filters are an essential building block of [subtractive synthesis](https://en.wikipedia.org/wiki/Subtractive_synthesis).
|
||||
|
||||
@@ -168,6 +168,20 @@ Using "!" we can repeat without speeding up:
|
||||
|
||||
<MiniRepl client:idle tune={`note("<[g3,b3,e4]!2 [a3,c3,e4] [b3,d3,f#4]>*2")`} punchcard />
|
||||
|
||||
## Randomness
|
||||
|
||||
Events with a "?" placed after them will have a 50% chance of being removed from the pattern:
|
||||
|
||||
<MiniRepl client:idle tune={`note("[g3,b3,e4]*8?")`} punchcard />
|
||||
|
||||
Adding a number between 0 and 1 after the "?" will affect the likelihood of the event being removed. For example, events with "?0.1" placed after them will have a 10% chance of being removed:
|
||||
|
||||
<MiniRepl client:idle tune={`note("[g3,b3,e4]*8?0.1")`} punchcard />
|
||||
|
||||
Events separated by a "|" will be chosen from at random:
|
||||
|
||||
<MiniRepl client:idle tune={`note("[g3,b3,e4] | [a3,c3,e4] | [b3,d3,f#4]")`} punchcard />
|
||||
|
||||
## Mini-notation review
|
||||
|
||||
To recap what we've learned so far, compare the following patterns:
|
||||
@@ -179,6 +193,8 @@ To recap what we've learned so far, compare the following patterns:
|
||||
<MiniRepl client:idle tune={`note("<[g3,b3,e4] _ [a3,c3,e4] [b3,d3,f#4]>*2")`} />
|
||||
<MiniRepl client:idle tune={`note("<[g3,b3,e4]@2 [a3,c3,e4] [b3,d3,f#4]>*2")`} />
|
||||
<MiniRepl client:idle tune={`note("<[g3,b3,e4]!2 [a3,c3,e4] [b3,d3,f#4]>*2")`} />
|
||||
<MiniRepl client:idle tune={`note("<[g3,b3,e4]? [a3,c3,e4] [b3,d3,f#4]>*2")`} />
|
||||
<MiniRepl client:idle tune={`note("<[g3|b3|e4] [a3,c3,e4] [b3,d3,f#4]>*2")`} />
|
||||
|
||||
## Euclidian rhythms
|
||||
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
---
|
||||
title: Xen Harmonic Functions
|
||||
layout: ../../layouts/MainLayout.astro
|
||||
---
|
||||
|
||||
import { MiniRepl } from '../../docs/MiniRepl';
|
||||
import { JsDoc } from '../../docs/JsDoc';
|
||||
|
||||
# Xen Harmonic Functions
|
||||
|
||||
These functions allow the use of scales other than your typical chromatic 12 based ones.
|
||||
|
||||
### tune(scale)
|
||||
|
||||
<JsDoc client:idle name="tune" h={0} />
|
||||
|
||||
Here's an example of how to configure a basic hexany scale:
|
||||
|
||||
<MiniRepl client:idle tune={`"0 1 2 3 4 5".tune("hexany15").mul("220").freq()`} />
|
||||
|
||||
Try other scales like `hexany1`, `iraq`, `gumbeng`, `gunkali`, or `tranh3`
|
||||
|
||||
For a full list of available scales from tunejs, see http://abbernie.github.io/tune/scales.html
|
||||
|
||||
You can set your root to be a particular note with `getFreq`
|
||||
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`"4 8 9 10 - - 5 7 9 11 - -".tune("tranh3")
|
||||
.mul(getFreq('c3'))
|
||||
.freq().clip(.5).room(1)`}
|
||||
/>
|
||||
|
||||
Some tunings become more pronounced with a longer reverb decay:
|
||||
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`"<[5 6 8 10] - [5 7 9 12] -> -".tune("gumbeng")
|
||||
.mul(getFreq('c3'))
|
||||
.freq().clip(.8).room("3:10").rdim(10000).rfade(5)`}
|
||||
/>
|
||||
|
||||
Additionally, you can combo this with `fmap` so that the base note changes:
|
||||
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`"9 11 12 10 - - -".tune("gunkali")
|
||||
.mul("<c3 c3 a3 d#3>".fmap(getFreq))
|
||||
.freq().legato("2 .7").room("1:15").rdim(8500).rlp(14000).rfade(8)`}
|
||||
/>
|
||||
|
||||
Combining this with various polyrhythm tricks can become very evocative:
|
||||
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`"<[0 3 1 -] [-1 4 2 8]> ~ ~,<-4 -5>"
|
||||
.transpose(4)
|
||||
.tune("iraq")
|
||||
.mul("<c3 d3 c#3>".fmap(getFreq))
|
||||
.freq().clip(.5).room(1).rfade(9)`}
|
||||
/>
|
||||
|
||||
Another helpful trick when exploring new tunings is to strum them.
|
||||
Many have a much more enchanting sound that was chosen over many generations of musicians for being strummed.
|
||||
|
||||
Take the `sanza` tuning:
|
||||
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`"4 5 6 7 8 9".tune("sanza")
|
||||
.mul(getFreq('c3'))
|
||||
.freq()`}
|
||||
/>
|
||||
|
||||
Notes 7 and 9 will clash quite a bit if you arp them normally. Many tunings will have this sort of sound, and it can feel distracting on its own.
|
||||
See how close they are on the pitch wheel?
|
||||
|
||||
<MiniRepl client:idle tune={`"[7 9]!3".tune("sanza").mul(getFreq('c3')).freq()._pitchwheel()`} />
|
||||
|
||||
This quality is often due to how the tunings were formed with instruments that were played differently than a piano.
|
||||
As such, some tunings are much better strummed, with the subtle clash of the detuned notes actually making the sound much more magical:
|
||||
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`"[0 1 2 3 4 5 6]@0.3 -"
|
||||
.transpose("<2 5 8 1>")
|
||||
.tune("sanza")
|
||||
.mul(getFreq('c3')).freq()
|
||||
.legato("3").room(1).rfade(5)`}
|
||||
/>
|
||||
|
||||
Note the legato and reverb effects make sure the sound of the strumming gets to wash together. Alternating the direction of the strum can make the
|
||||
tones sound even more alive, too.
|
||||
|
||||
The `tranh3` tuning has a similar set of notes, with two clashing. You might trying plugging that in above and see if you find a favorite strumming pattern.
|
||||
@@ -9,7 +9,9 @@ import { MiniRepl } from '../../docs/MiniRepl';
|
||||
|
||||
{/* The [REPL](https://strudel.cc/) is the place where all packages come together to form a live coding system. It can also be seen as a reference implementation for users of the library. */}
|
||||
|
||||
While Strudel can be used as a library in any JavaScript codebase, its main, reference user interface is the Strudel REPL^[REPL stands for read, evaluate, print/play, loop. It is friendly jargon for an interactive programming interface from computing heritage, usually for a commandline interface but also applied to live coding editors.], which is a browser-based live coding environment. This live code editor is dedicated to manipulating Strudel patterns while they play. The REPL features built-in visual feedback, highlighting which elements in the patterned (mini-notation) sequences are influencing the event that is currently being played. This feedback is designed to support both learning and live use of Strudel.
|
||||
While Strudel can be used as a library in any JavaScript codebase, its main, reference user interface is the Strudel REPL[^1], which is a browser-based live coding environment. This live code editor is dedicated to manipulating Strudel patterns while they play. The REPL features built-in visual feedback, highlighting which elements in the patterned (mini-notation) sequences are influencing the event that is currently being played. This feedback is designed to support both learning and live use of Strudel.
|
||||
|
||||
[^1]: REPL stands for read, evaluate, print/play, loop. It is friendly jargon for an interactive programming interface from computing heritage, usually for a commandline interface but also applied to live coding editors.
|
||||
|
||||
Besides a UI for playback control and meta information, the main part of the REPL interface is the code editor powered by CodeMirror. In it, the user can edit and evaluate pattern code live, using one of the available synthesis outputs to create music and/or sound art. The control flow of the REPL follows 3 basic steps:
|
||||
|
||||
|
||||
@@ -262,14 +262,14 @@ It is quite common that there are many ways to express the same idea.
|
||||
|
||||
**selecting sample numbers separately**
|
||||
|
||||
Instead of using ":", we can also use the `n` function to select sample numbers:
|
||||
|
||||
<MiniRepl client:visible tune={`n("0 1 [4 2] 3*2").sound("jazz")`} punchcard />
|
||||
|
||||
This is shorter and more readable than:
|
||||
Instead of selecting sample numbers one by one:
|
||||
|
||||
<MiniRepl client:visible tune={`sound("jazz:0 jazz:1 [jazz:4 jazz:2] jazz:3*2")`} punchcard />
|
||||
|
||||
We can also use the `n` function to make it shorter and more readable:
|
||||
|
||||
<MiniRepl client:visible tune={`n("0 1 [4 2] 3*2").sound("jazz")`} punchcard />
|
||||
|
||||
## Recap
|
||||
|
||||
Now we've learned the basics of the so called Mini-Notation, the rhythm language of Tidal.
|
||||
|
||||
@@ -72,29 +72,39 @@
|
||||
|
||||
/* Override default styles from the codemirror inline css for autocomplete info tooltip*/
|
||||
.cm-tooltip.cm-completionInfo {
|
||||
padding: 12px !important;
|
||||
padding-bottom: 12px !important;
|
||||
padding: 0 !important;
|
||||
border: 1px solid var(--foreground) !important;
|
||||
border-radius: 4px !important;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3) !important;
|
||||
max-width: 500px !important;
|
||||
min-width: 300px !important;
|
||||
max-height: 400px !important;
|
||||
white-space: normal !important;
|
||||
overflow: auto !important;
|
||||
background-color: var(--lineHighlight) !important;
|
||||
overflow: auto;
|
||||
background: var(--background) !important;
|
||||
}
|
||||
|
||||
/* Main tooltip container */
|
||||
.autocomplete-info-tooltip {
|
||||
.autocomplete-info-container {
|
||||
padding: 12px !important;
|
||||
border-radius: 4px !important;
|
||||
color: var(--foreground);
|
||||
font-family: var(--font-family, 'SF Mono', 'Monaco', monospace);
|
||||
font-size: var(--font-size, 13px);
|
||||
line-height: 1.4;
|
||||
max-width: 600px;
|
||||
max-height: 400px;
|
||||
height: 100%;
|
||||
min-width: 400px;
|
||||
white-space: normal !important;
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
|
||||
.autocomplete-info-tooltip {
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
|
||||
.autocomplete-info-function-description {
|
||||
white-space: pre-wrap !important;
|
||||
}
|
||||
|
||||
.autocomplete-info-function-name {
|
||||
@@ -104,6 +114,13 @@
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.autocomplete-info-function-synonyms {
|
||||
margin: 0 0 12px 0;
|
||||
color: var(--foreground);
|
||||
line-height: 1.5;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.autocomplete-info-function-description {
|
||||
margin: 0 0 12px 0;
|
||||
color: var(--foreground);
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import cx from '@src/cx.mjs';
|
||||
|
||||
export function ActionButton({ children, label, labelIsHidden, className, ...buttonProps }) {
|
||||
return (
|
||||
<button className={cx('hover:opacity-50 text-nowrap w-fit', className)} title={label} {...buttonProps}>
|
||||
{labelIsHidden !== true && label}
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -12,8 +12,8 @@ import { useMemo } from 'react';
|
||||
import { getMetadata } from '../../../metadata_parser.js';
|
||||
import { useExamplePatterns } from '../../useExamplePatterns.jsx';
|
||||
import { parseJSON, isUdels } from '../../util.mjs';
|
||||
import { ButtonGroup } from './Forms.jsx';
|
||||
import { settingsMap, useSettings } from '../../../settings.mjs';
|
||||
import { useSettings } from '../../../settings.mjs';
|
||||
import { ActionButton } from '../button/action-button.jsx';
|
||||
import { Pagination } from '../pagination/Pagination.jsx';
|
||||
import { useState } from 'react';
|
||||
import { useDebounce } from '../usedebounce.jsx';
|
||||
@@ -75,15 +75,6 @@ function PatternButtons({ patterns, activePattern, onClick, started }) {
|
||||
);
|
||||
}
|
||||
|
||||
function ActionButton({ children, onClick, label, labelIsHidden }) {
|
||||
return (
|
||||
<button className="hover:opacity-50 text-nowrap" onClick={onClick} title={label}>
|
||||
{labelIsHidden !== true && label}
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
const updateCodeWindow = (context, patternData, reset = false) => {
|
||||
context.handleUpdate(patternData, reset);
|
||||
};
|
||||
|
||||
@@ -2,9 +2,32 @@ import { useMemo, useState } from 'react';
|
||||
|
||||
import jsdocJson from '../../../../../doc.json';
|
||||
import { Textbox } from '../textbox/Textbox';
|
||||
const availableFunctions = jsdocJson.docs
|
||||
.filter(({ name, description }) => name && !name.startsWith('_') && !!description)
|
||||
.sort((a, b) => /* a.meta.filename.localeCompare(b.meta.filename) + */ a.name.localeCompare(b.name));
|
||||
|
||||
const isValid = ({ name, description }) => name && !name.startsWith('_') && !!description;
|
||||
|
||||
const availableFunctions = (() => {
|
||||
const seen = new Set(); // avoid repetition
|
||||
const functions = [];
|
||||
for (const doc of jsdocJson.docs) {
|
||||
if (!isValid(doc)) continue;
|
||||
functions.push(doc);
|
||||
const synonyms = doc.synonyms || [];
|
||||
for (const s of synonyms) {
|
||||
if (!s || seen.has(s)) continue;
|
||||
seen.add(s);
|
||||
// Swap `doc.name` in for `s` in the list of synonyms
|
||||
const synonymsWithDoc = [doc.name, ...synonyms].filter((x) => x && x !== s);
|
||||
functions.push({
|
||||
...doc,
|
||||
name: s, // update names for the synonym
|
||||
longname: s,
|
||||
synonyms: synonymsWithDoc,
|
||||
synonyms_text: synonymsWithDoc.join(', '),
|
||||
});
|
||||
}
|
||||
}
|
||||
return functions.sort((a, b) => /* a.meta.filename.localeCompare(b.meta.filename) + */ a.name.localeCompare(b.name));
|
||||
})();
|
||||
|
||||
const getInnerText = (html) => {
|
||||
var div = document.createElement('div');
|
||||
|
||||
@@ -312,7 +312,7 @@ export function SettingsTab({ started }) {
|
||||
confirmDialog('Sure?').then((r) => {
|
||||
if (r) {
|
||||
const { userPatterns } = settingsMap.get(); // keep current patterns
|
||||
settingsMap.set({...defaultSettings, userPatterns});
|
||||
settingsMap.set({ ...defaultSettings, userPatterns });
|
||||
}
|
||||
});
|
||||
}}
|
||||
|
||||
@@ -2,16 +2,21 @@ import useEvent from '@src/useEvent.mjs';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { getAudioContext, soundMap, connectToDestination } from '@strudel/webaudio';
|
||||
import { useMemo, useRef, useState } from 'react';
|
||||
import { settingsMap, useSettings } from '../../../settings.mjs';
|
||||
import { settingsMap, soundFilterType, useSettings } from '../../../settings.mjs';
|
||||
import { ButtonGroup } from './Forms.jsx';
|
||||
import ImportSoundsButton from './ImportSoundsButton.jsx';
|
||||
import { Textbox } from '../textbox/Textbox.jsx';
|
||||
import { ActionButton } from '../button/action-button.jsx';
|
||||
import { confirmDialog } from '@src/repl/util.mjs';
|
||||
import { clearIDB, userSamplesDBConfig } from '@src/repl/idbutils.mjs';
|
||||
import { prebake } from '@src/repl/prebake.mjs';
|
||||
|
||||
const getSamples = (samples) =>
|
||||
Array.isArray(samples) ? samples.length : typeof samples === 'object' ? Object.values(samples).length : 1;
|
||||
|
||||
export function SoundsTab() {
|
||||
const sounds = useStore(soundMap);
|
||||
|
||||
const { soundsFilter } = useSettings();
|
||||
const [search, setSearch] = useState('');
|
||||
const { BASE_URL } = import.meta.env;
|
||||
@@ -27,18 +32,19 @@ export function SoundsTab() {
|
||||
.sort((a, b) => a[0].localeCompare(b[0]))
|
||||
.filter(([name]) => name.toLowerCase().includes(search.toLowerCase()));
|
||||
|
||||
if (soundsFilter === 'user') {
|
||||
if (soundsFilter === soundFilterType.USER) {
|
||||
return filtered.filter(([_, { data }]) => !data.prebake);
|
||||
}
|
||||
if (soundsFilter === 'drums') {
|
||||
if (soundsFilter === soundFilterType.DRUMS) {
|
||||
return filtered.filter(([_, { data }]) => data.type === 'sample' && data.tag === 'drum-machines');
|
||||
}
|
||||
if (soundsFilter === 'samples') {
|
||||
if (soundsFilter === soundFilterType.SAMPLES) {
|
||||
return filtered.filter(([_, { data }]) => data.type === 'sample' && data.tag !== 'drum-machines');
|
||||
}
|
||||
if (soundsFilter === 'synths') {
|
||||
if (soundsFilter === soundFilterType.SYNTHS) {
|
||||
return filtered.filter(([_, { data }]) => ['synth', 'soundfont'].includes(data.type));
|
||||
}
|
||||
//TODO: tidy this up, it does not need to be saved in settings
|
||||
if (soundsFilter === 'importSounds') {
|
||||
return [];
|
||||
}
|
||||
@@ -57,10 +63,10 @@ export function SoundsTab() {
|
||||
});
|
||||
});
|
||||
return (
|
||||
<div id="sounds-tab" className="px-4 flex flex-col w-full h-full text-foreground">
|
||||
<div id="sounds-tab" className="px-4 flex gap-2 flex-col w-full h-full text-foreground">
|
||||
<Textbox placeholder="Search" value={search} onChange={(v) => setSearch(v)} />
|
||||
|
||||
<div className="pb-2 flex shrink-0 flex-wrap">
|
||||
<div className=" flex shrink-0 flex-wrap">
|
||||
<ButtonGroup
|
||||
value={soundsFilter}
|
||||
onChange={(value) => settingsMap.setKey('soundsFilter', value)}
|
||||
@@ -74,7 +80,26 @@ export function SoundsTab() {
|
||||
></ButtonGroup>
|
||||
</div>
|
||||
|
||||
<div className="min-h-0 max-h-full grow overflow-auto text-sm break-normal pb-2">
|
||||
{soundsFilter === soundFilterType.USER && soundEntries.length > 0 && (
|
||||
<ActionButton
|
||||
className="pl-2"
|
||||
label="delete-all"
|
||||
onClick={async () => {
|
||||
try {
|
||||
const confirmed = await confirmDialog('Delete all imported user samples?');
|
||||
if (confirmed) {
|
||||
clearIDB(userSamplesDBConfig.dbName);
|
||||
soundMap.set({});
|
||||
await prebake();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="min-h-0 max-h-full grow overflow-auto text-sm break-normal bg-background p-2 rounded-md">
|
||||
{soundEntries.map(([name, { data, onTrigger }]) => {
|
||||
return (
|
||||
<span
|
||||
@@ -151,9 +176,7 @@ export function SoundsTab() {
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
{!soundEntries.length && soundsFilter !== 'importSounds'
|
||||
? 'No custom sounds loaded in this pattern (yet).'
|
||||
: ''}
|
||||
{!soundEntries.length && soundsFilter !== 'importSounds' ? 'No sounds loaded' : ''}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -12,17 +12,21 @@ export const userSamplesDBConfig = {
|
||||
};
|
||||
|
||||
// deletes all of the databases, useful for debugging
|
||||
function clearIDB() {
|
||||
function clearAllIDB() {
|
||||
window.indexedDB
|
||||
.databases()
|
||||
.then((r) => {
|
||||
for (var i = 0; i < r.length; i++) window.indexedDB.deleteDatabase(r[i].name);
|
||||
for (var i = 0; i < r.length; i++) clearIDB(r[i].name);
|
||||
})
|
||||
.then(() => {
|
||||
alert('All data cleared.');
|
||||
});
|
||||
}
|
||||
|
||||
export function clearIDB(dbName) {
|
||||
return window.indexedDB.deleteDatabase(dbName);
|
||||
}
|
||||
|
||||
// queries the DB, and registers the sounds so they can be played
|
||||
export function registerSamplesFromDB(config = userSamplesDBConfig, onComplete = () => {}) {
|
||||
openDB(config, (objectStore) => {
|
||||
|
||||
@@ -8,6 +8,14 @@ export const audioEngineTargets = {
|
||||
osc: 'osc',
|
||||
};
|
||||
|
||||
export const soundFilterType = {
|
||||
USER: 'user',
|
||||
DRUMS: 'drums',
|
||||
SAMPLES: 'samples',
|
||||
SYNTHS: 'synths',
|
||||
ALL: 'all',
|
||||
};
|
||||
|
||||
export const defaultSettings = {
|
||||
activeFooter: 'intro',
|
||||
keybindings: 'codemirror',
|
||||
@@ -28,7 +36,7 @@ export const defaultSettings = {
|
||||
fontSize: 18,
|
||||
latestCode: '',
|
||||
isZen: false,
|
||||
soundsFilter: 'all',
|
||||
soundsFilter: soundFilterType.ALL,
|
||||
patternFilter: 'community',
|
||||
// panelPosition: window.innerWidth > 1000 ? 'right' : 'bottom', //FIX: does not work on astro
|
||||
panelPosition: 'right',
|
||||
|
||||
Reference in New Issue
Block a user