pattern.mjs - Core pattern representation for strudel Copyright (C) 2025 Strudel contributors - see https://codeberg.org/uzu/strudel/src/branch/main/packages/core/pattern.mjs 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 https://www.gnu.org/licenses/.
13import { 14 uniqsortr, 15 removeUndefineds, 16 flatten, 17 id, 18 listRange, 19 curry, 20 _mod, 21 numeralArgs, 22 parseNumeral, 23 pairs, 24 zipWith, 25 stringifyValues, 26} from './util.mjs'; 27import drawLine from './drawLine.mjs'; 28import { errorLogger, logger } from './logger.mjs'; 29import { strudelScope } from './evaluate.mjs'; 30 31let stringParser; 32 33let __steps = true; 34 35export const calculateSteps = function (x) { 36 __steps = x ? true : false; 37};
parser is expected to turn a string into a pattern if set, the reify function will parse all strings with it intended to use with mini to automatically interpret all strings as mini notation
42export const setStringParser = (parser) => (stringParser = parser);
@class Class representing a pattern.
45export class Pattern { 46 /** 47 * Create a pattern. As an end user, you will most likely not create a Pattern directly. 48 * 49 * @param {function} query - The function that maps a `State` to an array of `Hap`. 50 * @noAutocomplete 51 */ 52 constructor(query, steps = undefined) { 53 this.query = query; 54 this._Pattern = true; // this property is used to detectinstance of another Pattern 55 this._steps = steps; // in terms of number of steps per cycle 56 } 57 58 get _steps() { 59 return this.__steps; 60 } 61 62 set _steps(steps) { 63 this.__steps = steps === undefined ? undefined : Fraction(steps); 64 } 65 66 setSteps(steps) { 67 this._steps = steps; 68 return this; 69 } 70 71 withSteps(f) { 72 if (!__steps) { 73 return this; 74 } 75 return new Pattern(this.query, this._steps === undefined ? undefined : f(this._steps)); 76 } 77 78 get hasSteps() { 79 return this._steps !== undefined; 80 }
//////////////////////////////////////////////////////////////////// Haskell-style functor, applicative and monadic operations
Returns a new pattern, with the function applied to the value of
each hap. It has the alias fmap.
@tags functional
@synonyms fmap
@param {Function} func to to apply to the value
@returns Pattern
@example
"0 1 2".withValue(v => v + 10).log()
runs func on query state
Assumes 'this' is a pattern of functions, and given a function to resolve wholes, applies a given pattern of values to that pattern of functions. @tags functional @param {Function} whole_func @param {Function} func @noAutocomplete @returns Pattern
124 appWhole(whole_func, pat_val) { 125 const pat_func = this; 126 const query = function (state) { 127 const hap_funcs = pat_func.query(state); 128 const hap_vals = pat_val.query(state); 129 const apply = function (hap_func, hap_val) { 130 const s = hap_func.part.intersection(hap_val.part); 131 if (s == undefined) { 132 return undefined; 133 } 134 return new Hap( 135 whole_func(hap_func.whole, hap_val.whole), 136 s, 137 hap_func.value(hap_val.value), 138 hap_val.combineContext(hap_func), 139 ); 140 }; 141 return flatten( 142 hap_funcs.map((hap_func) => removeUndefineds(hap_vals.map((hap_val) => apply(hap_func, hap_val)))), 143 ); 144 }; 145 return new Pattern(query); 146 }
When this method is called on a pattern of functions, it matches its haps with those in the given pattern of values. A new pattern is returned, with each matching value applied to the corresponding function.
In this _appBoth variant, where timespans of the function and value haps
are not the same but do intersect, the resulting hap has a timespan of the
intersection. This applies to both the part and the whole timespan.
@tags functional
@param {Pattern} pat_val
@noAutocomplete
@returns Pattern
Tidal's <*>
165 const whole_func = function (span_a, span_b) { 166 if (span_a == undefined || span_b == undefined) { 167 return undefined; 168 } 169 return span_a.intersection_e(span_b); 170 }; 171 const result = pat_func.appWhole(whole_func, pat_val); 172 if (__steps) { 173 result._steps = lcm(pat_val._steps, pat_func._steps); 174 } 175 return result; 176 }
As with appBoth, but the whole timespan is not the intersection,
but the timespan from the function of patterns that this method is called
on. In practice, this means that the pattern structure, including onsets,
are preserved from the pattern of functions (often referred to as the left
hand or inner pattern).
@tags functional
@param {Pattern} pat_val
@noAutocomplete
@returns Pattern
189 appLeft(pat_val) { 190 const pat_func = this; 191 192 const query = function (state) { 193 const haps = []; 194 for (const hap_func of pat_func.query(state)) { 195 const hap_vals = pat_val.query(state.setSpan(hap_func.wholeOrPart())); 196 for (const hap_val of hap_vals) { 197 const new_whole = hap_func.whole; 198 const new_part = hap_func.part.intersection(hap_val.part); 199 if (new_part) { 200 const new_value = hap_func.value(hap_val.value); 201 const new_context = hap_val.combineContext(hap_func); 202 const hap = new Hap(new_whole, new_part, new_value, new_context); 203 haps.push(hap); 204 } 205 } 206 } 207 return haps; 208 }; 209 const result = new Pattern(query); 210 result._steps = this._steps; 211 return result; 212 }
As with appLeft, but whole timespans are instead taken from the
pattern of values, i.e. structure is preserved from the right hand/outer
pattern.
@tags functional
@param {Pattern} pat_val
@noAutocomplete
@returns Pattern
223 appRight(pat_val) { 224 const pat_func = this; 225 226 const query = function (state) { 227 const haps = []; 228 for (const hap_val of pat_val.query(state)) { 229 const hap_funcs = pat_func.query(state.setSpan(hap_val.wholeOrPart())); 230 for (const hap_func of hap_funcs) { 231 const new_whole = hap_val.whole; 232 const new_part = hap_func.part.intersection(hap_val.part); 233 if (new_part) { 234 const new_value = hap_func.value(hap_val.value); 235 const new_context = hap_val.combineContext(hap_func); 236 const hap = new Hap(new_whole, new_part, new_value, new_context); 237 haps.push(hap); 238 } 239 } 240 } 241 return haps; 242 }; 243 const result = new Pattern(query); 244 result._steps = pat_val._steps; 245 return result; 246 }
248 bindWhole(choose_whole, func) { 249 const pat_val = this; 250 const query = function (state) { 251 const withWhole = function (a, b) { 252 return new Hap( 253 choose_whole(a.whole, b.whole), 254 b.part, 255 b.value, 256 Object.assign({}, a.context, b.context, { 257 locations: (a.context.locations || []).concat(b.context.locations || []), 258 }), 259 ); 260 }; 261 const match = function (a) { 262 return func(a.value) 263 .query(state.setSpan(a.part)) 264 .map((b) => withWhole(a, b)); 265 }; 266 return flatten(pat_val.query(state).map((a) => match(a))); 267 }; 268 return new Pattern(query); 269 } 270 271 bind(func) { 272 const whole_func = function (a, b) { 273 if (a == undefined || b == undefined) { 274 return undefined; 275 } 276 return a.intersection_e(b); 277 }; 278 return this.bindWhole(whole_func, func); 279 } 280 281 join() { 282 // Flattens a pattern of patterns into a pattern, where wholes are 283 // the intersection of matched inner and outer haps. 284 return this.bind(id); 285 } 286 287 outerBind(func) { 288 return this.bindWhole((a) => a, func).setSteps(this._steps); 289 } 290 291 outerJoin() { 292 // Flattens a pattern of patterns into a pattern, where wholes are 293 // taken from outer haps. 294 return this.outerBind(id); 295 } 296 297 innerBind(func) { 298 return this.bindWhole((_, b) => b, func); 299 } 300 301 innerJoin() { 302 // Flattens a pattern of patterns into a pattern, where wholes are 303 // taken from inner haps. 304 return this.innerBind(id); 305 }
Flatterns patterns of patterns, by retriggering/resetting inner patterns at onsets of outer pattern haps
308 resetJoin(restart = false) { 309 const pat_of_pats = this; 310 return new Pattern((state) => { 311 return ( 312 pat_of_pats 313 // drop continuous haps from the outer pattern. 314 .discreteOnly() 315 .query(state) 316 .map((outer_hap) => { 317 return ( 318 outer_hap.value 319 // reset = align the inner pattern cycle start to outer pattern haps 320 // restart = align the inner pattern cycle zero to outer pattern haps 321 .late(restart ? outer_hap.whole.begin : outer_hap.whole.begin.cyclePos()) 322 .query(state) 323 .map((inner_hap) => 324 new Hap( 325 // Supports continuous haps in the inner pattern 326 inner_hap.whole ? inner_hap.whole.intersection(outer_hap.whole) : undefined, 327 inner_hap.part.intersection(outer_hap.part), 328 inner_hap.value, 329 ).setContext(outer_hap.combineContext(inner_hap)), 330 ) 331 // Drop haps that didn't intersect 332 .filter((hap) => hap.part) 333 ); 334 }) 335 .flat() 336 ); 337 }); 338 }
Like the other joins above, joins a pattern of patterns of values, into a flatter pattern of values. In this case it takes whole cycles of the inner pattern to fit each event in the outer pattern.
347 squeezeJoin() { 348 // A pattern of patterns, which we call the 'outer' pattern, with patterns 349 // as values which we call the 'inner' patterns. 350 const pat_of_pats = this; 351 function query(state) { 352 // Get the events with the inner patterns. Ignore continuous events (without 'wholes') 353 const haps = pat_of_pats.discreteOnly().query(state); 354 // A function to map over the events from the outer pattern. 355 function flatHap(outerHap) { 356 // Get the inner pattern, slowed and shifted so that the 'whole' 357 // timespan of the outer event corresponds to the first cycle of the 358 // inner event 359 const inner_pat = outerHap.value._focusSpan(outerHap.wholeOrPart()); 360 // Get the inner events, from the timespan of the outer event's part 361 const innerHaps = inner_pat.query(state.setSpan(outerHap.part)); 362 // A function to map over the inner events, to combine them with the 363 // outer event 364 function munge(outer, inner) { 365 let whole = undefined; 366 if (inner.whole && outer.whole) { 367 whole = inner.whole.intersection(outer.whole); 368 if (!whole) { 369 // The wholes are present, but don't intersect 370 return undefined; 371 } 372 } 373 const part = inner.part.intersection(outer.part); 374 if (!part) { 375 // The parts don't intersect 376 return undefined; 377 } 378 const context = inner.combineContext(outer); 379 return new Hap(whole, part, inner.value, context); 380 } 381 return innerHaps.map((innerHap) => munge(outerHap, innerHap)); 382 } 383 const result = flatten(haps.map(flatHap)); 384 // remove undefineds 385 return result.filter((x) => x); 386 } 387 return new Pattern(query); 388 }
//////////////////////////////////////////////////////////////////// Utility methods mainly for internal use
Query haps inside the given time span.
@tags internals @param {Fraction | number} begin from time @param {Fraction | number} end to time @returns Hap[] @example const pattern = sequence('a', ['b', 'c']) const haps = pattern.queryArc(0, 1) console.log(haps) silence @noAutocomplete
Returns a new pattern, with queries split at cycle boundaries. This makes some calculations easier to express, as all haps are then constrained to happen within a cycle. @tags internals @returns Pattern @noAutocomplete
Returns a new pattern, where the given function is applied to the query timespan before passing it to the original pattern. @tags internals @param {Function} func the function to apply @returns Pattern @noAutocomplete
As with withQuerySpan, but the function is applied to both the
begin and end time of the query timespan.
@tags internals
@param {Function} func the function to apply
@returns Pattern
@noAutocomplete
Similar to withQuerySpan, but the function is applied to the timespans
of all haps returned by pattern queries (both part timespans, and where
present, whole timespans).
@tags internals
@param {Function} func
@returns Pattern
@noAutocomplete
As with withHapSpan, but the function is applied to both the
begin and end time of the hap timespans.
@tags internals
@param {Function} func the function to apply
@returns Pattern
@noAutocomplete
Returns a new pattern with the given function applied to the list of haps returned by every query. @tags internals @param {Function} func @returns Pattern @noAutocomplete
As with withHaps, but applies the function to every hap, rather than every list of haps.
@tags internals
@param {Function} func
@returns Pattern
@noAutocomplete
Returns a new pattern with the context field set to every hap set to the given value. @tags internals @param {*} context @returns Pattern @noAutocomplete
Returns a new pattern with the given function applied to the context field of every hap. @tags internals @param {Function} func @returns Pattern @noAutocomplete
Returns a new pattern with the context field of every hap set to an empty object. @tags internals @returns Pattern @noAutocomplete
Returns a new pattern with the given location information added to the context of every hap. @tags internals @param {Number} start start offset @param {Number} end end offset @returns Pattern @noAutocomplete
575 withLoc(start, end) { 576 const location = { 577 start, 578 end, 579 }; 580 const result = this.withContext((context) => { 581 const locations = (context.locations || []).concat([location]); 582 return { ...context, locations }; 583 }); 584 if (this.__pure) { 585 result.__pure = this.__pure; 586 result.__pure_loc = location; 587 } 588 return result; 589 }
Returns a new Pattern, which only returns haps that meet the given test. @tags internals @param {Function} hap_test - a function which returns false for haps to be removed from the pattern @returns Pattern @example s("bd*8").velocity(rand).filterHaps((h) => (h.whole.begin % 1) < h.value.velocity)
As with filterHaps, but the function is applied to values
inside haps.
@tags internals
@param {Function} value_test
@returns Pattern
@example
const drums = s("bd sd bd sd")
kick: drums.filterValues((v) => v.s === 'bd').duck(2)
snare: drums.filterValues((v) => v.s === 'sd')
bass: s("saw!4").note("G#1").lpf(80).lpenv(4).orbit(2)
Returns a new pattern, with haps containing undefined values removed from query results. @tags internals @returns Pattern @noAutocomplete
Returns a new pattern, with all haps without onsets filtered out. A hap
with an onset is one with a whole timespan that begins at the same time
as its part timespan.
@tags internals
@returns Pattern
@noAutocomplete
Returns a new pattern, with 'continuous' haps (those without 'whole' timespans) removed from query results. @tags internals @returns Pattern @noAutocomplete
Combines adjacent haps with the same value and whole. Only intended for use in tests. @tags internals @noAutocomplete
663 defragmentHaps() { 664 // remove continuous haps 665 const pat = this.discreteOnly(); 666 667 return pat.withHaps((haps) => { 668 const result = []; 669 for (var i = 0; i < haps.length; ++i) { 670 var searching = true; 671 var a = haps[i]; 672 while (searching) { 673 const a_value = JSON.stringify(haps[i].value); 674 var found = false; 675 676 for (var j = i + 1; j < haps.length; j++) { 677 const b = haps[j]; 678 679 if (a.whole.equals(b.whole)) { 680 if (a.part.begin.eq(b.part.end)) { 681 if (a_value === JSON.stringify(b.value)) { 682 // eat the matching hap into 'a' 683 a = new Hap(a.whole, new TimeSpan(b.part.begin, a.part.end), a.value); 684 haps.splice(j, 1); 685 // restart the search 686 found = true; 687 break; 688 } 689 } else if (b.part.begin.eq(a.part.end)) { 690 if (a_value == JSON.stringify(b.value)) { 691 // eat the matching hap into 'a' 692 a = new Hap(a.whole, new TimeSpan(a.part.begin, b.part.end), a.value); 693 haps.splice(j, 1); 694 // restart the search 695 found = true; 696 break; 697 } 698 } 699 } 700 } 701 702 searching = found; 703 } 704 result.push(a); 705 } 706 return result; 707 }); 708 }
Queries the pattern for the first cycle, returning Haps. Mainly of use when debugging a pattern. @tags internals @param {Boolean} with_context - set to true, otherwise the context field will be stripped from the resulting haps. @returns [Hap] @noAutocomplete
Accessor for a list of values returned by querying the first cycle. @tags internals @noAutocomplete
More human-readable version of the firstCycleValues accessor.
@tags internals
@noAutocomplete
Returns a new pattern, which returns haps sorted in temporal order. Mainly of use when comparing two patterns for equality, in tests. @tags internals @returns Pattern @noAutocomplete
Returns a new pattern with all values parsed as numerals. @tags internals
//////////////////////////////////////////////////////////////////// Operators - see 'make composers' later..
776 _opIn(other, func) { 777 return this.fmap(func).appLeft(reify(other)); 778 } 779 _opOut(other, func) { 780 return this.fmap(func).appRight(reify(other)); 781 } 782 _opMix(other, func) { 783 return this.fmap(func).appBoth(reify(other)); 784 } 785 _opSqueeze(other, func) { 786 const otherPat = reify(other); 787 return this.fmap((a) => otherPat.fmap((b) => func(a)(b))).squeezeJoin(); 788 } 789 _opSqueezeOut(other, func) { 790 const thisPat = this; 791 const otherPat = reify(other); 792 return otherPat.fmap((a) => thisPat.fmap((b) => func(b)(a))).squeezeJoin(); 793 } 794 _opReset(other, func) { 795 const otherPat = reify(other); 796 return otherPat.fmap((b) => this.fmap((a) => func(a)(b))).resetJoin(); 797 } 798 _opRestart(other, func) { 799 const otherPat = reify(other); 800 return otherPat.fmap((b) => this.fmap((a) => func(a)(b))).restartJoin(); 801 } 802 _opPoly(other, func) { 803 const otherPat = reify(other); 804 return this.fmap((b) => otherPat.fmap((a) => func(a)(b))).polyJoin(); 805 }
//////////////////////////////////////////////////////////////////// End-user methods. Those beginning with an underscore (_) are 'patternified', i.e. versions are created without the underscore, that are magically transformed to accept patterns for all their arguments.
//////////////////////////////////////////////////////////////////// Methods without corresponding toplevel functions
Layers the result of the given function(s). Like superimpose, but without the original pattern:
@name layer
@tags combiners
@memberof Pattern
@returns Pattern
@example
"<0 2 4 6 ~ 4 ~ 2 0!3 ~!5>*8"
.layer(x=>x.add("0,2"))
.scale('C minor').note()
Superimposes the result of the given function(s) on top of the original pattern: @name superimpose @tags combiners @memberof Pattern @returns Pattern @example "<0 2 4 6 ~ 4 ~ 2 0!3 ~!5>*8" .superimpose(x=>x.add(2)) .scale('C minor').note()
//////////////////////////////////////////////////////////////////// Multi-pattern functions
853 sequence(...pats) { 854 return sequence(this, ...pats); 855 } 856 857 seq(...pats) { 858 return sequence(this, ...pats); 859 } 860 cat(...pats) { 861 return cat(this, ...pats); 862 } 863 864 fastcat(...pats) { 865 return fastcat(this, ...pats); 866 } 867 868 slowcat(...pats) { 869 return slowcat(this, ...pats); 870 }
//////////////////////////////////////////////////////////////////// Context methods - ones that deal with metadata
875 onTrigger(onTrigger, dominant = true) { 876 return this.withHap((hap) => 877 hap.setContext({ 878 ...hap.context, 879 onTrigger: (...args) => { 880 // run previously set trigger, if it exists 881 hap.context.onTrigger?.(...args); 882 onTrigger(...args); 883 }, 884 // if dominantTrigger is set to true, the default output (webaudio) will be disabled 885 // when using multiple triggers, you cannot flip this flag to false again! 886 // example: x.csound('CooLSynth').log() as well as x.log().csound('CooLSynth') should work the same 887 dominantTrigger: hap.context.dominantTrigger || dominant, 888 }), 889 ); 890 }
Writes the content of the current event to the console (visible in the side menu). @tags visualization @name log @memberof Pattern @example s("bd sd").log()
A simplified version of log which writes all "values" (various configurable parameters)
within the event to the console (visible in the side menu).
@tags visualization
@name logValues
@memberof Pattern
@example
s("bd sd").gain("0.25 0.5 1").n("2 1 0").logValues()
//////////////////////////////////////////////////////////////////// Visualisation
//////////////////////////////////////////////////////////////////// methods relating to breaking patterns into subcycles
Breaks a pattern into a pattern of patterns, according to the structure of the given binary pattern.
Breaks a pattern into pieces according to the structure of a given pattern. True values in the given pattern cause the corresponding subcycle of the source pattern to be looped, and for an (optional) given function to be applied. False values result in the corresponding part of the source pattern to be played unchanged. @tags temporal @name into @memberof Pattern @example sound("bd sd ht lt").into("1 0", hurry(2))
//////////////////////////////////////////////////////////////////// functions relating to chords/patterns of lists/lists of patterns
returns Array<Hap[]> where each list of haps satisfies eq
congruent haps = haps with equal spans
972const congruent = (a, b) => a.spanEquals(b);
Pattern<Hap<T>> -> Pattern<Hap<T[]>> returned pattern contains arrays of congruent haps
Selects indices in in stacked notes. @tags temporal @example note("<[c,eb,g]!2 [c,f,ab] [d,f,ab]>") .arpWith(haps => haps[2])
Selects indices in in stacked notes. @tags temporal @example note("<[c,eb,g]!2 [c,f,ab] [d,f,ab]>") .arp("0 [0,2] 1 [0,2]")
Takes a time duration followed by one or more patterns, and shifts the given patterns in time, so they are distributed equally over the given time duration. They are then combined with the pattern 'weave' is called on, after it has been stretched out (i.e. slowed down by) the time duration. @name weave @memberof Pattern @example pan(saw).weave(4, s("bd(3,8)"), s("~ sd")) @example n("0 1 2 3 4 5 6 7").weave(8, s("bd(3,8)"), s("~ sd"))
addToPrototype('weave', function (t, ...pats) { return this.weaveWith(t, ...pats.map((x) => set.out(x))); });
Like 'weave', but accepts functions rather than patterns, which are applied to the pattern. @name weaveWith @memberof Pattern
addToPrototype('weaveWith', function (t, ...funcs) { const pat = this; const l = funcs.length; t = Fraction(t); if (l == 0) { return silence; } return stack(...funcs.map((func, i) => pat.inside(t, func).early(Fraction(i).div(l))))._slow(t); });
//////////////////////////////////////////////////////////////////// compose matrix functions
1041function _nonArrayObject(x) { 1042 return !Array.isArray(x) && typeof x === 'object' && !isFraction(x); 1043} 1044function _composeOp(a, b, func) { 1045 if (_nonArrayObject(a) || _nonArrayObject(b)) { 1046 if (!_nonArrayObject(a)) { 1047 a = { value: a }; 1048 } 1049 if (!_nonArrayObject(b)) { 1050 b = { value: b }; 1051 } 1052 return unionWithObj(a, b, func); 1053 } 1054 return func(a, b); 1055}
pattern composers
1058const COMPOSERS = { 1059 /** 1060 * When called on a pattern `a`, with a input pattern `b` (`a.set(b)`), 1061 * combines `a` and `b` such that anything defined in `b` 1062 * and anything defined in `a` that is *not* defined in `b` 1063 * will be in the resulting pattern. 1064 * 1065 * The structure is maintained from `a`, 1066 * because the default pattern alignment is `in`, 1067 * see the section on `Pattern Alignment` 1068 * in the technical manual in the docs 1069 * 1070 * This is the inverse of `keep` 1071 * 1072 * See examples below 1073 * @name set 1074 * @param {Pattern} pat 1075 * @returns {Pattern} 1076 * @memberof Pattern 1077 * @tags internal, combiners 1078 * @example 1079 * // because input pattern has `s` set, 1080 * // it overrides the "sine" declared earlier 1081 * note("c a f e").s("sine").set(s("triangle")) 1082 */ 1083 set: [(a, b) => b], 1084 /** 1085 * When called on a pattern `a`, with a input pattern `b` (`a.keep(b)`), 1086 * combines `a` and `b` such that anything defined in `a`, 1087 * and anything defined in `b` that is *not* defined in `a` 1088 * will be in the resulting pattern 1089 * 1090 * The structure is maintained from `a`, 1091 * because the default pattern alignment is `in`, 1092 * see the section on `Pattern Alignment` 1093 * in the technical manual in the docs 1094 * 1095 * This is the inverse of `set` 1096 * 1097 * See examples below 1098 * @name keep 1099 * @param {Pattern} pat 1100 * @memberof Pattern 1101 * @returns {Pattern} 1102 * @tags internal, combiners 1103 * @example 1104 * // notes, already defined, will stay "c a f e", 1105 * // while "s", not defined, will be set to "piano" 1106 * note("c a f e").keep(note("e f a c").s("piano")) 1107 */ 1108 keep: [(a) => a], 1109 keepif: [(a, b) => (b ? a : undefined)],
numerical functions
Assumes a pattern of numbers. Adds the given number to each item in the pattern. @name add @memberof Pattern @tags math @example // Here, the triad 0, 2, 4 is shifted by different amounts n("0 2 4".add("<0 3 4 0>")).scale("C:major") // Without add, the equivalent would be: // n("<[0 2 4] [3 5 7] [4 6 8] [0 2 4]>").scale("C:major") @example // You can also use add with notes: note("c3 e3 g3".add("<0 5 7 0>")) // Behind the scenes, the notes are converted to midi numbers: // note("48 52 55".add("<0 5 7 0>"))
1129 add: [numeralArgs((a, b) => a + b)], // support string concatenation 1130 /** 1131 * 1132 * Like add, but the given numbers are subtracted. 1133 * @name sub 1134 * @memberof Pattern 1135 * @tags math 1136 * @example 1137 * n("0 2 4".sub("<0 1 2 3>")).scale("C4:minor") 1138 * // See add for more information. 1139 */ 1140 sub: [numeralArgs((a, b) => a - b)], 1141 /** 1142 * 1143 * Multiplies each number by the given factor. 1144 * @name mul 1145 * @memberof Pattern 1146 * @tags math 1147 * @example 1148 * "<1 1.5 [1.66, <2 2.33>]>*4".mul(150).freq() 1149 */ 1150 mul: [numeralArgs((a, b) => a * b)], 1151 /** 1152 * 1153 * Divides each number by the given factor. 1154 * @name div 1155 * @memberof Pattern 1156 * @tags math 1157 */ 1158 div: [numeralArgs((a, b) => a / b)], 1159 mod: [numeralArgs(_mod)], 1160 pow: [numeralArgs(Math.pow)], 1161 band: [numeralArgs((a, b) => a & b)], 1162 bor: [numeralArgs((a, b) => a | b)], 1163 bxor: [numeralArgs((a, b) => a ^ b)], 1164 blshift: [numeralArgs((a, b) => a << b)], 1165 brshift: [numeralArgs((a, b) => a >> b)],
TODO - force numerical comparison if both look like numbers?
1183const _setupAlignments = () => { 1184 // generate methods to do what and how 1185 for (const [what, [op, preprocess]] of Object.entries(COMPOSERS)) { 1186 // make plain version, e.g. pat._add(value) adds that plain value 1187 // to all the values in pat 1188 Pattern.prototype['_' + what] = function (value) { 1189 return this.fmap((x) => op(x, value)); 1190 };
make patternified monster version
1193 Object.defineProperty(Pattern.prototype, what, { 1194 // Set to configurable so we can update if the default alignment changes 1195 configurable: true, 1196 // a getter that returns a function, so 'pat' can be 1197 // accessed by closures that are methods of that function.. 1198 get: function () { 1199 const pat = this;
wrap the 'in' function as default behaviour
1202 const wrapper = (...other) => pat[what][DEFAULT_ALIGNMENT](...other);
add methods to that function for each behaviour
1205 for (const how of ALIGNMENTS) { 1206 wrapper[how.toLowerCase()] = function (...other) { 1207 var howpat = pat; 1208 other = sequence(other); 1209 if (preprocess) { 1210 howpat = preprocess(howpat); 1211 other = preprocess(other); 1212 } 1213 var result; 1214 // hack to remove undefs when doing 'keepif' 1215 if (what === 'keepif') { 1216 // avoid union, as we want to throw away the value of 'b' completely 1217 result = howpat['_op' + how](other, (a) => (b) => op(a, b)); 1218 result = result.removeUndefineds(); 1219 } else { 1220 result = howpat['_op' + how](other, (a) => (b) => _composeOp(a, b, op)); 1221 } 1222 return result; 1223 }; 1224 } 1225 wrapper.squeezein = wrapper.squeeze;
Default op to 'set', e.g. pat.squeeze(pat2) = pat.set.squeeze(pat2)
1242 for (const how of ALIGNMENTS) { 1243 Pattern.prototype[how.toLowerCase()] = function (...args) { 1244 return this.set[how.toLowerCase()](args); 1245 }; 1246 } 1247 // binary composers 1248 /** 1249 * Applies the given structure to the pattern: 1250 * 1251 * @tags temporal 1252 * @example 1253 * note("c,eb,g") 1254 * .struct("x ~ x ~ ~ x ~ x ~ ~ ~ x ~ x ~ ~") 1255 * .slow(2) 1256 */ 1257 Pattern.prototype.struct = function (...args) { 1258 return this.keepif.out(...args); 1259 }; 1260 Pattern.prototype.structAll = function (...args) { 1261 return this.keep.out(...args); 1262 }; 1263 /** 1264 * Returns silence when mask is 0 or "~" 1265 * 1266 * @tags temporal 1267 * @example 1268 * note("c [eb,g] d [eb,g]").mask("<1 [0 1]>") 1269 */ 1270 Pattern.prototype.mask = function (...args) { 1271 return this.keepif.in(...args); 1272 }; 1273 Pattern.prototype.maskAll = function (...args) { 1274 return this.keep.in(...args); 1275 }; 1276 /** 1277 * Resets the pattern to the start of the cycle for each onset of the reset pattern. 1278 * 1279 * @tags temporal 1280 * @example 1281 * s("[<bd lt> sd]*2, hh*8").reset("<x@3 x(5,8)>") 1282 */ 1283 Pattern.prototype.reset = function (...args) { 1284 return this.keepif.reset(...args); 1285 }; 1286 Pattern.prototype.resetAll = function (...args) { 1287 return this.keep.reset(...args); 1288 }; 1289 /** 1290 * Restarts the pattern for each onset of the restart pattern. 1291 * While reset will only reset the current cycle, restart will start from cycle 0. 1292 * 1293 * @tags temporal 1294 * @example 1295 * s("[<bd lt> sd]*2, hh*8").restart("<x@3 x(5,8)>") 1296 */ 1297 Pattern.prototype.restart = function (...args) { 1298 return this.keepif.restart(...args); 1299 }; 1300 Pattern.prototype.restartAll = function (...args) { 1301 return this.keep.restart(...args); 1302 }; 1303})();
Sets the default method of combining events from two patterns (aka alignment) in Strudel. The default method is 'in', meaning that patterns to the left will (typically) dictate the event timings when combined with patterns to the right. By changing alignment to 'out', the opposite will happen. With 'mix', they will combine their event timings.
Note that we say the default method, because alignments can also be set explicitly with calls like 'add.mix', 'set.squeeze', etc.
@param {string} method Default join method to use. Options: 'in', 'out', 'mix', 'squeeze', 'squeezeout', 'reset', 'restart', 'poly' @tags combiners @example setDefaultJoin('mix') // also try 'in', 'out', 'squeeze', etc. s("saw").vel("1 0.5").note("F A C E").delay("0 0.2 0.3")
1331export const pm = polymeter;
methods that create patterns, which are added to patternified Pattern methods TODO: remove? this is only used in old transpiler (shapeshifter) Pattern.prototype.factories = { pure, stack, slowcat, fastcat, cat, timecat, sequence, seq, polymeter, pm, polyrhythm, pr, }; the magic happens in Pattern constructor. Keeping this in prototype enables adding methods from the outside (e.g. see tonal.ts)
Elemental patterns
Does absolutely nothing, but with a given metrical 'steps' @name gap @tags generators @param {number} steps @example gap(3) // "~@3"
1361export const gap = (steps) => new Pattern(() => [], steps);
Does absolutely nothing.. @name silence @tags generators @example silence // "~"
1370export const silence = gap(1);
Like silence, but with a 'steps' (relative duration) of 0
1373export const nothing = gap(0);
A discrete value that repeats once per cycle.
@tags generators @returns {Pattern} @example pure('e4') // "e4" @noAutocomplete
1393export function isPattern(thing) { 1394 // thing?.constructor?.name !== 'Pattern' // <- this will fail when code is mangled 1395 const is = thing instanceof Pattern || thing?._Pattern; 1396 // TODO: find out how to check wrong core dependency. below will never work !thing === 'undefined' 1397 // wrapping it in (..) will result other checks to log that warning (e.g. isPattern('kalimba')) 1398 /* if (!thing instanceof Pattern) { 1399 console.warn( 1400 `Found Pattern that fails "instanceof Pattern" check. 1401 This may happen if you are using multiple versions of @strudel/core. 1402 Please check by running "npm ls @strudel/core".`, 1403 ); 1404 console.log(thing); 1405 } */ 1406 return is; 1407} 1408 1409export function reify(thing) { 1410 // Turns something into a pattern, unless it's already a pattern 1411 if (isPattern(thing)) { 1412 return thing; 1413 } 1414 if (stringParser && typeof thing === 'string') { 1415 return stringParser(thing); 1416 } 1417 return pure(thing); 1418}
Takes a list of patterns, and returns a pattern of lists.
@tags temporal
The given items are played at the same time at the same length.
@tags temporal @return {Pattern} @synonyms polyrhythm, pr @example stack("g3", "b3", ["e4", "d4"]).note() // "g3,b3,[e4 d4]".note()
@example // As a chained function: s("hh*4").stack( note("c4(5,8)") )
1449export function stack(...pats) { 1450 // Array test here is to avoid infinite recursions.. 1451 pats = pats.map((pat) => (Array.isArray(pat) ? sequence(...pat) : reify(pat))); 1452 const query = (state) => flatten(pats.map((pat) => pat.query(state))); 1453 const result = new Pattern(query); 1454 if (__steps) { 1455 result._steps = lcm(...pats.map((pat) => pat._steps)); 1456 } 1457 return result; 1458}
1460function _stackWith(func, pats) { 1461 pats = pats.map((pat) => (Array.isArray(pat) ? sequence(...pat) : reify(pat))); 1462 if (pats.length === 0) { 1463 return silence; 1464 } 1465 if (pats.length === 1) { 1466 return pats[0]; 1467 } 1468 const [left, ...right] = pats.map((pat) => pat._steps); 1469 const steps = __steps ? left.maximum(...right) : undefined; 1470 return stack(...func(steps, pats)); 1471} 1472 1473export function stackLeft(...pats) { 1474 return _stackWith( 1475 (steps, pats) => pats.map((pat) => (pat._steps.eq(steps) ? pat : stepcat(pat, gap(steps.sub(pat._steps))))), 1476 pats, 1477 ); 1478} 1479 1480export function stackRight(...pats) { 1481 return _stackWith( 1482 (steps, pats) => pats.map((pat) => (pat._steps.eq(steps) ? pat : stepcat(gap(steps.sub(pat._steps)), pat))), 1483 pats, 1484 ); 1485} 1486 1487export function stackCentre(...pats) { 1488 return _stackWith( 1489 (steps, pats) => 1490 pats.map((pat) => { 1491 if (pat._steps.eq(steps)) { 1492 return pat; 1493 } 1494 const g = gap(steps.sub(pat._steps).div(2)); 1495 return stepcat(g, pat, g); 1496 }), 1497 pats, 1498 ); 1499} 1500 1501export function stackBy(by, ...pats) { 1502 const [left, ...right] = pats.map((pat) => pat._steps); 1503 const steps = left.maximum(...right); 1504 const lookup = { 1505 centre: stackCentre, 1506 left: stackLeft, 1507 right: stackRight, 1508 expand: stack, 1509 repeat: (...args) => polymeter(...args).steps(steps), 1510 }; 1511 return by 1512 .inhabit(lookup) 1513 .fmap((func) => func(...pats)) 1514 .innerJoin() 1515 .setSteps(steps); 1516}
Concatenation: combines a list of patterns, switching between them successively, one per cycle.
@tags combiners @return {Pattern} @synonyms cat @example slowcat("e5", "b4", ["d5", "c5"])
1528export function slowcat(...pats) { 1529 // Array test here is to avoid infinite recursions.. 1530 pats = pats.map((pat) => (Array.isArray(pat) ? fastcat(...pat) : reify(pat))); 1531 1532 if (!pats.length) { 1533 return silence; 1534 } else if (pats.length == 1) { 1535 return pats[0]; 1536 } 1537 1538 const query = function (state) { 1539 const span = state.span; 1540 const pat_n = _mod(span.begin.sam(), pats.length); 1541 const pat = pats[pat_n]; 1542 // A bit of maths to make sure that cycles from constituent patterns aren't skipped. 1543 // For example if three patterns are slowcat-ed, the fourth cycle of the result should 1544 // be the second (rather than fourth) cycle from the first pattern. 1545 const offset = span.begin.floor().sub(span.begin.div(pats.length).floor()); 1546 return pat.withHapTime((t) => t.add(offset)).query(state.setSpan(span.withTime((t) => t.sub(offset)))); 1547 }; 1548 const steps = __steps ? lcm(...pats.map((x) => x._steps)) : undefined; 1549 return new Pattern(query).splitQueries().setSteps(steps); 1550}
Concatenation: combines a list of patterns, switching between them successively, one per cycle. Unlike slowcat, this version will skip cycles. @tags combiners @param {...any} items - The items to concatenate @return {Pattern}
1557export function slowcatPrime(...pats) { 1558 if (!pats.length) { 1559 return silence; 1560 } 1561 pats = pats.map(reify); 1562 const query = function (state) { 1563 const pat_n = _mod(Math.floor(state.span.begin), pats.length); 1564 const pat = pats[pat_n]; 1565 return pat.query(state); 1566 }; 1567 return new Pattern(query).splitQueries(); 1568}
The given items are concatenated, where each one takes one cycle.
@tags combiners @param {...any} items - The items to concatenate @synonyms slowcat @return {Pattern} @example cat("e5", "b4", ["d5", "c5"]).note() // "<e5 b4 [d5 c5]>".note()
@example // As a chained function: s("hh*4").cat( note("c4(5,8)") )
Allows to arrange multiple patterns together over multiple cycles. Takes a variable number of arrays with two elements specifying the number of cycles and the pattern to use.
@tags combiners @return {Pattern} @example arrange( [4, "<c a f e>(3,8)"], [2, "<g a>(5,8)"] ).note()
Similarly to arrange, allows you to arrange multiple patterns together over multiple cycles.
Unlike arrange, you specify a start and stop time for each pattern rather than duration, which
means that patterns can overlap.
@tags combiners
@return {Pattern}
@example
seqPLoop(
[0, 2, "bd(3,8)"],
[1, 3, "cp(3,8)"]
).sound()
1620export function seqPLoop(...parts) { 1621 let total = Fraction(0); 1622 const pats = []; 1623 for (let part of parts) { 1624 if (part.length == 2) { 1625 part.unshift(total); 1626 } 1627 total = part[1]; 1628 } 1629 1630 return stack( 1631 ...parts.map(([start, stop, pat]) => 1632 pure(reify(pat)).compress(Fraction(start).div(total), Fraction(stop).div(total)), 1633 ), 1634 ) 1635 .slow(total) 1636 .innerJoin(); // or resetJoin or restartJoin ?? 1637}
1639export function fastcat(...pats) { 1640 let result = slowcat(...pats); 1641 if (pats.length > 1) { 1642 result = result._fast(pats.length); 1643 result._steps = pats.length; 1644 } 1645 if (pats.length == 1 && pats[0].__steps_source) { 1646 pats._steps = pats[0]._steps; 1647 } 1648 return result; 1649}
See fastcat
@name sequence
@tags combiners
Like cat, but the items are crammed into one cycle. @tags combiners @synonyms fastcat @example seq("e5", "b4", ["d5", "c5"]).note() // "e5 b4 [d5 c5]".note()
@example // As a chained function: s("hh*4").seq( note("c4(5,8)") )
1677function _sequenceCount(x) { 1678 if (Array.isArray(x)) { 1679 if (x.length == 0) { 1680 return [silence, 0]; 1681 } 1682 if (x.length == 1) { 1683 return _sequenceCount(x[0]); 1684 } 1685 return [fastcat(...x.map((a) => _sequenceCount(a)[0])), x.length]; 1686 } 1687 return [reify(x), 1]; 1688} 1689 1690export const mask = curry((a, b) => reify(b).mask(a)); 1691export const struct = curry((a, b) => reify(b).struct(a)); 1692export const superimpose = curry((a, b) => reify(b).superimpose(...a)); 1693export const withValue = curry((a, b) => reify(b).withValue(a)); 1694 1695export const bind = curry((a, b) => reify(b).bind(a)); 1696export const innerBind = curry((a, b) => reify(b).innerBind(a)); 1697export const outerBind = curry((a, b) => reify(b).outerBind(a)); 1698export const squeezeBind = curry((a, b) => reify(b).squeezeBind(a)); 1699export const stepBind = curry((a, b) => reify(b).stepBind(a)); 1700export const polyBind = curry((a, b) => reify(b).polyBind(a));
operators
1703export const set = curry((a, b) => reify(b).set(a)); 1704export const keep = curry((a, b) => reify(b).keep(a)); 1705export const keepif = curry((a, b) => reify(b).keepif(a)); 1706export const add = curry((a, b) => reify(b).add(a)); 1707export const sub = curry((a, b) => reify(b).sub(a)); 1708export const mul = curry((a, b) => reify(b).mul(a)); 1709export const div = curry((a, b) => reify(b).div(a)); 1710export const mod = curry((a, b) => reify(b).mod(a)); 1711export const pow = curry((a, b) => reify(b).pow(a)); 1712export const band = curry((a, b) => reify(b).band(a)); 1713export const bor = curry((a, b) => reify(b).bor(a)); 1714export const bxor = curry((a, b) => reify(b).bxor(a)); 1715export const blshift = curry((a, b) => reify(b).blshift(a)); 1716export const brshift = curry((a, b) => reify(b).brshift(a)); 1717export const lt = curry((a, b) => reify(b).lt(a)); 1718export const gt = curry((a, b) => reify(b).gt(a)); 1719export const lte = curry((a, b) => reify(b).lte(a)); 1720export const gte = curry((a, b) => reify(b).gte(a)); 1721export const eq = curry((a, b) => reify(b).eq(a)); 1722export const eqt = curry((a, b) => reify(b).eqt(a)); 1723export const ne = curry((a, b) => reify(b).ne(a)); 1724export const net = curry((a, b) => reify(b).net(a)); 1725export const and = curry((a, b) => reify(b).and(a)); 1726export const or = curry((a, b) => reify(b).or(a)); 1727export const func = curry((a, b) => reify(b).func(a));
Registers a new pattern method. The method is added to the Pattern class + the standalone function is returned from register.
@tags functional
@param {string | string[]} name name of the function, or an array of names to be used as synonyms
@param {function} func function with 1 or more params, where last is the current pattern
@param {bool} patternify defaults to true; if set to false, you will have more control over the arguments to func as they will be
in their raw form and it will be up to you to patternify them and/or query them for values
@example
const vlpf = register('vlpf', (freq, pat) => {
return pat.fmap((v) => ({...v, cutoff: freq * (v.velocity ?? 1) }));
})
s("saw").seg(8).velocity(rand).vlpf(800)
1744export function register(name, func, patternify = true, preserveSteps = false, join = (x) => x.innerJoin()) { 1745 if (isPattern(name)) { 1746 throw new Error( 1747 'Name argument for register is a pattern, try using single quotes (\'name\') instead of double quotes ("name")', 1748 ); 1749 } 1750 1751 if (Array.isArray(name)) { 1752 const result = {}; 1753 for (const name_item of name) { 1754 result[name_item] = register(name_item, func, patternify, preserveSteps, join); 1755 } 1756 return result; 1757 } 1758 const arity = func.length; 1759 var pfunc; // the patternified function 1760 1761 if (patternify) { 1762 pfunc = function (...args) { 1763 args = args.map(reify); 1764 const pat = args[args.length - 1]; 1765 let result; 1766 1767 if (arity === 1) { 1768 result = func(pat); 1769 } else { 1770 const firstArgs = args.slice(0, -1); 1771 1772 if (firstArgs.every((arg) => arg.__pure != undefined)) { 1773 const pureArgs = firstArgs.map((arg) => arg.__pure); 1774 const pureLocs = firstArgs.filter((arg) => arg.__pure_loc).map((arg) => arg.__pure_loc); 1775 result = func(...pureArgs, pat); 1776 result = result.withContext((context) => { 1777 const locations = (context.locations || []).concat(pureLocs); 1778 return { ...context, locations }; 1779 }); 1780 } else { 1781 const [left, ...right] = firstArgs; 1782 1783 let mapFn = (...args) => { 1784 return func(...args, pat); 1785 }; 1786 mapFn = curry(mapFn, null, arity - 1); 1787 result = join(right.reduce((acc, p) => acc.appLeft(p), left.fmap(mapFn))); 1788 } 1789 } 1790 if (preserveSteps) { 1791 result._steps = pat._steps; 1792 } 1793 return result; 1794 }; 1795 } else { 1796 pfunc = function (...args) { 1797 args = args.map(reify); 1798 const result = func(...args); 1799 if (preserveSteps) { 1800 result._steps = args[args.length - 1]._steps; 1801 } 1802 return result; 1803 }; 1804 } 1805 1806 Pattern.prototype[name] = function (...args) { 1807 // For methods that take a single argument (plus 'this'), allow 1808 // multiple arguments but sequence them 1809 if (arity === 2 && args.length !== 1) { 1810 args = [sequence(...args)]; 1811 } else if (arity !== args.length + 1) { 1812 throw new Error(`.${name}() expects ${arity - 1} inputs but got ${args.length}.`); 1813 } 1814 args = args.map(reify); 1815 return pfunc(...args, this); 1816 }; 1817 1818 if (arity > 1) { 1819 // There are patternified args, so lets make an unpatternified 1820 // version, prefixed by '_' 1821 Pattern.prototype['_' + name] = function (...args) { 1822 const result = func(...args, this); 1823 if (preserveSteps) { 1824 result.setSteps(this._steps); 1825 } 1826 return result; 1827 }; 1828 }
toplevel functions get curried as well as patternified because pfunc uses spread args, we need to state the arity explicitly!
Like register, but defaults to stepJoin
//////////////////////////////////////////////////////////////////// Numerical transformations
Assumes a numerical pattern. Returns a new pattern with all values rounded to the nearest integer. @name round @tags math @memberof Pattern @returns Pattern @example n("0.5 1.5 2.5".round()).scale("C:major")
Assumes a numerical pattern. Returns a new pattern with all values set to
their mathematical floor. E.g. 3.7 replaced with to 3, and -4.2
replaced with -5.
@name floor
@memberof Pattern
@tags math
@returns Pattern
@example
note("42 42.1 42.5 43".floor())
1873export const log2 = register('log2', (pat) => pat.asNumber().fmap((v) => Math.log2(v)));
Assumes a numerical pattern. Returns a new pattern with all values set to
their mathematical ceiling. E.g. 3.2 replaced with 4, and -4.2
replaced with -4.
@name ceil
@memberof Pattern
@tags math
@returns Pattern
@example
note("42 42.1 42.5 43".ceil())
Assumes a numerical pattern, containing unipolar values in the range 0 ..
- Returns a new pattern with values scaled to the bipolar range -1 .. 1 @tags math @returns Pattern @noAutocomplete
Assumes a numerical pattern, containing bipolar values in the range -1 .. 1 Returns a new pattern with values scaled to the unipolar range 0 .. 1 @tags math @returns Pattern @noAutocomplete
Assumes a numerical pattern, containing unipolar values in the range 0 .. 1. Returns a new pattern with values scaled to the given min/max range. Most useful in combination with continuous patterns. @name range @memberof Pattern @tags math @returns Pattern @example s("[bd sd]2,hh8") .cutoff(sine.range(500,4000))
Assumes a numerical pattern, containing unipolar values in the range 0 .. 1 Returns a new pattern with values scaled to the given min/max range, following an exponential curve. @name rangex @memberof Pattern @tags math @returns Pattern @example s("[bd sd]2,hh8") .cutoff(sine.rangex(500,4000))
Assumes a numerical pattern, containing bipolar values in the range -1 .. 1 Returns a new pattern with values scaled to the given min/max range. @name range2 @memberof Pattern @tags math @returns Pattern @example s("[bd sd]2,hh8") .cutoff(sine2.range2(500,4000))
Allows dividing numbers via list notation using ":". Returns a new pattern with just numbers. @name ratio @memberof Pattern @tags math @returns Pattern @example ratio("1, 5:4, 3:2").mul(110) .freq().s("piano")
//////////////////////////////////////////////////////////////////// Structural and temporal transformations
Compress each cycle into the given timespan, leaving a gap @tags temporal @example cat( s("bd sd").compress(.25,.75), s("~ bd sd ~") )
speeds up a pattern like fast, but rather than it playing multiple times as fast would it instead leaves a gap in the remaining space of the cycle. For example, the following will play the sound pattern "bd sn" only once but compressed into the first half of the cycle, i.e. twice as fast. @tags temporal @name fastGap @synonyms fastgap @example s("bd sd").fastGap(2)
2010export const { fastGap, fastgap } = register(['fastGap', 'fastgap'], function (factor, pat) { 2011 // A bit fiddly, to drop zero-width queries at the start of the next cycle 2012 const qf = function (span) { 2013 const cycle = span.begin.sam(); 2014 const bpos = span.begin.sub(cycle).mul(factor).min(1); 2015 const epos = span.end.sub(cycle).mul(factor).min(1); 2016 if (bpos >= 1) { 2017 return undefined; 2018 } 2019 return new TimeSpan(cycle.add(bpos), cycle.add(epos)); 2020 }; 2021 // Also fiddly, to maintain the right 'whole' relative to the part 2022 const ef = function (hap) { 2023 const begin = hap.part.begin; 2024 const end = hap.part.end; 2025 const cycle = begin.sam(); 2026 const beginPos = begin.sub(cycle).div(factor).min(1); 2027 const endPos = end.sub(cycle).div(factor).min(1); 2028 const newPart = new TimeSpan(cycle.add(beginPos), cycle.add(endPos)); 2029 const newWhole = !hap.whole 2030 ? undefined 2031 : new TimeSpan( 2032 newPart.begin.sub(begin.sub(hap.whole.begin).div(factor)), 2033 newPart.end.add(hap.whole.end.sub(end).div(factor)), 2034 ); 2035 return new Hap(newWhole, newPart, hap.value, hap.context); 2036 }; 2037 return pat.withQuerySpanMaybe(qf).withHap(ef).splitQueries(); 2038});
Similar to compress, but doesn't leave gaps, and the 'focus' can be bigger than a cycle
@tags temporal
@example
s("bd hh sd hh").focus(1/4, 3/4)
The ply function repeats each event the given number of times. @tags temporal @example s("bd ~ sd cp").ply("<1 2 3>")
Speed up a pattern by the given factor. Used by "*" in mini notation.
@tags temporal @name fast @synonyms density @memberof Pattern @param {number | Pattern} factor speed up factor @returns Pattern @example s("bd hh sd hh").fast(2) // s("[bd hh sd hh]*2")
2084export const { fast, density } = register( 2085 ['fast', 'density'], 2086 function (factor, pat) { 2087 if (factor === 0) { 2088 return silence; 2089 } 2090 factor = Fraction(factor); 2091 const fastQuery = pat.withQueryTime((t) => t.mul(factor)); 2092 return fastQuery.withHapTime((t) => t.div(factor)).setSteps(pat._steps); 2093 }, 2094 true, 2095 true, 2096);
Both speeds up the pattern (like 'fast') and the sample playback (like 'speed'). @tags temporal @example s("bd sd:2").hurry("<1 2 4 3>").slow(1.5)
Slow down a pattern over the given number of cycles. Like the "/" operator in mini notation.
@tags temporal @name slow @synonyms sparsity @memberof Pattern @param {number | Pattern} factor slow down factor @returns Pattern @example s("bd hh sd hh").slow(2) // s("[bd hh sd hh]/2")
Carries out an operation 'inside' a cycle. @tags temporal @example "0 1 2 3 4 3 2 1".inside(4, rev).scale('C major').note() // "0 1 2 3 4 3 2 1".slow(4).rev().fast(4).scale('C major').note()
Carries out an operation 'outside' a cycle. @tags temporal @example "<[0 1] 2 [3 4] 5>".outside(4, rev).scale('C major').note() // "<[0 1] 2 [3 4] 5>".fast(4).rev().slow(4).scale('C major').note()
Applies the given function every n cycles, starting from the last cycle. @tags temporal @name lastOf @memberof Pattern @param {number} n how many cycles @param {function} func function to apply @returns Pattern @example note("c3 d3 e3 g3").lastOf(4, x=>x.rev())
Applies the given function every n cycles, starting from the first cycle. @tags temporal @name firstOf @memberof Pattern @param {number} n how many cycles @param {function} func function to apply @returns Pattern @example note("c3 d3 e3 g3").firstOf(4, x=>x.rev())
An alias for firstOf
@tags temporal
@name every
@memberof Pattern
@param {number} n how many cycles
@param {function} func function to apply
@returns Pattern
@example
note("c3 d3 e3 g3").every(4, x=>x.rev())
Applies the given function to the pattern. Like layer, but with a single function: @tags combiners @name apply @example "<c3 eb3 g3>".scale('C minor').apply(scaleTranspose("0,2,4")).note()
Plays the pattern at the given cycles per minute. @tags temporal @deprecated @example s("<bd sd>,hh*2").cpm(90) // = 90 bpm
this is redefined in repl.mjs, using the current cps as divisor
Nudge a pattern to start earlier in time. Equivalent of Tidal's <~ operator
@tags temporal @name early @memberof Pattern @param {number | Pattern} cycles number of cycles to nudge left @returns Pattern @example "bd ~".stack("hh ~".early(.1)).s()
Nudge a pattern to start later in time. Equivalent of Tidal's ~> operator
@tags temporal @name late @memberof Pattern @param {number | Pattern} cycles number of cycles to nudge right @returns Pattern @example "bd ~".stack("hh ~".late(.1)).s()
Plays a portion of a pattern, specified by the beginning and end of a time span. The new resulting pattern is played over the time period of the original pattern:
@tags temporal @example s("bd2 hh3 [sd bd]2 perc").zoom(0.25, 0.75) // s("hh3 [sd bd]*2") // equivalent
2268export const zoom = register('zoom', function (s, e, pat) { 2269 e = Fraction(e); 2270 s = Fraction(s); 2271 if (s.gte(e)) { 2272 return nothing; 2273 } 2274 const d = e.sub(s); 2275 const steps = __steps ? pat._steps?.mulmaybe(d) : undefined; 2276 return pat 2277 .withQuerySpan((span) => span.withCycle((t) => t.mul(d).add(s))) 2278 .withHapSpan((span) => span.withCycle((t) => t.sub(s).div(d))) 2279 .splitQueries() 2280 .setSteps(steps); 2281});
Splits a pattern into the given number of slices, and plays them according to a pattern of slice numbers.
Similar to slice, but slices up patterns rather than sound samples.
@tags temporal
@param {number} number of slices
@param {number} slices to play
@example
note("0 1 2 3 4 5 6 7".scale('c:mixolydian'))
.bite(4, "3 2 1 0")
@example
sound("bd - bd bd*2, - sd:6 - sd:5 sd:1 - [- sd:2] -, hh [- cp:7]")
.bank("RolandTR909").speed(1.2)
.bite(4, "0 0 [1 2] <3 2> 0 0 [2 1] 3")
2301export const bite = register( 2302 'bite', 2303 (npat, ipat, pat) => { 2304 return ipat 2305 .fmap((i) => (n) => { 2306 const a = Fraction(i).div(n).mod(1); 2307 const b = a.add(Fraction(1).div(n)); 2308 return pat.zoom(a, b); 2309 }) 2310 .appLeft(npat) 2311 .squeezeJoin(); 2312 }, 2313 false, 2314);
Selects the given fraction of the pattern and repeats that part to fill the remainder of the cycle. @tags temporal @param {number} fraction fraction to select @example s("lt ht mt cp, [hh oh]*2").linger("<1 .5 .25 .125>")
Samples the pattern at a rate of n events per cycle. Useful for turning a continuous pattern into a discrete one. @tags temporal @name segment @synonyms seg @param {number} segments number of segments per cycle @example note(saw.range(40,52).segment(24))
The function swingBy x n breaks each cycle into n slices, and then delays events in the second half of each slice by the amount x, which is relative to the size of the (half) slice. So if x is 0 it does nothing, 0.5 delays for half the note duration, and 1 will wrap around to doing nothing again. The end result is a shuffle or swing-like rhythm
@tags temporal
@param {number} subdivision
@param {number} offset
@example
s("hh*8").swingBy(1/3, 4)
2358export const swingBy = register('swingBy', (swing, n, pat) => pat.inside(n, late(seq(0, swing / 2))));
Shorthand for swingBy with 1/3: @tags temporal @param {number} subdivision @example s("hh8").swing(4) // s("hh8").swingBy(1/3, 4)
2368export const swing = register('swing', (n, pat) => pat.swingBy(1 / 3, n));
Swaps 1s and 0s in a binary pattern. @tags temporal @name invert @synonyms inv @example s("bd").struct("1 0 0 1 0 0 1 0".lastOf(4, invert))
Applies the given function whenever the given pattern is in a true state. @tags temporal @name when @memberof Pattern @param {Pattern} binary_pat @param {function} func @returns Pattern @example "c3 eb3 g3".when("<0 1>/2", x=>x.sub("5")).note()
Superimposes the function result on top of the original pattern, delayed by the given time. @tags temporal @name off @memberof Pattern @param {Pattern | number} time offset time @param {function} func function to apply @returns Pattern @example "c3 eb3 g3".off(1/8, x=>x.add(7)).note()
Returns a new pattern where every other cycle is played once, twice as fast, and offset in time by one quarter of a cycle. Creates a kind of breakbeat feel. @tags temporal @returns Pattern
Reverse all cycles in a pattern. See also revv for reversing a whole pattern.
@tags temporal @name rev @memberof Pattern @returns Pattern @example note("c d e g").rev()
2439export const rev = register( 2440 'rev', 2441 function (pat) { 2442 const query = function (state) { 2443 const span = state.span; 2444 const cycle = span.begin.sam(); 2445 const next_cycle = span.begin.nextSam(); 2446 const reflect = function (to_reflect) { 2447 const reflected = to_reflect.withTime((time) => cycle.add(next_cycle.sub(time))); 2448 // [reflected.begin, reflected.end] = [reflected.end, reflected.begin] -- didn't work 2449 const tmp = reflected.begin; 2450 reflected.begin = reflected.end; 2451 reflected.end = tmp; 2452 return reflected; 2453 }; 2454 const haps = pat.query(state.setSpan(reflect(span))); 2455 return haps.map((hap) => hap.withSpan(reflect)); 2456 }; 2457 return new Pattern(query).splitQueries(); 2458 }, 2459 false, 2460 true, 2461);
Reverse a whole pattern. See also rev for reversing each cycle.
@name revv
@tags temporal
@memberof Pattern
@returns Pattern
@example
// This is the same as <[g e] [d c]>. If rev() is used, you get
// the same as <[d c] [g e]>, where each cycle reverses, but the order of
// cycles stays the same.
note("<[c d] [e g]>").revv()
Like press, but allows you to specify the amount by which each event is shifted. pressBy(0.5) is the same as press, while pressBy(1/3) shifts each event by a third of its timespan. @tags temporal @example stack(s("hh*4"), s("bd mt sd ht").pressBy("<0 0.5 0.25>") ).slow(2)
Syncopates a rhythm, by shifting each event halfway into its timespan. @tags temporal @example stack(s("hh*4"), s("bd mt sd ht").every(4, press) ).slow(2)
Silences a pattern. @tags temporal @example stack( s("bd").hush(), s("hh*3") )
Applies rev to a pattern every other cycle, so that the pattern alternates between forwards and backwards.
@tags temporal
@example
note("c d e g").palindrome()
Jux with adjustable stereo width. 0 = mono, 1 = full stereo. @tags temporal @name juxBy @synonyms juxby @example s("bd lt [~ ht] mt cp ~ bd hh").juxBy("<0 .5 1>/2", rev)
2542export const { juxBy, juxby } = register(['juxBy', 'juxby'], function (by, func, pat) { 2543 by /= 2; 2544 const elem_or = function (dict, key, dflt) { 2545 if (key in dict) { 2546 return dict[key]; 2547 } 2548 return dflt; 2549 }; 2550 const left = pat.withValue((val) => Object.assign({}, val, { pan: elem_or(val, 'pan', 0.5) - by })); 2551 const right = func(pat.withValue((val) => Object.assign({}, val, { pan: elem_or(val, 'pan', 0.5) + by }))); 2552 2553 return stack(left, right).setSteps(__steps ? lcm(left._steps, right._steps) : undefined); 2554});
Like juxBy, except it flips the ears each cycle. @name juxFlipBy @synonyms juxflipby, fluxBy, fluxby @example s("bd lt [~ ht] mt cp ~ bd hh").juxFlipBy(".8", rev)
The jux function creates strange stereo effects, by applying a function to a pattern, but only in the right-hand channel. @tags temporal, superdough @example s("bd lt [~ ht] mt cp ~ bd hh").jux(rev) @example s("bd lt [~ ht] mt cp ~ bd hh").jux(press) @example s("bd lt [~ ht] mt cp ~ bd hh").jux(iter(4))
Like jux, but flips the ears each cycle. @name juxFlip @synonyms juxflip, flux @example s("bd lt [~ ht] mt cp ~ bd hh").juxFlip(rev) @example s("bd lt [~ ht] mt cp ~ bd hh").juxFlip(press) @example s("bd lt [~ ht] mt cp ~ bd hh").juxFlip(iter(4))
Superimpose and offset multiple times, applying the given function each time. @tags temporal, functional @name echoWith @synonyms echowith, stutWith, stutwith @param {number} times how many times to repeat @param {number} time cycle offset between iterations @param {function} func function to apply, given the pattern and the iteration index @example "<0 [2 4]>" .echoWith(4, 1/8, (p,n) => p.add(n*2)) .scale("C:minor").note()
Superimpose and offset multiple times, gradually decreasing the velocity @tags temporal @name echo @memberof Pattern @returns Pattern @param {number} times how many times to repeat @param {number} time cycle offset between iterations @param {number} feedback velocity multiplicator for each iteration @example s("bd sd").echo(3, 1/6, .8)
Deprecated. Like echo, but the last 2 parameters are flipped. @tags temporal @name stut @param {number} times how many times to repeat @param {number} feedback velocity multiplicator for each iteration @param {number} time cycle offset between iterations @example s("bd sd").stut(3, .8, 1/6)
The plyWith function repeats each event the given number of times, applying the given function to each event.\n @tags temporal @name plyWith @synonyms plywith @param {number} factor how many times to repeat @param {function} func function to apply, given the pattern @example "<0 [2 4]>" .plyWith(4, (p) => p.add(2)) .scale("C:minor").note()
2669export const plyWith = register(['plyWith', 'plywith'], function (factor, func, pat) { 2670 const result = pat 2671 .fmap((x) => cat(...listRange(0, factor - 1).map((i) => applyN(i, func, x)))._fast(factor)) 2672 .squeezeJoin(); 2673 if (__steps) { 2674 result._steps = Fraction(factor).mulmaybe(pat._steps); 2675 } 2676 return result; 2677});
The plyForEach function repeats each event the given number of times, applying the given function to each event. This version of ply uses the iteration index as an argument to the function, similar to echoWith. @tags temporal @name plyForEach @synonyms plyforeach @param {number} factor how many times to repeat @param {function} func function to apply, given the pattern and the iteration index @example "<0 [2 4]>" .plyForEach(4, (p,n) => p.add(n*2)) .scale("C:minor").note()
2692export const plyForEach = register(['plyForEach', 'plyforeach'], function (factor, func, pat) { 2693 const result = pat 2694 .fmap((x) => cat(cat(pure(x), ...listRange(1, factor - 1).map((i) => func(pure(x), i))))._fast(factor)) 2695 .squeezeJoin(); 2696 if (__steps) { 2697 result._steps = Fraction(factor).mulmaybe(pat._steps); 2698 } 2699 return result; 2700});
Divides a pattern into a given number of subdivisions, plays the subdivisions in order, but increments the starting subdivision each cycle. The pattern wraps to the first subdivision after the last subdivision is played. @tags temporal @name iter @memberof Pattern @returns Pattern @example note("0 1 2 3".scale('A minor')).iter(4)
Like iter, but plays the subdivisions in reverse order. Known as iter' in tidalcycles
@tags temporal
@name iterBack
@synonyms iterback
@memberof Pattern
@returns Pattern
@example
note("0 1 2 3".scale('A minor')).iterBack(4)
Repeats each cycle the given number of times. @tags temporal @name repeatCycles @memberof Pattern @returns Pattern @example note(irand(12).add(34)).segment(4).repeatCycles(2).s("gm_acoustic_guitar_nylon")
2758export const { repeatCycles } = register( 2759 'repeatCycles', 2760 function (n, pat) { 2761 return new Pattern(function (state) { 2762 const cycle = state.span.begin.sam(); 2763 const source_cycle = cycle.div(n).sam(); 2764 const delta = cycle.sub(source_cycle); 2765 state = state.withSpan((span) => span.withTime((spant) => spant.sub(delta))); 2766 return pat.query(state).map((hap) => hap.withSpan((span) => span.withTime((spant) => spant.add(delta)))); 2767 }).splitQueries(); 2768 }, 2769 true, 2770 true, 2771);
Divides a pattern into a given number of parts, then cycles through those parts in turn, applying the given function to each part in turn (one part per cycle). @tags temporal, functional @name chunk @synonyms slowChunk, slowchunk @memberof Pattern @returns Pattern @example "0 1 2 3".chunk(4, x=>x.add(7)) .scale("A:minor").note()
2784const _chunk = function (n, func, pat, back = false, fast = false) { 2785 const binary = Array(n - 1).fill(false); 2786 binary.unshift(true); 2787 // Invert the 'back' because we want to shift the pattern forwards, 2788 // and so time backwards 2789 const binary_pat = _iter(n, sequence(...binary), !back); 2790 if (!fast) { 2791 pat = pat.repeatCycles(n); 2792 } 2793 return pat.when(binary_pat, func); 2794};
Like chunk, but cycles through the parts in reverse order. Known as chunk' in tidalcycles
@tags temporal
@name chunkBack
@synonyms chunkback
@memberof Pattern
@returns Pattern
@example
"0 1 2 3".chunkBack(4, x=>x.add(7))
.scale("A:minor").note()
Like chunk, but the cycles of the source pattern aren't repeated
for each set of chunks.
@tags temporal
@name fastChunk
@synonyms fastchunk
@memberof Pattern
@returns Pattern
@example
"<0 8> 1 2 3 4 5 6 7"
.scale("C2:major").note()
.fastChunk(4, x => x.color('red')).slow(2)
Like chunk, but the function is applied to a looped subcycle of the source pattern.
@tags temporal
@name chunkInto
@synonyms chunkinto
@memberof Pattern
@example
sound("bd sd ht lt bd - cp lt").chunkInto(4, hurry(2))
.bank("tr909")
Like chunkInto, but moves backwards through the chunks.
@tags temporal
@name chunkBackInto
@synonyms chunkbackinto
@memberof Pattern
@example
sound("bd sd ht lt bd - cp lt").chunkInto(4, hurry(2))
.bank("tr909")
TODO - redefine elsewhere in terms of mask
Loops the pattern inside an offset for cycles.
If you think of the entire span of time in cycles as a ribbon, you can cut a single piece and loop it.
@tags temporal
@name ribbon
@synonyms rib
@param {number} offset start point of loop in cycles
@param {number} cycles loop length in cycles
@example
note("<c d e f>").ribbon(1, 2)
@example
// Looping a portion of randomness
n(irand(8).segment(4)).scale("c:pentatonic").ribbon(1337, 2)
@example
// rhythm generator
s("bd!16?").ribbon(29,.5)
Tags each Hap with an identifier. Good for filtering. The function populates Hap.context.tags (Array). @name tag @tags temporal @param {string} tag anything unique @example s("saw!16").note("F1") .lpf(tri.range(40, 80).slow(4)).lpenv(5).lpq(4).lpd(0.15) .when(rand.late(0.1).gte(0.5), x => x.transpose("12").tag('altered')) .when(rand.late(0.2).gte(0.5), x => x.s("square").tag('altered')) .when("<0 1>", x => x.filter((hap) => hap.hasTag('altered')))
Filters haps using the given function @name filter @tags temporal, functional @param {Function} test function to test Hap @example s("hh!7 oh").filter(hap => hap.value.s === 'hh')
2944export const filter = register('filter', (test, pat) => pat.withHaps((haps) => haps.filter(test)));
Filters haps by their begin time @name filterWhen @tags temporal, functional @param {Function} test function to test Hap.whole.begin @example oneCycle: s("bd*4").filterWhen((t) => t < 1)
2954export const filterWhen = register('filterWhen', (test, pat) => pat.filter((h) => test(h.whole.begin)));
Use within to apply a function to only a part of a pattern. @name within @tags temporal, functional @param {number} start start within cycle (0 - 1) @param {number} end end within cycle (0 - 1). Must be > start @param {Function} func function to be applied to the sub-pattern
//////////////////////////////////////////////////////////////////// Stepwise functions
2974Pattern.prototype.stepJoin = function () { 2975 const pp = this; 2976 const first_t = stepcat(..._retime(_slices(pp.queryArc(0, 1))))._steps; 2977 const q = function (state) { 2978 const shifted = pp.early(state.span.begin.sam()); 2979 const haps = shifted.query(state.setSpan(new TimeSpan(Fraction(0), Fraction(1)))); 2980 const pat = stepcat(..._retime(_slices(haps))); 2981 return pat.query(state); 2982 }; 2983 return new Pattern(q, first_t); 2984};
2986Pattern.prototype.stepBind = function (func) { 2987 return this.fmap(func).stepJoin(); 2988}; 2989 2990export function _retime(timedHaps) { 2991 const occupied_perc = timedHaps.filter((t, pat) => pat.hasSteps).reduce((a, b) => a.add(b), Fraction(0)); 2992 const occupied_steps = removeUndefineds(timedHaps.map((t, pat) => pat._steps)).reduce( 2993 (a, b) => a.add(b), 2994 Fraction(0), 2995 ); 2996 const total_steps = occupied_perc.eq(0) ? undefined : occupied_steps.div(occupied_perc); 2997 function adjust(dur, pat) { 2998 if (pat._steps === undefined) { 2999 return [dur.mulmaybe(total_steps), pat]; 3000 } 3001 return [pat._steps, pat]; 3002 } 3003 return timedHaps.map((x) => adjust(...x)); 3004} 3005 3006export function _slices(haps) { 3007 // slices evs = map (\s -> ((snd s - fst s), stack $ map value $ fit s evs)) 3008 // $ pairs $ sort $ nubOrd $ 0:1:concatMap (\ev -> start (part ev):stop (part ev):[]) evs 3009 const breakpoints = flatten(haps.map((hap) => [hap.part.begin, hap.part.end])); 3010 const unique = uniqsortr([Fraction(0), Fraction(1), ...breakpoints]); 3011 const slicespans = pairs(unique); 3012 return slicespans.map((s) => [ 3013 s[1].sub(s[0]), 3014 stack(..._fitslice(new TimeSpan(...s), haps).map((x) => x.value.withHap((h) => h.setContext(h.combineContext(x))))), 3015 ]); 3016} 3017 3018export function _fitslice(span, haps) { 3019 return removeUndefineds(haps.map((hap) => _match(span, hap))); 3020} 3021 3022export function _match(span, hap_p) { 3023 const subspan = span.intersection(hap_p.part); 3024 if (subspan == undefined) { 3025 return undefined; 3026 } 3027 return new Hap(hap_p.whole, subspan, hap_p.value, hap_p.context); 3028}
Experimental
Speeds a pattern up or down, to fit to the given number of steps per cycle. @tags stepwise @example sound("bd sd cp").pace(4) // The same as sound("{bd sd cp}%4") or sound("<bd sd cp>*4")
3039export const pace = register('pace', function (targetSteps, pat) { 3040 if (pat._steps === undefined) { 3041 return pat; 3042 } 3043 if (pat._steps.eq(Fraction(0))) { 3044 // avoid divide by zero.. 3045 return nothing; 3046 } 3047 return pat._fast(Fraction(targetSteps).div(pat._steps)).setSteps(targetSteps); 3048});
3050export function _polymeterListSteps(steps, ...args) { 3051 const seqs = args.map((a) => _sequenceCount(a)); 3052 if (seqs.length == 0) { 3053 return silence; 3054 } 3055 if (steps == 0) { 3056 steps = seqs[0][1]; 3057 } 3058 const pats = []; 3059 for (const seq of seqs) { 3060 if (seq[1] == 0) { 3061 continue; 3062 } 3063 if (steps == seq[1]) { 3064 pats.push(seq[0]); 3065 } else { 3066 pats.push(seq[0]._fast(Fraction(steps).div(Fraction(seq[1])))); 3067 } 3068 } 3069 return stack(...pats); 3070}
Experimental
Aligns the steps of the patterns, creating polymeters. The patterns are repeated until they all fit the cycle. For example, in the below the first pattern is repeated twice, and the second is repeated three times, to fit the lowest common multiple of six steps. @tags stepwise @synonyms pm @example // The same as note("{c eb g, c2 g2}%6") polymeter("c eb g", "c2 g2").note()
TODO currently ignoring arguments without steps...
3090 args = args.filter((arg) => arg.hasSteps);
'Concatenates' patterns like fastcat, but proportional to a number of steps per cycle.
The steps can either be inferred from the pattern, or provided as a [length, pattern] pair.
Has the alias timecat.
@name stepcat
@tags stepwise
@synonyms timeCat, timecat
@return {Pattern}
@example
stepcat([3,"e3"],[1, "g3"]).note()
// the same as "e3@3 g3".note()
@example
stepcat("bd sd cp","hh hh").sound()
// the same as "bd sd cp hh hh".sound()
3119export function stepcat(...timepats) { 3120 if (timepats.length === 0) { 3121 return nothing; 3122 } 3123 const findsteps = (x) => (Array.isArray(x) ? x : [x._steps ?? 1, x]); 3124 timepats = timepats.map(findsteps); 3125 if (timepats.find((x) => x[0] === undefined)) { 3126 const times = timepats.map((a) => a[0]).filter((x) => x !== undefined); 3127 if (times.length === 0) { 3128 return fastcat(...timepats.map((x) => x[1])); 3129 } 3130 if (times.length === timepats.length) { 3131 return nothing; 3132 } 3133 const avg = times.reduce((a, b) => a.add(b), Fraction(0)).div(times.length); 3134 for (let timepat of timepats) { 3135 if (timepat[0] === undefined) { 3136 timepat[0] = avg; 3137 } 3138 } 3139 } 3140 if (timepats.length == 1) { 3141 const result = reify(timepats[0][1]); 3142 return result.withSteps((_) => timepats[0][0]); 3143 } 3144 3145 const total = timepats.map((a) => a[0]).reduce((a, b) => a.add(b), Fraction(0)); 3146 let begin = Fraction(0); 3147 const pats = []; 3148 for (const [time, pat] of timepats) { 3149 if (Fraction(time).eq(0)) { 3150 continue; 3151 } 3152 const end = begin.add(time); 3153 pats.push(reify(pat)._compress(begin.div(total), end.div(total))); 3154 begin = end; 3155 } 3156 const result = stack(...pats); 3157 result._steps = total; 3158 return result; 3159}
Experimental
Concatenates patterns stepwise, according to an inferred 'steps per cycle'.
Similar to stepcat, but if an argument is a list, the whole pattern will alternate between the elements in the list.
@tags stepwise @return {Pattern} @example stepalt(["bd cp", "mt"], "bd").sound() // The same as "bd cp bd mt bd".sound()
3173export function stepalt(...groups) { 3174 groups = groups.map((a) => (Array.isArray(a) ? a.map(reify) : [reify(a)])); 3175 3176 const cycles = lcm(...groups.map((x) => Fraction(x.length))); 3177 3178 let result = []; 3179 for (let cycle = 0; cycle < cycles; ++cycle) { 3180 result.push(...groups.map((x) => (x.length == 0 ? silence : x[cycle % x.length]))); 3181 } 3182 result = result.filter((x) => x.hasSteps && x._steps > 0); 3183 const steps = result.reduce((a, b) => a.add(b._steps), Fraction(0)); 3184 result = stepcat(...result); 3185 result._steps = steps; 3186 return result; 3187}
Experimental
Takes the given number of steps from a pattern (dropping the rest). A positive number will take steps from the start of a pattern, and a negative number from the end. @tags stepwise @return {Pattern} @example "bd cp ht mt".take("2").sound() // The same as "bd cp".sound() @example "bd cp ht mt".take("1 2 3").sound() // The same as "bd bd cp bd cp ht".sound() @example "bd cp ht mt".take("-1 -2 -3").sound() // The same as "mt ht mt cp ht mt".sound()
3206export const take = stepRegister('take', function (i, pat) { 3207 if (!pat.hasSteps) { 3208 return nothing; 3209 } 3210 if (pat._steps.lte(0)) { 3211 return nothing; 3212 } 3213 i = Fraction(i); 3214 if (i.eq(0)) { 3215 return nothing; 3216 } 3217 const flip = i < 0; 3218 if (flip) { 3219 i = i.abs(); 3220 } 3221 const frac = i.div(pat._steps); 3222 if (frac.lte(0)) { 3223 return nothing; 3224 } 3225 if (frac.gte(1)) { 3226 return pat; 3227 } 3228 if (flip) { 3229 return pat.zoom(Fraction(1).sub(frac), 1); 3230 } 3231 return pat.zoom(0, frac); 3232});
Experimental
Drops the given number of steps from a pattern. A positive number will drop steps from the start of a pattern, and a negative number from the end. @tags stepwise @return {Pattern} @example "tha dhi thom nam".drop("1").sound().bank("mridangam") @example "tha dhi thom nam".drop("-1").sound().bank("mridangam") @example "tha dhi thom nam".drop("0 1 2 3").sound().bank("mridangam") @example "tha dhi thom nam".drop("0 -1 -2 -3").sound().bank("mridangam")
Experimental
extend is similar to fast in that it increases its density, but it also increases the step count
accordingly. So stepcat("a b".extend(2), "c d") would be the same as "a b a b c d", whereas
stepcat("a b".fast(2), "c d") would be the same as "[a b] [a b] c d".
@tags stepwise
@example
stepcat(
sound("bd bd - cp").extend(2),
sound("bd - sd -")
).pace(8)
Experimental
replicate is similar to fast in that it increases its density, but it also increases the step count
accordingly. So stepcat("a b".replicate(2), "c d") would be the same as "a b a b c d", whereas
stepcat("a b".fast(2), "c d") would be the same as "[a b] [a b] c d".
TODO: find out how this function differs from extend @tags stepwise @example stepcat( sound("bd bd - cp").replicate(2), sound("bd - sd -") ).pace(8)
Experimental
Expands the step size of the pattern by the given factor. @tags stepwise @example sound("tha dhi thom nam").bank("mridangam").expand("3 2 1 1 2 3").pace(8)
Experimental
Contracts the step size of the pattern by the given factor. See also expand.
@tags stepwise
@example
sound("tha dhi thom nam").bank("mridangam").contract("3 2 1 1 2 3").pace(8)
3322Pattern.prototype.shrinklist = function (amount) { 3323 const pat = this; 3324 3325 if (!pat.hasSteps) { 3326 return [pat]; 3327 } 3328 3329 let [amountv, times] = Array.isArray(amount) ? amount : [amount, pat._steps]; 3330 amountv = Fraction(amountv); 3331 3332 if (times === 0 || amountv === 0) { 3333 return [pat]; 3334 } 3335 3336 const fromstart = amountv > 0; 3337 const ranges = []; 3338 if (fromstart) { 3339 const seg = Fraction(1).div(pat._steps).mul(amountv); 3340 for (let i = 0; i < times; ++i) { 3341 const s = seg.mul(i); 3342 if (s.gt(1)) { 3343 break; 3344 } 3345 ranges.push([s, 1]); 3346 } 3347 } else { 3348 amountv = Fraction(0).sub(amountv); 3349 const seg = Fraction(1).div(pat._steps).mul(amountv); 3350 for (let i = 0; i < times; ++i) { 3351 const e = Fraction(1).sub(seg.mul(i)); 3352 if (e.lt(0)) { 3353 break; 3354 } 3355 ranges.push([Fraction(0), e]); 3356 } 3357 } 3358 return ranges.map((x) => pat.zoom(...x)); 3359}; 3360 3361export const shrinklist = (amount, pat) => pat.shrinklist(amount); 3362 3363Pattern.prototype.growlist = function (amount) { 3364 return this.shrinklist(amount).reverse(); 3365}; 3366export const growlist = (amount, pat) => pat.growlist(amount);
Experimental
Progressively shrinks the pattern by 'n' steps until there's nothing left, or if a second value is given (using mininotation list syntax with :),
that number of times.
A positive number will progressively drop steps from the start of a pattern, and a negative number from the end.
@tags stepwise
@return {Pattern}
@example
"tha dhi thom nam".shrink("1").sound()
.bank("mridangam")
@example
"tha dhi thom nam".shrink("-1").sound()
.bank("mridangam")
@example
"tha dhi thom nam".shrink("1 -1").sound().bank("mridangam").pace(4)
@example
note("0 1 2 3 4 5 6 7".scale("C:ritusen")).sound("folkharp")
.shrink("1 -1").pace(8)
3390export const shrink = register( 3391 'shrink', 3392 function (amount, pat) { 3393 if (!pat.hasSteps) { 3394 return nothing; 3395 } 3396 3397 const list = pat.shrinklist(amount); 3398 const result = stepcat(...list); 3399 // TODO is this calculation needed? 3400 result._steps = list.reduce((a, b) => a.add(b._steps), Fraction(0)); 3401 return result; 3402 }, 3403 true, 3404 false, 3405 (x) => x.stepJoin(), 3406);
Experimental
Progressively grows the pattern by 'n' steps until the full pattern is played, or if a second value is given (using mininotation list syntax with :),
that number of times.
A positive number will progressively grow steps from the start of a pattern, and a negative number from the end.
@tags stepwise
@return {Pattern}
@example
"tha dhi thom nam".grow("1").sound()
.bank("mridangam")
@example
"tha dhi thom nam".grow("-1").sound()
.bank("mridangam")
@example
"tha dhi thom nam".grow("1 -1").sound().bank("mridangam").pace(4)
@example
note("0 1 2 3 4 5 6 7".scale("C:ritusen")).sound("folkharp")
.grow("1 -1").pace(8)
3429export const grow = register( 3430 'grow', 3431 function (amount, pat) { 3432 if (!pat.hasSteps) { 3433 return nothing; 3434 } 3435 3436 const list = pat.shrinklist(Fraction(0).sub(amount)); 3437 list.reverse(); 3438 const result = stepcat(...list); 3439 // TODO is this calculation needed? 3440 result._steps = list.reduce((a, b) => a.add(b._steps), Fraction(0)); 3441 return result; 3442 }, 3443 true, 3444 false, 3445 (x) => x.stepJoin(), 3446);
Experimental
Inserts a pattern into a list of patterns. On the first repetition it will be inserted at the end of the list, then moved backwards through the list
on successive repetitions. The patterns are added together stepwise, with all repetitions taking place over a single cycle. Using pace to set the
number of steps per cycle is therefore usually recommended.
@tags stepwise @return {Pattern} @example "[c g]".tour("e f", "e f g", "g f e c").note() .sound("folkharp") .pace(8)
Experimental
'zips' together the steps of the provided patterns. This can create a long repetition, taking place over a single, dense cycle.
Using pace to set the number of steps per cycle is therefore usually recommended.
@tags stepwise @returns {Pattern} @example zip("e f", "e f g", "g [f e] a f4 c").note() .sound("folkharp") .pace(8)
Deprecated stepwise aliases
3501export const s_cat = stepcat; 3502export const s_alt = stepalt; 3503export const s_polymeter = polymeter; 3504Pattern.prototype.s_polymeter = Pattern.prototype.polymeter; 3505export const s_taper = shrink; 3506Pattern.prototype.s_taper = Pattern.prototype.shrink; 3507export const s_taperlist = shrinklist; 3508Pattern.prototype.s_taperlist = Pattern.prototype.shrinklist; 3509export const s_add = take; 3510Pattern.prototype.s_add = Pattern.prototype.take; 3511export const s_sub = drop; 3512Pattern.prototype.s_sub = Pattern.prototype.drop; 3513export const s_expand = expand; 3514Pattern.prototype.s_expand = Pattern.prototype.expand; 3515export const s_extend = extend; 3516Pattern.prototype.s_extend = Pattern.prototype.extend; 3517export const s_contract = contract; 3518Pattern.prototype.s_contract = Pattern.prototype.contract; 3519export const s_tour = tour; 3520Pattern.prototype.s_tour = Pattern.prototype.tour; 3521export const s_zip = zip; 3522Pattern.prototype.s_zip = Pattern.prototype.zip; 3523export const steps = pace; 3524Pattern.prototype.steps = Pattern.prototype.pace;
//////////////////////////////////////////////////////////////////// Control-related functions, i.e. ones that manipulate patterns of objects
Cuts each sample into the given number of parts, allowing you to explore a technique known as 'granular synthesis'. It turns a pattern of samples into a pattern of parts of samples. @name chop @tags samples @memberof Pattern @returns Pattern @example samples({ rhodes: 'https://cdn.freesound.org/previews/132/132051_316502-lq.mp3' }) s("rhodes") .chop(4) .rev() // reverse order of chops .loopAt(2) // fit sample into 2 cycles
3545export const chop = register('chop', function (n, pat) { 3546 const slices = Array.from({ length: n }, (x, i) => i); 3547 const slice_objects = slices.map((i) => ({ begin: i / n, end: (i + 1) / n })); 3548 const merge = function (a, b) { 3549 if ('begin' in a && 'end' in a && a.begin !== undefined && a.end !== undefined) { 3550 const d = a.end - a.begin; 3551 b = { begin: a.begin + b.begin * d, end: a.begin + b.end * d }; 3552 } 3553 // return a; 3554 return Object.assign({}, a, b); 3555 }; 3556 const func = function (o) { 3557 return sequence(slice_objects.map((slice_o) => merge(o, slice_o))); 3558 }; 3559 return pat.squeezeBind(func).setSteps(__steps ? Fraction(n).mulmaybe(pat._steps) : undefined); 3560});
Cuts each sample into the given number of parts, triggering progressive portions of each sample at each loop. @name striate @tags samples @memberof Pattern @returns Pattern @example s("numbers:0 numbers:1 numbers:2").striate(6).slow(3)
3571export const striate = register('striate', function (n, pat) { 3572 const slices = Array.from({ length: n }, (x, i) => i); 3573 const slice_objects = slices.map((i) => ({ begin: i / n, end: (i + 1) / n })); 3574 const slicePat = slowcat(...slice_objects); 3575 return pat 3576 .set(slicePat) 3577 ._fast(n) 3578 .setSteps(__steps ? Fraction(n).mulmaybe(pat._steps) : undefined); 3579});
Makes the sample fit the given number of cycles by changing the speed. @name loopAt @tags samples, pitch @memberof Pattern @returns Pattern @example samples({ rhodes: 'https://cdn.freesound.org/previews/132/132051_316502-lq.mp3' }) s("rhodes").loopAt(2)
Chops samples into the given number of slices, triggering those slices with a given pattern of slice numbers. Instead of a number, it also accepts a list of numbers from 0 to 1 to slice at specific points. @name slice @tags samples @memberof Pattern @returns Pattern @example samples('github:tidalcycles/dirt-samples') s("breaks165").slice(8, "0 1 <2 2*2> 3 [4 0] 5 6 7".every(3, rev)).slow(0.75) @example samples('github:tidalcycles/dirt-samples') s("breaks125").fit().slice([0,.25,.5,.75], "0 1 1 <2 3>")
3618export const slice = register( 3619 'slice', 3620 function (npat, ipat, opat) { 3621 return npat 3622 .innerBind((n) => 3623 ipat.outerBind((i) => 3624 opat.outerBind((o) => { 3625 // If it's not an object, assume it's a string and make it a 's' control parameter 3626 o = o instanceof Object ? o : { s: o }; 3627 const begin = Array.isArray(n) ? n[i] : i / n; 3628 const end = Array.isArray(n) ? n[i + 1] : (i + 1) / n; 3629 return pure({ begin, end, _slices: n, ...o }); 3630 }), 3631 ), 3632 ) 3633 .setSteps(ipat._steps); 3634 }, 3635 false, // turns off auto-patternification 3636);
make something happen on event time uses browser timeout which is innacurate for audio tasks @name onTriggerTime @tags external_io @memberof Pattern @returns Pattern @example s("bd!8").onTriggerTime((hap) => {console.log(hap)})
Works the same as slice, but changes the playback speed of each slice to match the duration of its step. @name splice @tags samples, pitch @example samples('github:tidalcycles/dirt-samples') s("breaks165") .splice(8, "0 1 [2 3 0]@2 3 0@2 7")
3668export const splice = register( 3669 'splice', 3670 function (npat, ipat, opat) { 3671 const sliced = slice(npat, ipat, opat); 3672 return new Pattern((state) => { 3673 // TODO - default cps to 0.5 3674 const cps = state.controls._cps || 1; 3675 const haps = sliced.query(state); 3676 return haps.map((hap) => 3677 hap.withValue((v) => ({ 3678 ...{ 3679 speed: (cps / v._slices / hap.whole.duration) * (v.speed || 1), 3680 unit: 'c', 3681 }, 3682 ...v, 3683 })), 3684 ); 3685 }).setSteps(ipat._steps); 3686 }, 3687 false, // turns off auto-patternification 3688);
Makes the sample fit its event duration. Good for rhythmical loops like drum breaks.
Similar to loopAt.
@name fit
@tags samples, pitch
@example
samples({ rhodes: 'https://cdn.freesound.org/previews/132/132051_316502-lq.mp3' })
s("rhodes/2").fit()
3699export const fit = register('fit', (pat) => 3700 pat.withHaps((haps, state) => 3701 haps.map((hap) => 3702 hap.withValue((v) => { 3703 const slicedur = ('end' in v ? v.end : 1) - ('begin' in v ? v.begin : 0); 3704 return { 3705 ...v, 3706 speed: ((state.controls._cps || 1) / hap.whole.duration) * slicedur, 3707 unit: 'c', 3708 }; 3709 }), 3710 ), 3711 ), 3712);
Makes the sample fit the given number of cycles and cps value, by changing the speed. deprecated: use loopAt or fit instead, together with setCps / setCpm. @name loopAtCps @tags samples, pitch @memberof Pattern @deprecated @returns Pattern @example samples({ rhodes: 'https://cdn.freesound.org/previews/132/132051_316502-lq.mp3' }) s("rhodes").loopAtCps(4,1.5).cps(1.5)
exposes a custom value at query time. basically allows mutating state without evaluation @tags internals
3738let fadeGain = (p) => (p < 0.5 ? 1 : 1 - (p - 0.5) / 0.5);
Cross-fades between left and right from 0 to 1:
- 0 = (full left, no right)
- .5 = (both equal)
- 1 = (no left, full right)
@name xfade @tags amplitude @example xfade(s("bd2"), "<0 .25 .5 .75 1>", s("hh8"))
the prototype version is actually flipped so left/right makes sense
creates a structure pattern from divisions of a cycle especially useful for creating rhythms @name beat @tags temporal @example s("bd").beat("0,7,10", 16) @example s("sd").beat("4,12", 16)
3783export const { beat } = register( 3784 ['beat'], 3785 __beat((x) => x.innerJoin()), 3786); 3787 3788export const _morph = (from, to, by) => { 3789 by = Fraction(by); 3790 const dur = Fraction(1).div(from.length); 3791 const positions = (list) => { 3792 const result = []; 3793 for (const [pos, value] of list.entries()) { 3794 if (value) { 3795 result.push([Fraction(pos).div(list.length), value]); 3796 } 3797 } 3798 return result; 3799 }; 3800 const arcs = zipWith( 3801 ([posa, valuea], [posb, valueb]) => { 3802 const b = by.mul(posb - posa).add(posa); 3803 const e = b.add(dur); 3804 return new TimeSpan(b, e); 3805 }, 3806 positions(from), 3807 positions(to), 3808 ); 3809 function query(state) { 3810 const cycle = state.span.begin.sam(); 3811 const cycleArc = state.span.cycleArc(); 3812 const result = []; 3813 for (const whole of arcs) { 3814 const part = whole.intersection(cycleArc); 3815 if (part !== undefined) { 3816 result.push( 3817 new Hap( 3818 whole.withTime((x) => x.add(cycle)), 3819 part.withTime((x) => x.add(cycle)), 3820 true, 3821 ), 3822 ); 3823 } 3824 } 3825 return result; 3826 } 3827 return new Pattern(query).splitQueries(); 3828};
Takes two binary rhythms represented as lists of 1s and 0s, and a number between 0 and 1 that morphs between them. The two lists should contain the same number of true values. @example sound("hh").struct(morph([1,0,1,0,1,0,1,0], // straight rhythm [1,1,0,1,0,1,0], // wonky rhythm 0.25 // creates a slightly wonky rhythm ) ) @example sound("hh").struct(morph("1:0:1:0:1:0:1:0", // straight rhythm "1:1:0:1:0:1:0", // wonky rhythm sine.slow(8) // slowly morph between the rhythms ) ) @tags temporal
3855const _distortWithAlg = function (name) { 3856 const func = function (args, pat) { 3857 const argsPat = reify(args).fmap((v) => (Array.isArray(v) ? [...v, name] : [v, 1, name])); 3858 if (!pat) { 3859 return pure({}).distort(argsPat); 3860 } 3861 return pat.distort(argsPat); 3862 }; 3863 Pattern.prototype[name] = function (args) { 3864 return func(args, this); 3865 }; 3866 return func; 3867};
Soft-clipping distortion
@name soft @tags distortion, superdough @param {number | Pattern} distortion amount of distortion to apply @param {number | Pattern} volume linear postgain of the distortion
3878export const soft = _distortWithAlg('soft');
Hard-clipping distortion
@name hard @tags distortion, superdough @param {number | Pattern} distortion amount of distortion to apply @param {number | Pattern} volume linear postgain of the distortion
3889export const hard = _distortWithAlg('hard');
Cubic polynomial distortion
@name cubic @tags distortion, superdough @param {number | Pattern} distortion amount of distortion to apply @param {number | Pattern} volume linear postgain of the distortion
3900export const cubic = _distortWithAlg('cubic');
Diode-emulating distortion
@name diode @tags distortion, superdough @param {number | Pattern} distortion amount of distortion to apply @param {number | Pattern} volume linear postgain of the distortion
3911export const diode = _distortWithAlg('diode');
Asymmetrical diode distortion
@name asym @tags distortion, superdough @param {number | Pattern} distortion amount of distortion to apply @param {number | Pattern} volume linear postgain of the distortion
3922export const asym = _distortWithAlg('asym');
Wavefolding distortion
@name fold @tags distortion, superdough @param {number | Pattern} distortion amount of distortion to apply @param {number | Pattern} volume linear postgain of the distortion
3933export const fold = _distortWithAlg('fold');
Wavefolding distortion composed with sinusoid
@name sinefold @tags distortion, superdough @param {number | Pattern} distortion amount of distortion to apply @param {number | Pattern} volume linear postgain of the distortion
3944export const sinefold = _distortWithAlg('sinefold');
Distortion via Chebyshev polynomials
@name chebyshev @tags distortion, superdough @param {number | Pattern} distortion amount of distortion to apply @param {number | Pattern} volume linear postgain of the distortion
3955export const chebyshev = _distortWithAlg('chebyshev');
Turns a list of patterns into a single pattern which outputs list-values
@name parray @tags combiners @returns Pattern
Scale the magnitude of the harmonics of one of the core synths ('sine', 'tri', 'saw', ..)
Can also be used to create a new synth via s('user').partials(...)
@name partials @tags superdough @param {number[] | Pattern} magnitudes List of [0, 1] magnitudes for partials. 0th entry is the fundamental harmonic (i.e. DC offset is skipped) @example s("user").seg(16).n(irand(8)).scale("A:major") .partials([1, 0, 1, 0, 0, 1]) @example s("saw").seg(8).n(irand(12)).scale("G#:minor") .partials(binaryL(irand(256).add("1")))
Also create a top-level function
Rotates the harmonics of one of the core synths ('sine', 'tri', 'saw', 'user', ..) by a list of phases
@name phases @tags superdough @param {number[] | Pattern} phases List of [0, 1) phases for partials. 0th entry is the fundamental phase (i.e. DC offset is skipped) @example // Phase cancellation s("saw").seg(8).n(irand(12)).scale("G#1:minor") .partials(partials([1, 1, 1])) .superimpose(x => x.phases([0.5, 0.5, 0.5]))
Also create a top-level function
Establishes an FX chain. Can be called by chaining .FX(fx1).FX(fx2).. calls and/or in a single .FX(fx1, fx2, ..) call. The fx1, .. are patterns which establish the controls of the given effect. See examples. @name FX @tags superdough @memberof Pattern @returns Pattern @example $: s("[sbd <hh [bd | lt | oh]>]*4").dec(.4) .FX( phaser(0.5).gain(2), bpf(800), distort(1.3), room(0.2), delay(0.5).gain(1.25), distort(0.3), ).fxr(1.7) // sets release time of effects (like delay) @example $: s("saw").fm(0.5) .delay(0.3) // outer effects are applied last .FX(coarse(4)) // first coarse .FX(lpf(500).lpe(4).lpa(1).lpd(2)) // then lpf .FX(distort(1)) // then distort
Produces a Kabelsalat modular sound engine.
This can be used as either an effect (by including audioin() at the beginning
of your kabel expression) or as a sound source (via any expression which doesn't
start with audioin()).
Some helpers you have available to you:
- Strudel mini notation works fine in K(..) via "" or ``
- More complex Strudel expressions (like "0 1 2".fast(4) or irand(24)) can be
written by wrapping them in
S(..)inside your Kabel code - We expose Strudel's note frequency under
sFreqand Strudel's gate information undersGate - You can use more complex multi-line expressions (like
let x = a; let y = b; x.lpf(y);) by wrapping them inside a function in K (see example).
@name K @tags generators, superdough @param {KabelsalatExpression | Function} expr Kabelsalat graph definition @memberof Pattern @returns Pattern
@example note("A c e".fast(4)).transpose("<0 2 4 6 8>") .scale("F:minor").transpose("12") .s("saw") .K( // audioin().mul(sGate.adsr(0.001, 0.3, 0, 0.2)) // as effect saw(saw(sFreq / "2!3 16").mul(8).add(sFreq).lag("0!3 0.1")).mul(0.3) // as source .mul(sGate.adsr(0, 0.15, 0.5, "0.1!3 1")) .lpf(sGate.adsr(0, 0.2, 0.3, 0.2).mul(1).add(0)) .add(x => x.delay(S("0.3 0.2".fast(2))).mul(0.7)) .add(x => x.delay("0.03 [0.08 0.01] 0.01 0.013").mul(0.77)).mul(0.7) .add(x => x.delay(.13).mul(0.7)) .out() )
@example n("<0 1 <2 3 2 4>>*16") .scale("G#2:minor").sometimes(x => x.transpose("12 | 24")) .K(() => { const att = S(rand.range(0, 0.05)) const dec = S(rand.range(0.05, 0.2)) let f = n(sFreq); const mod = sine(f).mul("0.1 | 0.2 | 0.3") .add("[[1.5 1] | 1 | 2 | 4 | [6 4@3]]*2") saw(f.mul(mod)) .mul(sGate.ad(att, dec)) .add(x => x.delay(0.4).mul(0.3)) .out() }).fxr(1).room(0.3)
Creates a worklet effect. Typically derived by writing K(...) in the REPL which will parse Kabelsalat code.
@name worklet @param {string} src Source code of the worklet update function @param {...number | ...Pattern} inputs Worklet inputs @memberof Pattern @returns Pattern @noAutocomplete
4125Pattern.prototype.worklet = function (src, ...inputs) { 4126 inputs = inputs.map(reify); 4127 return this.outerBind((v) => { 4128 return _asArrayPattern(inputs).withValue((vInput) => { 4129 const currInputs = v.workletInputs ?? []; 4130 return { ...v, workletSrc: src, workletInputs: currInputs.concat(vInput) }; 4131 }); 4132 }); 4133};
4135export const worklet = (...args) => pure({}).worklet(...args);
Creates a pattern of numbers in base b from a number or pattern of numbers limited to d digits long from the right
@name base @tags generators @param {number} n - number to convert (can be a pattern or array) @param {number} b - base to convert to (defaults to 10) (can be a pattern) @param {number} d - max number of digits to produce for each n (defaults to 0 for all) (can be a pattern) @example $: note(base("7175 543", 10, 3)).scale("c:major").s("saw") // $: note("1 7 5 5 4 3").scale("c:major").s("saw")
4150export const base = (n, b = 10, d = 0) => { 4151 if (Array.isArray(n)) { 4152 n = sequence(n); 4153 } 4154 n = reify(n); 4155 b = reify(b); 4156 d = reify(d); 4157 4158 return d 4159 .withValue((e) => { 4160 return b 4161 .withValue((c) => { 4162 return n 4163 .withValue((v) => { 4164 let digits = []; 4165 let value = v; 4166 while (value > 0) { 4167 digits.unshift(value % c); 4168 value = Math.floor(value / c); 4169 } 4170 if (e) { 4171 const l = digits.length; 4172 if (l > e) { 4173 digits = digits.slice(-1 * e); 4174 } 4175 /* 4176 if (l < e){ 4177 for (let i = l; i < e; i++) { 4178 digits.unshift("~");//0); //Would like to be padding this but ~- doesn't work 4179 } 4180 console.log("digits", digits); 4181 } 4182 */ 4183 } 4184 return sequence(digits); 4185 }) 4186 .squeezeJoin(); 4187 }) 4188 .squeezeJoin(); 4189 }) 4190 .squeezeJoin(); 4191};