Merge branch 'main' into glossing/scale-for-notes

This commit is contained in:
Aria 2025-08-21 15:58:43 -05:00
commit 938c92fda2
49 changed files with 857 additions and 194 deletions

View file

@ -83,4 +83,14 @@ export default [
], ],
}, },
}, },
{
// Properties provided by AudioWorkletGlobalScope
files: ['packages/superdough/worklets.mjs'],
languageOptions: {
globals: {
currentTime: 'readonly',
sampleRate: 'readonly',
},
},
},
]; ];

View file

@ -1,68 +1,91 @@
import jsdoc from '../../doc.json'; import jsdoc from '../../doc.json';
// import { javascriptLanguage } from '@codemirror/lang-javascript';
import { autocompletion } from '@codemirror/autocomplete'; import { autocompletion } from '@codemirror/autocomplete';
import { h } from './html'; import { h } from './html';
function plaintext(str) { const escapeHtml = (str) => {
const div = document.createElement('div'); const div = document.createElement('div');
div.innerText = str; div.innerText = str;
return div.innerHTML; return div.innerHTML;
} };
const getDocLabel = (doc) => doc.name || doc.longname; const stripHtml = (html) => {
const getInnerText = (html) => { const div = document.createElement('div');
var div = document.createElement('div');
div.innerHTML = html; div.innerHTML = html;
return div.textContent || div.innerText || ''; return div.textContent || div.innerText || '';
}; };
export function Autocomplete({ doc, label }) { const getDocLabel = (doc) => doc.name || doc.longname;
return h`<div class="prose dark:prose-invert max-h-[400px] overflow-auto p-2">
<h1 class="pt-0 mt-0">${label || getDocLabel(doc)}</h1> const buildParamsList = (params) =>
${doc.description} params?.length
<ul> ? `
${doc.params?.map( <div class="autocomplete-info-params-section">
({ name, type, description }) => <h4 class="autocomplete-info-section-title">Parameters</h4>
`<li>${name} : ${type.names?.join(' | ')} ${description ? ` - ${getInnerText(description)}` : ''}</li>`, <ul class="autocomplete-info-params-list">
)} ${params
</ul> .map(
<div> ({ name, type, description }) => `
${doc.examples?.map((example) => `<div><pre>${plaintext(example)}</pre></div>`)} <li class="autocomplete-info-param-item">
</div> <span class="autocomplete-info-param-name">${name}</span>
</div>`[0]; <span class="autocomplete-info-param-type">${type.names?.join(' | ')}</span>
/* ${description ? `<div class="autocomplete-info-param-desc">${stripHtml(description)}</div>` : ''}
<pre </li>
className="cursor-pointer" `,
onMouseDown={(e) => { )
console.log('ola!'); .join('')}
navigator.clipboard.writeText(example); </ul>
e.stopPropagation(); </div>
}} `
> : '';
{example}
</pre> const buildExamples = (examples) =>
*/ examples?.length
} ? `
<div class="autocomplete-info-examples-section">
<h4 class="autocomplete-info-section-title">Examples</h4>
${examples
.map(
(example) => `
<pre class="autocomplete-info-example-code">${escapeHtml(example)}</pre>
`,
)
.join('')}
</div>
`
: '';
export const Autocomplete = ({ doc, label }) =>
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>
`[0];
const isValidDoc = (doc) => {
const label = getDocLabel(doc);
return label && !label.startsWith('_') && !['package'].includes(doc.kind);
};
const hasExcludedTags = (doc) =>
['superdirtOnly', 'noAutocomplete'].some((tag) => doc.tags?.find((t) => t.originalTitle === tag));
const jsdocCompletions = jsdoc.docs const jsdocCompletions = jsdoc.docs
.filter( .filter((doc) => isValidDoc(doc) && !hasExcludedTags(doc))
(doc) =>
getDocLabel(doc) &&
!getDocLabel(doc).startsWith('_') &&
!['package'].includes(doc.kind) &&
!['superdirtOnly', 'noAutocomplete'].some((tag) => doc.tags?.find((t) => t.originalTitle === tag)),
)
// https://codemirror.net/docs/ref/#autocomplete.Completion // https://codemirror.net/docs/ref/#autocomplete.Completion
.map((doc) /*: Completion */ => ({ .map((doc) => ({
label: getDocLabel(doc), label: getDocLabel(doc),
// detail: 'xxx', // An optional short piece of information to show (with a different style) after the label. // detail: 'xxx', // An optional short piece of information to show (with a different style) after the label.
info: () => Autocomplete({ doc }), info: () => Autocomplete({ doc }),
type: 'function', // https://codemirror.net/docs/ref/#autocomplete.Completion.type type: 'function', // https://codemirror.net/docs/ref/#autocomplete.Completion.type
})); }));
export const strudelAutocomplete = (context /* : CompletionContext */) => { export const strudelAutocomplete = (context) => {
let word = context.matchBefore(/\w*/); const word = context.matchBefore(/\w*/);
if (word.from == word.to && !context.explicit) return null; if (word.from === word.to && !context.explicit) return null;
return { return {
from: word.from, from: word.from,
options: jsdocCompletions, options: jsdocCompletions,
@ -74,11 +97,5 @@ export const strudelAutocomplete = (context /* : CompletionContext */) => {
}; };
}; };
export function isAutoCompletionEnabled(on) { export const isAutoCompletionEnabled = (enabled) =>
return on enabled ? [autocompletion({ override: [strudelAutocomplete] })] : [];
? [
autocompletion({ override: [strudelAutocomplete] }),
//javascriptLanguage.data.of({ autocomplete: strudelAutocomplete }),
]
: []; // autocompletion({ override: [] })
}

View file

@ -1,6 +1,6 @@
{ {
"name": "@strudel/codemirror", "name": "@strudel/codemirror",
"version": "1.2.2", "version": "1.2.3",
"description": "Codemirror Extensions for Strudel", "description": "Codemirror Extensions for Strudel",
"main": "index.mjs", "main": "index.mjs",
"publishConfig": { "publishConfig": {

View file

@ -541,28 +541,40 @@ export const { tremoloshape } = registerControl('tremoloshape', 'tremshape');
* *
*/ */
// TODO: SUPRADOUGH implement post orbit "pump" sidechain effect /**
// /** * modulate the amplitude of an orbit to create a "sidechain" like effect
// * modulate the amplitude of an orbit to create a "sidechain" like effect *
// * * @name duckorbit
// * @name pump * @param {number | Pattern} orbit target orbit
// * @param {number | Pattern} speed modulation speed in cycles * @example
// * @example * $: n(run(16)).scale("c:minor:pentatonic").s("sawtooth").delay(.7).orbit(2)
// * note("{f g c d}%16").s("sawtooth").pump(".25:.75") * $: s("bd:4!4").beat("0,4,8,11,14",16).duckorbit(2).duckattack(0.2).duckdepth(1)
// * *
// */ */
// export const { pump } = registerControl(['pump', 'pumpdepth']); export const { duck } = registerControl('duckorbit', 'duck');
// /** /**
// * modulate the amplitude of an orbit to create a "sidechain" like effect * the amount of ducking applied to target orbit
// * *
// * @name pumpdepth * @name duckdepth
// * @param {number | Pattern} depth depth of modulation from 0 to 1 * @param {number | Pattern} depth depth of modulation from 0 to 1
// * @example * @example
// * note("{f g c d}%16").s("sawtooth").pump(".25").depth("<.25 .5 .75 1>") * 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).duckdepth("<1 .9 .6 0>"))
// * *
// */ */
// export const { pumpdepth } = registerControl('pumpdepth');
export const { duckdepth } = registerControl('duckdepth');
/**
* the attack time of the duck effect
*
* @name duckattack
* @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))
*
*/
export const { duckattack } = registerControl('duckattack', 'duckatt');
export const { drive } = registerControl('drive'); export const { drive } = registerControl('drive');

View file

@ -10,7 +10,7 @@ https://rohandrape.net/?t=hmt
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { timeCat, register, silence } from './pattern.mjs'; import { timeCat, register, silence, stack, pure, _morph } from './pattern.mjs';
import { rotate, flatten, splitAt, zipWith } from './util.mjs'; import { rotate, flatten, splitAt, zipWith } from './util.mjs';
import Fraction, { lcm } from './fraction.mjs'; import Fraction, { lcm } from './fraction.mjs';
@ -196,3 +196,26 @@ export const euclidLegato = register(['euclidLegato'], function (pulses, steps,
export const euclidLegatoRot = register(['euclidLegatoRot'], function (pulses, steps, rotation, pat) { export const euclidLegatoRot = register(['euclidLegatoRot'], function (pulses, steps, rotation, pat) {
return _euclidLegato(pulses, steps, rotation, pat); return _euclidLegato(pulses, steps, rotation, pat);
}); });
/**
* A 'euclid' variant with an additional parameter that morphs the resulting
* rhythm from 0 (no morphing) to 1 (completely 'even'). For example
* `sound("bd").euclidish(3,8,0)` would be the same as
* `sound("bd").euclid(3,8)`, and `sound("bd").euclidish(3,8,1)` would be the
* same as `sound("bd bd bd")`. `sound("bd").euclidish(3,8,0.5)` would have a
* groove somewhere between.
* Inspired by the work of Malcom Braff.
* @name euclidish
* @synonyms eish
* @memberof Pattern
* @param {number} pulses the number of onsets
* @param {number} steps the number of steps to fill
* @param {number} groove exists between the extremes of 0 (straight euclidian) and 1 (straight pulse)
* @example
* sound("hh").euclidish(7,12,sine.slow(8))
* .pan(sine.slow(8))
*/
export const { euclidish, eish } = register(['euclidish', 'eish'], function (pulses, steps, perc, pat) {
const morphed = _morph(bjork(pulses, steps), new Array(pulses).fill(1), perc);
return pat.struct(morphed).setSteps(steps);
});

View file

@ -4,6 +4,7 @@ Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import Fraction from './fraction.mjs'; import Fraction from './fraction.mjs';
import { stringifyValues } from './util.mjs';
export class Hap { export class Hap {
/* /*
@ -148,13 +149,7 @@ export class Hap {
} }
showWhole(compact = false) { showWhole(compact = false) {
return `${this.whole == undefined ? '~' : this.whole.show()}: ${ return `${this.whole == undefined ? '~' : this.whole.show()}: ${stringifyValues(this.value, compact)}`;
typeof this.value === 'object'
? compact
? JSON.stringify(this.value).slice(1, -1).replaceAll('"', '').replaceAll(',', ' ')
: JSON.stringify(this.value)
: this.value
}`;
} }
combineContext(b) { combineContext(b) {

View file

@ -6,7 +6,7 @@ let debounce = 1000,
export function errorLogger(e, origin = 'cyclist') { export function errorLogger(e, origin = 'cyclist') {
//TODO: add some kind of debug flag that enables this while in dev mode //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}`); logger(`[${origin}] error: ${e.message}`);
} }

View file

@ -1,6 +1,6 @@
{ {
"name": "@strudel/core", "name": "@strudel/core",
"version": "1.2.2", "version": "1.2.3",
"description": "Port of Tidal Cycles to JavaScript", "description": "Port of Tidal Cycles to JavaScript",
"main": "index.mjs", "main": "index.mjs",
"type": "module", "type": "module",

View file

@ -21,6 +21,8 @@ import {
numeralArgs, numeralArgs,
parseNumeral, parseNumeral,
pairs, pairs,
zipWith,
stringifyValues,
} from './util.mjs'; } from './util.mjs';
import drawLine from './drawLine.mjs'; import drawLine from './drawLine.mjs';
import { logger } from './logger.mjs'; import { logger } from './logger.mjs';
@ -852,14 +854,29 @@ export class Pattern {
); );
} }
log(func = (_, hap) => `[hap] ${hap.showWhole(true)}`, getData = (_, hap) => ({ hap })) { /**
* Writes the content of the current event to the console (visible in the side menu).
* @name log
* @memberof Pattern
* @example
* s("bd sd").log()
*/
log(func = (hap) => `[hap] ${hap.showWhole(true)}`, getData = (hap) => ({ hap })) {
return this.onTrigger((...args) => { return this.onTrigger((...args) => {
logger(func(...args), undefined, getData(...args)); logger(func(...args), undefined, getData(...args));
}, false); }, false);
} }
logValues(func = id) { /**
return this.log((_, hap) => func(hap.value)); * A simplified version of `log` which writes all "values" (various configurable parameters)
* within the event to the console (visible in the side menu).
* @name logValues
* @memberof Pattern
* @example
* s("bd sd").gain("0.25 0.5 1").n("2 1 0").logValues()
*/
logValues(func = (value) => `[hap] ${stringifyValues(value, true)}`) {
return this.log((hap) => func(hap.value));
} }
////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////
@ -3400,3 +3417,69 @@ export const { beat } = register(
['beat'], ['beat'],
__beat((x) => x.innerJoin()), __beat((x) => x.innerJoin()),
); );
export const _morph = (from, to, by) => {
by = Fraction(by);
const dur = Fraction(1).div(from.length);
const positions = (list) => {
const result = [];
for (const [pos, value] of list.entries()) {
if (value) {
result.push([Fraction(pos).div(list.length), value]);
}
}
return result;
};
const arcs = zipWith(
([posa, valuea], [posb, valueb]) => {
const b = by.mul(posb - posa).add(posa);
const e = b.add(dur);
return new TimeSpan(b, e);
},
positions(from),
positions(to),
);
function query(state) {
const cycle = state.span.begin.sam();
const cycleArc = state.span.cycleArc();
const result = [];
for (const whole of arcs) {
const part = whole.intersection(cycleArc);
if (part !== undefined) {
result.push(
new Hap(
whole.withTime((x) => x.add(cycle)),
part.withTime((x) => x.add(cycle)),
true,
),
);
}
}
return result;
}
return new Pattern(query).splitQueries();
};
/**
* Takes two binary rhythms represented as lists of 1s and 0s, and a number
* between 0 and 1 that morphs between them. The two lists should contain the same
* number of true values.
* @example
* sound("hh").struct(morph([1,0,1,0,1,0,1,0], // straight rhythm
* [1,1,0,1,0,1,0], // wonky rhythm
* 0.25 // creates a slightly wonky rhythm
* )
* )
* @example
* sound("hh").struct(morph("1:0:1:0:1:0:1:0", // straight rhythm
* "1:1:0:1:0:1:0", // wonky rhythm
* sine.slow(8) // slowly morph between the rhythms
* )
* )
*/
export const morph = (frompat, topat, bypat) => {
frompat = reify(frompat);
topat = reify(topat);
bypat = reify(bypat);
return frompat.innerBind((from) => topat.innerBind((to) => bypat.innerBind((by) => _morph(from, to, by))));
};

View file

@ -6,7 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th
import Fraction from 'fraction.js'; import Fraction from 'fraction.js';
import { describe, it, expect } from 'vitest'; import { describe, it, expect, vi } from 'vitest';
import { import {
TimeSpan, TimeSpan,
@ -55,6 +55,8 @@ import {
expand, expand,
} from '../index.mjs'; } from '../index.mjs';
import { log, logValues } from '../pattern.mjs';
import { steady } from '../signal.mjs'; import { steady } from '../signal.mjs';
import { n, s } from '../controls.mjs'; import { n, s } from '../controls.mjs';
@ -1306,4 +1308,40 @@ describe('Pattern', () => {
); );
}); });
}); });
describe('log', () => {
it('logs to console', () => {
const mockConsoleLog = vi.spyOn(console, 'log').mockImplementation(() => {});
const pattern = pure('a').log();
const haps = pattern.queryArc(0, 1);
// Force a trigger
haps.forEach((hap) => {
hap.context?.onTrigger?.(hap);
});
expect(mockConsoleLog).toHaveBeenCalledWith(
'%c[hap] 0/1 → 1/1: a',
'background-color: black;color:white;border-radius:15px',
);
mockConsoleLog.mockRestore();
});
});
describe('logValues', () => {
it('logs values to console', () => {
const mockConsoleLog = vi.spyOn(console, 'log').mockImplementation(() => {});
const pattern = pure('a').note('c#').logValues();
const haps = pattern.queryArc(0, 1);
// Force a trigger
haps.forEach((hap) => {
hap.context?.onTrigger?.(hap);
});
expect(mockConsoleLog).toHaveBeenCalledWith(
'%c[hap] value:a note:c#',
'background-color: black;color:white;border-radius:15px',
);
mockConsoleLog.mockRestore();
});
});
}); });

View file

@ -72,7 +72,7 @@ export class TimeSpan {
} }
intersection(other) { intersection(other) {
// Intersection of two timespans, returns None if they don't intersect. // Intersection of two timespans, returns undefined if they don't intersect.
const intersect_begin = this.begin.max(other.begin); const intersect_begin = this.begin.max(other.begin);
const intersect_end = this.end.min(other.end); const intersect_end = this.end.min(other.end);

View file

@ -487,3 +487,13 @@ export function getCurrentKeyboardState() {
// } // }
// return lcm((x * y) / gcd(x, y), ...z); // return lcm((x * y) / gcd(x, y), ...z);
// }; // };
// Takes values -- typically derived from events, i.e. `hap`s -- and renders them
// into a readable format
export function stringifyValues(value, compact = false) {
return typeof value === 'object'
? compact
? JSON.stringify(value).slice(1, -1).replaceAll('"', '').replaceAll(',', ' ')
: JSON.stringify(value)
: value;
}

View file

@ -1,6 +1,6 @@
{ {
"name": "@strudel/csound", "name": "@strudel/csound",
"version": "1.2.3", "version": "1.2.4",
"description": "csound bindings for strudel", "description": "csound bindings for strudel",
"main": "index.mjs", "main": "index.mjs",
"type": "module", "type": "module",

View file

@ -1,6 +1,6 @@
{ {
"name": "@strudel/draw", "name": "@strudel/draw",
"version": "1.2.2", "version": "1.2.3",
"description": "Helpers for drawing with Strudel", "description": "Helpers for drawing with Strudel",
"main": "index.mjs", "main": "index.mjs",
"type": "module", "type": "module",

View file

@ -1,6 +1,6 @@
{ {
"name": "@strudel/embed", "name": "@strudel/embed",
"version": "1.1.0", "version": "1.1.1",
"description": "Embeddable Web Component to load a Strudel REPL into an iframe", "description": "Embeddable Web Component to load a Strudel REPL into an iframe",
"main": "embed.js", "main": "embed.js",
"type": "module", "type": "module",

View file

@ -1,6 +1,6 @@
{ {
"name": "@strudel/gamepad", "name": "@strudel/gamepad",
"version": "1.2.2", "version": "1.2.3",
"description": "Gamepad Inputs for strudel", "description": "Gamepad Inputs for strudel",
"main": "index.mjs", "main": "index.mjs",
"type": "module", "type": "module",

View file

@ -1,6 +1,6 @@
{ {
"name": "@strudel/hydra", "name": "@strudel/hydra",
"version": "1.2.2", "version": "1.2.3",
"description": "Hydra integration for strudel", "description": "Hydra integration for strudel",
"main": "hydra.mjs", "main": "hydra.mjs",
"type": "module", "type": "module",

View file

@ -493,6 +493,9 @@ export async function midin(input) {
otherInputs?.length ? `Also available: ${getMidiDeviceNamesString(otherInputs)}` : '' otherInputs?.length ? `Also available: ${getMidiDeviceNamesString(otherInputs)}` : ''
}`, }`,
); );
}
// ensure refs for this input are initialized
if (!refs[input]) {
refs[input] = {}; refs[input] = {};
} }
const cc = (cc) => ref(() => refs[input][cc] || 0); const cc = (cc) => ref(() => refs[input][cc] || 0);

View file

@ -1,6 +1,6 @@
{ {
"name": "@strudel/midi", "name": "@strudel/midi",
"version": "1.2.3", "version": "1.2.4",
"description": "Midi API for strudel", "description": "Midi API for strudel",
"main": "index.mjs", "main": "index.mjs",
"type": "module", "type": "module",

View file

@ -1,6 +1,6 @@
{ {
"name": "@strudel/mini", "name": "@strudel/mini",
"version": "1.2.2", "version": "1.2.3",
"description": "Mini notation for strudel", "description": "Mini notation for strudel",
"main": "index.mjs", "main": "index.mjs",
"type": "module", "type": "module",

View file

@ -1,6 +1,6 @@
{ {
"name": "mondolang", "name": "mondolang",
"version": "1.1.0", "version": "1.1.1",
"description": "a language for functional composition that translates to js", "description": "a language for functional composition that translates to js",
"main": "mondo.mjs", "main": "mondo.mjs",
"type": "module", "type": "module",

View file

@ -1,6 +1,6 @@
{ {
"name": "@strudel/mondo", "name": "@strudel/mondo",
"version": "1.1.0", "version": "1.1.1",
"description": "mondo notation for strudel", "description": "mondo notation for strudel",
"main": "mondough.mjs", "main": "mondough.mjs",
"type": "module", "type": "module",

View file

@ -1,6 +1,6 @@
{ {
"name": "@strudel/motion", "name": "@strudel/motion",
"version": "1.2.2", "version": "1.2.3",
"description": "DeviceMotion API for strudel", "description": "DeviceMotion API for strudel",
"main": "index.mjs", "main": "index.mjs",
"type": "module", "type": "module",

View file

@ -1,6 +1,6 @@
{ {
"name": "@strudel/mqtt", "name": "@strudel/mqtt",
"version": "1.2.2", "version": "1.2.3",
"description": "MQTT API for strudel", "description": "MQTT API for strudel",
"main": "mqtt.mjs", "main": "mqtt.mjs",
"type": "module", "type": "module",

View file

@ -1,6 +1,6 @@
{ {
"name": "@strudel/osc", "name": "@strudel/osc",
"version": "1.2.2", "version": "1.2.3",
"description": "OSC messaging for strudel", "description": "OSC messaging for strudel",
"main": "osc.mjs", "main": "osc.mjs",
"type": "module", "type": "module",

View file

@ -1,6 +1,6 @@
{ {
"name": "@strudel/reference", "name": "@strudel/reference",
"version": "1.2.0", "version": "1.2.1",
"description": "Headless reference of all strudel functions", "description": "Headless reference of all strudel functions",
"main": "index.mjs", "main": "index.mjs",
"type": "module", "type": "module",

View file

@ -1,6 +1,6 @@
{ {
"name": "@strudel/repl", "name": "@strudel/repl",
"version": "1.2.3", "version": "1.2.4",
"description": "Strudel REPL as a Web Component", "description": "Strudel REPL as a Web Component",
"module": "index.mjs", "module": "index.mjs",
"publishConfig": { "publishConfig": {

View file

@ -20,3 +20,13 @@ samples('http://localhost:5432')
LOG=1 npx @strudel/sampler # adds logging LOG=1 npx @strudel/sampler # adds logging
PORT=5555 npx @strudel/sampler # changes port PORT=5555 npx @strudel/sampler # changes port
``` ```
## static json
when running with `--json`, you will simply get the json logged back:
```sh
npx --yes @strudel/sampler --json > strudel.json
```
this is useful if you want to create a sample pack from the current folder.

View file

@ -1,6 +1,6 @@
{ {
"name": "@strudel/sampler", "name": "@strudel/sampler",
"version": "0.2.0", "version": "0.2.2",
"description": "", "description": "",
"keywords": [ "keywords": [
"tidalcycles", "tidalcycles",

View file

@ -10,14 +10,6 @@ import os from 'os';
// eslint-disable-next-line // eslint-disable-next-line
const LOG = !!process.env.LOG || false; const LOG = !!process.env.LOG || false;
console.log(
cowsay.say({
text: 'welcome to @strudel/sampler',
e: 'oO',
T: 'U ',
}),
);
async function getFilesInDirectory(directory) { async function getFilesInDirectory(directory) {
let files = []; let files = [];
const dirents = await readdir(directory, { withFileTypes: true }); const dirents = await readdir(directory, { withFileTypes: true });
@ -60,8 +52,26 @@ async function getBanks(directory) {
return { banks, files }; return { banks, files };
} }
const args = process.argv.slice(2);
// eslint-disable-next-line // eslint-disable-next-line
const directory = process.cwd(); const directory = process.cwd();
if (args.includes('--json')) {
const { banks, files } = await getBanks(directory);
const json = JSON.stringify(banks);
console.log(json);
process.exit(0);
}
console.log(
cowsay.say({
text: 'welcome to @strudel/sampler',
e: 'oO',
T: 'U ',
}),
);
const server = http.createServer(async (req, res) => { const server = http.createServer(async (req, res) => {
res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Origin', '*');
const { banks, files } = await getBanks(directory); const { banks, files } = await getBanks(directory);

View file

@ -1,6 +1,6 @@
{ {
"name": "@strudel/serial", "name": "@strudel/serial",
"version": "1.2.2", "version": "1.2.3",
"description": "Webserial API for strudel", "description": "Webserial API for strudel",
"main": "serial.mjs", "main": "serial.mjs",
"type": "module", "type": "module",

View file

@ -1,6 +1,6 @@
{ {
"name": "@strudel/soundfonts", "name": "@strudel/soundfonts",
"version": "1.2.3", "version": "1.2.4",
"description": "Soundsfont support for strudel", "description": "Soundsfont support for strudel",
"main": "index.mjs", "main": "index.mjs",
"publishConfig": { "publishConfig": {

View file

@ -209,7 +209,8 @@ export function getVibratoOscillator(param, value, t) {
// ConstantSource inherits AudioScheduledSourceNode, which has scheduling abilities // ConstantSource inherits AudioScheduledSourceNode, which has scheduling abilities
// a bit of a hack, but it works very well :) // a bit of a hack, but it works very well :)
export function webAudioTimeout(audioContext, onComplete, startTime, stopTime) { export function webAudioTimeout(audioContext, onComplete, startTime, stopTime) {
const constantNode = audioContext.createConstantSource(); const constantNode = new ConstantSourceNode(audioContext);
constantNode.start(startTime); constantNode.start(startTime);
constantNode.stop(stopTime); constantNode.stop(stopTime);
constantNode.onended = () => { constantNode.onended = () => {
@ -264,7 +265,7 @@ export function applyFM(param, value, begin) {
modulator = fmmod.node; modulator = fmmod.node;
stop = fmmod.stop; stop = fmmod.stop;
if (![fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity].find((v) => v !== undefined)) { if (![fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity].some((v) => v !== undefined)) {
// no envelope by default // no envelope by default
modulator.connect(param); modulator.connect(param);
} else { } else {

View file

@ -1,5 +1,11 @@
let log = (msg) => console.log(msg); let log = (msg) => console.log(msg);
export function errorLogger(e, origin = 'cyclist') {
//TODO: add some kind of debug flag that enables this while in dev mode
// console.error(e);
logger(`[${origin}] error: ${e.message}`);
}
export const logger = (...args) => log(...args); export const logger = (...args) => log(...args);
export const setLogger = (fn) => { export const setLogger = (fn) => {

View file

@ -1,6 +1,6 @@
{ {
"name": "superdough", "name": "superdough",
"version": "1.2.3", "version": "1.2.4",
"description": "simple web audio synth and sampler intended for live coding. inspired by superdirt and webdirt.", "description": "simple web audio synth and sampler intended for live coding. inspired by superdirt and webdirt.",
"main": "index.mjs", "main": "index.mjs",
"type": "module", "type": "module",

View file

@ -9,9 +9,9 @@ import './reverb.mjs';
import './vowel.mjs'; import './vowel.mjs';
import { clamp, nanFallback, _mod, cycleToSeconds, secondsToCycle } from './util.mjs'; import { clamp, nanFallback, _mod, cycleToSeconds, secondsToCycle } from './util.mjs';
import workletsUrl from './worklets.mjs?audioworklet'; import workletsUrl from './worklets.mjs?audioworklet';
import { createFilter, gainNode, getCompressor, getWorklet } from './helpers.mjs'; import { createFilter, gainNode, getCompressor, getWorklet, webAudioTimeout } from './helpers.mjs';
import { map } from 'nanostores'; import { map } from 'nanostores';
import { logger } from './logger.mjs'; import { logger, errorLogger } from './logger.mjs';
import { loadBuffer } from './sampler.mjs'; import { loadBuffer } from './sampler.mjs';
export const DEFAULT_MAX_POLYPHONY = 128; export const DEFAULT_MAX_POLYPHONY = 128;
@ -331,16 +331,18 @@ function getDelay(orbit, delaytime, delayfeedback, t, channels) {
//logger(`delayfeedback was clamped to ${maxfeedback} to save your ears`); //logger(`delayfeedback was clamped to ${maxfeedback} to save your ears`);
} }
delayfeedback = clamp(delayfeedback, 0, 0.98); delayfeedback = clamp(delayfeedback, 0, 0.98);
if (!delays[orbit]) { if (!orbits[orbit].delayNode) {
const ac = getAudioContext(); const ac = getAudioContext();
const dly = ac.createFeedbackDelay(1, delaytime, delayfeedback); const dly = ac.createFeedbackDelay(1, delaytime, delayfeedback);
dly.start?.(t); // for some reason, this throws when audion extension is installed.. dly.start?.(t); // for some reason, this throws when audion extension is installed..
connectToDestination(dly, channels); connectToOrbit(dly, orbit);
delays[orbit] = dly; orbits[orbit].delayNode = dly;
} }
delays[orbit].delayTime.value !== delaytime && delays[orbit].delayTime.setValueAtTime(delaytime, t); orbits[orbit].delayNode.delayTime.value !== delaytime &&
delays[orbit].feedback.value !== delayfeedback && delays[orbit].feedback.setValueAtTime(delayfeedback, t); orbits[orbit].delayNode.delayTime.setValueAtTime(delaytime, t);
return delays[orbit]; orbits[orbit].delayNode.feedback.value !== delayfeedback &&
orbits[orbit].delayNode.feedback.setValueAtTime(delayfeedback, t);
return orbits[orbit].delayNode;
} }
export function getLfo(audioContext, begin, end, properties = {}) { export function getLfo(audioContext, begin, end, properties = {}) {
@ -365,22 +367,6 @@ export function getLfo(audioContext, begin, end, properties = {}) {
return getWorklet(audioContext, 'lfo-processor', lfoprops); return getWorklet(audioContext, 'lfo-processor', lfoprops);
} }
export function getSyncedLfo(audioContext, time, end, cps, cycle, properties = {}) {
const frequency = cycle / cps;
return getWorklet(audioContext, 'lfo-processor', {
frequency,
depth: 1,
skew: 0,
phaseoffset: 0,
time,
end,
shape: 1,
dcoffset: -0.5,
...properties,
});
}
function getPhaser(time, end, frequency = 1, depth = 0.5, centerFrequency = 1000, sweep = 2000) { function getPhaser(time, end, frequency = 1, depth = 0.5, centerFrequency = 1000, sweep = 2000) {
const ac = getAudioContext(); const ac = getAudioContext();
const lfoGain = getLfo(ac, time, end, { frequency, depth: sweep * 2 }); const lfoGain = getLfo(ac, time, end, { frequency, depth: sweep * 2 });
@ -412,31 +398,73 @@ function getFilterType(ftype) {
return typeof ftype === 'number' ? filterTypes[Math.floor(_mod(ftype, filterTypes.length))] : ftype; return typeof ftype === 'number' ? filterTypes[Math.floor(_mod(ftype, filterTypes.length))] : ftype;
} }
let reverbs = {}; //type orbit {
// gain: number,
// reverbNode: reverbNode
// delayNode: delayNode
//}
let orbits = {};
function connectToOrbit(node, orbit) {
if (orbits[orbit] == null) {
errorLogger(new Error('target orbit does not exist'), 'superdough');
}
node.connect(orbits[orbit].gain);
}
function setOrbit(audioContext, orbit, channels) {
if (orbits[orbit] == null) {
orbits[orbit] = {
gain: new GainNode(audioContext, { gain: 1 }),
};
connectToDestination(orbits[orbit].gain, channels);
}
}
function duckOrbit(audioContext, targetOrbit, t, attacktime = 0.1, duckdepth = 1) {
const targetArr = [targetOrbit].flat();
targetArr.forEach((target) => {
if (orbits[target] == null) {
errorLogger(new Error(`duck target orbit ${target} does not exist`), 'superdough');
return;
}
webAudioTimeout(
audioContext,
() => {
orbits[target].gain.gain.cancelScheduledValues(t);
const currVal = orbits[target].gain.gain.value;
orbits[target].gain.gain.linearRampToValueAtTime(clamp(1 - Math.pow(duckdepth, 0.5), 0.01, currVal), t);
orbits[target].gain.gain.exponentialRampToValueAtTime(1, t + Math.max(0.002, attacktime));
},
0,
t - 0.01,
);
});
}
let hasChanged = (now, before) => now !== undefined && now !== before; let hasChanged = (now, before) => now !== undefined && now !== before;
function getReverb(orbit, duration, fade, lp, dim, ir, channels) { function getReverb(orbit, duration, fade, lp, dim, ir) {
// If no reverb has been created for a given orbit, create one // If no reverb has been created for a given orbit, create one
if (!reverbs[orbit]) { if (!orbits[orbit].reverbNode) {
const ac = getAudioContext(); const ac = getAudioContext();
const reverb = ac.createReverb(duration, fade, lp, dim, ir); const reverb = ac.createReverb(duration, fade, lp, dim, ir);
connectToDestination(reverb, channels); connectToOrbit(reverb, orbit);
reverbs[orbit] = reverb; orbits[orbit].reverbNode = reverb;
} }
if ( if (
hasChanged(duration, reverbs[orbit].duration) || hasChanged(duration, orbits[orbit].reverbNode.duration) ||
hasChanged(fade, reverbs[orbit].fade) || hasChanged(fade, orbits[orbit].reverbNode.fade) ||
hasChanged(lp, reverbs[orbit].lp) || hasChanged(lp, orbits[orbit].reverbNode.lp) ||
hasChanged(dim, reverbs[orbit].dim) || hasChanged(dim, orbits[orbit].reverbNode.dim) ||
reverbs[orbit].ir !== ir orbits[orbit].reverbNode.ir !== ir
) { ) {
// only regenerate when something has changed // only regenerate when something has changed
// avoids endless regeneration on things like // avoids endless regeneration on things like
// stack(s("a"), s("b").rsize(8)).room(.5) // stack(s("a"), s("b").rsize(8)).room(.5)
// this only works when args may stay undefined until here // this only works when args may stay undefined until here
// setting default values breaks this // setting default values breaks this
reverbs[orbit].generate(duration, fade, lp, dim, ir); orbits[orbit].reverbNode.generate(duration, fade, lp, dim, ir);
} }
return reverbs[orbit]; return orbits[orbit].reverbNode;
} }
export let analysers = {}, export let analysers = {},
@ -479,8 +507,7 @@ function effectSend(input, effect, wet) {
} }
export function resetGlobalEffects() { export function resetGlobalEffects() {
delays = {}; orbits = {};
reverbs = {};
analysers = {}; analysers = {};
analysersData = {}; analysersData = {};
} }
@ -533,6 +560,9 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
gain = getDefaultValue('gain'), gain = getDefaultValue('gain'),
postgain = getDefaultValue('postgain'), postgain = getDefaultValue('postgain'),
density = getDefaultValue('density'), density = getDefaultValue('density'),
duckorbit,
duckattack,
duckdepth,
// filters // filters
fanchor = getDefaultValue('fanchor'), fanchor = getDefaultValue('fanchor'),
drive = 0.69, drive = 0.69,
@ -571,6 +601,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
coarse, coarse,
crush, crush,
dry,
shape, shape,
shapevol = getDefaultValue('shapevol'), shapevol = getDefaultValue('shapevol'),
distort, distort,
@ -604,7 +635,13 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
const orbitChannels = mapChannelNumbers( const orbitChannels = mapChannelNumbers(
multiChannelOrbits && orbit > 0 ? [orbit * 2 - 1, orbit * 2] : getDefaultValue('channels'), multiChannelOrbits && orbit > 0 ? [orbit * 2 - 1, orbit * 2] : getDefaultValue('channels'),
); );
const channels = value.channels != null ? mapChannelNumbers(value.channels) : orbitChannels; const channels = value.channels != null ? mapChannelNumbers(value.channels) : orbitChannels;
setOrbit(ac, orbit, channels, t, cycle, cps);
if (duckorbit != null) {
duckOrbit(ac, duckorbit, t, duckattack, duckdepth);
}
gain = applyGainCurve(nanFallback(gain, 1)); gain = applyGainCurve(nanFallback(gain, 1));
postgain = applyGainCurve(postgain); postgain = applyGainCurve(postgain);
@ -791,7 +828,6 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
// last gain // last gain
const post = new GainNode(ac, { gain: postgain }); const post = new GainNode(ac, { gain: postgain });
chain.push(post); chain.push(post);
connectToDestination(post, channels);
// delay // delay
let delaySend; let delaySend;
@ -826,6 +862,14 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
analyserSend = effectSend(post, analyserNode, 1); analyserSend = effectSend(post, analyserNode, 1);
audioNodes.push(analyserSend); audioNodes.push(analyserSend);
} }
if (dry != null) {
dry = applyGainCurve(dry);
const dryGain = new GainNode(ac, { gain: dry });
chain.push(dryGain);
connectToOrbit(dryGain, orbit);
} else {
connectToOrbit(post, orbit);
}
// connect chain elements together // connect chain elements together
chain.slice(1).reduce((last, current) => last.connect(current), chain[0]); chain.slice(1).reduce((last, current) => last.connect(current), chain[0]);

View file

@ -201,10 +201,7 @@ export function registerSynthSounds() {
const gainAdjustment = 1 / Math.sqrt(voices); const gainAdjustment = 1 / Math.sqrt(voices);
getPitchEnvelope(o.parameters.get('detune'), value, begin, holdend); getPitchEnvelope(o.parameters.get('detune'), value, begin, holdend);
const vibratoOscillator = getVibratoOscillator(o.parameters.get('detune'), value, begin); const vibratoOscillator = getVibratoOscillator(o.parameters.get('detune'), value, begin);
// const fm = applyFM(o.parameters.get('frequency'), value, begin); const fm = applyFM(o.parameters.get('frequency'), value, begin);
// https://codeberg.org/uzu/strudel/issues/1428
// if you think about re-enabling this, please test with fm > 1 first
// it's like 10x gain, so it's really dangerous
let envGain = gainNode(1); let envGain = gainNode(1);
envGain = o.connect(envGain); envGain = o.connect(envGain);
@ -216,7 +213,7 @@ export function registerSynthSounds() {
destroyAudioWorkletNode(o); destroyAudioWorkletNode(o);
envGain.disconnect(); envGain.disconnect();
onended(); onended();
// fm?.stop(); fm?.stop();
vibratoOscillator?.stop(); vibratoOscillator?.stop();
}, },
begin, begin,

View file

@ -8,18 +8,28 @@ import FFT from './fft.js';
const clamp = (num, min, max) => Math.min(Math.max(num, min), max); const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
const _mod = (n, m) => ((n % m) + m) % m; const _mod = (n, m) => ((n % m) + m) % m;
// Restrict phase to the range [0, maxPhase) via wrapping
function wrapPhase(phase, maxPhase = 1) {
if (phase >= maxPhase) {
phase -= maxPhase;
} else if (phase < 0) {
phase += maxPhase;
}
return phase;
}
const blockSize = 128; const blockSize = 128;
// adjust waveshape to remove frequencies above nyquist to prevent aliasing // Smooth waveshape near discontinuities to remove frequencies above Nyquist and prevent aliasing
// referenced from https://www.kvraudio.com/forum/viewtopic.php?t=375517 // referenced from https://www.kvraudio.com/forum/viewtopic.php?t=375517
function polyBlep(phase, dt) { function polyBlep(phase, dt) {
// 0 <= phase < 1 dt = Math.min(dt, 1 - dt);
// Start of cycle
if (phase < dt) { if (phase < dt) {
phase /= dt; phase /= dt;
// 2 * (phase - phase^2/2 - 0.5) // 2 * (phase - phase^2/2 - 0.5)
return phase + phase - phase * phase - 1; return phase + phase - phase * phase - 1;
} }
// -1 < phase < 0 // End of cycle
else if (phase > 1 - dt) { else if (phase > 1 - dt) {
phase = (phase - 1) / dt; phase = (phase - 1) / dt;
// 2 * (phase^2/2 + phase + 0.5) // 2 * (phase^2/2 + phase + 0.5)
@ -115,7 +125,6 @@ class LFOProcessor extends AudioWorkletProcessor {
process(inputs, outputs, parameters) { process(inputs, outputs, parameters) {
const begin = parameters['begin'][0]; const begin = parameters['begin'][0];
// eslint-disable-next-line no-undef
if (currentTime >= parameters.end[0]) { if (currentTime >= parameters.end[0]) {
return false; return false;
} }
@ -143,7 +152,6 @@ class LFOProcessor extends AudioWorkletProcessor {
if (this.phase == null) { if (this.phase == null) {
this.phase = _mod(time * frequency + phaseoffset, 1); this.phase = _mod(time * frequency + phaseoffset, 1);
} }
// eslint-disable-next-line no-undef
const dt = frequency / sampleRate; const dt = frequency / sampleRate;
for (let n = 0; n < blockSize; n++) { for (let n = 0; n < blockSize; n++) {
for (let i = 0; i < output.length; i++) { for (let i = 0; i < output.length; i++) {
@ -305,7 +313,6 @@ class LadderProcessor extends AudioWorkletProcessor {
const drive = clamp(Math.exp(parameters.drive[0]), 0.1, 2000); const drive = clamp(Math.exp(parameters.drive[0]), 0.1, 2000);
let cutoff = parameters.frequency[0]; let cutoff = parameters.frequency[0];
// eslint-disable-next-line no-undef
cutoff = (cutoff * 2 * _PI) / sampleRate; cutoff = (cutoff * 2 * _PI) / sampleRate;
cutoff = cutoff > 1 ? 1 : cutoff; cutoff = cutoff > 1 ? 1 : cutoff;
@ -438,18 +445,13 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
]; ];
} }
process(input, outputs, params) { process(input, outputs, params) {
// eslint-disable-next-line no-undef
if (currentTime <= params.begin[0]) { if (currentTime <= params.begin[0]) {
return true; return true;
} }
// eslint-disable-next-line no-undef
if (currentTime >= params.end[0]) { if (currentTime >= params.end[0]) {
// this.port.postMessage({ type: 'onended' }); // this.port.postMessage({ type: 'onended' });
return false; return false;
} }
let frequency = params.frequency[0];
//apply detune in cents
frequency = frequency * Math.pow(2, params.detune[0] / 1200);
const output = outputs[0]; const output = outputs[0];
const voices = params.voices[0]; const voices = params.voices[0];
@ -460,9 +462,6 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
for (let n = 0; n < voices; n++) { for (let n = 0; n < voices; n++) {
const isOdd = (n & 1) == 1; const isOdd = (n & 1) == 1;
//applies unison "spread" detune in semitones
const freq = applySemitoneDetuneToFrequency(frequency, getUnisonDetune(voices, freqspread, n));
let gainL = gain1; let gainL = gain1;
let gainR = gain2; let gainR = gain2;
// invert right and left gain // invert right and left gain
@ -470,21 +469,21 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
gainL = gain2; gainL = gain2;
gainR = gain1; gainR = gain1;
} }
// eslint-disable-next-line no-undef
const dt = freq / sampleRate;
for (let i = 0; i < output[0].length; i++) { for (let i = 0; i < output[0].length; i++) {
// Main detuning
let freq = applySemitoneDetuneToFrequency(params.frequency[i] ?? params.frequency[0], params.detune[0] / 100);
// Individual voice detuning
freq = applySemitoneDetuneToFrequency(freq, getUnisonDetune(voices, freqspread, n));
// We must wrap this here because it is passed into sawblep below which
// has domain [0, 1]
const dt = _mod(freq / sampleRate, 1);
this.phase[n] = this.phase[n] ?? Math.random(); this.phase[n] = this.phase[n] ?? Math.random();
const v = waveshapes.sawblep(this.phase[n], dt); const v = waveshapes.sawblep(this.phase[n], dt);
output[0][i] = output[0][i] + v * gainL; output[0][i] = output[0][i] + v * gainL;
output[1][i] = output[1][i] + v * gainR; output[1][i] = output[1][i] + v * gainR;
this.phase[n] += dt; this.phase[n] = wrapPhase(this.phase[n] + dt);
if (this.phase[n] > 1.0) {
this.phase[n] = this.phase[n] - 1;
}
} }
} }
return true; return true;
@ -493,7 +492,7 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
registerProcessor('supersaw-oscillator', SuperSawOscillatorProcessor); registerProcessor('supersaw-oscillator', SuperSawOscillatorProcessor);
// Phase Vocoder sourced from // sourced from https://github.com/olvb/phaze/tree/master?tab=readme-ov-file // Phase Vocoder sourced from https://github.com/olvb/phaze/tree/master?tab=readme-ov-file
const BUFFERED_BLOCK_SIZE = 2048; const BUFFERED_BLOCK_SIZE = 2048;
function genHannWindow(length) { function genHannWindow(length) {

View file

@ -1,6 +1,6 @@
{ {
"name": "@strudel/tonal", "name": "@strudel/tonal",
"version": "1.2.2", "version": "1.2.3",
"description": "Tonal functions for strudel", "description": "Tonal functions for strudel",
"main": "index.mjs", "main": "index.mjs",
"publishConfig": { "publishConfig": {

View file

@ -1,6 +1,6 @@
{ {
"name": "@strudel/transpiler", "name": "@strudel/transpiler",
"version": "1.2.2", "version": "1.2.3",
"description": "Transpiler for strudel user code. Converts syntactically correct but semantically meaningless JS into evaluatable strudel code.", "description": "Transpiler for strudel user code. Converts syntactically correct but semantically meaningless JS into evaluatable strudel code.",
"main": "index.mjs", "main": "index.mjs",
"type": "module", "type": "module",

View file

@ -1,6 +1,6 @@
{ {
"name": "@strudel/web", "name": "@strudel/web",
"version": "1.2.3", "version": "1.2.4",
"description": "Easy to setup, opiniated bundle of Strudel for the browser.", "description": "Easy to setup, opiniated bundle of Strudel for the browser.",
"module": "web.mjs", "module": "web.mjs",
"publishConfig": { "publishConfig": {

View file

@ -1,6 +1,6 @@
{ {
"name": "@strudel/webaudio", "name": "@strudel/webaudio",
"version": "1.2.3", "version": "1.2.4",
"description": "Web Audio helpers for Strudel", "description": "Web Audio helpers for Strudel",
"main": "index.mjs", "main": "index.mjs",
"type": "module", "type": "module",

View file

@ -1,6 +1,6 @@
{ {
"name": "@strudel/xen", "name": "@strudel/xen",
"version": "1.2.2", "version": "1.2.3",
"description": "Xenharmonic API for strudel", "description": "Xenharmonic API for strudel",
"main": "index.mjs", "main": "index.mjs",
"type": "module", "type": "module",

View file

@ -3036,6 +3036,145 @@ exports[`runs examples > example "dry" example index 0 1`] = `
] ]
`; `;
exports[`runs examples > example "duckattack" example index 0 1`] = `
[
"[ 0/1 → 1/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 0/1 → 1/8 | note:C3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 1/8 → 1/4 | note:D3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 1/4 → 5/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 1/4 → 3/8 | note:Eb3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 3/8 → 1/2 | note:F3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 1/2 → 9/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 1/2 → 5/8 | note:G3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 5/8 → 3/4 | note:Ab3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 11/16 → 3/4 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 3/4 → 7/8 | note:Bb3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 7/8 → 15/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 7/8 → 1/1 | note:C4 s:sawtooth delay:0.7 orbit:2 ]",
"[ 1/1 → 17/16 | s:bd n:4 duckorbit:2 duckattack:0 duckdepth:1 ]",
"[ 1/1 → 9/8 | note:C3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 9/8 → 5/4 | note:D3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 5/4 → 21/16 | s:bd n:4 duckorbit:2 duckattack:0 duckdepth:1 ]",
"[ 5/4 → 11/8 | note:Eb3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 11/8 → 3/2 | note:F3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 3/2 → 25/16 | s:bd n:4 duckorbit:2 duckattack:0 duckdepth:1 ]",
"[ 3/2 → 13/8 | note:G3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 13/8 → 7/4 | note:Ab3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 27/16 → 7/4 | s:bd n:4 duckorbit:2 duckattack:0 duckdepth:1 ]",
"[ 7/4 → 15/8 | note:Bb3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 15/8 → 31/16 | s:bd n:4 duckorbit:2 duckattack:0 duckdepth:1 ]",
"[ 15/8 → 2/1 | note:C4 s:sawtooth delay:0.7 orbit:2 ]",
"[ 2/1 → 33/16 | s:bd n:4 duckorbit:2 duckattack:0.4 duckdepth:1 ]",
"[ 2/1 → 17/8 | note:C3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 17/8 → 9/4 | note:D3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 9/4 → 37/16 | s:bd n:4 duckorbit:2 duckattack:0.4 duckdepth:1 ]",
"[ 9/4 → 19/8 | note:Eb3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 19/8 → 5/2 | note:F3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 5/2 → 41/16 | s:bd n:4 duckorbit:2 duckattack:0.4 duckdepth:1 ]",
"[ 5/2 → 21/8 | note:G3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 21/8 → 11/4 | note:Ab3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 43/16 → 11/4 | s:bd n:4 duckorbit:2 duckattack:0.4 duckdepth:1 ]",
"[ 11/4 → 23/8 | note:Bb3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 23/8 → 47/16 | s:bd n:4 duckorbit:2 duckattack:0.4 duckdepth:1 ]",
"[ 23/8 → 3/1 | note:C4 s:sawtooth delay:0.7 orbit:2 ]",
"[ 3/1 → 49/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 3/1 → 25/8 | note:C3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 25/8 → 13/4 | note:D3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 13/4 → 53/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 13/4 → 27/8 | note:Eb3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 27/8 → 7/2 | note:F3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 7/2 → 57/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 7/2 → 29/8 | note:G3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 29/8 → 15/4 | note:Ab3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 59/16 → 15/4 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 15/4 → 31/8 | note:Bb3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 31/8 → 63/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 31/8 → 4/1 | note:C4 s:sawtooth delay:0.7 orbit:2 ]",
]
`;
exports[`runs examples > example "duckdepth" example index 0 1`] = `
[
"[ 0/1 → 1/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 0/1 → 1/8 | note:C3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 1/8 → 1/4 | note:D3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 1/4 → 5/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 1/4 → 3/8 | note:Eb3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 3/8 → 1/2 | note:F3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 1/2 → 9/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 1/2 → 5/8 | note:G3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 5/8 → 3/4 | note:Ab3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 11/16 → 3/4 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 3/4 → 7/8 | note:Bb3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 7/8 → 15/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 7/8 → 1/1 | note:C4 s:sawtooth delay:0.7 orbit:2 ]",
"[ 1/1 → 17/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:0.9 ]",
"[ 1/1 → 9/8 | note:C3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 9/8 → 5/4 | note:D3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 5/4 → 21/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:0.9 ]",
"[ 5/4 → 11/8 | note:Eb3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 11/8 → 3/2 | note:F3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 3/2 → 25/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:0.9 ]",
"[ 3/2 → 13/8 | note:G3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 13/8 → 7/4 | note:Ab3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 27/16 → 7/4 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:0.9 ]",
"[ 7/4 → 15/8 | note:Bb3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 15/8 → 31/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:0.9 ]",
"[ 15/8 → 2/1 | note:C4 s:sawtooth delay:0.7 orbit:2 ]",
"[ 2/1 → 33/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:0.6 ]",
"[ 2/1 → 17/8 | note:C3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 17/8 → 9/4 | note:D3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 9/4 → 37/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:0.6 ]",
"[ 9/4 → 19/8 | note:Eb3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 19/8 → 5/2 | note:F3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 5/2 → 41/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:0.6 ]",
"[ 5/2 → 21/8 | note:G3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 21/8 → 11/4 | note:Ab3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 43/16 → 11/4 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:0.6 ]",
"[ 11/4 → 23/8 | note:Bb3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 23/8 → 47/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:0.6 ]",
"[ 23/8 → 3/1 | note:C4 s:sawtooth delay:0.7 orbit:2 ]",
"[ 3/1 → 49/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:0 ]",
"[ 3/1 → 25/8 | note:C3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 25/8 → 13/4 | note:D3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 13/4 → 53/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:0 ]",
"[ 13/4 → 27/8 | note:Eb3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 27/8 → 7/2 | note:F3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 7/2 → 57/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:0 ]",
"[ 7/2 → 29/8 | note:G3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 29/8 → 15/4 | note:Ab3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 59/16 → 15/4 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:0 ]",
"[ 15/4 → 31/8 | note:Bb3 s:sawtooth delay:0.7 orbit:2 ]",
"[ 31/8 → 63/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:0 ]",
"[ 31/8 → 4/1 | note:C4 s:sawtooth delay:0.7 orbit:2 ]",
]
`;
exports[`runs examples > example "duckorbit" example index 0 1`] = `
[
"[ 0/1 → 1/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 1/4 → 5/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 1/2 → 9/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 11/16 → 3/4 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 7/8 → 15/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 1/1 → 17/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 5/4 → 21/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 3/2 → 25/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 27/16 → 7/4 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 15/8 → 31/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 2/1 → 33/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 9/4 → 37/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 5/2 → 41/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 43/16 → 11/4 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 23/8 → 47/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 3/1 → 49/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 13/4 → 53/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 7/2 → 57/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 59/16 → 15/4 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
"[ 31/8 → 63/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
]
`;
exports[`runs examples > example "duration" example index 0 1`] = ` exports[`runs examples > example "duration" example index 0 1`] = `
[ [
"[ 0/1 → 1/4 | note:c s:piano duration:0.5 ]", "[ 0/1 → 1/4 | note:c s:piano duration:0.5 ]",
@ -3271,6 +3410,39 @@ exports[`runs examples > example "euclidRot" example index 0 1`] = `
] ]
`; `;
exports[`runs examples > example "euclidish" example index 0 1`] = `
[
"[ 0/1 → 1/12 | s:hh pan:0.5 ]",
"[ 13/84 → 5/21 | s:hh pan:0.5606253170575308 ]",
"[ 15/56 → 59/168 | s:hh pan:0.604413082836085 ]",
"[ 71/168 → 85/168 | s:hh pan:0.6629314122869361 ]",
"[ 97/168 → 37/56 | s:hh pan:0.7190455010067492 ]",
"[ 29/42 → 65/84 | s:hh pan:0.7580531369037533 ]",
"[ 71/84 → 13/14 | s:hh pan:0.8080762739548087 ]",
"[ 1/1 → 13/12 | s:hh pan:0.8535533905932737 ]",
"[ 451099417/393511398 → 322594689/262340932 | s:hh pan:0.891768001805729 ]",
"[ 335923379/262340932 → 536677685/393511398 | s:hh pan:0.9222657853371297 ]",
"[ 1122946175/787022796 → 99044284/65585233 | s:hh pan:0.9501869591788796 ]",
"[ 1238122213/787022796 → 651853723/393511398 | s:hh pan:0.9721673436944069 ]",
"[ 335923379/196755699 → 469759583/262340932 | s:hh pan:0.9868472639237561 ]",
"[ 729434777/393511398 → 1524454787/787022796 | s:hh pan:0.9967009321321423 ]",
"[ 2/1 → 25/12 | s:hh pan:1 ]",
"[ 15/7 → 187/84 | s:hh pan:0.9968561049466214 ]",
"[ 16/7 → 199/84 | s:hh pan:0.9874639560909118 ]",
"[ 17/7 → 211/84 | s:hh pan:0.9719416651541839 ]",
"[ 18/7 → 223/84 | s:hh pan:0.9504844339512095 ]",
"[ 19/7 → 235/84 | s:hh pan:0.9233620996141421 ]",
"[ 20/7 → 247/84 | s:hh pan:0.890915741234015 ]",
"[ 3/1 → 37/12 | s:hh pan:0.8535533905932737 ]",
"[ 1238122213/393511398 → 847276553/262340932 | s:hh pan:0.8106731928589048 ]",
"[ 860605243/262340932 → 1323700481/393511398 | s:hh pan:0.7677528833339 ]",
"[ 2696991767/787022796 → 230214750/65585233 | s:hh pan:0.7175585019834292 ]",
"[ 2812167805/787022796 → 1438876519/393511398 | s:hh pan:0.6644931595798675 ]",
"[ 729434777/196755699 → 994441447/262340932 | s:hh pan:0.6139286689554151 ]",
"[ 1516457573/393511398 → 3098500379/787022796 | s:hh pan:0.557342689325327 ]",
]
`;
exports[`runs examples > example "every" example index 0 1`] = ` exports[`runs examples > example "every" example index 0 1`] = `
[ [
"[ 0/1 → 1/4 | note:g3 ]", "[ 0/1 → 1/4 | note:g3 ]",
@ -5289,6 +5461,40 @@ exports[`runs examples > example "lock" example index 0 1`] = `
] ]
`; `;
exports[`runs examples > example "log" example index 0 1`] = `
[
"[ 0/1 → 1/2 | s:bd ]",
"[ 1/2 → 1/1 | s:sd ]",
"[ 1/1 → 3/2 | s:bd ]",
"[ 3/2 → 2/1 | s:sd ]",
"[ 2/1 → 5/2 | s:bd ]",
"[ 5/2 → 3/1 | s:sd ]",
"[ 3/1 → 7/2 | s:bd ]",
"[ 7/2 → 4/1 | s:sd ]",
]
`;
exports[`runs examples > example "logValues" example index 0 1`] = `
[
"[ (0/1 → 1/3) ⇝ 1/2 | s:bd gain:0.25 n:2 ]",
"[ 0/1 ⇜ (1/3 → 1/2) | s:bd gain:0.5 n:1 ]",
"[ (1/2 → 2/3) ⇝ 1/1 | s:sd gain:0.5 n:1 ]",
"[ 1/2 ⇜ (2/3 → 1/1) | s:sd gain:1 n:0 ]",
"[ (1/1 → 4/3) ⇝ 3/2 | s:bd gain:0.25 n:2 ]",
"[ 1/1 ⇜ (4/3 → 3/2) | s:bd gain:0.5 n:1 ]",
"[ (3/2 → 5/3) ⇝ 2/1 | s:sd gain:0.5 n:1 ]",
"[ 3/2 ⇜ (5/3 → 2/1) | s:sd gain:1 n:0 ]",
"[ (2/1 → 7/3) ⇝ 5/2 | s:bd gain:0.25 n:2 ]",
"[ 2/1 ⇜ (7/3 → 5/2) | s:bd gain:0.5 n:1 ]",
"[ (5/2 → 8/3) ⇝ 3/1 | s:sd gain:0.5 n:1 ]",
"[ 5/2 ⇜ (8/3 → 3/1) | s:sd gain:1 n:0 ]",
"[ (3/1 → 10/3) ⇝ 7/2 | s:bd gain:0.25 n:2 ]",
"[ 3/1 ⇜ (10/3 → 7/2) | s:bd gain:0.5 n:1 ]",
"[ (7/2 → 11/3) ⇝ 4/1 | s:sd gain:0.5 n:1 ]",
"[ 7/2 ⇜ (11/3 → 4/1) | s:sd gain:1 n:0 ]",
]
`;
exports[`runs examples > example "loop" example index 0 1`] = ` exports[`runs examples > example "loop" example index 0 1`] = `
[ [
"[ 0/1 → 1/1 | s:casio loop:1 ]", "[ 0/1 → 1/1 | s:casio loop:1 ]",
@ -5968,6 +6174,48 @@ exports[`runs examples > example "miditouch" example index 0 1`] = `
] ]
`; `;
exports[`runs examples > example "morph" example index 0 1`] = `
[
"[ 0/1 → 1/8 | s:hh ]",
"[ 25/112 → 39/112 | s:hh ]",
"[ 27/56 → 17/28 | s:hh ]",
"[ 83/112 → 97/112 | s:hh ]",
"[ 1/1 → 9/8 | s:hh ]",
"[ 137/112 → 151/112 | s:hh ]",
"[ 83/56 → 45/28 | s:hh ]",
"[ 195/112 → 209/112 | s:hh ]",
"[ 2/1 → 17/8 | s:hh ]",
"[ 249/112 → 263/112 | s:hh ]",
"[ 139/56 → 73/28 | s:hh ]",
"[ 307/112 → 321/112 | s:hh ]",
"[ 3/1 → 25/8 | s:hh ]",
"[ 361/112 → 375/112 | s:hh ]",
"[ 195/56 → 101/28 | s:hh ]",
"[ 419/112 → 433/112 | s:hh ]",
]
`;
exports[`runs examples > example "morph" example index 1 1`] = `
[
"[ 0/1 → 1/8 | s:hh ]",
"[ 11/56 → 9/28 | s:hh ]",
"[ 13/28 → 33/56 | s:hh ]",
"[ 41/56 → 6/7 | s:hh ]",
"[ 1/1 → 9/8 | s:hh ]",
"[ 303934523/262340932 → 673454279/524681864 | s:hh ]",
"[ 188758485/131170466 → 820619173/524681864 | s:hh ]",
"[ 451099417/262340932 → 967784067/524681864 | s:hh ]",
"[ 2/1 → 17/8 | s:hh ]",
"[ 15/7 → 127/56 | s:hh ]",
"[ 17/7 → 143/56 | s:hh ]",
"[ 19/7 → 159/56 | s:hh ]",
"[ 3/1 → 25/8 | s:hh ]",
"[ 828616387/262340932 → 1722818007/524681864 | s:hh ]",
"[ 451099417/131170466 → 1869982901/524681864 | s:hh ]",
"[ 975781281/262340932 → 2017147795/524681864 | s:hh ]",
]
`;
exports[`runs examples > example "mousex" example index 0 1`] = ` exports[`runs examples > example "mousex" example index 0 1`] = `
[ [
"[ 0/1 → 1/4 | note:C3 ]", "[ 0/1 → 1/4 | note:C3 ]",

View file

@ -8,8 +8,7 @@
"start": "astro dev", "start": "astro dev",
"build": "astro build", "build": "astro build",
"preview": "astro preview --port 3009 --host 0.0.0.0", "preview": "astro preview --port 3009 --host 0.0.0.0",
"astro": "astro", "astro": "astro"
"postinstall": "cp node_modules/hs2js/dist/tree-sitter.wasm public && cp node_modules/hs2js/dist/tree-sitter-haskell.wasm public"
}, },
"dependencies": { "dependencies": {
"@algolia/client-search": "^5.20.0", "@algolia/client-search": "^5.20.0",

View file

@ -34,11 +34,11 @@ import { JsDoc } from '../../docs/JsDoc';
## arp ## arp
<JsDoc client:idle name="Pattern#arp" h={0} /> <JsDoc client:idle name="arp" h={0} />
## arpWith 🧪 ## arpWith 🧪
<JsDoc client:idle name="Pattern#arpWith" h={0} /> <JsDoc client:idle name="arpWith" h={0} />
## struct ## struct
@ -58,7 +58,7 @@ import { JsDoc } from '../../docs/JsDoc';
## hush ## hush
<JsDoc client:idle name="hush" h={0} /> <JsDoc client:idle name="Pattern#hush" h={0} />
## invert ## invert

View file

@ -339,4 +339,18 @@ global effects use the same chain for all events of the same orbit:
<JsDoc client:idle name="phasersweep" h={0} /> <JsDoc client:idle name="phasersweep" h={0} />
## Duck
### duckorbit
<JsDoc client:idle name="duckorbit" h={0} />
### duckattack
<JsDoc client:idle name="duckattack" h={0} />
### duckdepth
<JsDoc client:idle name="duckdepth" h={0} />
Next, we'll look at input / output via [MIDI, OSC and other methods](/learn/input-output). Next, we'll look at input / output via [MIDI, OSC and other methods](/learn/input-output).

View file

@ -178,6 +178,16 @@ the version number).
It is also possible, of course, to just remove it from cache (deleting cache in browser Privacy settings, It is also possible, of course, to just remove it from cache (deleting cache in browser Privacy settings,
or from the dev console if you're technically minded, or by using a cache deleting extension). or from the dev console if you're technically minded, or by using a cache deleting extension).
## Generating strudel.json
You can use [@strudel/sampler](https://www.npmjs.com/package/@strudel/sampler) to generate a strudel.json file for you, by running:
```sh
npx --yes @strudel/sampler --json > strudel.json
```
See other uses of strudel/sampler further below, under "From Disk via @strudel/sampler".
## Github Shortcut ## Github Shortcut
Because loading samples from github is common, there is a shortcut: Because loading samples from github is common, there is a shortcut:
@ -361,6 +371,10 @@ Sampler effects are functions that can be used to change the behaviour of sample
<JsDoc client:idle name="splice" h={0} /> <JsDoc client:idle name="splice" h={0} />
### scrub
<JsDoc client:idle name="Pattern.scrub" h={0} />{' '}
### speed ### speed
<JsDoc client:idle name="speed" h={0} /> <JsDoc client:idle name="speed" h={0} />

View file

@ -69,3 +69,133 @@
text-decoration: underline 0.18rem; text-decoration: underline 0.18rem;
text-underline-offset: 0.22rem; text-underline-offset: 0.22rem;
} }
/* Override default styles from the codemirror inline css for autocomplete info tooltip*/
.cm-tooltip.cm-completionInfo {
padding: 12px !important;
padding-bottom: 12px !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;
}
/* Main tooltip container */
.autocomplete-info-tooltip {
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;
min-width: 400px;
}
.autocomplete-info-function-name {
font-size: 15px;
font-weight: 600;
color: var(--foreground);
margin: 0 0 8px 0;
}
.autocomplete-info-function-description {
margin: 0 0 12px 0;
color: var(--foreground);
line-height: 1.5;
opacity: 0.8;
}
.autocomplete-info-section-title {
font-size: 12px;
font-weight: 600;
color: var(--foreground);
margin: 16px 0 6px 0;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.autocomplete-info-section-title:first-child {
margin-top: 0;
}
.autocomplete-info-params-section {
margin-top: 12px;
}
.autocomplete-info-params-list {
list-style: none;
margin: 0;
padding: 0;
}
.autocomplete-info-param-item {
margin-bottom: 8px;
padding: 8px;
background-color: var(--lineBackground);
border-radius: 3px;
border-left: 2px solid var(--foreground, #555);
}
.autocomplete-info-param-item:last-child {
margin-bottom: 0;
}
.autocomplete-info-param-name {
font-weight: 600;
color: var(--variable, var(--foreground));
margin-right: 8px;
}
.autocomplete-info-param-type {
color: var(--comment);
font-size: 12px;
background-color: var(--gutterForeground);
padding: 1px 4px;
border-radius: 2px;
}
.autocomplete-info-param-desc {
color: var(--foreground);
font-size: 10px;
margin-top: 4px;
line-height: 1.4;
opacity: 0.7;
}
.autocomplete-info-examples-section {
margin-top: 12px;
}
.autocomplete-info-example-code {
background: var(--lineBackground);
color: var(--foreground);
padding: 8px;
border-radius: 3px;
font-family: var(--font-family, 'SF Mono', 'Monaco', monospace);
font-size: 12px;
line-height: 1.5;
margin: 4px 0;
overflow-x: auto;
white-space: pre;
border: 1px solid var(--foreground, #3a3a3a);
}
.autocomplete-info-tooltip::-webkit-scrollbar {
width: 4px;
}
.autocomplete-info-tooltip::-webkit-scrollbar-track {
}
.autocomplete-info-tooltip::-webkit-scrollbar-thumb {
border-radius: 2px;
}
.autocomplete-info-tooltip::-webkit-scrollbar-thumb:hover {
}