1/* 2pick.mjs - methods that use one pattern to pick events from other patterns. 3Copyright (C) 2024 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/core/signal.mjs> 4This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>. 5*/ 6 7import { Pattern, reify, silence, register } from './pattern.mjs'; 8 9import { _mod, clamp, objectMap } from './util.mjs'; 10 11const _pick = function (lookup, pat, modulo = true) { 12 const array = Array.isArray(lookup); 13 const len = Object.keys(lookup).length; 14 15 lookup = objectMap(lookup, reify); 16 17 if (len === 0) { 18 return silence; 19 } 20 return pat.fmap((i) => { 21 let key = i; 22 if (array) { 23 key = modulo ? Math.round(key) % len : clamp(Math.round(key), 0, lookup.length - 1); 24 } 25 return lookup[key]; 26 }); 27}; 28 29/** * Picks patterns (or plain values) either from a list (by index) or a lookup table (by name). 30 * Similar to `inhabit`, but maintains the structure of the original patterns. 31 * @tags combiners 32 * @param {Pattern} pat 33 * @param {*} xs 34 * @returns {Pattern} 35 * @example 36 * note("<0 1 2!2 3>".pick(["g a", "e f", "f g f g" , "g c d"])) 37 * @example 38 * sound("<0 1 [2,0]>".pick(["bd sd", "cp cp", "hh hh"])) 39 * @example 40 * sound("<0!2 [0,1] 1>".pick(["bd(3,8)", "sd sd"])) 41 * @example 42 * s("<a!2 [a,b] b>".pick({a: "bd(3,8)", b: "sd sd"})) 43 */ 44 45export const pick = function (lookup, pat) { 46 // backward compatibility - the args used to be flipped 47 if (Array.isArray(pat)) { 48 [pat, lookup] = [lookup, pat]; 49 } 50 return __pick(lookup, pat); 51}; 52 53const __pick = register('pick', function (lookup, pat) { 54 return _pick(lookup, pat, false).innerJoin(); 55}); 56 57/** * The same as `pick`, but if you pick a number greater than the size of the list, 58 * it wraps around, rather than sticking at the maximum value. 59 * For example, if you pick the fifth pattern of a list of three, you'll get the 60 * second one. 61 * @tags combiners 62 * @param {Pattern} pat 63 * @param {*} xs 64 * @returns {Pattern} 65 */ 66 67export const pickmod = register('pickmod', function (lookup, pat) { 68 return _pick(lookup, pat, true).innerJoin(); 69}); 70 71/** * pickF lets you use a pattern of numbers to pick which function to apply to another pattern. 72 * @tags combiners, functional 73 * @param {Pattern} pat 74 * @param {Pattern} lookup a pattern of indices or names 75 * @param {function[] | object} lookup the array or lookup object of functions from which to pull 76 * @returns {Pattern} 77 * @example 78 * s("bd [rim hh]").pickF("<0 1 2>", [rev,jux(rev),fast(2)]) 79 * @example 80 * note("<c2 d2>(3,8)").s("square") 81 * .pickF("<0 2> 1", [jux(rev), fast(2), x=>x.lpf(800)]) 82 * @example 83 * note("<c2 d2>(3,8)").s("square") 84 * .pickF("<jr l> f", { jr:jux(rev), f:fast(2), l:x=>x.lpf(800) }) 85 */ 86export const pickF = register('pickF', function (pickPattern, lookup, pat) { 87 return pat.apply(pick(lookup, pickPattern)); 88}); 89 90/** * The same as `pickF`, but if you pick a number greater than the size of the functions list, 91 * it wraps around, rather than sticking at the maximum value. 92 * @tags combiners 93 * @param {Pattern} pat 94 * @param {Pattern} lookup a pattern of indices or names 95 * @param {function[] | object} lookup the array or lookup object of functions from which to pull 96 * @returns {Pattern} 97 */ 98export const pickmodF = register('pickmodF', function (pickPattern, lookup, pat) { 99 return pat.apply(pickmod(lookup, pickPattern)); 100}); 101 102/** * Similar to `pick`, but it applies an outerJoin instead of an innerJoin. 103 * @tags combiners 104 * @param {Pattern} pat 105 * @param {*} xs 106 * @returns {Pattern} 107 */ 108export const pickOut = register('pickOut', function (lookup, pat) { 109 return _pick(lookup, pat, false).outerJoin(); 110}); 111 112/** * The same as `pickOut`, but if you pick a number greater than the size of the list, 113 * it wraps around, rather than sticking at the maximum value. 114 * @tags combiners 115 * @param {Pattern} pat 116 * @param {*} xs 117 * @returns {Pattern} 118 */ 119export const pickmodOut = register('pickmodOut', function (lookup, pat) { 120 return _pick(lookup, pat, true).outerJoin(); 121}); 122 123/** * Similar to `pick`, but the choosen pattern is restarted when its index is triggered. 124 * @tags combiners 125 * @param {Pattern} pat 126 * @param {*} xs 127 * @returns {Pattern} 128 */ 129export const pickRestart = register('pickRestart', function (lookup, pat) { 130 return _pick(lookup, pat, false).restartJoin(); 131}); 132 133/** * The same as `pickRestart`, but if you pick a number greater than the size of the list, 134 * it wraps around, rather than sticking at the maximum value. 135 * @tags combiners 136 * @param {Pattern} pat 137 * @param {*} xs 138 * @returns {Pattern} 139 * @example 140 * "<a@2 b@2 c@2 d@2>".pickRestart({ 141 a: n("0 1 2 0"), 142 b: n("2 3 4 ~"), 143 c: n("[4 5] [4 3] 2 0"), 144 d: n("0 -3 0 ~") 145 }).scale("C:major").s("piano") 146 */ 147export const pickmodRestart = register('pickmodRestart', function (lookup, pat) { 148 return _pick(lookup, pat, true).restartJoin(); 149}); 150 151/** * Similar to `pick`, but the choosen pattern is reset when its index is triggered. 152 * @tags combiners 153 * @param {Pattern} pat 154 * @param {*} xs 155 * @returns {Pattern} 156 */ 157export const pickReset = register('pickReset', function (lookup, pat) { 158 return _pick(lookup, pat, false).resetJoin(); 159}); 160 161/** * The same as `pickReset`, but if you pick a number greater than the size of the list, 162 * it wraps around, rather than sticking at the maximum value. 163 * @tags combiners 164 * @param {Pattern} pat 165 * @param {*} xs 166 * @returns {Pattern} 167 */ 168export const pickmodReset = register('pickmodReset', function (lookup, pat) { 169 return _pick(lookup, pat, true).resetJoin(); 170}); 171 172/** Picks patterns (or plain values) either from a list (by index) or a lookup table (by name). 173 * Similar to `pick`, but cycles are squeezed into the target ('inhabited') pattern. 174 * @name inhabit 175 * @tags combiners 176 * @synonyms pickSqueeze 177 * @param {Pattern} pat 178 * @param {*} xs 179 * @returns {Pattern} 180 * @example 181 * let a = s("bd(3,8)") 182 * let b = s("cp sd") 183 * "<a b [a,b]>".inhabit({ a, b }) 184 * @example 185 * s("a@2 [a b] a" 186 * .inhabit({a: "bd(3,8)", b: "sd sd"})) 187 * .slow(4) 188 */ 189export const { inhabit, pickSqueeze } = register(['inhabit', 'pickSqueeze'], function (lookup, pat) { 190 return _pick(lookup, pat, false).squeezeJoin(); 191}); 192 193/** * The same as `inhabit`, but if you pick a number greater than the size of the list, 194 * it wraps around, rather than sticking at the maximum value. 195 * For example, if you pick the fifth pattern of a list of three, you'll get the 196 * second one. 197 * @name inhabitmod 198 * @synonyms pickmodSqueeze 199 * @tags combiners 200 * @param {Pattern} pat 201 * @param {*} xs 202 * @returns {Pattern} 203 */ 204 205export const { inhabitmod, pickmodSqueeze } = register(['inhabitmod', 'pickmodSqueeze'], function (lookup, pat) { 206 return _pick(lookup, pat, true).squeezeJoin(); 207}); 208 209/** 210 * Pick from the list of values (or patterns of values) via the index using the given 211 * pattern of integers. The selected pattern will be compressed to fit the duration of the selecting event 212 * @tags combiners 213 * @param {Pattern} pat 214 * @param {*} xs 215 * @returns {Pattern} 216 * @example 217 * note(squeeze("<0@2 [1!2] 2>", ["g a", "f g f g" , "g a c d"])) 218 */ 219 220export const squeeze = (pat, xs) => { 221 xs = xs.map(reify); 222 if (xs.length == 0) { 223 return silence; 224 } 225 return pat 226 .fmap((i) => { 227 const key = _mod(Math.round(i), xs.length); 228 return xs[key]; 229 }) 230 .squeezeJoin(); 231};