From 86bc28dfb3fcfa1b1fa8b3821e5e8455c63b4da8 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 11 Jan 2024 20:46:22 +0100 Subject: [PATCH 01/31] add deprecation notes --- packages/core/README.md | 2 ++ packages/csound/README.md | 3 +++ packages/embed/README.md | 2 ++ packages/midi/README.md | 2 ++ packages/mini/README.md | 2 ++ packages/osc/README.md | 2 ++ packages/serial/README.md | 2 ++ packages/soundfonts/README.md | 3 +++ packages/tonal/README.md | 2 ++ packages/transpiler/README.md | 2 ++ packages/webaudio/README.md | 2 ++ packages/xen/README.md | 2 ++ 12 files changed, 26 insertions(+) create mode 100644 packages/csound/README.md create mode 100644 packages/soundfonts/README.md diff --git a/packages/core/README.md b/packages/core/README.md index 8e42800e..5b01edcb 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -1,5 +1,7 @@ # @strudel.cycles/core +**DEPRECATION NOTE**: This package is old and won't get any updates! The newer version goes by the name of [@strudel/core](https://www.npmjs.com/package/@strudel/core). + This package contains the bare essence of strudel. ## Install diff --git a/packages/csound/README.md b/packages/csound/README.md new file mode 100644 index 00000000..3961bd95 --- /dev/null +++ b/packages/csound/README.md @@ -0,0 +1,3 @@ +# @strudel.cycles/csound + +**DEPRECATION NOTE**: This package is old and won't get any updates! The newer version goes by the name of [@strudel/csound](https://www.npmjs.com/package/@strudel/csound). diff --git a/packages/embed/README.md b/packages/embed/README.md index f293c931..d6ff83d7 100644 --- a/packages/embed/README.md +++ b/packages/embed/README.md @@ -1,5 +1,7 @@ # @strudel.cycles/embed +**DEPRECATION NOTE**: This package is old and won't get any updates! The newer version goes by the name of [@strudel/embed](https://www.npmjs.com/package/@strudel/embed). + This package contains a embeddable web component for the Strudel REPL. ## Usage diff --git a/packages/midi/README.md b/packages/midi/README.md index bf8a85eb..29444cba 100644 --- a/packages/midi/README.md +++ b/packages/midi/README.md @@ -1,5 +1,7 @@ # @strudel.cycles/midi +**DEPRECATION NOTE**: This package is old and won't get any updates! The newer version goes by the name of [@strudel/midi](https://www.npmjs.com/package/@strudel/midi). + This package adds midi functionality to strudel Patterns. ## Install diff --git a/packages/mini/README.md b/packages/mini/README.md index fce7a045..a8232304 100644 --- a/packages/mini/README.md +++ b/packages/mini/README.md @@ -1,5 +1,7 @@ # @strudel.cycles/mini +**DEPRECATION NOTE**: This package is old and won't get any updates! The newer version goes by the name of [@strudel/mini](https://www.npmjs.com/package/@strudel/mini). + This package contains the mini notation parser and pattern generator. ## Install diff --git a/packages/osc/README.md b/packages/osc/README.md index 824d79a4..828dcb9d 100644 --- a/packages/osc/README.md +++ b/packages/osc/README.md @@ -1,5 +1,7 @@ # @strudel.cycles/osc +**DEPRECATION NOTE**: This package is old and won't get any updates! The newer version goes by the name of [@strudel/osc](https://www.npmjs.com/package/@strudel/osc). + OSC output for strudel patterns! Currently only tested with super collider / super dirt. ## Usage diff --git a/packages/serial/README.md b/packages/serial/README.md index 5246c2f8..3eaae91e 100644 --- a/packages/serial/README.md +++ b/packages/serial/README.md @@ -1,3 +1,5 @@ # @strudel.cycles/serial +**DEPRECATION NOTE**: This package is old and won't get any updates! The newer version goes by the name of [@strudel/serial](https://www.npmjs.com/package/@strudel/serial). + This package adds webserial functionality to strudel Patterns, for e.g. sending messages to arduino microcontrollers. diff --git a/packages/soundfonts/README.md b/packages/soundfonts/README.md new file mode 100644 index 00000000..e1423952 --- /dev/null +++ b/packages/soundfonts/README.md @@ -0,0 +1,3 @@ +# @strudel.cycles/soundfonts + +**DEPRECATION NOTE**: This package is old and won't get any updates! The newer version goes by the name of [@strudel/soundfonts](https://www.npmjs.com/package/@strudel/soundfonts). diff --git a/packages/tonal/README.md b/packages/tonal/README.md index 0bb344fd..b95c2e1a 100644 --- a/packages/tonal/README.md +++ b/packages/tonal/README.md @@ -1,5 +1,7 @@ # @strudel.cycles/tonal +**DEPRECATION NOTE**: This package is old and won't get any updates! The newer version goes by the name of [@strudel/tonal](https://www.npmjs.com/package/@strudel/tonal). + This package adds tonal / harmonic functions to strudel Patterns. ## Install diff --git a/packages/transpiler/README.md b/packages/transpiler/README.md index 1b0ed365..8c5ef50f 100644 --- a/packages/transpiler/README.md +++ b/packages/transpiler/README.md @@ -1,5 +1,7 @@ # @strudel.cycles/transpiler +**DEPRECATION NOTE**: This package is old and won't get any updates! The newer version goes by the name of [@strudel/transpiler](https://www.npmjs.com/package/@strudel/transpiler). + This package contains a JS code transpiler with the following features: - add locations of mini notation strings (double quoted or backticked) for highlighting diff --git a/packages/webaudio/README.md b/packages/webaudio/README.md index 8f974a25..36ac0cc8 100644 --- a/packages/webaudio/README.md +++ b/packages/webaudio/README.md @@ -1,5 +1,7 @@ # @strudel.cycles/webaudio +**DEPRECATION NOTE**: This package is old and won't get any updates! The newer version goes by the name of [@strudel/webaudio](https://www.npmjs.com/package/@strudel/webaudio). + This package contains helpers to make music with strudel and the Web Audio API. It is a thin binding to [superdough](https://www.npmjs.com/package/superdough). diff --git a/packages/xen/README.md b/packages/xen/README.md index aed60e4d..cfb75358 100644 --- a/packages/xen/README.md +++ b/packages/xen/README.md @@ -1,5 +1,7 @@ # @strudel.cycles/xen +**DEPRECATION NOTE**: This package is old and won't get any updates! The newer version goes by the name of [@strudel/xen](https://www.npmjs.com/package/@strudel/xen). + This package adds xenharmonic / microtonal functions to strudel Patterns. Further documentation + examples will follow. ## Install From e051d383a07f54a7064e8cb67ce84e05f1de7ca9 Mon Sep 17 00:00:00 2001 From: fnordomat <46D46D1246803312401472B5A7427E237B7908CA> Date: Sat, 13 Jan 2024 11:07:59 +0100 Subject: [PATCH 02/31] add 10 new vowel qualities to formant table --- packages/superdough/vowel.mjs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/superdough/vowel.mjs b/packages/superdough/vowel.mjs index 25357642..dd2fd52d 100644 --- a/packages/superdough/vowel.mjs +++ b/packages/superdough/vowel.mjs @@ -5,6 +5,16 @@ export var vowelFormant = { i: { freqs: [270, 1850, 2900, 3350, 3590], gains: [1, 0.0631, 0.0631, 0.0158, 0.0158], qs: [40, 90, 100, 120, 120] }, o: { freqs: [430, 820, 2700, 3000, 3300], gains: [1, 0.3162, 0.0501, 0.0794, 0.01995], qs: [40, 80, 100, 120, 120] }, u: { freqs: [370, 630, 2750, 3000, 3400], gains: [1, 0.1, 0.0708, 0.0316, 0.01995], qs: [40, 60, 100, 120, 120] }, + ae: { freqs: [650, 1515, 2400, 3000, 3350], gains: [1, 0.5, 0.1008, 0.0631, 0.0126], qs: [80, 90, 120, 130, 140] }, + aa: { freqs: [560, 900, 2570, 3000, 3300], gains: [1, 0.5, 0.0708, 0.0631, 0.0126], qs: [80, 90, 120, 130, 140] }, + oe: { freqs: [500, 1430, 2300, 3000, 3300], gains: [1, 0.2, 0.0708, 0.0316, 0.01995], qs: [40, 60, 100, 120, 120] }, + ue: { freqs: [250, 1750, 2150, 3200, 3300], gains: [1, 0.1, 0.0708, 0.0316, 0.01995], qs: [40, 60, 100, 120, 120] }, + y: { freqs: [400, 1460, 2400, 3000, 3300], gains: [1, 0.2, 0.0708, 0.0316, 0.02995], qs: [40, 60, 100, 120, 120] }, + uh: { freqs: [600, 1250, 2100, 3100, 3500], gains: [1, 0.3, 0.0608, 0.0316, 0.01995], qs: [40, 70, 100, 120, 130] }, + un: { freqs: [500, 1240, 2280, 3000, 3500], gains: [1, 0.1, 0.1708, 0.0216, 0.02995], qs: [40, 60, 100, 120, 120] }, + en: { freqs: [600, 1480, 2450, 3200, 3300], gains: [1, 0.15, 0.0708, 0.0316, 0.02995], qs: [40, 60, 100, 120, 120] }, + an: { freqs: [700, 1050, 2500, 3000, 3300], gains: [1, 0.1, 0.0708, 0.0316, 0.02995], qs: [40, 60, 100, 120, 120] }, + on: { freqs: [500, 1080, 2350, 3000, 3300], gains: [1, 0.1, 0.0708, 0.0316, 0.02995], qs: [40, 60, 100, 120, 120] }, }; if (typeof GainNode !== 'undefined') { class VowelNode extends GainNode { From 6232560273e5bafbb65854f02d7d5914470cd887 Mon Sep 17 00:00:00 2001 From: fnordomat <46D46D1246803312401472B5A7427E237B7908CA> Date: Sat, 13 Jan 2024 11:07:59 +0100 Subject: [PATCH 03/31] document extra vowels in quick reference --- packages/core/controls.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index d979ff23..f449f85f 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -1209,7 +1209,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] [ɯ] [ʌ] [œ̃] [ɛ̃] [ɑ̃] [ɔ̃]. * @example * note("c2 >").s('sawtooth') * .vowel(">") From efa0737660b9d13ba127ed6eaea6db8d35053afa Mon Sep 17 00:00:00 2001 From: fnordomat <46D46D1246803312401472B5A7427E237B7908CA> Date: Sat, 13 Jan 2024 15:11:30 +0100 Subject: [PATCH 04/31] added some aliases for the vowels (cosmetics) --- packages/core/controls.mjs | 2 +- packages/superdough/vowel.mjs | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index f449f85f..8605d7be 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -1209,7 +1209,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 ae aa oe ue y uh un en an on, corresponding to [a] [e] [i] [o] [u] [æ] [ɑ] [ø] [y] [ɯ] [ʌ] [œ̃] [ɛ̃] [ɑ̃] [ɔ̃]. + * @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 >").s('sawtooth') * .vowel(">") diff --git a/packages/superdough/vowel.mjs b/packages/superdough/vowel.mjs index dd2fd52d..7d5dac4a 100644 --- a/packages/superdough/vowel.mjs +++ b/packages/superdough/vowel.mjs @@ -15,6 +15,14 @@ export var vowelFormant = { en: { freqs: [600, 1480, 2450, 3200, 3300], gains: [1, 0.15, 0.0708, 0.0316, 0.02995], qs: [40, 60, 100, 120, 120] }, an: { freqs: [700, 1050, 2500, 3000, 3300], gains: [1, 0.1, 0.0708, 0.0316, 0.02995], qs: [40, 60, 100, 120, 120] }, on: { freqs: [500, 1080, 2350, 3000, 3300], gains: [1, 0.1, 0.0708, 0.0316, 0.02995], qs: [40, 60, 100, 120, 120] }, + get æ() { return this.ae; }, + get ø() { return this.oe; }, + get ɑ() { return this.aa; }, + get å() { return this.aa; }, + get ö() { return this.oe; }, + get ü() { return this.ue; }, + get ı() { return this.y; }, + }; if (typeof GainNode !== 'undefined') { class VowelNode extends GainNode { From 70712bdab653825507e5334c9dcb99cf0f2430c3 Mon Sep 17 00:00:00 2001 From: fnordomat <46D46D1246803312401472B5A7427E237B7908CA> Date: Sun, 14 Jan 2024 10:19:49 +0100 Subject: [PATCH 05/31] fix codeformat --- packages/superdough/vowel.mjs | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/packages/superdough/vowel.mjs b/packages/superdough/vowel.mjs index 7d5dac4a..3f30aef1 100644 --- a/packages/superdough/vowel.mjs +++ b/packages/superdough/vowel.mjs @@ -15,14 +15,27 @@ export var vowelFormant = { en: { freqs: [600, 1480, 2450, 3200, 3300], gains: [1, 0.15, 0.0708, 0.0316, 0.02995], qs: [40, 60, 100, 120, 120] }, an: { freqs: [700, 1050, 2500, 3000, 3300], gains: [1, 0.1, 0.0708, 0.0316, 0.02995], qs: [40, 60, 100, 120, 120] }, on: { freqs: [500, 1080, 2350, 3000, 3300], gains: [1, 0.1, 0.0708, 0.0316, 0.02995], qs: [40, 60, 100, 120, 120] }, - get æ() { return this.ae; }, - get ø() { return this.oe; }, - get ɑ() { return this.aa; }, - get å() { return this.aa; }, - get ö() { return this.oe; }, - get ü() { return this.ue; }, - get ı() { return this.y; }, - + get æ() { + return this.ae; + }, + get ø() { + return this.oe; + }, + get ɑ() { + return this.aa; + }, + get å() { + return this.aa; + }, + get ö() { + return this.oe; + }, + get ü() { + return this.ue; + }, + get ı() { + return this.y; + }, }; if (typeof GainNode !== 'undefined') { class VowelNode extends GainNode { From bdc2af9733574ae4fbe9817a426a943a3b9b329e Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Mon, 15 Jan 2024 23:25:22 +0100 Subject: [PATCH 06/31] basic pitch envelope --- packages/core/controls.mjs | 7 +++++++ packages/superdough/synth.mjs | 12 ++++++++++++ 2 files changed, 19 insertions(+) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 97129779..13856608 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -891,6 +891,13 @@ const generic_params = [ * */ ['freq'], + // pitch envelope + ['pattack', 'patt'], + ['pdecay', 'pdec'], + ['psustain', 'psus'], + ['prelease', 'prel'], + ['penv'], + ['panchor'], // TODO // TODO: https://tidalcycles.org/docs/configuration/MIDIOSC/control-voltage/#gate ['gate', 'gat'], // ['hatgrain'], diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index 6a3e381a..d484284b 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -126,6 +126,12 @@ export function getOscillator( fmvelocity: fmVelocity, fmwave: fmWaveform = 'sine', duration, + penv, + // panchor = 0, // TODO + pattack, + pdecay, + psustain, + prelease, }, ) { let ac = getAudioContext(); @@ -196,6 +202,12 @@ export function getOscillator( vibratoOscillator.start(t); } + // pitch envelope + if (penv) { + const holdEnd = t + duration; + getParamADSR(o.detune, pattack, pdecay, psustain, prelease, 0, penv * 100, t, holdEnd, 'linear'); + } + let noiseMix; if (noise) { noiseMix = getNoiseMix(o, noise, t); From 32456d69663acce0b6932f06c8c64e6e2c4e7b73 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Mon, 15 Jan 2024 23:55:49 +0100 Subject: [PATCH 07/31] pitch envelope for sampler and soundfonts + added getPitchEnvelope helper --- packages/soundfonts/fontloader.mjs | 13 ++++++++++++- packages/superdough/helpers.mjs | 13 +++++++++++++ packages/superdough/sampler.mjs | 7 ++++++- packages/superdough/synth.mjs | 21 ++++++--------------- 4 files changed, 37 insertions(+), 17 deletions(-) diff --git a/packages/soundfonts/fontloader.mjs b/packages/soundfonts/fontloader.mjs index dc3f4b61..fced1c28 100644 --- a/packages/soundfonts/fontloader.mjs +++ b/packages/soundfonts/fontloader.mjs @@ -1,5 +1,11 @@ import { noteToMidi, freqToMidi, getSoundIndex } from '@strudel.cycles/core'; -import { getAudioContext, registerSound, getParamADSR, getADSRValues } from '@strudel.cycles/webaudio'; +import { + getAudioContext, + registerSound, + getParamADSR, + getADSRValues, + getPitchEnvelope, +} from '@strudel.cycles/webaudio'; import gm from './gm.mjs'; let loadCache = {}; @@ -149,6 +155,11 @@ export function registerSoundfonts() { getParamADSR(node.gain, attack, decay, sustain, release, 0, 0.3, time, holdEnd, 'linear'); let envEnd = holdEnd + release + 0.01; + // pitch envelope + if (value.penv) { + getPitchEnvelope(bufferSource.detune, value, time, holdEnd); + } + bufferSource.stop(envEnd); const stop = (releaseTime) => {}; bufferSource.onended = () => { diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index bee26f38..d69afd36 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -144,3 +144,16 @@ export function drywet(dry, wet, wetAmount = 0) { wet_gain.connect(mix); return mix; } + +export function getPitchEnvelope(param, value, t, holdEnd) { + if (value.penv) { + let [pattack, pdecay, psustain, prelease] = getADSRValues([ + value.pattack, + value.pdecay, + value.psustain, + value.prelease, + ]); + const cents = value.penv * 100; + getParamADSR(param, pattack, pdecay, psustain, prelease, 0, cents, t, holdEnd, 'linear'); + } +} diff --git a/packages/superdough/sampler.mjs b/packages/superdough/sampler.mjs index 932be995..5e842da5 100644 --- a/packages/superdough/sampler.mjs +++ b/packages/superdough/sampler.mjs @@ -1,6 +1,6 @@ import { noteToMidi, valueToMidi, getSoundIndex } from './util.mjs'; import { getAudioContext, registerSound } from './index.mjs'; -import { getADSRValues, getParamADSR } from './helpers.mjs'; +import { getADSRValues, getParamADSR, getPitchEnvelope } from './helpers.mjs'; import { logger } from './logger.mjs'; const bufferCache = {}; // string: Promise @@ -310,6 +310,11 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) { getParamADSR(node.gain, attack, decay, sustain, release, 0, 1, t, holdEnd, 'linear'); + // pitch envelope + if (value.penv) { + getPitchEnvelope(bufferSource.detune, value, t, holdEnd); + } + const out = ac.createGain(); // we need a separate gain for the cutgroups because firefox... node.connect(out); bufferSource.onended = function () { diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index d484284b..9d70e037 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -1,6 +1,6 @@ import { midiToFreq, noteToMidi } from './util.mjs'; import { registerSound, getAudioContext } from './superdough.mjs'; -import { gainNode, getADSRValues, getParamADSR } from './helpers.mjs'; +import { gainNode, getADSRValues, getParamADSR, getPitchEnvelope } from './helpers.mjs'; import { getNoiseMix, getNoiseOscillator } from './noise.mjs'; const mod = (freq, range = 1, type = 'sine') => { @@ -105,10 +105,8 @@ export function waveformN(partials, type) { } // expects one of waveforms as s -export function getOscillator( - s, - t, - { +export function getOscillator(s, t, value) { + let { n: partials, note, freq, @@ -126,14 +124,7 @@ export function getOscillator( fmvelocity: fmVelocity, fmwave: fmWaveform = 'sine', duration, - penv, - // panchor = 0, // TODO - pattack, - pdecay, - psustain, - prelease, - }, -) { + } = value; let ac = getAudioContext(); let o; // If no partials are given, use stock waveforms @@ -203,9 +194,9 @@ export function getOscillator( } // pitch envelope - if (penv) { + if (value.penv) { const holdEnd = t + duration; - getParamADSR(o.detune, pattack, pdecay, psustain, prelease, 0, penv * 100, t, holdEnd, 'linear'); + getPitchEnvelope(o.detune, value, t, holdEnd); } let noiseMix; From 3506de8d1a5c412537961ba32c2821b7c666598a Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Tue, 16 Jan 2024 00:07:56 +0100 Subject: [PATCH 08/31] vibrato for soundfonts + add getVibratoOscillator helper --- packages/soundfonts/fontloader.mjs | 4 ++++ packages/superdough/helpers.mjs | 16 ++++++++++++++++ packages/superdough/sampler.mjs | 16 ++-------------- packages/superdough/synth.mjs | 16 ++-------------- 4 files changed, 24 insertions(+), 28 deletions(-) diff --git a/packages/soundfonts/fontloader.mjs b/packages/soundfonts/fontloader.mjs index fced1c28..4be29fa1 100644 --- a/packages/soundfonts/fontloader.mjs +++ b/packages/soundfonts/fontloader.mjs @@ -5,6 +5,7 @@ import { getParamADSR, getADSRValues, getPitchEnvelope, + getVibratoOscillator, } from '@strudel.cycles/webaudio'; import gm from './gm.mjs'; @@ -155,6 +156,8 @@ export function registerSoundfonts() { getParamADSR(node.gain, attack, decay, sustain, release, 0, 0.3, time, holdEnd, 'linear'); let envEnd = holdEnd + release + 0.01; + // vibrato + let vibratoOscillator = getVibratoOscillator(bufferSource.detune, value, time); // pitch envelope if (value.penv) { getPitchEnvelope(bufferSource.detune, value, time, holdEnd); @@ -164,6 +167,7 @@ export function registerSoundfonts() { const stop = (releaseTime) => {}; bufferSource.onended = () => { bufferSource.disconnect(); + vibratoOscillator?.stop(); node.disconnect(); onended(); }; diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index d69afd36..7a11d84d 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -157,3 +157,19 @@ export function getPitchEnvelope(param, value, t, holdEnd) { getParamADSR(param, pattack, pdecay, psustain, prelease, 0, cents, t, holdEnd, 'linear'); } } + +export function getVibratoOscillator(param, value, t) { + const { vibmod = 0.5, vib } = value; + let vibratoOscillator; + if (vib > 0) { + vibratoOscillator = getAudioContext().createOscillator(); + vibratoOscillator.frequency.value = vib; + const gain = getAudioContext().createGain(); + // Vibmod is the amount of vibrato, in semitones + gain.gain.value = vibmod * 100; + vibratoOscillator.connect(gain); + gain.connect(param); + vibratoOscillator.start(t); + return vibratoOscillator; + } +} diff --git a/packages/superdough/sampler.mjs b/packages/superdough/sampler.mjs index 5e842da5..67ee06c0 100644 --- a/packages/superdough/sampler.mjs +++ b/packages/superdough/sampler.mjs @@ -1,6 +1,6 @@ import { noteToMidi, valueToMidi, getSoundIndex } from './util.mjs'; import { getAudioContext, registerSound } from './index.mjs'; -import { getADSRValues, getParamADSR, getPitchEnvelope } from './helpers.mjs'; +import { getADSRValues, getParamADSR, getPitchEnvelope, getVibratoOscillator } from './helpers.mjs'; import { logger } from './logger.mjs'; const bufferCache = {}; // string: Promise @@ -244,8 +244,6 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) { loopEnd = 1, end = 1, duration, - vib, - vibmod = 0.5, } = value; // load sample if (speed === 0) { @@ -263,17 +261,7 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) { const bufferSource = await getSampleBufferSource(s, n, note, speed, freq, bank, resolveUrl); // vibrato - let vibratoOscillator; - if (vib > 0) { - vibratoOscillator = getAudioContext().createOscillator(); - vibratoOscillator.frequency.value = vib; - const gain = getAudioContext().createGain(); - // Vibmod is the amount of vibrato, in semitones - gain.gain.value = vibmod * 100; - vibratoOscillator.connect(gain); - gain.connect(bufferSource.detune); - vibratoOscillator.start(0); - } + let vibratoOscillator = getVibratoOscillator(bufferSource.detune, value, t); // asny stuff above took too long? if (ac.currentTime > t) { diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index 9d70e037..7bde4d3a 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -1,6 +1,6 @@ import { midiToFreq, noteToMidi } from './util.mjs'; import { registerSound, getAudioContext } from './superdough.mjs'; -import { gainNode, getADSRValues, getParamADSR, getPitchEnvelope } from './helpers.mjs'; +import { gainNode, getADSRValues, getParamADSR, getPitchEnvelope, getVibratoOscillator } from './helpers.mjs'; import { getNoiseMix, getNoiseOscillator } from './noise.mjs'; const mod = (freq, range = 1, type = 'sine') => { @@ -110,8 +110,6 @@ export function getOscillator(s, t, value) { n: partials, note, freq, - vib = 0, - vibmod = 0.5, noise = 0, // fm fmh: fmHarmonicity = 1, @@ -181,17 +179,7 @@ export function getOscillator(s, t, value) { } // Additional oscillator for vibrato effect - let vibratoOscillator; - if (vib > 0) { - vibratoOscillator = getAudioContext().createOscillator(); - vibratoOscillator.frequency.value = vib; - const gain = getAudioContext().createGain(); - // Vibmod is the amount of vibrato, in semitones - gain.gain.value = vibmod * 100; - vibratoOscillator.connect(gain); - gain.connect(o.detune); - vibratoOscillator.start(t); - } + let vibratoOscillator = getVibratoOscillator(o.detune, value, t); // pitch envelope if (value.penv) { From fd73571744e46565e797dc729c43ecf5f8897fae Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Tue, 16 Jan 2024 17:56:38 +0100 Subject: [PATCH 09/31] make sure the sustained pitch is the selected note --- packages/superdough/helpers.mjs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index 7a11d84d..797d6db1 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -153,8 +153,11 @@ export function getPitchEnvelope(param, value, t, holdEnd) { value.psustain, value.prelease, ]); - const cents = value.penv * 100; - getParamADSR(param, pattack, pdecay, psustain, prelease, 0, cents, t, holdEnd, 'linear'); + let panchor = value.panchor ?? psustain; + const cents = value.penv * 100; // penv is in semitones + const min = 0 - cents * panchor; + const max = cents - cents * panchor; + getParamADSR(param, pattack, pdecay, psustain, prelease, min, max, t, holdEnd, 'linear'); } } From 3fc0fdbe62c549c8dd53a0076d991d6a41e01128 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Tue, 16 Jan 2024 17:57:52 +0100 Subject: [PATCH 10/31] remove todo --- packages/core/controls.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 13856608..83499300 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -897,7 +897,7 @@ const generic_params = [ ['psustain', 'psus'], ['prelease', 'prel'], ['penv'], - ['panchor'], // TODO + ['panchor'], // TODO: https://tidalcycles.org/docs/configuration/MIDIOSC/control-voltage/#gate ['gate', 'gat'], // ['hatgrain'], From 0655af4d4fbcc50bf6354015489a13bfbad7ef9b Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Wed, 17 Jan 2024 17:35:35 +0100 Subject: [PATCH 11/31] add pcurve --- packages/core/controls.mjs | 1 + packages/superdough/helpers.mjs | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 83499300..08037b1a 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -897,6 +897,7 @@ const generic_params = [ ['psustain', 'psus'], ['prelease', 'prel'], ['penv'], + ['pcurve'], ['panchor'], // TODO: https://tidalcycles.org/docs/configuration/MIDIOSC/control-voltage/#gate ['gate', 'gat'], diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index 797d6db1..a56d985a 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -145,6 +145,7 @@ export function drywet(dry, wet, wetAmount = 0) { return mix; } +let curves = ['linear', 'exponential']; export function getPitchEnvelope(param, value, t, holdEnd) { if (value.penv) { let [pattack, pdecay, psustain, prelease] = getADSRValues([ @@ -157,7 +158,8 @@ export function getPitchEnvelope(param, value, t, holdEnd) { const cents = value.penv * 100; // penv is in semitones const min = 0 - cents * panchor; const max = cents - cents * panchor; - getParamADSR(param, pattack, pdecay, psustain, prelease, min, max, t, holdEnd, 'linear'); + const curve = curves[value.pcurve ?? 0]; + getParamADSR(param, pattack, pdecay, psustain, prelease, min, max, t, holdEnd, curve); } } From 96c2144857543ebf71299b996711eab1a3137162 Mon Sep 17 00:00:00 2001 From: Oscar Byrne Date: Wed, 17 Jan 2024 17:09:11 -0800 Subject: [PATCH 12/31] Use astro Response class for swatch/[name].png.js endpoint --- website/src/pages/swatch/[name].png.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/website/src/pages/swatch/[name].png.js b/website/src/pages/swatch/[name].png.js index 117b75a6..1906dcba 100644 --- a/website/src/pages/swatch/[name].png.js +++ b/website/src/pages/swatch/[name].png.js @@ -14,10 +14,7 @@ export async function GET({ params, request }) { const ctx = canvas.getContext('2d'); pianoroll({ time: 4, haps, ctx, playhead: 1, fold: 1, background: 'transparent', playheadColor: 'transparent' }); const buffer = canvas.toBuffer('image/png'); - return { - body: buffer, - encoding: 'binary', - }; + return new Response(buffer); } export async function getStaticPaths() { const patterns = await getMyPatterns(); From d3ea4959b4513ddff3e2fe2825df5bb843c02ff1 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 18 Jan 2024 06:47:12 +0100 Subject: [PATCH 13/31] - more flexible pitch envelopes - fix: exponential envelopes could break due to 0 values --- packages/superdough/helpers.mjs | 42 ++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index a56d985a..39e9977a 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -31,10 +31,10 @@ export const getParamADSR = ( decay = nanFallback(decay); sustain = nanFallback(sustain); release = nanFallback(release); - const ramp = curve === 'exponential' ? 'exponentialRampToValueAtTime' : 'linearRampToValueAtTime'; if (curve === 'exponential') { - min = Math.max(0.0001, min); + min = min === 0 ? 0.001 : min; + max = max === 0 ? 0.001 : max; } const range = max - min; const peak = max; @@ -42,12 +42,17 @@ export const getParamADSR = ( const duration = end - begin; const envValAtTime = (time) => { + let val; if (attack > time) { let slope = getSlope(min, peak, 0, attack); - return time * slope + (min > peak ? min : 0); + val = time * slope + (min > peak ? min : 0); } else { - return (time - attack) * getSlope(peak, sustainVal, 0, decay) + peak; + val = (time - attack) * getSlope(peak, sustainVal, 0, decay) + peak; } + if (curve === 'exponential') { + val = val || 0.001; + } + return val; }; param.setValueAtTime(min, begin); @@ -147,20 +152,23 @@ export function drywet(dry, wet, wetAmount = 0) { let curves = ['linear', 'exponential']; export function getPitchEnvelope(param, value, t, holdEnd) { - if (value.penv) { - let [pattack, pdecay, psustain, prelease] = getADSRValues([ - value.pattack, - value.pdecay, - value.psustain, - value.prelease, - ]); - let panchor = value.panchor ?? psustain; - const cents = value.penv * 100; // penv is in semitones - const min = 0 - cents * panchor; - const max = cents - cents * panchor; - const curve = curves[value.pcurve ?? 0]; - getParamADSR(param, pattack, pdecay, psustain, prelease, min, max, t, holdEnd, curve); + // envelope is active when any of these values is set + const hasEnvelope = value.pattack ?? value.pdecay ?? value.psustain ?? value.prelease ?? value.penv; + if (!hasEnvelope) { + return; } + const penv = nanFallback(value.penv, 1, true); + const curve = curves[value.pcurve ?? 0]; + let [pattack, pdecay, psustain, prelease] = getADSRValues( + [value.pattack, value.pdecay, value.psustain, value.prelease], + curve, + [0.2, 0.001, 1, 0.001], + ); + let panchor = value.panchor ?? psustain; + const cents = penv * 100; // penv is in semitones + const min = 0 - cents * panchor; + const max = cents - cents * panchor; + getParamADSR(param, pattack, pdecay, psustain, prelease, min, max, t, holdEnd, curve); } export function getVibratoOscillator(param, value, t) { From 1c99944a32ba8b3822a6504c16f6cac180fe0078 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 18 Jan 2024 06:47:44 +0100 Subject: [PATCH 14/31] pitch envelope now also works when setting one of the controls (penv not needed) --- packages/soundfonts/fontloader.mjs | 4 +--- packages/superdough/sampler.mjs | 4 +--- packages/superdough/synth.mjs | 5 +---- 3 files changed, 3 insertions(+), 10 deletions(-) diff --git a/packages/soundfonts/fontloader.mjs b/packages/soundfonts/fontloader.mjs index 4be29fa1..1a3395d5 100644 --- a/packages/soundfonts/fontloader.mjs +++ b/packages/soundfonts/fontloader.mjs @@ -159,9 +159,7 @@ export function registerSoundfonts() { // vibrato let vibratoOscillator = getVibratoOscillator(bufferSource.detune, value, time); // pitch envelope - if (value.penv) { - getPitchEnvelope(bufferSource.detune, value, time, holdEnd); - } + getPitchEnvelope(bufferSource.detune, value, time, holdEnd); bufferSource.stop(envEnd); const stop = (releaseTime) => {}; diff --git a/packages/superdough/sampler.mjs b/packages/superdough/sampler.mjs index 67ee06c0..19fec661 100644 --- a/packages/superdough/sampler.mjs +++ b/packages/superdough/sampler.mjs @@ -299,9 +299,7 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) { getParamADSR(node.gain, attack, decay, sustain, release, 0, 1, t, holdEnd, 'linear'); // pitch envelope - if (value.penv) { - getPitchEnvelope(bufferSource.detune, value, t, holdEnd); - } + getPitchEnvelope(bufferSource.detune, value, t, holdEnd); const out = ac.createGain(); // we need a separate gain for the cutgroups because firefox... node.connect(out); diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index 7bde4d3a..6d646862 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -182,10 +182,7 @@ export function getOscillator(s, t, value) { let vibratoOscillator = getVibratoOscillator(o.detune, value, t); // pitch envelope - if (value.penv) { - const holdEnd = t + duration; - getPitchEnvelope(o.detune, value, t, holdEnd); - } + getPitchEnvelope(o.detune, value, t, t + duration); let noiseMix; if (noise) { From 67f0758cb56573124b276b9d9855b23f9ee1ec3d Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 18 Jan 2024 06:47:54 +0100 Subject: [PATCH 15/31] document pitch envelope --- packages/core/controls.mjs | 68 +++++++++++++++++++++++++++++ website/src/pages/learn/effects.mdx | 54 +++++++++++++++++++++++ 2 files changed, 122 insertions(+) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 08037b1a..4aa5349d 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -892,12 +892,80 @@ const generic_params = [ */ ['freq'], // pitch envelope + /** + * Attack time of pitch envelope. + * + * @name pattack + * @synonyms patt + * @param {number | Pattern} time time in seconds + * @example + * note("").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("").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(" ~") + * .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'], diff --git a/website/src/pages/learn/effects.mdx b/website/src/pages/learn/effects.mdx index a3ae91d7..d0615dd9 100644 --- a/website/src/pages/learn/effects.mdx +++ b/website/src/pages/learn/effects.mdx @@ -138,6 +138,60 @@ There is one filter envelope for each filter type and thus one set of envelope f +## Pitch Envelope + +You can also control the pitch with envelopes! +Pitch envelopes can breathe life into static sounds: + +*<2!3 4>") + .scale("/8:pentatonic") + .s("gm_electric_guitar_jazz") + .penv("<.5 0 7 -2>*2").vib("4:.1") + .phaser(2).delay(.25).room(.3) + .size(4).fast(.75)`} +/> + +You also create some lovely chiptune-style sounds: + +/16")).jux(rev) +.chord(">") +.dict('ireal') +.voicing().add(note("<0 1>/8")) +.dec(.1).room(.2) +.segment("<4 [2 8]>") +.penv("<0 <2 -2>>").patt(.02)`} +/> + +Let's break down all pitch envelope controls: + +## pattack + + + +## pdecay + + + +## prelease + + + +## penv + + + +## pcurve + + + +## panchor + + + # Dynamics ## gain From 4107711dae2667f637f3f3e32055973417683568 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 18 Jan 2024 06:48:21 +0100 Subject: [PATCH 16/31] snapshot --- test/__snapshots__/examples.test.mjs.snap | 58 +++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index dd7392d1..002ccb5f 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -3332,6 +3332,55 @@ exports[`runs examples > example "pan" example index 0 1`] = ` ] `; +exports[`runs examples > example "panchor" example index 0 1`] = ` +[ + "[ 0/1 → 1/1 | note:c penv:12 panchor:0 ]", + "[ 1/1 → 2/1 | note:c penv:12 panchor:0.5 ]", + "[ 2/1 → 3/1 | note:c penv:12 panchor:1 ]", + "[ 3/1 → 4/1 | note:c penv:12 panchor:0.5 ]", +] +`; + +exports[`runs examples > example "pattack" example index 0 1`] = ` +[ + "[ 0/1 → 1/1 | note:c pattack:0 ]", + "[ 1/1 → 2/1 | note:eb pattack:0.1 ]", + "[ 2/1 → 3/1 | note:g pattack:0.25 ]", + "[ 3/1 → 4/1 | note:bb pattack:0.5 ]", +] +`; + +exports[`runs examples > example "pcurve" example index 0 1`] = ` +[ + "[ 0/1 → 1/2 | note:g1 s:sine pdecay:0.5 penv:32 pcurve:0 ]", + "[ 1/2 → 1/1 | note:g1 s:sine pdecay:0.5 penv:32 pcurve:0 ]", + "[ 1/1 → 3/2 | note:g1 s:sine pdecay:0.5 penv:32 pcurve:1 ]", + "[ 3/2 → 2/1 | note:g1 s:sine pdecay:0.5 penv:32 pcurve:1 ]", + "[ 2/1 → 5/2 | note:g1 s:sine pdecay:0.5 penv:32 pcurve:0 ]", + "[ 5/2 → 3/1 | note:g1 s:sine pdecay:0.5 penv:32 pcurve:0 ]", + "[ 3/1 → 7/2 | note:g1 s:sine pdecay:0.5 penv:32 pcurve:1 ]", + "[ 7/2 → 4/1 | note:g1 s:sine pdecay:0.5 penv:32 pcurve:1 ]", +] +`; + +exports[`runs examples > example "pdecay" example index 0 1`] = ` +[ + "[ 0/1 → 1/1 | note:c pdecay:0 ]", + "[ 1/1 → 2/1 | note:eb pdecay:0.1 ]", + "[ 2/1 → 3/1 | note:g pdecay:0.25 ]", + "[ 3/1 → 4/1 | note:bb pdecay:0.5 ]", +] +`; + +exports[`runs examples > example "penv" example index 0 1`] = ` +[ + "[ 0/1 → 1/1 | note:c penv:12 ]", + "[ 1/1 → 2/1 | note:c penv:7 ]", + "[ 2/1 → 3/1 | note:c penv:1 ]", + "[ 3/1 → 4/1 | note:c penv:0.5 ]", +] +`; + exports[`runs examples > example "perlin" example index 0 1`] = ` [ "[ 0/1 → 1/4 | s:hh cutoff:512.5097280354112 ]", @@ -3660,6 +3709,15 @@ exports[`runs examples > example "postgain" example index 0 1`] = ` ] `; +exports[`runs examples > example "prelease" example index 0 1`] = ` +[ + "[ 0/1 → 1/2 | note:c release:0.5 prelease:0 ]", + "[ 1/1 → 3/2 | note:eb release:0.5 prelease:0.1 ]", + "[ 2/1 → 5/2 | note:g release:0.5 prelease:0.25 ]", + "[ 3/1 → 7/2 | note:bb release:0.5 prelease:0.5 ]", +] +`; + exports[`runs examples > example "press" example index 0 1`] = ` [ "[ 0/1 → 1/2 | s:hh ]", From 95cf201c6d0f7d2a68dde16ae860164daa99b548 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 18 Jan 2024 06:49:09 +0100 Subject: [PATCH 17/31] fix: heading --- website/src/pages/learn/effects.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/pages/learn/effects.mdx b/website/src/pages/learn/effects.mdx index d0615dd9..be4a1184 100644 --- a/website/src/pages/learn/effects.mdx +++ b/website/src/pages/learn/effects.mdx @@ -138,7 +138,7 @@ There is one filter envelope for each filter type and thus one set of envelope f -## Pitch Envelope +# Pitch Envelope You can also control the pitch with envelopes! Pitch envelopes can breathe life into static sounds: From b10612da5cf68ee3953e347b62919ada4828b143 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 18 Jan 2024 09:35:20 +0100 Subject: [PATCH 18/31] Publish - @strudel/codemirror@0.10.0 - @strudel.cycles/core@0.10.0 - @strudel.cycles/csound@0.10.0 - @strudel.cycles/embed@0.10.0 - @strudel/hydra@0.10.0 - @strudel.cycles/midi@0.10.0 - @strudel.cycles/mini@0.10.0 - @strudel.cycles/osc@0.10.0 - @strudel/repl@0.10.0 - @strudel.cycles/serial@0.10.0 - @strudel.cycles/soundfonts@0.10.0 - superdough@0.10.0 - @strudel.cycles/tonal@0.10.0 - @strudel.cycles/transpiler@0.10.0 - @strudel/web@0.10.0 - @strudel.cycles/webaudio@0.10.0 - @strudel.cycles/xen@0.10.0 --- packages/codemirror/package.json | 6 +++--- packages/core/package.json | 2 +- packages/csound/package.json | 2 +- packages/embed/package.json | 2 +- packages/hydra/package.json | 2 +- packages/midi/package.json | 2 +- packages/mini/package.json | 2 +- packages/osc/package.json | 2 +- packages/repl/package.json | 2 +- packages/serial/package.json | 2 +- packages/soundfonts/package.json | 2 +- packages/superdough/package.json | 2 +- packages/tonal/package.json | 2 +- packages/transpiler/package.json | 2 +- packages/web/package.json | 2 +- packages/webaudio/package.json | 2 +- packages/xen/package.json | 2 +- 17 files changed, 19 insertions(+), 19 deletions(-) diff --git a/packages/codemirror/package.json b/packages/codemirror/package.json index bd21b513..edf5eb5a 100644 --- a/packages/codemirror/package.json +++ b/packages/codemirror/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/codemirror", - "version": "0.9.0", + "version": "0.10.0", "description": "Codemirror Extensions for Strudel", "main": "index.mjs", "publishConfig": { @@ -41,14 +41,14 @@ "@codemirror/state": "^6.4.0", "@codemirror/view": "^6.23.0", "@lezer/highlight": "^1.2.0", + "@nanostores/persistent": "^0.9.1", "@replit/codemirror-emacs": "^6.0.1", "@replit/codemirror-vim": "^6.1.0", "@replit/codemirror-vscode-keymap": "^6.0.2", "@strudel.cycles/core": "workspace:*", "@uiw/codemirror-themes": "^4.21.21", "@uiw/codemirror-themes-all": "^4.21.21", - "nanostores": "^0.9.5", - "@nanostores/persistent": "^0.9.1" + "nanostores": "^0.9.5" }, "devDependencies": { "vite": "^5.0.10" diff --git a/packages/core/package.json b/packages/core/package.json index 7e382824..320dd5e9 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@strudel.cycles/core", - "version": "0.9.0", + "version": "0.10.0", "description": "Port of Tidal Cycles to JavaScript", "main": "index.mjs", "type": "module", diff --git a/packages/csound/package.json b/packages/csound/package.json index 802afef3..d01f4e1c 100644 --- a/packages/csound/package.json +++ b/packages/csound/package.json @@ -1,6 +1,6 @@ { "name": "@strudel.cycles/csound", - "version": "0.9.0", + "version": "0.10.0", "description": "csound bindings for strudel", "main": "index.mjs", "publishConfig": { diff --git a/packages/embed/package.json b/packages/embed/package.json index ccf8652d..edabf9e9 100644 --- a/packages/embed/package.json +++ b/packages/embed/package.json @@ -1,6 +1,6 @@ { "name": "@strudel.cycles/embed", - "version": "0.2.0", + "version": "0.10.0", "description": "Embeddable Web Component to load a Strudel REPL into an iframe", "main": "embed.js", "type": "module", diff --git a/packages/hydra/package.json b/packages/hydra/package.json index 057a973b..1eeddb81 100644 --- a/packages/hydra/package.json +++ b/packages/hydra/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/hydra", - "version": "0.9.0", + "version": "0.10.0", "description": "Hydra integration for strudel", "main": "hydra.mjs", "publishConfig": { diff --git a/packages/midi/package.json b/packages/midi/package.json index 67df04ce..29d62f7a 100644 --- a/packages/midi/package.json +++ b/packages/midi/package.json @@ -1,6 +1,6 @@ { "name": "@strudel.cycles/midi", - "version": "0.9.0", + "version": "0.10.0", "description": "Midi API for strudel", "main": "index.mjs", "publishConfig": { diff --git a/packages/mini/package.json b/packages/mini/package.json index a234e1fc..35cbbfd6 100644 --- a/packages/mini/package.json +++ b/packages/mini/package.json @@ -1,6 +1,6 @@ { "name": "@strudel.cycles/mini", - "version": "0.9.0", + "version": "0.10.0", "description": "Mini notation for strudel", "main": "index.mjs", "type": "module", diff --git a/packages/osc/package.json b/packages/osc/package.json index d05a3b2c..bacbfafb 100644 --- a/packages/osc/package.json +++ b/packages/osc/package.json @@ -1,6 +1,6 @@ { "name": "@strudel.cycles/osc", - "version": "0.9.0", + "version": "0.10.0", "description": "OSC messaging for strudel", "main": "osc.mjs", "publishConfig": { diff --git a/packages/repl/package.json b/packages/repl/package.json index 04116c5e..6fc39103 100644 --- a/packages/repl/package.json +++ b/packages/repl/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/repl", - "version": "0.9.4", + "version": "0.10.0", "description": "Strudel REPL as a Web Component", "main": "index.mjs", "publishConfig": { diff --git a/packages/serial/package.json b/packages/serial/package.json index 43e4a045..0098a051 100644 --- a/packages/serial/package.json +++ b/packages/serial/package.json @@ -1,6 +1,6 @@ { "name": "@strudel.cycles/serial", - "version": "0.9.0", + "version": "0.10.0", "description": "Webserial API for strudel", "main": "serial.mjs", "publishConfig": { diff --git a/packages/soundfonts/package.json b/packages/soundfonts/package.json index 253d28cc..107d0644 100644 --- a/packages/soundfonts/package.json +++ b/packages/soundfonts/package.json @@ -1,6 +1,6 @@ { "name": "@strudel.cycles/soundfonts", - "version": "0.9.0", + "version": "0.10.0", "description": "Soundsfont support for strudel", "main": "index.mjs", "publishConfig": { diff --git a/packages/superdough/package.json b/packages/superdough/package.json index b0074246..e27f95f2 100644 --- a/packages/superdough/package.json +++ b/packages/superdough/package.json @@ -1,6 +1,6 @@ { "name": "superdough", - "version": "0.9.12", + "version": "0.10.0", "description": "simple web audio synth and sampler intended for live coding. inspired by superdirt and webdirt.", "main": "index.mjs", "type": "module", diff --git a/packages/tonal/package.json b/packages/tonal/package.json index 389e8de8..026fbc75 100644 --- a/packages/tonal/package.json +++ b/packages/tonal/package.json @@ -1,6 +1,6 @@ { "name": "@strudel.cycles/tonal", - "version": "0.9.0", + "version": "0.10.0", "description": "Tonal functions for strudel", "main": "index.mjs", "publishConfig": { diff --git a/packages/transpiler/package.json b/packages/transpiler/package.json index 918c7906..ebd37e56 100644 --- a/packages/transpiler/package.json +++ b/packages/transpiler/package.json @@ -1,6 +1,6 @@ { "name": "@strudel.cycles/transpiler", - "version": "0.9.0", + "version": "0.10.0", "description": "Transpiler for strudel user code. Converts syntactically correct but semantically meaningless JS into evaluatable strudel code.", "main": "index.mjs", "publishConfig": { diff --git a/packages/web/package.json b/packages/web/package.json index c46bc360..1114e9b3 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/web", - "version": "0.9.0", + "version": "0.10.0", "description": "Easy to setup, opiniated bundle of Strudel for the browser.", "main": "web.mjs", "publishConfig": { diff --git a/packages/webaudio/package.json b/packages/webaudio/package.json index 3d810ff1..5c02756a 100644 --- a/packages/webaudio/package.json +++ b/packages/webaudio/package.json @@ -1,6 +1,6 @@ { "name": "@strudel.cycles/webaudio", - "version": "0.9.0", + "version": "0.10.0", "description": "Web Audio helpers for Strudel", "main": "index.mjs", "type": "module", diff --git a/packages/xen/package.json b/packages/xen/package.json index b2d639da..2805b299 100644 --- a/packages/xen/package.json +++ b/packages/xen/package.json @@ -1,6 +1,6 @@ { "name": "@strudel.cycles/xen", - "version": "0.9.0", + "version": "0.10.0", "description": "Xenharmonic API for strudel", "main": "index.mjs", "publishConfig": { From 96bafa7f0b500cc9b96484fadfbca8a50b43a4ec Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 18 Jan 2024 09:54:37 +0100 Subject: [PATCH 19/31] the big rename: @strudel.cycles/* -> @strudel/* --- CONTRIBUTING.md | 2 +- examples/buildless/basic.html | 2 +- examples/buildless/canvas.html | 2 +- examples/codemirror-repl/main.js | 18 +- examples/codemirror-repl/package.json | 12 +- examples/minimal-repl/main.js | 14 +- examples/minimal-repl/package.json | 10 +- package.json | 14 +- packages/README.md | 2 +- packages/codemirror/codemirror.mjs | 2 +- packages/codemirror/package.json | 2 +- packages/codemirror/slider.mjs | 2 +- packages/core/README.md | 10 +- packages/core/index.mjs | 6 +- packages/core/package.json | 2 +- packages/core/pattern.mjs | 4 +- packages/csound/README.md | 4 +- packages/csound/index.mjs | 4 +- packages/csound/package.json | 6 +- packages/desktopbridge/midibridge.mjs | 2 +- packages/desktopbridge/oscbridge.mjs | 2 +- packages/desktopbridge/package.json | 2 +- packages/embed/README.md | 8 +- packages/embed/package.json | 2 +- packages/hydra/README.md | 2 +- packages/hydra/hydra.mjs | 2 +- packages/hydra/package.json | 2 +- packages/midi/README.md | 6 +- packages/midi/midi.mjs | 4 +- packages/midi/package.json | 6 +- packages/mini/README.md | 10 +- packages/mini/mini.mjs | 2 +- packages/mini/package.json | 4 +- packages/mini/test/mini.test.mjs | 2 +- packages/osc/README.md | 4 +- packages/osc/osc.mjs | 2 +- packages/osc/package.json | 4 +- packages/repl/package.json | 14 +- packages/repl/prebake.mjs | 32 +-- packages/repl/repl-component.mjs | 6 +- packages/serial/README.md | 4 +- packages/serial/package.json | 4 +- packages/serial/serial.mjs | 2 +- packages/soundfonts/README.md | 4 +- packages/soundfonts/fontloader.mjs | 4 +- packages/soundfonts/package.json | 6 +- packages/soundfonts/sfumato.mjs | 4 +- packages/tonal/README.md | 12 +- packages/tonal/package.json | 4 +- packages/tonal/test/tonal.test.mjs | 2 +- packages/tonal/tonal.mjs | 2 +- packages/tonal/tonleiter.mjs | 2 +- packages/tonal/voicings.mjs | 2 +- packages/transpiler/README.md | 10 +- packages/transpiler/index.mjs | 2 +- packages/transpiler/package.json | 6 +- packages/transpiler/transpiler.mjs | 4 +- packages/web/package.json | 10 +- packages/web/web.mjs | 32 +-- packages/webaudio/README.md | 10 +- packages/webaudio/package.json | 4 +- packages/webaudio/scope.mjs | 2 +- packages/webaudio/webaudio.mjs | 2 +- packages/xen/README.md | 6 +- packages/xen/package.json | 4 +- packages/xen/tune.mjs | 2 +- packages/xen/xen.mjs | 2 +- pnpm-lock.yaml | 210 +++++++++--------- src-tauri/README.md | 2 +- test/runtime.mjs | 38 ++-- website/package.json | 24 +- website/src/components/PitchSlider.jsx | 4 +- website/src/docs/Colors.jsx | 2 +- website/src/docs/MiniRepl.jsx | 6 +- website/src/pages/de/workshop/first-notes.mdx | 2 +- website/src/pages/img/example-[name].png.js | 4 +- website/src/pages/recipes/microrhythms.mdx | 2 - website/src/pages/recipes/rhythms.mdx | 2 - website/src/pages/swatch/[name].png.js | 4 +- .../src/pages/technical-manual/packages.mdx | 25 ++- website/src/pages/workshop/first-notes.mdx | 2 +- website/src/repl/Repl.jsx | 6 +- website/src/repl/files.mjs | 2 +- website/src/repl/idbutils.mjs | 4 +- website/src/repl/panel/Panel.jsx | 2 +- website/src/repl/panel/SoundsTab.jsx | 2 +- website/src/repl/piano.mjs | 2 +- website/src/repl/prebake.mjs | 10 +- website/src/repl/util.mjs | 22 +- website/src/settings.mjs | 4 +- 90 files changed, 369 insertions(+), 396 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 556df3fc..f9bc117c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -114,7 +114,7 @@ You can run the same check with `pnpm check` ## Package Workflow The project is split into multiple [packages](https://github.com/tidalcycles/strudel/tree/main/packages) with independent versioning. -When you run `pnpm i` on the root folder, [pnpm workspaces](https://pnpm.io/workspaces) will install all dependencies of all subpackages. This will allow any js file to import `@strudel.cycles/` to get the local version, +When you run `pnpm i` on the root folder, [pnpm workspaces](https://pnpm.io/workspaces) will install all dependencies of all subpackages. This will allow any js file to import `@strudel/` to get the local version, allowing to develop multiple packages at the same time. ## Package Publishing diff --git a/examples/buildless/basic.html b/examples/buildless/basic.html index 97508132..bd74d83b 100644 --- a/examples/buildless/basic.html +++ b/examples/buildless/basic.html @@ -7,7 +7,7 @@ />
+ - + diff --git a/website/src/components/HeadSEO.astro b/website/src/components/HeadSEO.astro index 556b50a7..413668c1 100644 --- a/website/src/components/HeadSEO.astro +++ b/website/src/components/HeadSEO.astro @@ -1,5 +1,5 @@ --- -import { SITE, OPEN_GRAPH, Frontmatter } from '../config'; +import { SITE, OPEN_GRAPH, type Frontmatter } from '../config'; export interface Props { frontmatter: Frontmatter; @@ -25,11 +25,3 @@ const imageAlt = frontmatter.image?.alt ?? OPEN_GRAPH.image.alt; - - - - - - - - diff --git a/website/src/pages/rss.xml.js b/website/src/pages/rss.xml.js new file mode 100644 index 00000000..0aeb7bf4 --- /dev/null +++ b/website/src/pages/rss.xml.js @@ -0,0 +1,19 @@ +import rss from '@astrojs/rss'; +import { getCollection } from 'astro:content'; + +export async function GET(context) { + const posts = (await getCollection('blog')).filter((p) => !p.data.draft); + const options = { + title: 'Strudel Blog', + description: + 'The Strudel Blog will keep you updated with the latest changes and things happening in the strudelsphere.', + site: context.site, + items: posts.map((post) => ({ + link: `/${post.slug}/`, + title: post.data.title, + pubDate: post.data.date, + description: post.data.description, + })), + }; + return rss(options); +} From 2ed5f5aa654122cccb8bd08b61df8d446833e163 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 18 Jan 2024 12:50:35 +0100 Subject: [PATCH 24/31] blog style fixes --- website/src/components/BlogPost.astro | 4 ++-- website/src/pages/blog.astro | 11 ++++++++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/website/src/components/BlogPost.astro b/website/src/components/BlogPost.astro index b5b82680..6117d3ce 100644 --- a/website/src/components/BlogPost.astro +++ b/website/src/components/BlogPost.astro @@ -1,7 +1,7 @@ --- import type { CollectionEntry } from 'astro:content'; -type Props = CollectionEntry<'blog'>['data']; +type Props = { post: CollectionEntry<'blog'> }; const { post } = Astro.props; const { Content } = await post.render(); @@ -9,7 +9,7 @@ import { format } from 'date-fns'; ---
diff --git a/website/src/pages/blog.astro b/website/src/pages/blog.astro index 80270353..43845a69 100644 --- a/website/src/pages/blog.astro +++ b/website/src/pages/blog.astro @@ -32,7 +32,16 @@ const posts = (await getCollection('blog')).sort((a, b) => compareDesc(a.data.da - {posts.map((post) => )} +
+

Strudel Blog

+

+ Welcome to the Strudel Blog, where you we will keep you updated with the latest changes and things + happening in the strudelsphere. +

+
+
+ {posts.map((post) => )} +
-
+
From f85fb35c669bc52ad8fa79d10e4a38157242042e Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 18 Jan 2024 13:46:35 +0100 Subject: [PATCH 26/31] minirepl multi tune support + add other getting-started examples (more to come) --- website/src/docs/Icon.jsx | 15 ++++ website/src/docs/MiniRepl.jsx | 43 ++++++++-- website/src/examples.mjs | 81 +++++++++++++++++++ .../src/pages/workshop/getting-started.mdx | 36 ++------- 4 files changed, 139 insertions(+), 36 deletions(-) create mode 100644 website/src/examples.mjs diff --git a/website/src/docs/Icon.jsx b/website/src/docs/Icon.jsx index 64d5f88a..91583e50 100644 --- a/website/src/docs/Icon.jsx +++ b/website/src/docs/Icon.jsx @@ -1,4 +1,12 @@ export function Icon({ type }) { + if (type === 'skip') { + // !Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc. + return ( + + + + ); + } return ( { @@ -31,6 +39,13 @@ export function Icon({ type }) { clipRule="evenodd" /> ), + skip: ( + + ), }[type] } diff --git a/website/src/docs/MiniRepl.jsx b/website/src/docs/MiniRepl.jsx index 33dadedf..10eff483 100644 --- a/website/src/docs/MiniRepl.jsx +++ b/website/src/docs/MiniRepl.jsx @@ -1,8 +1,8 @@ import { useState, useRef, useCallback, useMemo, useEffect } from 'react'; import { Icon } from './Icon'; -import { silence, getPunchcardPainter, noteToMidi } from '@strudel/core'; +import { silence, getPunchcardPainter, noteToMidi, _mod } from '@strudel/core'; import { transpiler } from '@strudel/transpiler'; -import { getAudioContext, webaudioOutput } from '@strudel/webaudio'; +import { getAudioContext, webaudioOutput, initAudioOnFirstClick } from '@strudel/webaudio'; import { StrudelMirror } from '@strudel/codemirror'; // import { prebake } from '@strudel/repl'; import { prebake } from '../repl/prebake.mjs'; @@ -10,14 +10,16 @@ import { loadModules } from '../repl/util.mjs'; import Claviature from '@components/Claviature'; import useClient from '@src/useClient.mjs'; -let prebaked, modulesLoading; +let prebaked, modulesLoading, audioLoading; if (typeof window !== 'undefined') { prebaked = prebake(); modulesLoading = loadModules(); + audioLoading = initAudioOnFirstClick(); } export function MiniRepl({ - tune: code, + tune, + tunes, hideHeader = false, canvasHeight = 100, onTrigger, @@ -26,6 +28,7 @@ export function MiniRepl({ claviature, claviatureLabels, }) { + const code = tunes ? tunes[0] : tune; const id = useMemo(() => s4(), []); const canvasId = useMemo(() => `canvas-${id}`, [id]); const shouldDraw = !!punchcard || !!claviature; @@ -75,7 +78,7 @@ export function MiniRepl({ } return pat; }, - prebake: async () => Promise.all([modulesLoading, prebaked]), + prebake: async () => Promise.all([modulesLoading, prebaked, audioLoading]), onUpdateState: (state) => { setReplState({ ...state }); }, @@ -91,6 +94,14 @@ export function MiniRepl({ const containerRef = useRef(); const client = useClient(); + const [tuneIndex, setTuneIndex] = useState(0); + const changeTune = (index) => { + index = _mod(index, tunes.length); + setTuneIndex(index); + editorRef.current?.setCode(tunes[index]); + editorRef.current?.evaluate(); + }; + if (!client) { return
{code}
; } @@ -119,6 +130,28 @@ export function MiniRepl({
+ {tunes && ( +
+ + +
+ )}
)}
diff --git a/website/src/examples.mjs b/website/src/examples.mjs new file mode 100644 index 00000000..d752e711 --- /dev/null +++ b/website/src/examples.mjs @@ -0,0 +1,81 @@ +export const examples = [ + `// "coastline" @by eddyflux +await samples('github:eddyflux/crate') +setcps(.75) +let chords = chord("/4").dict('ireal') +stack( + stack( // DRUMS + s("bd").struct("<[x*<1 2> [~@3 x]] x>"), + s("~ [rim, sd:<2 3>]").room("<0 .2>"), + n("[0 <1 3>]*<2!3 4>").s("hh"), + s("rd:<1!3 2>*2").mask("<0 0 1 1>/16").gain(.5) + ).bank('crate') + .mask("<[0 1] 1 1 1>/16".early(.5)) + , // CHORDS + chords.offset(-1).voicing().s("gm_epiano1:1") + .phaser(4).room(.5) + , // MELODY + n("<0!3 1*2>").set(chords).mode("root:g2") + .voicing().s("gm_acoustic_bass"), + chords.n("[0 <4 3 <2 5>>*2](<3 5>,8)") + .set(x).anchor("D5").voicing() + .segment(4).clip(rand.range(.4,.8)) + .room(.75).shape(.3).delay(.25) + .fm(sine.range(3,8).slow(8)) + .lpf(sine.range(500,1000).slow(8)).lpq(5) + .rarely(ply("2")).chunk(4, fast(2)) + .gain(perlin.range(.6, .9)) + .mask("<0 1 1 0>/16") +) +.late("[0 .01]*4").late("[0 .01]*2").size(4)`, + `// "broken cut 1" @by froos + +await samples('github:tidalcycles/Dirt-Samples/master') +samples({ + 'slap': 'https://cdn.freesound.org/previews/495/495416_10350281-lq.mp3', + 'whirl': 'https://cdn.freesound.org/previews/495/495313_10350281-lq.mp3', + 'attack': 'https://cdn.freesound.org/previews/494/494947_10350281-lq.mp3' +}) + +setcps(1.25) + +note("[c2 ~](3,8)*2,eb,g,bb,d").s("sawtooth") + .noise(0.3) + .lpf(perlin.range(800,2000).mul(0.6)) + .lpenv(perlin.range(1,5)).lpa(.25).lpd(.1).lps(0) + .add.mix(note("<0!3 [1 <4!3 12>]>")).late(.5) + .vib("4:.2") + .room(1).roomsize(4).slow(4) + .stack( + s("bd").late("<0.01 .251>"), + s("breaks165:1/2").fit() + .chop(4).sometimesBy(.4, ply("2")) + .sometimesBy(.1, ply("4")).release(.01) + .gain(1.5).sometimes(mul(speed("1.05"))).cut(1) + , + s("?").delay(".8:.1:.8").room(2).slow(8).cut(2), + ).reset("".late(2))`, + `// "acidic tooth" @by eddyflux + setcps(1) + stack( + note("[/8](<3 5>,8)") + .clip(perlin.range(.15,1.5)) + .release(.1) + .s("sawtooth") + .lpf(sine.range(400,800).slow(16)) + .lpq(cosine.range(6,14).slow(3)) + .lpenv(sine.mul(4).slow(4)) + .lpd(.2).lpa(.02) + .ftype('24db') + .rarely(add(note(12))) + .room(.2).shape(.3).postgain(.5) + .superimpose(x=>x.add(note(12)).delay(.5).bpf(1000)) + .gain("[.2 1@3]*2") // fake sidechain + , + stack( + s("bd*2").mask("<0@4 1@16>"), + s("hh*8").gain(saw.mul(saw.fast(2))).clip(sine) + .mask("<0@8 1@16>") + ).bank('RolandTR909') + )`, +]; diff --git a/website/src/pages/workshop/getting-started.mdx b/website/src/pages/workshop/getting-started.mdx index 3d37a5cf..83dc8894 100644 --- a/website/src/pages/workshop/getting-started.mdx +++ b/website/src/pages/workshop/getting-started.mdx @@ -4,6 +4,7 @@ layout: ../../layouts/MainLayout.astro --- import { MiniRepl } from '../../docs/MiniRepl'; +import { examples } from '../../examples.mjs'; # Welcome @@ -29,40 +30,13 @@ The best place to actually make music with Strudel is the [Strudel REPL](https:/ - teaching: focussing on a low barrier of entry, Strudel is a good fit for teaching music and code at the same time. - integrate into your existing music setup: either via MIDI or OSC, you can use Strudel as a really flexible sequencer -## Example +## Examples -Here is an example of how strudel can sound: +Here are some examples of how strudel can sound: -],hh*8") - .speed(perlin.range(.8,.9)), // random sample speed variation - // bassline - "" - .off(1/8,x=>x.add(12).degradeBy(.5)) // random octave jumps - .add(perlin.range(0,.5)) // random pitch variation - .superimpose(add(.05)) // add second, slightly detuned voice - .note() // wrap in "note" - .decay(.15).sustain(0) // make each note of equal length - .s('sawtooth') // waveform - .gain(.4) // turn down - .cutoff(sine.slow(7).range(300,5000)), // automate cutoff - // chords - ">".voicings('lefthand') - .superimpose(x=>x.add(.04)) // add second, slightly detuned voice - .add(perlin.range(0,.5)) // random pitch variation - .note() // wrap in "note" - .s('sawtooth') // waveform - .gain(.16) // turn down - .cutoff(500) // fixed cutoff - .attack(1) // slowly fade in -) -.slow(3/2)`} -/> + -To hear more, go to the [Strudel REPL](https://strudel.cc/) and press shuffle to hear a random example pattern. +These examples cannot fully encompass the variety of things you can do, so [check out the showcase](/intro/showcase/) for some videos of how people use Strudel. ## Getting Started From 0778b4809e104b99a377d283123325dcdec13987 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 18 Jan 2024 13:47:34 +0100 Subject: [PATCH 27/31] format --- website/src/docs/Icon.jsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/website/src/docs/Icon.jsx b/website/src/docs/Icon.jsx index 91583e50..48b05d87 100644 --- a/website/src/docs/Icon.jsx +++ b/website/src/docs/Icon.jsx @@ -2,7 +2,14 @@ export function Icon({ type }) { if (type === 'skip') { // !Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc. return ( - + ); From 873a4121791f83c64ddbe829075f4c1173ba441b Mon Sep 17 00:00:00 2001 From: Alex McLean Date: Thu, 18 Jan 2024 16:45:39 +0000 Subject: [PATCH 28/31] `pick` now accepts lookup tables, with alternate cycle squeezing behaviour as new `inhabit` function (#918) - the args for `pick` are now reversed (breaking change!) - `pick` is also now a method on the inhabited pattern - `pick` now also accepts a lookup table - `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 now 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 | 99 ++++++++++++++++++----- packages/core/test/pattern.test.mjs | 51 ++++++++++++ packages/core/util.mjs | 7 ++ test/__snapshots__/examples.test.mjs.snap | 93 ++++++++++++++++++++- 6 files changed, 248 insertions(+), 48 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..9e8db9a0 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,88 @@ 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; - } - return pat - .fmap((i) => { - const key = clamp(Math.round(i), 0, xs.length - 1); - return xs[key]; - }) - .innerJoin(); -}; +export 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 +413,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 +437,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 +472,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..d9da114d 100644 --- a/packages/core/test/pattern.test.mjs +++ b/packages/core/test/pattern.test.mjs @@ -1057,4 +1057,55 @@ 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)), + ); + }); + }); + 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 ]", ] `; From a8db70744053264274fedc6663bad15d440432d3 Mon Sep 17 00:00:00 2001 From: Alex McLean Date: Thu, 18 Jan 2024 17:04:26 +0000 Subject: [PATCH 29/31] Revert "`pick` now accepts lookup tables, with alternate cycle squeezing behaviour as new `inhabit` function" (#920) --- packages/core/controls.mjs | 10 +-- packages/core/pattern.mjs | 36 ++++---- packages/core/signal.mjs | 103 +++++----------------- packages/core/test/pattern.test.mjs | 51 ----------- packages/core/util.mjs | 7 -- test/__snapshots__/examples.test.mjs.snap | 93 +------------------ 6 files changed, 50 insertions(+), 250 deletions(-) diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 42212dd3..444b4ea1 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -97,7 +97,7 @@ const generic_params = [ */ ['postgain'], /** - * Like `gain`, but linear. + * Like {@link gain}, but linear. * * @name amp * @param {number | Pattern} amount gain. @@ -856,7 +856,7 @@ const generic_params = [ */ ['detune', 'det'], /** - * Set dryness of reverb. See `room` and `size` for more information about reverb. + * Set dryness of reverb. See {@link room} and {@link 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 `begin`/`end` or `chop`/`striate` and friends, to change the fade out time of the 'grain' envelope. + * Used when using {@link begin}/{@link end} or {@link chop}/{@link 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 `room`. + * Sets the room size of the reverb, see {@link 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 `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 {@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`. * * @name unit * @param {number | string | Pattern} unit see description above diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index 6a7b210b..9e804b23 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 `State` to an array of `Hap`. + * @param {function} query - The function that maps a {@link State} to an array of {@link 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 `fmap`. + * each hap. It has the alias {@link Pattern#fmap}. * @synonyms fmap * @param {Function} func to to apply to the value * @returns Pattern @@ -51,7 +51,7 @@ export class Pattern { } /** - * see `withValue` + * see {@link Pattern#withValue} * @noAutocomplete */ fmap(func) { @@ -115,7 +115,7 @@ export class Pattern { } /** - * As with `appBoth`, but the `whole` timespan is not the intersection, + * As with {@link Pattern#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 `appLeft`, but `whole` timespans are instead taken from the + * As with {@link Pattern#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 `withQuerySpan`, but the function is applied to both the + * As with {@link Pattern#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 `withQuerySpan`, but the function is applied to the timespans + * Similar to {@link Pattern#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 `withHapSpan`, but the function is applied to both the + * As with {@link Pattern#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 `withHaps`, but applies the function to every hap, rather than every list of haps. + * As with {@link Pattern#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 `filterHaps`, but the function is applied to values + * As with {@link Pattern#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 `firstCycleValues` accessor. + * More human-readable version of the {@link Pattern#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 `superimpose`, but without the original pattern: + * Layers the result of the given function(s). Like {@link Pattern.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: `cat` + * synonyms: {@link cat} * * @return {Pattern} * @example @@ -1244,7 +1244,7 @@ export function cat(...pats) { return slowcat(...pats); } -/** Like `seq`, but each step has a length, relative to the whole. +/** Like {@link Pattern.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 `fastcat` */ +/** See {@link 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 `firstOf` + * An alias for {@link 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 9e8db9a0..1446beeb 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, objectMap } from './util.mjs'; +import { id, _mod, clamp } from './util.mjs'; export function steady(value) { // A continuous value @@ -156,88 +156,31 @@ export const _irand = (i) => rand.fmap((x) => Math.trunc(x * i)); */ export const irand = (ipat) => reify(ipat).fmap(_irand).innerJoin(); -const _pick = function (lookup, pat, modulo = true) { - const array = Array.isArray(lookup); - const len = Object.keys(lookup).length; +/** + * pick from the list of values (or patterns of values) via the index using the given + * pattern of integers + * @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"])) + */ - lookup = objectMap(lookup, reify); - - if (len === 0) { +export const pick = (pat, xs) => { + xs = xs.map(reify); + if (xs.length == 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]; - }); + return pat + .fmap((i) => { + const key = clamp(Math.round(i), 0, xs.length - 1); + return xs[key]; + }) + .innerJoin(); }; -/** * 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("<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 = 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(); -}); - /** -/** * 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 + * 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 @@ -413,7 +356,7 @@ export const degradeBy = register('degradeBy', function (x, pat) { export const degrade = register('degrade', (pat) => pat._degradeBy(0.5)); /** - * Inverse of `degradeBy`: Randomly removes events from the pattern by a given amount. + * Inverse of {@link Pattern#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). @@ -437,7 +380,7 @@ export const undegrade = register('undegrade', (pat) => pat._undegradeBy(0.5)); /** * * Randomly applies the given function by the given probability. - * Similar to `someCyclesBy` + * Similar to {@link Pattern#someCyclesBy} * * @name sometimesBy * @memberof Pattern @@ -472,7 +415,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 `sometimesBy` + * Similar to {@link Pattern#sometimesBy} * * @name someCyclesBy * @memberof Pattern diff --git a/packages/core/test/pattern.test.mjs b/packages/core/test/pattern.test.mjs index d9da114d..928bfcef 100644 --- a/packages/core/test/pattern.test.mjs +++ b/packages/core/test/pattern.test.mjs @@ -1057,55 +1057,4 @@ 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)), - ); - }); - }); - 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 ca3cfc12..ef55de95 100644 --- a/packages/core/util.mjs +++ b/packages/core/util.mjs @@ -316,10 +316,3 @@ 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 b3114ed5..002ccb5f 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -2408,40 +2408,6 @@ 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 ]", @@ -3635,61 +3601,10 @@ 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: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 ]", + "[ 3/1 → 13/4 | note:g ]", + "[ 13/4 → 7/2 | note:a ]", + "[ 7/2 → 15/4 | note:c ]", + "[ 15/4 → 4/1 | note:d ]", ] `; From 98b785960544b4695905d6472a5fb6d7cefdfdf4 Mon Sep 17 00:00:00 2001 From: Alex McLean Date: Thu, 18 Jan 2024 17:08:29 +0000 Subject: [PATCH 30/31] 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 ]", ] `; From d8677c6261c33a7a0d2a05fbda96ea9a0b30ad57 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 18 Jan 2024 23:40:22 +0100 Subject: [PATCH 31/31] hotfix: rss links --- website/src/pages/rss.xml.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/pages/rss.xml.js b/website/src/pages/rss.xml.js index 0aeb7bf4..02ecd761 100644 --- a/website/src/pages/rss.xml.js +++ b/website/src/pages/rss.xml.js @@ -9,7 +9,7 @@ export async function GET(context) { 'The Strudel Blog will keep you updated with the latest changes and things happening in the strudelsphere.', site: context.site, items: posts.map((post) => ({ - link: `/${post.slug}/`, + link: `/blog/#${post.slug}`, title: post.data.title, pubDate: post.data.date, description: post.data.description,