Merge branch 'tidalcycles:main' into repl_sync

This commit is contained in:
Jade (Rose) Rowland 2024-02-04 13:29:52 -05:00 committed by GitHub
commit e5ec62695c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
198 changed files with 8854 additions and 4409 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

@ -40,7 +40,7 @@ const generic_params = [
* @name n
* @param {number | Pattern} value sample index starting from 0
* @example
* s("bd sd,hh*3").n("<0 1>")
* s("bd sd [~ bd] sd,hh*6").n("<0 1>")
*/
// also see https://github.com/tidalcycles/strudel/pull/63
['n'],
@ -82,7 +82,7 @@ const generic_params = [
* @name gain
* @param {number | Pattern} amount gain.
* @example
* s("hh*8").gain(".4!2 1 .4!2 1 .4 1")
* s("hh*8").gain(".4!2 1 .4!2 1 .4 1").fast(2)
*
*/
['gain'],
@ -91,13 +91,13 @@ const generic_params = [
*
* @name postgain
* @example
* s("bd sd,hh*4")
* s("bd sd [~ bd] sd,hh*8")
* .compressor("-20:20:10:.002:.02").postgain(1.5)
*
*/
['postgain'],
/**
* Like {@link gain}, but linear.
* Like `gain`, but linear.
*
* @name amp
* @param {number | Pattern} amount gain.
@ -114,7 +114,7 @@ const generic_params = [
* @param {number | Pattern} attack time in seconds.
* @synonyms att
* @example
* note("c3 e3").attack("<0 .1 .5>")
* note("c3 e3 f3 g3").attack("<0 .1 .5>")
*
*/
['attack', 'att'],
@ -128,7 +128,7 @@ const generic_params = [
* @name fmh
* @param {number | Pattern} harmonicity
* @example
* note("c e g b")
* note("c e g b g e")
* .fm(4)
* .fmh("<1 2 1.5 1.61>")
* .scope()
@ -143,7 +143,7 @@ const generic_params = [
* @param {number | Pattern} brightness modulation index
* @synonyms fmi
* @example
* note("c e g b")
* note("c e g b g e")
* .fm("<0 1 2 8 32>")
* .scope()
*
@ -156,7 +156,7 @@ const generic_params = [
* @name fmenv
* @param {number | Pattern} type lin | exp
* @example
* note("c e g b")
* note("c e g b g e")
* .fm(4)
* .fmdecay(.2)
* .fmsustain(0)
@ -171,7 +171,7 @@ const generic_params = [
* @name fmattack
* @param {number | Pattern} time attack time
* @example
* note("c e g b")
* note("c e g b g e")
* .fm(4)
* .fmattack("<0 .05 .1 .2>")
* .scope()
@ -184,7 +184,7 @@ const generic_params = [
* @name fmdecay
* @param {number | Pattern} time decay time
* @example
* note("c e g b")
* note("c e g b g e")
* .fm(4)
* .fmdecay("<.01 .05 .1 .2>")
* .fmsustain(.4)
@ -198,7 +198,7 @@ const generic_params = [
* @name fmsustain
* @param {number | Pattern} level sustain level
* @example
* note("c e g b")
* note("c e g b g e")
* .fm(4)
* .fmdecay(.1)
* .fmsustain("<1 .75 .5 0>")
@ -216,7 +216,7 @@ const generic_params = [
* @name bank
* @param {string | Pattern} bank the name of the bank
* @example
* s("bd sd").bank('RolandTR909') // = s("RolandTR909_bd RolandTR909_sd")
* s("bd sd [~ bd] sd").bank('RolandTR909') // = s("RolandTR909_bd RolandTR909_sd")
*
*/
['bank'],
@ -231,10 +231,10 @@ const generic_params = [
* @name decay
* @param {number | Pattern} time decay time in seconds
* @example
* note("c3 e3").decay("<.1 .2 .3 .4>").sustain(0)
* note("c3 e3 f3 g3").decay("<.1 .2 .3 .4>").sustain(0)
*
*/
['decay'],
['decay', 'dec'],
/**
* Amplitude envelope sustain level: The level which is reached after attack / decay, being sustained until the offset.
*
@ -242,7 +242,7 @@ const generic_params = [
* @param {number | Pattern} gain sustain level between 0 and 1
* @synonyms sus
* @example
* note("c3 e3").decay(.2).sustain("<0 .1 .4 .6 1>")
* note("c3 e3 f3 g3").decay(.2).sustain("<0 .1 .4 .6 1>")
*
*/
['sustain', 'sus'],
@ -267,10 +267,10 @@ const generic_params = [
* @param {number | Pattern} frequency center frequency
* @synonyms bandf, bp
* @example
* s("bd sd,hh*3").bpf("<1000 2000 4000 8000>")
* s("bd sd [~ bd] sd,hh*6").bpf("<1000 2000 4000 8000>")
*
*/
[['bandf', 'bandq'], 'bpf', 'bp'],
[['bandf', 'bandq', 'bpenv'], 'bpf', 'bp'],
// TODO: in tidal, it seems to be normalized
/**
* Sets the **b**and-**p**ass **q**-factor (resonance).
@ -279,7 +279,7 @@ const generic_params = [
* @param {number | Pattern} q q factor
* @synonyms bandq
* @example
* s("bd sd").bpf(500).bpq("<0 1 2 3>")
* s("bd sd [~ bd] sd").bpf(500).bpq("<0 1 2 3>")
*
*/
// currently an alias of 'bandq' https://github.com/tidalcycles/strudel/issues/496
@ -293,7 +293,7 @@ const generic_params = [
* @param {number | Pattern} amount between 0 and 1, where 1 is the length of the sample
* @example
* samples({ rave: 'rave/AREUREADY.wav' }, 'github:tidalcycles/Dirt-Samples/master/')
* s("rave").begin("<0 .25 .5 .75>")
* s("rave").begin("<0 .25 .5 .75>").fast(2)
*
*/
['begin'],
@ -304,7 +304,7 @@ const generic_params = [
* @name end
* @param {number | Pattern} length 1 = whole sample, .5 = half sample, .25 = quarter sample etc..
* @example
* s("bd*2,oh*4").end("<.1 .2 .5 1>")
* s("bd*2,oh*4").end("<.1 .2 .5 1>").fast(2)
*
*/
['end'],
@ -376,7 +376,7 @@ const generic_params = [
* @name coarse
* @param {number | Pattern} factor 1 for original 2 for half, 3 for a third and so on.
* @example
* s("bd sd,hh*4").coarse("<1 4 8 16 32>")
* s("bd sd [~ bd] sd,hh*8").coarse("<1 4 8 16 32>")
*
*/
['coarse'],
@ -463,7 +463,7 @@ const generic_params = [
* @name cut
* @param {number | Pattern} group cut group number
* @example
* s("rd*4").cut(1)
* s("[oh hh]*4").cut(1)
*
*/
['cut'],
@ -476,12 +476,12 @@ const generic_params = [
* @param {number | Pattern} frequency audible between 0 and 20000
* @synonyms cutoff, ctf, lp
* @example
* s("bd sd,hh*3").lpf("<4000 2000 1000 500 200 100>")
* s("bd sd [~ bd] sd,hh*6").lpf("<4000 2000 1000 500 200 100>")
* @example
* s("bd*8").lpf("1000:0 1000:10 1000:20 1000:30")
* s("bd*16").lpf("1000:0 1000:10 1000:20 1000:30")
*
*/
[['cutoff', 'resonance'], 'ctf', 'lpf', 'lp'],
[['cutoff', 'resonance', 'lpenv'], 'ctf', 'lpf', 'lp'],
/**
* Sets the lowpass filter envelope modulation depth.
@ -489,7 +489,7 @@ const generic_params = [
* @param {number | Pattern} modulation depth of the lowpass filter envelope between 0 and _n_
* @synonyms lpe
* @example
* note("<c2 e2 f2 g2>")
* note("c2 e2 f2 g2")
* .sound('sawtooth')
* .lpf(500)
* .lpa(.5)
@ -502,7 +502,7 @@ const generic_params = [
* @param {number | Pattern} modulation depth of the highpass filter envelope between 0 and _n_
* @synonyms hpe
* @example
* note("<c2 e2 f2 g2>")
* note("c2 e2 f2 g2")
* .sound('sawtooth')
* .hpf(500)
* .hpa(.5)
@ -515,7 +515,7 @@ const generic_params = [
* @param {number | Pattern} modulation depth of the bandpass filter envelope between 0 and _n_
* @synonyms bpe
* @example
* note("<c2 e2 f2 g2>")
* note("c2 e2 f2 g2")
* .sound('sawtooth')
* .bpf(500)
* .bpa(.5)
@ -528,7 +528,7 @@ const generic_params = [
* @param {number | Pattern} attack time of the filter envelope
* @synonyms lpa
* @example
* note("<c2 e2 f2 g2>")
* note("c2 e2 f2 g2")
* .sound('sawtooth')
* .lpf(500)
* .lpa("<.5 .25 .1 .01>/4")
@ -541,7 +541,7 @@ const generic_params = [
* @param {number | Pattern} attack time of the highpass filter envelope
* @synonyms hpa
* @example
* note("<c2 e2 f2 g2>")
* note("c2 e2 f2 g2")
* .sound('sawtooth')
* .hpf(500)
* .hpa("<.5 .25 .1 .01>/4")
@ -554,7 +554,7 @@ const generic_params = [
* @param {number | Pattern} attack time of the bandpass filter envelope
* @synonyms bpa
* @example
* note("<c2 e2 f2 g2>")
* note("c2 e2 f2 g2")
* .sound('sawtooth')
* .bpf(500)
* .bpa("<.5 .25 .1 .01>/4")
@ -567,7 +567,7 @@ const generic_params = [
* @param {number | Pattern} decay time of the filter envelope
* @synonyms lpd
* @example
* note("<c2 e2 f2 g2>")
* note("c2 e2 f2 g2")
* .sound('sawtooth')
* .lpf(500)
* .lpd("<.5 .25 .1 0>/4")
@ -581,7 +581,7 @@ const generic_params = [
* @param {number | Pattern} decay time of the highpass filter envelope
* @synonyms hpd
* @example
* note("<c2 e2 f2 g2>")
* note("c2 e2 f2 g2")
* .sound('sawtooth')
* .hpf(500)
* .hpd("<.5 .25 .1 0>/4")
@ -595,7 +595,7 @@ const generic_params = [
* @param {number | Pattern} decay time of the bandpass filter envelope
* @synonyms bpd
* @example
* note("<c2 e2 f2 g2>")
* note("c2 e2 f2 g2")
* .sound('sawtooth')
* .bpf(500)
* .bpd("<.5 .25 .1 0>/4")
@ -609,7 +609,7 @@ const generic_params = [
* @param {number | Pattern} sustain amplitude of the lowpass filter envelope
* @synonyms lps
* @example
* note("<c2 e2 f2 g2>")
* note("c2 e2 f2 g2")
* .sound('sawtooth')
* .lpf(500)
* .lpd(.5)
@ -623,7 +623,7 @@ const generic_params = [
* @param {number | Pattern} sustain amplitude of the highpass filter envelope
* @synonyms hps
* @example
* note("<c2 e2 f2 g2>")
* note("c2 e2 f2 g2")
* .sound('sawtooth')
* .hpf(500)
* .hpd(.5)
@ -637,7 +637,7 @@ const generic_params = [
* @param {number | Pattern} sustain amplitude of the bandpass filter envelope
* @synonyms bps
* @example
* note("<c2 e2 f2 g2>")
* note("c2 e2 f2 g2")
* .sound('sawtooth')
* .bpf(500)
* .bpd(.5)
@ -651,7 +651,7 @@ const generic_params = [
* @param {number | Pattern} release time of the filter envelope
* @synonyms lpr
* @example
* note("<c2 e2 f2 g2>")
* note("c2 e2 f2 g2")
* .sound('sawtooth')
* .clip(.5)
* .lpf(500)
@ -666,7 +666,7 @@ const generic_params = [
* @param {number | Pattern} release time of the highpass filter envelope
* @synonyms hpr
* @example
* note("<c2 e2 f2 g2>")
* note("c2 e2 f2 g2")
* .sound('sawtooth')
* .clip(.5)
* .hpf(500)
@ -681,7 +681,7 @@ const generic_params = [
* @param {number | Pattern} release time of the bandpass filter envelope
* @synonyms bpr
* @example
* note("<c2 e2 f2 g2>")
* note("c2 e2 f2 g2")
* .sound('sawtooth')
* .clip(.5)
* .bpf(500)
@ -695,11 +695,11 @@ const generic_params = [
* @name ftype
* @param {number | Pattern} type 12db (default) or 24db
* @example
* note("<c2 e2 f2 g2>")
* note("c2 e2 f2 g2")
* .sound('sawtooth')
* .lpf(500)
* .bpenv(4)
* .ftype("<12db 24db>")
* .ftype("12db 24db")
*/
['ftype'],
['fanchor'],
@ -712,9 +712,9 @@ const generic_params = [
* @param {number | Pattern} frequency audible between 0 and 20000
* @synonyms hp, hcutoff
* @example
* s("bd sd,hh*4").hpf("<4000 2000 1000 500 200 100>")
* s("bd sd [~ bd] sd,hh*8").hpf("<4000 2000 1000 500 200 100>")
* @example
* s("bd sd,hh*4").hpf("<2000 2000:25>")
* s("bd sd [~ bd] sd,hh*8").hpf("<2000 2000:25>")
*
*/
// currently an alias of 'hcutoff' https://github.com/tidalcycles/strudel/issues/496
@ -726,11 +726,11 @@ const generic_params = [
* @synonyms vibrato, v
* @param {number | Pattern} frequency of the vibrato in hertz
* @example
* note("a")
* note("a e")
* .vib("<.5 1 2 4 8 16>")
* @example
* // change the modulation depth with ":"
* note("a")
* note("a e")
* .vib("<.5 1 2 4 8 16>:12")
*/
[['vib', 'vibmod'], 'vibrato', 'v'],
@ -750,15 +750,15 @@ const generic_params = [
* @synonyms vmod
* @param {number | Pattern} depth of vibrato (in semitones)
* @example
* note("a").vib(4)
* note("a e").vib(4)
* .vibmod("<.25 .5 1 2 12>")
* @example
* // change the vibrato frequency with ":"
* note("a")
* note("a e")
* .vibmod("<.25 .5 1 2 12>:8")
*/
[['vibmod', 'vib'], 'vmod'],
[['hcutoff', 'hresonance'], 'hpf', 'hp'],
[['hcutoff', 'hresonance', 'hpenv'], 'hpf', 'hp'],
/**
* Controls the **h**igh-**p**ass **q**-value.
*
@ -766,7 +766,7 @@ const generic_params = [
* @param {number | Pattern} q resonance factor between 0 and 50
* @synonyms hresonance
* @example
* s("bd sd,hh*4").hpf(2000).hpq("<0 10 20 30>")
* s("bd sd [~ bd] sd,hh*8").hpf(2000).hpq("<0 10 20 30>")
*
*/
['hresonance', 'hpq'],
@ -777,7 +777,7 @@ const generic_params = [
* @param {number | Pattern} q resonance factor between 0 and 50
* @synonyms resonance
* @example
* s("bd sd,hh*4").lpf(2000).lpq("<0 10 20 30>")
* s("bd sd [~ bd] sd,hh*8").lpf(2000).lpq("<0 10 20 30>")
*
*/
// currently an alias of 'resonance' https://github.com/tidalcycles/strudel/issues/496
@ -804,7 +804,7 @@ const generic_params = [
* @name delay
* @param {number | Pattern} level between 0 and 1
* @example
* s("bd").delay("<0 .25 .5 1>")
* s("bd bd").delay("<0 .25 .5 1>")
* @example
* s("bd bd").delay("0.65:0.25:0.9 0.65:0.125:0.7")
*
@ -818,7 +818,7 @@ const generic_params = [
* @param {number | Pattern} feedback between 0 and 1
* @synonyms delayfb, dfb
* @example
* s("bd").delay(.25).delayfeedback("<.25 .5 .75 1>").slow(2)
* s("bd").delay(.25).delayfeedback("<.25 .5 .75 1>")
*
*/
['delayfeedback', 'delayfb', 'dfb'],
@ -829,7 +829,7 @@ const generic_params = [
* @param {number | Pattern} seconds between 0 and Infinity
* @synonyms delayt, dt
* @example
* s("bd").delay(.25).delaytime("<.125 .25 .5 1>").slow(2)
* s("bd bd").delay(.25).delaytime("<.125 .25 .5 1>")
*
*/
['delaytime', 'delayt', 'dt'],
@ -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").slow(2)
*
*/
['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*4")
* .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 c4").penv(12).panchor("<0 .5 1 .5>")
*
*/
['panchor'],
// TODO: https://tidalcycles.org/docs/configuration/MIDIOSC/control-voltage/#gate
['gate', 'gat'],
// ['hatgrain'],
@ -986,8 +1062,8 @@ const generic_params = [
* @param {number | Pattern} number
* @example
* stack(
* s("hh*3").delay(.5).delaytime(.25).orbit(1),
* s("~ sd").delay(.5).delaytime(.125).orbit(2)
* s("hh*6").delay(.5).delaytime(.25).orbit(1),
* s("~ sd ~ sd").delay(.5).delaytime(.125).orbit(2)
* )
*/
['orbit'],
@ -1000,6 +1076,8 @@ const generic_params = [
* @param {number | Pattern} pan between 0 and 1, from left to right (assuming stereo), once round a circle (assuming multichannel)
* @example
* s("[bd hh]*2").pan("<.5 1 .5 0>")
* @example
* s("bd rim sd rim bd ~ cp rim").pan(sine.slow(2))
*
*/
['pan'],
@ -1057,9 +1135,9 @@ const generic_params = [
* @name room
* @param {number | Pattern} level between 0 and 1
* @example
* s("bd sd").room("<0 .2 .4 .6 .8 1>")
* s("bd sd [~ bd] sd").room("<0 .2 .4 .6 .8 1>")
* @example
* s("bd sd").room("<0.9:1 0.9:4>")
* s("bd sd [~ bd] sd").room("<0.9:1 0.9:4>")
*
*/
[['room', 'size']],
@ -1071,9 +1149,9 @@ const generic_params = [
* @synonyms rlp
* @param {number} frequency between 0 and 20000hz
* @example
* s("bd sd").room(0.5).rlp(10000)
* s("bd sd [~ bd] sd").room(0.5).rlp(10000)
* @example
* s("bd sd").room(0.5).rlp(5000)
* s("bd sd [~ bd] sd").room(0.5).rlp(5000)
*/
['roomlp', 'rlp'],
/**
@ -1084,9 +1162,9 @@ const generic_params = [
* @synonyms rdim
* @param {number} frequency between 0 and 20000hz
* @example
* s("bd sd").room(0.5).rlp(10000).rdim(8000)
* s("bd sd [~ bd] sd").room(0.5).rlp(10000).rdim(8000)
* @example
* s("bd sd").room(0.5).rlp(5000).rdim(400)
* s("bd sd [~ bd] sd").room(0.5).rlp(5000).rdim(400)
*
*/
['roomdim', 'rdim'],
@ -1098,9 +1176,9 @@ const generic_params = [
* @synonyms rfade
* @param {number} seconds for the reverb to fade
* @example
* s("bd sd").room(0.5).rlp(10000).rfade(0.5)
* s("bd sd [~ bd] sd").room(0.5).rlp(10000).rfade(0.5)
* @example
* s("bd sd").room(0.5).rlp(5000).rfade(4)
* s("bd sd [~ bd] sd").room(0.5).rlp(5000).rfade(4)
*
*/
['roomfade', 'rfade'],
@ -1110,25 +1188,25 @@ const generic_params = [
* @param {string | Pattern} sample to use as an impulse response
* @synonyms ir
* @example
* s("bd sd").room(.8).ir("<shaker_large:0 shaker_large:2>")
* s("bd sd [~ bd] sd").room(.8).ir("<shaker_large:0 shaker_large:2>")
*
*/
[['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
* @param {number | Pattern} size between 0 and 10
* @synonyms rsize, sz, size
* @example
* s("bd sd").room(.8).rsize(1)
* s("bd sd [~ bd] sd").room(.8).rsize(1)
* @example
* s("bd sd").room(.8).rsize(4)
* s("bd sd [~ bd] sd").room(.8).rsize(4)
*
*/
// TODO: find out why :
// s("bd sd").room(.8).roomsize("<0 .2 .4 .6 .8 [1,0]>").osc()
// s("bd sd [~ bd] sd").room(.8).roomsize("<0 .2 .4 .6 .8 [1,0]>").osc()
// .. does not work. Is it because room is only one effect?
['roomsize', 'size', 'sz', 'rsize'],
// ['sagogo'],
@ -1141,7 +1219,7 @@ const generic_params = [
* @name shape
* @param {number | Pattern} distortion between 0 and 1
* @example
* s("bd sd,hh*4").shape("<0 .2 .4 .6 .8>")
* s("bd sd [~ bd] sd,hh*8").shape("<0 .2 .4 .6 .8>")
*
*/
['shape'],
@ -1151,7 +1229,7 @@ const generic_params = [
*
* @name compressor
* @example
* s("bd sd,hh*4")
* s("bd sd [~ bd] sd,hh*8")
* .compressor("-20:20:10:.002:.02")
*
*/
@ -1166,14 +1244,14 @@ const generic_params = [
* @name speed
* @param {number | Pattern} speed -inf to inf, negative numbers play the sample backwards.
* @example
* s("bd").speed("<1 2 4 1 -2 -4>")
* s("bd*6").speed("1 2 4 1 -2 -4")
* @example
* speed("1 1.5*2 [2 1.1]").s("piano").clip(1)
*
*/
['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,10 +1287,12 @@ 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')
* note("[c2 <eb2 <g2 g1>>]*2").s('sawtooth')
* .vowel("<a e i <o u>>")
* @example
* s("bd sd mt ht bd [~ cp] ht lt").vowel("[a|e|i|o|u]")
*
*/
['vowel'],
@ -1387,17 +1467,27 @@ controls.createParams = (...names) =>
* @param {number | Pattern} gain sustain level (0 to 1)
* @param {number | Pattern} time release time in seconds
* @example
* note("<c3 bb2 f3 eb3>").sound("sawtooth").lpf(600).adsr(".1:.1:.5:.2")
* note("[c3 bb2 f3 eb3]*2").sound("sawtooth").lpf(600).adsr(".1:.1:.5:.2")
*/
controls.adsr = register('adsr', (adsr, pat) => {
adsr = !Array.isArray(adsr) ? [adsr] : adsr;
const [attack, decay, sustain, release] = adsr;
return pat.set({ attack, decay, sustain, release });
});
controls.ds = register('ds', (ds, pat) => {
ds = !Array.isArray(ds) ? [ds] : ds;
const [decay, sustain] = ds;
controls.ad = register('ad', (t, pat) => {
t = !Array.isArray(t) ? [t] : t;
const [attack, decay = attack] = t;
return pat.attack(attack).decay(decay);
});
controls.ds = register('ds', (t, pat) => {
t = !Array.isArray(t) ? [t] : t;
const [decay, sustain = 0] = t;
return pat.set({ decay, sustain });
});
controls.ds = register('ar', (t, pat) => {
t = !Array.isArray(t) ? [t] : t;
const [attack, release = attack] = t;
return pat.set({ attack, release });
});
export default controls;

View file

@ -10,7 +10,7 @@ import { logger } from './logger.mjs';
export class Cyclist {
constructor({ interval, onTrigger, onToggle, onError, getTime, latency = 0.1 }) {
this.started = false;
this.cps = 1;
this.cps = 0.5;
this.num_ticks_since_cps_change = 0;
this.lastTick = 0; // absolute time when last tick (clock callback) happened
this.lastBegin = 0; // query begin of last tick
@ -43,7 +43,7 @@ export class Cyclist {
this.lastEnd = end;
// query the pattern for events
const haps = this.pattern.queryArc(begin, end);
const haps = this.pattern.queryArc(begin, end, { _cps: this.cps });
const tickdeadline = phase - time; // time left until the phase is a whole number
@ -99,7 +99,7 @@ export class Cyclist {
this.start();
}
}
setCps(cps = 1) {
setCps(cps = 0.5) {
if (this.cps === cps) {
return;
}

View file

@ -136,7 +136,7 @@ export class Drawer {
this.lastFrame = phase;
this.visibleHaps = (this.visibleHaps || [])
// filter out haps that are too far in the past (think left edge of screen for pianoroll)
.filter((h) => h.whole.end >= phase - lookbehind - lookahead)
.filter((h) => h.whole?.end >= phase - lookbehind - lookahead)
// add new haps with onset (think right edge bars scrolling in)
.concat(haps.filter((h) => h.hasOnset()));
const time = phase - lookahead;

View file

@ -148,7 +148,7 @@ export const { euclidrot, euclidRot } = register(['euclidrot', 'euclidRot'], fun
* @param {number} pulses the number of onsets / beats
* @param {number} steps the number of steps to fill
* @example
* n("g2").decay(.1).sustain(.3).euclidLegato(3,8)
* note("c3").euclidLegato(3,8)
*/
const _euclidLegato = function (pulses, steps, rotation, pat) {

View file

@ -31,12 +31,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": "1.0.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
@ -340,9 +340,9 @@ export class Pattern {
* silence
* @noAutocomplete
*/
queryArc(begin, end) {
queryArc(begin, end, controls = {}) {
try {
return this.query(new State(new TimeSpan(begin, end)));
return this.query(new State(new TimeSpan(begin, end), controls));
} catch (err) {
logger(`[query]: ${err.message}`, 'error');
return [];
@ -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
@ -427,11 +427,11 @@ export class Pattern {
* @noAutocomplete
*/
withHaps(func) {
return new Pattern((state) => func(this.query(state)));
return new Pattern((state) => func(this.query(state), state));
}
/**
* 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,13 +691,13 @@ 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
* @returns Pattern
* @example
* "<0 2 4 6 ~ 4 ~ 2 0!3 ~!5>*4"
* "<0 2 4 6 ~ 4 ~ 2 0!3 ~!5>*8"
* .layer(x=>x.add("0,2"))
* .scale('C minor').note()
*/
@ -711,7 +711,7 @@ export class Pattern {
* @memberof Pattern
* @returns Pattern
* @example
* "<0 2 4 6 ~ 4 ~ 2 0!3 ~!5>*4"
* "<0 2 4 6 ~ 4 ~ 2 0!3 ~!5>*8"
* .superimpose(x=>x.add(2))
* .scale('C minor').note()
*/
@ -727,8 +727,8 @@ export class Pattern {
* @name stack
* @memberof Pattern
* @example
* s("hh*2").stack(
* note("c2(3,8)")
* s("hh*4").stack(
* note("c4(5,8)")
* )
*/
stack(...pats) {
@ -745,8 +745,8 @@ export class Pattern {
* @memberof Pattern
* @synonyms sequence, fastcat
* @example
* s("hh*2").seq(
* note("c2(3,8)")
* s("hh*4").seq(
* note("c4(5,8)")
* )
*/
seq(...pats) {
@ -759,8 +759,8 @@ export class Pattern {
* @memberof Pattern
* @synonyms slowcat
* @example
* s("hh*2").cat(
* note("c2(3,8)")
* s("hh*4").cat(
* note("c4(5,8)")
* )
*/
cat(...pats) {
@ -858,7 +858,7 @@ Pattern.prototype.arpWith = function (func) {
* Selects indices in in stacked notes.
* @example
* note("<[c,eb,g]!2 [c,f,ab] [d,f,ab]>")
* .arp("0 [0,2] 1 [0,2]").slow(2)
* .arp("0 [0,2] 1 [0,2]")
* */
Pattern.prototype.arp = function (pat) {
return this.arpWith((haps) => pat.fmap((i) => haps[i % haps.length]));
@ -929,14 +929,14 @@ function _composeOp(a, b, func) {
* @memberof Pattern
* @example
* // Here, the triad 0, 2, 4 is shifted by different amounts
* "0 2 4".add("<0 3 4 0>").scale('C major').note()
* n("0 2 4".add("<0 3 4 0>")).scale("C:major")
* // Without add, the equivalent would be:
* // "<[0 2 4] [3 5 7] [4 6 8] [0 2 4]>".scale('C major').note()
* // n("<[0 2 4] [3 5 7] [4 6 8] [0 2 4]>").scale("C:major")
* @example
* // You can also use add with notes:
* "c3 e3 g3".add("<0 5 7 0>").note()
* note("c3 e3 g3".add("<0 5 7 0>"))
* // Behind the scenes, the notes are converted to midi numbers:
* // "48 52 55".add("<0 5 7 0>").note()
* // note("48 52 55".add("<0 5 7 0>"))
*/
add: [numeralArgs((a, b) => a + b)], // support string concatenation
/**
@ -945,7 +945,7 @@ function _composeOp(a, b, func) {
* @name sub
* @memberof Pattern
* @example
* "0 2 4".sub("<0 1 2 3>").scale('C4 minor').note()
* n("0 2 4".sub("<0 1 2 3>")).scale("C4:minor")
* // See add for more information.
*/
sub: [numeralArgs((a, b) => a - b)],
@ -955,7 +955,7 @@ function _composeOp(a, b, func) {
* @name mul
* @memberof Pattern
* @example
* "1 1.5 [1.66, <2 2.33>]".mul(150).freq()
* "<1 1.5 [1.66, <2 2.33>]>*4".mul(150).freq()
*/
mul: [numeralArgs((a, b) => a * b)],
/**
@ -1049,9 +1049,9 @@ function _composeOp(a, b, func) {
* Applies the given structure to the pattern:
*
* @example
* note("c3,eb3,g3")
* note("c,eb,g")
* .struct("x ~ x ~ ~ x ~ x ~ ~ ~ x ~ x ~ ~")
* .slow(4)
* .slow(2)
*/
Pattern.prototype.struct = function (...args) {
return this.keepif.out(...args);
@ -1063,7 +1063,7 @@ function _composeOp(a, b, func) {
* Returns silence when mask is 0 or "~"
*
* @example
* note("c [eb,g] d [eb,g]").mask("<1 [0 1]>").slow(2)
* note("c [eb,g] d [eb,g]").mask("<1 [0 1]>")
*/
Pattern.prototype.mask = function (...args) {
return this.keepif.in(...args);
@ -1075,7 +1075,7 @@ function _composeOp(a, b, func) {
* Resets the pattern to the start of the cycle for each onset of the reset pattern.
*
* @example
* s("<bd lt> sd, hh*4").reset("<x@3 x(3,8)>")
* s("[<bd lt> sd]*2, hh*8").reset("<x@3 x(5,8)>")
*/
Pattern.prototype.reset = function (...args) {
return this.keepif.trig(...args);
@ -1088,7 +1088,7 @@ function _composeOp(a, b, func) {
* While reset will only reset the current cycle, restart will start from cycle 0.
*
* @example
* s("<bd lt> sd, hh*4").restart("<x@3 x(3,8)>")
* s("[<bd lt> sd]*2, hh*8").restart("<x@3 x(5,8)>")
*/
Pattern.prototype.restart = function (...args) {
return this.keepif.trigzero(...args);
@ -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);
} */
@ -1178,7 +1178,8 @@ export function reify(thing) {
* @return {Pattern}
* @synonyms polyrhythm, pr
* @example
* stack("g3", "b3", ["e4", "d4"]).note() // "g3,b3,[e4,d4]".note()
* stack("g3", "b3", ["e4", "d4"]).note()
* // "g3,b3,[e4,d4]".note()
*/
export function stack(...pats) {
// Array test here is to avoid infinite recursions..
@ -1189,7 +1190,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
@ -1237,17 +1238,19 @@ export function slowcatPrime(...pats) {
* @synonyms slowcat
* @return {Pattern}
* @example
* cat("e5", "b4", ["d5", "c5"]).note() // "<e5 b4 [d5 c5]>".note()
* cat("e5", "b4", ["d5", "c5"]).note()
* // "<e5 b4 [d5 c5]>".note()
*
*/
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()
* timeCat([3,"e3"],[1, "g3"]).note()
* // "e3@3 g3".note()
*/
export function timeCat(...timepats) {
const total = timepats.map((a) => a[0]).reduce((a, b) => a.add(b), Fraction(0));
@ -1267,7 +1270,10 @@ export function timeCat(...timepats) {
*
* @return {Pattern}
* @example
* arrange([4, "<c a f e>(3,8)"],[2, "<g a>(5,8)"]).note()
* arrange(
* [4, "<c a f e>(3,8)"],
* [2, "<g a>(5,8)"]
* ).note()
*/
export function arrange(...sections) {
const total = sections.reduce((sum, [cycles]) => sum + cycles, 0);
@ -1279,7 +1285,7 @@ export function fastcat(...pats) {
return slowcat(...pats)._fast(pats.length);
}
/** See {@link fastcat} */
/** See `fastcat` */
export function sequence(...pats) {
return fastcat(...pats);
}
@ -1287,7 +1293,8 @@ export function sequence(...pats) {
/** Like **cat**, but the items are crammed into one cycle.
* @synonyms fastcat, sequence
* @example
* seq("e5", "b4", ["d5", "c5"]).note() // "e5 b4 [d5 c5]".note()
* seq("e5", "b4", ["d5", "c5"]).note()
* // "e5 b4 [d5 c5]".note()
*
*/
export function seq(...pats) {
@ -1313,9 +1320,9 @@ function _sequenceCount(x) {
* @param {number} steps how many items are placed in one cycle
* @param {any[]} sequences one or more arrays of Patterns / values
* @example
* polymeterSteps(2, ["c", "d", "e", "f", "g", "f", "e", "d"])
* .note().stack(s("bd")) // 1 cycle = 1 bd = 2 notes
* // note("{c d e f g f e d}%2").stack(s("bd"))
* polymeterSteps(4, ["c", "d", "e"])
* .note().stack(s("bd"))
* // note("{c d e}%4").stack(s("bd"))
*/
export function polymeterSteps(steps, ...args) {
const seqs = args.map((a) => _sequenceCount(a));
@ -1463,7 +1470,7 @@ export function register(name, func, patternify = true) {
* @memberof Pattern
* @returns Pattern
* @example
* "0.5 1.5 2.5".round().scale('C major').note()
* n("0.5 1.5 2.5".round()).scale("C:major")
*/
export const round = register('round', function (pat) {
return pat.asNumber().fmap((v) => Math.round(v));
@ -1477,7 +1484,7 @@ export const round = register('round', function (pat) {
* @memberof Pattern
* @returns Pattern
* @example
* "42 42.1 42.5 43".floor().note()
* note("42 42.1 42.5 43".floor())
*/
export const floor = register('floor', function (pat) {
return pat.asNumber().fmap((v) => Math.floor(v));
@ -1491,7 +1498,7 @@ export const floor = register('floor', function (pat) {
* @memberof Pattern
* @returns Pattern
* @example
* "42 42.1 42.5 43".ceil().note()
* note("42 42.1 42.5 43".ceil())
*/
export const ceil = register('ceil', function (pat) {
return pat.asNumber().fmap((v) => Math.ceil(v));
@ -1524,7 +1531,8 @@ export const fromBipolar = register('fromBipolar', function (pat) {
* @memberof Pattern
* @returns Pattern
* @example
* s("bd sd,hh*4").cutoff(sine.range(500,2000).slow(4))
* s("[bd sd]*2,hh*8")
* .cutoff(sine.range(500,4000))
*/
export const range = register('range', function (min, max, pat) {
return pat.mul(max - min).add(min);
@ -1538,7 +1546,8 @@ export const range = register('range', function (min, max, pat) {
* @memberof Pattern
* @returns Pattern
* @example
* s("bd sd,hh*4").cutoff(sine.rangex(500,2000).slow(4))
* s("[bd sd]*2,hh*8")
* .cutoff(sine.rangex(500,4000))
*/
export const rangex = register('rangex', function (min, max, pat) {
return pat._range(Math.log(min), Math.log(max)).fmap(Math.exp);
@ -1551,7 +1560,8 @@ export const rangex = register('rangex', function (min, max, pat) {
* @memberof Pattern
* @returns Pattern
* @example
* s("bd sd,hh*4").cutoff(sine2.range2(500,2000).slow(4))
* s("[bd sd]*2,hh*8")
* .cutoff(sine2.range2(500,4000))
*/
export const range2 = register('range2', function (min, max, pat) {
return pat.fromBipolar()._range(min, max);
@ -1564,7 +1574,8 @@ export const range2 = register('range2', function (min, max, pat) {
* @memberof Pattern
* @returns Pattern
* @example
* ratio("1, 5:4, 3:2").mul(110).freq().s("piano").slow(2)
* ratio("1, 5:4, 3:2").mul(110)
* .freq().s("piano")
*/
export const ratio = register('ratio', (pat) =>
pat.fmap((v) => {
@ -1636,7 +1647,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)
*/
@ -1667,7 +1678,7 @@ export const ply = register('ply', function (factor, pat) {
* @param {number | Pattern} factor speed up factor
* @returns Pattern
* @example
* s("<bd sd> hh").fast(2) // s("[<bd sd> hh]*2")
* s("bd hh sd hh").fast(2) // s("[bd hh sd hh]*2")
*/
export const { fast, density } = register(['fast', 'density'], function (factor, pat) {
if (factor === 0) {
@ -1696,7 +1707,7 @@ export const hurry = register('hurry', function (r, pat) {
* @param {number | Pattern} factor slow down factor
* @returns Pattern
* @example
* s("<bd sd> hh").slow(2) // s("[<bd sd> hh]/2")
* s("bd hh sd hh").slow(2) // s("[bd hh sd hh]/2")
*/
export const { slow, sparsity } = register(['slow', 'sparsity'], function (factor, pat) {
if (factor === 0) {
@ -1753,7 +1764,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
@ -1785,9 +1796,9 @@ export const apply = register('apply', function (func, pat) {
* @example
* s("<bd sd>,hh*2").cpm(90) // = 90 bpm
*/
// TODO - global clock
// this is redefined in repl.mjs, using the current cps as divisor
export const cpm = register('cpm', function (cpm, pat) {
return pat._fast(cpm / 60);
return pat._fast(cpm / 60 / 1);
});
/**
@ -1860,7 +1871,7 @@ export const linger = register('linger', function (t, pat) {
* Samples the pattern at a rate of n events per cycle. Useful for turning a continuous pattern into a discrete one.
* @param {number} segments number of segments per cycle
* @example
* note(saw.range(0,12).segment(24)).add(40)
* note(saw.range(40,52).segment(24))
*/
export const segment = register('segment', function (rate, pat) {
return pat.struct(pure(true)._fast(rate));
@ -1886,7 +1897,7 @@ export const { invert, inv } = register(['invert', 'inv'], function (pat) {
* @param {function} func
* @returns Pattern
* @example
* "c3 eb3 g3".when("<0 1>/2", x=>x.sub(5)).note()
* "c3 eb3 g3".when("<0 1>/2", x=>x.sub("5")).note()
*/
export const when = register('when', function (on, func, pat) {
return on ? func(pat) : pat;
@ -1923,7 +1934,7 @@ export const brak = register('brak', function (pat) {
* @memberof Pattern
* @returns Pattern
* @example
* note("c3 d3 e3 g3").rev()
* note("c d e g").rev()
*/
export const rev = register('rev', function (pat) {
const query = function (state) {
@ -1993,7 +2004,7 @@ export const palindrome = register('palindrome', function (pat) {
* @name juxBy
* @synonyms juxby
* @example
* s("lt ht mt ht hh").juxBy("<0 .5 1>/2", rev)
* s("bd lt [~ ht] mt cp ~ bd hh").juxBy("<0 .5 1>/2", rev)
*/
export const { juxBy, juxby } = register(['juxBy', 'juxby'], function (by, func, pat) {
by /= 2;
@ -2012,7 +2023,11 @@ export const { juxBy, juxby } = register(['juxBy', 'juxby'], function (by, func,
/**
* The jux function creates strange stereo effects, by applying a function to a pattern, but only in the right-hand channel.
* @example
* s("lt ht mt ht hh").jux(rev)
* 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))
*/
export const jux = register('jux', function (func, pat) {
return pat._juxBy(1, func, pat);
@ -2028,7 +2043,7 @@ export const jux = register('jux', function (func, pat) {
* @example
* "<0 [2 4]>"
* .echoWith(4, 1/8, (p,n) => p.add(n*2))
* .scale('C minor').note().clip(.2)
* .scale("C:minor").note()
*/
export const { echoWith, echowith, stutWith, stutwith } = register(
['echoWith', 'echowith', 'stutWith', 'stutwith'],
@ -2121,7 +2136,8 @@ const { repeatCycles } = register('repeatCycles', _repeatCycles);
* @memberof Pattern
* @returns Pattern
* @example
* "0 1 2 3".chunk(4, x=>x.add(7)).scale('A minor').note()
* "0 1 2 3".chunk(4, x=>x.add(7))
* .scale("A:minor").note()
*/
const _chunk = function (n, func, pat, back = false, fast = false) {
const binary = Array(n - 1).fill(false);
@ -2146,7 +2162,8 @@ const { chunk, slowchunk, slowChunk } = register(['chunk', 'slowchunk', 'slowChu
* @memberof Pattern
* @returns Pattern
* @example
* "0 1 2 3".chunkBack(4, x=>x.add(7)).scale('A minor').note()
* "0 1 2 3".chunkBack(4, x=>x.add(7))
* .scale("A:minor").note()
*/
export const { chunkBack, chunkback } = register(['chunkBack', 'chunkback'], function (n, func, pat) {
return _chunk(n, func, pat, true);
@ -2160,10 +2177,11 @@ export const { chunkBack, chunkback } = register(['chunkBack', 'chunkback'], fun
* @memberof Pattern
* @returns Pattern
* @example
* "<0 8> 1 2 3 4 5 6 7".fastChunk(4, x => x.color('red')).slow(4).scale("C2:major").note()
.s("folkharp")
* "<0 8> 1 2 3 4 5 6 7"
* .fastChunk(4, x => x.color('red')).slow(2)
* .scale("C2:major").note()
*/
const { fastchunk, fastChunk } = register(['fastchunk', 'fastChunk'], function (n, func, pat) {
export const { fastchunk, fastChunk } = register(['fastchunk', 'fastChunk'], function (n, func, pat) {
return _chunk(n, func, pat, false, true);
});
@ -2178,6 +2196,8 @@ export const bypass = register('bypass', function (on, pat) {
* @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).fast(2)
* @example
* // Looping a portion of randomness
* note(irand(8).segment(4).scale('C3 minor')).ribbon(1337, 2)
*/
@ -2251,7 +2271,7 @@ export const legato = register('legato', function (value, pat) {
* s("rhodes")
* .chop(4)
* .rev() // reverse order of chops
* .loopAt(4) // fit sample into 4 cycles
* .loopAt(2) // fit sample into 2 cycles
*
*/
export const chop = register('chop', function (n, pat) {
@ -2269,7 +2289,7 @@ export const chop = register('chop', function (n, pat) {
* @memberof Pattern
* @returns Pattern
* @example
* s("numbers:0 numbers:1 numbers:2").striate(6).slow(6)
* s("numbers:0 numbers:1 numbers:2").striate(6).slow(3)
*/
export const striate = register('striate', function (n, pat) {
const slices = Array.from({ length: n }, (x, i) => i);
@ -2285,10 +2305,10 @@ export const striate = register('striate', function (n, pat) {
* @returns Pattern
* @example
* samples({ rhodes: 'https://cdn.freesound.org/previews/132/132051_316502-lq.mp3' })
* s("rhodes").loopAt(4)
* s("rhodes").loopAt(2)
*/
// TODO - global cps clock
const _loopAt = function (factor, pat, cps = 1) {
const _loopAt = function (factor, pat, cps = 0.5) {
return pat
.speed((1 / factor) * cps)
.unit('c')
@ -2303,10 +2323,10 @@ const _loopAt = function (factor, pat, cps = 1) {
* @returns Pattern
* @example
* await samples('github:tidalcycles/Dirt-Samples/master')
* s("breaks165").slice(8, "0 1 <2 2*2> 3 [4 0] 5 6 7".every(3, rev)).slow(1.5)
* s("breaks165").slice(8, "0 1 <2 2*2> 3 [4 0] 5 6 7".every(3, rev)).slow(0.75)
* @example
* await samples('github:tidalcycles/Dirt-Samples/master')
* s("breaks125/2").fit().slice([0,.25,.5,.75], "0 1 1 <2 3>")
* s("breaks125").fit().slice([0,.25,.5,.75], "0 1 1 <2 3>")
*/
export const slice = register(
@ -2334,50 +2354,51 @@ export const slice = register(
* await samples('github:tidalcycles/Dirt-Samples/master')
* s("breaks165")
* .splice(8, "0 1 [2 3 0]@2 3 0@2 7")
* .hurry(0.65)
*/
export const splice = register(
'splice',
function (npat, ipat, opat) {
const sliced = slice(npat, ipat, opat);
return sliced.withHap(function (hap) {
return hap.withValue((v) => ({
...{
speed: (1 / v._slices / hap.whole.duration) * (v.speed || 1),
unit: 'c',
},
...v,
}));
return new Pattern((state) => {
// TODO - default cps to 0.5
const cps = state.controls._cps || 1;
const haps = sliced.query(state);
return haps.map((hap) =>
hap.withValue((v) => ({
...{
speed: (cps / v._slices / hap.whole.duration) * (v.speed || 1),
unit: 'c',
},
...v,
})),
);
});
},
false, // turns off auto-patternification
);
// this function will be redefined in repl.mjs to use the correct cps value.
// It is still here to work in cases where repl.mjs is not used
export const { loopAt, loopat } = register(['loopAt', 'loopat'], function (factor, pat) {
return _loopAt(factor, pat, 1);
return new Pattern((state) => _loopAt(factor, pat, state.controls._cps).query(state));
});
// the fit function will be redefined in repl.mjs to use the correct cps value.
// 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' })
* s("rhodes/4").fit()
* s("rhodes/2").fit()
*/
export const fit = register('fit', (pat) =>
pat.withHap((hap) =>
hap.withValue((v) => ({
...v,
speed: 1 / hap.whole.duration,
unit: 'c',
})),
pat.withHaps((haps, state) =>
haps.map((hap) =>
hap.withValue((v) => ({
...v,
speed: (state.controls._cps || 1) / hap.whole.duration,
unit: 'c',
})),
),
),
);

View file

@ -160,8 +160,13 @@ export function pianoroll({
maxMidi = max;
valueExtent = maxMidi - minMidi + 1;
}
// foldValues = values.sort((a, b) => a - b);
foldValues = values.sort((a, b) => String(a).localeCompare(String(b)));
foldValues = values.sort((a, b) =>
typeof a === 'number' && typeof b === 'number'
? a - b
: typeof a === 'number'
? 1
: String(a).localeCompare(String(b)),
);
barThickness = fold ? valueAxis / foldValues.length : valueAxis / valueExtent;
ctx.fillStyle = background;
ctx.globalAlpha = 1; // reset!

View file

@ -74,12 +74,67 @@ export function repl({
scheduler.setPattern(pattern, autostart);
};
setTime(() => scheduler.now()); // TODO: refactor?
const stop = () => scheduler.stop();
const start = () => scheduler.start();
const pause = () => scheduler.pause();
const toggle = () => scheduler.toggle();
const setCps = (cps) => scheduler.setCps(cps);
const setCpm = (cpm) => scheduler.setCps(cpm / 60);
const all = function (transform) {
allTransform = transform;
return silence;
};
// set pattern methods that use this repl via closure
const injectPatternMethods = () => {
Pattern.prototype.p = function (id) {
pPatterns[id] = this;
return this;
};
Pattern.prototype.q = function (id) {
return silence;
};
try {
for (let i = 1; i < 10; ++i) {
Object.defineProperty(Pattern.prototype, `d${i}`, {
get() {
return this.p(i);
},
configurable: true,
});
Object.defineProperty(Pattern.prototype, `p${i}`, {
get() {
return this.p(i);
},
configurable: true,
});
Pattern.prototype[`q${i}`] = silence;
}
} catch (err) {
console.warn('injectPatternMethods: error:', err);
}
const cpm = register('cpm', function (cpm, pat) {
return pat._fast(cpm / 60 / scheduler.cps);
});
evalScope({
all,
hush,
cpm,
setCps,
setcps: setCps,
setCpm,
setcpm: setCpm,
});
};
const evaluate = async (code, autostart = true, shouldHush = true) => {
if (!code) {
throw new Error('no code to evaluate');
}
try {
updateState({ code, pending: true });
injectPatternMethods();
await beforeEval?.({ code });
shouldHush && hush();
let { pattern, meta } = await _evaluate(code, transpiler);
@ -107,74 +162,11 @@ export function repl({
afterEval?.({ code, pattern, meta });
return pattern;
} catch (err) {
// console.warn(`[repl] eval error: ${err.message}`);
logger(`[eval] error: ${err.message}`, 'error');
updateState({ evalError: err, pending: false });
onEvalError?.(err);
}
};
const stop = () => scheduler.stop();
const start = () => scheduler.start();
const pause = () => scheduler.pause();
const toggle = () => scheduler.toggle();
const setCps = (cps) => scheduler.setCps(cps);
const setCpm = (cpm) => scheduler.setCps(cpm / 60);
// the following functions use the cps value, which is why they are defined here..
const loopAt = register('loopAt', (cycles, pat) => {
return pat.loopAtCps(cycles, scheduler.cps);
});
Pattern.prototype.p = function (id) {
pPatterns[id] = this;
return this;
};
Pattern.prototype.q = function (id) {
return silence;
};
const all = function (transform) {
allTransform = transform;
return silence;
};
try {
for (let i = 1; i < 10; ++i) {
Object.defineProperty(Pattern.prototype, `d${i}`, {
get() {
return this.p(i);
},
});
Object.defineProperty(Pattern.prototype, `p${i}`, {
get() {
return this.p(i);
},
});
Pattern.prototype[`q${i}`] = silence;
}
} catch (err) {
// already defined..
}
const fit = register('fit', (pat) =>
pat.withHap((hap) =>
hap.withValue((v) => ({
...v,
speed: scheduler.cps / hap.whole.duration, // overwrite speed completely?
unit: 'c',
})),
),
);
evalScope({
loopAt,
fit,
all,
hush,
setCps,
setcps: setCps,
setCpm,
setcpm: setCpm,
});
const setCode = (code) => updateState({ code });
return { scheduler, evaluate, start, stop, pause, setCps, setPattern, setCode, toggle, state };
}

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
@ -27,9 +27,11 @@ export const isaw2 = isaw.toBipolar();
*
* @return {Pattern}
* @example
* "c3 [eb3,g3] g2 [g3,bb3]".note().clip(saw.slow(4))
* note("<c3 [eb3,g3] g2 [g3,bb3]>*8")
* .clip(saw.slow(2))
* @example
* saw.range(0,8).segment(8).scale('C major').slow(4).note()
* n(saw.range(0,8).segment(8))
* .scale('C major')
*
*/
export const saw = signal((t) => t % 1);
@ -42,7 +44,8 @@ export const sine2 = signal((t) => Math.sin(Math.PI * 2 * t));
*
* @return {Pattern}
* @example
* sine.segment(16).range(0,15).slow(2).scale('C minor').note()
* n(sine.segment(16).range(0,15))
* .scale("C:minor")
*
*/
export const sine = sine2.fromBipolar();
@ -52,7 +55,8 @@ export const sine = sine2.fromBipolar();
*
* @return {Pattern}
* @example
* stack(sine,cosine).segment(16).range(0,15).slow(2).scale('C minor').note()
* n(stack(sine,cosine).segment(16).range(0,15))
* .scale("C:minor")
*
*/
export const cosine = sine._early(Fraction(1).div(4));
@ -63,7 +67,7 @@ export const cosine2 = sine2._early(Fraction(1).div(4));
*
* @return {Pattern}
* @example
* square.segment(2).range(0,7).scale('C minor').note()
* n(square.segment(4).range(0,7)).scale("C:minor")
*
*/
export const square = signal((t) => Math.floor((t * 2) % 2));
@ -74,7 +78,7 @@ export const square2 = square.toBipolar();
*
* @return {Pattern}
* @example
* tri.segment(8).range(0,7).scale('C minor').note()
* n(tri.segment(8).range(0,7)).scale("C:minor")
*
*/
export const tri = fastcat(isaw, saw);
@ -118,8 +122,8 @@ const timeToRands = (t, n) => timeToRandsPrime(timeToIntSeed(t), n);
/**
* A discrete pattern of numbers from 0 to n-1
* @example
* run(4).scale('C4 major').note()
* // "0 1 2 3".scale('C4 major').note()
* n(run(4)).scale("C4:pentatonic")
* // n("0 1 2 3").scale("C4:pentatonic")
*/
export const run = (n) => saw.range(0, n).floor().segment(n);
@ -129,7 +133,7 @@ export const run = (n) => saw.range(0, n).floor().segment(n);
* @name rand
* @example
* // randomly change the cutoff
* s("bd sd,hh*4").cutoff(rand.range(500,2000))
* s("bd*4,hh*8").cutoff(rand.range(500,8000))
*
*/
export const rand = signal(timeToRand);
@ -151,36 +155,127 @@ export const _irand = (i) => rand.fmap((x) => Math.trunc(x * i));
* @param {number} n max value (exclusive)
* @example
* // randomly select scale notes from 0 - 7 (= C to C)
* irand(8).struct("x(3,8)").scale('C minor').note()
* n(irand(8)).struct("x x*2 x x*3").scale("C:minor")
*
*/
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();
});
/** * pickF lets you use a pattern of numbers to pick which function to apply to another pattern.
* @param {Pattern} pat
* @param {Pattern} lookup a pattern of indices
* @param {function[]} funcs the array of functions from which to pull
* @returns {Pattern}
* @example
* s("bd [rim hh]").pickF("<0 1 2>", [rev,jux(rev),fast(2)])
* @example
* note("<c2 d2>(3,8)").s("square")
* .pickF("<0 2> 1", [jux(rev),fast(2),x=>x.lpf(800)])
*/
export const pickF = register('pickF', function (lookup, funcs, pat) {
return pat.apply(pick(lookup, funcs));
});
/** * The same as `pickF`, but if you pick a number greater than the size of the functions list,
* it wraps around, rather than sticking at the maximum value.
* @param {Pattern} pat
* @param {Pattern} lookup a pattern of indices
* @param {function[]} funcs the array of functions from which to pull
* @returns {Pattern}
*/
export const pickmodF = register('pickmodF', function (lookup, funcs, pat) {
return pat.apply(pickmod(lookup, funcs));
});
/**
* 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
@ -269,9 +364,9 @@ Pattern.prototype.choose2 = function (...xs) {
* Picks one of the elements at random each cycle.
* @returns {Pattern}
* @example
* chooseCycles("bd", "hh", "sd").s().fast(4)
* chooseCycles("bd", "hh", "sd").s().fast(8)
* @example
* "bd | hh | sd".s().fast(4)
* s("bd | hh | sd").fast(8)
*/
export const chooseCycles = (...xs) => chooseInWith(rand.segment(1), xs);
@ -314,7 +409,7 @@ export const perlinWith = (pat) => {
* @name perlin
* @example
* // randomly change the cutoff
* s("bd sd,hh*4").cutoff(perlin.range(500,2000))
* s("bd*4,hh*8").cutoff(perlin.range(500,8000))
*
*/
export const perlin = perlinWith(time.fmap((v) => Number(v)));
@ -356,7 +451,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 +475,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
@ -388,7 +483,7 @@ export const undegrade = register('undegrade', (pat) => pat._undegradeBy(0.5));
* @param {function} function - the transformation to apply
* @returns Pattern
* @example
* s("hh(3,8)").sometimesBy(.4, x=>x.speed("0.5"))
* s("hh*8").sometimesBy(.4, x=>x.speed("0.5"))
*/
export const sometimesBy = register('sometimesBy', function (patx, func, pat) {
@ -406,7 +501,7 @@ export const sometimesBy = register('sometimesBy', function (patx, func, pat) {
* @param {function} function - the transformation to apply
* @returns Pattern
* @example
* s("hh*4").sometimes(x=>x.speed("0.5"))
* s("hh*8").sometimes(x=>x.speed("0.5"))
*/
export const sometimes = register('sometimes', function (func, pat) {
return pat._sometimesBy(0.5, func);
@ -415,7 +510,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
@ -423,7 +518,7 @@ export const sometimes = register('sometimes', function (func, pat) {
* @param {function} function - the transformation to apply
* @returns Pattern
* @example
* s("hh(3,8)").someCyclesBy(.3, x=>x.speed("0.5"))
* s("bd,hh*8").someCyclesBy(.3, x=>x.speed("0.5"))
*/
export const someCyclesBy = register('someCyclesBy', function (patx, func, pat) {
@ -445,7 +540,7 @@ export const someCyclesBy = register('someCyclesBy', function (patx, func, pat)
* @memberof Pattern
* @returns Pattern
* @example
* s("hh(3,8)").someCycles(x=>x.speed("0.5"))
* s("bd,hh*8").someCycles(x=>x.speed("0.5"))
*/
export const someCycles = register('someCycles', function (func, pat) {
return pat._someCyclesBy(0.5, func);

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

@ -86,7 +86,7 @@ export const midi2note = (n) => {
// modulo that works with negative numbers e.g. _mod(-1, 3) = 2. Works on numbers (rather than patterns of numbers, as @mod@ from pattern.mjs does)
export const _mod = (n, m) => ((n % m) + m) % m;
export function nanFallback(value, fallback) {
export function nanFallback(value, fallback = 0) {
if (isNaN(Number(value))) {
logger(`"${value}" is not a number, falling back to ${fallback}`, 'warning');
return fallback;
@ -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)]));
}