pick.mjsannotatedpick.mjssource231 lines · 7.8 KB · raw
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};