Compare commits

..

4 Commits

Author SHA1 Message Date
Jade (Rose) Rowland 44886ad90b fix test 2024-09-07 12:38:31 -04:00
Jade (Rose) Rowland 48bc3fca13 fix typo 2024-09-07 12:33:43 -04:00
Jade (Rose) Rowland 057bb42438 formatting 2024-09-07 12:32:55 -04:00
Jade (Rose) Rowland 1f7343f43d normalize z param for supersaw 2024-09-07 12:31:43 -04:00
74 changed files with 10450 additions and 13535 deletions
-1
View File
@@ -23,4 +23,3 @@ reverbGen.mjs
hydra.mjs
jsdoc-synonyms.js
packages/hs2js/src/hs2js.mjs
samples
-12
View File
@@ -138,18 +138,6 @@ pnpm --filter "./packages/**" publish --access public
To manually publish a single package, increase the version in the `package.json`, then run `pnpm publish`.
Important: Always publish with `pnpm`, as `npm` does not support overriding main files in `publishConfig`, which is done in all the packages.
## useful commands
```sh
#regenerate the test snapshots (ex: when updating or creating new pattern functions)
pnpm snapshot
#start the OSC server
pnpm run osc
#build the standalone version
pnpm tauri build
```
## Have Fun
Remember to have fun, and that this project is driven by the passion of volunteers!
+4 -10
View File
@@ -14,16 +14,10 @@ An experiment in making a [Tidal](https://github.com/tidalcycles/tidal/) using w
After cloning the project, you can run the REPL locally:
1. Install [Node.js](https://nodejs.org/)
2. Install [pnpm](https://pnpm.io/installation)
3. Install dependencies by running the following command:
```bash
pnpm i
```
4. Run the development server:
```bash
pnpm dev
```
```bash
pnpm i
pnpm dev
```
## Using Strudel In Your Project
+7 -7
View File
@@ -56,18 +56,18 @@
},
"devDependencies": {
"@tauri-apps/cli": "^1.5.9",
"@vitest/ui": "^2.1.3",
"acorn": "^8.13.0",
"@vitest/ui": "^1.1.0",
"acorn": "^8.11.3",
"dependency-tree": "^10.0.9",
"eslint": "^8.56.0",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-import": "^2.29.1",
"events": "^3.3.0",
"jsdoc": "^4.0.3",
"jsdoc": "^4.0.2",
"jsdoc-json": "^2.0.2",
"jsdoc-to-markdown": "^8.0.0",
"lerna": "^8.1.8",
"prettier": "^3.3.3",
"lerna": "^8.0.1",
"prettier": "^3.1.1",
"rollup-plugin-visualizer": "^5.12.0",
"vitest": "^2.1.3"
"vitest": "^1.1.0"
}
}
+12 -23
View File
@@ -13,7 +13,7 @@ import {
lineNumbers,
drawSelection,
} from '@codemirror/view';
import { repl, registerControl } from '@strudel/core';
import { Pattern, repl } from '@strudel/core';
import { Drawer, cleanupDraw } from '@strudel/draw';
import { isAutoCompletionEnabled } from './autocomplete.mjs';
import { isTooltipEnabled } from './tooltip.mjs';
@@ -99,8 +99,10 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo
},
{
key: 'Alt-.',
preventDefault: true,
run: () => onStop?.(),
run: (_, e) => {
e.preventDefault();
onStop?.();
},
},
/* {
key: 'Ctrl-Shift-.',
@@ -133,7 +135,6 @@ export class StrudelMirror {
autodraw,
prebake,
bgFill = true,
solo = true,
...replOptions
} = options;
this.code = initialCode;
@@ -144,7 +145,6 @@ export class StrudelMirror {
this.drawContext = drawContext;
this.onDraw = onDraw || this.draw;
this.id = id || s4();
this.solo = solo;
this.drawer = new Drawer((haps, time, _, painters) => {
const currentFrame = haps.filter((hap) => hap.isActive(time));
@@ -161,14 +161,12 @@ export class StrudelMirror {
replOptions?.onToggle?.(started);
if (started) {
this.drawer.start(this.repl.scheduler);
if (this.solo) {
// stop other repls when this one is started
document.dispatchEvent(
new CustomEvent('start-repl', {
detail: this.id,
}),
);
}
// stop other repls when this one is started
document.dispatchEvent(
new CustomEvent('start-repl', {
detail: this.id,
}),
);
} else {
this.drawer.stop();
updateMiniLocations(this.editor, []);
@@ -223,7 +221,7 @@ export class StrudelMirror {
// stop this repl when another repl is started
this.onStartRepl = (e) => {
if (this.solo && e.detail !== this.id) {
if (e.detail !== this.id) {
this.stop();
}
};
@@ -358,12 +356,3 @@ function s4() {
.toString(16)
.substring(1);
}
/**
* Overrides the css of highlighted events. Make sure to use single quotes!
* @name markcss
* @example
* note("c a f e")
* .markcss('text-decoration:underline')
*/
export const markcss = registerControl('markcss');
+1 -2
View File
@@ -93,7 +93,6 @@ const miniLocationHighlights = EditorView.decorations.compute([miniLocations, vi
if (haps.has(id)) {
const hap = haps.get(id);
const color = hap.value?.color ?? 'var(--foreground)';
const style = hap.value?.markcss || `outline: solid 2px ${color}`;
// Get explicit channels for color values
/*
const swatch = document.createElement('div');
@@ -115,7 +114,7 @@ const miniLocationHighlights = EditorView.decorations.compute([miniLocations, vi
to,
Decoration.mark({
// attributes: { style: `outline: solid 2px rgba(${channels.join(', ')})` },
attributes: { style },
attributes: { style: `outline: solid 2px ${color}` },
}),
);
}
+20 -20
View File
@@ -18,25 +18,25 @@ export default createTheme({
theme: 'light',
settings,
styles: [
{ tag: t.labelName, color: 'inherit' },
{ tag: t.keyword, color: 'inherit' },
{ tag: t.operator, color: 'inherit' },
{ tag: t.special(t.variableName), color: 'inherit' },
{ tag: t.typeName, color: 'inherit' },
{ tag: t.atom, color: 'inherit' },
{ tag: t.number, color: 'inherit' },
{ tag: t.definition(t.variableName), color: 'inherit' },
{ tag: t.string, color: 'inherit' },
{ tag: t.special(t.string), color: 'inherit' },
{ tag: t.comment, color: 'inherit' },
{ tag: t.variableName, color: 'inherit' },
{ tag: t.tagName, color: 'inherit' },
{ tag: t.bracket, color: 'inherit' },
{ tag: t.meta, color: 'inherit' },
{ tag: t.attributeName, color: 'inherit' },
{ tag: t.propertyName, color: 'inherit' },
{ tag: t.className, color: 'inherit' },
{ tag: t.invalid, color: 'inherit' },
{ tag: [t.unit, t.punctuation], color: 'inherit' },
{ tag: t.labelName, color: '#0f380f' },
{ tag: t.keyword, color: '#0f380f' },
{ tag: t.operator, color: '#0f380f' },
{ tag: t.special(t.variableName), color: '#0f380f' },
{ tag: t.typeName, color: '#0f380f' },
{ tag: t.atom, color: '#0f380f' },
{ tag: t.number, color: '#0f380f' },
{ tag: t.definition(t.variableName), color: '#0f380f' },
{ tag: t.string, color: '#0f380f' },
{ tag: t.special(t.string), color: '#0f380f' },
{ tag: t.comment, color: '#0f380f' },
{ tag: t.variableName, color: '#0f380f' },
{ tag: t.tagName, color: '#0f380f' },
{ tag: t.bracket, color: '#0f380f' },
{ tag: t.meta, color: '#0f380f' },
{ tag: t.attributeName, color: '#0f380f' },
{ tag: t.propertyName, color: '#0f380f' },
{ tag: t.className, color: '#0f380f' },
{ tag: t.invalid, color: '#0f380f' },
{ tag: [t.unit, t.punctuation], color: '#0f380f' },
],
});
+20 -20
View File
@@ -15,25 +15,25 @@ export default createTheme({
theme: 'dark',
settings,
styles: [
{ tag: t.labelName, color: 'inherit' },
{ tag: t.keyword, color: 'inherit' },
{ tag: t.operator, color: 'inherit' },
{ tag: t.special(t.variableName), color: 'inherit' },
{ tag: t.typeName, color: 'inherit' },
{ tag: t.atom, color: 'inherit' },
{ tag: t.number, color: 'inherit' },
{ tag: t.definition(t.variableName), color: 'inherit' },
{ tag: t.string, color: 'inherit' },
{ tag: t.special(t.string), color: 'inherit' },
{ tag: t.comment, color: 'inherit' },
{ tag: t.variableName, color: 'inherit' },
{ tag: t.tagName, color: 'inherit' },
{ tag: t.bracket, color: 'inherit' },
{ tag: t.meta, color: 'inherit' },
{ tag: t.attributeName, color: 'inherit' },
{ tag: t.propertyName, color: 'inherit' },
{ tag: t.className, color: 'inherit' },
{ tag: t.invalid, color: 'inherit' },
{ tag: [t.unit, t.punctuation], color: 'inherit' },
{ tag: t.labelName, color: 'white' },
{ tag: t.keyword, color: 'white' },
{ tag: t.operator, color: 'white' },
{ tag: t.special(t.variableName), color: 'white' },
{ tag: t.typeName, color: 'white' },
{ tag: t.atom, color: 'white' },
{ tag: t.number, color: 'white' },
{ tag: t.definition(t.variableName), color: 'white' },
{ tag: t.string, color: 'white' },
{ tag: t.special(t.string), color: 'white' },
{ tag: t.comment, color: 'white' },
{ tag: t.variableName, color: 'white' },
{ tag: t.tagName, color: 'white' },
{ tag: t.bracket, color: 'white' },
{ tag: t.meta, color: 'white' },
{ tag: t.attributeName, color: 'white' },
{ tag: t.propertyName, color: 'white' },
{ tag: t.className, color: 'white' },
{ tag: t.invalid, color: 'white' },
{ tag: [t.unit, t.punctuation], color: 'white' },
],
});
+20 -20
View File
@@ -18,25 +18,25 @@ export default createTheme({
theme: 'dark',
settings,
styles: [
{ tag: t.labelName, color: 'inherit' },
{ tag: t.keyword, color: 'inherit' },
{ tag: t.operator, color: 'inherit' },
{ tag: t.special(t.variableName), color: 'inherit' },
{ tag: t.typeName, color: 'inherit' },
{ tag: t.atom, color: 'inherit' },
{ tag: t.number, color: 'inherit' },
{ tag: t.definition(t.variableName), color: 'inherit' },
{ tag: t.string, color: 'inherit' },
{ tag: t.special(t.string), color: 'inherit' },
{ tag: t.comment, color: 'inherit' },
{ tag: t.variableName, color: 'inherit' },
{ tag: t.tagName, color: 'inherit' },
{ tag: t.bracket, color: 'inherit' },
{ tag: t.meta, color: 'inherit' },
{ tag: t.attributeName, color: 'inherit' },
{ tag: t.propertyName, color: 'inherit' },
{ tag: t.className, color: 'inherit' },
{ tag: t.invalid, color: 'inherit' },
{ tag: [t.unit, t.punctuation], color: 'inherit' },
{ tag: t.labelName, color: 'white' },
{ tag: t.keyword, color: 'white' },
{ tag: t.operator, color: 'white' },
{ tag: t.special(t.variableName), color: 'white' },
{ tag: t.typeName, color: 'white' },
{ tag: t.atom, color: 'white' },
{ tag: t.number, color: 'white' },
{ tag: t.definition(t.variableName), color: 'white' },
{ tag: t.string, color: 'white' },
{ tag: t.special(t.string), color: 'white' },
{ tag: t.comment, color: 'white' },
{ tag: t.variableName, color: 'white' },
{ tag: t.tagName, color: 'white' },
{ tag: t.bracket, color: 'white' },
{ tag: t.meta, color: 'white' },
{ tag: t.attributeName, color: 'white' },
{ tag: t.propertyName, color: 'white' },
{ tag: t.className, color: 'white' },
{ tag: t.invalid, color: 'white' },
{ tag: [t.unit, t.punctuation], color: 'white' },
],
});
+19 -19
View File
@@ -14,24 +14,24 @@ export default createTheme({
theme: 'dark',
settings,
styles: [
{ tag: t.labelName, color: 'inherit' },
{ tag: t.keyword, color: 'inherit' },
{ tag: t.operator, color: 'inherit' },
{ tag: t.special(t.variableName), color: 'inherit' },
{ tag: t.typeName, color: 'inherit' },
{ tag: t.atom, color: 'inherit' },
{ tag: t.number, color: 'inherit' },
{ tag: t.definition(t.variableName), color: 'inherit' },
{ tag: t.string, color: 'inherit' },
{ tag: t.special(t.string), color: 'inherit' },
{ tag: t.comment, color: 'inherit' },
{ tag: t.variableName, color: 'inherit' },
{ tag: t.tagName, color: 'inherit' },
{ tag: t.bracket, color: 'inherit' },
{ tag: t.meta, color: 'inherit' },
{ tag: t.attributeName, color: 'inherit' },
{ tag: t.propertyName, color: 'inherit' },
{ tag: t.className, color: 'inherit' },
{ tag: t.invalid, color: 'inherit' },
{ tag: t.labelName, color: '#41FF00' },
{ tag: t.keyword, color: '#41FF00' },
{ tag: t.operator, color: '#41FF00' },
{ tag: t.special(t.variableName), color: '#41FF00' },
{ tag: t.typeName, color: '#41FF00' },
{ tag: t.atom, color: '#41FF00' },
{ tag: t.number, color: '#41FF00' },
{ tag: t.definition(t.variableName), color: '#41FF00' },
{ tag: t.string, color: '#41FF00' },
{ tag: t.special(t.string), color: '#41FF00' },
{ tag: t.comment, color: '#41FF00' },
{ tag: t.variableName, color: '#41FF00' },
{ tag: t.tagName, color: '#41FF00' },
{ tag: t.bracket, color: '#41FF00' },
{ tag: t.meta, color: '#41FF00' },
{ tag: t.attributeName, color: '#41FF00' },
{ tag: t.propertyName, color: '#41FF00' },
{ tag: t.className, color: '#41FF00' },
{ tag: t.invalid, color: '#41FF00' },
],
});
+19 -19
View File
@@ -16,24 +16,24 @@ export default createTheme({
theme: 'light',
settings,
styles: [
{ tag: t.labelName, color: 'inherit' },
{ tag: t.keyword, color: 'inherit' },
{ tag: t.operator, color: 'inherit' },
{ tag: t.special(t.variableName), color: 'inherit' },
{ tag: t.typeName, color: 'inherit' },
{ tag: t.atom, color: 'inherit' },
{ tag: t.number, color: 'inherit' },
{ tag: t.definition(t.variableName), color: 'inherit' },
{ tag: t.string, color: 'inherit' },
{ tag: t.special(t.string), color: 'inherit' },
{ tag: t.comment, color: 'inherit' },
{ tag: t.variableName, color: 'inherit' },
{ tag: t.tagName, color: 'inherit' },
{ tag: t.bracket, color: 'inherit' },
{ tag: t.meta, color: 'inherit' },
{ tag: t.attributeName, color: 'inherit' },
{ tag: t.propertyName, color: 'inherit' },
{ tag: t.className, color: 'inherit' },
{ tag: t.invalid, color: 'inherit' },
{ tag: t.labelName, color: 'black' },
{ tag: t.keyword, color: 'black' },
{ tag: t.operator, color: 'black' },
{ tag: t.special(t.variableName), color: 'black' },
{ tag: t.typeName, color: 'black' },
{ tag: t.atom, color: 'black' },
{ tag: t.number, color: 'black' },
{ tag: t.definition(t.variableName), color: 'black' },
{ tag: t.string, color: 'black' },
{ tag: t.special(t.string), color: 'black' },
{ tag: t.comment, color: 'black' },
{ tag: t.variableName, color: 'black' },
{ tag: t.tagName, color: 'black' },
{ tag: t.bracket, color: 'black' },
{ tag: t.meta, color: 'black' },
{ tag: t.attributeName, color: 'black' },
{ tag: t.propertyName, color: 'black' },
{ tag: t.className, color: 'black' },
{ tag: t.invalid, color: 'black' },
],
});
-7
View File
@@ -133,10 +133,3 @@ registerWidget('_pitchwheel', (id, options = {}, pat) => {
const ctx = getCanvasWidget(id, options).getContext('2d');
return pat.pitchwheel({ ...options, ctx, id });
});
registerWidget('_spectrum', (id, options = {}, pat) => {
let _size = options.size || 200;
options = { width: _size, height: _size, ...options, size: _size / 5 };
const ctx = getCanvasWidget(id, options).getContext('2d');
return pat.spectrum({ ...options, ctx, id });
});
+43 -12
View File
@@ -73,6 +73,49 @@ export function registerControl(names, ...aliases) {
*/
export const { s, sound } = registerControl(['s', 'n', 'gain'], 'sound');
/**
* generic macro param for a sound synth
*
* @name z1
* @synonyms z
* @param {number | Pattern}
* @example
* n(run(8)).scale("D:pentatonic").s("supersaw").z(".01 .75").release(0.5)
*
*/
export const { z1, z } = registerControl(['z1', 'z2', 'z3', 'z4'], 'z');
/**
* generic macro param for a sound synth
*
* @name z2
* @param {number | Pattern}
* @example
* n(run(8)).scale("D:pentatonic").s("supersaw").z2("2 .7").release(0.5)
*
*/
export const { z2 } = registerControl('z2');
/**
* generic macro param for a sound synth
*
* @name z3
* @param {number | Pattern}
* @example
* n(run(8)).scale("D:pentatonic").s("supersaw").z3(".1 .75").release(0.5)
*
*/
export const { z3 } = registerControl('z3');
/**
* generic macro param for a sound synth
*
* @name z4
* @param {number | Pattern}
*
*/
export const { z4 } = registerControl('z4');
/**
* Define a custom webaudio node to use as a sound source.
*
@@ -546,18 +589,6 @@ export const { cut } = registerControl('cut');
*/
export const { cutoff, ctf, lpf, lp } = registerControl(['cutoff', 'resonance', 'lpenv'], 'ctf', 'lpf', 'lp');
/**
* Sets the lowpass filter lfo modulation depth.
* @name lplfo
* @param {number | Pattern} modulation depth of the lowpass filter lfo between 0 and _n_
* @example
* note("c2 e2 f2 g2")
* .sound('sawtooth')
* .lpf(300)
* .lplfo(2)
*/
export const { lplfo } = registerControl('lplfo');
/**
* Sets the lowpass filter envelope modulation depth.
* @name lpenv
-4
View File
@@ -68,10 +68,6 @@ export class Cyclist {
// see https://github.com/tidalcycles/strudel/pull/1004
const deadline = targetTime - phase;
onTrigger?.(hap, deadline, duration, this.cps, targetTime);
if (hap.value.cps !== undefined && this.cps != hap.value.cps) {
this.cps = hap.value.cps;
this.num_ticks_since_cps_change = 0;
}
}
});
} catch (e) {
-1
View File
@@ -14,7 +14,6 @@ export * from './controls.mjs';
export * from './hap.mjs';
export * from './pattern.mjs';
export * from './signal.mjs';
export * from './pick.mjs';
export * from './state.mjs';
export * from './timespan.mjs';
export * from './util.mjs';
+1 -1
View File
@@ -36,6 +36,6 @@
"gitHead": "0e26d4e741500f5bae35b023608f062a794905c2",
"devDependencies": {
"vite": "^5.0.10",
"vitest": "^2.1.3"
"vitest": "^1.1.0"
}
}
+33 -151
View File
@@ -809,6 +809,15 @@ export class Pattern {
//////////////////////////////////////////////////////////////////////
// Multi-pattern functions
/**
* Stacks the given pattern(s) to the current pattern.
* @name stack
* @memberof Pattern
* @example
* s("hh*4").stack(
* note("c4(5,8)")
* )
*/
stack(...pats) {
return stack(this, ...pats);
}
@@ -817,9 +826,30 @@ export class Pattern {
return sequence(this, ...pats);
}
/**
* Appends the given pattern(s) to the current pattern.
* @name seq
* @memberof Pattern
* @synonyms sequence, fastcat
* @example
* s("hh*4").seq(
* note("c4(5,8)")
* )
*/
seq(...pats) {
return sequence(this, ...pats);
}
/**
* Appends the given pattern(s) to the next cycle.
* @name cat
* @memberof Pattern
* @synonyms slowcat
* @example
* s("hh*4").cat(
* note("c4(5,8)")
* )
*/
cat(...pats) {
return cat(this, ...pats);
}
@@ -1023,7 +1053,6 @@ function _composeOp(a, b, func) {
div: [numeralArgs((a, b) => a / b)],
mod: [numeralArgs(_mod)],
pow: [numeralArgs(Math.pow)],
log2: [numeralArgs(Math.log2)],
band: [numeralArgs((a, b) => a & b)],
bor: [numeralArgs((a, b) => a | b)],
bxor: [numeralArgs((a, b) => a ^ b)],
@@ -1251,12 +1280,6 @@ export function reify(thing) {
* @example
* stack("g3", "b3", ["e4", "d4"]).note()
* // "g3,b3,[e4,d4]".note()
*
* @example
* // As a chained function:
* s("hh*4").stack(
* note("c4(5,8)")
* )
*/
export function stack(...pats) {
// Array test here is to avoid infinite recursions..
@@ -1385,11 +1408,6 @@ export function slowcatPrime(...pats) {
* cat("e5", "b4", ["d5", "c5"]).note()
* // "<e5 b4 [d5 c5]>".note()
*
* @example
* // As a chained function:
* s("hh*4").cat(
* note("c4(5,8)")
* )
*/
export function cat(...pats) {
return slowcat(...pats);
@@ -1412,36 +1430,6 @@ export function arrange(...sections) {
return s_cat(...sections).slow(total);
}
/**
* Similarly to `arrange`, allows you to arrange multiple patterns together over multiple cycles.
* Unlike `arrange`, you specify a start and stop time for each pattern rather than duration, which
* means that patterns can overlap.
* @return {Pattern}
* @example
seqPLoop([0, 2, "bd(3,8)"],
[1, 3, "cp(3,8)"]
)
.sound()
*/
export function seqPLoop(...parts) {
let total = Fraction(0);
const pats = [];
for (let part of parts) {
if (part.length == 2) {
part.unshift(total);
}
total = part[1];
}
return stack(
...parts.map(([start, stop, pat]) =>
pure(reify(pat)).compress(Fraction(start).div(total), Fraction(stop).div(total)),
),
)
.slow(total)
.innerJoin(); // or resetJoin or restartJoin ??
}
export function fastcat(...pats) {
let result = slowcat(...pats);
if (pats.length > 1) {
@@ -1460,18 +1448,12 @@ export function sequence(...pats) {
}
/** Like **cat**, but the items are crammed into one cycle.
* @synonyms sequence, fastcat
* @synonyms fastcat, sequence
* @example
* seq("e5", "b4", ["d5", "c5"]).note()
* // "e5 b4 [d5 c5]".note()
*
* @example
* // As a chained function:
* s("hh*4").seq(
* note("c4(5,8)")
* )
*/
export function seq(...pats) {
return fastcat(...pats);
}
@@ -1492,14 +1474,6 @@ function _sequenceCount(x) {
export const mask = curry((a, b) => reify(b).mask(a));
export const struct = curry((a, b) => reify(b).struct(a));
export const superimpose = curry((a, b) => reify(b).superimpose(...a));
export const withValue = curry((a, b) => reify(b).withValue(a));
export const bind = curry((a, b) => reify(b).bind(a));
export const innerBind = curry((a, b) => reify(b).innerBind(a));
export const outerBind = curry((a, b) => reify(b).outerBind(a));
export const squeezeBind = curry((a, b) => reify(b).squeezeBind(a));
export const stepBind = curry((a, b) => reify(b).stepBind(a));
export const polyBind = curry((a, b) => reify(b).polyBind(a));
// operators
export const set = curry((a, b) => reify(b).set(a));
@@ -2042,34 +2016,6 @@ export const { zoomArc, zoomarc } = register(['zoomArc', 'zoomarc'], function (a
return pat.zoom(a.begin, a.end);
});
/**
* Splits a pattern into the given number of slices, and plays them according to a pattern of slice numbers.
* Similar to `slice`, but slices up patterns rather than sound samples.
* @param {number} number of slices
* @param {number} slices to play
* @example
* note("0 1 2 3 4 5 6 7".scale('c:mixolydian'))
*.bite(4, "3 2 1 0")
* @example
* sound("bd - bd bd*2, - sd:6 - sd:5 sd:1 - [- sd:2] -, hh [- cp:7]")
.bank("RolandTR909").speed(1.2)
.bite(4, "0 0 [1 2] <3 2> 0 0 [2 1] 3")
*/
export const bite = register(
'bite',
(npat, ipat, pat) => {
return ipat
.fmap((i) => (n) => {
const a = Fraction(i).div(n).mod(1);
const b = a.add(Fraction(1).div(n));
return pat.zoom(a, b);
})
.appLeft(npat)
.squeezeJoin();
},
false,
);
/**
* Selects the given fraction of the pattern and repeats that part to fill the remainder of the cycle.
* @param {number} fraction fraction to select
@@ -2501,37 +2447,6 @@ Pattern.prototype.tag = function (tag) {
return this.withContext((ctx) => ({ ...ctx, tags: (ctx.tags || []).concat([tag]) }));
};
/**
* Filters haps using the given function
* @name filter
* @param {Function} test function to test Hap
* @example
* s("hh!7 oh").filter(hap => hap.value.s==='hh')
*/
export const filter = register('filter', (test, pat) => pat.withHaps((haps) => haps.filter(test)));
/**
* Filters haps by their begin time
* @name filterWhen
* @noAutocomplete
* @param {Function} test function to test Hap.whole.begin
*/
export const filterWhen = register('filterWhen', (test, pat) => pat.filter((h) => test(h.whole.begin)));
/**
* Use within to apply a function to only a part of a pattern.
* @name within
* @param {number} start start within cycle (0 - 1)
* @param {number} end end within cycle (0 - 1). Must be > start
* @param {Function} func function to be applied to the sub-pattern
*/
export const within = register('within', (a, b, fn, pat) =>
stack(
fn(pat.filterWhen((t) => t.cyclePos() >= a && t.cyclePos() <= b)),
pat.filterWhen((t) => t.cyclePos() < a || t.cyclePos() > b),
),
);
//////////////////////////////////////////////////////////////////////
// Tactus-related functions, i.e. ones that do stepwise
// transformations
@@ -2548,10 +2463,6 @@ Pattern.prototype.stepJoin = function () {
return new Pattern(q, first_t);
};
Pattern.prototype.stepBind = function (func) {
return this.fmap(func).stepJoin();
};
export function _retime(timedHaps) {
const occupied_perc = timedHaps.filter((t, pat) => pat.hasTactus).reduce((a, b) => a.add(b), Fraction(0));
const occupied_tactus = removeUndefineds(timedHaps.map((t, pat) => pat.tactus)).reduce(
@@ -2606,7 +2517,7 @@ export const steps = register('steps', function (targetTactus, pat) {
// avoid divide by zero..
return nothing;
}
return pat._fast(Fraction(targetTactus).div(pat.tactus)).setTactus(targetTactus);
return pat.fast(Fraction(targetTactus).div(pat.tactus));
});
export function _polymeterListSteps(steps, ...args) {
@@ -2814,7 +2725,7 @@ export const s_sub = stepRegister('s_sub', function (i, pat) {
return pat.s_add(pat.tactus.sub(i));
});
export const s_extend = stepRegister('s_extend', function (factor, pat) {
export const s_cycles = stepRegister('s_extend', function (factor, pat) {
return pat.fast(factor).s_expand(factor);
});
@@ -2895,13 +2806,6 @@ export const s_tour = function (pat, ...many) {
return pat.s_tour(...many);
};
const s_zip = function (...pats) {
pats = pats.filter((pat) => pat.hasTactus);
const zipped = slowcat(...pats.map((pat) => pat._slow(pat.tactus)));
// Should maybe use lcm or gcd for tactus?
return zipped._fast(pats[0].tactus).setTactus(pats[0].tactus);
};
//////////////////////////////////////////////////////////////////////
// Control-related functions, i.e. ones that manipulate patterns of
// objects
@@ -3113,25 +3017,3 @@ export let xfade = (a, pos, b) => {
Pattern.prototype.xfade = function (pos, b) {
return xfade(this, pos, b);
};
/**
* creates a structure pattern from divisions of a cycle
* especially useful for creating rhythms
* @name beat
* @example
* s("bd").beat("0:7:10", 16)
* @example
* s("sd").beat("4:12", 16)
*/
const __beat = (join) => (t, div, pat) => {
t = Fraction(t).mod(div);
div = Fraction(div);
const b = t.div(div);
const e = t.add(1).div(div);
return join(pat.fmap((x) => pure(x)._compress(b, e)));
};
export const { beat } = register(
['beat'],
__beat((x) => x.innerJoin()),
);
-214
View File
@@ -1,214 +0,0 @@
/*
pick.mjs - methods that use one pattern to pick events from other patterns.
Copyright (C) 2024 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/signal.mjs>
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 { Pattern, reify, silence, register } from './pattern.mjs';
import { _mod, clamp, objectMap } from './util.mjs';
const _pick = function (lookup, pat, modulo = true) {
const array = Array.isArray(lookup);
const len = Object.keys(lookup).length;
lookup = objectMap(lookup, reify);
if (len === 0) {
return silence;
}
return pat.fmap((i) => {
let key = i;
if (array) {
key = modulo ? Math.round(key) % len : clamp(Math.round(key), 0, lookup.length - 1);
}
return lookup[key];
});
};
/** * Picks patterns (or plain values) either from a list (by index) or a lookup table (by name).
* Similar to `inhabit`, but maintains the structure of the original patterns.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
* @example
* note("<0 1 2!2 3>".pick(["g a", "e f", "f g f g" , "g c d"]))
* @example
* sound("<0 1 [2,0]>".pick(["bd sd", "cp cp", "hh hh"]))
* @example
* sound("<0!2 [0,1] 1>".pick(["bd(3,8)", "sd sd"]))
* @example
* s("<a!2 [a,b] b>".pick({a: "bd(3,8)", b: "sd sd"}))
*/
export const pick = function (lookup, pat) {
// backward compatibility - the args used to be flipped
if (Array.isArray(pat)) {
[pat, lookup] = [lookup, pat];
}
return __pick(lookup, pat);
};
const __pick = register('pick', function (lookup, pat) {
return _pick(lookup, pat, false).innerJoin();
});
/** * The same as `pick`, but if you pick a number greater than the size of the list,
* it wraps around, rather than sticking at the maximum value.
* For example, if you pick the fifth pattern of a list of three, you'll get the
* second one.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
*/
export const pickmod = register('pickmod', function (lookup, pat) {
return _pick(lookup, pat, true).innerJoin();
});
/** * pickF lets you use a pattern of numbers to pick which function to apply to another pattern.
* @param {Pattern} pat
* @param {Pattern} lookup a pattern of indices
* @param {function[]} funcs the array of functions from which to pull
* @returns {Pattern}
* @example
* s("bd [rim hh]").pickF("<0 1 2>", [rev,jux(rev),fast(2)])
* @example
* note("<c2 d2>(3,8)").s("square")
* .pickF("<0 2> 1", [jux(rev),fast(2),x=>x.lpf(800)])
*/
export const pickF = register('pickF', function (lookup, funcs, pat) {
return pat.apply(pick(lookup, funcs));
});
/** * The same as `pickF`, but if you pick a number greater than the size of the functions list,
* it wraps around, rather than sticking at the maximum value.
* @param {Pattern} pat
* @param {Pattern} lookup a pattern of indices
* @param {function[]} funcs the array of functions from which to pull
* @returns {Pattern}
*/
export const pickmodF = register('pickmodF', function (lookup, funcs, pat) {
return pat.apply(pickmod(lookup, funcs));
});
/** * Similar to `pick`, but it applies an outerJoin instead of an innerJoin.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
*/
export const pickOut = register('pickOut', function (lookup, pat) {
return _pick(lookup, pat, false).outerJoin();
});
/** * The same as `pickOut`, but if you pick a number greater than the size of the list,
* it wraps around, rather than sticking at the maximum value.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
*/
export const pickmodOut = register('pickmodOut', function (lookup, pat) {
return _pick(lookup, pat, true).outerJoin();
});
/** * Similar to `pick`, but the choosen pattern is restarted when its index is triggered.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
*/
export const pickRestart = register('pickRestart', function (lookup, pat) {
return _pick(lookup, pat, false).restartJoin();
});
/** * The same as `pickRestart`, but if you pick a number greater than the size of the list,
* it wraps around, rather than sticking at the maximum value.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
* @example
* "<a@2 b@2 c@2 d@2>".pickRestart({
a: n("0 1 2 0"),
b: n("2 3 4 ~"),
c: n("[4 5] [4 3] 2 0"),
d: n("0 -3 0 ~")
}).scale("C:major").s("piano")
*/
export const pickmodRestart = register('pickmodRestart', function (lookup, pat) {
return _pick(lookup, pat, true).restartJoin();
});
/** * Similar to `pick`, but the choosen pattern is reset when its index is triggered.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
*/
export const pickReset = register('pickReset', function (lookup, pat) {
return _pick(lookup, pat, false).resetJoin();
});
/** * The same as `pickReset`, but if you pick a number greater than the size of the list,
* it wraps around, rather than sticking at the maximum value.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
*/
export const pickmodReset = register('pickmodReset', function (lookup, pat) {
return _pick(lookup, pat, true).resetJoin();
});
/**
/** * Picks patterns (or plain values) either from a list (by index) or a lookup table (by name).
* Similar to `pick`, but cycles are squeezed into the target ('inhabited') pattern.
* @name inhabit
* @synonyms pickSqueeze
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
* @example
* "<a b [a,b]>".inhabit({a: s("bd(3,8)"),
b: s("cp sd")
})
* @example
* s("a@2 [a b] a".inhabit({a: "bd(3,8)", b: "sd sd"})).slow(4)
*/
export const { inhabit, pickSqueeze } = register(['inhabit', 'pickSqueeze'], function (lookup, pat) {
return _pick(lookup, pat, false).squeezeJoin();
});
/** * The same as `inhabit`, but if you pick a number greater than the size of the list,
* it wraps around, rather than sticking at the maximum value.
* For example, if you pick the fifth pattern of a list of three, you'll get the
* second one.
* @name inhabitmod
* @synonyms pickmodSqueeze
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
*/
export const { inhabitmod, pickmodSqueeze } = register(['inhabitmod', 'pickmodSqueeze'], function (lookup, pat) {
return _pick(lookup, pat, true).squeezeJoin();
});
/**
* Pick from the list of values (or patterns of values) via the index using the given
* pattern of integers. The selected pattern will be compressed to fit the duration of the selecting event
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
* @example
* note(squeeze("<0@2 [1!2] 2>", ["g a", "f g f g" , "g a c d"]))
*/
export const squeeze = (pat, xs) => {
xs = xs.map(reify);
if (xs.length == 0) {
return silence;
}
return pat
.fmap((i) => {
const key = _mod(Math.round(i), xs.length);
return xs[key];
})
.squeezeJoin();
};
+2 -39
View File
@@ -63,13 +63,11 @@ export function repl({
let pPatterns = {};
let anonymousIndex = 0;
let allTransform;
let eachTransform;
const hush = function () {
pPatterns = {};
anonymousIndex = 0;
allTransform = undefined;
eachTransform = undefined;
return silence;
};
@@ -86,42 +84,15 @@ export function repl({
const toggle = () => scheduler.toggle();
const setCps = (cps) => scheduler.setCps(cps);
const setCpm = (cpm) => scheduler.setCps(cpm / 60);
// TODO - not documented as jsdoc examples as the test framework doesn't simulate enough context for `each` and `all`..
/** Applies a function to all the running patterns. Note that the patterns are groups together into a single `stack` before the function is applied. This is probably what you want, but see `each` for
* a version that applies the function to each pattern separately.
* ```
* $: sound("bd - cp sd")
* $: sound("hh*8")
* all(fast("<2 3>"))
* ```
* ```
* $: sound("bd - cp sd")
* $: sound("hh*8")
* all(x => x.pianoroll())
* ```
*/
const all = function (transform) {
allTransform = transform;
return silence;
};
/** Applies a function to each of the running patterns separately. This is intended for future use with upcoming 'stepwise' features. See `all` for a version that applies the function to all the patterns stacked together into a single pattern.
* ```
* $: sound("bd - cp sd")
* $: sound("hh*8")
* each(fast("<2 3>"))
* ```
*/
const each = function (transform) {
eachTransform = transform;
return silence;
};
// set pattern methods that use this repl via closure
const injectPatternMethods = () => {
Pattern.prototype.p = function (id) {
if (typeof id === 'string' && (id.startsWith('_') || id.endsWith('_'))) {
if (id.startsWith('_') || id.endsWith('_')) {
// allows muting a pattern x with x_ or _x
return silence;
}
@@ -160,7 +131,6 @@ export function repl({
});
return evalScope({
all,
each,
hush,
cpm,
setCps,
@@ -182,14 +152,7 @@ export function repl({
shouldHush && hush();
let { pattern, meta } = await _evaluate(code, transpiler, transpilerOptions);
if (Object.keys(pPatterns).length) {
let patterns = Object.values(pPatterns);
if (eachTransform) {
// Explicit lambda so only element (not index and array) are passed
patterns = patterns.map((x) => eachTransform(x));
}
pattern = stack(...patterns);
} else if (eachTransform) {
pattern = eachTransform(pattern);
pattern = stack(...Object.values(pPatterns));
}
if (allTransform) {
pattern = allTransform(pattern);
+223 -108
View File
@@ -1,14 +1,13 @@
/*
signal.mjs - continuous patterns
Copyright (C) 2024 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/signal.mjs>
signal.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/signal.mjs>
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 { Hap } from './hap.mjs';
import { Pattern, fastcat, pure, register, reify, silence, stack } from './pattern.mjs';
import { Pattern, fastcat, reify, silence, stack, register } from './pattern.mjs';
import Fraction from './fraction.mjs';
import { id, keyAlias, getCurrentKeyboardState } from './util.mjs';
import { id, _mod, clamp, objectMap } from './util.mjs';
export function steady(value) {
// A continuous value
@@ -160,37 +159,6 @@ const timeToRands = (t, n) => timeToRandsPrime(timeToIntSeed(t), n);
*/
export const run = (n) => saw.range(0, n).floor().segment(n);
/**
* Creates a pattern from a binary number.
*
* @name binary
* @param {number} n - input number to convert to binary
* @example
* "hh".s().struct(binary(5))
* // "hh".s().struct("1 0 1")
*/
export const binary = (n) => {
const nBits = reify(n).log2(0).floor().add(1);
return binaryN(n, nBits);
};
/**
* Creates a pattern from a binary number, padded to n bits long.
*
* @name binaryN
* @param {number} n - input number to convert to binary
* @param {number} nBits - pattern length, defaults to 16
* @example
* "hh".s().struct(binaryN(55532, 16))
* // "hh".s().struct("1 1 0 1 1 0 0 0 1 1 1 0 1 1 0 0")
*/
export const binaryN = (n, nBits = 16) => {
nBits = reify(nBits);
// Shift and mask, putting msb on the right-side
const bitPos = run(nBits).mul(-1).add(nBits.sub(1));
return reify(n).segment(nBits).brshift(bitPos).band(pure(1));
};
export const randrun = (n) => {
return signal((t) => {
// Without adding 0.5, the first cycle is always 0,1,2,3,...
@@ -285,6 +253,211 @@ export const _irand = (i) => rand.fmap((x) => Math.trunc(x * i));
*/
export const irand = (ipat) => reify(ipat).fmap(_irand).innerJoin();
const _pick = function (lookup, pat, modulo = true) {
const array = Array.isArray(lookup);
const len = Object.keys(lookup).length;
lookup = objectMap(lookup, reify);
if (len === 0) {
return silence;
}
return pat.fmap((i) => {
let key = i;
if (array) {
key = modulo ? Math.round(key) % len : clamp(Math.round(key), 0, lookup.length - 1);
}
return lookup[key];
});
};
/** * Picks patterns (or plain values) either from a list (by index) or a lookup table (by name).
* Similar to `inhabit`, but maintains the structure of the original patterns.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
* @example
* note("<0 1 2!2 3>".pick(["g a", "e f", "f g f g" , "g c d"]))
* @example
* sound("<0 1 [2,0]>".pick(["bd sd", "cp cp", "hh hh"]))
* @example
* sound("<0!2 [0,1] 1>".pick(["bd(3,8)", "sd sd"]))
* @example
* s("<a!2 [a,b] b>".pick({a: "bd(3,8)", b: "sd sd"}))
*/
export const pick = function (lookup, pat) {
// backward compatibility - the args used to be flipped
if (Array.isArray(pat)) {
[pat, lookup] = [lookup, pat];
}
return __pick(lookup, pat);
};
const __pick = register('pick', function (lookup, pat) {
return _pick(lookup, pat, false).innerJoin();
});
/** * The same as `pick`, but if you pick a number greater than the size of the list,
* it wraps around, rather than sticking at the maximum value.
* For example, if you pick the fifth pattern of a list of three, you'll get the
* second one.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
*/
export const pickmod = register('pickmod', function (lookup, pat) {
return _pick(lookup, pat, true).innerJoin();
});
/** * pickF lets you use a pattern of numbers to pick which function to apply to another pattern.
* @param {Pattern} pat
* @param {Pattern} lookup a pattern of indices
* @param {function[]} funcs the array of functions from which to pull
* @returns {Pattern}
* @example
* s("bd [rim hh]").pickF("<0 1 2>", [rev,jux(rev),fast(2)])
* @example
* note("<c2 d2>(3,8)").s("square")
* .pickF("<0 2> 1", [jux(rev),fast(2),x=>x.lpf(800)])
*/
export const pickF = register('pickF', function (lookup, funcs, pat) {
return pat.apply(pick(lookup, funcs));
});
/** * The same as `pickF`, but if you pick a number greater than the size of the functions list,
* it wraps around, rather than sticking at the maximum value.
* @param {Pattern} pat
* @param {Pattern} lookup a pattern of indices
* @param {function[]} funcs the array of functions from which to pull
* @returns {Pattern}
*/
export const pickmodF = register('pickmodF', function (lookup, funcs, pat) {
return pat.apply(pickmod(lookup, funcs));
});
/** * Similar to `pick`, but it applies an outerJoin instead of an innerJoin.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
*/
export const pickOut = register('pickOut', function (lookup, pat) {
return _pick(lookup, pat, false).outerJoin();
});
/** * The same as `pickOut`, but if you pick a number greater than the size of the list,
* it wraps around, rather than sticking at the maximum value.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
*/
export const pickmodOut = register('pickmodOut', function (lookup, pat) {
return _pick(lookup, pat, true).outerJoin();
});
/** * Similar to `pick`, but the choosen pattern is restarted when its index is triggered.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
*/
export const pickRestart = register('pickRestart', function (lookup, pat) {
return _pick(lookup, pat, false).restartJoin();
});
/** * The same as `pickRestart`, but if you pick a number greater than the size of the list,
* it wraps around, rather than sticking at the maximum value.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
* @example
* "<a@2 b@2 c@2 d@2>".pickRestart({
a: n("0 1 2 0"),
b: n("2 3 4 ~"),
c: n("[4 5] [4 3] 2 0"),
d: n("0 -3 0 ~")
}).scale("C:major").s("piano")
*/
export const pickmodRestart = register('pickmodRestart', function (lookup, pat) {
return _pick(lookup, pat, true).restartJoin();
});
/** * Similar to `pick`, but the choosen pattern is reset when its index is triggered.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
*/
export const pickReset = register('pickReset', function (lookup, pat) {
return _pick(lookup, pat, false).resetJoin();
});
/** * The same as `pickReset`, but if you pick a number greater than the size of the list,
* it wraps around, rather than sticking at the maximum value.
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
*/
export const pickmodReset = register('pickmodReset', function (lookup, pat) {
return _pick(lookup, pat, true).resetJoin();
});
/**
/** * Picks patterns (or plain values) either from a list (by index) or a lookup table (by name).
* Similar to `pick`, but cycles are squeezed into the target ('inhabited') pattern.
* @name inhabit
* @synonyms pickSqueeze
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
* @example
* "<a b [a,b]>".inhabit({a: s("bd(3,8)"),
b: s("cp sd")
})
* @example
* s("a@2 [a b] a".inhabit({a: "bd(3,8)", b: "sd sd"})).slow(4)
*/
export const { inhabit, pickSqueeze } = register(['inhabit', 'pickSqueeze'], function (lookup, pat) {
return _pick(lookup, pat, false).squeezeJoin();
});
/** * The same as `inhabit`, but if you pick a number greater than the size of the list,
* it wraps around, rather than sticking at the maximum value.
* For example, if you pick the fifth pattern of a list of three, you'll get the
* second one.
* @name inhabitmod
* @synonyms pickmodSqueeze
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
*/
export const { inhabitmod, pickmodSqueeze } = register(['inhabitmod', 'pickmodSqueeze'], function (lookup, pat) {
return _pick(lookup, pat, true).squeezeJoin();
});
/**
* Pick from the list of values (or patterns of values) via the index using the given
* pattern of integers. The selected pattern will be compressed to fit the duration of the selecting event
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
* @example
* note(squeeze("<0@2 [1!2] 2>", ["g a", "f g f g" , "g a c d"]))
*/
export const squeeze = (pat, xs) => {
xs = xs.map(reify);
if (xs.length == 0) {
return silence;
}
return pat
.fmap((i) => {
const key = _mod(Math.round(i), xs.length);
return xs[key];
})
.squeezeJoin();
};
export const __chooseWith = (pat, xs) => {
xs = xs.map(reify);
if (xs.length == 0) {
@@ -423,11 +596,8 @@ export const perlinWith = (pat) => {
*/
export const perlin = perlinWith(time.fmap((v) => Number(v)));
export const degradeByWith = register(
'degradeByWith',
(withPat, x, pat) => pat.fmap((a) => (_) => a).appLeft(withPat.filterValues((v) => v > x)),
true,
true,
export const degradeByWith = register('degradeByWith', (withPat, x, pat) =>
pat.fmap((a) => (_) => a).appLeft(withPat.filterValues((v) => v > x)),
);
/**
@@ -444,14 +614,9 @@ export const degradeByWith = register(
* @example
* s("[hh?0.2]*8")
*/
export const degradeBy = register(
'degradeBy',
function (x, pat) {
return pat._degradeByWith(rand, x);
},
true,
true,
);
export const degradeBy = register('degradeBy', function (x, pat) {
return pat._degradeByWith(rand, x);
});
/**
*
@@ -465,7 +630,7 @@ export const degradeBy = register(
* @example
* s("[hh?]*8")
*/
export const degrade = register('degrade', (pat) => pat._degradeBy(0.5), true, true);
export const degrade = register('degrade', (pat) => pat._degradeBy(0.5));
/**
* Inverse of `degradeBy`: Randomly removes events from the pattern by a given amount.
@@ -485,17 +650,12 @@ export const degrade = register('degrade', (pat) => pat._degradeBy(0.5), true, t
* x => x.undegradeBy(0.8).pan(1)
* )
*/
export const undegradeBy = register(
'undegradeBy',
function (x, pat) {
return pat._degradeByWith(
rand.fmap((r) => 1 - r),
x,
);
},
true,
true,
);
export const undegradeBy = register('undegradeBy', function (x, pat) {
return pat._degradeByWith(
rand.fmap((r) => 1 - r),
x,
);
});
/**
* Inverse of `degrade`: Randomly removes 50% of events from the pattern. Shorthand for `.undegradeBy(0.5)`
@@ -512,7 +672,7 @@ export const undegradeBy = register(
* x => x.undegrade().pan(1)
* )
*/
export const undegrade = register('undegrade', (pat) => pat._undegradeBy(0.5), true, true);
export const undegrade = register('undegrade', (pat) => pat._undegradeBy(0.5));
/**
*
@@ -530,7 +690,7 @@ export const undegrade = register('undegrade', (pat) => pat._undegradeBy(0.5), t
export const sometimesBy = register('sometimesBy', function (patx, func, pat) {
return reify(patx)
.fmap((x) => stack(pat._degradeBy(x), func(pat)._undegradeBy(1 - x)))
.fmap((x) => stack(pat._degradeBy(x), func(pat._undegradeBy(1 - x))))
.innerJoin();
});
@@ -671,48 +831,3 @@ export const never = register('never', function (_, pat) {
export const always = register('always', function (func, pat) {
return func(pat);
});
//keyname: string | Array<string>
//keyname reference: https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values
export function _keyDown(keyname) {
if (Array.isArray(keyname) === false) {
keyname = [keyname];
}
const keyState = getCurrentKeyboardState();
return keyname.every((x) => {
const keyName = keyAlias.get(x) ?? x;
return keyState[keyName];
});
}
/**
*
* Do something on a keypress, or array of keypresses
* [Key name reference](https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values)
*
* @name whenKey
* @memberof Pattern
* @returns Pattern
* @example
* s("bd(5,8)").whenKey("Control:j", x => x.segment(16).color("red")).whenKey("Control:i", x => x.fast(2).color("blue"))
*/
export const whenKey = register('whenKey', function (input, func, pat) {
return pat.when(_keyDown(input), func);
});
/**
*
* returns true when a key or array of keys is held
* [Key name reference](https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values)
*
* @name keyDown
* @memberof Pattern
* @returns Pattern
* @example
* keyDown("Control:j").pick([s("bd(5,8)"), s("cp(3,8)")])
*/
export const keyDown = register('keyDown', function (pat) {
return pat.fmap(_keyDown);
});
-23
View File
@@ -46,14 +46,12 @@ import {
rev,
time,
run,
binaryN,
pick,
stackLeft,
stackRight,
stackCentre,
s_cat,
calculateTactus,
sometimes,
} from '../index.mjs';
import { steady } from '../signal.mjs';
@@ -960,18 +958,6 @@ describe('Pattern', () => {
expect(run(4).firstCycle()).toStrictEqual(sequence(0, 1, 2, 3).firstCycle());
});
});
describe('binaryN', () => {
it('Can make a binary pattern from a decimal', () => {
expect(binaryN(55532).firstCycle()).toStrictEqual(
sequence(1, 1, 0, 1, 1, 0, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0).firstCycle(),
);
});
it('Can make a binary pattern from patterned inputs', () => {
expect(binaryN(pure(0x1337), pure(14)).firstCycle()).toStrictEqual(
sequence(0, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1).firstCycle(),
);
});
});
describe('ribbon', () => {
it('Can ribbon', () => {
expect(cat(0, 1, 2, 3, 4, 5, 6, 7).ribbon(2, 4).fast(4).firstCycle()).toStrictEqual(
@@ -1267,13 +1253,4 @@ describe('Pattern', () => {
expect(s('bev').chop(8).loopAt(2).tactus).toStrictEqual(Fraction(4));
});
});
describe('sometimes', () => {
it('works with constant functions', () => {
expect(
pure('a')
.sometimes((x) => pure('b'))
.fast(16).firstCycleValues.length,
).toStrictEqual(16);
});
});
});
-32
View File
@@ -434,38 +434,6 @@ function getUnixTimeSeconds() {
return Date.now() * 0.001;
}
export const keyAlias = new Map([
['control', 'Control'],
['ctrl', 'Control'],
['alt', 'Alt'],
['shift', 'Shift'],
['down', 'ArrowDown'],
['up', 'ArrowUp'],
['left', 'ArrowLeft'],
['right', 'ArrowRight'],
]);
let keyState;
export function getCurrentKeyboardState() {
if (keyState == null) {
if (typeof window === 'undefined') {
return;
}
keyState = {};
// Listen for the keydown event to mark the key as pressed
window.addEventListener('keydown', (event) => {
keyState[event.key] = true; // Mark the key as pressed
});
// Listen for the keyup event to mark the key as released
window.addEventListener('keyup', (event) => {
keyState[event.key] = false; // Mark the key as released
});
}
return { ...keyState }; // Return a shallow copy of the key state object
}
// Floating point versions, see Fraction for rational versions
// // greatest common divisor
// export const gcd = function (x, y, ...z) {
+1 -1
View File
@@ -26,7 +26,7 @@ export const getDrawContext = (id = 'test-canvas', options) => {
}, 200);
});
}
return canvas.getContext(contextType, { willReadFrequently: true });
return canvas.getContext(contextType);
};
let animationFrames = {};
+1 -6
View File
@@ -169,18 +169,13 @@ const refs = {};
export async function midin(input) {
if (isPattern(input)) {
throw new Error(
`midin: does not accept Pattern as input. Make sure to pass device name with single quotes. Example: midin('${
`.midi does not accept Pattern input. Make sure to pass device name with single quotes. Example: .midi('${
WebMidi.outputs?.[0]?.name || 'IAC Driver Bus 1'
}')`,
);
}
const initial = await enableWebMidi(); // only returns on first init
const device = getDevice(input, WebMidi.inputs);
if (!device) {
throw new Error(
`midiin: device "${input}" not found.. connected devices: ${getMidiDeviceNamesString(WebMidi.inputs)}`,
);
}
if (initial) {
const otherInputs = WebMidi.inputs.filter((o) => o.name !== device.name);
logger(
+1 -1
View File
@@ -37,6 +37,6 @@
"devDependencies": {
"peggy": "^3.0.2",
"vite": "^5.0.10",
"vitest": "^2.1.3"
"vitest": "^1.1.0"
}
}
-3
View File
@@ -1,3 +0,0 @@
# @strudel/serial
This package adds webserial functionality to strudel Patterns, for e.g. sending messages to arduino microcontrollers.
-87
View File
@@ -1,87 +0,0 @@
/*
mqtt.mjs - for patterning the internet of things from strudel
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/serial/serial.mjs>
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 { Pattern, isPattern } from '@strudel/core';
import Paho from 'paho-mqtt';
const connections = {};
// Handle connection loss
function onConnectionLost(responseObject) {
if (responseObject.errorCode !== 0) {
console.error(' mqtt connection lost: ', responseObject.errorMessage);
}
}
// Handle received messages
function onMessageArrived(message) {
console.log('incoming mqtt message: ', message.payloadString); // prettier-ignore
}
function onFailure(err) {
console.error('Connection failed: ', err);
}
Pattern.prototype.mqtt = function (
username = undefined,
password = undefined,
topic = undefined,
host = 'wss://localhost:8883/',
client = undefined,
latency = 0,
) {
const key = host + '-' + client;
let connected = false;
if (!client) {
client = 'strudel-' + String(Math.floor(Math.random() * 1000000));
}
function onConnect() {
console.log('Connected to mqtt broker');
connected = true;
}
let cx;
if (connections[key]) {
cx = connections[key];
} else {
cx = new Paho.Client(host, client);
cx.onConnectionLost = onConnectionLost;
cx.onMessageArrived = onMessageArrived;
const props = {
onSuccess: onConnect,
onFailure: onFailure,
useSSL: true,
};
if (username) {
props.userName = username;
props.password = password;
}
cx.connect(props);
}
return this.withHap((hap) => {
const onTrigger = (t_deprecate, hap, currentTime, cps, targetTime) => {
if (!connected) {
return;
}
let message = '';
if (typeof hap.value === 'object') {
message = JSON.stringify(hap.value);
} else {
message = hap.value;
}
message = new Paho.Message(message);
message.destinationName = topic;
const offset = (targetTime - currentTime + latency) * 1000;
window.setTimeout(function () {
cx.send(message);
}, offset);
};
return hap.setContext({ ...hap.context, onTrigger, dominantTrigger: true });
});
};
-38
View File
@@ -1,38 +0,0 @@
{
"name": "@strudel/mqtt",
"version": "1.1.0",
"description": "MQTT API for strudel",
"main": "mqtt.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": [
"titdalcycles",
"strudel",
"pattern",
"livecoding",
"algorave"
],
"author": "Alex McLean <alex@slab.org>",
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel/core": "workspace:*",
"paho-mqtt": "^1.1.0"
},
"devDependencies": {
"vite": "^5.0.10"
}
}
-19
View File
@@ -1,19 +0,0 @@
import { defineConfig } from 'vite';
import { dependencies } from './package.json';
import { resolve } from 'path';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [],
build: {
lib: {
entry: resolve(__dirname, 'mqtt.mjs'),
formats: ['es'],
fileName: (ext) => ({ es: 'index.mjs' })[ext],
},
rollupOptions: {
external: [...Object.keys(dependencies)],
},
target: 'esnext',
},
});
-4
View File
@@ -10,8 +10,6 @@ if (typeof HTMLElement !== 'undefined') {
static observedAttributes = ['code'];
settings = codemirrorSettings.get();
editor = null;
sync = false;
solo = true;
constructor() {
super();
}
@@ -51,8 +49,6 @@ if (typeof HTMLElement !== 'undefined') {
});
this.dispatchEvent(event);
},
solo: this.solo,
sync: this.sync,
});
// init settings
this.editor.updateSettings(this.settings);
+2 -2
View File
@@ -68,7 +68,7 @@ Pattern.prototype.serial = function (br = 115200, sendcrc = false, singlecharids
if (!(name in writeMessagers)) {
getWriter(name, br);
}
const onTrigger = (t_deprecate, hap, currentTime, cps, targetTime) => {
const onTrigger = (time, hap, currentTime) => {
var message = '';
var chk = 0;
if (typeof hap.value === 'object') {
@@ -105,7 +105,7 @@ Pattern.prototype.serial = function (br = 115200, sendcrc = false, singlecharids
} else {
message = hap.value;
}
const offset = (targetTime - currentTime + latency) * 1000;
const offset = (time - currentTime + latency) * 1000;
window.setTimeout(function () {
writeMessagers[name](message, chk);
+3 -23
View File
@@ -112,27 +112,11 @@ export const getADSRValues = (params, curve = 'linear', defaultValues) => {
return [Math.max(a ?? 0, envmin), Math.max(d ?? 0, envmin), Math.min(sustain, envmax), Math.max(r ?? 0, releaseMin)];
};
export function createFilter(
context,
type,
frequency,
Q,
att,
dec,
sus,
rel,
fenv,
start,
end,
fanchor,
model,
drive,
flfo,
) {
export function createFilter(context, type, frequency, Q, att, dec, sus, rel, fenv, start, end, fanchor, model, drive) {
const curve = 'exponential';
const [attack, decay, sustain, release] = getADSRValues([att, dec, sus, rel], curve, [0.005, 0.14, 0, 0.1]);
let filter;
let frequencyParam, detuneParam;
let frequencyParam;
if (model === 'ladder') {
filter = getWorklet(context, 'ladder-processor', { frequency, q: Q, drive });
frequencyParam = filter.parameters.get('frequency');
@@ -142,7 +126,6 @@ export function createFilter(
filter.Q.value = Q;
filter.frequency.value = frequency;
frequencyParam = filter.frequency;
detuneParam = filter.detune;
}
// envelope is active when any of these values is set
@@ -157,10 +140,7 @@ export function createFilter(
let max = clamp(2 ** (fenvAbs - offset) * frequency, 0, 20000);
if (fenv < 0) [min, max] = [max, min];
getParamADSR(frequencyParam, attack, decay, sustain, release, min, max, start, end, curve);
}
const hasLFO = detuneParam && flfo !== undefined;
if (hasLFO !== undefined) {
let lfo = getVibratoOscillator(detuneParam, { vib: flfo, vibmod: 4 }, start);
return filter;
}
return filter;
}
+49 -62
View File
@@ -22,13 +22,16 @@ function humanFileSize(bytes, si) {
return bytes.toFixed(1) + ' ' + units[u];
}
// deduces relevant info for sample loading from hap.value and sample definition
// it encapsulates the core sampler logic into a pure and synchronous function
// hapValue: Hap.value, bank: sample bank definition for sound "s" (values in strudel.json format)
export function getSampleInfo(hapValue, bank) {
const { s, n = 0, speed = 1.0 } = hapValue;
let midi = valueToMidi(hapValue, 36);
let transpose = midi - 36; // C3 is middle C;
export const getSampleBufferSource = async (s, n, note, speed, freq, bank, resolveUrl) => {
let transpose = 0;
if (freq !== undefined && note !== undefined) {
logger('[sampler] hap has note and freq. ignoring note', 'warning');
}
let midi = valueToMidi({ freq, note }, 36);
transpose = midi - 36; // C3 is middle C
const ac = getAudioContext();
let sampleUrl;
let index = 0;
if (Array.isArray(bank)) {
@@ -47,54 +50,19 @@ export function getSampleInfo(hapValue, bank) {
index = getSoundIndex(n, bank[closest].length);
sampleUrl = bank[closest][index];
}
const label = `${s}:${index}`;
let playbackRate = Math.abs(speed) * Math.pow(2, transpose / 12);
return { transpose, sampleUrl, index, midi, label, playbackRate };
}
// takes hapValue and returns buffer + playbackRate.
export const getSampleBuffer = async (hapValue, bank, resolveUrl) => {
let { sampleUrl, label, playbackRate } = getSampleInfo(hapValue, bank);
if (resolveUrl) {
sampleUrl = await resolveUrl(sampleUrl);
}
const ac = getAudioContext();
const buffer = await loadBuffer(sampleUrl, ac, label);
if (hapValue.unit === 'c') {
playbackRate = playbackRate * buffer.duration;
}
return { buffer, playbackRate };
};
// creates playback ready AudioBufferSourceNode from hapValue
export const getSampleBufferSource = async (hapValue, bank, resolveUrl) => {
let { buffer, playbackRate } = await getSampleBuffer(hapValue, bank, resolveUrl);
if (hapValue.speed < 0) {
let buffer = await loadBuffer(sampleUrl, ac, s, index);
if (speed < 0) {
// should this be cached?
buffer = reverseBuffer(buffer);
}
const ac = getAudioContext();
const bufferSource = ac.createBufferSource();
bufferSource.buffer = buffer;
const playbackRate = 1.0 * Math.pow(2, transpose / 12);
bufferSource.playbackRate.value = playbackRate;
const { s, loopBegin = 0, loopEnd = 1, begin = 0, end = 1 } = hapValue;
// "The computation of the offset into the sound is performed using the sound buffer's natural sample rate,
// rather than the current playback rate, so even if the sound is playing at twice its normal speed,
// the midway point through a 10-second audio buffer is still 5."
const offset = begin * bufferSource.buffer.duration;
const loop = s.startsWith('wt_') ? 1 : hapValue.loop;
if (loop) {
bufferSource.loop = true;
bufferSource.loopStart = loopBegin * bufferSource.buffer.duration - offset;
bufferSource.loopEnd = loopEnd * bufferSource.buffer.duration - offset;
}
const bufferDuration = bufferSource.buffer.duration / bufferSource.playbackRate.value;
const sliceDuration = (end - begin) * bufferDuration;
return { bufferSource, offset, bufferDuration, sliceDuration };
return bufferSource;
};
export const loadBuffer = (url, ac, s, n = 0) => {
@@ -264,10 +232,10 @@ export const samples = async (sampleMap, baseUrl = sampleMap._base || '', option
const { prebake, tag } = options;
processSampleMap(
sampleMap,
(key, bank) =>
registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, bank), {
(key, value) =>
registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, value), {
type: 'sample',
samples: bank,
samples: value,
baseUrl,
prebake,
tag,
@@ -281,26 +249,38 @@ const cutGroups = [];
export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
let {
s,
freq,
unit,
nudge = 0, // TODO: is this in seconds?
cut,
loop,
clip = undefined, // if set, samples will be cut off when the hap ends
n = 0,
note,
speed = 1, // sample playback speed
loopBegin = 0,
begin = 0,
loopEnd = 1,
end = 1,
duration,
} = value;
// load sample
if (speed === 0) {
// no playback
return;
}
loop = s.startsWith('wt_') ? 1 : value.loop;
const ac = getAudioContext();
// destructure adsr here, because the default should be different for synths and samples
let [attack, decay, sustain, release] = getADSRValues([value.attack, value.decay, value.sustain, value.release]);
const { bufferSource, sliceDuration, offset } = await getSampleBufferSource(value, bank, resolveUrl);
let [attack, decay, sustain, release] = getADSRValues([value.attack, value.decay, value.sustain, value.release]);
//const soundfont = getSoundfontKey(s);
const time = t + nudge;
const bufferSource = await getSampleBufferSource(s, n, note, speed, freq, bank, resolveUrl);
// vibrato
let vibratoOscillator = getVibratoOscillator(bufferSource.detune, value, t);
// asny stuff above took too long?
if (ac.currentTime > t) {
@@ -312,19 +292,26 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
logger(`[sampler] could not load "${s}:${n}"`, 'error');
return;
}
// vibrato
let vibratoOscillator = getVibratoOscillator(bufferSource.detune, value, t);
const time = t + nudge;
bufferSource.playbackRate.value = Math.abs(speed) * bufferSource.playbackRate.value;
if (unit === 'c') {
// are there other units?
bufferSource.playbackRate.value = bufferSource.playbackRate.value * bufferSource.buffer.duration * 1; //cps;
}
// "The computation of the offset into the sound is performed using the sound buffer's natural sample rate,
// rather than the current playback rate, so even if the sound is playing at twice its normal speed,
// the midway point through a 10-second audio buffer is still 5."
const offset = begin * bufferSource.buffer.duration;
if (loop) {
bufferSource.loop = true;
bufferSource.loopStart = loopBegin * bufferSource.buffer.duration - offset;
bufferSource.loopEnd = loopEnd * bufferSource.buffer.duration - offset;
}
bufferSource.start(time, offset);
const envGain = ac.createGain();
const node = bufferSource.connect(envGain);
// if none of these controls is set, the duration of the sound will be set to the duration of the sample slice
if (clip == null && loop == null && value.release == null) {
duration = sliceDuration;
const bufferDuration = bufferSource.buffer.duration / bufferSource.playbackRate.value;
duration = (end - begin) * bufferDuration;
}
let holdEnd = t + duration;
+1 -5
View File
@@ -270,12 +270,11 @@ function getReverb(orbit, duration, fade, lp, dim, ir) {
export let analysers = {},
analysersData = {};
export function getAnalyserById(id, fftSize = 1024, smoothingTimeConstant = 0.5) {
export function getAnalyserById(id, fftSize = 1024) {
if (!analysers[id]) {
// make sure this doesn't happen too often as it piles up garbage
const analyserNode = getAudioContext().createAnalyser();
analyserNode.fftSize = fftSize;
analyserNode.smoothingTimeConstant = smoothingTimeConstant;
// getDestination().connect(analyserNode);
analysers[id] = analyserNode;
analysersData[id] = new Float32Array(analysers[id].frequencyBinCount);
@@ -357,7 +356,6 @@ export const superdough = async (value, t, hapDuration) => {
lpdecay,
lpsustain,
lprelease,
lplfo,
resonance = getDefaultValue('resonance'),
// high pass
hpenv,
@@ -423,7 +421,6 @@ export const superdough = async (value, t, hapDuration) => {
};
if (bank && s) {
s = `${bank}_${s}`;
value.s = s;
}
// get source AudioNode
@@ -476,7 +473,6 @@ export const superdough = async (value, t, hapDuration) => {
fanchor,
ftype,
drive,
lplfo,
);
chain.push(lp());
if (ftype === '24db') {
+3 -2
View File
@@ -73,8 +73,9 @@ export function registerSynthSounds() {
'supersaw',
(begin, value, onended) => {
const ac = getAudioContext();
let { duration, n, unison = 5, spread = 0.6, detune } = value;
detune = detune ?? n ?? 0.18;
const { z1, z2, z3 } = value;
let { duration, n, unison = z2 == null ? 5 : Math.round(z2 * 10), spread = z3 ?? 0.6, detune } = value;
detune = detune ?? n ?? z1 ?? 0.18;
const frequency = getFrequencyFromValue(value);
const [attack, decay, sustain, release] = getADSRValues(
+1 -1
View File
@@ -37,6 +37,6 @@
},
"devDependencies": {
"vite": "^5.0.10",
"vitest": "^2.1.3"
"vitest": "^1.1.0"
}
}
+1 -1
View File
@@ -38,6 +38,6 @@
},
"devDependencies": {
"vite": "^5.0.10",
"vitest": "^2.1.3"
"vitest": "^1.1.0"
}
}
-1
View File
@@ -6,5 +6,4 @@ This program is free software: you can redistribute it and/or modify it under th
export * from './webaudio.mjs';
export * from './scope.mjs';
export * from './spectrum.mjs';
export * from 'superdough';
-69
View File
@@ -1,69 +0,0 @@
import { Pattern, clamp } from '@strudel/core';
import { getDrawContext, getTheme } from '@strudel/draw';
import { analysers, getAnalyzerData } from 'superdough';
/**
* Renders a spectrum analyzer for the incoming audio signal.
* @name spectrum
* @param {object} config optional config with options:
* @param {integer} thickness line thickness in px (default 3)
* @param {integer} speed scroll speed (default 1)
* @param {integer} min min db (default -80)
* @param {integer} max max db (default 0)
* @example
* n("<0 4 <2 3> 1>*3")
* .off(1/8, add(n(5)))
* .off(1/5, add(n(7)))
* .scale("d3:minor:pentatonic")
* .s('sine')
* .dec(.3).room(.5)
* ._spectrum()
*/
let latestColor = {};
Pattern.prototype.spectrum = function (config = {}) {
let id = config.id ?? 1;
return this.analyze(id).draw(
(haps) => {
config.color = haps[0]?.value?.color || latestColor[id] || getTheme().foreground;
latestColor[id] = config.color;
drawSpectrum(analysers[id], config);
},
{ id },
);
};
Pattern.prototype.scope = Pattern.prototype.tscope;
const lastFrames = new Map();
function drawSpectrum(
analyser,
{ thickness = 3, speed = 1, min = -80, max = 0, ctx = getDrawContext(), id = 1, color } = {},
) {
ctx.lineWidth = thickness;
ctx.strokeStyle = color;
if (!analyser) {
// if analyser is undefined, draw straight line
// it may be undefined when no sound has been played yet
return;
}
const scrollSize = speed;
const dataArray = getAnalyzerData('frequency', id);
const canvas = ctx.canvas;
ctx.fillStyle = color;
const bufferSize = analyser.frequencyBinCount;
let imageData = lastFrames.get(id) || ctx.getImageData(0, 0, canvas.width, canvas.height);
lastFrames.set(id, imageData);
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.putImageData(imageData, -scrollSize, 0);
let q = canvas.width - speed;
for (let i = 0; i < bufferSize; i++) {
const normalized = clamp((dataArray[i] - min) / (max - min), 0, 1);
ctx.globalAlpha = normalized;
const next = (Math.log(i + 1) / Math.log(bufferSize)) * canvas.height;
const size = 2; //next - pos;
ctx.fillRect(q, canvas.height - next, scrollSize, size);
}
lastFrames.set(id, ctx.getImageData(0, 0, canvas.width, canvas.height));
}
+1 -1
View File
@@ -34,6 +34,6 @@
},
"devDependencies": {
"vite": "^5.0.10",
"vitest": "^2.1.3"
"vitest": "^1.1.0"
}
}
+9264 -11500
View File
File diff suppressed because it is too large Load Diff
+159 -442
View File
@@ -954,24 +954,6 @@ exports[`runs examples > example "bank" example index 0 1`] = `
]
`;
exports[`runs examples > example "beat" example index 0 1`] = `
[
"[ 0/1 → 1/16 | s:bd ]",
"[ 1/1 → 17/16 | s:bd ]",
"[ 2/1 → 33/16 | s:bd ]",
"[ 3/1 → 49/16 | s:bd ]",
]
`;
exports[`runs examples > example "beat" example index 1 1`] = `
[
"[ 1/48 → 1/12 | s:sd ]",
"[ 49/48 → 13/12 | s:sd ]",
"[ 97/48 → 25/12 | s:sd ]",
"[ 145/48 → 37/12 | s:sd ]",
]
`;
exports[`runs examples > example "begin" example index 0 1`] = `
[
"[ 0/1 → 1/2 | s:rave begin:0 ]",
@@ -985,210 +967,6 @@ exports[`runs examples > example "begin" example index 0 1`] = `
]
`;
exports[`runs examples > example "binary" example index 0 1`] = `
[
"[ 0/1 → 1/3 | s:hh ]",
"[ 2/3 → 1/1 | s:hh ]",
"[ 1/1 → 4/3 | s:hh ]",
"[ 5/3 → 2/1 | s:hh ]",
"[ 2/1 → 7/3 | s:hh ]",
"[ 8/3 → 3/1 | s:hh ]",
"[ 3/1 → 10/3 | s:hh ]",
"[ 11/3 → 4/1 | s:hh ]",
]
`;
exports[`runs examples > example "binaryN" example index 0 1`] = `
[
"[ 0/1 → 1/16 | s:hh ]",
"[ 1/16 → 1/8 | s:hh ]",
"[ 3/16 → 1/4 | s:hh ]",
"[ 1/4 → 5/16 | s:hh ]",
"[ 1/2 → 9/16 | s:hh ]",
"[ 9/16 → 5/8 | s:hh ]",
"[ 5/8 → 11/16 | s:hh ]",
"[ 3/4 → 13/16 | s:hh ]",
"[ 13/16 → 7/8 | s:hh ]",
"[ 1/1 → 17/16 | s:hh ]",
"[ 17/16 → 9/8 | s:hh ]",
"[ 19/16 → 5/4 | s:hh ]",
"[ 5/4 → 21/16 | s:hh ]",
"[ 3/2 → 25/16 | s:hh ]",
"[ 25/16 → 13/8 | s:hh ]",
"[ 13/8 → 27/16 | s:hh ]",
"[ 7/4 → 29/16 | s:hh ]",
"[ 29/16 → 15/8 | s:hh ]",
"[ 2/1 → 33/16 | s:hh ]",
"[ 33/16 → 17/8 | s:hh ]",
"[ 35/16 → 9/4 | s:hh ]",
"[ 9/4 → 37/16 | s:hh ]",
"[ 5/2 → 41/16 | s:hh ]",
"[ 41/16 → 21/8 | s:hh ]",
"[ 21/8 → 43/16 | s:hh ]",
"[ 11/4 → 45/16 | s:hh ]",
"[ 45/16 → 23/8 | s:hh ]",
"[ 3/1 → 49/16 | s:hh ]",
"[ 49/16 → 25/8 | s:hh ]",
"[ 51/16 → 13/4 | s:hh ]",
"[ 13/4 → 53/16 | s:hh ]",
"[ 7/2 → 57/16 | s:hh ]",
"[ 57/16 → 29/8 | s:hh ]",
"[ 29/8 → 59/16 | s:hh ]",
"[ 15/4 → 61/16 | s:hh ]",
"[ 61/16 → 31/8 | s:hh ]",
]
`;
exports[`runs examples > example "bite" example index 0 1`] = `
[
"[ 0/1 → 1/8 | note:Bb3 ]",
"[ 1/8 → 1/4 | note:C4 ]",
"[ 1/4 → 3/8 | note:G3 ]",
"[ 3/8 → 1/2 | note:A3 ]",
"[ 1/2 → 5/8 | note:E3 ]",
"[ 5/8 → 3/4 | note:F3 ]",
"[ 3/4 → 7/8 | note:C3 ]",
"[ 7/8 → 1/1 | note:D3 ]",
"[ 1/1 → 9/8 | note:Bb3 ]",
"[ 9/8 → 5/4 | note:C4 ]",
"[ 5/4 → 11/8 | note:G3 ]",
"[ 11/8 → 3/2 | note:A3 ]",
"[ 3/2 → 13/8 | note:E3 ]",
"[ 13/8 → 7/4 | note:F3 ]",
"[ 7/4 → 15/8 | note:C3 ]",
"[ 15/8 → 2/1 | note:D3 ]",
"[ 2/1 → 17/8 | note:Bb3 ]",
"[ 17/8 → 9/4 | note:C4 ]",
"[ 9/4 → 19/8 | note:G3 ]",
"[ 19/8 → 5/2 | note:A3 ]",
"[ 5/2 → 21/8 | note:E3 ]",
"[ 21/8 → 11/4 | note:F3 ]",
"[ 11/4 → 23/8 | note:C3 ]",
"[ 23/8 → 3/1 | note:D3 ]",
"[ 3/1 → 25/8 | note:Bb3 ]",
"[ 25/8 → 13/4 | note:C4 ]",
"[ 13/4 → 27/8 | note:G3 ]",
"[ 27/8 → 7/2 | note:A3 ]",
"[ 7/2 → 29/8 | note:E3 ]",
"[ 29/8 → 15/4 | note:F3 ]",
"[ 15/4 → 31/8 | note:C3 ]",
"[ 31/8 → 4/1 | note:D3 ]",
]
`;
exports[`runs examples > example "bite" example index 1 1`] = `
[
"[ 0/1 → 1/8 | s:bd bank:RolandTR909 speed:1.2 ]",
"[ 0/1 → 1/8 | s:hh bank:RolandTR909 speed:1.2 ]",
"[ 1/16 → 1/8 | s:sd n:6 bank:RolandTR909 speed:1.2 ]",
"[ 1/8 → 1/4 | s:bd bank:RolandTR909 speed:1.2 ]",
"[ 1/8 → 1/4 | s:hh bank:RolandTR909 speed:1.2 ]",
"[ 3/16 → 1/4 | s:sd n:6 bank:RolandTR909 speed:1.2 ]",
"[ 1/4 → 5/16 | s:hh bank:RolandTR909 speed:1.2 ]",
"[ 9/32 → 5/16 | s:sd n:5 bank:RolandTR909 speed:1.2 ]",
"[ 5/16 → 11/32 | s:sd n:1 bank:RolandTR909 speed:1.2 ]",
"[ 5/16 → 3/8 | s:bd bank:RolandTR909 speed:1.2 ]",
"[ 3/8 → 7/16 | s:bd bank:RolandTR909 speed:1.2 ]",
"[ 3/8 → 1/2 | s:cp n:7 bank:RolandTR909 speed:1.2 ]",
"[ 13/32 → 7/16 | s:sd n:2 bank:RolandTR909 speed:1.2 ]",
"[ 7/16 → 1/2 | s:bd bank:RolandTR909 speed:1.2 ]",
"[ 1/2 → 5/8 | s:bd bank:RolandTR909 speed:1.2 ]",
"[ 1/2 → 5/8 | s:hh bank:RolandTR909 speed:1.2 ]",
"[ 9/16 → 5/8 | s:sd n:6 bank:RolandTR909 speed:1.2 ]",
"[ 5/8 → 3/4 | s:bd bank:RolandTR909 speed:1.2 ]",
"[ 5/8 → 3/4 | s:hh bank:RolandTR909 speed:1.2 ]",
"[ 11/16 → 3/4 | s:sd n:6 bank:RolandTR909 speed:1.2 ]",
"[ 3/4 → 25/32 | s:sd n:1 bank:RolandTR909 speed:1.2 ]",
"[ 3/4 → 13/16 | s:bd bank:RolandTR909 speed:1.2 ]",
"[ 13/16 → 7/8 | s:hh bank:RolandTR909 speed:1.2 ]",
"[ 27/32 → 7/8 | s:sd n:5 bank:RolandTR909 speed:1.2 ]",
"[ 7/8 → 15/16 | s:bd bank:RolandTR909 speed:1.2 ]",
"[ 7/8 → 1/1 | s:cp n:7 bank:RolandTR909 speed:1.2 ]",
"[ 29/32 → 15/16 | s:sd n:2 bank:RolandTR909 speed:1.2 ]",
"[ 15/16 → 1/1 | s:bd bank:RolandTR909 speed:1.2 ]",
"[ 1/1 → 9/8 | s:bd bank:RolandTR909 speed:1.2 ]",
"[ 1/1 → 9/8 | s:hh bank:RolandTR909 speed:1.2 ]",
"[ 17/16 → 9/8 | s:sd n:6 bank:RolandTR909 speed:1.2 ]",
"[ 9/8 → 5/4 | s:bd bank:RolandTR909 speed:1.2 ]",
"[ 9/8 → 5/4 | s:hh bank:RolandTR909 speed:1.2 ]",
"[ 19/16 → 5/4 | s:sd n:6 bank:RolandTR909 speed:1.2 ]",
"[ 5/4 → 21/16 | s:hh bank:RolandTR909 speed:1.2 ]",
"[ 41/32 → 21/16 | s:sd n:5 bank:RolandTR909 speed:1.2 ]",
"[ 21/16 → 43/32 | s:sd n:1 bank:RolandTR909 speed:1.2 ]",
"[ 21/16 → 11/8 | s:bd bank:RolandTR909 speed:1.2 ]",
"[ 11/8 → 23/16 | s:sd n:1 bank:RolandTR909 speed:1.2 ]",
"[ 11/8 → 3/2 | s:bd bank:RolandTR909 speed:1.2 ]",
"[ 3/2 → 13/8 | s:bd bank:RolandTR909 speed:1.2 ]",
"[ 3/2 → 13/8 | s:hh bank:RolandTR909 speed:1.2 ]",
"[ 25/16 → 13/8 | s:sd n:6 bank:RolandTR909 speed:1.2 ]",
"[ 13/8 → 7/4 | s:bd bank:RolandTR909 speed:1.2 ]",
"[ 13/8 → 7/4 | s:hh bank:RolandTR909 speed:1.2 ]",
"[ 27/16 → 7/4 | s:sd n:6 bank:RolandTR909 speed:1.2 ]",
"[ 7/4 → 57/32 | s:sd n:1 bank:RolandTR909 speed:1.2 ]",
"[ 7/4 → 29/16 | s:bd bank:RolandTR909 speed:1.2 ]",
"[ 29/16 → 15/8 | s:hh bank:RolandTR909 speed:1.2 ]",
"[ 59/32 → 15/8 | s:sd n:5 bank:RolandTR909 speed:1.2 ]",
"[ 15/8 → 31/16 | s:bd bank:RolandTR909 speed:1.2 ]",
"[ 15/8 → 2/1 | s:cp n:7 bank:RolandTR909 speed:1.2 ]",
"[ 61/32 → 31/16 | s:sd n:2 bank:RolandTR909 speed:1.2 ]",
"[ 31/16 → 2/1 | s:bd bank:RolandTR909 speed:1.2 ]",
"[ 2/1 → 17/8 | s:bd bank:RolandTR909 speed:1.2 ]",
"[ 2/1 → 17/8 | s:hh bank:RolandTR909 speed:1.2 ]",
"[ 33/16 → 17/8 | s:sd n:6 bank:RolandTR909 speed:1.2 ]",
"[ 17/8 → 9/4 | s:bd bank:RolandTR909 speed:1.2 ]",
"[ 17/8 → 9/4 | s:hh bank:RolandTR909 speed:1.2 ]",
"[ 35/16 → 9/4 | s:sd n:6 bank:RolandTR909 speed:1.2 ]",
"[ 9/4 → 37/16 | s:hh bank:RolandTR909 speed:1.2 ]",
"[ 73/32 → 37/16 | s:sd n:5 bank:RolandTR909 speed:1.2 ]",
"[ 37/16 → 75/32 | s:sd n:1 bank:RolandTR909 speed:1.2 ]",
"[ 37/16 → 19/8 | s:bd bank:RolandTR909 speed:1.2 ]",
"[ 19/8 → 39/16 | s:bd bank:RolandTR909 speed:1.2 ]",
"[ 19/8 → 5/2 | s:cp n:7 bank:RolandTR909 speed:1.2 ]",
"[ 77/32 → 39/16 | s:sd n:2 bank:RolandTR909 speed:1.2 ]",
"[ 39/16 → 5/2 | s:bd bank:RolandTR909 speed:1.2 ]",
"[ 5/2 → 21/8 | s:bd bank:RolandTR909 speed:1.2 ]",
"[ 5/2 → 21/8 | s:hh bank:RolandTR909 speed:1.2 ]",
"[ 41/16 → 21/8 | s:sd n:6 bank:RolandTR909 speed:1.2 ]",
"[ 21/8 → 11/4 | s:bd bank:RolandTR909 speed:1.2 ]",
"[ 21/8 → 11/4 | s:hh bank:RolandTR909 speed:1.2 ]",
"[ 43/16 → 11/4 | s:sd n:6 bank:RolandTR909 speed:1.2 ]",
"[ 11/4 → 89/32 | s:sd n:1 bank:RolandTR909 speed:1.2 ]",
"[ 11/4 → 45/16 | s:bd bank:RolandTR909 speed:1.2 ]",
"[ 45/16 → 23/8 | s:hh bank:RolandTR909 speed:1.2 ]",
"[ 91/32 → 23/8 | s:sd n:5 bank:RolandTR909 speed:1.2 ]",
"[ 23/8 → 47/16 | s:bd bank:RolandTR909 speed:1.2 ]",
"[ 23/8 → 3/1 | s:cp n:7 bank:RolandTR909 speed:1.2 ]",
"[ 93/32 → 47/16 | s:sd n:2 bank:RolandTR909 speed:1.2 ]",
"[ 47/16 → 3/1 | s:bd bank:RolandTR909 speed:1.2 ]",
"[ 3/1 → 25/8 | s:bd bank:RolandTR909 speed:1.2 ]",
"[ 3/1 → 25/8 | s:hh bank:RolandTR909 speed:1.2 ]",
"[ 49/16 → 25/8 | s:sd n:6 bank:RolandTR909 speed:1.2 ]",
"[ 25/8 → 13/4 | s:bd bank:RolandTR909 speed:1.2 ]",
"[ 25/8 → 13/4 | s:hh bank:RolandTR909 speed:1.2 ]",
"[ 51/16 → 13/4 | s:sd n:6 bank:RolandTR909 speed:1.2 ]",
"[ 13/4 → 53/16 | s:hh bank:RolandTR909 speed:1.2 ]",
"[ 105/32 → 53/16 | s:sd n:5 bank:RolandTR909 speed:1.2 ]",
"[ 53/16 → 107/32 | s:sd n:1 bank:RolandTR909 speed:1.2 ]",
"[ 53/16 → 27/8 | s:bd bank:RolandTR909 speed:1.2 ]",
"[ 27/8 → 55/16 | s:sd n:1 bank:RolandTR909 speed:1.2 ]",
"[ 27/8 → 7/2 | s:bd bank:RolandTR909 speed:1.2 ]",
"[ 7/2 → 29/8 | s:bd bank:RolandTR909 speed:1.2 ]",
"[ 7/2 → 29/8 | s:hh bank:RolandTR909 speed:1.2 ]",
"[ 57/16 → 29/8 | s:sd n:6 bank:RolandTR909 speed:1.2 ]",
"[ 29/8 → 15/4 | s:bd bank:RolandTR909 speed:1.2 ]",
"[ 29/8 → 15/4 | s:hh bank:RolandTR909 speed:1.2 ]",
"[ 59/16 → 15/4 | s:sd n:6 bank:RolandTR909 speed:1.2 ]",
"[ 15/4 → 121/32 | s:sd n:1 bank:RolandTR909 speed:1.2 ]",
"[ 15/4 → 61/16 | s:bd bank:RolandTR909 speed:1.2 ]",
"[ 61/16 → 31/8 | s:hh bank:RolandTR909 speed:1.2 ]",
"[ 123/32 → 31/8 | s:sd n:5 bank:RolandTR909 speed:1.2 ]",
"[ 31/8 → 63/16 | s:bd bank:RolandTR909 speed:1.2 ]",
"[ 31/8 → 4/1 | s:cp n:7 bank:RolandTR909 speed:1.2 ]",
"[ 125/32 → 63/16 | s:sd n:2 bank:RolandTR909 speed:1.2 ]",
"[ 63/16 → 4/1 | s:bd bank:RolandTR909 speed:1.2 ]",
]
`;
exports[`runs examples > example "bpattack" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:c2 s:sawtooth bandf:500 bpattack:0.5 bpenv:4 ]",
@@ -1451,16 +1229,6 @@ exports[`runs examples > example "brandBy" example index 0 1`] = `
`;
exports[`runs examples > example "cat" example index 0 1`] = `
[
"[ 0/1 → 1/1 | note:e5 ]",
"[ 1/1 → 2/1 | note:b4 ]",
"[ 2/1 → 5/2 | note:d5 ]",
"[ 5/2 → 3/1 | note:c5 ]",
"[ 3/1 → 4/1 | note:e5 ]",
]
`;
exports[`runs examples > example "cat" example index 1 1`] = `
[
"[ 0/1 → 1/4 | s:hh ]",
"[ 1/4 → 1/2 | s:hh ]",
@@ -1483,6 +1251,16 @@ exports[`runs examples > example "cat" example index 1 1`] = `
]
`;
exports[`runs examples > example "cat" example index 0 2`] = `
[
"[ 0/1 → 1/1 | note:e5 ]",
"[ 1/1 → 2/1 | note:b4 ]",
"[ 2/1 → 5/2 | note:d5 ]",
"[ 5/2 → 3/1 | note:c5 ]",
"[ 3/1 → 4/1 | note:e5 ]",
]
`;
exports[`runs examples > example "ceil" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:42 ]",
@@ -2888,43 +2666,6 @@ exports[`runs examples > example "fastGap" example index 0 1`] = `
]
`;
exports[`runs examples > example "filter" example index 0 1`] = `
[
"[ 0/1 → 1/8 | s:hh ]",
"[ 1/8 → 1/4 | s:hh ]",
"[ 1/4 → 3/8 | s:hh ]",
"[ 3/8 → 1/2 | s:hh ]",
"[ 1/2 → 5/8 | s:hh ]",
"[ 5/8 → 3/4 | s:hh ]",
"[ 3/4 → 7/8 | s:hh ]",
"[ 7/8 → 1/1 | s:oh ]",
"[ 1/1 → 9/8 | s:hh ]",
"[ 9/8 → 5/4 | s:hh ]",
"[ 5/4 → 11/8 | s:hh ]",
"[ 11/8 → 3/2 | s:hh ]",
"[ 3/2 → 13/8 | s:hh ]",
"[ 13/8 → 7/4 | s:hh ]",
"[ 7/4 → 15/8 | s:hh ]",
"[ 15/8 → 2/1 | s:oh ]",
"[ 2/1 → 17/8 | s:hh ]",
"[ 17/8 → 9/4 | s:hh ]",
"[ 9/4 → 19/8 | s:hh ]",
"[ 19/8 → 5/2 | s:hh ]",
"[ 5/2 → 21/8 | s:hh ]",
"[ 21/8 → 11/4 | s:hh ]",
"[ 11/4 → 23/8 | s:hh ]",
"[ 23/8 → 3/1 | s:oh ]",
"[ 3/1 → 25/8 | s:hh ]",
"[ 25/8 → 13/4 | s:hh ]",
"[ 13/4 → 27/8 | s:hh ]",
"[ 27/8 → 7/2 | s:hh ]",
"[ 7/2 → 29/8 | s:hh ]",
"[ 29/8 → 15/4 | s:hh ]",
"[ 15/4 → 31/8 | s:hh ]",
"[ 31/8 → 4/1 | s:oh ]",
]
`;
exports[`runs examples > example "firstOf" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:g3 ]",
@@ -4137,8 +3878,6 @@ exports[`runs examples > example "juxBy" example index 0 1`] = `
]
`;
exports[`runs examples > example "keyDown" example index 0 1`] = `[]`;
exports[`runs examples > example "lastOf" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:c3 ]",
@@ -4496,27 +4235,6 @@ exports[`runs examples > example "lpf" example index 1 1`] = `
]
`;
exports[`runs examples > example "lplfo" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:c2 s:sawtooth cutoff:300 lplfo:2 ]",
"[ 1/4 → 1/2 | note:e2 s:sawtooth cutoff:300 lplfo:2 ]",
"[ 1/2 → 3/4 | note:f2 s:sawtooth cutoff:300 lplfo:2 ]",
"[ 3/4 → 1/1 | note:g2 s:sawtooth cutoff:300 lplfo:2 ]",
"[ 1/1 → 5/4 | note:c2 s:sawtooth cutoff:300 lplfo:2 ]",
"[ 5/4 → 3/2 | note:e2 s:sawtooth cutoff:300 lplfo:2 ]",
"[ 3/2 → 7/4 | note:f2 s:sawtooth cutoff:300 lplfo:2 ]",
"[ 7/4 → 2/1 | note:g2 s:sawtooth cutoff:300 lplfo:2 ]",
"[ 2/1 → 9/4 | note:c2 s:sawtooth cutoff:300 lplfo:2 ]",
"[ 9/4 → 5/2 | note:e2 s:sawtooth cutoff:300 lplfo:2 ]",
"[ 5/2 → 11/4 | note:f2 s:sawtooth cutoff:300 lplfo:2 ]",
"[ 11/4 → 3/1 | note:g2 s:sawtooth cutoff:300 lplfo:2 ]",
"[ 3/1 → 13/4 | note:c2 s:sawtooth cutoff:300 lplfo:2 ]",
"[ 13/4 → 7/2 | note:e2 s:sawtooth cutoff:300 lplfo:2 ]",
"[ 7/2 → 15/4 | note:f2 s:sawtooth cutoff:300 lplfo:2 ]",
"[ 15/4 → 4/1 | note:g2 s:sawtooth cutoff:300 lplfo:2 ]",
]
`;
exports[`runs examples > example "lpq" example index 0 1`] = `
[
"[ 0/1 → 1/8 | s:hh cutoff:2000 resonance:0 ]",
@@ -4646,27 +4364,6 @@ exports[`runs examples > example "lsize" example index 0 1`] = `
]
`;
exports[`runs examples > example "markcss" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:c ]",
"[ 1/4 → 1/2 | note:a ]",
"[ 1/2 → 3/4 | note:f ]",
"[ 3/4 → 1/1 | note:e ]",
"[ 1/1 → 5/4 | note:c ]",
"[ 5/4 → 3/2 | note:a ]",
"[ 3/2 → 7/4 | note:f ]",
"[ 7/4 → 2/1 | note:e ]",
"[ 2/1 → 9/4 | note:c ]",
"[ 9/4 → 5/2 | note:a ]",
"[ 5/2 → 11/4 | note:f ]",
"[ 11/4 → 3/1 | note:e ]",
"[ 3/1 → 13/4 | note:c ]",
"[ 13/4 → 7/2 | note:a ]",
"[ 7/2 → 15/4 | note:f ]",
"[ 15/4 → 4/1 | note:e ]",
]
`;
exports[`runs examples > example "mask" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:c ]",
@@ -7092,27 +6789,6 @@ exports[`runs examples > example "segment" example index 0 1`] = `
`;
exports[`runs examples > example "seq" example index 0 1`] = `
[
"[ 0/1 → 1/3 | note:e5 ]",
"[ 1/3 → 2/3 | note:b4 ]",
"[ 2/3 → 5/6 | note:d5 ]",
"[ 5/6 → 1/1 | note:c5 ]",
"[ 1/1 → 4/3 | note:e5 ]",
"[ 4/3 → 5/3 | note:b4 ]",
"[ 5/3 → 11/6 | note:d5 ]",
"[ 11/6 → 2/1 | note:c5 ]",
"[ 2/1 → 7/3 | note:e5 ]",
"[ 7/3 → 8/3 | note:b4 ]",
"[ 8/3 → 17/6 | note:d5 ]",
"[ 17/6 → 3/1 | note:c5 ]",
"[ 3/1 → 10/3 | note:e5 ]",
"[ 10/3 → 11/3 | note:b4 ]",
"[ 11/3 → 23/6 | note:d5 ]",
"[ 23/6 → 4/1 | note:c5 ]",
]
`;
exports[`runs examples > example "seq" example index 1 1`] = `
[
"[ 0/1 → 1/8 | s:hh ]",
"[ 1/8 → 1/4 | s:hh ]",
@@ -7153,23 +6829,24 @@ exports[`runs examples > example "seq" example index 1 1`] = `
]
`;
exports[`runs examples > example "seqPLoop" example index 0 1`] = `
exports[`runs examples > example "seq" example index 0 2`] = `
[
"[ 0/1 → 1/8 | s:bd ]",
"[ 3/8 → 1/2 | s:bd ]",
"[ 3/4 → 7/8 | s:bd ]",
"[ 1/1 → 9/8 | s:bd ]",
"[ 1/1 → 9/8 | s:cp ]",
"[ 11/8 → 3/2 | s:bd ]",
"[ 11/8 → 3/2 | s:cp ]",
"[ 7/4 → 15/8 | s:bd ]",
"[ 7/4 → 15/8 | s:cp ]",
"[ 2/1 → 17/8 | s:cp ]",
"[ 19/8 → 5/2 | s:cp ]",
"[ 11/423/8 | s:cp ]",
"[ 3/1 → 25/8 | s:bd ]",
"[ 27/8 → 7/2 | s:bd ]",
"[ 15/4 → 31/8 | s:bd ]",
"[ 0/1 → 1/3 | note:e5 ]",
"[ 1/3 → 2/3 | note:b4 ]",
"[ 2/3 → 5/6 | note:d5 ]",
"[ 5/6 → 1/1 | note:c5 ]",
"[ 1/1 → 4/3 | note:e5 ]",
"[ 4/3 → 5/3 | note:b4 ]",
"[ 5/3 → 11/6 | note:d5 ]",
"[ 11/6 → 2/1 | note:c5 ]",
"[ 2/1 → 7/3 | note:e5 ]",
"[ 7/3 → 8/3 | note:b4 ]",
"[ 8/3 → 17/6 | note:d5 ]",
"[ 17/6 → 3/1 | note:c5 ]",
"[ 3/1 → 10/3 | note:e5 ]",
"[ 10/3 → 11/3 | note:b4 ]",
"[ 11/3 → 23/6 | note:d5 ]",
"[ 23/6 → 4/1 | note:c5 ]",
]
`;
@@ -7603,75 +7280,6 @@ exports[`runs examples > example "sometimesBy" example index 0 1`] = `
]
`;
exports[`runs examples > example "spectrum" example index 0 1`] = `
[
"[ -5/24 ⇜ (0/1 → 1/8) | note:F4 s:sine decay:0.3 room:0.5 ]",
"[ -2/15 ⇜ (0/1 → 1/5) | note:A4 s:sine decay:0.3 room:0.5 ]",
"[ -1/120 ⇜ (0/1 → 1/5) ⇝ 13/40 | note:A5 s:sine decay:0.3 room:0.5 ]",
"[ 0/1 → 1/3 | note:D3 s:sine decay:0.3 room:0.5 ]",
"[ 1/8 → 11/24 | note:D4 s:sine decay:0.3 room:0.5 ]",
"[ -1/120 ⇜ (1/5 → 13/40) | note:A5 s:sine decay:0.3 room:0.5 ]",
"[ 1/5 → 8/15 | note:G4 s:sine decay:0.3 room:0.5 ]",
"[ 13/40 → 79/120 | note:G5 s:sine decay:0.3 room:0.5 ]",
"[ 1/3 → 2/3 | note:C4 s:sine decay:0.3 room:0.5 ]",
"[ 11/24 → 19/24 | note:C5 s:sine decay:0.3 room:0.5 ]",
"[ 8/15 → 13/15 | note:F5 s:sine decay:0.3 room:0.5 ]",
"[ 79/120 → 119/120 | note:F6 s:sine decay:0.3 room:0.5 ]",
"[ 2/3 → 1/1 | note:G3 s:sine decay:0.3 room:0.5 ]",
"[ (19/24 → 1/1) ⇝ 9/8 | note:G4 s:sine decay:0.3 room:0.5 ]",
"[ (13/15 → 1/1) ⇝ 6/5 | note:C5 s:sine decay:0.3 room:0.5 ]",
"[ (119/120 → 1/1) ⇝ 53/40 | note:C6 s:sine decay:0.3 room:0.5 ]",
"[ 19/24 ⇜ (1/1 → 9/8) | note:G4 s:sine decay:0.3 room:0.5 ]",
"[ 13/15 ⇜ (1/1 → 6/5) | note:C5 s:sine decay:0.3 room:0.5 ]",
"[ 119/120 ⇜ (1/1 → 6/5) ⇝ 53/40 | note:C6 s:sine decay:0.3 room:0.5 ]",
"[ 1/1 → 4/3 | note:F3 s:sine decay:0.3 room:0.5 ]",
"[ 9/8 → 35/24 | note:F4 s:sine decay:0.3 room:0.5 ]",
"[ 119/120 ⇜ (6/5 → 53/40) | note:C6 s:sine decay:0.3 room:0.5 ]",
"[ 6/5 → 23/15 | note:A4 s:sine decay:0.3 room:0.5 ]",
"[ 53/40 → 199/120 | note:A5 s:sine decay:0.3 room:0.5 ]",
"[ 4/3 → 5/3 | note:D3 s:sine decay:0.3 room:0.5 ]",
"[ 35/24 → 43/24 | note:D4 s:sine decay:0.3 room:0.5 ]",
"[ 23/15 → 28/15 | note:G4 s:sine decay:0.3 room:0.5 ]",
"[ 199/120 → 239/120 | note:G5 s:sine decay:0.3 room:0.5 ]",
"[ 5/3 → 2/1 | note:C4 s:sine decay:0.3 room:0.5 ]",
"[ (43/24 → 2/1) ⇝ 17/8 | note:C5 s:sine decay:0.3 room:0.5 ]",
"[ (28/15 → 2/1) ⇝ 11/5 | note:F5 s:sine decay:0.3 room:0.5 ]",
"[ (239/120 → 2/1) ⇝ 93/40 | note:F6 s:sine decay:0.3 room:0.5 ]",
"[ 43/24 ⇜ (2/1 → 17/8) | note:C5 s:sine decay:0.3 room:0.5 ]",
"[ 28/15 ⇜ (2/1 → 11/5) | note:F5 s:sine decay:0.3 room:0.5 ]",
"[ 239/120 ⇜ (2/1 → 11/5) ⇝ 93/40 | note:F6 s:sine decay:0.3 room:0.5 ]",
"[ 2/1 → 7/3 | note:A3 s:sine decay:0.3 room:0.5 ]",
"[ 17/8 → 59/24 | note:A4 s:sine decay:0.3 room:0.5 ]",
"[ 239/120 ⇜ (11/5 → 93/40) | note:F6 s:sine decay:0.3 room:0.5 ]",
"[ 11/5 → 38/15 | note:D5 s:sine decay:0.3 room:0.5 ]",
"[ 93/40 → 319/120 | note:D6 s:sine decay:0.3 room:0.5 ]",
"[ 7/3 → 8/3 | note:F3 s:sine decay:0.3 room:0.5 ]",
"[ 59/24 → 67/24 | note:F4 s:sine decay:0.3 room:0.5 ]",
"[ 38/15 → 43/15 | note:A4 s:sine decay:0.3 room:0.5 ]",
"[ 319/120 → 359/120 | note:A5 s:sine decay:0.3 room:0.5 ]",
"[ 8/3 → 3/1 | note:D3 s:sine decay:0.3 room:0.5 ]",
"[ (67/24 → 3/1) ⇝ 25/8 | note:D4 s:sine decay:0.3 room:0.5 ]",
"[ (43/15 → 3/1) ⇝ 16/5 | note:G4 s:sine decay:0.3 room:0.5 ]",
"[ (359/120 → 3/1) ⇝ 133/40 | note:G5 s:sine decay:0.3 room:0.5 ]",
"[ 67/24 ⇜ (3/1 → 25/8) | note:D4 s:sine decay:0.3 room:0.5 ]",
"[ 43/15 ⇜ (3/1 → 16/5) | note:G4 s:sine decay:0.3 room:0.5 ]",
"[ 359/120 ⇜ (3/1 → 16/5) ⇝ 133/40 | note:G5 s:sine decay:0.3 room:0.5 ]",
"[ 3/1 → 10/3 | note:C4 s:sine decay:0.3 room:0.5 ]",
"[ 25/8 → 83/24 | note:C5 s:sine decay:0.3 room:0.5 ]",
"[ 359/120 ⇜ (16/5 → 133/40) | note:G5 s:sine decay:0.3 room:0.5 ]",
"[ 16/5 → 53/15 | note:F5 s:sine decay:0.3 room:0.5 ]",
"[ 133/40 → 439/120 | note:F6 s:sine decay:0.3 room:0.5 ]",
"[ 10/3 → 11/3 | note:G3 s:sine decay:0.3 room:0.5 ]",
"[ 83/24 → 91/24 | note:G4 s:sine decay:0.3 room:0.5 ]",
"[ 53/15 → 58/15 | note:C5 s:sine decay:0.3 room:0.5 ]",
"[ 439/120 → 479/120 | note:C6 s:sine decay:0.3 room:0.5 ]",
"[ 11/3 → 4/1 | note:F3 s:sine decay:0.3 room:0.5 ]",
"[ (91/24 → 4/1) ⇝ 33/8 | note:F4 s:sine decay:0.3 room:0.5 ]",
"[ (58/15 → 4/1) ⇝ 21/5 | note:A4 s:sine decay:0.3 room:0.5 ]",
"[ (479/120 → 4/1) ⇝ 173/40 | note:A5 s:sine decay:0.3 room:0.5 ]",
]
`;
exports[`runs examples > example "speed" example index 0 1`] = `
[
"[ 0/1 → 1/6 | s:bd speed:1 ]",
@@ -7915,27 +7523,6 @@ exports[`runs examples > example "squiz" example index 0 1`] = `
`;
exports[`runs examples > example "stack" example index 0 1`] = `
[
"[ 0/1 → 1/2 | note:e4 ]",
"[ 0/1 → 1/1 | note:g3 ]",
"[ 0/1 → 1/1 | note:b3 ]",
"[ 1/2 → 1/1 | note:d4 ]",
"[ 1/1 → 3/2 | note:e4 ]",
"[ 1/1 → 2/1 | note:g3 ]",
"[ 1/1 → 2/1 | note:b3 ]",
"[ 3/2 → 2/1 | note:d4 ]",
"[ 2/1 → 5/2 | note:e4 ]",
"[ 2/1 → 3/1 | note:g3 ]",
"[ 2/1 → 3/1 | note:b3 ]",
"[ 5/2 → 3/1 | note:d4 ]",
"[ 3/1 → 7/2 | note:e4 ]",
"[ 3/1 → 4/1 | note:g3 ]",
"[ 3/1 → 4/1 | note:b3 ]",
"[ 7/2 → 4/1 | note:d4 ]",
]
`;
exports[`runs examples > example "stack" example index 1 1`] = `
[
"[ 0/1 → 1/8 | note:c4 ]",
"[ 0/1 → 1/4 | s:hh ]",
@@ -7976,6 +7563,27 @@ exports[`runs examples > example "stack" example index 1 1`] = `
]
`;
exports[`runs examples > example "stack" example index 0 2`] = `
[
"[ 0/1 → 1/2 | note:e4 ]",
"[ 0/1 → 1/1 | note:g3 ]",
"[ 0/1 → 1/1 | note:b3 ]",
"[ 1/2 → 1/1 | note:d4 ]",
"[ 1/1 → 3/2 | note:e4 ]",
"[ 1/1 → 2/1 | note:g3 ]",
"[ 1/1 → 2/1 | note:b3 ]",
"[ 3/2 → 2/1 | note:d4 ]",
"[ 2/1 → 5/2 | note:e4 ]",
"[ 2/1 → 3/1 | note:g3 ]",
"[ 2/1 → 3/1 | note:b3 ]",
"[ 5/2 → 3/1 | note:d4 ]",
"[ 3/1 → 7/2 | note:e4 ]",
"[ 3/1 → 4/1 | note:g3 ]",
"[ 3/1 → 4/1 | note:b3 ]",
"[ 7/2 → 4/1 | note:d4 ]",
]
`;
exports[`runs examples > example "steps" example index 0 1`] = `
[
"[ 0/1 → 1/4 | s:bd ]",
@@ -8927,8 +8535,6 @@ exports[`runs examples > example "when" example index 0 1`] = `
]
`;
exports[`runs examples > example "whenKey" example index 0 1`] = `[]`;
exports[`runs examples > example "withValue" example index 0 1`] = `
[
"[ 0/1 → 1/3 | 10 ]",
@@ -8991,6 +8597,117 @@ exports[`runs examples > example "xfade" example index 0 1`] = `
]
`;
exports[`runs examples > example "z1" example index 0 1`] = `
[
"[ 0/1 → 1/8 | note:D3 s:supersaw z1:0.01 release:0.5 ]",
"[ 1/8 → 1/4 | note:E3 s:supersaw z1:0.01 release:0.5 ]",
"[ 1/4 → 3/8 | note:F#3 s:supersaw z1:0.01 release:0.5 ]",
"[ 3/8 → 1/2 | note:A3 s:supersaw z1:0.01 release:0.5 ]",
"[ 1/2 → 5/8 | note:B3 s:supersaw z1:0.75 release:0.5 ]",
"[ 5/8 → 3/4 | note:D4 s:supersaw z1:0.75 release:0.5 ]",
"[ 3/4 → 7/8 | note:E4 s:supersaw z1:0.75 release:0.5 ]",
"[ 7/8 → 1/1 | note:F#4 s:supersaw z1:0.75 release:0.5 ]",
"[ 1/1 → 9/8 | note:D3 s:supersaw z1:0.01 release:0.5 ]",
"[ 9/8 → 5/4 | note:E3 s:supersaw z1:0.01 release:0.5 ]",
"[ 5/4 → 11/8 | note:F#3 s:supersaw z1:0.01 release:0.5 ]",
"[ 11/8 → 3/2 | note:A3 s:supersaw z1:0.01 release:0.5 ]",
"[ 3/2 → 13/8 | note:B3 s:supersaw z1:0.75 release:0.5 ]",
"[ 13/8 → 7/4 | note:D4 s:supersaw z1:0.75 release:0.5 ]",
"[ 7/4 → 15/8 | note:E4 s:supersaw z1:0.75 release:0.5 ]",
"[ 15/8 → 2/1 | note:F#4 s:supersaw z1:0.75 release:0.5 ]",
"[ 2/1 → 17/8 | note:D3 s:supersaw z1:0.01 release:0.5 ]",
"[ 17/8 → 9/4 | note:E3 s:supersaw z1:0.01 release:0.5 ]",
"[ 9/4 → 19/8 | note:F#3 s:supersaw z1:0.01 release:0.5 ]",
"[ 19/8 → 5/2 | note:A3 s:supersaw z1:0.01 release:0.5 ]",
"[ 5/2 → 21/8 | note:B3 s:supersaw z1:0.75 release:0.5 ]",
"[ 21/8 → 11/4 | note:D4 s:supersaw z1:0.75 release:0.5 ]",
"[ 11/4 → 23/8 | note:E4 s:supersaw z1:0.75 release:0.5 ]",
"[ 23/8 → 3/1 | note:F#4 s:supersaw z1:0.75 release:0.5 ]",
"[ 3/1 → 25/8 | note:D3 s:supersaw z1:0.01 release:0.5 ]",
"[ 25/8 → 13/4 | note:E3 s:supersaw z1:0.01 release:0.5 ]",
"[ 13/4 → 27/8 | note:F#3 s:supersaw z1:0.01 release:0.5 ]",
"[ 27/8 → 7/2 | note:A3 s:supersaw z1:0.01 release:0.5 ]",
"[ 7/2 → 29/8 | note:B3 s:supersaw z1:0.75 release:0.5 ]",
"[ 29/8 → 15/4 | note:D4 s:supersaw z1:0.75 release:0.5 ]",
"[ 15/4 → 31/8 | note:E4 s:supersaw z1:0.75 release:0.5 ]",
"[ 31/8 → 4/1 | note:F#4 s:supersaw z1:0.75 release:0.5 ]",
]
`;
exports[`runs examples > example "z2" example index 0 1`] = `
[
"[ 0/1 → 1/8 | note:D3 s:supersaw z2:2 release:0.5 ]",
"[ 1/8 → 1/4 | note:E3 s:supersaw z2:2 release:0.5 ]",
"[ 1/4 → 3/8 | note:F#3 s:supersaw z2:2 release:0.5 ]",
"[ 3/8 → 1/2 | note:A3 s:supersaw z2:2 release:0.5 ]",
"[ 1/2 → 5/8 | note:B3 s:supersaw z2:0.7 release:0.5 ]",
"[ 5/8 → 3/4 | note:D4 s:supersaw z2:0.7 release:0.5 ]",
"[ 3/4 → 7/8 | note:E4 s:supersaw z2:0.7 release:0.5 ]",
"[ 7/8 → 1/1 | note:F#4 s:supersaw z2:0.7 release:0.5 ]",
"[ 1/1 → 9/8 | note:D3 s:supersaw z2:2 release:0.5 ]",
"[ 9/8 → 5/4 | note:E3 s:supersaw z2:2 release:0.5 ]",
"[ 5/4 → 11/8 | note:F#3 s:supersaw z2:2 release:0.5 ]",
"[ 11/8 → 3/2 | note:A3 s:supersaw z2:2 release:0.5 ]",
"[ 3/2 → 13/8 | note:B3 s:supersaw z2:0.7 release:0.5 ]",
"[ 13/8 → 7/4 | note:D4 s:supersaw z2:0.7 release:0.5 ]",
"[ 7/4 → 15/8 | note:E4 s:supersaw z2:0.7 release:0.5 ]",
"[ 15/8 → 2/1 | note:F#4 s:supersaw z2:0.7 release:0.5 ]",
"[ 2/1 → 17/8 | note:D3 s:supersaw z2:2 release:0.5 ]",
"[ 17/8 → 9/4 | note:E3 s:supersaw z2:2 release:0.5 ]",
"[ 9/4 → 19/8 | note:F#3 s:supersaw z2:2 release:0.5 ]",
"[ 19/8 → 5/2 | note:A3 s:supersaw z2:2 release:0.5 ]",
"[ 5/2 → 21/8 | note:B3 s:supersaw z2:0.7 release:0.5 ]",
"[ 21/8 → 11/4 | note:D4 s:supersaw z2:0.7 release:0.5 ]",
"[ 11/4 → 23/8 | note:E4 s:supersaw z2:0.7 release:0.5 ]",
"[ 23/8 → 3/1 | note:F#4 s:supersaw z2:0.7 release:0.5 ]",
"[ 3/1 → 25/8 | note:D3 s:supersaw z2:2 release:0.5 ]",
"[ 25/8 → 13/4 | note:E3 s:supersaw z2:2 release:0.5 ]",
"[ 13/4 → 27/8 | note:F#3 s:supersaw z2:2 release:0.5 ]",
"[ 27/8 → 7/2 | note:A3 s:supersaw z2:2 release:0.5 ]",
"[ 7/2 → 29/8 | note:B3 s:supersaw z2:0.7 release:0.5 ]",
"[ 29/8 → 15/4 | note:D4 s:supersaw z2:0.7 release:0.5 ]",
"[ 15/4 → 31/8 | note:E4 s:supersaw z2:0.7 release:0.5 ]",
"[ 31/8 → 4/1 | note:F#4 s:supersaw z2:0.7 release:0.5 ]",
]
`;
exports[`runs examples > example "z3" example index 0 1`] = `
[
"[ 0/1 → 1/8 | note:D3 s:supersaw z3:0.1 release:0.5 ]",
"[ 1/8 → 1/4 | note:E3 s:supersaw z3:0.1 release:0.5 ]",
"[ 1/4 → 3/8 | note:F#3 s:supersaw z3:0.1 release:0.5 ]",
"[ 3/8 → 1/2 | note:A3 s:supersaw z3:0.1 release:0.5 ]",
"[ 1/2 → 5/8 | note:B3 s:supersaw z3:0.75 release:0.5 ]",
"[ 5/8 → 3/4 | note:D4 s:supersaw z3:0.75 release:0.5 ]",
"[ 3/4 → 7/8 | note:E4 s:supersaw z3:0.75 release:0.5 ]",
"[ 7/8 → 1/1 | note:F#4 s:supersaw z3:0.75 release:0.5 ]",
"[ 1/1 → 9/8 | note:D3 s:supersaw z3:0.1 release:0.5 ]",
"[ 9/8 → 5/4 | note:E3 s:supersaw z3:0.1 release:0.5 ]",
"[ 5/4 → 11/8 | note:F#3 s:supersaw z3:0.1 release:0.5 ]",
"[ 11/8 → 3/2 | note:A3 s:supersaw z3:0.1 release:0.5 ]",
"[ 3/2 → 13/8 | note:B3 s:supersaw z3:0.75 release:0.5 ]",
"[ 13/8 → 7/4 | note:D4 s:supersaw z3:0.75 release:0.5 ]",
"[ 7/4 → 15/8 | note:E4 s:supersaw z3:0.75 release:0.5 ]",
"[ 15/8 → 2/1 | note:F#4 s:supersaw z3:0.75 release:0.5 ]",
"[ 2/1 → 17/8 | note:D3 s:supersaw z3:0.1 release:0.5 ]",
"[ 17/8 → 9/4 | note:E3 s:supersaw z3:0.1 release:0.5 ]",
"[ 9/4 → 19/8 | note:F#3 s:supersaw z3:0.1 release:0.5 ]",
"[ 19/8 → 5/2 | note:A3 s:supersaw z3:0.1 release:0.5 ]",
"[ 5/2 → 21/8 | note:B3 s:supersaw z3:0.75 release:0.5 ]",
"[ 21/8 → 11/4 | note:D4 s:supersaw z3:0.75 release:0.5 ]",
"[ 11/4 → 23/8 | note:E4 s:supersaw z3:0.75 release:0.5 ]",
"[ 23/8 → 3/1 | note:F#4 s:supersaw z3:0.75 release:0.5 ]",
"[ 3/1 → 25/8 | note:D3 s:supersaw z3:0.1 release:0.5 ]",
"[ 25/8 → 13/4 | note:E3 s:supersaw z3:0.1 release:0.5 ]",
"[ 13/4 → 27/8 | note:F#3 s:supersaw z3:0.1 release:0.5 ]",
"[ 27/8 → 7/2 | note:A3 s:supersaw z3:0.1 release:0.5 ]",
"[ 7/2 → 29/8 | note:B3 s:supersaw z3:0.75 release:0.5 ]",
"[ 29/8 → 15/4 | note:D4 s:supersaw z3:0.75 release:0.5 ]",
"[ 15/4 → 31/8 | note:E4 s:supersaw z3:0.75 release:0.5 ]",
"[ 31/8 → 4/1 | note:F#4 s:supersaw z3:0.75 release:0.5 ]",
]
`;
exports[`runs examples > example "zoom" example index 0 1`] = `
[
"[ 0/1 → 1/6 | s:hh ]",
+6 -4
View File
@@ -8,7 +8,6 @@ exports[`renders tunes > tune: amensister 1`] = `
"[ 0/1 → 1/4 | n:0 s:amencutup room:0.5 ]",
"[ 1/16 → 1/8 | s:breath room:1 shape:0.6 begin:0.875 end:0.9375 ]",
"[ 1/8 → 3/16 | s:breath room:1 shape:0.6 begin:0.8125 end:0.875 ]",
"[ 1/8 → 1/4 | n:0 s:amencutup room:0.5 ]",
"[ 1/8 → 1/4 | note:45 s:sawtooth gain:0.4 decay:0.1 sustain:0 lpattack:0.1 lpenv:-4 resonance:10 cutoff:300.174310575404 ]",
"[ 1/8 → 1/4 | note:45 s:sawtooth gain:0.4 decay:0.1 sustain:0 lpattack:0.1 lpenv:-4 resonance:10 cutoff:300.174310575404 ]",
"[ 3/16 → 1/4 | s:breath room:1 shape:0.6 begin:0.75 end:0.8125 ]",
@@ -18,20 +17,22 @@ exports[`renders tunes > tune: amensister 1`] = `
"[ 1/4 → 3/8 | note:A1 s:sawtooth gain:0.4 decay:0.1 sustain:0 lpattack:0.1 lpenv:-4 resonance:10 cutoff:300.7878869297153 ]",
"[ 5/16 → 3/8 | s:breath room:1 shape:0.6 begin:0.625 end:0.6875 ]",
"[ 3/8 → 7/16 | s:breath room:1 shape:0.6 begin:0.5625 end:0.625 ]",
"[ 3/8 → 1/2 | n:1 s:amencutup room:0.5 ]",
"[ 3/8 → 1/2 | note:F1 s:sawtooth gain:0.4 decay:0.1 sustain:0 lpattack:0.1 lpenv:-4 resonance:10 cutoff:302.11020572391345 ]",
"[ 3/8 → 1/2 | note:F1 s:sawtooth gain:0.4 decay:0.1 sustain:0 lpattack:0.1 lpenv:-4 resonance:10 cutoff:302.11020572391345 ]",
"[ 7/16 → 1/2 | s:breath room:1 shape:0.6 begin:0.5 end:0.5625 ]",
"[ 1/2 → 9/16 | s:breath room:1 shape:0.6 begin:0.4375 end:0.5 ]",
"[ 1/2 → 5/8 | n:2 s:amencutup room:0.5 ]",
"[ 1/2 → 3/4 | n:2 s:amencutup room:0.5 ]",
"[ 9/16 → 5/8 | s:breath room:1 shape:0.6 begin:0.375 end:0.4375 ]",
"[ 5/8 → 11/16 | s:breath room:1 shape:0.6 begin:0.3125 end:0.375 ]",
"[ 11/16 → 3/4 | s:breath room:1 shape:0.6 begin:0.25 end:0.3125 ]",
"[ 3/4 → 13/16 | s:breath room:1 shape:0.6 begin:0.1875 end:0.25 ]",
"[ 3/4 → 7/8 | n:3 s:amencutup room:0.5 ]",
"[ 3/4 → 7/8 | note:Bb0 s:sawtooth gain:0.4 decay:0.1 sustain:0 lpattack:0.1 lpenv:-4 resonance:10 cutoff:312.54769231985796 ]",
"[ 3/4 → 7/8 | note:Bb0 s:sawtooth gain:0.4 decay:0.1 sustain:0 lpattack:0.1 lpenv:-4 resonance:10 cutoff:312.54769231985796 ]",
"[ 13/16 → 7/8 | s:breath room:1 shape:0.6 begin:0.125 end:0.1875 ]",
"[ 7/8 → 15/16 | s:breath room:1 shape:0.6 begin:0.0625 end:0.125 ]",
"[ 7/8 → 1/1 | n:3 s:amencutup room:0.5 ]",
"[ 7/8 → 1/1 | note:D1 s:sawtooth gain:0.4 decay:0.1 sustain:0 lpattack:0.1 lpenv:-4 resonance:10 cutoff:318.7927796831686 ]",
"[ 7/8 → 1/1 | note:D1 s:sawtooth gain:0.4 decay:0.1 sustain:0 lpattack:0.1 lpenv:-4 resonance:10 cutoff:318.7927796831686 ]",
"[ 15/16 → 1/1 | s:breath room:1 shape:0.6 begin:0 end:0.0625 ]",
@@ -6905,10 +6906,10 @@ exports[`renders tunes > tune: flatrave 1`] = `
"[ 1/8 → 1/4 | s:hh n:1 speed:0.5 delay:0.5 end:0.020001936784171157 bank:RolandTR909 room:0.5 gain:0.4 ]",
"[ 1/8 → 1/4 | s:hh n:1 speed:0.5 delay:0.5 end:0.020001936784171157 bank:RolandTR909 room:0.5 gain:0.4 ]",
"[ 1/8 → 1/4 | note:G1 s:sawtooth decay:0.1 sustain:0 lpattack:0.1 lpenv:-4 cutoff:800 resonance:8 ]",
"[ 1/4 → 3/8 | s:hh n:1 end:0.02000875429921906 bank:RolandTR909 room:0.5 gain:0.4 ]",
"[ 1/4 → 3/8 | s:hh n:1 end:0.02000875429921906 bank:RolandTR909 room:0.5 gain:0.4 ]",
"[ 1/4 → 3/8 | note:G1 s:sawtooth decay:0.1 sustain:0 lpattack:0.1 lpenv:-4 cutoff:800 resonance:8 ]",
"[ 3/8 → 1/2 | s:hh n:1 end:0.020023446730265706 bank:RolandTR909 room:0.5 gain:0.4 ]",
"[ 1/2 → 5/8 | s:hh n:1 speed:0.5 delay:0.5 end:0.020048626493108724 bank:RolandTR909 room:0.5 gain:0.4 ]",
"[ 1/2 → 5/8 | s:hh n:1 speed:0.5 delay:0.5 end:0.020048626493108724 bank:RolandTR909 room:0.5 gain:0.4 ]",
"[ 1/2 → 5/8 | note:G1 s:sawtooth decay:0.1 sustain:0 lpattack:0.1 lpenv:-4 cutoff:800 resonance:8 ]",
"[ 1/2 → 1/1 | s:bd bank:RolandTR909 ]",
"[ 1/2 → 1/1 | s:cp bank:RolandTR909 ]",
@@ -6916,6 +6917,7 @@ exports[`renders tunes > tune: flatrave 1`] = `
"[ 5/8 → 3/4 | s:hh n:1 end:0.020086608138500644 bank:RolandTR909 room:0.5 gain:0.4 ]",
"[ 5/8 → 3/4 | s:hh n:1 end:0.020086608138500644 bank:RolandTR909 room:0.5 gain:0.4 ]",
"[ 5/8 → 3/4 | note:G1 s:sawtooth decay:0.1 sustain:0 lpattack:0.1 lpenv:-4 cutoff:800 resonance:8 ]",
"[ 3/4 → 7/8 | s:hh n:1 end:0.02013941880355398 bank:RolandTR909 room:0.5 gain:0.4 ]",
"[ 7/8 → 1/1 | note:G1 s:sawtooth decay:0.1 sustain:0 lpattack:0.1 lpenv:-4 cutoff:800 resonance:8 ]",
]
`;
-6
View File
@@ -134,12 +134,6 @@ strudel.Pattern.prototype._pitchwheel = function () {
strudel.Pattern.prototype._pianoroll = function () {
return this;
};
strudel.Pattern.prototype._spectrum = function () {
return this;
};
strudel.Pattern.prototype.markcss = function () {
return this;
};
const uiHelpersMocked = {
backgroundImage: id,
+5
View File
@@ -3,6 +3,11 @@ import { configDefaults, defineConfig } from 'vitest/config';
/// <reference types="vitest" />
export default defineConfig({
test: {
// ...
deps: {
registerNodeLoader: true,
},
threads: false,
reporters: 'verbose',
isolate: false,
silent: true,
+5
View File
@@ -0,0 +1,5 @@
{
"devToolbar": {
"enabled": false
}
}
+290
View File
@@ -0,0 +1,290 @@
declare module 'astro:content' {
interface Render {
'.mdx': Promise<{
Content: import('astro').MarkdownInstance<{}>['Content'];
headings: import('astro').MarkdownHeading[];
remarkPluginFrontmatter: Record<string, any>;
}>;
}
}
declare module 'astro:content' {
interface Render {
'.md': Promise<{
Content: import('astro').MarkdownInstance<{}>['Content'];
headings: import('astro').MarkdownHeading[];
remarkPluginFrontmatter: Record<string, any>;
}>;
}
}
declare module 'astro:content' {
export { z } from 'astro/zod';
type Flatten<T> = T extends { [K: string]: infer U } ? U : never;
export type CollectionKey = keyof AnyEntryMap;
export type CollectionEntry<C extends CollectionKey> = Flatten<AnyEntryMap[C]>;
export type ContentCollectionKey = keyof ContentEntryMap;
export type DataCollectionKey = keyof DataEntryMap;
// This needs to be in sync with ImageMetadata
export type ImageFunction = () => import('astro/zod').ZodObject<{
src: import('astro/zod').ZodString;
width: import('astro/zod').ZodNumber;
height: import('astro/zod').ZodNumber;
format: import('astro/zod').ZodUnion<
[
import('astro/zod').ZodLiteral<'png'>,
import('astro/zod').ZodLiteral<'jpg'>,
import('astro/zod').ZodLiteral<'jpeg'>,
import('astro/zod').ZodLiteral<'tiff'>,
import('astro/zod').ZodLiteral<'webp'>,
import('astro/zod').ZodLiteral<'gif'>,
import('astro/zod').ZodLiteral<'svg'>,
import('astro/zod').ZodLiteral<'avif'>,
]
>;
}>;
type BaseSchemaWithoutEffects =
| import('astro/zod').AnyZodObject
| import('astro/zod').ZodUnion<[BaseSchemaWithoutEffects, ...BaseSchemaWithoutEffects[]]>
| import('astro/zod').ZodDiscriminatedUnion<string, import('astro/zod').AnyZodObject[]>
| import('astro/zod').ZodIntersection<BaseSchemaWithoutEffects, BaseSchemaWithoutEffects>;
type BaseSchema =
| BaseSchemaWithoutEffects
| import('astro/zod').ZodEffects<BaseSchemaWithoutEffects>;
export type SchemaContext = { image: ImageFunction };
type DataCollectionConfig<S extends BaseSchema> = {
type: 'data';
schema?: S | ((context: SchemaContext) => S);
};
type ContentCollectionConfig<S extends BaseSchema> = {
type?: 'content';
schema?: S | ((context: SchemaContext) => S);
};
type CollectionConfig<S> = ContentCollectionConfig<S> | DataCollectionConfig<S>;
export function defineCollection<S extends BaseSchema>(
input: CollectionConfig<S>
): CollectionConfig<S>;
type AllValuesOf<T> = T extends any ? T[keyof T] : never;
type ValidContentEntrySlug<C extends keyof ContentEntryMap> = AllValuesOf<
ContentEntryMap[C]
>['slug'];
export function getEntryBySlug<
C extends keyof ContentEntryMap,
E extends ValidContentEntrySlug<C> | (string & {}),
>(
collection: C,
// Note that this has to accept a regular string too, for SSR
entrySlug: E
): E extends ValidContentEntrySlug<C>
? Promise<CollectionEntry<C>>
: Promise<CollectionEntry<C> | undefined>;
export function getDataEntryById<C extends keyof DataEntryMap, E extends keyof DataEntryMap[C]>(
collection: C,
entryId: E
): Promise<CollectionEntry<C>>;
export function getCollection<C extends keyof AnyEntryMap, E extends CollectionEntry<C>>(
collection: C,
filter?: (entry: CollectionEntry<C>) => entry is E
): Promise<E[]>;
export function getCollection<C extends keyof AnyEntryMap>(
collection: C,
filter?: (entry: CollectionEntry<C>) => unknown
): Promise<CollectionEntry<C>[]>;
export function getEntry<
C extends keyof ContentEntryMap,
E extends ValidContentEntrySlug<C> | (string & {}),
>(entry: {
collection: C;
slug: E;
}): E extends ValidContentEntrySlug<C>
? Promise<CollectionEntry<C>>
: Promise<CollectionEntry<C> | undefined>;
export function getEntry<
C extends keyof DataEntryMap,
E extends keyof DataEntryMap[C] | (string & {}),
>(entry: {
collection: C;
id: E;
}): E extends keyof DataEntryMap[C]
? Promise<DataEntryMap[C][E]>
: Promise<CollectionEntry<C> | undefined>;
export function getEntry<
C extends keyof ContentEntryMap,
E extends ValidContentEntrySlug<C> | (string & {}),
>(
collection: C,
slug: E
): E extends ValidContentEntrySlug<C>
? Promise<CollectionEntry<C>>
: Promise<CollectionEntry<C> | undefined>;
export function getEntry<
C extends keyof DataEntryMap,
E extends keyof DataEntryMap[C] | (string & {}),
>(
collection: C,
id: E
): E extends keyof DataEntryMap[C]
? Promise<DataEntryMap[C][E]>
: Promise<CollectionEntry<C> | undefined>;
/** Resolve an array of entry references from the same collection */
export function getEntries<C extends keyof ContentEntryMap>(
entries: {
collection: C;
slug: ValidContentEntrySlug<C>;
}[]
): Promise<CollectionEntry<C>[]>;
export function getEntries<C extends keyof DataEntryMap>(
entries: {
collection: C;
id: keyof DataEntryMap[C];
}[]
): Promise<CollectionEntry<C>[]>;
export function reference<C extends keyof AnyEntryMap>(
collection: C
): import('astro/zod').ZodEffects<
import('astro/zod').ZodString,
C extends keyof ContentEntryMap
? {
collection: C;
slug: ValidContentEntrySlug<C>;
}
: {
collection: C;
id: keyof DataEntryMap[C];
}
>;
// Allow generic `string` to avoid excessive type errors in the config
// if `dev` is not running to update as you edit.
// Invalid collection names will be caught at build time.
export function reference<C extends string>(
collection: C
): import('astro/zod').ZodEffects<import('astro/zod').ZodString, never>;
type ReturnTypeOrOriginal<T> = T extends (...args: any[]) => infer R ? R : T;
type InferEntrySchema<C extends keyof AnyEntryMap> = import('astro/zod').infer<
ReturnTypeOrOriginal<Required<ContentConfig['collections'][C]>['schema']>
>;
type ContentEntryMap = {
"blog": {
"release-0.0.2-schwindlig.mdx": {
id: "release-0.0.2-schwindlig.mdx";
slug: "release-002-schwindlig";
body: string;
collection: "blog";
data: InferEntrySchema<"blog">
} & { render(): Render[".mdx"] };
"release-0.0.2.1-stuermisch.mdx": {
id: "release-0.0.2.1-stuermisch.mdx";
slug: "release-0021-stuermisch";
body: string;
collection: "blog";
data: InferEntrySchema<"blog">
} & { render(): Render[".mdx"] };
"release-0.0.3-maelstrom.mdx": {
id: "release-0.0.3-maelstrom.mdx";
slug: "release-003-maelstrom";
body: string;
collection: "blog";
data: InferEntrySchema<"blog">
} & { render(): Render[".mdx"] };
"release-0.0.4-gischt.mdx": {
id: "release-0.0.4-gischt.mdx";
slug: "release-004-gischt";
body: string;
collection: "blog";
data: InferEntrySchema<"blog">
} & { render(): Render[".mdx"] };
"release-0.3.0-donauwelle.mdx": {
id: "release-0.3.0-donauwelle.mdx";
slug: "release-030-donauwelle";
body: string;
collection: "blog";
data: InferEntrySchema<"blog">
} & { render(): Render[".mdx"] };
"release-0.4.0-brandung.mdx": {
id: "release-0.4.0-brandung.mdx";
slug: "release-040-brandung";
body: string;
collection: "blog";
data: InferEntrySchema<"blog">
} & { render(): Render[".mdx"] };
"release-0.5.0-wirbel.mdx": {
id: "release-0.5.0-wirbel.mdx";
slug: "release-050-wirbel";
body: string;
collection: "blog";
data: InferEntrySchema<"blog">
} & { render(): Render[".mdx"] };
"release-0.6.0-zimtschnecke.mdx": {
id: "release-0.6.0-zimtschnecke.mdx";
slug: "release-060-zimtschnecke";
body: string;
collection: "blog";
data: InferEntrySchema<"blog">
} & { render(): Render[".mdx"] };
"release-0.7.0-zuckerguss.mdx": {
id: "release-0.7.0-zuckerguss.mdx";
slug: "release-070-zuckerguss";
body: string;
collection: "blog";
data: InferEntrySchema<"blog">
} & { render(): Render[".mdx"] };
"release-0.8.0-himbeermuffin.mdx": {
id: "release-0.8.0-himbeermuffin.mdx";
slug: "release-080-himbeermuffin";
body: string;
collection: "blog";
data: InferEntrySchema<"blog">
} & { render(): Render[".mdx"] };
"release-0.9.0-bananenbrot.mdx": {
id: "release-0.9.0-bananenbrot.mdx";
slug: "release-090-bananenbrot";
body: string;
collection: "blog";
data: InferEntrySchema<"blog">
} & { render(): Render[".mdx"] };
"release-1.0.0-geburtstagskuchen.mdx": {
id: "release-1.0.0-geburtstagskuchen.mdx";
slug: "release-100-geburtstagskuchen";
body: string;
collection: "blog";
data: InferEntrySchema<"blog">
} & { render(): Render[".mdx"] };
"year-2.mdx": {
id: "year-2.mdx";
slug: "year-2";
body: string;
collection: "blog";
data: InferEntrySchema<"blog">
} & { render(): Render[".mdx"] };
};
};
type DataEntryMap = {
};
type AnyEntryMap = ContentEntryMap & DataEntryMap;
type ContentConfig = typeof import("../src/content/config");
}
-1
View File
@@ -1,4 +1,3 @@
.astro
# build output
dist/
-1
View File
@@ -33,7 +33,6 @@
"@strudel/hydra": "workspace:*",
"@strudel/midi": "workspace:*",
"@strudel/mini": "workspace:*",
"@strudel/mqtt": "workspace:*",
"@strudel/osc": "workspace:*",
"@strudel/serial": "workspace:*",
"@strudel/soundfonts": "workspace:*",
+5 -5
View File
@@ -26,15 +26,15 @@ const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL
title="Top Navigation"
>
<div class="flex overflow-visible items-center grow" style="overflow:visible">
<div class="flex items-center text-2xl space-x-2">
<a href={`${baseNoTrailing}/`} class="flex items-center text-2xl space-x-2">
<h1 class="font-bold flex space-x-2 items-baseline text-xl">
<span class="block rotate-90 text-blue-500">꩜</span>
<div class="flex space-x-2 items-baseline">
<span>🌀</span>
<div class="flex space-x-1 items-baseline">
<span class="">strudel</span>
<span class="text-sm font-medium">DOCS</span>
<span class="text-sm">DOCS</span>
</div>
</h1>
</div>
</a>
</div>
{/* KNOWN_LANGUAGE_CODES.length > 1 && <LanguageSelect lang={lang} client:idle /> */}
<div class="search-item h-10">
+2 -2
View File
@@ -1,5 +1,5 @@
import Loader from '@src/repl/components/Loader';
import { HorizontalPanel } from '@src/repl/components/panel/Panel';
import { Panel } from '@src/repl/components/panel/Panel';
import { Code } from '@src/repl/components/Code';
import BigPlayButton from '@src/repl/components/BigPlayButton';
import UserFacingErrorMessage from '@src/repl/components/UserFacingErrorMessage';
@@ -20,7 +20,7 @@ export default function UdelsEditor(Props) {
<Code containerRef={containerRef} editorRef={editorRef} init={init} />
</div>
<UserFacingErrorMessage error={error} />
<HorizontalPanel context={context} />
<Panel context={context} />
</div>
);
}
@@ -26,7 +26,7 @@ author: froos
- fix: 💄 Enhance visualisation of the Tutorial on mobile by @puria in https://github.com/tidalcycles/strudel/pull/19
- Stateful queries and events (WIP) by @yaxu in https://github.com/tidalcycles/strudel/pull/14
- Fix resolveState by @yaxu in https://github.com/tidalcycles/strudel/pull/22
- added \_asNumber + interpret numbers as midi by @felixroos in https://github.com/tidalcycles/strudel/pull/21
- added \_asNumber + interprete numbers as midi by @felixroos in https://github.com/tidalcycles/strudel/pull/21
- Update package.json by @ChiakiUehira in https://github.com/tidalcycles/strudel/pull/23
- packaging by @felixroos in https://github.com/tidalcycles/strudel/pull/24
@@ -28,7 +28,7 @@ author: froos
- support freq in pianoroll by @felixroos in https://github.com/tidalcycles/strudel/pull/308
- ICLC2023 paper WIP by @yaxu in https://github.com/tidalcycles/strudel/pull/306
- fix: copy share link to clipboard was broken for some browsers by @felixroos in https://github.com/tidalcycles/strudel/pull/311
- fix: copy share link to clipboard was broken for some browers by @felixroos in https://github.com/tidalcycles/strudel/pull/311
- Jsdoc component by @felixroos in https://github.com/tidalcycles/strudel/pull/312
- object support for .scale by @felixroos in https://github.com/tidalcycles/strudel/pull/307
- Astro build by @felixroos in https://github.com/tidalcycles/strudel/pull/315
@@ -138,7 +138,7 @@ Plenty of things have been added to the docs, including a [showcase of what peop
- Export patterns + ui tweaks by @felixroos in https://github.com/tidalcycles/strudel/pull/855
- Pattern organization by @felixroos in https://github.com/tidalcycles/strudel/pull/858
- Sound Import from local file system by @daslyfe in https://github.com/tidalcycles/strudel/pull/839
- bugfix: suspend and close existing audio context when changing interface by @daslyfe in https://github.com/tidalcycles/strudel/pull/882
- bugfix: suspend and close exisiting audio context when changing interface by @daslyfe in https://github.com/tidalcycles/strudel/pull/882
- add root mode for voicings by @felixroos in https://github.com/tidalcycles/strudel/pull/887
- scales can now be anchored by @felixroos in https://github.com/tidalcycles/strudel/pull/888
- add dough function for raw dsp by @felixroos in https://github.com/tidalcycles/strudel/pull/707 (experimental)
@@ -195,7 +195,7 @@ Plenty of things have been added to the docs, including a [showcase of what peop
- Update tauri.yml workflow file by @vasilymilovidov in https://github.com/tidalcycles/strudel/pull/705
- vite-vanilla-repl readme fix by @felixroos in https://github.com/tidalcycles/strudel/pull/737
- completely revert config mess by @felixroos in https://github.com/tidalcycles/strudel/pull/745
- hopefully fix trailing slashes bug by @felixroos in https://github.com/tidalcycles/strudel/pull/753
- hopefully fix trainling slashes bug by @felixroos in https://github.com/tidalcycles/strudel/pull/753
- Update vite pwa by @felixroos in https://github.com/tidalcycles/strudel/pull/772
- Update to Astro 3 by @felixroos in https://github.com/tidalcycles/strudel/pull/775
- support multiple named serial connections, change default baudrate by @yaxu in https://github.com/tidalcycles/strudel/pull/551
+2 -5
View File
@@ -1,6 +1,7 @@
import { useState, useRef, useCallback, useMemo, useEffect } from 'react';
import { Icon } from './Icon';
import { silence, noteToMidi, _mod } from '@strudel/core';
import { clearHydra } from '@strudel/hydra';
import { getDrawContext, getPunchcardPainter } from '@strudel/draw';
import { transpiler } from '@strudel/transpiler';
import { getAudioContext, webaudioOutput, initAudioOnFirstClick } from '@strudel/webaudio';
@@ -36,7 +37,7 @@ export function MiniRepl({
const shouldShowCanvas = !!punchcard;
const canvasId = shouldShowCanvas ? useMemo(() => `canvas-${id}`, [id]) : null;
autodraw = !!punchcard || !!claviature || !!autodraw;
drawTime = (drawTime ?? punchcard) ? [0, 4] : [-2, 2];
drawTime = drawTime ?? punchcard ? [0, 4] : [-2, 2];
if (claviature) {
drawTime = [0, 0];
}
@@ -119,7 +120,6 @@ export function MiniRepl({
'cursor-pointer w-16 flex items-center justify-center p-1 border-r border-lineHighlight text-foreground bg-lineHighlight hover:bg-background',
started ? 'animate-pulse' : '',
)}
aria-label={started ? 'stop' : 'play'}
onClick={() => editorRef.current?.toggle()}
>
<Icon type={started ? 'stop' : 'play'} />
@@ -129,7 +129,6 @@ export function MiniRepl({
'w-16 flex items-center justify-center p-1 text-foreground border-lineHighlight bg-lineHighlight',
isDirty ? 'text-foreground hover:bg-background cursor-pointer' : 'opacity-50 cursor-not-allowed',
)}
aria-label="update"
onClick={() => editorRef.current?.evaluate()}
>
<Icon type="refresh" />
@@ -141,7 +140,6 @@ export function MiniRepl({
className={
'cursor-pointer w-16 flex items-center justify-center p-1 border-r border-lineHighlight text-foreground bg-lineHighlight hover:bg-background'
}
aria-label="previous example"
onClick={() => changeTune(tuneIndex - 1)}
>
<div className="rotate-180">
@@ -152,7 +150,6 @@ export function MiniRepl({
className={
'cursor-pointer w-16 flex items-center justify-center p-1 border-r border-lineHighlight text-foreground bg-lineHighlight hover:bg-background'
}
aria-label="next example"
onClick={() => changeTune(tuneIndex + 1)}
>
<Icon type="skip" />
@@ -82,7 +82,18 @@ To modify a parameter value, you can either:
<MiniRepl client:only="react" tune={`note("50 60 70").room(.1).add(note("<0 1 2>")).log()`} />
Remember the execution of the chained functions goes from left to right.
- Modify _all_ numeral params:
<MiniRepl client:only="react" tune={`note("50 60 70").room(.1).add("<0 1 2>").log()`} />
Which of these 3 ways to use strongly depends on the context!
Note that the order of chaining param functions also matters!
In the last example, the `room` value would not have changed if it was applied later:
<MiniRepl client:only="react" tune={`note("50 60 70").add("<0 1 2>").room(.1).log()`} />
This shows how the execution of the chained functions goes from left to right.
In this case, the `.add` will only modify what's on the left side.
# Operators
+13 -9
View File
@@ -25,14 +25,26 @@ These are the equivalents used by the Mini Notation:
<JsDoc client:idle name="cat" h={0} />
You can also use cat as a chained function like this:
<JsDoc client:idle name="Pattern.cat" h={0} hideDescription />
## seq
<JsDoc client:idle name="seq" h={0} />
Or as a chained function:
<JsDoc client:idle name="Pattern.seq" h={0} hideDescription />
## stack
<JsDoc client:idle name="stack" h={0} />
As a chained function:
<JsDoc client:idle name="Pattern.stack" h={0} hideDescription />
## s_cat
<JsDoc client:idle name="s_cat" h={0} />
@@ -55,14 +67,6 @@ These are the equivalents used by the Mini Notation:
## run
<JsDoc client:idle name="run" h={0} />
## binary
<JsDoc client:idle name="binary" h={0} />
## binaryN
<JsDoc client:idle name="binaryN" h={0} />
<JsDoc client:idle name="run" h={0} punchcard />
After Pattern Constructors, let's see what [Time Modifiers](/learn/time-modifiers) are available.
+14 -81
View File
@@ -1,20 +1,19 @@
---
title: MIDI, OSC & MQTT
title: MIDI & OSC
layout: ../../layouts/MainLayout.astro
---
import { MiniRepl } from '../../docs/MiniRepl';
import { JsDoc } from '../../docs/JsDoc';
# MIDI, OSC and MQTT
# MIDI and OSC
Normally, Strudel is used to pattern sound, using its own '[web audio](https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API)'-based synthesiser called [SuperDough](https://github.com/tidalcycles/strudel/tree/main/packages/superdough).
The default audio output of Strudel uses the [Web Audio API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API).
It is also possible to use Strudel with MIDI and OSC / [SuperDirt](https://github.com/musikinformatik/SuperDirt/) instead.
It is also possible to pattern other things with Strudel, such as software and hardware synthesisers with MIDI, other software using Open Sound Control/OSC (including the [SuperDirt](https://github.com/musikinformatik/SuperDirt/) synthesiser commonly used with Strudel's sibling [TidalCycles](https://tidalcycles.org/)), or the MQTT 'internet of things' protocol.
# MIDI API
# MIDI
Strudel supports MIDI without any additional software (thanks to [webmidi](https://npmjs.com/package/webmidi)), just by adding methods to your pattern:
Strudel also supports midi via [webmidi](https://npmjs.com/package/webmidi).
## midi(outputName?)
@@ -46,22 +45,20 @@ But you can also control cc messages separately like this:
$: ccv(sine.segment(16).slow(4)).ccn(74).midi()`}
/>
# OSC/SuperDirt/StrudelDirt
# OSC/SuperDirt API
In TidalCycles, sound is usually generated using [SuperDirt](https://github.com/musikinformatik/SuperDirt/), which runs inside SuperCollider. Strudel also supports using SuperDirt, although it requires installing some additional software.
There is also [StrudelDirt](https://github.com/daslyfe/StrudelDirt) which is SuperDirt with some optimisations for working with Strudel. (A longer term aim is to merge these optimisations back into mainline SuperDirt)
In mainline tidal, the actual sound is generated via [SuperDirt](https://github.com/musikinformatik/SuperDirt/), which runs inside SuperCollider.
Strudel also supports using [SuperDirt](https://github.com/musikinformatik/SuperDirt/) as a backend, although it requires some developer tooling to run.
## Prequisites
To get SuperDirt to work with Strudel, you need to
Getting [SuperDirt](https://github.com/musikinformatik/SuperDirt/) to work with Strudel, you need to
1. install SuperCollider + sc3 plugins, see [Tidal Docs](https://tidalcycles.org/docs/) (Install Tidal) for more info.
2. install SuperDirt, or the [StrudelDirt](https://github.com/daslyfe/StrudelDirt) fork which is optimised for use with Strudel
3. install [node.js](https://nodejs.org/en/)
4. download [Strudel Repo](https://github.com/tidalcycles/strudel/) (or git clone, if you have git installed)
5. run `pnpm i` in the strudel directory
6. run `pnpm run osc` to start the osc server, which forwards OSC messages from Strudel REPL to SuperCollider
2. install [node.js](https://nodejs.org/en/)
3. download [Strudel Repo](https://github.com/tidalcycles/strudel/) (or git clone, if you have git installed)
4. run `pnpm i` in the strudel directory
5. run `pnpm run osc` to start the osc server, which forwards OSC messages from Strudel REPL to SuperCollider
Now you're all set!
@@ -89,67 +86,3 @@ Please refer to [Tidal Docs](https://tidalcycles.org/) for more info.
<br />
But can we use Strudel [offline](/learn/pwa)?
# MQTT
MQTT is a lightweight network protocol, designed for 'internet of things' devices. For use with strudel, you will
need access to an MQTT server known as a 'broker' configured to accept secure 'websocket' connections. You could
run one yourself (e.g. by running [mosquitto](https://mosquitto.org/)), although getting an SSL certificate that
your web browser will trust might be a bit tricky for those without systems administration experience.
Alternatively, you can use [a public broker](https://www.hivemq.com/mqtt/public-mqtt-broker/).
Strudel does not yet support receiving messages over MQTT, only sending them.
## Usage
The following example shows how to send a pattern to an MQTT broker:
<MiniRepl
client:only="react"
tune={`"hello world"
.mqtt(undefined, // username (undefined for open/public servers)
undefined, // password
'/strudel-pattern', // mqtt 'topic'
'wss://mqtt.eclipseprojects.io:443/mqtt', // MQTT server address
'mystrudel', // MQTT client id - randomly generated if not supplied
0 // latency / delay before sending messages (0 = no delay)
)`}
/>
Other software can then receive the messages. For example using the [mosquitto](https://mosquitto.org/) commandline client tools:
```
> mosquitto_sub -h mqtt.eclipseprojects.io -p 1883 -t "/strudel-pattern"
hello
world
hello
world
...
```
Control patterns will be encoded as JSON, for example:
<MiniRepl
client:only="react"
tune={`sound("sax(3,8)").speed("2 3")
.mqtt(undefined, // username (undefined for open/public servers)
undefined, // password
'/strudel-pattern', // mqtt 'topic'
'wss://mqtt.eclipseprojects.io:443/mqtt', // MQTT server address
'mystrudel', // MQTT client id - randomly generated if not supplied
0 // latency / delay before sending messages (0 = no delay)
)`}
/>
Will send messages like the following:
```
{"s":"sax","speed":2}
{"s":"sax","speed":2}
{"s":"sax","speed":3}
{"s":"sax","speed":2}
...
```
Libraries for receiving MQTT are available for many programming languages.
@@ -98,11 +98,3 @@ What follows is the API doc of all the options you can pass:
## Pitchwheel
<JsDoc client:idle name="pitchwheel" h={0} />
## Spectrum
<JsDoc client:idle name="spectrum" h={0} />
## markcss
<JsDoc client:idle name="markcss" h={0} />
+24 -25
View File
@@ -1,4 +1,8 @@
import AcademicCapIcon from '@heroicons/react/20/solid/AcademicCapIcon';
import ArrowPathIcon from '@heroicons/react/20/solid/ArrowPathIcon';
import LinkIcon from '@heroicons/react/20/solid/LinkIcon';
import PlayCircleIcon from '@heroicons/react/20/solid/PlayCircleIcon';
import SparklesIcon from '@heroicons/react/20/solid/SparklesIcon';
import StopCircleIcon from '@heroicons/react/20/solid/StopCircleIcon';
import cx from '@src/cx.mjs';
import { useSettings, setIsZen } from '../../settings.mjs';
@@ -11,19 +15,24 @@ export function Header({ context, embedded = false }) {
const { started, pending, isDirty, activeCode, handleTogglePlay, handleEvaluate, handleShuffle, handleShare } =
context;
const isEmbedded = typeof window !== 'undefined' && (embedded || window.location !== window.parent.location);
const { isZen, isButtonRowHidden, isCSSAnimationDisabled } = useSettings();
const { isZen } = useSettings();
return (
<header
id="header"
className={cx(
'flex-none text-black z-[100] text-lg select-none h-20 md:h-14',
'flex-none text-black z-[100] text-lg select-none',
!isZen && !isEmbedded && 'bg-lineHighlight',
isZen ? 'h-12 w-8 fixed top-0 left-0' : 'sticky top-0 w-full py-1 justify-between',
isEmbedded ? 'flex' : 'md:flex',
)}
>
<div className="px-4 flex space-x-2 md:pt-0 select-none">
{/* <img
src={logo}
className={cx('Tidal-logo', isEmbedded ? 'w-8 h-8' : 'w-10 h-10', started && 'animate-pulse')} // 'bg-[#ffffff80] rounded-full'
alt="logo"
/> */}
<h1
onClick={() => {
if (isEmbedded) window.open(window.location.href.replace('embed', ''));
@@ -34,46 +43,31 @@ export function Header({ context, embedded = false }) {
)}
>
<div
className={cx(
'mt-[1px]',
started && !isCSSAnimationDisabled && 'animate-spin',
'cursor-pointer text-blue-500',
isZen && 'fixed top-2 right-4',
)}
className={cx('mt-[1px]', started && 'animate-spin', 'cursor-pointer')}
onClick={() => {
if (!isEmbedded) {
setIsZen(!isZen);
}
}}
>
<span className="block rotate-90"></span>
🌀
</div>
{!isZen && (
<div className="space-x-2">
<span className="">strudel</span>
<span className="text-sm font-medium">REPL</span>
{!isEmbedded && isButtonRowHidden && (
<a href={`${baseNoTrailing}/learn`} className="text-sm opacity-25 font-medium">
DOCS
</a>
)}
<div className={cx(started && 'animate-pulse')}>
<span className="">strudel</span> <span className="text-sm">REPL</span>
</div>
)}
</h1>
</div>
{!isZen && !isButtonRowHidden && (
<div className="flex max-w-full overflow-auto text-foreground px-1 md:px-2">
{!isZen && (
<div className="flex max-w-full overflow-auto text-foreground">
<button
onClick={handleTogglePlay}
title={started ? 'stop' : 'play'}
className={cx(
!isEmbedded ? 'p-2' : 'px-2',
'hover:opacity-50',
!started && !isCSSAnimationDisabled && 'animate-pulse',
)}
className={cx(!isEmbedded ? 'p-2' : 'px-2', 'hover:opacity-50', !started && 'animate-pulse')}
>
{!pending ? (
<span className={cx('flex items-center space-x-2')}>
<span className={cx('flex items-center space-x-1', isEmbedded ? '' : 'w-16')}>
{started ? <StopCircleIcon className="w-6 h-6" /> : <PlayCircleIcon className="w-6 h-6" />}
{!isEmbedded && <span>{started ? 'stop' : 'play'}</span>}
</span>
@@ -90,6 +84,8 @@ export function Header({ context, embedded = false }) {
!isDirty || !activeCode ? 'opacity-50' : 'hover:opacity-50',
)}
>
{/* <CommandLineIcon className="w-6 h-6" /> */}
<ArrowPathIcon className="w-6 h-6" />
{!isEmbedded && <span>update</span>}
</button>
{!isEmbedded && (
@@ -98,6 +94,7 @@ export function Header({ context, embedded = false }) {
className="hover:opacity-50 p-2 flex items-center space-x-1"
onClick={handleShuffle}
>
<SparklesIcon className="w-6 h-6" />
<span> shuffle</span>
</button>
)}
@@ -110,6 +107,7 @@ export function Header({ context, embedded = false }) {
)}
onClick={handleShare}
>
<LinkIcon className="w-6 h-6" />
<span>share</span>
</button>
)}
@@ -119,6 +117,7 @@ export function Header({ context, embedded = false }) {
href={`${baseNoTrailing}/workshop/getting-started/`}
className={cx('hover:opacity-50 flex items-center space-x-1', !isEmbedded ? 'p-2' : 'px-2')}
>
<AcademicCapIcon className="w-6 h-6" />
<span>learn</span>
</a>
)}
+4 -4
View File
@@ -1,5 +1,5 @@
import Loader from '@src/repl/components/Loader';
import { HorizontalPanel, VerticalPanel } from '@src/repl/components/panel/Panel';
import { Panel } from '@src/repl/components/panel/Panel';
import { Code } from '@src/repl/components/Code';
import UserFacingErrorMessage from '@src/repl/components/UserFacingErrorMessage';
import { Header } from './Header';
@@ -13,7 +13,7 @@ export default function ReplEditor(Props) {
const { context } = Props;
const { containerRef, editorRef, error, init, pending } = context;
const settings = useSettings();
const { panelPosition, isZen } = settings;
const { panelPosition } = settings;
return (
<div className="h-full flex flex-col relative">
@@ -21,10 +21,10 @@ export default function ReplEditor(Props) {
<Header context={context} />
<div className="grow flex relative overflow-hidden">
<Code containerRef={containerRef} editorRef={editorRef} init={init} />
{!isZen && panelPosition === 'right' && <VerticalPanel context={context} />}
{panelPosition === 'right' && <Panel context={context} />}
</div>
<UserFacingErrorMessage error={error} />
{!isZen && panelPosition === 'bottom' && <HorizontalPanel context={context} />}
{panelPosition === 'bottom' && <Panel context={context} />}
</div>
);
}
@@ -1,48 +1,18 @@
import { logger } from '@strudel/core';
import useEvent from '@src/useEvent.mjs';
import cx from '@src/cx.mjs';
import { nanoid } from 'nanoid';
import { useCallback, useState } from 'react';
import { useSettings } from '../../../settings.mjs';
export function ConsoleTab() {
const [log, setLog] = useState([]);
const { fontFamily, fontSize } = useSettings();
useLogger(
useCallback((e) => {
const { message, type, data } = e.detail;
setLog((l) => {
const lastLog = l.length ? l[l.length - 1] : undefined;
const id = nanoid(12);
// if (type === 'loaded-sample' && lastLog.type === 'load-sample' && lastLog.url === data.url) {
if (type === 'loaded-sample') {
// const loadIndex = l.length - 1;
const loadIndex = l.findIndex(({ data: { url }, type }) => type === 'load-sample' && url === data.url);
l[loadIndex] = { message, type, id, data };
} else if (lastLog && lastLog.message === message) {
l = l.slice(0, -1).concat([{ message, type, count: (lastLog.count ?? 1) + 1, id, data }]);
} else {
l = l.concat([{ message, type, id, data }]);
}
return l.slice(-20);
});
}, []),
);
export function ConsoleTab({ log }) {
return (
<div
id="console-tab"
className="break-all px-4 dark:text-white text-stone-900 text-sm py-2 space-y-1"
style={{ fontFamily, fontSize }}
>
<div id="console-tab" className="break-all px-4 dark:text-white text-stone-900 text-sm">
<pre>{`███████╗████████╗██████╗ ██╗ ██╗██████╗ ███████╗██╗
`}</pre>
{log.map((l, i) => {
const message = linkify(l.message);
const color = l.data?.hap?.value?.color;
return (
<div
key={l.id}
className={cx(l.type === 'error' && 'text-red-500', l.type === 'highlight' && 'underline')}
style={color ? { color } : {}}
>
<div key={l.id} className={cx(l.type === 'error' && 'text-red-500', l.type === 'highlight' && 'underline')}>
<span dangerouslySetInnerHTML={{ __html: message }} />
{l.count ? ` (${l.count})` : ''}
</div>
@@ -72,7 +42,3 @@ function linkify(inputText) {
return replacedText;
}
function useLogger(onTrigger) {
useEvent(logger.key, onTrigger);
}
@@ -6,7 +6,6 @@ export function ButtonGroup({ value, onChange, items }) {
{Object.entries(items).map(([key, label], i, arr) => (
<button
key={key}
id={key}
onClick={() => onChange(key)}
className={cx(
'px-2 border-b h-8 whitespace-nowrap',
+103 -187
View File
@@ -1,5 +1,10 @@
import XMarkIcon from '@heroicons/react/20/solid/XMarkIcon';
import { logger } from '@strudel/core';
import useEvent from '@src/useEvent.mjs';
import cx from '@src/cx.mjs';
import { setPanelPinned, setActiveFooter as setTab, setIsPanelOpened, useSettings } from '../../../settings.mjs';
import { nanoid } from 'nanoid';
import { useCallback, useLayoutEffect, useEffect, useRef, useState } from 'react';
import { setActiveFooter, useSettings } from '../../../settings.mjs';
import { ConsoleTab } from './ConsoleTab';
import { FilesTab } from './FilesTab';
import { Reference } from './Reference';
@@ -7,206 +12,117 @@ import { SettingsTab } from './SettingsTab';
import { SoundsTab } from './SoundsTab';
import { WelcomeTab } from './WelcomeTab';
import { PatternsTab } from './PatternsTab';
import { ChevronLeftIcon, XMarkIcon } from '@heroicons/react/16/solid';
import useClient from '@src/useClient.mjs';
// https://gist.github.com/gaearon/e7d97cdf38a2907924ea12e4ebdf3c85
export const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;
const TAURI = typeof window !== 'undefined' && window.__TAURI__;
export function HorizontalPanel({ context }) {
const settings = useSettings();
const { isPanelOpen, activeFooter: tab } = settings;
export function Panel({ context }) {
const footerContent = useRef();
const [log, setLog] = useState([]);
const { activeFooter, isZen, panelPosition } = useSettings();
return (
<PanelNav
settings={settings}
className={cx(isPanelOpen ? `min-h-[360px] max-h-[360px]` : 'min-h-12 max-h-12', 'overflow-hidden flex flex-col')}
>
{isPanelOpen && (
<div className="flex h-full overflow-auto pr-10 ">
<PanelContent context={context} tab={tab} />
</div>
)}
useIsomorphicLayoutEffect(() => {
if (footerContent.current && activeFooter === 'console') {
// scroll log box to bottom when log changes
footerContent.current.scrollTop = footerContent.current?.scrollHeight;
}
}, [log, activeFooter]);
useIsomorphicLayoutEffect(() => {
if (!footerContent.current) {
} else if (activeFooter === 'console') {
footerContent.current.scrollTop = footerContent.current?.scrollHeight;
} else {
footerContent.current.scrollTop = 0;
}
}, [activeFooter]);
<div className="absolute right-4 pt-4">
<PanelActionButton settings={settings} />
</div>
<div className="flex justify-between min-h-12 max-h-12 grid-cols-2 items-center">
<Tabs setTab={setTab} tab={tab} />
</div>
</PanelNav>
useLogger(
useCallback((e) => {
const { message, type, data } = e.detail;
setLog((l) => {
const lastLog = l.length ? l[l.length - 1] : undefined;
const id = nanoid(12);
// if (type === 'loaded-sample' && lastLog.type === 'load-sample' && lastLog.url === data.url) {
if (type === 'loaded-sample') {
// const loadIndex = l.length - 1;
const loadIndex = l.findIndex(({ data: { url }, type }) => type === 'load-sample' && url === data.url);
l[loadIndex] = { message, type, id, data };
} else if (lastLog && lastLog.message === message) {
l = l.slice(0, -1).concat([{ message, type, count: (lastLog.count ?? 1) + 1, id, data }]);
} else {
l = l.concat([{ message, type, id, data }]);
}
return l.slice(-20);
});
}, []),
);
}
export function VerticalPanel({ context }) {
const settings = useSettings();
const { activeFooter: tab, isPanelOpen } = settings;
const PanelTab = ({ children, name, label }) => (
<>
<div
onClick={() => setActiveFooter(name)}
className={cx(
'h-8 px-2 text-foreground cursor-pointer hover:opacity-50 flex items-center space-x-1 border-b',
activeFooter === name ? 'border-foreground' : 'border-transparent',
)}
>
{label || name}
</div>
{activeFooter === name && <>{children}</>}
</>
);
const client = useClient();
if (isZen) {
return null;
}
const isActive = activeFooter !== '';
let positions = {
right: cx('max-w-full flex-grow-0 flex-none overflow-hidden', isActive ? 'w-[600px] h-full' : 'absolute right-0'),
bottom: cx('relative', isActive ? 'h-[360px] min-h-[360px]' : ''),
};
if (!client) {
return null;
}
return (
<PanelNav
settings={settings}
className={cx(isPanelOpen ? `min-w-[min(600px,80vw)] max-w-[min(600px,80vw)]` : 'min-w-12 max-w-12')}
>
{isPanelOpen ? (
<div className={cx('flex flex-col h-full')}>
<div className="flex justify-between w-full ">
<Tabs setTab={setTab} tab={tab} />
<PanelActionButton settings={settings} />
</div>
<div className="overflow-auto h-full">
<PanelContent context={context} tab={tab} />
<nav className={cx('bg-lineHighlight z-[10] flex flex-col', positions[panelPosition])}>
<div className="flex justify-between px-2">
<div className={cx('flex select-none max-w-full overflow-auto', activeFooter && 'pb-2')}>
<PanelTab name="intro" label="welcome" />
<PanelTab name="patterns" />
<PanelTab name="sounds" />
<PanelTab name="console" />
<PanelTab name="reference" />
<PanelTab name="settings" />
{TAURI && <PanelTab name="files" />}
</div>
{activeFooter !== '' && (
<button onClick={() => setActiveFooter('')} className="text-foreground px-2" aria-label="Close Panel">
<XMarkIcon className="w-5 h-5" />
</button>
)}
</div>
{activeFooter !== '' && (
<div className="relative overflow-hidden">
<div className="text-white overflow-auto h-full max-w-full" ref={footerContent}>
{activeFooter === 'intro' && <WelcomeTab context={context} />}
{activeFooter === 'patterns' && <PatternsTab context={context} />}
{activeFooter === 'console' && <ConsoleTab log={log} />}
{activeFooter === 'sounds' && <SoundsTab />}
{activeFooter === 'reference' && <Reference />}
{activeFooter === 'settings' && <SettingsTab started={context.started} />}
{activeFooter === 'files' && <FilesTab />}
</div>
</div>
) : (
<button
onClick={(e) => {
setIsPanelOpened(true);
}}
aria-label="open menu panel"
className={cx(
'flex flex-col hover:bg-lineBackground items-center cursor-pointer justify-center w-full h-full',
)}
>
<ChevronLeftIcon className="text-foreground opacity-50 w-6 h-6" />
</button>
)}
</PanelNav>
);
}
const tabNames = {
welcome: 'intro',
patterns: 'patterns',
sounds: 'sounds',
reference: 'reference',
console: 'console',
settings: 'settings',
};
if (TAURI) {
tabNames.files = 'files';
}
function PanelNav({ children, className, settings, ...props }) {
const isHoverBehavior = settings.togglePanelTrigger === 'hover';
return (
<nav
onClick={() => {
if (!settings.isPanelOpen) {
setIsPanelOpened(true);
}
}}
onMouseEnter={() => {
if (isHoverBehavior && !settings.isPanelOpen) {
setIsPanelOpened(true);
}
}}
onMouseLeave={() => {
if (isHoverBehavior && !settings.isPanelPinned) {
setIsPanelOpened(false);
}
}}
aria-label="Menu Panel"
className={cx('bg-lineHighlight group overflow-x-auto', className)}
{...props}
>
{children}
</nav>
);
}
function PanelContent({ context, tab }) {
switch (tab) {
case tabNames.patterns:
return <PatternsTab context={context} />;
case tabNames.console:
return <ConsoleTab />;
case tabNames.sounds:
return <SoundsTab />;
case tabNames.reference:
return <Reference />;
case tabNames.settings:
return <SettingsTab started={context.started} />;
case tabNames.files:
return <FilesTab />;
default:
return <WelcomeTab context={context} />;
}
}
function PanelTab({ label, isSelected, onClick }) {
return (
<>
<button
onClick={onClick}
className={cx(
'h-8 px-2 text-foreground cursor-pointer hover:opacity-50 flex items-center space-x-1 border-b',
isSelected ? 'border-foreground' : 'border-transparent',
)}
>
{label}
</button>
</>
);
}
function Tabs({ setTab, tab, className }) {
return (
<div className={cx('flex select-none max-w-full overflow-auto pb-2', className)}>
{Object.keys(tabNames).map((key) => {
const val = tabNames[key];
return <PanelTab key={key} isSelected={tab === val} label={key} onClick={() => setTab(val)} />;
})}
</div>
);
}
function PanelActionButton({ settings }) {
const { togglePanelTrigger, isPanelPinned, isPanelOpen } = settings;
const isHoverBehavior = togglePanelTrigger === 'hover';
if (!isPanelOpen) {
return;
}
if (isHoverBehavior) {
return <PinButton pinned={isPanelPinned} />;
}
return <CloseButton onClick={() => setIsPanelOpened(false)} />;
}
function PinButton({ pinned }) {
return (
<button
onClick={() => setPanelPinned(!pinned)}
className={cx(
'text-foreground max-h-8 min-h-8 max-w-8 min-w-8 items-center justify-center p-1.5 group-hover:flex',
pinned ? 'flex' : 'hidden',
)}
aria-label="Pin Menu Panel"
>
<svg
stroke="currentColor"
fill={'currentColor'}
strokeWidth="0"
className="w-full h-full"
opacity={pinned ? 1 : '.3'}
viewBox="0 0 16 16"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M9.828.722a.5.5 0 0 1 .354.146l4.95 4.95a.5.5 0 0 1 0 .707c-.48.48-1.072.588-1.503.588-.177 0-.335-.018-.46-.039l-3.134 3.134a6 6 0 0 1 .16 1.013c.046.702-.032 1.687-.72 2.375a.5.5 0 0 1-.707 0l-2.829-2.828-3.182 3.182c-.195.195-1.219.902-1.414.707s.512-1.22.707-1.414l3.182-3.182-2.828-2.829a.5.5 0 0 1 0-.707c.688-.688 1.673-.767 2.375-.72a6 6 0 0 1 1.013.16l3.134-3.133a3 3 0 0 1-.04-.461c0-.43.108-1.022.589-1.503a.5.5 0 0 1 .353-.146"></path>
</svg>
</button>
);
}
function CloseButton({ onClick }) {
return (
<button
onClick={onClick}
className={cx(
'text-foreground max-h-8 min-h-8 max-w-8 min-w-8 items-center justify-center p-1.5 group-hover:flex',
)}
aria-label="Close Menu"
>
<XMarkIcon />
</button>
);
function useLogger(onTrigger) {
useEvent(logger.key, onTrigger);
}
@@ -77,7 +77,7 @@ function PatternButtons({ patterns, activePattern, onClick, started }) {
function ActionButton({ children, onClick, label, labelIsHidden }) {
return (
<button className="hover:opacity-50 text-nowrap" onClick={onClick} title={label}>
<button className="hover:opacity-50" onClick={onClick} title={label}>
{labelIsHidden !== true && label}
{children}
</button>
@@ -102,7 +102,7 @@ export function PatternsTab({ context }) {
const autoResetPatternOnChange = !isUdels();
return (
<div className="px-4 w-full dark:text-white text-stone-900 space-y-2 flex flex-col overflow-hidden max-h-full h-full">
<div className="px-4 w-full dark:text-white text-stone-900 space-y-2 pb-4 flex flex-col overflow-hidden max-h-full">
<ButtonGroup
value={patternFilter}
onChange={(value) => settingsMap.setKey('patternFilter', value)}
@@ -155,7 +155,7 @@ export function PatternsTab({ context }) {
</div>
)}
<section className="flex overflow-y-auto max-h-full flex-grow flex-col">
<section className="flex overflow-y-scroll max-h-full flex-col">
{patternFilter === patternFilterName.user && (
<PatternButtons
onClick={(id) =>
+20 -47
View File
@@ -1,7 +1,5 @@
import { useMemo, useState } from 'react';
import jsdocJson from '../../../../../doc.json';
const availableFunctions = jsdocJson.docs
const visibleFunctions = jsdocJson.docs
.filter(({ name, description }) => name && !name.startsWith('_') && !!description)
.sort((a, b) => /* a.meta.filename.localeCompare(b.meta.filename) + */ a.name.localeCompare(b.name));
@@ -12,54 +10,29 @@ const getInnerText = (html) => {
};
export function Reference() {
const [search, setSearch] = useState('');
const visibleFunctions = useMemo(() => {
return availableFunctions.filter((entry) => {
if (!search) {
return true;
}
return entry.name.includes(search) || (entry.synonyms?.some((s) => s.includes(search)) ?? false);
});
}, [search]);
return (
<div className="flex h-full w-full p-2 text-foreground overflow-hidden">
<div className="h-full flex flex-col gap-2 w-1/3 max-w-72 ">
<div class="w-full flex">
<input
className="w-full p-1 bg-background rounded-md border-none"
placeholder="Search"
value={search}
onInput={(event) => setSearch(event.target.value)}
/>
</div>
<div className="flex flex-col h-full overflow-y-auto gap-1.5 bg-background bg-opacity-50 rounded-md">
{visibleFunctions.map((entry, i) => (
<a
key={i}
className="cursor-pointer flex-none hover:bg-lineHighlight overflow-x-hidden px-1 text-ellipsis"
onClick={() => {
const el = document.getElementById(`doc-${i}`);
const container = document.getElementById('reference-container');
container.scrollTo(0, el.offsetTop);
}}
>
{entry.name} {/* <span className="text-gray-600">{entry.meta.filename}</span> */}
</a>
))}
</div>
<div className="flex h-full w-full pt-2 text-foreground overflow-hidden">
<div className="w-42 flex-none h-full overflow-y-auto overflow-x-hidden pr-4">
{visibleFunctions.map((entry, i) => (
<a
key={i}
className="cursor-pointer block hover:bg-lineHighlight py-1 px-4"
onClick={() => {
const el = document.getElementById(`doc-${i}`);
const container = document.getElementById('reference-container');
container.scrollTo(0, el.offsetTop);
}}
>
{entry.name} {/* <span className="text-gray-600">{entry.meta.filename}</span> */}
</a>
))}
</div>
<div
className="break-normal flex-grow flex-col overflow-y-auto overflow-x-hidden px-2 flex relative"
id="reference-container"
>
<div className="prose dark:prose-invert min-w-full px-1 ">
<div className="break-normal w-full h-full overflow-auto pl-4 flex relative" id="reference-container">
<div className="prose dark:prose-invert max-w-full pr-4">
<h2>API Reference</h2>
<p>
This is the long list of functions you can use. Remember that you don't need to remember all of those and
that you can already make music with a small set of functions!
This is the long list functions you can use! Remember that you don't need to remember all of those and that
you can already make music with a small set of functions!
</p>
{visibleFunctions.map((entry, i) => (
<section key={i}>
@@ -94,8 +94,6 @@ export function SettingsTab({ started }) {
isAutoCompletionEnabled,
isTooltipEnabled,
isFlashEnabled,
isButtonRowHidden,
isCSSAnimationDisabled,
isSyncEnabled,
isLineWrappingEnabled,
fontSize,
@@ -103,12 +101,11 @@ export function SettingsTab({ started }) {
panelPosition,
audioDeviceName,
audioEngineTarget,
togglePanelTrigger,
} = useSettings();
const shouldAlwaysSync = isUdels();
const canChangeAudioDevice = AudioContext.prototype.setSinkId != null;
return (
<div className="text-foreground p-4 space-y-4 w-full">
<div className="text-foreground p-4 space-y-4">
{canChangeAudioDevice && (
<FormItem label="Audio Output Device">
<AudioDeviceSelector
@@ -173,32 +170,7 @@ export function SettingsTab({ started }) {
items={{ bottom: 'Bottom', right: 'Right' }}
></ButtonGroup>
</FormItem>
<FormItem label="Open Panel on: ">
<ButtonGroup
value={togglePanelTrigger}
onChange={(value) => settingsMap.setKey('togglePanelTrigger', value)}
items={{ click: 'Click', hover: 'Hover' }}
></ButtonGroup>
{/* <Checkbox
label="Click"
onChange={(cbEvent) => {
if (cbEvent.target.checked) {
settingsMap.setKey('togglePanelTrigger', 'click');
}
}}
value={togglePanelTrigger != 'hover'}
/>
<Checkbox
label="Hover"
onChange={(cbEvent) => {
if (cbEvent.target.checked) {
settingsMap.setKey('togglePanelTrigger', 'hover');
}
}}
value={togglePanelTrigger == 'hover'}
/> */}
</FormItem>
<FormItem label="More Settings">
<FormItem label="Code Settings">
<Checkbox
label="Enable bracket matching"
onChange={(cbEvent) => settingsMap.setKey('isBracketMatchingEnabled', cbEvent.target.checked)}
@@ -258,16 +230,6 @@ export function SettingsTab({ started }) {
disabled={shouldAlwaysSync}
value={isSyncEnabled}
/>
<Checkbox
label="Hide top buttons"
onChange={(cbEvent) => settingsMap.setKey('isButtonRowHidden', cbEvent.target.checked)}
value={isButtonRowHidden}
/>
<Checkbox
label="Disable CSS Animations"
onChange={(cbEvent) => settingsMap.setKey('isCSSAnimationDisabled', cbEvent.target.checked)}
value={isCSSAnimationDisabled}
/>
</FormItem>
<FormItem label="Zen Mode">Try clicking the logo in the top left!</FormItem>
<FormItem label="Reset Settings">
@@ -1,7 +1,7 @@
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 React, { useMemo, useRef } from 'react';
import { settingsMap, useSettings } from '../../../settings.mjs';
import { ButtonGroup } from './Forms.jsx';
import ImportSoundsButton from './ImportSoundsButton.jsx';
@@ -12,20 +12,15 @@ const getSamples = (samples) =>
export function SoundsTab() {
const sounds = useStore(soundMap);
const { soundsFilter } = useSettings();
const [search, setSearch] = useState('');
const soundEntries = useMemo(() => {
let filtered = Object.entries(sounds)
.filter(([key]) => !key.startsWith('_'))
.sort((a, b) => a[0].localeCompare(b[0]));
if (!sounds) {
return [];
}
let filtered = Object.entries(sounds)
.filter(([key]) => !key.startsWith('_'))
.sort((a, b) => a[0].localeCompare(b[0]))
.filter(([name]) => name.toLowerCase().includes(search.toLowerCase()));
if (soundsFilter === 'user') {
return filtered.filter(([_, { data }]) => !data.prebake);
return filtered.filter(([key, { data }]) => !data.prebake);
}
if (soundsFilter === 'drums') {
return filtered.filter(([_, { data }]) => data.type === 'sample' && data.tag === 'drum-machines');
@@ -37,11 +32,9 @@ export function SoundsTab() {
return filtered.filter(([_, { data }]) => ['synth', 'soundfont'].includes(data.type));
}
return filtered;
}, [sounds, soundsFilter, search]);
}, [sounds, soundsFilter]);
// holds mutable ref to current triggered sound
const trigRef = useRef();
// stop current sound on mouseup
useEvent('mouseup', () => {
const t = trigRef.current;
@@ -50,16 +43,8 @@ export function SoundsTab() {
ref?.stop(getAudioContext().currentTime + 0.01);
});
});
return (
<div id="sounds-tab" className="px-4 flex flex-col w-full h-full dark:text-white text-stone-900">
<input
className="w-full p-1 bg-background rounded-md my-2"
placeholder="Search"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
<div id="sounds-tab" className="px-4 flex flex-col w-full h-full dark:text-white text-stone-900">
<div className="pb-2 flex shrink-0 flex-wrap">
<ButtonGroup
value={soundsFilter}
@@ -73,8 +58,7 @@ export function SoundsTab() {
></ButtonGroup>
<ImportSoundsButton onComplete={() => settingsMap.setKey('soundsFilter', 'user')} />
</div>
<div className="min-h-0 max-h-full grow overflow-auto font-mono text-sm break-normal pb-2">
<div className="min-h-0 max-h-full grow overflow-auto font-mono text-sm break-normal">
{soundEntries.map(([name, { data, onTrigger }]) => {
return (
<span
@@ -5,8 +5,10 @@ const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL
export function WelcomeTab({ context }) {
return (
<div className="prose dark:prose-invert min-w-full pt-2 font-sans pb-8 px-4 ">
<h3> welcome</h3>
<div className="prose dark:prose-invert max-w-[600px] pt-2 font-sans pb-8 px-4">
<h3>
<span className={cx('animate-spin inline-block select-none')}>🌀</span> welcome
</h3>
<p>
You have found <span className="underline">strudel</span>, a new live coding platform to write dynamic music
pieces in the browser! It is free and open-source and made for beginners and experts alike. To get started:
@@ -28,7 +30,7 @@ export function WelcomeTab({ context }) {
</a>{' '}
to ask any questions, give feedback or just say hello.
</p>
<h3> about</h3>
<h3>about</h3>
<p>
strudel is a JavaScript version of{' '}
<a href="https://tidalcycles.org/" target="_blank">
+3 -3
View File
@@ -23,10 +23,10 @@ async function hasStrudelJson(subpath) {
async function loadStrudelJson(subpath) {
const contents = await readTextFile(subpath + '/strudel.json', { dir });
const sampleMap = JSON.parse(contents);
processSampleMap(sampleMap, (key, bank) => {
registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, bank, fileResolver(subpath)), {
processSampleMap(sampleMap, (key, value) => {
registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, value, fileResolver(subpath)), {
type: 'sample',
samples: bank,
samples: value,
fileSystem: true,
tag: 'local',
});
-1
View File
@@ -81,7 +81,6 @@ export function loadModules() {
import('@strudel/soundfonts'),
import('@strudel/csound'),
import('@strudel/tidal'),
import('@strudel/mqtt'),
];
if (isTauri()) {
modules = modules.concat([
-12
View File
@@ -30,16 +30,10 @@ export const defaultSettings = {
isZen: false,
soundsFilter: 'all',
patternFilter: 'community',
// panelPosition: window.innerWidth > 1000 ? 'right' : 'bottom', //FIX: does not work on astro
panelPosition: 'right',
isPanelPinned: false,
isPanelOpen: true,
togglePanelTrigger: 'click', //click | hover
userPatterns: '{}',
audioDeviceName: defaultAudioDeviceName,
audioEngineTarget: audioEngineTargets.webaudio,
isButtonRowHidden: false,
isCSSAnimationDisabled: false,
};
let search = null;
@@ -72,23 +66,17 @@ export function useSettings() {
isActiveLineHighlighted: parseBoolean(state.isActiveLineHighlighted),
isAutoCompletionEnabled: parseBoolean(state.isAutoCompletionEnabled),
isPatternHighlightingEnabled: parseBoolean(state.isPatternHighlightingEnabled),
isButtonRowHidden: parseBoolean(state.isButtonRowHidden),
isCSSAnimationDisabled: parseBoolean(state.isCSSAnimationDisabled),
isTooltipEnabled: parseBoolean(state.isTooltipEnabled),
isLineWrappingEnabled: parseBoolean(state.isLineWrappingEnabled),
isFlashEnabled: parseBoolean(state.isFlashEnabled),
isSyncEnabled: isUdels() ? true : parseBoolean(state.isSyncEnabled),
fontSize: Number(state.fontSize),
panelPosition: state.activeFooter !== '' && !isUdels() ? state.panelPosition : 'bottom', // <-- keep this 'bottom' where it is!
isPanelPinned: parseBoolean(state.isPanelPinned),
isPanelOpen: parseBoolean(state.isPanelOpen),
userPatterns: userPatterns,
};
}
export const setActiveFooter = (tab) => settingsMap.setKey('activeFooter', tab);
export const setPanelPinned = (bool) => settingsMap.setKey('isPanelPinned', bool);
export const setIsPanelOpened = (bool) => settingsMap.setKey('isPanelOpen', bool);
export const setIsZen = (active) => settingsMap.setKey('isZen', !!active);
-1
View File
@@ -52,7 +52,6 @@
}
:root {
--app-height: 100vh;
--app-width: 100vw;
}
#console-tab {
-1
View File
@@ -34,7 +34,6 @@ module.exports = {
},
spacing: {
'app-height': 'var(--app-height)',
'app-width': 'var(--app-width)',
},
typography(theme) {
return {