diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index adb883e5..7d8170cf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -138,6 +138,18 @@ 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! diff --git a/README.md b/README.md index 7743bcab..475497a3 100644 --- a/README.md +++ b/README.md @@ -14,10 +14,16 @@ An experiment in making a [Tidal](https://github.com/tidalcycles/tidal/) using w After cloning the project, you can run the REPL locally: -```bash -pnpm i -pnpm dev -``` +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 + ``` ## Using Strudel In Your Project diff --git a/packages/codemirror/codemirror.mjs b/packages/codemirror/codemirror.mjs index 180ff103..5886c476 100644 --- a/packages/codemirror/codemirror.mjs +++ b/packages/codemirror/codemirror.mjs @@ -133,6 +133,7 @@ export class StrudelMirror { autodraw, prebake, bgFill = true, + solo = true, ...replOptions } = options; this.code = initialCode; @@ -143,6 +144,7 @@ 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)); @@ -159,12 +161,14 @@ export class StrudelMirror { replOptions?.onToggle?.(started); if (started) { this.drawer.start(this.repl.scheduler); - // stop other repls when this one is started - document.dispatchEvent( - new CustomEvent('start-repl', { - detail: this.id, - }), - ); + if (this.solo) { + // stop other repls when this one is started + document.dispatchEvent( + new CustomEvent('start-repl', { + detail: this.id, + }), + ); + } } else { this.drawer.stop(); updateMiniLocations(this.editor, []); @@ -219,7 +223,7 @@ export class StrudelMirror { // stop this repl when another repl is started this.onStartRepl = (e) => { - if (e.detail !== this.id) { + if (this.solo && e.detail !== this.id) { this.stop(); } }; diff --git a/packages/codemirror/widget.mjs b/packages/codemirror/widget.mjs index 72b4ec65..42d3b151 100644 --- a/packages/codemirror/widget.mjs +++ b/packages/codemirror/widget.mjs @@ -133,3 +133,10 @@ 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 }); +}); diff --git a/packages/core/cyclist.mjs b/packages/core/cyclist.mjs index 472e83a8..bd2db122 100644 --- a/packages/core/cyclist.mjs +++ b/packages/core/cyclist.mjs @@ -68,6 +68,10 @@ 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) { diff --git a/packages/core/index.mjs b/packages/core/index.mjs index 506fa77a..a10b68b0 100644 --- a/packages/core/index.mjs +++ b/packages/core/index.mjs @@ -14,6 +14,7 @@ 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'; diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index fec26804..34a63c93 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -1023,6 +1023,7 @@ 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)], @@ -1491,6 +1492,14 @@ 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)); @@ -2539,6 +2548,10 @@ 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( @@ -2593,7 +2606,7 @@ export const steps = register('steps', function (targetTactus, pat) { // avoid divide by zero.. return nothing; } - return pat.fast(Fraction(targetTactus).div(pat.tactus)); + return pat._fast(Fraction(targetTactus).div(pat.tactus)).setTactus(targetTactus); }); export function _polymeterListSteps(steps, ...args) { @@ -2801,7 +2814,7 @@ export const s_sub = stepRegister('s_sub', function (i, pat) { return pat.s_add(pat.tactus.sub(i)); }); -export const s_cycles = stepRegister('s_extend', function (factor, pat) { +export const s_extend = stepRegister('s_extend', function (factor, pat) { return pat.fast(factor).s_expand(factor); }); @@ -2882,6 +2895,13 @@ 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 @@ -3093,3 +3113,25 @@ 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()), +); diff --git a/packages/core/pick.mjs b/packages/core/pick.mjs new file mode 100644 index 00000000..334551e1 --- /dev/null +++ b/packages/core/pick.mjs @@ -0,0 +1,214 @@ +/* +pick.mjs - methods that use one pattern to pick events from other patterns. +Copyright (C) 2024 Strudel contributors - see +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 . +*/ + +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("".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("(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 + * "".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 + * "".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(); +}; diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index 016eca3e..e703909f 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -63,11 +63,13 @@ export function repl({ let pPatterns = {}; let anonymousIndex = 0; let allTransform; + let eachTransform; const hush = function () { pPatterns = {}; anonymousIndex = 0; allTransform = undefined; + eachTransform = undefined; return silence; }; @@ -84,15 +86,42 @@ 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 (id.startsWith('_') || id.endsWith('_')) { + if (typeof id === 'string' && (id.startsWith('_') || id.endsWith('_'))) { // allows muting a pattern x with x_ or _x return silence; } @@ -131,6 +160,7 @@ export function repl({ }); return evalScope({ all, + each, hush, cpm, setCps, @@ -152,7 +182,14 @@ export function repl({ shouldHush && hush(); let { pattern, meta } = await _evaluate(code, transpiler, transpilerOptions); if (Object.keys(pPatterns).length) { - pattern = stack(...Object.values(pPatterns)); + 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); } if (allTransform) { pattern = allTransform(pattern); diff --git a/packages/core/signal.mjs b/packages/core/signal.mjs index c0637383..51aac609 100644 --- a/packages/core/signal.mjs +++ b/packages/core/signal.mjs @@ -1,13 +1,14 @@ /* -signal.mjs - -Copyright (C) 2022 Strudel contributors - see +signal.mjs - continuous patterns +Copyright (C) 2024 Strudel contributors - see 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 . */ import { Hap } from './hap.mjs'; -import { Pattern, fastcat, reify, silence, stack, register } from './pattern.mjs'; +import { Pattern, fastcat, pure, register, reify, silence, stack } from './pattern.mjs'; import Fraction from './fraction.mjs'; -import { id, _mod, clamp, objectMap } from './util.mjs'; + +import { id, keyAlias, getCurrentKeyboardState } from './util.mjs'; export function steady(value) { // A continuous value @@ -159,6 +160,37 @@ 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,... @@ -253,211 +285,6 @@ 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("".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("(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 - * "".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 - * "".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) { @@ -596,8 +423,11 @@ 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)), +export const degradeByWith = register( + 'degradeByWith', + (withPat, x, pat) => pat.fmap((a) => (_) => a).appLeft(withPat.filterValues((v) => v > x)), + true, + true, ); /** @@ -614,9 +444,14 @@ export const degradeByWith = register('degradeByWith', (withPat, x, pat) => * @example * s("[hh?0.2]*8") */ -export const degradeBy = register('degradeBy', function (x, pat) { - return pat._degradeByWith(rand, x); -}); +export const degradeBy = register( + 'degradeBy', + function (x, pat) { + return pat._degradeByWith(rand, x); + }, + true, + true, +); /** * @@ -630,7 +465,7 @@ export const degradeBy = register('degradeBy', function (x, pat) { * @example * s("[hh?]*8") */ -export const degrade = register('degrade', (pat) => pat._degradeBy(0.5)); +export const degrade = register('degrade', (pat) => pat._degradeBy(0.5), true, true); /** * Inverse of `degradeBy`: Randomly removes events from the pattern by a given amount. @@ -650,12 +485,17 @@ export const degrade = register('degrade', (pat) => pat._degradeBy(0.5)); * x => x.undegradeBy(0.8).pan(1) * ) */ -export const undegradeBy = register('undegradeBy', function (x, pat) { - return pat._degradeByWith( - rand.fmap((r) => 1 - r), - x, - ); -}); +export const undegradeBy = register( + 'undegradeBy', + function (x, pat) { + return pat._degradeByWith( + rand.fmap((r) => 1 - r), + x, + ); + }, + true, + true, +); /** * Inverse of `degrade`: Randomly removes 50% of events from the pattern. Shorthand for `.undegradeBy(0.5)` @@ -672,7 +512,7 @@ export const undegradeBy = register('undegradeBy', function (x, pat) { * x => x.undegrade().pan(1) * ) */ -export const undegrade = register('undegrade', (pat) => pat._undegradeBy(0.5)); +export const undegrade = register('undegrade', (pat) => pat._undegradeBy(0.5), true, true); /** * @@ -690,7 +530,7 @@ export const undegrade = register('undegrade', (pat) => pat._undegradeBy(0.5)); 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(); }); @@ -831,3 +671,48 @@ export const never = register('never', function (_, pat) { export const always = register('always', function (func, pat) { return func(pat); }); + +//keyname: string | Array +//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); +}); diff --git a/packages/core/test/pattern.test.mjs b/packages/core/test/pattern.test.mjs index 31ec4868..2d29ea5d 100644 --- a/packages/core/test/pattern.test.mjs +++ b/packages/core/test/pattern.test.mjs @@ -46,12 +46,14 @@ import { rev, time, run, + binaryN, pick, stackLeft, stackRight, stackCentre, s_cat, calculateTactus, + sometimes, } from '../index.mjs'; import { steady } from '../signal.mjs'; @@ -958,6 +960,18 @@ 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( @@ -1253,4 +1267,13 @@ 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); + }); + }); }); diff --git a/packages/core/util.mjs b/packages/core/util.mjs index bc844057..b7b1e841 100644 --- a/packages/core/util.mjs +++ b/packages/core/util.mjs @@ -434,6 +434,38 @@ 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) { diff --git a/packages/draw/draw.mjs b/packages/draw/draw.mjs index e3737600..0576c297 100644 --- a/packages/draw/draw.mjs +++ b/packages/draw/draw.mjs @@ -26,7 +26,7 @@ export const getDrawContext = (id = 'test-canvas', options) => { }, 200); }); } - return canvas.getContext(contextType); + return canvas.getContext(contextType, { willReadFrequently: true }); }; let animationFrames = {}; diff --git a/packages/mqtt/README.md b/packages/mqtt/README.md new file mode 100644 index 00000000..7952b553 --- /dev/null +++ b/packages/mqtt/README.md @@ -0,0 +1,3 @@ +# @strudel/serial + +This package adds webserial functionality to strudel Patterns, for e.g. sending messages to arduino microcontrollers. diff --git a/packages/mqtt/mqtt.mjs b/packages/mqtt/mqtt.mjs new file mode 100644 index 00000000..75f7904e --- /dev/null +++ b/packages/mqtt/mqtt.mjs @@ -0,0 +1,87 @@ +/* +mqtt.mjs - for patterning the internet of things from strudel +Copyright (C) 2022 Strudel contributors - see +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 . +*/ + +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 }); + }); +}; diff --git a/packages/mqtt/package.json b/packages/mqtt/package.json new file mode 100644 index 00000000..b6be50f5 --- /dev/null +++ b/packages/mqtt/package.json @@ -0,0 +1,38 @@ +{ + "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 ", + "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" + } +} diff --git a/packages/mqtt/vite.config.js b/packages/mqtt/vite.config.js new file mode 100644 index 00000000..b45b7ca6 --- /dev/null +++ b/packages/mqtt/vite.config.js @@ -0,0 +1,19 @@ +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', + }, +}); diff --git a/packages/repl/repl-component.mjs b/packages/repl/repl-component.mjs index e6e0ee0e..36208413 100644 --- a/packages/repl/repl-component.mjs +++ b/packages/repl/repl-component.mjs @@ -10,6 +10,8 @@ if (typeof HTMLElement !== 'undefined') { static observedAttributes = ['code']; settings = codemirrorSettings.get(); editor = null; + sync = false; + solo = true; constructor() { super(); } @@ -49,6 +51,8 @@ if (typeof HTMLElement !== 'undefined') { }); this.dispatchEvent(event); }, + solo: this.solo, + sync: this.sync, }); // init settings this.editor.updateSettings(this.settings); diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 7d57b5bb..b61f4263 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -270,11 +270,12 @@ function getReverb(orbit, duration, fade, lp, dim, ir) { export let analysers = {}, analysersData = {}; -export function getAnalyserById(id, fftSize = 1024) { +export function getAnalyserById(id, fftSize = 1024, smoothingTimeConstant = 0.5) { 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); diff --git a/packages/webaudio/index.mjs b/packages/webaudio/index.mjs index a425e683..59672b61 100644 --- a/packages/webaudio/index.mjs +++ b/packages/webaudio/index.mjs @@ -6,4 +6,5 @@ This program is free software: you can redistribute it and/or modify it under th export * from './webaudio.mjs'; export * from './scope.mjs'; +export * from './spectrum.mjs'; export * from 'superdough'; diff --git a/packages/webaudio/spectrum.mjs b/packages/webaudio/spectrum.mjs new file mode 100644 index 00000000..2ddd214f --- /dev/null +++ b/packages/webaudio/spectrum.mjs @@ -0,0 +1,69 @@ +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)); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a18f06e7..6cb259a9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -331,6 +331,19 @@ importers: specifier: ^2.1.3 version: 2.1.3(@types/node@22.7.6)(@vitest/ui@2.1.3)(terser@5.36.0) + packages/mqtt: + dependencies: + '@strudel/core': + specifier: workspace:* + version: link:../core + paho-mqtt: + specifier: ^1.1.0 + version: 1.1.0 + devDependencies: + vite: + specifier: ^5.0.10 + version: 5.4.9(@types/node@22.7.6)(terser@5.36.0) + packages/osc: dependencies: '@strudel/core': @@ -623,6 +636,9 @@ importers: '@strudel/mini': specifier: workspace:* version: link:../packages/mini + '@strudel/mqtt': + specifier: workspace:* + version: link:../packages/mqtt '@strudel/osc': specifier: workspace:* version: link:../packages/osc @@ -5991,6 +6007,9 @@ packages: engines: {node: ^16.14.0 || >=18.0.0} hasBin: true + paho-mqtt@1.1.0: + resolution: {integrity: sha512-KPbL9KAB0ASvhSDbOrZBaccXS+/s7/LIofbPyERww8hM5Ko71GUJQ6Nmg0BWqj8phAIT8zdf/Sd/RftHU9i2HA==} + parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -7740,6 +7759,7 @@ packages: workbox-google-analytics@7.0.0: resolution: {integrity: sha512-MEYM1JTn/qiC3DbpvP2BVhyIH+dV/5BjHk756u9VbwuAhu0QHyKscTnisQuz21lfRpOwiS9z4XdqeVAKol0bzg==} + deprecated: It is not compatible with newer versions of GA starting with v4, as long as you are using GAv3 it should be ok, but the package is not longer being maintained workbox-navigation-preload@7.0.0: resolution: {integrity: sha512-juWCSrxo/fiMz3RsvDspeSLGmbgC0U9tKqcUPZBCf35s64wlaLXyn2KdHHXVQrb2cqF7I0Hc9siQalainmnXJA==} @@ -14686,6 +14706,8 @@ snapshots: - bluebird - supports-color + paho-mqtt@1.1.0: {} + parent-module@1.0.1: dependencies: callsites: 3.1.0 diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 3ad6ea01..c071b536 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -954,6 +954,24 @@ 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 ]", @@ -967,6 +985,60 @@ 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 ]", @@ -4065,6 +4137,8 @@ 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 ]", @@ -7508,6 +7582,75 @@ 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 ]", @@ -8763,6 +8906,8 @@ 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 ]", diff --git a/test/__snapshots__/tunes.test.mjs.snap b/test/__snapshots__/tunes.test.mjs.snap index f851077a..840f9f32 100644 --- a/test/__snapshots__/tunes.test.mjs.snap +++ b/test/__snapshots__/tunes.test.mjs.snap @@ -8,6 +8,7 @@ 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 ]", @@ -17,22 +18,20 @@ 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 ]", @@ -6906,10 +6905,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 ]", @@ -6917,7 +6916,6 @@ 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 ]", ] `; diff --git a/test/runtime.mjs b/test/runtime.mjs index a8f37a34..18d4a29e 100644 --- a/test/runtime.mjs +++ b/test/runtime.mjs @@ -134,6 +134,9 @@ 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; }; diff --git a/website/package.json b/website/package.json index 942fee9c..5b6c462a 100644 --- a/website/package.json +++ b/website/package.json @@ -33,6 +33,7 @@ "@strudel/hydra": "workspace:*", "@strudel/midi": "workspace:*", "@strudel/mini": "workspace:*", + "@strudel/mqtt": "workspace:*", "@strudel/osc": "workspace:*", "@strudel/serial": "workspace:*", "@strudel/soundfonts": "workspace:*", diff --git a/website/src/pages/functions/value-modifiers.mdx b/website/src/pages/functions/value-modifiers.mdx index 94372e75..19843e10 100644 --- a/website/src/pages/functions/value-modifiers.mdx +++ b/website/src/pages/functions/value-modifiers.mdx @@ -82,18 +82,7 @@ To modify a parameter value, you can either: ")).log()`} /> -- Modify _all_ numeral params: - - ").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: - -").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. +Remember the execution of the chained functions goes from left to right. # Operators diff --git a/website/src/pages/learn/factories.mdx b/website/src/pages/learn/factories.mdx index 6a2e165b..b43d3abf 100644 --- a/website/src/pages/learn/factories.mdx +++ b/website/src/pages/learn/factories.mdx @@ -55,6 +55,14 @@ These are the equivalents used by the Mini Notation: ## run - + + +## binary + + + +## binaryN + + After Pattern Constructors, let's see what [Time Modifiers](/learn/time-modifiers) are available. diff --git a/website/src/pages/learn/input-output.mdx b/website/src/pages/learn/input-output.mdx index 17435a81..3871216c 100644 --- a/website/src/pages/learn/input-output.mdx +++ b/website/src/pages/learn/input-output.mdx @@ -1,19 +1,20 @@ --- -title: MIDI & OSC +title: MIDI, OSC & MQTT layout: ../../layouts/MainLayout.astro --- import { MiniRepl } from '../../docs/MiniRepl'; import { JsDoc } from '../../docs/JsDoc'; -# MIDI and OSC +# MIDI, OSC and MQTT -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. +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). -# MIDI API +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. -Strudel also supports midi via [webmidi](https://npmjs.com/package/webmidi). +# MIDI + +Strudel supports MIDI without any additional software (thanks to [webmidi](https://npmjs.com/package/webmidi)), just by adding methods to your pattern: ## midi(outputName?) @@ -84,13 +85,14 @@ There is also [StrudelDirt](https://github.com/daslyfe/StrudelDirt) which is Sup ## Prequisites -Getting [SuperDirt](https://github.com/musikinformatik/SuperDirt/) to work with Strudel, you need to +To get 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 [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 +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 Now you're all set! @@ -118,3 +120,67 @@ Please refer to [Tidal Docs](https://tidalcycles.org/) for more info.
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: + + + +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: + + + +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. diff --git a/website/src/pages/learn/visual-feedback.mdx b/website/src/pages/learn/visual-feedback.mdx index ec275c6e..35202ca6 100644 --- a/website/src/pages/learn/visual-feedback.mdx +++ b/website/src/pages/learn/visual-feedback.mdx @@ -99,6 +99,10 @@ What follows is the API doc of all the options you can pass: +## Spectrum + + + ## markcss diff --git a/website/src/repl/components/panel/Reference.jsx b/website/src/repl/components/panel/Reference.jsx index 07820807..fbbf0a08 100644 --- a/website/src/repl/components/panel/Reference.jsx +++ b/website/src/repl/components/panel/Reference.jsx @@ -58,8 +58,8 @@ export function Reference() {

API Reference

- 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! + 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!

{visibleFunctions.map((entry, i) => (
diff --git a/website/src/repl/util.mjs b/website/src/repl/util.mjs index 905e16b0..d7890dde 100644 --- a/website/src/repl/util.mjs +++ b/website/src/repl/util.mjs @@ -81,6 +81,7 @@ export function loadModules() { import('@strudel/soundfonts'), import('@strudel/csound'), import('@strudel/tidal'), + import('@strudel/mqtt'), ]; if (isTauri()) { modules = modules.concat([