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..f3f19f87 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -2593,7 +2593,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 +2801,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 +2882,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 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 a3824f12..ca20a8a2 100644 --- a/packages/core/signal.mjs +++ b/packages/core/signal.mjs @@ -1,6 +1,6 @@ /* -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 . */ @@ -8,7 +8,7 @@ import { Hap } from './hap.mjs'; import { Pattern, fastcat, reify, silence, stack, register } from './pattern.mjs'; import Fraction from './fraction.mjs'; -import { id, _mod, clamp, objectMap, getCurrentKeyboardState } from './util.mjs'; +import { id, getCurrentKeyboardState } from './util.mjs'; export function steady(value) { // A continuous value @@ -254,211 +254,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) { @@ -597,8 +392,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, ); /** @@ -615,9 +413,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, +); /** * @@ -631,7 +434,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. @@ -651,12 +454,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)` @@ -673,7 +481,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); /** * 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/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 3ad6ea01..7c21d464 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -7508,6 +7508,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 ]", 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/components/Header/Header.astro b/website/src/components/Header/Header.astro index c3b1b9c3..e9626677 100644 --- a/website/src/components/Header/Header.astro +++ b/website/src/components/Header/Header.astro @@ -28,11 +28,10 @@ const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL
diff --git a/website/src/pages/learn/input-output.mdx b/website/src/pages/learn/input-output.mdx index 93cbfdc5..14e46ba7 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?) @@ -45,20 +46,22 @@ But you can also control cc messages separately like this: $: ccv(sine.segment(16).slow(4)).ccn(74).midi()`} /> -# OSC/SuperDirt API +# OSC/SuperDirt/StrudelDirt -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. +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) ## 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! @@ -86,3 +89,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/Header.jsx b/website/src/repl/components/Header.jsx index 2789e4a1..2e8b3094 100644 --- a/website/src/repl/components/Header.jsx +++ b/website/src/repl/components/Header.jsx @@ -1,8 +1,4 @@ -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'; @@ -21,7 +17,7 @@ export function Header({ context, embedded = false }) {
{!isZen && ( -
+
strudel - REPL - {!isEmbedded && ( - + REPL + {!isEmbedded && isButtonRowHidden && ( + DOCS )} @@ -66,7 +62,7 @@ export function Header({ context, embedded = false }) {
{!isZen && !isButtonRowHidden && ( -
+
{!isEmbedded && ( @@ -104,7 +98,6 @@ export function Header({ context, embedded = false }) { className="hover:opacity-50 p-2 flex items-center space-x-1" onClick={handleShuffle} > - shuffle )} @@ -117,7 +110,6 @@ export function Header({ context, embedded = false }) { )} onClick={handleShare} > - share )} @@ -127,7 +119,6 @@ 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')} > - learn )} diff --git a/website/src/repl/components/panel/ConsoleTab.jsx b/website/src/repl/components/panel/ConsoleTab.jsx index f0fca584..b02227b9 100644 --- a/website/src/repl/components/panel/ConsoleTab.jsx +++ b/website/src/repl/components/panel/ConsoleTab.jsx @@ -1,18 +1,48 @@ +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({ log }) { +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); + }); + }, []), + ); return ( -
- +
{log.map((l, i) => { const message = linkify(l.message); + const color = l.data?.hap?.value?.color; return ( -
+
{l.count ? ` (${l.count})` : ''}
@@ -42,3 +72,7 @@ function linkify(inputText) { return replacedText; } + +function useLogger(onTrigger) { + useEvent(logger.key, onTrigger); +} diff --git a/website/src/repl/components/panel/Panel.jsx b/website/src/repl/components/panel/Panel.jsx index 57cd26c0..81fcacf7 100644 --- a/website/src/repl/components/panel/Panel.jsx +++ b/website/src/repl/components/panel/Panel.jsx @@ -1,8 +1,4 @@ -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 { setPanelPinned, setActiveFooter as setTab, setIsPanelOpened, useSettings } from '../../../settings.mjs'; import { ConsoleTab } from './ConsoleTab'; import { FilesTab } from './FilesTab'; @@ -119,33 +115,11 @@ function PanelNav({ children, className, settings, ...props }) { } function PanelContent({ context, tab }) { - const [log, setLog] = useState([]); - 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); - }); - }, []), - ); - switch (tab) { case tabNames.patterns: return ; case tabNames.console: - return ; + return ; case tabNames.sounds: return ; case tabNames.reference: @@ -236,7 +210,3 @@ function CloseButton({ onClick }) { ); } - -function useLogger(onTrigger) { - useEvent(logger.key, onTrigger); -} 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/components/panel/WelcomeTab.jsx b/website/src/repl/components/panel/WelcomeTab.jsx index b7d45bfd..3e77e71b 100644 --- a/website/src/repl/components/panel/WelcomeTab.jsx +++ b/website/src/repl/components/panel/WelcomeTab.jsx @@ -6,9 +6,7 @@ const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL export function WelcomeTab({ context }) { return (
-

- 🌀 welcome -

+

꩜ welcome

You have found strudel, 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: @@ -30,7 +28,7 @@ export function WelcomeTab({ context }) { {' '} to ask any questions, give feedback or just say hello.

-

about

+

꩜ about

strudel is a JavaScript version of{' '} 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([