diff --git a/README.md b/README.md index 7743bcab1..475497a32 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 180ff103e..5886c4769 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/core/index.mjs b/packages/core/index.mjs index 506fa77a5..a10b68b09 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 fec268042..f3f19f874 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 000000000..334551e15 --- /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/signal.mjs b/packages/core/signal.mjs index c0637383d..215eac2f4 100644 --- a/packages/core/signal.mjs +++ b/packages/core/signal.mjs @@ -1,13 +1,13 @@ /* -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 Fraction from './fraction.mjs'; -import { id, _mod, clamp, objectMap } from './util.mjs'; +import { id, _mod } from './util.mjs'; export function steady(value) { // A continuous value @@ -253,211 +253,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 +391,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 +412,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 +433,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 +453,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 +480,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/repl/repl-component.mjs b/packages/repl/repl-component.mjs index e6e0ee0ec..362084132 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);