From 98b785960544b4695905d6472a5fb6d7cefdfdf4 Mon Sep 17 00:00:00 2001 From: Alex McLean Date: Thu, 18 Jan 2024 17:08:29 +0000 Subject: [PATCH] pick, pickmod, inhabit, inhabitmod (#921) * the args for `pick` are now reversed as standard (old behaviour still supported to avoid breaking change) * `pick` is also now a pattern method * `pick` now also accepts a lookup table for pick-by-name as an alternative to pick-by-index from a list * `inhabit` added with same behaviour as `pick`, except cycles from source patterns are squeezed into events of inhabited patterns * Also some general doc tidying, sorry for the noise.. * There is also `pickmod` and `inhabitmod`, for wrapping indexes around rather than clamping them --- packages/core/controls.mjs | 10 +-- packages/core/pattern.mjs | 36 ++++---- packages/core/signal.mjs | 103 ++++++++++++++++++---- packages/core/test/pattern.test.mjs | 60 +++++++++++++ packages/core/util.mjs | 7 ++ test/__snapshots__/examples.test.mjs.snap | 93 ++++++++++++++++++- 6 files changed, 263 insertions(+), 46 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 444b4ea1..42212dd3 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -97,7 +97,7 @@ const generic_params = [ */ ['postgain'], /** - * Like {@link gain}, but linear. + * Like `gain`, but linear. * * @name amp * @param {number | Pattern} amount gain. @@ -856,7 +856,7 @@ const generic_params = [ */ ['detune', 'det'], /** - * Set dryness of reverb. See {@link room} and {@link size} for more information about reverb. + * Set dryness of reverb. See `room` and `size` for more information about reverb. * * @name dry * @param {number | Pattern} dry 0 = wet, 1 = dry @@ -868,7 +868,7 @@ const generic_params = [ ['dry'], // TODO: does not seem to do anything /* - * Used when using {@link begin}/{@link end} or {@link chop}/{@link striate} and friends, to change the fade out time of the 'grain' envelope. + * Used when using `begin`/`end` or `chop`/`striate` and friends, to change the fade out time of the 'grain' envelope. * * @name fadeTime * @param {number | Pattern} time between 0 and 1 @@ -1191,7 +1191,7 @@ const generic_params = [ */ [['ir', 'i'], 'iresponse'], /** - * Sets the room size of the reverb, see {@link room}. + * Sets the room size of the reverb, see `room`. * When this property is changed, the reverb will be recaculated, so only change this sparsely.. * * @name roomsize @@ -1249,7 +1249,7 @@ const generic_params = [ */ ['speed'], /** - * Used in conjunction with {@link speed}, accepts values of "r" (rate, default behavior), "c" (cycles), or "s" (seconds). Using `unit "c"` means `speed` will be interpreted in units of cycles, e.g. `speed "1"` means samples will be stretched to fill a cycle. Using `unit "s"` means the playback speed will be adjusted so that the duration is the number of seconds specified by `speed`. + * Used in conjunction with `speed`, accepts values of "r" (rate, default behavior), "c" (cycles), or "s" (seconds). Using `unit "c"` means `speed` will be interpreted in units of cycles, e.g. `speed "1"` means samples will be stretched to fill a cycle. Using `unit "s"` means the playback speed will be adjusted so that the duration is the number of seconds specified by `speed`. * * @name unit * @param {number | string | Pattern} unit see description above diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index 9e804b23..6a7b210b 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -26,7 +26,7 @@ export class Pattern { /** * Create a pattern. As an end user, you will most likely not create a Pattern directly. * - * @param {function} query - The function that maps a {@link State} to an array of {@link Hap}. + * @param {function} query - The function that maps a `State` to an array of `Hap`. * @noAutocomplete */ constructor(query) { @@ -39,7 +39,7 @@ export class Pattern { /** * Returns a new pattern, with the function applied to the value of - * each hap. It has the alias {@link Pattern#fmap}. + * each hap. It has the alias `fmap`. * @synonyms fmap * @param {Function} func to to apply to the value * @returns Pattern @@ -51,7 +51,7 @@ export class Pattern { } /** - * see {@link Pattern#withValue} + * see `withValue` * @noAutocomplete */ fmap(func) { @@ -115,7 +115,7 @@ export class Pattern { } /** - * As with {@link Pattern#appBoth}, but the `whole` timespan is not the intersection, + * 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 @@ -148,7 +148,7 @@ export class Pattern { } /** - * As with {@link Pattern#appLeft}, but `whole` timespans are instead taken from the + * 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. * @param {Pattern} pat_val @@ -387,7 +387,7 @@ export class Pattern { } /** - * As with {@link Pattern#withQuerySpan}, but the function is applied to both the + * As with `withQuerySpan`, but the function is applied to both the * begin and end time of the query timespan. * @param {Function} func the function to apply * @returns Pattern @@ -398,7 +398,7 @@ export class Pattern { } /** - * Similar to {@link Pattern#withQuerySpan}, but the function is applied to the timespans + * 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). * @param {Function} func @@ -410,7 +410,7 @@ export class Pattern { } /** - * As with {@link Pattern#withHapSpan}, but the function is applied to both the + * As with `withHapSpan`, but the function is applied to both the * begin and end time of the hap timespans. * @param {Function} func the function to apply * @returns Pattern @@ -431,7 +431,7 @@ export class Pattern { } /** - * As with {@link Pattern#withHaps}, but applies the function to every hap, rather than every list of haps. + * As with `withHaps`, but applies the function to every hap, rather than every list of haps. * @param {Function} func * @returns Pattern * @noAutocomplete @@ -499,7 +499,7 @@ export class Pattern { } /** - * As with {@link Pattern#filterHaps}, but the function is applied to values + * As with `filterHaps`, but the function is applied to values * inside haps. * @param {Function} value_test * @returns Pattern @@ -621,7 +621,7 @@ export class Pattern { } /** - * More human-readable version of the {@link Pattern#firstCycleValues} accessor. + * More human-readable version of the `firstCycleValues` accessor. * @noAutocomplete */ get showFirstCycle() { @@ -691,7 +691,7 @@ export class Pattern { // Methods without corresponding toplevel functions /** - * Layers the result of the given function(s). Like {@link Pattern.superimpose}, but without the original pattern: + * Layers the result of the given function(s). Like `superimpose`, but without the original pattern: * @name layer * @memberof Pattern * @synonyms apply @@ -1189,7 +1189,7 @@ export function stack(...pats) { /** Concatenation: combines a list of patterns, switching between them successively, one per cycle: * - * synonyms: {@link cat} + * synonyms: `cat` * * @return {Pattern} * @example @@ -1244,7 +1244,7 @@ export function cat(...pats) { return slowcat(...pats); } -/** Like {@link Pattern.seq}, but each step has a length, relative to the whole. +/** Like `seq`, but each step has a length, relative to the whole. * @return {Pattern} * @example * timeCat([3,"e3"],[1, "g3"]).note() // "e3@3 g3".note() @@ -1279,7 +1279,7 @@ export function fastcat(...pats) { return slowcat(...pats)._fast(pats.length); } -/** See {@link fastcat} */ +/** See `fastcat` */ export function sequence(...pats) { return fastcat(...pats); } @@ -1636,7 +1636,7 @@ export const { fastGap, fastgap } = register(['fastGap', 'fastgap'], function (f }); /** - * Similar to compress, but doesn't leave gaps, and the 'focus' can be bigger than a cycle + * Similar to `compress`, but doesn't leave gaps, and the 'focus' can be bigger than a cycle * @example * s("bd hh sd hh").focus(1/4, 3/4) */ @@ -1753,7 +1753,7 @@ export const lastOf = register('lastOf', function (n, func, pat) { */ /** - * An alias for {@link firstOf} + * An alias for `firstOf` * @name every * @memberof Pattern * @param {number} n how many cycles @@ -2365,7 +2365,7 @@ export const { loopAt, loopat } = register(['loopAt', 'loopat'], function (facto // It is still here to work in cases where repl.mjs is not used /** * Makes the sample fit its event duration. Good for rhythmical loops like drum breaks. - * Similar to loopAt. + * Similar to `loopAt`. * @name fit * @example * samples({ rhodes: 'https://cdn.freesound.org/previews/132/132051_316502-lq.mp3' }) diff --git a/packages/core/signal.mjs b/packages/core/signal.mjs index 1446beeb..253458ce 100644 --- a/packages/core/signal.mjs +++ b/packages/core/signal.mjs @@ -7,7 +7,7 @@ This program is free software: you can redistribute it and/or modify it under th import { Hap } from './hap.mjs'; import { Pattern, fastcat, reify, silence, stack, register } from './pattern.mjs'; import Fraction from './fraction.mjs'; -import { id, _mod, clamp } from './util.mjs'; +import { id, _mod, clamp, objectMap } from './util.mjs'; export function steady(value) { // A continuous value @@ -156,31 +156,96 @@ export const _irand = (i) => rand.fmap((x) => Math.trunc(x * i)); */ export const irand = (ipat) => reify(ipat).fmap(_irand).innerJoin(); -/** - * pick from the list of values (or patterns of values) via the index using the given - * pattern of integers +const _pick = function (lookup, pat, modulo = true) { + const array = Array.isArray(lookup); + const len = Object.keys(lookup).length; + + lookup = objectMap(lookup, reify); + + if (len === 0) { + return silence; + } + return pat.fmap((i) => { + let key = i; + if (array) { + key = modulo ? Math.round(key) % len : clamp(Math.round(key), 0, lookup.length - 1); + } + return lookup[key]; + }); +}; + +/** * Picks patterns (or plain values) either from a list (by index) or a lookup table (by name). + * Similar to `inhabit`, but maintains the structure of the original patterns. * @param {Pattern} pat * @param {*} xs * @returns {Pattern} * @example - * note(pick("<0 1 [2!2] 3>", ["g a", "e f", "f g f g" , "g a c d"])) + * note("<0 1 2!2 3>".pick(["g a", "e f", "f g f g" , "g c d"])) + * @example + * sound("<0 1 [2,0]>".pick(["bd sd", "cp cp", "hh hh"])) + * @example + * sound("<0!2 [0,1] 1>".pick(["bd(3,8)", "sd sd"])) + * @example + * s("".pick({a: "bd(3,8)", b: "sd sd"})) */ -export const pick = (pat, xs) => { - xs = xs.map(reify); - if (xs.length == 0) { - return silence; +export const pick = function (lookup, pat) { + // backward compatibility - the args used to be flipped + if (Array.isArray(pat)) { + [pat, lookup] = [lookup, pat]; } - return pat - .fmap((i) => { - const key = clamp(Math.round(i), 0, xs.length - 1); - return xs[key]; - }) - .innerJoin(); + return __pick(lookup, pat); }; +const __pick = register('pick', function (lookup, pat) { + return _pick(lookup, pat, false).innerJoin(); +}); + +/** * The same as `pick`, but if you pick a number greater than the size of the list, + * it wraps around, rather than sticking at the maximum value. + * For example, if you pick the fifth pattern of a list of three, you'll get the + * second one. + * @param {Pattern} pat + * @param {*} xs + * @returns {Pattern} + */ + +export const pickmod = register('pickmod', function (lookup, pat) { + return _pick(lookup, pat, true).innerJoin(); +}); + /** - * pick from the list of values (or patterns of values) via the index using the given +/** * Picks patterns (or plain values) either from a list (by index) or a lookup table (by name). + * Similar to `pick`, but cycles are squeezed into the target ('inhabited') pattern. + * @param {Pattern} pat + * @param {*} xs + * @returns {Pattern} + * @example + * "".inhabit({a: s("bd(3,8)"), + b: s("cp sd") + }) + * @example + * s("a@2 [a b] a".inhabit({a: "bd(3,8)", b: "sd sd"})).slow(4) + */ +export const inhabit = register('inhabit', function (lookup, pat) { + return _pick(lookup, pat, true).squeezeJoin(); +}); + +/** * The same as `inhabit`, but if you pick a number greater than the size of the list, + * it wraps around, rather than sticking at the maximum value. + * For example, if you pick the fifth pattern of a list of three, you'll get the + * second one. + * @param {Pattern} pat + * @param {*} xs + * @returns {Pattern} + */ + +export const inhabitmod = register('inhabit', function (lookup, pat) { + return _pick(lookup, pat, false).squeezeJoin(); +}); + +/** + * Pick from the list of values (or patterns of values) via the index using the given * pattern of integers. The selected pattern will be compressed to fit the duration of the selecting event * @param {Pattern} pat * @param {*} xs @@ -356,7 +421,7 @@ export const degradeBy = register('degradeBy', function (x, pat) { export const degrade = register('degrade', (pat) => pat._degradeBy(0.5)); /** - * Inverse of {@link Pattern#degradeBy}: Randomly removes events from the pattern by a given amount. + * Inverse of `degradeBy`: Randomly removes events from the pattern by a given amount. * 0 = 100% chance of removal * 1 = 0% chance of removal * Events that would be removed by degradeBy are let through by undegradeBy and vice versa (see second example). @@ -380,7 +445,7 @@ export const undegrade = register('undegrade', (pat) => pat._undegradeBy(0.5)); /** * * Randomly applies the given function by the given probability. - * Similar to {@link Pattern#someCyclesBy} + * Similar to `someCyclesBy` * * @name sometimesBy * @memberof Pattern @@ -415,7 +480,7 @@ export const sometimes = register('sometimes', function (func, pat) { /** * * Randomly applies the given function by the given probability on a cycle by cycle basis. - * Similar to {@link Pattern#sometimesBy} + * Similar to `sometimesBy` * * @name someCyclesBy * @memberof Pattern diff --git a/packages/core/test/pattern.test.mjs b/packages/core/test/pattern.test.mjs index 928bfcef..bd0aa52b 100644 --- a/packages/core/test/pattern.test.mjs +++ b/packages/core/test/pattern.test.mjs @@ -46,6 +46,7 @@ import { rev, time, run, + pick, } from '../index.mjs'; import { steady } from '../signal.mjs'; @@ -1057,4 +1058,63 @@ describe('Pattern', () => { expect(slowcat(0, 1).repeatCycles(2).fast(6).firstCycleValues).toStrictEqual([0, 0, 1, 1, 0, 0]); }); }); + describe('inhabit', () => { + it('Can pattern named patterns', () => { + expect( + sameFirst( + sequence('a', 'b', stack('a', 'b')).inhabit({ a: sequence(1, 2), b: sequence(10, 20, 30) }), + sequence([1, 2], [10, 20, 30], stack([1, 2], [10, 20, 30])), + ), + ); + }); + it('Can pattern indexed patterns', () => { + expect( + sameFirst( + sequence('0', '1', stack('0', '1')).inhabit([sequence(1, 2), sequence(10, 20, 30)]), + sequence([1, 2], [10, 20, 30], stack([1, 2], [10, 20, 30])), + ), + ); + }); + }); + describe('pick', () => { + it('Can pattern named patterns', () => { + expect( + sameFirst( + sequence('a', 'b', 'a', stack('a', 'b')).pick({ a: sequence(1, 2, 3, 4), b: sequence(10, 20, 30, 40) }), + sequence(1, 20, 3, stack(4, 40)), + ), + ); + }); + it('Can pattern indexed patterns', () => { + expect( + sameFirst( + sequence(0, 1, 0, stack(0, 1)).pick([sequence(1, 2, 3, 4), sequence(10, 20, 30, 40)]), + sequence(1, 20, 3, stack(4, 40)), + ), + ); + }); + it('Clamps indexes', () => { + expect( + sameFirst(sequence(0, 1, 2, 3).pick([sequence(1, 2, 3, 4), sequence(10, 20, 30, 40)]), sequence(1, 20, 30, 40)), + ); + }); + it('Is backwards compatible', () => { + expect( + sameFirst( + pick([sequence('a', 'b'), sequence('c', 'd')], sequence(0, 1)), + pick(sequence(0, 1), [sequence('a', 'b'), sequence('c', 'd')]), + ), + ); + }); + }); + describe('pickmod', () => { + it('Wraps indexes', () => { + expect( + sameFirst( + sequence(0, 1, 2, 3).pickmod([sequence(1, 2, 3, 4), sequence(10, 20, 30, 40)]), + sequence(1, 20, 3, 40), + ), + ); + }); + }); }); diff --git a/packages/core/util.mjs b/packages/core/util.mjs index ef55de95..ca3cfc12 100644 --- a/packages/core/util.mjs +++ b/packages/core/util.mjs @@ -316,3 +316,10 @@ export function hash2code(hash) { return base64ToUnicode(decodeURIComponent(hash)); //return atob(decodeURIComponent(codeParam || '')); } + +export function objectMap(obj, fn) { + if (Array.isArray(obj)) { + return obj.map(fn); + } + return Object.fromEntries(Object.entries(obj).map(([k, v], i) => [k, fn(v, k, i)])); +} diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 002ccb5f..b3114ed5 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -2408,6 +2408,40 @@ exports[`runs examples > example "hush" example index 0 1`] = ` ] `; +exports[`runs examples > example "inhabit" example index 0 1`] = ` +[ + "[ 0/1 → 1/8 | s:bd ]", + "[ 3/8 → 1/2 | s:bd ]", + "[ 3/4 → 7/8 | s:bd ]", + "[ 1/1 → 3/2 | s:cp ]", + "[ 3/2 → 2/1 | s:sd ]", + "[ 2/1 → 17/8 | s:bd ]", + "[ 2/1 → 5/2 | s:cp ]", + "[ 19/8 → 5/2 | s:bd ]", + "[ 5/2 → 3/1 | s:sd ]", + "[ 11/4 → 23/8 | s:bd ]", + "[ 3/1 → 25/8 | s:bd ]", + "[ 27/8 → 7/2 | s:bd ]", + "[ 15/4 → 31/8 | s:bd ]", +] +`; + +exports[`runs examples > example "inhabit" example index 1 1`] = ` +[ + "[ 0/1 → 1/4 | s:bd ]", + "[ 3/4 → 1/1 | s:bd ]", + "[ 3/2 → 7/4 | s:bd ]", + "[ 2/1 → 33/16 | s:bd ]", + "[ 35/16 → 9/4 | s:bd ]", + "[ 19/8 → 39/16 | s:bd ]", + "[ 5/2 → 11/4 | s:sd ]", + "[ 11/4 → 3/1 | s:sd ]", + "[ 3/1 → 25/8 | s:bd ]", + "[ 27/8 → 7/2 | s:bd ]", + "[ 15/4 → 31/8 | s:bd ]", +] +`; + exports[`runs examples > example "inside" example index 0 1`] = ` [ "[ 0/1 → 1/8 | note:D3 ]", @@ -3601,10 +3635,61 @@ exports[`runs examples > example "pick" example index 0 1`] = ` "[ 9/4 → 5/2 | note:g ]", "[ 5/2 → 11/4 | note:f ]", "[ 11/4 → 3/1 | note:g ]", - "[ 3/1 → 13/4 | note:g ]", - "[ 13/4 → 7/2 | note:a ]", - "[ 7/2 → 15/4 | note:c ]", - "[ 15/4 → 4/1 | note:d ]", + "[ 3/1 → 13/4 | note:f ]", + "[ 13/4 → 7/2 | note:g ]", + "[ 7/2 → 15/4 | note:f ]", + "[ 15/4 → 4/1 | note:g ]", +] +`; + +exports[`runs examples > example "pick" example index 1 1`] = ` +[ + "[ 0/1 → 1/2 | s:bd ]", + "[ 1/2 → 1/1 | s:sd ]", + "[ 1/1 → 3/2 | s:cp ]", + "[ 3/2 → 2/1 | s:cp ]", + "[ 2/1 → 5/2 | s:hh ]", + "[ 2/1 → 5/2 | s:bd ]", + "[ 5/2 → 3/1 | s:hh ]", + "[ 5/2 → 3/1 | s:sd ]", + "[ 3/1 → 7/2 | s:bd ]", + "[ 7/2 → 4/1 | s:sd ]", +] +`; + +exports[`runs examples > example "pick" example index 2 1`] = ` +[ + "[ 0/1 → 1/8 | s:bd ]", + "[ 3/8 → 1/2 | s:bd ]", + "[ 3/4 → 7/8 | s:bd ]", + "[ 1/1 → 9/8 | s:bd ]", + "[ 11/8 → 3/2 | s:bd ]", + "[ 7/4 → 15/8 | s:bd ]", + "[ 2/1 → 17/8 | s:bd ]", + "[ 2/1 → 5/2 | s:sd ]", + "[ 19/8 → 5/2 | s:bd ]", + "[ 5/2 → 3/1 | s:sd ]", + "[ 11/4 → 23/8 | s:bd ]", + "[ 3/1 → 7/2 | s:sd ]", + "[ 7/2 → 4/1 | s:sd ]", +] +`; + +exports[`runs examples > example "pick" example index 3 1`] = ` +[ + "[ 0/1 → 1/8 | s:bd ]", + "[ 3/8 → 1/2 | s:bd ]", + "[ 3/4 → 7/8 | s:bd ]", + "[ 1/1 → 9/8 | s:bd ]", + "[ 11/8 → 3/2 | s:bd ]", + "[ 7/4 → 15/8 | s:bd ]", + "[ 2/1 → 17/8 | s:bd ]", + "[ 2/1 → 5/2 | s:sd ]", + "[ 19/8 → 5/2 | s:bd ]", + "[ 5/2 → 3/1 | s:sd ]", + "[ 11/4 → 23/8 | s:bd ]", + "[ 3/1 → 7/2 | s:sd ]", + "[ 7/2 → 4/1 | s:sd ]", ] `;