Merge branch 'main' into pattern_selection

This commit is contained in:
Jade (Rose) Rowland 2024-01-19 00:23:06 -05:00 committed by GitHub
commit 500ddf3946
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
111 changed files with 1165 additions and 538 deletions

View file

@ -1,17 +1,17 @@
# @strudel.cycles/core
# @strudel/core
This package contains the bare essence of strudel.
## Install
```sh
npm i @strudel.cycles/core --save
npm i @strudel/core --save
```
## Example
```js
import { sequence } from '@strudel.cycles/core';
import { sequence } from '@strudel/core';
const pattern = sequence('a', ['b', 'c']);
@ -33,7 +33,7 @@ b: 3/2 - 7/4
c: 7/4 - 2
```
- [play with @strudel.cycles/core on codesandbox](https://codesandbox.io/s/strudel-core-test-forked-9ywhv7?file=/src/index.js).
- [play with @strudel/core on codesandbox](https://codesandbox.io/s/strudel-core-test-forked-9ywhv7?file=/src/index.js).
- [open color pattern example](https://raw.githack.com/tidalcycles/strudel/main/packages/core/examples/canvas.html)
- [open minimal repl example](https://raw.githack.com/tidalcycles/strudel/main/packages/core/examples/vanilla.html)
- [open minimal vite example](./examples/vite-vanilla-repl/)

View file

@ -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
@ -891,6 +891,82 @@ const generic_params = [
*
*/
['freq'],
// pitch envelope
/**
* Attack time of pitch envelope.
*
* @name pattack
* @synonyms patt
* @param {number | Pattern} time time in seconds
* @example
* note("<c eb g bb>").pattack("<0 .1 .25 .5>")
*
*/
['pattack', 'patt'],
/**
* Decay time of pitch envelope.
*
* @name pdecay
* @synonyms pdec
* @param {number | Pattern} time time in seconds
* @example
* note("<c eb g bb>").pdecay("<0 .1 .25 .5>")
*
*/
['pdecay', 'pdec'],
// TODO: how to use psustain?!
['psustain', 'psus'],
/**
* Release time of pitch envelope
*
* @name prelease
* @synonyms prel
* @param {number | Pattern} time time in seconds
* @example
* note("<c eb g bb> ~")
* .release(.5) // to hear the pitch release
* .prelease("<0 .1 .25 .5>")
*
*/
['prelease', 'prel'],
/**
* Amount of pitch envelope. Negative values will flip the envelope.
* If you don't set other pitch envelope controls, `pattack:.2` will be the default.
*
* @name penv
* @param {number | Pattern} semitones change in semitones
* @example
* note("c")
* .penv("<12 7 1 .5 0 -1 -7 -12>")
*
*/
['penv'],
/**
* Curve of envelope. Defaults to linear. exponential is good for kicks
*
* @name pcurve
* @param {number | Pattern} type 0 = linear, 1 = exponential
* @example
* note("g1*2")
* .s("sine").pdec(.5)
* .penv(32)
* .pcurve("<0 1>")
*
*/
['pcurve'],
/**
* Sets the range anchor of the envelope:
* - anchor 0: range = [note, note + penv]
* - anchor 1: range = [note - penv, note]
* If you don't set an anchor, the value will default to the psustain value.
*
* @name panchor
* @param {number | Pattern} anchor anchor offset
* @example
* note("c").penv(12).panchor("<0 .5 1 .5>")
*
*/
['panchor'],
// TODO: https://tidalcycles.org/docs/configuration/MIDIOSC/control-voltage/#gate
['gate', 'gat'],
// ['hatgrain'],
@ -1115,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
@ -1173,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
@ -1209,7 +1285,7 @@ const generic_params = [
* Formant filter to make things sound like vowels.
*
* @name vowel
* @param {string | Pattern} vowel You can use a e i o u.
* @param {string | Pattern} vowel You can use a e i o u ae aa oe ue y uh un en an on, corresponding to [a] [e] [i] [o] [u] [æ] [ɑ] [ø] [y] [ɯ] [ʌ] [œ̃] [ɛ̃] [ɑ̃] [ɔ̃]. Aliases: aa = å = ɑ, oe = ø = ö, y = ı, ae = æ.
* @example
* note("c2 <eb2 <g2 g1>>").s('sawtooth')
* .vowel("<a e i <o u>>")

View file

@ -30,12 +30,12 @@ export { default as drawLine } from './drawLine.mjs';
// below won't work with runtime.mjs (json import fails)
/* import * as p from './package.json';
export const version = p.version; */
logger('🌀 @strudel.cycles/core loaded 🌀');
logger('🌀 @strudel/core loaded 🌀');
if (globalThis._strudelLoaded) {
console.warn(
`@strudel.cycles/core was loaded more than once...
`@strudel/core was loaded more than once...
This might happen when you have multiple versions of strudel installed.
Please check with "npm ls @strudel.cycles/core".`,
Please check with "npm ls @strudel/core".`,
);
}
globalThis._strudelLoaded = true;

View file

@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/core",
"version": "0.9.0",
"name": "@strudel/core",
"version": "0.11.0",
"description": "Port of Tidal Cycles to JavaScript",
"main": "index.mjs",
"type": "module",

View file

@ -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
@ -1154,8 +1154,8 @@ export function isPattern(thing) {
/* if (!thing instanceof Pattern) {
console.warn(
`Found Pattern that fails "instanceof Pattern" check.
This may happen if you are using multiple versions of @strudel.cycles/core.
Please check by running "npm ls @strudel.cycles/core".`,
This may happen if you are using multiple versions of @strudel/core.
Please check by running "npm ls @strudel/core".`,
);
console.log(thing);
} */
@ -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' })

View file

@ -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("<a!2 [a,b] b>".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
* "<a b [a,b]>".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

View file

@ -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),
),
);
});
});
});

View file

@ -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)]));
}