Compare commits

..

90 Commits

Author SHA1 Message Date
Felix Roos 126e45af9a move onTriggerTime back to pattern.mjs 2026-10-11 21:59:07 +02:00
Felix Roos a6943f1eeb minor renames 2026-10-11 21:23:01 +02:00
Felix Roos 4a0d9de664 rename repl defaultOutput to onSchedule, error when defaultOutput is
passed. also rename xScheduler to xOutput, to be consistent with
webaudioOutput and friends
2026-10-11 20:54:50 +02:00
Felix Roos 7f486556c6 add getClockTime to get current seconds on scheduler clock 2026-10-11 18:36:06 +02:00
Felix Roos 257787d38a Update pnpm-lock.yaml 2026-10-11 15:32:53 +02:00
Felix Roos 9eebf0ad21 Merge remote-tracking branch 'origin/main' into trigger-refactor 2026-10-11 15:23:01 +02:00
froos d97e91e928 Merge pull request 'Add cyclestruct function' (#2107) from tzwaan/cyclestruct into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/2107
2026-10-11 13:44:26 +02:00
froos e78cc85ad7 Merge pull request 'document undocumented things' (#1996) from tyow/localstrudel:jsdocs into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1996
2026-10-11 13:26:54 +02:00
froos ee9138b514 Merge pull request 'fix: correct typos in soundfonts (gm.mjs) and docs (effects.mdx)' (#2134) from CatoPaus/strudel:catopaus-fix-soundfonts-typos into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/2134
2026-10-11 13:13:20 +02:00
froos 0b786700d0 Merge pull request 'improve custom scale support and highlighting' (#2018) from tyow/localstrudel:xenscalethings into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/2018
2026-10-11 12:26:44 +02:00
Felix Roos 076bb8a152 hotfix: more missing containers 2026-10-11 11:47:21 +02:00
Felix Roos 357af69052 Merge branch 'main' of https://codeberg.org/uzu/strudel 2026-10-11 11:43:01 +02:00
Felix Roos fd2626d11d hotfix: pin ci container (deploy is failing) 2026-10-11 11:42:38 +02:00
froos 2b5f31f605 Merge pull request 'Negative steps change euclid into euclidLegato and vice versa' (#2099) from tzwaan/euclid-legato-notation into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/2099
2026-10-11 11:20:54 +02:00
froos c57c5a7c4d Merge pull request 'keep space reserved for the sidebars above 1280px (fixes #2110)' (#2111) from bwagner/strudel:fix/sidebar-overlap into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/2111
2026-10-11 11:11:33 +02:00
Tijmen Zwaan c7d0f15826 Rename cyclestruct to isobite with synonyms and add some examples 2026-10-09 22:17:10 +02:00
Tijmen Zwaan 60ac02905d Add cyclestruct function 2026-10-09 21:13:23 +02:00
tyow 5148066b01 stackCenter/Centre changes, fix from merge, snapshot 2026-10-07 16:44:43 -04:00
tyow 29a4e30042 Merge main into jsdocs 2026-10-07 16:30:41 -04:00
Alex McLean 0a6d61e14a Merge pull request 'New FAQ entry: what's a hap?' (#2088) from scrappy_fiddler/strudel:main into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/2088
2026-10-04 17:44:52 +02:00
CatoPaus 74de75644f fix(soundfonts): correct preset filename typos in gm.mjs
This PR fixes minor filename typos and invalid entries in `packages/soundfonts/gm.mjs`:
1. **`gm_electric_bass_finger`**: Corrected typo `'0330_FluidR3_GM_sf2_fible'` -> `'0330_FluidR3_GM_sf2_file'`. Index 1 previously returned HTTP 404 when cycling through bass soundfont variations.
2. **`gm_slap_bass_2`**: Removed accidental space `'0370_GeneralUserGS_sf2_fil e'` -> `'0370_GeneralUserGS_sf2_file'`.
3. **`gm_gunshot`**: Removed stray empty string `''` from the variations array.
2026-10-04 10:04:11 +02:00
scrappy_fiddler 6e50217a53 Merge branch 'main' into main 2026-10-03 14:24:28 +02:00
froos c57320a2de Merge pull request 'Add error handling to stack so non-broken patterns continue playing' (#2133) from tzwaan/stack-error-handling into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/2133
2026-09-29 09:21:52 +02:00
Tijmen Zwaan a1be663b15 Add error handling to stack so non-broken patterns continue playing 2026-09-28 23:44:34 +02:00
CatoPaus 9c05f8ebc6 Update website/src/pages/learn/effects.mdx
Just a small typo.
2026-09-18 07:30:23 +02:00
scrappy_fiddler b2c2dfb0b4 Merge branch 'main' into main 2026-08-30 11:18:41 +02:00
scrappy_fiddler 38956e4d16 now with prettier FAQ 2026-08-30 11:17:51 +02:00
Tijmen Zwaan ecc7bc078c Fix formatting 2026-08-22 00:12:13 +02:00
Tijmen Zwaan 078aa60779 Allow euclid pulses > steps to stretch the rhythm over multiple cycles
Also fix a bug in euclidLegato where the rotation offset would be
applied to the whole source pattern, instead of the euclid structure.
2026-08-21 23:58:47 +02:00
Tijmen Zwaan d7384dfc3d Negative steps change euclid into euclidLegato and vice versa
Also fix that euclidLegato did not support negative pulses for inversion
2026-08-21 23:58:46 +02:00
Bernhard Wagner a3a38cfe33 keep space reserved for the sidebars above 1280px (fixes #2110)
At xl the wrapper dropped its left padding and relied on centering to clear the
fixed sidebars, which only works from about 1338px up. Reserving the right
gutter instead leaves md:pl-[300px] in force, so the column narrows between
1280px and 1338px rather than sliding under the nav.
2026-08-20 11:08:23 +02:00
Felix Roos 98e99e8166 Merge remote-tracking branch 'origin/main' into trigger-refactor 2026-08-19 21:33:43 +02:00
froos 8f81463b9c Merge pull request 'dough output' (#2108) from dough-output into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/2108
2026-08-19 21:22:18 +02:00
Felix Roos d7fb9fe992 add onSchedule with simpler args to replace onTrigger 2026-08-19 18:07:55 +02:00
Felix Roos 37d524e639 Merge remote-tracking branch 'origin/main' into trigger-refactor 2026-08-18 22:25:11 +02:00
froos beb16f4040 Merge pull request 'better osc timing + clockbridge class' (#2106) from osc-timing into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/2106
2026-08-18 13:06:07 +02:00
Felix Roos f2be5e4f5e refactor: new clockbridge class to fix midi and osc timing 2026-08-16 22:15:39 +02:00
yaxu 692170e117 Update website/src/pages/learn/faq.mdx 2026-07-27 16:55:41 +02:00
scrappy_fiddler 14ac26fa45 what's a hap? 2026-07-23 09:17:17 +02:00
tyow 704018f774 preform/perform, mod 2026-07-21 22:48:28 -04:00
tyow d5171e2ab7 named things 2026-04-29 15:35:05 -04:00
tyow 81257b5f06 lt,gt,lte,etc. 2026-04-29 15:30:32 -04:00
tyow 0d73c02314 format 2026-03-26 01:10:42 -04:00
tyow 3dff15eab0 readded chooseIn due to mondough using it 2026-03-25 15:36:26 -04:00
tyow 8f1a0840c0 edit dry tags to add to reference 2026-03-25 14:07:10 -04:00
tyow 948f78da11 wording 2026-03-12 12:23:30 -04:00
tyow 08eaa8a578 more cleanup 2026-03-11 18:04:29 -04:00
tyow aeeaa50071 cleanup 2026-03-11 18:03:50 -04:00
tyow 91681983e2 wording 2026-03-11 17:32:14 -04:00
tyow 6df34625c3 chooseInWith examples, other fixes 2026-03-11 17:31:19 -04:00
tyow 8ad767363f choosein tag 2026-03-11 15:54:42 -04:00
tyow 581bbc05fc room isn't a 0-1 value 2026-03-11 15:24:30 -04:00
tyow a58711a311 bugfix 2026-03-11 14:40:52 -04:00
tyow 4596926ac2 support old functionality 2026-03-11 14:16:26 -04:00
tyow f2acdc580f fix edo bug 2026-03-11 14:01:57 -04:00
tyow beee2bc921 refactor 2026-03-11 13:53:24 -04:00
tyow b586d8c4c4 add notes on missing functionality 2026-03-10 19:39:08 -04:00
tyow c901ffb37f example, deprecate tune, swap xen.mdx sections 2026-03-10 19:24:59 -04:00
tyow c0a43a20dc complete parity with current implementation 2026-03-10 19:02:25 -04:00
tyow fa4d060ed2 add todo 2026-03-10 18:25:44 -04:00
tyow 25bf6856ae format, snapshot 2026-03-10 18:25:01 -04:00
tyow f769b9c7a5 It looks good in repl and bad on test 2026-03-10 18:11:34 -04:00
tyow 7c7da3e783 progress 2026-03-10 18:01:56 -04:00
tyow 3e5c945e12 added thoughts. 2026-03-10 15:00:39 -04:00
tyow a1ccb9c0bb working stack for named scales 2026-03-10 14:39:25 -04:00
tyow 7510cb715f fixed prev bug. next: support stack 2026-03-10 00:41:23 -04:00
tyow 3554a544d6 erroring out 2026-03-10 00:18:34 -04:00
tyow a161d5a5d3 comment in jisc3 2026-03-09 19:44:36 -04:00
tyow 02e0c43f77 thoughts 2026-03-09 19:39:27 -04:00
tyow 814510f637 init 2026-03-09 19:32:28 -04:00
tyow e502a3c3e5 change bus tags s.t. it appears in reference 2026-03-08 19:06:49 -04:00
tyow 1ef39a2ea7 small fixes 2026-03-01 12:42:13 -05:00
tyow ee4b8fa315 cleanup 2026-02-28 14:46:01 -05:00
tyow 36a456eeb4 Update replicate and extend examples to show difference 2026-02-28 14:43:04 -05:00
tyow 37d052a655 Change "tidal" to "strudel" in docs 2026-02-24 01:51:38 -05:00
tyow 44ec79182e added examples to withHap and withHaps 2026-02-23 20:06:38 -05:00
tyow e5a08091f1 format 2026-02-23 17:21:57 -05:00
tyow 70213a26ae added some examples to things 2026-02-23 17:21:27 -05:00
tyow aedd7de2df getDur doc fix 2026-02-23 17:00:29 -05:00
tyow 4d0a4902ab enable stack variants to be chained 2026-02-22 17:42:09 -05:00
tyow 58e6b0543f add stackRepeat (already supported, just not exposed to the user) 2026-02-22 17:37:57 -05:00
tyow dfb2335b1c document stackRight, Left, Centre/Center 2026-02-22 17:28:32 -05:00
tyow 53cce1d97b revert undocumented.json 2026-02-22 13:35:10 -05:00
tyow a76a23a43d replace a missing sample with an existing one in example 2026-02-21 23:37:00 -05:00
tyow 787adfb20f format and snapshot 2026-02-21 23:16:15 -05:00
tyow d4ec7e9a55 modify degradeByWith to be more user friendly, add doc 2026-02-21 23:15:45 -05:00
tyow 10b0e30057 a bunch more docs 2026-02-21 22:13:46 -05:00
tyow c2572f4fad deprecate chooseIn 2026-02-21 21:00:26 -05:00
tyow 4f7a0f76d3 make getFreq and getFrequency synonyms and add doc 2026-02-21 20:49:37 -05:00
tyow 88d7868e12 document some math funcs 2026-02-21 20:25:20 -05:00
62 changed files with 2327 additions and 411 deletions
+3 -2
View File
@@ -13,10 +13,11 @@ jobs:
build: build:
if: ${{ github.event.label.name == 'serve-hot' }} if: ${{ github.event.label.name == 'serve-hot' }}
runs-on: docker runs-on: docker
container: node:22-bookworm
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
ref: ${{ github.event.pull_request.head.sha }} ref: ${{ github.event.pull_request.head.sha }}
- uses: pnpm/action-setup@v4 - uses: pnpm/action-setup@v4
with: with:
version: 9.12.2 version: 9.12.2
@@ -41,4 +42,4 @@ jobs:
ssh-keyscan matrix.toplap.org > ~/.ssh/known_hosts ssh-keyscan matrix.toplap.org > ~/.ssh/known_hosts
rsync -atv --delete --delete-after --progress \ rsync -atv --delete --delete-after --progress \
./website/dist/ \ ./website/dist/ \
strudel@matrix.toplap.org:/home/strudel/deploy/pr-${{ github.event.pull_request.number }}.hot.strudel.cc strudel@matrix.toplap.org:/home/strudel/deploy/pr-${{ github.event.pull_request.number }}.hot.strudel.cc
+2 -1
View File
@@ -13,6 +13,7 @@ concurrency:
jobs: jobs:
build: build:
runs-on: docker runs-on: docker
container: node:22-bookworm
env: env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
steps: steps:
@@ -37,4 +38,4 @@ jobs:
apt update && apt install -y rsync apt update && apt install -y rsync
mkdir ~/.ssh mkdir ~/.ssh
ssh-keyscan matrix.toplap.org > ~/.ssh/known_hosts ssh-keyscan matrix.toplap.org > ~/.ssh/known_hosts
rsync -atv --delete --delete-after --progress ./website/dist/ strudel@matrix.toplap.org:/home/strudel/deploy/strudel.cc rsync -atv --delete --delete-after --progress ./website/dist/ strudel@matrix.toplap.org:/home/strudel/deploy/strudel.cc
+2 -1
View File
@@ -10,6 +10,7 @@ concurrency:
jobs: jobs:
build: build:
runs-on: docker runs-on: docker
container: node:22-bookworm
env: env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
steps: steps:
@@ -34,4 +35,4 @@ jobs:
apt update && apt install -y rsync apt update && apt install -y rsync
mkdir ~/.ssh mkdir ~/.ssh
ssh-keyscan matrix.toplap.org > ~/.ssh/known_hosts ssh-keyscan matrix.toplap.org > ~/.ssh/known_hosts
rsync -atv --delete --delete-after --progress ./website/dist/ strudel@matrix.toplap.org:/home/strudel/deploy/cold.strudel.cc rsync -atv --delete --delete-after --progress ./website/dist/ strudel@matrix.toplap.org:/home/strudel/deploy/cold.strudel.cc
+1
View File
@@ -5,6 +5,7 @@ on: [push, pull_request]
jobs: jobs:
build: build:
runs-on: docker runs-on: docker
container: node:22-bookworm
strategy: strategy:
matrix: matrix:
node-version: [20] node-version: [20]
+1 -1
View File
@@ -16,7 +16,7 @@ const drawContext = canvas.getContext('2d');
const drawTime = [-2, 2]; // time window of drawn haps const drawTime = [-2, 2]; // time window of drawn haps
const editor = new StrudelMirror({ const editor = new StrudelMirror({
defaultOutput: webaudioOutput, onSchedule: webaudioOutput,
getTime: () => getAudioContext().currentTime, getTime: () => getAudioContext().currentTime,
transpiler, transpiler,
root: document.getElementById('editor'), root: document.getElementById('editor'),
+1 -1
View File
@@ -12,7 +12,7 @@ registerSynthSounds();
evalScope(import('@strudel/core'), import('@strudel/mini'), import('@strudel/webaudio'), import('@strudel/tonal')); evalScope(import('@strudel/core'), import('@strudel/mini'), import('@strudel/webaudio'), import('@strudel/tonal'));
const { evaluate } = repl({ const { evaluate } = repl({
defaultOutput: webaudioOutput, onSchedule: webaudioOutput,
getTime: () => ctx.currentTime, getTime: () => ctx.currentTime,
transpiler, transpiler,
}); });
+5 -6
View File
@@ -118,7 +118,7 @@ export const { s, sound } = registerControl(['s', 'n', 'gain'], 'sound');
* @param {number | Pattern} position Position in the wavetable from 0 to 1 * @param {number | Pattern} position Position in the wavetable from 0 to 1
* @synonyms wavetablePosition * @synonyms wavetablePosition
* @example * @example
* s("squelch").bank("wt_digital").seg(8).note("F1").wt("0 0.25 0.5 0.75 1") * s("wt_digital").seg(8).note("F1").wt("0 0.25 0.5 0.75 1")
*/ */
export const { wt, wavetablePosition } = registerControl('wt', 'wavetablePosition'); export const { wt, wavetablePosition } = registerControl('wt', 'wavetablePosition');
@@ -1988,11 +1988,10 @@ export const { spread } = registerControl('spread');
* Set dryness of reverb. See `room` and `size` for more information about reverb. * Set dryness of reverb. See `room` and `size` for more information about reverb.
* *
* @name dry * @name dry
* @tags superdirt * @tags superdirt, superdough
* @param {number | Pattern} dry 0 = wet, 1 = dry * @param {number | Pattern} dry 0 = wet, 1 = dry
* @example * @example
* n("[0,3,7](3,8)").s("superpiano").room(.7).dry("<0 .5 .75 1>").osc() * n("[0,3,7](3,8)").s("superpiano").room(.7).dry("<0 .5 .75 1>").osc()
* @superdirtOnly
* *
*/ */
export const { dry } = registerControl('dry'); export const { dry } = registerControl('dry');
@@ -2226,7 +2225,7 @@ export const { orbit } = registerControl('orbit', 'o');
* otherPat.bmod(..) (to modulate another pattern with the bus) * otherPat.bmod(..) (to modulate another pattern with the bus)
* *
* @name bus * @name bus
* @tags superdirt * @tags superdough
* @param {number | Pattern} number * @param {number | Pattern} number
*/ */
export const { bus } = registerControl('bus'); export const { bus } = registerControl('bus');
@@ -2235,7 +2234,7 @@ export const { bus } = registerControl('bus');
* Postgain multiplier prior to sending the signal to the audio bus. * Postgain multiplier prior to sending the signal to the audio bus.
* *
* @name busgain * @name busgain
* @tags superdirt * @tags superdough
* @synonyms bgain * @synonyms bgain
* @param {number | Pattern} number * @param {number | Pattern} number
*/ */
@@ -2377,7 +2376,7 @@ export const { mode } = registerControl(['mode', 'anchor']);
* *
* @name room * @name room
* @tags orbit, superdough * @tags orbit, superdough
* @param {number | Pattern} level between 0 and 1 * @param {number | Pattern} level
* @example * @example
* s("bd sd [~ bd] sd").room("<0 .2 .4 .6 .8 1>") * s("bd sd [~ bd] sd").room("<0 .2 .4 .6 .8 1>")
* @example * @example
+7 -6
View File
@@ -10,6 +10,7 @@ import { errorLogger, logger } from './logger.mjs';
export class Cyclist { export class Cyclist {
constructor({ constructor({
interval, interval,
onSchedule,
onTrigger, onTrigger,
onToggle, onToggle,
onError, onError,
@@ -19,6 +20,11 @@ export class Cyclist {
clearInterval, clearInterval,
beforeStart, beforeStart,
}) { }) {
if (onTrigger) {
throw new Error(
'onTrigger has been dropped. use onSchedule(hap, time, cps) instead, see https://codeberg.org/uzu/strudel/pulls/2138',
);
}
this.started = false; this.started = false;
this.beforeStart = beforeStart; this.beforeStart = beforeStart;
this.cps = 0.5; this.cps = 0.5;
@@ -63,12 +69,7 @@ export class Cyclist {
if (hap.hasOnset()) { if (hap.hasOnset()) {
const targetTime = const targetTime =
(hap.whole.begin - this.num_cycles_at_cps_change) / this.cps + this.seconds_at_cps_change + latency; (hap.whole.begin - this.num_cycles_at_cps_change) / this.cps + this.seconds_at_cps_change + latency;
const duration = hap.duration / this.cps; onSchedule?.(hap, targetTime, this.cps);
// the following line is dumb and only here for backwards compatibility
// see https://codeberg.org/uzu/strudel/pulls/1004
const deadline = targetTime - phase;
// this onTrigger has another signature
onTrigger?.(hap, deadline, duration, this.cps, targetTime);
if (hap.value.cps !== undefined && this.cps != hap.value.cps) { if (hap.value.cps !== undefined && this.cps != hap.value.cps) {
this.cps = hap.value.cps; this.cps = hap.value.cps;
this.num_ticks_since_cps_change = 0; this.num_ticks_since_cps_change = 0;
+45 -19
View File
@@ -59,6 +59,11 @@ export const bjorklund = function (ons, steps) {
* really useful for computer/algorithmic music because they can * really useful for computer/algorithmic music because they can
* describe a large number of rhythms with a couple of numbers. * describe a large number of rhythms with a couple of numbers.
* *
* A negative number of pulses inverts the pattern
* so pulses happen in the gaps of the normal euclidean rhythm.
*
* A negative number of steps changes it into `euclidLegato`.
*
* @memberof Pattern * @memberof Pattern
* @name euclid * @name euclid
* @tags temporal * @tags temporal
@@ -137,8 +142,38 @@ const _euclidRot = function (pulses, steps, rotation) {
return b; return b;
}; };
const _euclidLegato = function (pulses, steps, rotation, pat) {
var bin_pat = _euclidRot(pulses, steps, 0);
// Because the `gapless` algorithm logic assumes the first entry in bin_pat is a 1,
// we rotate the pattern so the first onset is at the start.
const firstOnset = bin_pat.indexOf(1);
bin_pat = rotate(bin_pat, firstOnset);
steps = Math.abs(steps);
const gapless = bin_pat
.join('')
.split('1')
.slice(1)
.map((s) => [s.length + 1, true]);
return pat.struct(timeCat(...gapless).late(Fraction(rotation + firstOnset).div(steps)));
};
const _euclid = function (pulses, steps, rotation, pat) {
if (steps === 0 || pulses === 0) {
return silence;
}
const cycles = Math.ceil(Math.abs(pulses) / Math.abs(steps));
const totalSteps = cycles * steps;
if (totalSteps < 0) {
return _euclidLegato(pulses, -totalSteps, rotation, pat.fast(cycles)).slow(cycles);
}
return pat
.fast(cycles)
.struct(_euclidRot(pulses, totalSteps, rotation))
.slow(cycles);
};
export const euclid = register('euclid', function (pulses, steps, pat) { export const euclid = register('euclid', function (pulses, steps, pat) {
return pat.struct(_euclidRot(pulses, steps, 0)); return _euclid(pulses, steps, 0, pat);
}); });
export const bjork = register('bjork', function (euc, pat) { export const bjork = register('bjork', function (euc, pat) {
@@ -146,16 +181,18 @@ export const bjork = register('bjork', function (euc, pat) {
euc = [euc]; euc = [euc];
} }
const [pulses, steps = pulses, rot = 0] = euc; const [pulses, steps = pulses, rot = 0] = euc;
return pat.struct(_euclidRot(pulses, steps, rot)); return _euclid(pulses, steps, rot, pat);
}); });
export const { euclidrot, euclidRot } = register(['euclidrot', 'euclidRot'], function (pulses, steps, rotation, pat) { export const { euclidrot, euclidRot } = register(['euclidrot', 'euclidRot'], function (pulses, steps, rotation, pat) {
return pat.struct(_euclidRot(pulses, steps, rotation)); return _euclid(pulses, steps, rotation, pat);
}); });
/** /**
* Similar to `euclid`, but each pulse is held until the next pulse, * Similar to `euclid`, but each pulse is held until the next pulse,
* so there will be no gaps. * so there will be no gaps.
*
* A negative number of steps changes it into `euclid`.
* @name euclidLegato * @name euclidLegato
* @memberof Pattern * @memberof Pattern
* @tags temporal * @tags temporal
@@ -167,27 +204,16 @@ export const { euclidrot, euclidRot } = register(['euclidrot', 'euclidRot'], fun
* note("c3").euclidLegato(3,8) * note("c3").euclidLegato(3,8)
*/ */
const _euclidLegato = function (pulses, steps, rotation, pat) {
if (pulses < 1) {
return silence;
}
const bin_pat = _euclidRot(pulses, steps, 0);
const gapless = bin_pat
.join('')
.split('1')
.slice(1)
.map((s) => [s.length + 1, true]);
return pat.struct(timeCat(...gapless)).late(Fraction(rotation).div(steps));
};
export const euclidLegato = register(['euclidLegato'], function (pulses, steps, pat) { export const euclidLegato = register(['euclidLegato'], function (pulses, steps, pat) {
return _euclidLegato(pulses, steps, 0, pat); return _euclid(pulses, -steps, 0, pat);
}); });
/** /**
* Similar to `euclid`, but each pulse is held until the next pulse, * Similar to `euclid`, but each pulse is held until the next pulse,
* so there will be no gaps, and has an additional parameter for 'rotating' * so there will be no gaps, and has an additional parameter for 'rotating'
* the resulting sequence * the resulting sequence.
*
* A negative number of steps changes it into `euclidRot`.
* @name euclidLegatoRot * @name euclidLegatoRot
* @memberof Pattern * @memberof Pattern
* @tags temporal * @tags temporal
@@ -198,7 +224,7 @@ export const euclidLegato = register(['euclidLegato'], function (pulses, steps,
* note("c3").euclidLegatoRot(3,5,2) * note("c3").euclidLegatoRot(3,5,2)
*/ */
export const euclidLegatoRot = register(['euclidLegatoRot'], function (pulses, steps, rotation, pat) { export const euclidLegatoRot = register(['euclidLegatoRot'], function (pulses, steps, rotation, pat) {
return _euclidLegato(pulses, steps, rotation, pat); return _euclid(pulses, -steps, rotation, pat);
}); });
/** /**
+7 -3
View File
@@ -8,7 +8,12 @@ import { logger } from './logger.mjs';
import { ClockCollator, cycleToSeconds } from './util.mjs'; import { ClockCollator, cycleToSeconds } from './util.mjs';
export class NeoCyclist { export class NeoCyclist {
constructor({ onTrigger, onToggle, getTime }) { constructor({ onSchedule, onToggle, getTime, onTrigger }) {
if (onTrigger) {
throw new Error(
'onTrigger has been dropped. use onSchedule(hap, time, cps) instead, see https://codeberg.org/uzu/strudel/pulls/2138',
);
}
this.started = false; this.started = false;
this.cps = 0.5; this.cps = 0.5;
this.getTime = getTime; // get absolute time this.getTime = getTime; // get absolute time
@@ -43,8 +48,7 @@ export class NeoCyclist {
if (hap.hasOnset()) { if (hap.hasOnset()) {
const timeUntilTrigger = cycleToSeconds(hap.whole.begin - this.cycle, this.cps); const timeUntilTrigger = cycleToSeconds(hap.whole.begin - this.cycle, this.cps);
const targetTime = timeUntilTrigger + currentTime + this.latency; const targetTime = timeUntilTrigger + currentTime + this.latency;
const duration = cycleToSeconds(hap.duration, this.cps); onSchedule?.(hap, targetTime, this.cps);
onTrigger?.(hap, 0, duration, this.cps, targetTime);
} }
}); });
}; };
+401 -40
View File
@@ -27,6 +27,7 @@ import {
import drawLine from './drawLine.mjs'; import drawLine from './drawLine.mjs';
import { errorLogger, logger } from './logger.mjs'; import { errorLogger, logger } from './logger.mjs';
import { strudelScope } from './evaluate.mjs'; import { strudelScope } from './evaluate.mjs';
import { getClockTime } from './schedulerState.mjs';
let stringParser; let stringParser;
@@ -504,10 +505,25 @@ export class Pattern {
/** /**
* Returns a new pattern with the given function applied to the list of haps returned by every query. * Returns a new pattern with the given function applied to the list of haps returned by every query.
*
* As an end user, you will most likely not need to use this; look at `withContext`, `withHapSpan`, etc. instead.
* @tags internals * @tags internals
* @param {Function} func * @param {Function} func
* @returns Pattern * @returns Pattern
* @noAutocomplete * @noAutocomplete
* @example
* note("[a b, c d] a b c d").s("gm_piano").withHaps(x=>{
* if (x.length > 1) {
* x = x.map(hap => hap.withValue(val => {
* return {
* n: 1,
* // select gm_piano:1 sample
* ...val
* }
* }));
* }
* return x
* })
*/ */
withHaps(func) { withHaps(func) {
const result = new Pattern((state) => func(this.query(state), state)); const result = new Pattern((state) => func(this.query(state), state));
@@ -517,10 +533,23 @@ export class Pattern {
/** /**
* As with `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.
*
* As an end user, you will most likely not need to use this; look at `withContext`, `withHapSpan`, etc. instead.
* @tags internals * @tags internals
* @param {Function} func * @param {Function} func
* @returns Pattern * @returns Pattern
* @noAutocomplete * @noAutocomplete
* @example
* note("c a f e").withHap(x=>{
* if (x.value.note == 'c') {
* x.value.s = 'gm_piano'
* }
* else {
* x.value.s = 'gm_piano'
* x.value.n = 2
* }
* return x
* })
*/ */
withHap(func) { withHap(func) {
return this.withHaps((haps) => haps.map(func)); return this.withHaps((haps) => haps.map(func));
@@ -850,6 +879,26 @@ export class Pattern {
return stack(this, ...pats); return stack(this, ...pats);
} }
stackLeft(...pats) {
return stackLeft(this, ...pats);
}
stackRight(...pats) {
return stackRight(this, ...pats);
}
stackCenter(...pats) {
return stackCenter(this, ...pats);
}
stackCentre(...pats) {
return stackCentre(this, ...pats);
}
stackRepeat(...pats) {
return stackRepeat(this, ...pats);
}
sequence(...pats) { sequence(...pats) {
return sequence(this, ...pats); return sequence(this, ...pats);
} }
@@ -872,23 +921,41 @@ export class Pattern {
////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////
// Context methods - ones that deal with metadata // Context methods - ones that deal with metadata
onTrigger(onTrigger, dominant = true) { /**
* Attaches a function that is called for each hap onset.
* @name onSchedule
* @memberof Pattern
* @example
* s("bd sd").onSchedule((hap, time, cps) => {
* console.log('hap scheduled', time, cps);
* })
*/
onSchedule(onSchedule, dominant = true) {
return this.withHap((hap) => return this.withHap((hap) =>
hap.setContext({ hap.setContext({
...hap.context, ...hap.context,
onTrigger: (...args) => { onSchedule: (scheduleHap, t, cps) => {
// run previously set trigger, if it exists // run previously set trigger, if it exists
hap.context.onTrigger?.(...args); hap.context.onSchedule?.(scheduleHap, t, cps);
onTrigger(...args); onSchedule(scheduleHap, t, cps);
}, },
// if dominantTrigger is set to true, the default output (webaudio) will be disabled // if dominantSchedule is set to true, the default output (webaudio) will be disabled
// when using multiple triggers, you cannot flip this flag to false again! // when using multiple triggers, you cannot flip this flag to false again!
// example: x.csound('CooLSynth').log() as well as x.log().csound('CooLSynth') should work the same // example: x.csound('CooLSynth').log() as well as x.log().csound('CooLSynth') should work the same
dominantTrigger: hap.context.dominantTrigger || dominant, dominantSchedule: hap.context.dominantSchedule || dominant,
}), }),
); );
} }
// legacy:
onTrigger(onTrigger, dominant = true) {
console.warn('onTrigger is deprecated.. use onSchedule(hap, time, cps) instead');
return this.onSchedule((hap, t, cps) => {
const currentTime = getClockTime();
return onTrigger(hap, currentTime, cps, t);
}, dominant);
}
/** /**
* Writes the content of the current event to the console (visible in the side menu). * Writes the content of the current event to the console (visible in the side menu).
* @tags visualization * @tags visualization
@@ -898,7 +965,7 @@ export class Pattern {
* s("bd sd").log() * s("bd sd").log()
*/ */
log(func = (hap) => `[hap] ${hap.showWhole(true)}`, getData = (hap) => ({ hap })) { log(func = (hap) => `[hap] ${hap.showWhole(true)}`, getData = (hap) => ({ hap })) {
return this.onTrigger((...args) => { return this.onSchedule((...args) => {
logger(func(...args), undefined, getData(...args)); logger(func(...args), undefined, getData(...args));
}, false); }, false);
} }
@@ -1074,7 +1141,7 @@ const COMPOSERS = {
* @param {Pattern} pat * @param {Pattern} pat
* @returns {Pattern} * @returns {Pattern}
* @memberof Pattern * @memberof Pattern
* @tags internal, combiners * @tags internals, combiners
* @example * @example
* // because input pattern has `s` set, * // because input pattern has `s` set,
* // it overrides the "sine" declared earlier * // it overrides the "sine" declared earlier
@@ -1099,7 +1166,7 @@ const COMPOSERS = {
* @param {Pattern} pat * @param {Pattern} pat
* @memberof Pattern * @memberof Pattern
* @returns {Pattern} * @returns {Pattern}
* @tags internal, combiners * @tags internals, combiners
* @example * @example
* // notes, already defined, will stay "c a f e", * // notes, already defined, will stay "c a f e",
* // while "s", not defined, will be set to "piano" * // while "s", not defined, will be set to "piano"
@@ -1156,24 +1223,163 @@ const COMPOSERS = {
* @tags math * @tags math
*/ */
div: [numeralArgs((a, b) => a / b)], div: [numeralArgs((a, b) => a / b)],
/**
*
* Divides each number by the given factor and returns the remainder
* @name mod
* @memberof Pattern
* @tags math
*/
mod: [numeralArgs(_mod)], mod: [numeralArgs(_mod)],
/**
*
* Raises each number to the power of the given factor
* @name pow
* @memberof Pattern
* @tags math
*/
pow: [numeralArgs(Math.pow)], pow: [numeralArgs(Math.pow)],
/**
*
* Performs a bitwise `and` on each number and the given factor
* @name band
* @memberof Pattern
* @tags math
*/
band: [numeralArgs((a, b) => a & b)], band: [numeralArgs((a, b) => a & b)],
/**
*
* Performs a bitwise `or` on each number and the given factor
* @name bor
* @memberof Pattern
* @tags math
*/
bor: [numeralArgs((a, b) => a | b)], bor: [numeralArgs((a, b) => a | b)],
/**
*
* Performs a bitwise `xor` on each number and the given factor
* @name bxor
* @memberof Pattern
* @tags math
*/
bxor: [numeralArgs((a, b) => a ^ b)], bxor: [numeralArgs((a, b) => a ^ b)],
/**
*
* Performs a bitwise `left shift` on each number by the given factor
* @name blshift
* @memberof Pattern
* @tags math
*/
blshift: [numeralArgs((a, b) => a << b)], blshift: [numeralArgs((a, b) => a << b)],
/**
*
* Performs a bitwise `right shift` on each number by the given factor
* @name brshift
* @memberof Pattern
* @tags math
*/
brshift: [numeralArgs((a, b) => a >> b)], brshift: [numeralArgs((a, b) => a >> b)],
// TODO - force numerical comparison if both look like numbers? // TODO - force numerical comparison if both look like numbers?
/**
* performs a < b
* @name lt
* @memberof Pattern
* @tags math
* @param {any} a
* @param {any} b
* @returns Pattern with boolean value(s)
*/
lt: [(a, b) => a < b], lt: [(a, b) => a < b],
/**
* performs a > b
* @name gt
* @memberof Pattern
* @tags math
* @param {any} a
* @param {any} b
* @returns Pattern with boolean value(s)
*/
gt: [(a, b) => a > b], gt: [(a, b) => a > b],
/**
* performs a <= b
* @name lte
* @memberof Pattern
* @tags math
* @param {any} a
* @param {any} b
* @returns Pattern with boolean value(s)
*/
lte: [(a, b) => a <= b], lte: [(a, b) => a <= b],
/**
* performs a >= b
* @name gte
* @memberof Pattern
* @tags math
* @param {any} a
* @param {any} b
* @returns Pattern with boolean value(s)
*/
gte: [(a, b) => a >= b], gte: [(a, b) => a >= b],
/**
* performs a == b
* @name eq
* @memberof Pattern
* @tags math
* @param {any} a
* @param {any} b
* @returns Pattern with boolean value(s)
*/
eq: [(a, b) => a == b], eq: [(a, b) => a == b],
/**
* performs a === b (strictly equal)
* @name eqt
* @memberof Pattern
* @tags math
* @param {any} a
* @param {any} b
* @returns Pattern with boolean value(s)
*/
eqt: [(a, b) => a === b], eqt: [(a, b) => a === b],
/**
* performs a != b
* @name ne
* @memberof Pattern
* @tags math
* @param {any} a
* @param {any} b
* @returns Pattern with boolean value(s)
*/
ne: [(a, b) => a != b], ne: [(a, b) => a != b],
/**
* performs a !== b (strictly not equal)
* @name net
* @memberof Pattern
* @tags math
* @param {any} a
* @param {any} b
* @returns Pattern with boolean value(s)
*/
net: [(a, b) => a !== b], net: [(a, b) => a !== b],
/**
* performs a && b
* @name and
* @memberof Pattern
* @tags math
* @param {any} a
* @param {any} b
* @returns Pattern with boolean value(s)
*/
and: [(a, b) => a && b], and: [(a, b) => a && b],
/**
* performs a || b
* @name or
* @memberof Pattern
* @tags math
* @param {any} a
* @param {any} b
* @returns Pattern with boolean value(s)
*/
or: [(a, b) => a || b], or: [(a, b) => a || b],
// bitwise ops // bitwise ops
@@ -1449,7 +1655,19 @@ export function sequenceP(pats) {
export function stack(...pats) { export function stack(...pats) {
// Array test here is to avoid infinite recursions.. // Array test here is to avoid infinite recursions..
pats = pats.map((pat) => (Array.isArray(pat) ? sequence(...pat) : reify(pat))); pats = pats.map((pat) => (Array.isArray(pat) ? sequence(...pat) : reify(pat)));
const query = (state) => flatten(pats.map((pat) => pat.query(state))); const query = (state) =>
flatten(
pats.map((pat) => {
try {
// An error occurring in one part of the stack should not break
// playback of the other parts of the stack.
return pat.query(state);
} catch (e) {
errorLogger(e);
return [];
}
}),
);
const result = new Pattern(query); const result = new Pattern(query);
if (__steps) { if (__steps) {
result._steps = lcm(...pats.map((pat) => pat._steps)); result._steps = lcm(...pats.map((pat) => pat._steps));
@@ -1470,6 +1688,18 @@ function _stackWith(func, pats) {
return stack(...func(steps, pats)); return stack(...func(steps, pats));
} }
/**
* The input patterns are aligned to the left, with silence padded to the shorter pattern(s) in the final steps if there's a stepcount disparity. See also `stackRight` and `stackCenter/stackCentre`
*
* @name stackLeft
* @tags temporal, stepwise, combiners
* @return {Pattern}
* @example
* stackLeft("0 1 2 3", "3 4", "[4 5 6] 7 8 9 10").scale("c:minor").note()
* @example
* // chainable
* "0 1 2 3".stackLeft("3 4", "[4 5 6] 7 8 9 10").scale("c:minor").note()
*/
export function stackLeft(...pats) { export function stackLeft(...pats) {
return _stackWith( return _stackWith(
(steps, pats) => pats.map((pat) => (pat._steps.eq(steps) ? pat : stepcat(pat, gap(steps.sub(pat._steps))))), (steps, pats) => pats.map((pat) => (pat._steps.eq(steps) ? pat : stepcat(pat, gap(steps.sub(pat._steps))))),
@@ -1477,6 +1707,18 @@ export function stackLeft(...pats) {
); );
} }
/**
* The input patterns are aligned to the right, with silence padded to the shorter pattern(s) in the initial steps if there's a stepcount disparity. See also `stackLeft` and `stackCenter/stackCentre`
*
* @name stackRight
* @tags temporal, stepwise, combiners
* @return {Pattern}
* @example
* stackRight("0 1 2 3", "3 4", "[4 5 6] 7 8 9 10").scale("c:minor").note()
* @example
* // chainable
* "0 1 2 3".stackRight("3 4", "[4 5 6] 7 8 9 10").scale("c:minor").note()
*/
export function stackRight(...pats) { export function stackRight(...pats) {
return _stackWith( return _stackWith(
(steps, pats) => pats.map((pat) => (pat._steps.eq(steps) ? pat : stepcat(gap(steps.sub(pat._steps)), pat))), (steps, pats) => pats.map((pat) => (pat._steps.eq(steps) ? pat : stepcat(gap(steps.sub(pat._steps)), pat))),
@@ -1484,7 +1726,19 @@ export function stackRight(...pats) {
); );
} }
export function stackCentre(...pats) { /**
* The input patterns are aligned to the right, with silence padded to the shorter pattern(s) in the initial steps if there's a stepcount disparity. See also `stackLeft` and `stackRight`
* @name stackCenter
* @tags temporal, stepwise, combiners
* @synonyms stackCentre
* @return {Pattern}
* @example
* stackCenter("0 1 2 3", "3 4", "[4 5 6] 7 8 9 10").scale("c:minor").note()
* @example
* // chainable
* "0 1 2 3".stackCenter("3 4", "[4 5 6] 7 8 9 10").scale("c:minor").note()
*/
export function stackCenter(...pats) {
return _stackWith( return _stackWith(
(steps, pats) => (steps, pats) =>
pats.map((pat) => { pats.map((pat) => {
@@ -1498,6 +1752,25 @@ export function stackCentre(...pats) {
); );
} }
export function stackCentre(...pats) {
return stackCenter(...pats);
}
/**
* Shorthand for `pm(patterns).pace(m)` where m is the max step count in the input patterns
* @name stackRepeat
* @tags temporal, stepwise, combiners
* @return {Pattern}
* @example
* stackRepeat("0 1 2 3", "3 4", "[4 5 6] 7 8 9 10").scale("c:minor").note()
* @example
* // chainable
* "0 1 2 3".stackRepeat("3 4", "[4 5 6] 7 8 9 10").scale("c:minor").note()
*/
export function stackRepeat(...pats) {
return stackBy(reify('repeat'), ...pats);
}
export function stackBy(by, ...pats) { export function stackBy(by, ...pats) {
const [left, ...right] = pats.map((pat) => pat._steps); const [left, ...right] = pats.map((pat) => pat._steps);
const steps = left.maximum(...right); const steps = left.maximum(...right);
@@ -1612,10 +1885,10 @@ export function arrange(...sections) {
* @tags combiners * @tags combiners
* @return {Pattern} * @return {Pattern}
* @example * @example
seqPLoop( * seqPLoop(
[0, 2, "bd(3,8)"], * [0, 2, "bd(3,8)"],
[1, 3, "cp(3,8)"] * [1, 3, "cp(3,8)"]
).sound() * ).sound()
*/ */
export function seqPLoop(...parts) { export function seqPLoop(...parts) {
let total = Fraction(0); let total = Fraction(0);
@@ -2292,11 +2565,11 @@ export const { zoomArc, zoomarc } = register(['zoomArc', 'zoomarc'], function (a
* @param {number} slices to play * @param {number} slices to play
* @example * @example
* note("0 1 2 3 4 5 6 7".scale('c:mixolydian')) * note("0 1 2 3 4 5 6 7".scale('c:mixolydian'))
*.bite(4, "3 2 1 0") * .bite(4, "3 2 1 0")
* @example * @example
* sound("bd - bd bd*2, - sd:6 - sd:5 sd:1 - [- sd:2] -, hh [- cp:7]") * sound("bd - bd bd*2, - sd:6 - sd:5 sd:1 - [- sd:2] -, hh [- cp:7]")
.bank("RolandTR909").speed(1.2) * .bank("RolandTR909").speed(1.2)
.bite(4, "0 0 [1 2] <3 2> 0 0 [2 1] 3") * .bite(4, "0 0 [1 2] <3 2> 0 0 [2 1] 3")
*/ */
export const bite = register( export const bite = register(
'bite', 'bite',
@@ -2313,6 +2586,74 @@ export const bite = register(
false, false,
); );
/**
* Similar to `struct`, but every hap in the structure pattern plays the next
* cycle of the main pattern.
* With a different number of cycles in the main pattern and haps in the
* structure pattern, this creates an isorhythm.
*
* Note: This function works on the assumption that the structure pattern
* repeats every cycle. If the structure pattern only repeats after multiple
* cycles, it may skip or repeat certain parts of the melody.
* @tags temporal
* @name isobite
* @synonyms cycleStruct, cyclestruct
* @memberof Pattern
* @param {Pattern} structure structure of the pattern
* @example
* "<0 1 2 3 4 5>".isobite("[x - x*2]*2").n().scale("c:major")
* // the same as "0 - [1 2] 3 - [4 5]".n().scale("c:major")
*
* @example
* "<0 1 2 3 4>".isobite("x - x").n().scale("c:major")
* // the same as "<[0 - 1] [2 - 3] [4 - 0] [1 - 2] [3 - 4]>".n().scale("c:major")
*
* @example
* // Here the structure pattern repeats after 2 cycles,
* // so certain haps from the melody are skipped.
* "<0 1 2 3>".isobite("x x*2".slow(2)).n().scale("c:major")
* // the same as "<[0] [2 3] [2] [2 3]>".n().scale("c:major")
*
* @example
* // This can be avoided by making the struct pattern 1 cycle,
* // and slowing the whole pattern down as follows:
* "<0 1 2 3>".isobite("x x*2").slow(2).n().scale("c:major")
* // the same as "<[0] [1 2] [3] [0 1] [2] [3 0] [1] [2 3]>".n().scale("c:major")
*/
export const { isobite, cyclestruct, cycleStruct } = register(
['isobite', 'cyclestruct', 'cycleStruct'],
(structure, pat) => {
const structPat = new Pattern((state) => {
const haps_cycle = structure
.withQuerySpan((span) => span.begin.wholeCycle())
.query(state)
.filter((hap) => hap.value != 0)
.sort((a, b) => a.whole.begin - b.whole.begin)
.map((hap, i) => hap.withValue((_) => i));
const haps = [];
for (const hap of haps_cycle) {
const new_part = state.span.intersection(hap.part);
if (new_part) {
haps.push(new Hap(hap.whole, new_part, hap.value, hap.context));
}
}
return haps;
}).splitQueries();
const stepsPattern = new Pattern((state) => {
const nrHaps = structure
.withQuerySpan((span) => span.begin.wholeCycle())
.query(state)
.filter((hap) => hap.value != 0).length;
return [new Hap(state.span.begin.wholeCycle(), state.span, nrHaps, {})];
}).splitQueries();
return pat.fast(stepsPattern).bite(stepsPattern, structPat);
},
false,
);
/** /**
* Selects the given fraction of the pattern and repeats that part to fill the remainder of the cycle. * Selects the given fraction of the pattern and repeats that part to fill the remainder of the cycle.
* @tags temporal * @tags temporal
@@ -2419,8 +2760,12 @@ export const off = register('off', function (time_pat, func, pat) {
* Returns a new pattern where every other cycle is played once, twice as * Returns a new pattern where every other cycle is played once, twice as
* fast, and offset in time by one quarter of a cycle. Creates a kind of * fast, and offset in time by one quarter of a cycle. Creates a kind of
* breakbeat feel. * breakbeat feel.
* @name brak
* @tags temporal * @tags temporal
* @returns Pattern * @returns Pattern
* @memberof Pattern
* @example
* s("bd hh!2 rim").brak()
*/ */
export const brak = register('brak', function (pat) { export const brak = register('brak', function (pat) {
return pat.when(slowcat(false, true), (x) => fastcat(x, silence)._late(0.25)); return pat.when(slowcat(false, true), (x) => fastcat(x, silence)._late(0.25));
@@ -2909,10 +3254,29 @@ export const { ribbon, rib } = register(['ribbon', 'rib'], (offset, cycles, pat)
pat.early(offset).restart(pure(1).slow(cycles)), pat.early(offset).restart(pure(1).slow(cycles)),
); );
/**
* Sets the pattern's color to the given [hsla](https://www.w3schools.com/htmL/html_colors_hsl.asp)
*
* @name hsla
* @param {Number} hue (0 - 360)
* @param {Number} saturation percentage (0 - 100)
* @param {Number} lightness percentage (0 - 100)
* @param {Number} alpha (transparency) (0.0 - 1.0)
* @tags visualization
*/
export const hsla = register('hsla', (h, s, l, a, pat) => { export const hsla = register('hsla', (h, s, l, a, pat) => {
return pat.color(`hsla(${h}turn,${s * 100}%,${l * 100}%,${a})`); return pat.color(`hsla(${h}turn,${s * 100}%,${l * 100}%,${a})`);
}); });
/**
* Sets the pattern's color to the given [hsl](https://www.w3schools.com/htmL/html_colors_hsl.asp)
*
* @name hsl
* @param {Number} hue (0 - 360)
* @param {Number} saturation percentage (0 - 100)
* @param {Number} lightness percentage (0 - 100)
* @tags visualization
*/
export const hsl = register('hsl', (h, s, l, pat) => { export const hsl = register('hsl', (h, s, l, pat) => {
return pat.color(`hsl(${h}turn,${s * 100}%,${l * 100}%)`); return pat.color(`hsl(${h}turn,${s * 100}%,${l * 100}%)`);
}); });
@@ -3268,7 +3632,7 @@ export const drop = stepRegister('drop', function (i, pat) {
* @tags stepwise * @tags stepwise
* @example * @example
* stepcat( * stepcat(
* sound("bd bd - cp").extend(2), * sound("bd bd - <cp hh>").extend(2),
* sound("bd - sd -") * sound("bd - sd -")
* ).pace(8) * ).pace(8)
*/ */
@@ -3279,15 +3643,12 @@ export const extend = stepRegister('extend', function (factor, pat) {
/** /**
* *Experimental* * *Experimental*
* *
* `replicate` is similar to `fast` in that it increases its density, but it also increases the step count * `replicate` is similar to `extend`, except that `replicate` will "replicate" the thing being extended. This will only differ from `extend` when the input pattern differs from cycle to cycle.
* accordingly. So `stepcat("a b".replicate(2), "c d")` would be the same as `"a b a b c d"`, whereas
* `stepcat("a b".fast(2), "c d")` would be the same as `"[a b] [a b] c d"`.
* *
* TODO: find out how this function differs from extend
* @tags stepwise * @tags stepwise
* @example * @example
* stepcat( * stepcat(
* sound("bd bd - cp").replicate(2), * sound("bd bd - <cp hh>").replicate(2),
* sound("bd - sd -") * sound("bd - sd -")
* ).pace(8) * ).pace(8)
*/ */
@@ -3383,8 +3744,8 @@ export const growlist = (amount, pat) => pat.growlist(amount);
* "tha dhi thom nam".shrink("1 -1").sound().bank("mridangam").pace(4) * "tha dhi thom nam".shrink("1 -1").sound().bank("mridangam").pace(4)
* @example * @example
* note("0 1 2 3 4 5 6 7".scale("C:ritusen")).sound("folkharp") * note("0 1 2 3 4 5 6 7".scale("C:ritusen")).sound("folkharp")
.shrink("1 -1").pace(8) * .shrink("1 -1").pace(8)
*
*/ */
export const shrink = register( export const shrink = register(
@@ -3423,7 +3784,7 @@ export const shrink = register(
* "tha dhi thom nam".grow("1 -1").sound().bank("mridangam").pace(4) * "tha dhi thom nam".grow("1 -1").sound().bank("mridangam").pace(4)
* @example * @example
* note("0 1 2 3 4 5 6 7".scale("C:ritusen")).sound("folkharp") * note("0 1 2 3 4 5 6 7".scale("C:ritusen")).sound("folkharp")
.grow("1 -1").pace(8) * .grow("1 -1").pace(8)
*/ */
export const grow = register( export const grow = register(
@@ -3447,17 +3808,17 @@ export const grow = register(
/** /**
* *Experimental* * *Experimental*
* *
* Inserts a pattern into a list of patterns. On the first repetition it will be inserted at the end of the list, then moved backwards through the list * Inserts a pattern into a list of patterns. On the first repetition it will be inserted at the end of the list, then moved backwards through the list
* on successive repetitions. The patterns are added together stepwise, with all repetitions taking place over a single cycle. Using `pace` to set the * on successive repetitions. The patterns are added together stepwise, with all repetitions taking place over a single cycle. Using `pace` to set the
* number of steps per cycle is therefore usually recommended. * number of steps per cycle is therefore usually recommended.
* *
* @tags stepwise * @tags stepwise
* @return {Pattern} * @return {Pattern}
* @example * @example
* "[c g]".tour("e f", "e f g", "g f e c").note() * "[c g]".tour("e f", "e f g", "g f e c").note()
.sound("folkharp") * .sound("folkharp")
.pace(8) * .pace(8)
*/ */
export const tour = function (pat, ...many) { export const tour = function (pat, ...many) {
return pat.tour(...many); return pat.tour(...many);
@@ -3475,16 +3836,16 @@ Pattern.prototype.tour = function (...many) {
/** /**
* *Experimental* * *Experimental*
* *
* 'zips' together the steps of the provided patterns. This can create a long repetition, taking place over a single, dense cycle. * 'zips' together the steps of the provided patterns. This can create a long repetition, taking place over a single, dense cycle.
* Using `pace` to set the number of steps per cycle is therefore usually recommended. * Using `pace` to set the number of steps per cycle is therefore usually recommended.
* *
* @tags stepwise * @tags stepwise
* @returns {Pattern} * @returns {Pattern}
* @example * @example
* zip("e f", "e f g", "g [f e] a f4 c").note() * zip("e f", "e f g", "g [f e] a f4 c").note()
.sound("folkharp") * .sound("folkharp")
.pace(8) * .pace(8)
*/ */
export const zip = function (...pats) { export const zip = function (...pats) {
pats = pats.filter((pat) => pat.hasSteps); pats = pats.filter((pat) => pat.hasSteps);
@@ -3647,9 +4008,9 @@ export const slice = register(
* s("bd!8").onTriggerTime((hap) => {console.log(hap)}) * s("bd!8").onTriggerTime((hap) => {console.log(hap)})
*/ */
Pattern.prototype.onTriggerTime = function (func) { Pattern.prototype.onTriggerTime = function (func) {
return this.onTrigger((hap, currentTime, _cps, targetTime) => { return this.onSchedule((hap, targetTime) => {
const diff = targetTime - currentTime; const diff = targetTime - getClockTime();
window.setTimeout(() => { setTimeout(() => {
func(hap); func(hap);
}, diff * 1000); }, diff * 1000);
}, false); }, false);
+2
View File
@@ -104,6 +104,8 @@ export const pickmodF = register('pickmodF', function (pickPattern, lookup, pat)
* @param {Pattern} pat * @param {Pattern} pat
* @param {*} xs * @param {*} xs
* @returns {Pattern} * @returns {Pattern}
* @example
* note("<0*4 1 2*2 3*3>".pickOut(["g a", "e f", "f g f g" , "g c d"]))
*/ */
export const pickOut = register('pickOut', function (lookup, pat) { export const pickOut = register('pickOut', function (lookup, pat) {
return _pick(lookup, pat, false).outerJoin(); return _pick(lookup, pat, false).outerJoin();
+21 -14
View File
@@ -7,13 +7,15 @@ import {
setIsStarted, setIsStarted,
setPattern as exposeSchedulerPattern, setPattern as exposeSchedulerPattern,
setTime, setTime,
setTriggerFunc, setScheduleFunc,
setClockTime,
} from './schedulerState.mjs'; } from './schedulerState.mjs';
import { evalScope } from './evaluate.mjs'; import { evalScope } from './evaluate.mjs';
import { register, Pattern, isPattern, silence, stack } from './pattern.mjs'; import { register, Pattern, isPattern, silence, stack } from './pattern.mjs';
import { reset_state } from './impure.mjs'; import { reset_state } from './impure.mjs';
export function repl({ export function repl({
onSchedule,
defaultOutput, defaultOutput,
onEvalError, onEvalError,
beforeEval, beforeEval,
@@ -30,6 +32,11 @@ export function repl({
id, id,
mondo = false, mondo = false,
}) { }) {
if (defaultOutput) {
throw new Error(
'defaultOutput has been removed. use onSchedule(hap, time, cps) instead. see https://codeberg.org/uzu/strudel/pulls/2138/',
);
}
const state = { const state = {
schedulerError: undefined, schedulerError: undefined,
evalError: undefined, evalError: undefined,
@@ -55,7 +62,7 @@ export function repl({
}; };
const schedulerOptions = { const schedulerOptions = {
onTrigger: getTrigger({ defaultOutput, getTime }), onSchedule: getSchedule({ onSchedule }),
getTime, getTime,
onToggle: (started) => { onToggle: (started) => {
updateState({ started }); updateState({ started });
@@ -73,7 +80,7 @@ export function repl({
// NeoCyclist uses a shared worker to communicate between instances, which is not supported on mobile chrome // NeoCyclist uses a shared worker to communicate between instances, which is not supported on mobile chrome
const scheduler = const scheduler =
sync && typeof SharedWorker != 'undefined' ? new NeoCyclist(schedulerOptions) : new Cyclist(schedulerOptions); sync && typeof SharedWorker != 'undefined' ? new NeoCyclist(schedulerOptions) : new Cyclist(schedulerOptions);
setTriggerFunc(schedulerOptions.onTrigger); setScheduleFunc(schedulerOptions.onSchedule);
setCpsFunc(() => scheduler.cps); setCpsFunc(() => scheduler.cps);
let pPatterns = {}; let pPatterns = {};
let anonymousIndex = 0; let anonymousIndex = 0;
@@ -224,6 +231,7 @@ export function repl({
return pattern; return pattern;
}; };
setTime(() => scheduler.now()); // TODO: refactor? setTime(() => scheduler.now()); // TODO: refactor?
setClockTime(() => scheduler.getTime()); // TODO: refactor?
// Helper function to apply pattern transformations (solo, each, all) // Helper function to apply pattern transformations (solo, each, all)
// this should be abstracted more // this should be abstracted more
@@ -407,6 +415,7 @@ export function repl({
updateState({ code, pending: true }); updateState({ code, pending: true });
await injectPatternMethods(); await injectPatternMethods();
setTime(() => scheduler.now()); // TODO: refactor? setTime(() => scheduler.now()); // TODO: refactor?
setClockTime(() => scheduler.getTime()); // TODO: refactor?
await beforeEval?.({ code, blockBased: false }); await beforeEval?.({ code, blockBased: false });
allTransforms = []; // reset all transforms allTransforms = []; // reset all transforms
@@ -452,6 +461,7 @@ export function repl({
updateState({ code, pending: true }); updateState({ code, pending: true });
await injectPatternMethods(); await injectPatternMethods();
setTime(() => scheduler.now()); // TODO: refactor? setTime(() => scheduler.now()); // TODO: refactor?
setClockTime(() => scheduler.getTime()); // TODO: refactor?
await beforeEval?.({ code, blockBased: true }); await beforeEval?.({ code, blockBased: true });
allTransforms = []; // reset all transforms allTransforms = []; // reset all transforms
@@ -560,20 +570,17 @@ export function repl({
return { scheduler, evaluate, evaluateBlock, start, stop, pause, setCps, setPattern, setCode, toggle, state }; return { scheduler, evaluate, evaluateBlock, start, stop, pause, setCps, setPattern, setCode, toggle, state };
} }
export const getTrigger = export const getSchedule =
({ getTime, defaultOutput }) => ({ onSchedule }) =>
async (hap, deadline, duration, cps, t) => { async (hap, t, cps) => {
// ^ this signature is different from hap.context.onTrigger, as set by Pattern.onTrigger(onTrigger)
// TODO: get rid of deadline after https://codeberg.org/uzu/strudel/pulls/1004
try { try {
if (!hap.context.onTrigger || !hap.context.dominantTrigger) { if (!hap.context.onSchedule || !hap.context.dominantSchedule) {
await defaultOutput(hap, deadline, duration, cps, t); await onSchedule(hap, t, cps);
} }
if (hap.context.onTrigger) { if (hap.context.onSchedule) {
// call signature of output / onTrigger is different... await hap.context.onSchedule(hap, t, cps);
await hap.context.onTrigger(hap, getTime(), cps, t);
} }
} catch (err) { } catch (err) {
errorLogger(err, 'getTrigger'); errorLogger(err, 'getSchedule');
} }
}; };
+33 -5
View File
@@ -7,8 +7,14 @@ This program is free software: you can redistribute it and/or modify it under th
let time; let time;
let cpsFunc; let cpsFunc;
let pattern; let pattern;
let triggerFunc; let scheduleFunc;
let isStarted; let isStarted;
/**
* @name getTime
* @tags internals
* @returns {Number} the current cycle time as a number
*/
export function getTime() { export function getTime() {
if (!time) { if (!time) {
throw new Error('no time set! use setTime to define a time source'); throw new Error('no time set! use setTime to define a time source');
@@ -20,10 +26,32 @@ export function setTime(func) {
time = func; time = func;
} }
let clockTime;
/**
* @name getClockTime
* @tags internals
* @returns {Number} the current clock time in seconds (onSchedule time)
*/
export function getClockTime() {
if (!clockTime) {
throw new Error('no clockTime set! use setClockTime to define a time source');
}
return clockTime();
}
export function setClockTime(func) {
clockTime = func;
}
export function setCpsFunc(func) { export function setCpsFunc(func) {
cpsFunc = func; cpsFunc = func;
} }
/**
* @name getCps
* @tags internals
* @returns {Number} the current cycles per second
*/
export function getCps() { export function getCps() {
return cpsFunc?.(); return cpsFunc?.();
} }
@@ -36,12 +64,12 @@ export function getPattern() {
return pattern; return pattern;
} }
export function setTriggerFunc(func) { export function setScheduleFunc(func) {
triggerFunc = func; scheduleFunc = func;
} }
export function getTriggerFunc() { export function getScheduleFunc() {
return triggerFunc; return scheduleFunc;
} }
export function setIsStarted(val) { export function setIsStarted(val) {
+55 -9
View File
@@ -5,7 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th
*/ */
import { Hap } from './hap.mjs'; import { Hap } from './hap.mjs';
import { Pattern, fastcat, pure, register, reify, silence, stack, sequenceP } from './pattern.mjs'; import { Pattern, fastcat, pure, register, reify, silence, stack, sequenceP, isPattern } from './pattern.mjs';
import { _mod } from './util.mjs'; import { _mod } from './util.mjs';
import Fraction from './fraction.mjs'; import Fraction from './fraction.mjs';
@@ -16,6 +16,24 @@ export function steady(value) {
return new Pattern((state) => [new Hap(undefined, state.span, value)]); return new Pattern((state) => [new Hap(undefined, state.span, value)]);
} }
/**
* Creates a signal given by the input function
*
* @name signal
* @param {*} func
* @returns {Pattern} signal
* @tags generators
* @example
* // sawtooth signal definition
* signal(t=>t%1).segment(16).range(30,40).note()
* @example
* signal(t=>Math.pow(t,2)%1).range(40, 60).add(5).segment(16)
* .off(0, x=>x.add(7))
* .note()
* .s("gm_electric_guitar_jazz,gm_epiano")
* .gain(1.7)
* .pianoroll()
*/
export const signal = (func) => { export const signal = (func) => {
const query = (state) => [new Hap(undefined, state.span, func(state.span.begin, state.controls))]; const query = (state) => [new Hap(undefined, state.span, func(state.span.begin, state.controls))];
return new Pattern(query); return new Pattern(query);
@@ -189,6 +207,7 @@ export const time = signal(id);
/** /**
* The mouse's x position value ranges from 0 to 1. * The mouse's x position value ranges from 0 to 1.
* @name mousex * @name mousex
* @synonyms mouseX
* @return {Pattern} * @return {Pattern}
* @tags external_io * @tags external_io
* @example * @example
@@ -199,6 +218,7 @@ export const time = signal(id);
/** /**
* The mouse's y position value ranges from 0 to 1. * The mouse's y position value ranges from 0 to 1.
* @name mousey * @name mousey
* @synonyms mouseY
* @return {Pattern} * @return {Pattern}
* @tags external_io * @tags external_io
* @example * @example
@@ -559,6 +579,9 @@ export const __chooseWith = (pat, xs) => {
* @returns {Pattern} * @returns {Pattern}
* @example * @example
* note("c2 g2!2 d2 f1").s(chooseWith(sine.fast(2), ["sawtooth", "triangle", "bd:6"])) * note("c2 g2!2 d2 f1").s(chooseWith(sine.fast(2), ["sawtooth", "triangle", "bd:6"]))
* @example
* // Compare with chooseInWith
* note(chooseWith("<0 0.5 1>*3", ["c3", "b3", "d3"]))
*/ */
export const chooseWith = (pat, xs) => { export const chooseWith = (pat, xs) => {
return __chooseWith(pat, xs).outerJoin(); return __chooseWith(pat, xs).outerJoin();
@@ -570,6 +593,13 @@ export const chooseWith = (pat, xs) => {
* @tags temporal * @tags temporal
* @param {Pattern} pat * @param {Pattern} pat
* @param {*} xs * @param {*} xs
* @example
* // Because the structure comes from the chosen values (the notes, here)
* // The choosing pattern will only advance once per cycle (not 3 times per cycle)
* note(chooseInWith("<0 0.5 1>*3", ["c3", "b3", "d3"]))
* @example
* // Here, the chosen values have a structure such that the choosing pattern will advance 3 times per cycle
* note(chooseInWith("<0 0.5 1>*3", ["c3*3", "b3*3", "d3*3"]))
* @returns {Pattern} * @returns {Pattern}
*/ */
export const chooseInWith = (pat, xs) => { export const chooseInWith = (pat, xs) => {
@@ -586,9 +616,7 @@ export const chooseInWith = (pat, xs) => {
*/ */
export const choose = (...xs) => chooseWith(rand, xs); export const choose = (...xs) => chooseWith(rand, xs);
// todo: doc
export const chooseIn = (...xs) => chooseInWith(rand, xs); export const chooseIn = (...xs) => chooseInWith(rand, xs);
export const chooseOut = choose;
/** /**
* Chooses from the given list of values (or patterns of values), according * Chooses from the given list of values (or patterns of values), according
@@ -734,12 +762,30 @@ export const perlin = signal((t, controls) => _perlin(t, controls.randSeed));
*/ */
export const berlin = signal((t, controls) => _berlin(t, controls.randSeed)); export const berlin = signal((t, controls) => _berlin(t, controls.randSeed));
export const degradeByWith = register( /**
'degradeByWith', * `degradeByWith` is like `degradeBy`, but with control over the random or other signal used to filter out the events.
(withPat, x, pat) => pat.fmap((a) => (_) => a).appLeft(withPat.filterValues((v) => v > x)), *
true, * `degradeBy(x)` is essentially `degradeByWith(rand, x)`
true, *
); * This allows you to do things other than with `rand`, see examples below
*
* @name degradeByWith
* @param {Pattern|number} test
* @param {Pattern|number} testAgainst
* @memberof Pattern
* @tags temporal
* @returns {Pattern}
* @example
* note("c a f e").degradeByWith(sine, .1)
* @example
* note("c a f e").degradeByWith(perlin, .5)
* @example
* note("c a f e").degradeByWith(sine, cosine)
*/
export const degradeByWith = register('degradeByWith', (withPat, x, pat) => {
if (!isPattern(withPat)) withPat = pure(withPat);
return pat.fmap((a) => (_) => a).appLeft(withPat.filterValues((v) => v > x));
});
/** /**
* Randomly removes events from the pattern by a given amount. * Randomly removes events from the pattern by a given amount.
+1 -1
View File
@@ -32,7 +32,7 @@ function triggerSpeech(words, lang, voice) {
} }
export const speak = register('speak', function (lang, voice, pat) { export const speak = register('speak', function (lang, voice, pat) {
return pat.onTrigger((hap) => { return pat.onSchedule((hap) => {
triggerSpeech(hap.value, lang, voice); triggerSpeech(hap.value, lang, voice);
}); });
}); });
+77
View File
@@ -29,6 +29,44 @@ describe('euclid', () => {
.map((a) => a.showWhole(true)), .map((a) => a.showWhole(true)),
).toStrictEqual(['0/1 → 1/8: a', '1/4 → 3/8: a', '3/8 → 1/2: a', '5/8 → 3/4: a', '3/4 → 7/8: a']); ).toStrictEqual(['0/1 → 1/8: a', '1/4 → 3/8: a', '3/8 → 1/2: a', '5/8 → 3/4: a', '3/4 → 7/8: a']);
}); });
it('Can create euclidLegato using negative steps', () => {
expect(
fastcat('a')
.euclid(3, -8)
.firstCycle()
.sort((a, b) => a.part.begin.sub(b.part.begin))
.map((a) => a.showWhole(true)),
).toStrictEqual(
fastcat('a')
.euclidLegato(3, 8)
.firstCycle()
.sort((a, b) => a.part.begin.sub(b.part.begin))
.map((a) => a.showWhole(true)),
);
expect(
fastcat('a')
.euclid(5, -8)
.firstCycle()
.sort((a, b) => a.part.begin.sub(b.part.begin))
.map((a) => a.showWhole(true)),
).toStrictEqual(
fastcat('a')
.euclidLegato(5, 8)
.firstCycle()
.sort((a, b) => a.part.begin.sub(b.part.begin))
.map((a) => a.showWhole(true)),
);
});
it('stretches over multiple cycles if pulses is greater than steps', () => {
expect(
fastcat('a')
.euclid(5, 4)
.fast(2)
.firstCycle()
.sort((a, b) => a.part.begin.sub(b.part.begin))
.map((a) => a.showWhole(true)),
).toStrictEqual(['0/1 → 1/8: a', '1/4 → 3/8: a', '3/8 → 1/2: a', '5/8 → 3/4: a', '3/4 → 7/8: a']);
});
}); });
describe('euclidRot', () => { describe('euclidRot', () => {
@@ -67,6 +105,45 @@ describe('euclidLegato', () => {
.map((a) => a.showWhole(true)), .map((a) => a.showWhole(true)),
).toStrictEqual(['0/1 → 1/4: a', '1/4 → 3/8: a', '3/8 → 5/8: a', '5/8 → 3/4: a', '3/4 → 1/1: a']); ).toStrictEqual(['0/1 → 1/4: a', '1/4 → 3/8: a', '3/8 → 5/8: a', '5/8 → 3/4: a', '3/4 → 1/1: a']);
}); });
it('Can create euclid using negative steps', () => {
expect(
fastcat('a')
.euclidLegato(3, -8)
.firstCycle()
.sort((a, b) => a.part.begin.sub(b.part.begin))
.map((a) => a.showWhole(true)),
).toStrictEqual(
fastcat('a')
.euclid(3, 8)
.firstCycle()
.sort((a, b) => a.part.begin.sub(b.part.begin))
.map((a) => a.showWhole(true)),
);
expect(
fastcat('a')
.euclidLegato(5, -8)
.firstCycle()
.sort((a, b) => a.part.begin.sub(b.part.begin))
.map((a) => a.showWhole(true)),
).toStrictEqual(
fastcat('a')
.euclid(5, 8)
.firstCycle()
.sort((a, b) => a.part.begin.sub(b.part.begin))
.map((a) => a.showWhole(true)),
);
});
it('stretches over multiple cycles if pulses is greater than steps', () => {
expect(
fastcat('a')
.euclidLegato(5, 4)
.fast(2)
.firstCycle()
.filter((hap) => hap.hasOnset())
.sort((a, b) => a.part.begin.sub(b.part.begin))
.map((a) => a.showWhole(true)),
).toStrictEqual(['0/1 → 1/4: a', '1/4 → 3/8: a', '3/8 → 5/8: a', '5/8 → 3/4: a', '3/4 → 1/1: a']);
});
}); });
describe('euclidLegatoRot', () => { describe('euclidLegatoRot', () => {
+23 -3
View File
@@ -58,7 +58,7 @@ import {
arp, arp,
} from '../index.mjs'; } from '../index.mjs';
import { log, logValues } from '../pattern.mjs'; import { log, logValues, stackCenter } from '../pattern.mjs';
import { steady } from '../signal.mjs'; import { steady } from '../signal.mjs';
@@ -75,6 +75,10 @@ const sameFirst = (a, b) => {
return expect(a.sortHapsByPart().firstCycle()).toStrictEqual(b.sortHapsByPart().firstCycle()); return expect(a.sortHapsByPart().firstCycle()).toStrictEqual(b.sortHapsByPart().firstCycle());
}; };
const sameFirstX = (x, a, b) => {
return expect(a.fast(x).sortHapsByPart().firstCycle()).toStrictEqual(b.fast(x).sortHapsByPart().firstCycle());
};
describe('TimeSpan', () => { describe('TimeSpan', () => {
describe('equals()', () => { describe('equals()', () => {
it('Should be equal to the same value', () => { it('Should be equal to the same value', () => {
@@ -1215,6 +1219,7 @@ describe('Pattern', () => {
expect(stackRight(fastcat(0, 1, 2), fastcat(3, 4))._steps).toStrictEqual(Fraction(3)); expect(stackRight(fastcat(0, 1, 2), fastcat(3, 4))._steps).toStrictEqual(Fraction(3));
// maybe this should double when they are either all even or all odd // maybe this should double when they are either all even or all odd
expect(stackCentre(fastcat(0, 1, 2), fastcat(3, 4))._steps).toStrictEqual(Fraction(3)); expect(stackCentre(fastcat(0, 1, 2), fastcat(3, 4))._steps).toStrictEqual(Fraction(3));
expect(stackCenter(fastcat(0, 1, 2), fastcat(3, 4))._steps).toStrictEqual(Fraction(3));
expect(fastcat(0, 1).ply(3)._steps).toStrictEqual(Fraction(6)); expect(fastcat(0, 1).ply(3)._steps).toStrictEqual(Fraction(6));
expect(fastcat(0, 1).setSteps(undefined).ply(3)._steps).toStrictEqual(undefined); expect(fastcat(0, 1).setSteps(undefined).ply(3)._steps).toStrictEqual(undefined);
expect(fastcat(0, 1).fast(3)._steps).toStrictEqual(Fraction(2)); expect(fastcat(0, 1).fast(3)._steps).toStrictEqual(Fraction(2));
@@ -1320,6 +1325,21 @@ describe('Pattern', () => {
); );
}); });
}); });
describe('isobite', () => {
it('can play a melody with a structure', () => {
sameFirst(
slowcat(0, 1, 2, 3, 4, 5).isobite(fastcat(true, silence, [true, true]).fast(2)),
fastcat(0, silence, [1, 2], 3, silence, [4, 5]),
);
});
it('syncopates over multiple cycles if the number of haps in the patterns differ', () => {
sameFirstX(
4,
slowcat(0, 1, 2, 3).isobite(fastcat(true, [true, true])),
slowcat(0, [1, 2], 3, [0, 1], 2, [3, 0], 1, [2, 3]).fast(2),
);
});
});
describe('unjoin', () => { describe('unjoin', () => {
it('destructures a pattern into subcycles', () => { it('destructures a pattern into subcycles', () => {
sameFirst( sameFirst(
@@ -1363,7 +1383,7 @@ describe('Pattern', () => {
// Force a trigger // Force a trigger
haps.forEach((hap) => { haps.forEach((hap) => {
hap.context?.onTrigger?.(hap); hap.context?.onSchedule?.(hap);
}); });
expect(mockConsoleLog).toHaveBeenCalledWith( expect(mockConsoleLog).toHaveBeenCalledWith(
@@ -1381,7 +1401,7 @@ describe('Pattern', () => {
// Force a trigger // Force a trigger
haps.forEach((hap) => { haps.forEach((hap) => {
hap.context?.onTrigger?.(hap); hap.context?.onSchedule?.(hap);
}); });
expect(mockConsoleLog).toHaveBeenCalledWith( expect(mockConsoleLog).toHaveBeenCalledWith(
+28 -14
View File
@@ -70,17 +70,6 @@ export const getEventOffsetMs = (targetTimeSeconds, currentTimeSeconds) => {
return (targetTimeSeconds - currentTimeSeconds) * 1000; return (targetTimeSeconds - currentTimeSeconds) * 1000;
}; };
/**
* @deprecated does not appear to be referenced or invoked anywhere in the codebase
* @noAutocomplete
*/
export const getFreq = (noteOrMidi) => {
if (typeof noteOrMidi === 'number') {
return midiToFreq(noteOrMidi);
}
return midiToFreq(noteToMidi(noteOrMidi));
};
const pcs = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B']; const pcs = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B'];
/** /**
* @deprecated only used in workshop (first-notes) * @deprecated only used in workshop (first-notes)
@@ -130,8 +119,31 @@ export const getPlayableNoteValue = (hap) => {
return note; return note;
}; };
/**
* Given a hap, returns the frequency of the note or freq, if defined
*
* Given a note, returns the frequency of that note
* `getFreq('c3')`
*
* Given a midi note number, returns the frequency of that midi note
* `getFreq('c3')`
*
* @name getFrequency
* @tags pitch
* @synonyms getFreq
* @param {Hap | note | number} input
* @returns {Number}
* @example
* i("4 8 9 10 - - 5 7 9 11 - -").tune("tranh3")
* .mul(getFreq('c3'))
* .freq().clip(.5).room(1)
*/
export const getFrequency = (hap) => { export const getFrequency = (hap) => {
let { value, context } = hap; let { value, context } =
typeof hap == 'object'
? hap
: // default: interpret value as midi note.
{ value: hap, context: { type: 'note' } };
// if value is number => interpret as midi number as long as its not marked as frequency // if value is number => interpret as midi number as long as its not marked as frequency
if (typeof value === 'object') { if (typeof value === 'object') {
if (value.freq) { if (value.freq) {
@@ -140,15 +152,17 @@ export const getFrequency = (hap) => {
return getFreq(value.note ?? value.n ?? value.value); return getFreq(value.note ?? value.n ?? value.value);
} }
if (typeof value === 'number' && context.type !== 'frequency') { if (typeof value === 'number' && context.type !== 'frequency') {
value = midiToFreq(hap.value); value = midiToFreq(value);
} else if (typeof value === 'string' && isNote(value)) { } else if (typeof value === 'string' && isNote(value)) {
value = midiToFreq(noteToMidi(hap.value)); value = midiToFreq(noteToMidi(value));
} else if (typeof value !== 'number') { } else if (typeof value !== 'number') {
throw new Error('not a note or frequency: ' + value); throw new Error('not a note or frequency: ' + value);
} }
return value; return value;
}; };
export const getFreq = (hap) => getFrequency(hap);
// rotate array by n steps (to the left) // rotate array by n steps (to the left)
export const rotate = (arr, n) => arr.slice(n).concat(arr.slice(0, n)); export const rotate = (arr, n) => arr.slice(n).concat(arr.slice(0, n));
+5 -3
View File
@@ -23,7 +23,7 @@ export const csound = register('csound', (instrument, pat) => {
instrument = instrument || 'triangle'; instrument = instrument || 'triangle';
init(); // not async to support csound inside other patterns + to be able to call pattern methods after it init(); // not async to support csound inside other patterns + to be able to call pattern methods after it
// TODO: find a alternative way to wait for csound to load (to wait with first time playback) // TODO: find a alternative way to wait for csound to load (to wait with first time playback)
return pat.onTrigger((hap, currentTime, _cps, targetTime) => { return pat.onSchedule((hap, targetTime) => {
if (!_csound) { if (!_csound) {
logger('[csound] not loaded yet', 'warning'); logger('[csound] not loaded yet', 'warning');
return; return;
@@ -36,9 +36,10 @@ export const csound = register('csound', (instrument, pat) => {
const controls = Object.entries({ ...hap.value, freq }) const controls = Object.entries({ ...hap.value, freq })
.flat() .flat()
.join('/'); .join('/');
// TODO: find out how to send a precise ctx based time // TODO: find out how to send a precise ctx based time
// http://www.csounds.com/manual/html/i.html // http://www.csounds.com/manual/html/i.html
const timeOffset = targetTime - currentTime; // latency ? const timeOffset = targetTime - getAudioContext().currentTime; // latency ?
//const timeOffset = time_deprecate - getAudioContext().currentTime //const timeOffset = time_deprecate - getAudioContext().currentTime
const params = [ const params = [
`"${instrument}"`, // p1: instrument name `"${instrument}"`, // p1: instrument name
@@ -144,7 +145,7 @@ export const csoundm = register('csoundm', (instrument, pat) => {
p1 = `"${instrument}"`; p1 = `"${instrument}"`;
} }
init(); // not async to support csound inside other patterns + to be able to call pattern methods after it init(); // not async to support csound inside other patterns + to be able to call pattern methods after it
return pat.onTrigger((hap, currentTime, _cps, targetTime) => { return pat.onSchedule((hap, targetTime) => {
if (!_csound) { if (!_csound) {
logger('[csound] not loaded yet', 'warning'); logger('[csound] not loaded yet', 'warning');
return; return;
@@ -152,6 +153,7 @@ export const csoundm = register('csoundm', (instrument, pat) => {
if (typeof hap.value !== 'object') { if (typeof hap.value !== 'object') {
throw new Error('csound only support objects as hap values'); throw new Error('csound only support objects as hap values');
} }
const currentTime = getAudioContext().currentTime;
// Time in seconds counting from now. // Time in seconds counting from now.
const p2 = targetTime - currentTime; const p2 = targetTime - currentTime;
const p3 = hap.duration.valueOf() + 0; const p3 = hap.duration.valueOf() + 0;
+3 -1
View File
@@ -1,3 +1,4 @@
import { getAudioContext } from '@strudel/webaudio/index.mjs';
import { Invoke } from './utils.mjs'; import { Invoke } from './utils.mjs';
import { Pattern, getEventOffsetMs, noteToMidi } from '@strudel/core'; import { Pattern, getEventOffsetMs, noteToMidi } from '@strudel/core';
@@ -6,7 +7,8 @@ const OFF_MESSAGE = 0x80;
const CC_MESSAGE = 0xb0; const CC_MESSAGE = 0xb0;
Pattern.prototype.midi = function (output) { Pattern.prototype.midi = function (output) {
return this.onTrigger((hap, currentTime, cps, targetTime) => { return this.onSchedule((hap, targetTime, cps) => {
const currentTime = getAudioContext().currentTime;
let { note, nrpnn, nrpv, ccn, ccv, velocity = 0.9, gain = 1 } = hap.value; let { note, nrpnn, nrpv, ccn, ccv, velocity = 0.9, gain = 1 } = hap.value;
//magic number to get audio engine to line up, can probably be calculated somehow //magic number to get audio engine to line up, can probably be calculated somehow
const latencyMs = 34; const latencyMs = 34;
+5 -3
View File
@@ -1,12 +1,14 @@
import { Pattern, ClockCollator } from '@strudel/core'; import { Pattern, ClockCollator } from '@strudel/core';
import { parseControlsFromHap } from 'node_modules/@strudel/osc/osc.mjs'; import { parseControlsFromHap } from '@strudel/osc/osc.mjs';
import { Invoke } from './utils.mjs'; import { Invoke } from './utils.mjs';
import { getAudioContext } from '@strudel/webaudio/index.mjs';
const collator = new ClockCollator({}); const collator = new ClockCollator({});
export async function oscTriggerTauri(hap, currentTime, cps = 1, targetTime) { export async function tauriOscOutput(hap, targetTime, cps = 1) {
const controls = parseControlsFromHap(hap, cps); const controls = parseControlsFromHap(hap, cps);
const params = []; const params = [];
const currentTime = getAudioContext().currentTime;
const timestamp = collator.calculateTimestamp(currentTime, targetTime); const timestamp = collator.calculateTimestamp(currentTime, targetTime);
Object.keys(controls).forEach((key) => { Object.keys(controls).forEach((key) => {
@@ -32,5 +34,5 @@ export async function oscTriggerTauri(hap, currentTime, cps = 1, targetTime) {
}); });
} }
Pattern.prototype.osc = function () { Pattern.prototype.osc = function () {
return this.onTrigger(oscTriggerTauri); return this.onSchedule(tauriOscOutput);
}; };
+1
View File
@@ -23,6 +23,7 @@
}, },
"dependencies": { "dependencies": {
"@strudel/core": "workspace:*", "@strudel/core": "workspace:*",
"@strudel/webaudio": "workspace:*",
"@tauri-apps/api": "^2.2.0" "@tauri-apps/api": "^2.2.0"
}, },
"homepage": "https://codeberg.org/uzu/strudel#readme", "homepage": "https://codeberg.org/uzu/strudel#readme",
+2 -2
View File
@@ -37,7 +37,7 @@ export function initDough() {
return D.ready; return D.ready;
} }
export async function doughTrigger(hap, _currentTime, cps = 1, targetTime) { export async function doughOutput(hap, targetTime, cps = 1) {
const offset = D.context_offset?.[0]; const offset = D.context_offset?.[0];
if (!offset) { if (!offset) {
return; // not ready return; // not ready
@@ -71,5 +71,5 @@ export async function doughTrigger(hap, _currentTime, cps = 1, targetTime) {
export const dough = register('dough', (pat) => { export const dough = register('dough', (pat) => {
initDough(); initDough();
ensureMinimalOutput(); ensureMinimalOutput();
return pat.onTrigger(doughTrigger); return pat.onSchedule(doughOutput);
}); });
+15 -27
View File
@@ -13,7 +13,7 @@ import {
getIsStarted, getIsStarted,
getPattern, getPattern,
getTime, getTime,
getTriggerFunc, getScheduleFunc,
isPattern, isPattern,
logger, logger,
ref, ref,
@@ -21,7 +21,7 @@ import {
} from '@strudel/core'; } from '@strudel/core';
import { noteToMidi, getControlName } from '@strudel/core'; import { noteToMidi, getControlName } from '@strudel/core';
import { Note } from 'webmidi'; import { Note } from 'webmidi';
import { getAudioContext } from '@strudel/webaudio'; import { getAudioContext, getClockBridge } from '@strudel/webaudio';
import { scheduleAtTime, ensureMinimalOutput } from '../superdough/helpers.mjs'; import { scheduleAtTime, ensureMinimalOutput } from '../superdough/helpers.mjs';
import { getMidiDeviceNamesString, getDevice } from './util.mjs'; import { getMidiDeviceNamesString, getDevice } from './util.mjs';
import { MidiInput } from './input.mjs'; import { MidiInput } from './input.mjs';
@@ -177,7 +177,11 @@ const isFirefox = navigator?.userAgent?.includes('Firefox');
// firefox bug: https://bugzilla.mozilla.org/show_bug.cgi?id=2062997 // firefox bug: https://bugzilla.mozilla.org/show_bug.cgi?id=2062997
function timedSend(timeMs, fn) { function timedSend(timeMs, fn) {
if (isFirefox) { if (isFirefox) {
const audioTime = getAudioContext().currentTime + (timeMs - performance.now()) / 1000; const audioTime = getClockBridge().getAudioContextTime(timeMs);
if (!audioTime) {
logger('[midi]: skip event, not ready');
return;
}
scheduleAtTime(() => fn(undefined), audioTime); scheduleAtTime(() => fn(undefined), audioTime);
} else { } else {
fn(timeMs); fn(timeMs);
@@ -281,11 +285,6 @@ function sendNote(note, velocity, duration, device, midichan, timeMs) {
timedSend(timeMs + duration, (timeMs) => device.sendNoteOff(midiNote, { channels: midichan, time: timeMs })); timedSend(timeMs + duration, (timeMs) => device.sendNoteOff(midiNote, { channels: midichan, time: timeMs }));
} }
// thanks freya https://youtu.be/LSNQuFEDOyQ?si=ukZI2IGgWV_NDZzP&t=2979
function expDecay(a, b, decay, dt) {
return b + (a - b) * Math.exp(-decay * dt);
}
/** /**
* MIDI output: Opens a MIDI output port. * MIDI output: Opens a MIDI output port.
* @tags external_io * @tags external_io
@@ -343,26 +342,16 @@ Pattern.prototype.midi = function (midiport, options = {}) {
ensureMinimalOutput(); ensureMinimalOutput();
let p; // filtered clock offset return this.sortHapsByPart().onSchedule((hap, targetTime, cps) => {
let lastTime;
return this.sortHapsByPart().onTrigger((hap, _currentTime, cps, targetTime) => {
if (!WebMidi.enabled) { if (!WebMidi.enabled) {
logger('Midi not enabled'); logger('Midi not enabled');
return; return;
} }
const { contextTime, performanceTime } = getAudioContext().getOutputTimestamp(); const timeMs = getClockBridge().getPerformanceTime(targetTime);
if (!contextTime || !performanceTime) { if (!timeMs) {
logger('[midi] skip midi event: not ready yet?'); logger('[midi] clockbridge not ready');
return; return;
} }
// time conversion from audio context time (targetTime) to performance time (what midi needs)
const offset = performanceTime - contextTime * 1000; // clock offset in ms
const dt = performanceTime - (lastTime ?? performanceTime); // delta time since last midi hap
const decay = 1 / 10000; // how fast offset changes have an effect
p = expDecay(p ?? offset, offset, decay, dt); // smooth clock offset
lastTime = performanceTime;
const timeMs = targetTime * 1000 + p; // this is now correct in performance time
hap.ensureObjectValue(); hap.ensureObjectValue();
@@ -581,8 +570,8 @@ const kListeners = {};
function _triggerKeyboard(input, cps, now, latencyCycles) { function _triggerKeyboard(input, cps, now, latencyCycles) {
const pattern = getPattern(); const pattern = getPattern();
const trigger = getTriggerFunc(); const schedule = getScheduleFunc();
if (!pattern || !trigger) { if (!pattern || !schedule) {
return false; return false;
} }
const t = now + latencyCycles; const t = now + latencyCycles;
@@ -599,8 +588,7 @@ function _triggerKeyboard(input, cps, now, latencyCycles) {
return; return;
} }
const t = ctxNow + (hap.whole.begin - now) / cps; const t = ctxNow + (hap.whole.begin - now) / cps;
const duration = hap.duration / cps; schedule(hap, t, cps);
trigger(hap, t - ctxNow, duration, cps, t);
}); });
return true; return true;
@@ -635,7 +623,7 @@ export async function midikeys(input) {
noteoff ||= noteon && velocity === 0; // handle devices which may use velocity = 0 to signal noteoff noteoff ||= noteon && velocity === 0; // handle devices which may use velocity = 0 to signal noteoff
const key = `${input}_${note}`; const key = `${input}_${note}`;
const cps = getCps() ?? 0.5; const cps = getCps() ?? 0.5;
const triggerAvailable = !!(getPattern() && getTriggerFunc()); const triggerAvailable = !!(getPattern() && getScheduleFunc());
const latencySeconds = triggerAvailable ? 0.01 : 0.06; // avoid missing notes due to cyclist / trigger latency const latencySeconds = triggerAvailable ? 0.01 : 0.06; // avoid missing notes due to cyclist / trigger latency
const now = getTime(); const now = getTime();
const t = now + latencySeconds * cps; const t = now + latencySeconds * cps;
+50
View File
@@ -84,6 +84,22 @@ describe('mini', () => {
it('supports patterning euclidean rhythms', () => { it('supports patterning euclidean rhythms', () => {
expect(minS('[a(<3 5>, <8 16>)]*2')).toEqual(minS('a(3,8) a(5,16)')); expect(minS('[a(<3 5>, <8 16>)]*2')).toEqual(minS('a(3,8) a(5,16)'));
}); });
it('supports inverted euclidean rhythms', () => {
expect(minS('a(-3, 8)')).toEqual(['a: 1/8 - 1/4', 'a: 1/4 - 3/8', 'a: 1/2 - 5/8', 'a: 5/8 - 3/4', 'a: 7/8 - 1']);
});
it('supports euclidean legato rhythms', () => {
expect(minS('a(3, -8)')).toEqual(['a: 0 - 3/8', 'a: 3/8 - 3/4', 'a: 3/4 - 1']);
});
it('supports inverted euclidean legato rhythms', () => {
expect(minS('a(-3, -8)')).toEqual([
'a: -1/8 - 1/8',
'a: 1/8 - 1/4',
'a: 1/4 - 1/2',
'a: 1/2 - 5/8',
'a: 5/8 - 7/8',
'a: 7/8 - 9/8',
]);
});
it("reproduces Toussaint's example euclidean algorithms", () => { it("reproduces Toussaint's example euclidean algorithms", () => {
const checkEuclid = function (spec, target) { const checkEuclid = function (spec, target) {
expect(minS(`x(${spec[0]},${spec[1]})`)).toEqual(minS(target)); expect(minS(`x(${spec[0]},${spec[1]})`)).toEqual(minS(target));
@@ -118,6 +134,40 @@ describe('mini', () => {
checkEuclid([11, 24], 'x ~ ~ x ~ x ~ x ~ x ~ x ~ ~ x ~ x ~ x ~ x ~ x ~'); checkEuclid([11, 24], 'x ~ ~ x ~ x ~ x ~ x ~ x ~ ~ x ~ x ~ x ~ x ~ x ~');
checkEuclid([13, 24], 'x ~ x x ~ x ~ x ~ x ~ x ~ x x ~ x ~ x ~ x ~ x ~'); checkEuclid([13, 24], 'x ~ x x ~ x ~ x ~ x ~ x ~ x x ~ x ~ x ~ x ~ x ~');
}); });
it("reproduces Toussaint's example euclidean legato algorithms", () => {
const checkEuclid = function (spec, target) {
expect(minS(`x(${spec[0]},${spec[1]})`)).toEqual(minS(target));
};
checkEuclid([1, -2], 'x _');
checkEuclid([1, -3], 'x _ _');
checkEuclid([1, -4], 'x _ _ _');
checkEuclid([4, -12], 'x _ _ x _ _ x _ _ x _ _');
checkEuclid([2, -5], 'x _ x _ _');
// checkEuclid([3, 4], "x _ x x"); // Toussaint is wrong..
checkEuclid([3, -4], 'x x x _'); // correction
checkEuclid([3, -5], 'x _ x _ x');
checkEuclid([3, -7], 'x _ x _ x _ _');
checkEuclid([3, -8], 'x _ _ x _ _ x _');
checkEuclid([4, -7], 'x _ x _ x _ x');
checkEuclid([4, -9], 'x _ x _ x _ x _ _');
checkEuclid([4, -11], 'x _ _ x _ _ x _ _ x _');
// checkEuclid([5, 6], "x _ x x x x"); // Toussaint is wrong..
checkEuclid([5, -6], 'x x x x x _'); // correction
checkEuclid([5, -7], 'x _ x x _ x x');
checkEuclid([5, -8], 'x _ x x _ x x _');
checkEuclid([5, -9], 'x _ x _ x _ x _ x');
checkEuclid([5, -11], 'x _ x _ x _ x _ x _ _');
checkEuclid([5, -12], 'x _ _ x _ x _ _ x _ x _');
// checkEuclid([5, 16], "x _ _ x _ _ x _ _ x _ _ x _ _ _ _"); // Toussaint is wrong..
checkEuclid([5, -16], 'x _ _ x _ _ x _ _ x _ _ x _ _ _'); // correction
// checkEuclid([7, 8], "x _ x x x x x x"); // Toussaint is wrong..
checkEuclid([7, -8], 'x x x x x x x _'); // Correction
checkEuclid([7, -12], 'x _ x x _ x _ x x _ x _');
checkEuclid([7, -16], 'x _ _ x _ x _ x _ _ x _ x _ x _');
checkEuclid([9, -16], 'x _ x x _ x _ x _ x x _ x _ x _');
checkEuclid([11, -24], 'x _ _ x _ x _ x _ x _ x _ _ x _ x _ x _ x _ x _');
checkEuclid([13, -24], 'x _ x x _ x _ x _ x _ x _ x x _ x _ x _ x _ x _');
});
it('supports the - alias for ~', () => { it('supports the - alias for ~', () => {
expect(minS('a - b [- c]')).toEqual(minS('a ~ b [~ c]')); expect(minS('a - b [- c]')).toEqual(minS('a ~ b [~ c]'));
}); });
-1
View File
@@ -46,7 +46,6 @@ lib[':'] = tail;
lib['..'] = range; lib['..'] = range;
lib['def'] = () => silence; lib['def'] = () => silence;
lib['or'] = (...children) => chooseIn(...children); // always has structure but is cyclewise.. e.g. "s oh*8.dec[.04 | .5]" lib['or'] = (...children) => chooseIn(...children); // always has structure but is cyclewise.. e.g. "s oh*8.dec[.04 | .5]"
//lib['or'] = (...children) => chooseOut(...children); // "s oh*8.dec[.04 | .5]" is better but "dec[.04 | .5].s oh*8" has no struct
function evaluator(node, scope) { function evaluator(node, scope) {
const { type } = node; const { type } = node;
+29 -32
View File
@@ -4,7 +4,7 @@ Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Pattern, isPattern, createParams } from '@strudel/core'; import { Pattern, getClockTime, createParams } from '@strudel/core';
import Paho from 'paho-mqtt'; import Paho from 'paho-mqtt';
const connections = {}; const connections = {};
@@ -80,42 +80,39 @@ Pattern.prototype.mqtt = function (
} }
cx.connect(props); cx.connect(props);
} }
return this.withHap((hap) => { return this.onSchedule((hap, targetTime, cps) => {
const onTrigger = (hap, currentTime, cps, targetTime) => { let msg_topic = topic;
let msg_topic = topic; if (!cx || !cx.isConnected()) {
if (!cx || !cx.isConnected()) { return;
return; }
} let message = '';
let message = ''; if (typeof hap.value === 'object') {
if (typeof hap.value === 'object') { let value = hap.value;
let value = hap.value;
// Try to take topic from pattern if it's not set // Try to take topic from pattern if it's not set
if (typeof msg_topic === 'undefined' && 'topic' in value) { if (typeof msg_topic === 'undefined' && 'topic' in value) {
msg_topic = value.topic; msg_topic = value.topic;
if (Array.isArray(msg_topic)) { if (Array.isArray(msg_topic)) {
msg_topic = msg_topic.join('/'); msg_topic = msg_topic.join('/');
}
msg_topic = '/' + msg_topic;
} }
if (add_meta) { msg_topic = '/' + msg_topic;
const duration = hap.duration.div(cps);
value = { ...value, duration: duration.valueOf(), cps: cps };
}
message = JSON.stringify(value);
} else {
message = hap.value;
} }
message = new Paho.Message(message); if (add_meta) {
message.destinationName = msg_topic; const duration = hap.duration.div(cps);
value = { ...value, duration: duration.valueOf(), cps: cps };
}
message = JSON.stringify(value);
} else {
message = hap.value;
}
message = new Paho.Message(message);
message.destinationName = msg_topic;
const offset = (targetTime - currentTime + latency) * 1000; const offset = (targetTime - getClockTime() + latency) * 1000;
window.setTimeout(function () { window.setTimeout(function () {
cx.send(message); cx.send(message);
}, offset); }, offset);
};
return hap.setContext({ ...hap.context, onTrigger, dominantTrigger: true });
}); });
}; };
+15 -6
View File
@@ -4,7 +4,8 @@ Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { logger, parseNumeral, register, isNote, noteToMidi, ClockCollator } from '@strudel/core'; import { logger, parseNumeral, register, isNote, noteToMidi } from '@strudel/core';
import { ensureMinimalOutput, getClockBridge } from '@strudel/webaudio';
let connection; // Promise<OSC> let connection; // Promise<OSC>
function connect() { function connect() {
@@ -54,13 +55,18 @@ export function parseControlsFromHap(hap, cps) {
return controls; return controls;
} }
const collator = new ClockCollator({}); export async function oscOutput(hap, targetTime, cps = 1) {
export async function oscTrigger(hap, currentTime, cps = 1, targetTime) {
const ws = await connect(); const ws = await connect();
const timeMs = getClockBridge().getPerformanceTime(targetTime);
if (!timeMs) {
logger('[osc] clockbridge not ready');
return;
}
const ts = performance.timeOrigin + timeMs;
const controls = parseControlsFromHap(hap, cps); const controls = parseControlsFromHap(hap, cps);
const keyvals = Object.entries(controls).flat(); const keyvals = Object.entries(controls).flat();
const ts = collator.calculateTimestamp(currentTime, targetTime) * 1000;
const msg = { address: '/dirt/play', args: keyvals, timestamp: ts }; const msg = { address: '/dirt/play', args: keyvals, timestamp: ts };
if ('oschost' in hap.value) { if ('oschost' in hap.value) {
@@ -82,4 +88,7 @@ export async function oscTrigger(hap, currentTime, cps = 1, targetTime) {
* @memberof Pattern * @memberof Pattern
* @returns Pattern * @returns Pattern
*/ */
export const osc = register('osc', (pat) => pat.onTrigger(oscTrigger)); export const osc = register('osc', (pat) => {
ensureMinimalOutput();
return pat.onSchedule(oscOutput);
});
+1
View File
@@ -38,6 +38,7 @@
"homepage": "https://codeberg.org/uzu/strudel#readme", "homepage": "https://codeberg.org/uzu/strudel#readme",
"dependencies": { "dependencies": {
"@strudel/core": "workspace:*", "@strudel/core": "workspace:*",
"@strudel/webaudio": "workspace:*",
"osc": "^2.4.5", "osc": "^2.4.5",
"ws": "^8.18.3" "ws": "^8.18.3"
}, },
+4 -7
View File
@@ -1,10 +1,7 @@
/* import { oscTriggerTauri } from '../desktopbridge/oscbridge.mjs'; /* import { tauriOscOutput } from '../desktopbridge/oscbridge.mjs';
import { isTauri } from '../desktopbridge/utils.mjs'; */ import { isTauri } from '../desktopbridge/utils.mjs'; */
import { oscTrigger } from './osc.mjs'; import { oscOutput } from './osc.mjs';
const trigger = /* isTauri() ? oscTriggerTauri : */ oscTrigger; const output = /* isTauri() ? tauriOscOutput : */ oscOutput;
export const superdirtOutput = (hap, deadline, hapDuration, cps, targetTime) => { export const superdirtOutput = (hap, targetTime, cps) => output(hap, targetTime, cps);
const currentTime = performance.now() / 1000;
return trigger(hap, currentTime, cps, targetTime);
};
+1 -1
View File
@@ -37,7 +37,7 @@ if (typeof HTMLElement !== 'undefined') {
const drawContext = getDrawContext(); const drawContext = getDrawContext();
const drawTime = [-2, 2]; const drawTime = [-2, 2];
this.editor = new StrudelMirror({ this.editor = new StrudelMirror({
defaultOutput: webaudioOutput, onSchedule: webaudioOutput,
getTime: () => getAudioContext().currentTime, getTime: () => getAudioContext().currentTime,
transpiler, transpiler,
root: container, root: container,
+40 -43
View File
@@ -4,7 +4,7 @@ Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Pattern, isPattern } from '@strudel/core'; import { Pattern, getClockTime } from '@strudel/core';
var writeMessagers = {}; var writeMessagers = {};
var choosing = false; var choosing = false;
@@ -64,53 +64,50 @@ function crc16(data) {
} }
Pattern.prototype.serial = function (br = 115200, sendcrc = false, singlecharids = false, name = 'default') { Pattern.prototype.serial = function (br = 115200, sendcrc = false, singlecharids = false, name = 'default') {
return this.withHap((hap) => { if (!(name in writeMessagers)) {
if (!(name in writeMessagers)) { getWriter(name, br);
getWriter(name, br); }
} return this.onSchedule((hap, targetTime) => {
const onTrigger = (hap, currentTime, _cps, targetTime) => { var message = '';
var message = ''; var chk = 0;
var chk = 0; if (typeof hap.value === 'object') {
if (typeof hap.value === 'object') { if ('action' in hap.value) {
if ('action' in hap.value) { var action = hap.value['action'];
var action = hap.value['action']; if (singlecharids) {
action = action.charAt(0);
}
message += action + '(';
var first = true;
for (var [key, val] of Object.entries(hap.value)) {
if (key === 'action') {
continue;
}
if (first) {
first = false;
} else {
message += ',';
}
if (singlecharids) { if (singlecharids) {
action = action.charAt(0); key = key.charAt(0);
}
message += action + '(';
var first = true;
for (var [key, val] of Object.entries(hap.value)) {
if (key === 'action') {
continue;
}
if (first) {
first = false;
} else {
message += ',';
}
if (singlecharids) {
key = key.charAt(0);
}
message += key + ':' + val;
}
message += ')';
if (sendcrc) {
chk = crc16(message);
}
} else {
for (const [key, val] of Object.entries(hap.value)) {
message += `${key}:${val}`;
} }
message += key + ':' + val;
}
message += ')';
if (sendcrc) {
chk = crc16(message);
} }
} else { } else {
message = hap.value; for (const [key, val] of Object.entries(hap.value)) {
message += `${key}:${val}`;
}
} }
const offset = (targetTime - currentTime + latency) * 1000; } else {
message = hap.value;
}
const offset = (targetTime - getClockTime() + latency) * 1000;
window.setTimeout(function () { window.setTimeout(function () {
writeMessagers[name](message, chk); writeMessagers[name](message, chk);
}, offset); }, offset);
};
return hap.setContext({ ...hap.context, onTrigger, dominantTrigger: true });
}); });
}; };
+2 -3
View File
@@ -479,7 +479,7 @@ export default {
gm_electric_bass_finger: [ gm_electric_bass_finger: [
// Electric Bass (finger): Bass // Electric Bass (finger): Bass
'0330_JCLive_sf2_file', '0330_JCLive_sf2_file',
'0330_FluidR3_GM_sf2_fible', '0330_FluidR3_GM_sf2_file',
'0330_Aspirin_sf2_file', '0330_Aspirin_sf2_file',
//0330_Chaos_sf2_file // same as last //0330_Chaos_sf2_file // same as last
'0330_GeneralUserGS_sf2_file', '0330_GeneralUserGS_sf2_file',
@@ -527,7 +527,7 @@ export default {
'0370_Aspirin_sf2_file', '0370_Aspirin_sf2_file',
// 0370_Chaos_sf2_file // same as last // 0370_Chaos_sf2_file // same as last
'0370_FluidR3_GM_sf2_file', '0370_FluidR3_GM_sf2_file',
'0370_GeneralUserGS_sf2_fil e', '0370_GeneralUserGS_sf2_file',
'0370_JCLive_sf2_file', '0370_JCLive_sf2_file',
//0370_SBLive_sf2 //0370_SBLive_sf2
//0370_SoundBlasterOld_sf2 //0370_SoundBlasterOld_sf2
@@ -1782,6 +1782,5 @@ export default {
'1272_GeneralUserGS_sf2_file', '1272_GeneralUserGS_sf2_file',
'1273_GeneralUserGS_sf2_file', '1273_GeneralUserGS_sf2_file',
'1274_GeneralUserGS_sf2_file', '1274_GeneralUserGS_sf2_file',
'',
], ],
}; };
+1 -1
View File
@@ -3,7 +3,7 @@ import { getAudioContext, registerSound } from '@strudel/webaudio';
import { loadSoundfont as _loadSoundfont, startPresetNote } from 'sfumato'; import { loadSoundfont as _loadSoundfont, startPresetNote } from 'sfumato';
Pattern.prototype.soundfont = function (sf, n = 0) { Pattern.prototype.soundfont = function (sf, n = 0) {
return this.onTrigger((h, ct, cps, targetTime) => { return this.onSchedule((h, targetTime) => {
const ctx = getAudioContext(); const ctx = getAudioContext();
const note = getPlayableNoteValue(h); const note = getPlayableNoteValue(h);
const preset = sf.presets[n % sf.presets.length]; const preset = sf.presets[n % sf.presets.length];
+10 -1
View File
@@ -8,8 +8,9 @@ This program is free software: you can redistribute it and/or modify it under th
*/ */
import { clearNodePool } from './nodePools.mjs'; import { clearNodePool } from './nodePools.mjs';
import { ClockBridge } from './clockbridge.mjs';
let audioContext; let audioContext, clockBridge;
export const setDefaultAudioContext = () => { export const setDefaultAudioContext = () => {
return setAudioContext(new AudioContext()); return setAudioContext(new AudioContext());
@@ -23,6 +24,7 @@ export const setAudioContext = (context) => {
audioContext.close(); audioContext.close();
} }
audioContext = context; audioContext = context;
clockBridge = new ClockBridge(audioContext);
return audioContext; return audioContext;
}; };
@@ -34,6 +36,13 @@ export const getAudioContext = () => {
return audioContext; return audioContext;
}; };
export function getClockBridge() {
if (!clockBridge) {
getAudioContext(); // creates clockBridge
}
return clockBridge;
}
export function getAudioContextCurrentTime() { export function getAudioContextCurrentTime() {
return getAudioContext().currentTime; return getAudioContext().currentTime;
} }
+41
View File
@@ -0,0 +1,41 @@
/*
clockbridge.mjs
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/superdough/index.mjs>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// thanks freya https://youtu.be/LSNQuFEDOyQ?si=ukZI2IGgWV_NDZzP&t=2979
export function expDecay(a, b, decay, dt) {
return b + (a - b) * Math.exp(-decay * dt);
}
// translates between audio and performance clock
export class ClockBridge {
p; // smoothed clock offset
lastTime;
audioContext;
constructor(audioContext) {
this.audioContext = audioContext;
}
// delta between audio and performance time in ms
getOffset() {
const { contextTime, performanceTime } = this.audioContext.getOutputTimestamp();
if (!contextTime || !performanceTime) {
return;
}
const offset = performanceTime - contextTime * 1000; // clock offset in ms
const dt = performanceTime - (this.lastTime ?? performanceTime); // delta time since last hap
const decay = 1 / 10000; // how fast offset changes have an effect
this.p = expDecay(this.p ?? offset, offset, decay, dt); // smooth clock offset
this.lastTime = performanceTime;
return this.p;
}
getPerformanceTime(audioContextTime) {
const offset = this.getOffset();
return audioContextTime * 1000 + offset; // this is now correct in performance time (ms)
}
getAudioContextTime(performanceTime) {
const offset = this.getOffset();
return (performanceTime - offset) / 1000; // this is now correct in audio context time (seconds)
}
}
+3 -2
View File
@@ -74,6 +74,7 @@ export const rawdsp = async (code) => {
worklet.node.connect(ac.destination); worklet.node.connect(ac.destination);
}; };
export function rawdspTrigger(hap, currentTime, cps, targetTime) { export function rawdspOutput(hap, time, cps) {
window.postMessage({ time: targetTime, dough: hap.value, currentTime, duration: hap.duration, cps }); const currentTime = getAudioContext().currentTime;
window.postMessage({ time, dough: hap.value, currentTime, duration: hap.duration, cps });
} }
+1
View File
@@ -14,3 +14,4 @@ export * from './modulators.mjs';
export * from './dspworklet.mjs'; export * from './dspworklet.mjs';
export * from './audioContext.mjs'; export * from './audioContext.mjs';
export * from './wavetable.mjs'; export * from './wavetable.mjs';
export * from './clockbridge.mjs';
+3 -2
View File
@@ -36,8 +36,9 @@ function humanFileSize(bytes, si) {
* *
* Note: `must` be called with await, otherwise you'll get a pending Promise object. * Note: `must` be called with await, otherwise you'll get a pending Promise object.
* *
* @name getDuration,getDur * @name getDuration
* @tag samples * @synonym getDur
* @tags samples
* @param {string} sampleName * @param {string} sampleName
* @param {number} (optional) n * @param {number} (optional) n
* *
-4
View File
@@ -1048,7 +1048,3 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
} }
}); });
}; };
export const superdoughTrigger = (t, hap, ct, cps) => {
superdough(hap, t - ct, hap.duration / cps, cps);
};
+2 -1
View File
@@ -6,7 +6,7 @@ export * from '@strudel/mini';
export * from '@strudel/edo'; export * from '@strudel/edo';
export * from '@strudel/tonal'; export * from '@strudel/tonal';
export * from '@strudel/webaudio'; export * from '@strudel/webaudio';
import { Pattern, evalScope, setTime } from '@strudel/core'; import { Pattern, evalScope, setTime, setClockTime } from '@strudel/core';
import { initAudioOnFirstClick, registerSynthSounds, webaudioRepl } from '@strudel/webaudio'; import { initAudioOnFirstClick, registerSynthSounds, webaudioRepl } from '@strudel/webaudio';
// import { registerSoundfonts } from '@strudel/soundfonts'; // import { registerSoundfonts } from '@strudel/soundfonts';
import { evaluate as _evaluate, transpiler } from '@strudel/transpiler'; import { evaluate as _evaluate, transpiler } from '@strudel/transpiler';
@@ -41,6 +41,7 @@ export function initStrudel(options = {}) {
return repl; return repl;
})(); })();
setTime(() => repl.scheduler.now()); setTime(() => repl.scheduler.now());
setClockTime(() => repl.scheduler.getTime()); // TODO: refactor?
return initDone; return initDone;
} }
+1 -1
View File
@@ -19,7 +19,7 @@ initAudioOnFirstClick();
const ctx = getAudioContext(); const ctx = getAudioContext();
const { scheduler } = repl({ const { scheduler } = repl({
defaultOutput: webaudioOutput, onSchedule: webaudioOutput,
getTime: () => ctx.currentTime getTime: () => ctx.currentTime
}); });
+1 -1
View File
@@ -20,7 +20,7 @@ const soundMap = new Map();
const loadedSounds = new Map(); const loadedSounds = new Map();
Pattern.prototype.supradough = function () { Pattern.prototype.supradough = function () {
return this.onTrigger((hap, __, cps, begin) => { return this.onSchedule((hap, begin, cps) => {
hap.value._begin = begin; hap.value._begin = begin;
hap.value._duration = hap.duration / cps; hap.value._duration = hap.duration / cps;
!doughWorklet && initDoughWorklet(); !doughWorklet && initDoughWorklet();
+5 -5
View File
@@ -9,7 +9,7 @@ import {
superdough, superdough,
getAudioContext, getAudioContext,
setLogger, setLogger,
rawdspTrigger, rawdspOutput,
registerWorklet, registerWorklet,
setAudioContext, setAudioContext,
initAudio, initAudio,
@@ -32,8 +32,8 @@ const hap2value = (hap) => {
}; };
// uses more precise, absolute t if available, see https://github.com/tidalcycles/strudel/pull/1004 // uses more precise, absolute t if available, see https://github.com/tidalcycles/strudel/pull/1004
// TODO: refactor output callbacks to eliminate deadline export const webaudioOutput = (hap, t, cps) => {
export const webaudioOutput = (hap, _deadline, hapDuration, cps, t) => { const hapDuration = hap.duration / cps;
return superdough(hap2value(hap), t, hapDuration, cps, hap.whole?.begin.valueOf()); return superdough(hap2value(hap), t, hapDuration, cps, hap.whole?.begin.valueOf());
}; };
@@ -167,14 +167,14 @@ export function webaudioRepl(options = {}) {
setAudioContext(audioContext); setAudioContext(audioContext);
options = { options = {
getTime: () => audioContext.currentTime, getTime: () => audioContext.currentTime,
defaultOutput: webaudioOutput, onSchedule: webaudioOutput,
...options, ...options,
}; };
return repl(options); return repl(options);
} }
Pattern.prototype.rawdsp = function () { Pattern.prototype.rawdsp = function () {
return this.onTrigger(rawdspTrigger, 1); return this.onSchedule(rawdspOutput, 1);
}; };
function audioBufferToWav(buffer, opt) { function audioBufferToWav(buffer, opt) {
+3
View File
@@ -8,11 +8,14 @@ import Tune from './tunejs.js';
import { register } from '@strudel/core'; import { register } from '@strudel/core';
/** /**
* Deprecated.
*
* Assumes pattern contains numerical scale degrees on the `i` control (see examples below). Accepts a scale name or list of frequencies (see all available names at the link on the reference). Returns a new pattern with all values mapped to a frequency ratio. Similar to `xen`. * Assumes pattern contains numerical scale degrees on the `i` control (see examples below). Accepts a scale name or list of frequencies (see all available names at the link on the reference). Returns a new pattern with all values mapped to a frequency ratio. Similar to `xen`.
* @name tune * @name tune
* @returns Pattern * @returns Pattern
* @memberof Pattern * @memberof Pattern
* @param {(string | number[] )} scale * @param {(string | number[] )} scale
* @deprecated
* @example * @example
* i("0 1 2 3 4 5").tune("hexany15").mul("220").freq() * i("0 1 2 3 4 5").tune("hexany15").mul("220").freq()
* @example * @example
+105 -29
View File
@@ -4,7 +4,7 @@ Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { register, _mod, parseNumeral, removeUndefineds } from '@strudel/core'; import { register, _mod, parseNumeral, removeUndefineds, Pattern, parray, State } from '@strudel/core';
import Tune from './tunejs.js'; import Tune from './tunejs.js';
// returns a list of frequency ratios for given edo scale // returns a list of frequency ratios for given edo scale
@@ -61,14 +61,8 @@ function xenOffset(xenScale, offset, index = 0) {
const trimFreq = (freq) => parseFloat(freq.toPrecision(10)); const trimFreq = (freq) => parseFloat(freq.toPrecision(10));
// accepts a scale name such as 31edo, and a pattern
// pattern expected to follow format such that a value can be mapped
// to an edostep within the scale. Returns the pattern with
// values mapped to the frequencies associated with the given edosteps
// scaleNameOrRatios: string || number[], steps?: number
/** /**
* Assumes a numerical pattern of scale steps, and a scale. Scales accepted are all preset scale names of `tune`, arbitrary edos such as 31edo, or an array of frequency ratios. Assumes scales repeat at octave (2/1). Returns a new pattern with all values mapped to their associated frequency, assuming a base frequency of 220hz. * Assumes a numerical pattern of scale steps, and a scale. Scales accepted are all preset scale names of `tune`, arbitrary edos such as 31edo, or an array of frequency ratios. Assumes scales repeat at octave (2/1). Returns a new pattern with all values mapped to their associated frequency, assuming a base frequency of 220hz. See first 4 examples for the different ways you can input a scale into `xen`
* *
* @name xen * @name xen
* @returns Pattern * @returns Pattern
@@ -87,6 +81,15 @@ const trimFreq = (freq) => parseFloat(freq.toPrecision(10));
* Math.pow(2, 18/31), * Math.pow(2, 18/31),
* ]).piano() * ]).piano()
* @example * @example
* // However, with the above you won't get highlighting of the scale indices,
* // instead you could do something like:
* i("[<2 ->, 0 1 <- 2> 3]*2").xen([
* "1:1", // tonic
* "5:4", // Major 3rd
* "[3:2 4:3]", // Perfect 5th, 4th
* "5:3" // Major 6th
* ]).s("tri")
* @example
* // xen also supports all scale names that * // xen also supports all scale names that
* // tune does: * // tune does:
* i("0 1 2 3 4 5").xen("hexany15") * i("0 1 2 3 4 5").xen("hexany15")
@@ -96,27 +99,100 @@ const trimFreq = (freq) => parseFloat(freq.toPrecision(10));
* i("0 1 2 3 4 5 6 7").xen("<5edo 10edo 15edo hexany15>") * i("0 1 2 3 4 5 6 7").xen("<5edo 10edo 15edo hexany15>")
*/ */
export const xen = register('xen', function (scaleNameOrRatios, pat) { export const xen = register(
return pat.withHaps((haps) => { 'xen',
haps = haps.map((hap) => { function (scaleNameOrRatios, pat) {
let hVal = hap.value; const JIsc2 = (input) => {
const isObject = typeof hVal === 'object'; let k = parray(input);
if (!isObject) {
throw new Error(`Expected hap to have control 'i' set, but received ${hap.value.i}, try wrapping input in i()`); return new Pattern((state) => {
} let res = k.withValue((x) => x.map((y) => eval(y.join('/')))).query(state);
const { i, ...otherValues } = hVal; return res;
const scale = getXenScale(scaleNameOrRatios); });
let freq = xenOffset(scale, parseNumeral(hVal.i)); };
// 10 is somewhat arbitrary
freq = trimFreq(freq); const query = (state) => {
hap.value = { ...otherValues, freq }; let haps = pat.query(state);
return isEdo(scaleNameOrRatios) let resHaps = [];
? hap.setContext({ ...hap.context, edoSize: scaleNameOrRatios.match(/^([1-9]+[0-9]*)edo$/)[1] })
: hap; haps.forEach((hap) => {
}); let hVal = hap.value;
return removeUndefineds(haps); const isObject = typeof hVal === 'object';
}); if (!isObject) {
}); throw new Error(
`Expected hap to have control 'i' set, but received ${hap.value.i}, try wrapping input in i()`,
);
}
const { i, ...otherValues } = hVal;
let edo = false;
let st = new State(hap.wholeOrPart().intersection(state.span), state.controls);
// Format: An array of values
// Each value is a pattern, representing an index on the scale
let sc = scaleNameOrRatios.query(st);
// Check to see if input is attempting to be a named scale or a raw scale
let isStr = typeof sc[0].value == 'string';
let isRawScale = Array.isArray(sc[0].value) && sc[0].value.every((x) => typeof x == 'number');
if (!isStr && !isRawScale) {
sc = sc[0].value;
} else if (isRawScale) {
sc = getXenScale(sc[0].value);
}
if (!isStr && !isRawScale) {
let scaleByIndices = [];
for (let j = 0; j < sc.length; j++) {
scaleByIndices[j] = sc[j].query(st);
}
/*
Format: An array of 1 or more Haps, each Hap has an array of values of
one or more scale index definitions (contains whole scale)
Assumption: inner value array should be same length between each
*/
let sc2 = JIsc2(sc).query(st);
let index = _mod(i, sc.length);
for (let j = 0; j < scaleByIndices[index].length; j++) {
let scale = getXenScale(sc2[j].value);
let freq = xenOffset(scale, parseNumeral(i));
let newHap = hap;
freq = trimFreq(freq);
newHap.value = { ...otherValues, freq };
newHap = newHap.setContext(newHap.combineContext(scaleByIndices[index][j]));
resHaps.push(newHap);
}
} else if (isRawScale) {
let freq = xenOffset(sc, parseNumeral(i));
let newHap = hap;
freq = trimFreq(freq);
newHap.value = { ...otherValues, freq };
resHaps.push(newHap);
} else if (isStr) {
// scale is named scale(s)
for (let j = 0; j < sc.length; j++) {
let scaleName = sc[j].value;
let scale = getXenScale(scaleName);
let freq = xenOffset(scale, parseNumeral(i));
let newHap = hap;
freq = trimFreq(freq);
newHap.value = { ...otherValues, freq };
newHap = newHap.setContext(newHap.combineContext(sc[j]));
if (isEdo(scaleName)) {
edo = scaleName.match(/^([1-9]+[0-9]*)edo$/)[1];
newHap = newHap.setContext({ ...newHap.context, edoSize: edo });
}
resHaps.push(newHap);
}
} else {
throw new Error('failed to parse scale');
}
});
return resHaps;
};
return new Pattern(query);
},
false,
);
/** /**
* Assumes pattern of frequencies tuned to some `base` frequency, such as the output of `xen` * Assumes pattern of frequencies tuned to some `base` frequency, such as the output of `xen`
+6
View File
@@ -299,6 +299,9 @@ importers:
'@strudel/core': '@strudel/core':
specifier: workspace:* specifier: workspace:*
version: link:../core version: link:../core
'@strudel/webaudio':
specifier: workspace:*
version: link:../webaudio
'@tauri-apps/api': '@tauri-apps/api':
specifier: ^2.2.0 specifier: ^2.2.0
version: 2.2.0 version: 2.2.0
@@ -486,6 +489,9 @@ importers:
'@strudel/core': '@strudel/core':
specifier: workspace:* specifier: workspace:*
version: link:../core version: link:../core
'@strudel/webaudio':
specifier: workspace:*
version: link:../webaudio
osc: osc:
specifier: ^2.4.5 specifier: ^2.4.5
version: 2.4.5 version: 2.4.5
File diff suppressed because it is too large Load Diff
+12 -12
View File
@@ -7322,12 +7322,12 @@ exports[`renders tunes > tune: randomBells 1`] = `
[ [
"[ -9/8 ⇜ (0/1 → 3/8) | gain:0.6 note:A3 velocity:0.598990320228 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", "[ -9/8 ⇜ (0/1 → 3/8) | gain:0.6 note:A3 velocity:0.598990320228 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]",
"[ -3/4 ⇜ (0/1 → 3/4) | gain:0.6 note:C5 velocity:0.836992966942 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", "[ -3/4 ⇜ (0/1 → 3/4) | gain:0.6 note:C5 velocity:0.836992966942 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]",
"[ 0/1 → 3/2 | note:F2 s:bass clip:1 gain:0.8 ]", "[ 0/1 → 3/2 | note:D2 s:bass clip:1 gain:0.8 ]",
"[ 0/1 → 9/4 | gain:0.6 note:D3 velocity:0.5 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", "[ 0/1 → 9/4 | gain:0.6 note:D3 velocity:0.5 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]",
"[ 3/8 → 21/8 | gain:0.6 note:F5 velocity:0.921303892508 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", "[ 3/8 → 21/8 | gain:0.6 note:F5 velocity:0.921303892508 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]",
"[ 3/4 → 3/1 | gain:0.6 note:C5 velocity:0.842607785016 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", "[ 3/4 → 3/1 | gain:0.6 note:C5 velocity:0.842607785016 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]",
"[ 3/2 → 9/4 | note:F2 s:bass clip:1 gain:0.8 ]", "[ 3/2 → 9/4 | note:D2 s:bass clip:1 gain:0.8 ]",
"[ 9/4 → 3/1 | note:F2 s:bass clip:1 gain:0.8 ]", "[ 9/4 → 3/1 | note:D2 s:bass clip:1 gain:0.8 ]",
"[ 9/4 → 9/2 | gain:0.6 note:D4 velocity:0.700696257874 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", "[ 9/4 → 9/2 | gain:0.6 note:D4 velocity:0.700696257874 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]",
"[ 21/8 → 39/8 | gain:0.6 note:C4 velocity:0.650794394314 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", "[ 21/8 → 39/8 | gain:0.6 note:C4 velocity:0.650794394314 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]",
"[ 3/1 → 9/2 | note:D2 s:bass clip:1 gain:0.8 ]", "[ 3/1 → 9/2 | note:D2 s:bass clip:1 gain:0.8 ]",
@@ -7339,12 +7339,12 @@ exports[`renders tunes > tune: randomBells 1`] = `
"[ (21/4 → 6/1) ⇝ 27/4 | gain:0.6 note:D4 velocity:0.698815540411 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", "[ (21/4 → 6/1) ⇝ 27/4 | gain:0.6 note:D4 velocity:0.698815540411 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]",
"[ 39/8 ⇜ (6/1 → 51/8) | gain:0.6 note:D5 velocity:0.875811336562 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", "[ 39/8 ⇜ (6/1 → 51/8) | gain:0.6 note:D5 velocity:0.875811336562 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]",
"[ 21/4 ⇜ (6/1 → 27/4) | gain:0.6 note:D4 velocity:0.698815540411 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", "[ 21/4 ⇜ (6/1 → 27/4) | gain:0.6 note:D4 velocity:0.698815540411 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]",
"[ 6/1 → 15/2 | note:D2 s:bass clip:1 gain:0.8 ]", "[ 6/1 → 15/2 | note:A2 s:bass clip:1 gain:0.8 ]",
"[ 6/1 → 33/4 | gain:0.6 note:G4 velocity:0.75977108255 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", "[ 6/1 → 33/4 | gain:0.6 note:G4 velocity:0.75977108255 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]",
"[ 51/8 → 69/8 | gain:0.6 note:G4 velocity:0.774316444062 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", "[ 51/8 → 69/8 | gain:0.6 note:G4 velocity:0.774316444062 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]",
"[ 27/4 → 9/1 | gain:0.6 note:C5 velocity:0.836244757287 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", "[ 27/4 → 9/1 | gain:0.6 note:C5 velocity:0.836244757287 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]",
"[ 15/2 → 33/4 | note:D2 s:bass clip:1 gain:0.8 ]", "[ 15/2 → 33/4 | note:A2 s:bass clip:1 gain:0.8 ]",
"[ 33/4 → 9/1 | note:D2 s:bass clip:1 gain:0.8 ]", "[ 33/4 → 9/1 | note:A2 s:bass clip:1 gain:0.8 ]",
"[ 33/4 → 21/2 | gain:0.6 note:A3 velocity:0.59140187595 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", "[ 33/4 → 21/2 | gain:0.6 note:A3 velocity:0.59140187595 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]",
"[ 69/8 → 87/8 | gain:0.6 note:G4 velocity:0.754063542932 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", "[ 69/8 → 87/8 | gain:0.6 note:G4 velocity:0.754063542932 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]",
"[ 9/1 → 21/2 | note:A2 s:bass clip:1 gain:0.8 ]", "[ 9/1 → 21/2 | note:A2 s:bass clip:1 gain:0.8 ]",
@@ -7356,12 +7356,12 @@ exports[`renders tunes > tune: randomBells 1`] = `
"[ (45/4 → 12/1) ⇝ 51/4 | gain:0.6 note:A4 velocity:0.797278589569 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", "[ (45/4 → 12/1) ⇝ 51/4 | gain:0.6 note:A4 velocity:0.797278589569 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]",
"[ 87/8 ⇜ (12/1 → 99/8) | gain:0.6 note:F4 velocity:0.734787144698 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", "[ 87/8 ⇜ (12/1 → 99/8) | gain:0.6 note:F4 velocity:0.734787144698 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]",
"[ 45/4 ⇜ (12/1 → 51/4) | gain:0.6 note:A4 velocity:0.797278589569 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", "[ 45/4 ⇜ (12/1 → 51/4) | gain:0.6 note:A4 velocity:0.797278589569 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]",
"[ 12/1 → 27/2 | note:A2 s:bass clip:1 gain:0.8 ]", "[ 12/1 → 27/2 | note:G2 s:bass clip:1 gain:0.8 ]",
"[ 12/1 → 57/4 | gain:0.6 note:G5 velocity:0.979763559997 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", "[ 12/1 → 57/4 | gain:0.6 note:G5 velocity:0.979763559997 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]",
"[ 99/8 → 117/8 | gain:0.6 note:C4 velocity:0.666239210404 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", "[ 99/8 → 117/8 | gain:0.6 note:C4 velocity:0.666239210404 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]",
"[ 51/4 → 15/1 | gain:0.6 note:F5 velocity:0.95169514697 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", "[ 51/4 → 15/1 | gain:0.6 note:F5 velocity:0.95169514697 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]",
"[ 27/2 → 57/4 | note:A2 s:bass clip:1 gain:0.8 ]", "[ 27/2 → 57/4 | note:G2 s:bass clip:1 gain:0.8 ]",
"[ 57/4 → 15/1 | note:A2 s:bass clip:1 gain:0.8 ]", "[ 57/4 → 15/1 | note:G2 s:bass clip:1 gain:0.8 ]",
"[ 57/4 → 33/2 | gain:0.6 note:F5 velocity:0.918253320269 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", "[ 57/4 → 33/2 | gain:0.6 note:F5 velocity:0.918253320269 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]",
"[ 117/8 → 135/8 | gain:0.6 note:G3 velocity:0.571157105267 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", "[ 117/8 → 135/8 | gain:0.6 note:G3 velocity:0.571157105267 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]",
"[ 15/1 → 33/2 | note:G2 s:bass clip:1 gain:0.8 ]", "[ 15/1 → 33/2 | note:G2 s:bass clip:1 gain:0.8 ]",
@@ -7373,12 +7373,12 @@ exports[`renders tunes > tune: randomBells 1`] = `
"[ (69/4 → 18/1) ⇝ 75/4 | gain:0.6 note:F3 velocity:0.508127085865 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", "[ (69/4 → 18/1) ⇝ 75/4 | gain:0.6 note:F3 velocity:0.508127085865 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]",
"[ 135/8 ⇜ (18/1 → 147/8) | gain:0.6 note:F5 velocity:0.945647025481 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", "[ 135/8 ⇜ (18/1 → 147/8) | gain:0.6 note:F5 velocity:0.945647025481 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]",
"[ 69/4 ⇜ (18/1 → 75/4) | gain:0.6 note:F3 velocity:0.508127085865 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", "[ 69/4 ⇜ (18/1 → 75/4) | gain:0.6 note:F3 velocity:0.508127085865 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]",
"[ 18/1 → 39/2 | note:G2 s:bass clip:1 gain:0.8 ]", "[ 18/1 → 39/2 | note:F2 s:bass clip:1 gain:0.8 ]",
"[ 18/1 → 81/4 | gain:0.6 note:A3 velocity:0.608644555323 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", "[ 18/1 → 81/4 | gain:0.6 note:A3 velocity:0.608644555323 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]",
"[ 147/8 → 165/8 | gain:0.6 note:F3 velocity:0.506259439513 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", "[ 147/8 → 165/8 | gain:0.6 note:F3 velocity:0.506259439513 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]",
"[ 75/4 → 21/1 | gain:0.6 note:D4 velocity:0.671621939167 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", "[ 75/4 → 21/1 | gain:0.6 note:D4 velocity:0.671621939167 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]",
"[ 39/2 → 81/4 | note:G2 s:bass clip:1 gain:0.8 ]", "[ 39/2 → 81/4 | note:F2 s:bass clip:1 gain:0.8 ]",
"[ 81/4 → 21/1 | note:G2 s:bass clip:1 gain:0.8 ]", "[ 81/4 → 21/1 | note:F2 s:bass clip:1 gain:0.8 ]",
"[ 81/4 → 45/2 | gain:0.6 note:D4 velocity:0.704345900565 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", "[ 81/4 → 45/2 | gain:0.6 note:D4 velocity:0.704345900565 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]",
"[ 165/8 → 183/8 | gain:0.6 note:D5 velocity:0.887838857248 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]", "[ 165/8 → 183/8 | gain:0.6 note:D5 velocity:0.887838857248 s:bell delay:0.2 delaytime:0.333333333333 delayfeedback:0.8 ]",
"[ 21/1 → 45/2 | note:F2 s:bass clip:1 gain:0.8 ]", "[ 21/1 → 45/2 | note:F2 s:bass clip:1 gain:0.8 ]",
+4 -4
View File
@@ -22,7 +22,7 @@ export function MiniRepl({
tunes, tunes,
hideHeader = false, hideHeader = false,
canvasHeight = 100, canvasHeight = 100,
onTrigger, onSchedule,
punchcard, punchcard,
punchcardLabels = true, punchcardLabels = true,
claviature, claviature,
@@ -48,7 +48,7 @@ export function MiniRepl({
const editor = new StrudelMirror({ const editor = new StrudelMirror({
id, id,
defaultOutput: webaudioOutput, onSchedule: webaudioOutput,
getTime: () => getAudioContext().currentTime, getTime: () => getAudioContext().currentTime,
transpiler, transpiler,
autodraw, autodraw,
@@ -58,8 +58,8 @@ export function MiniRepl({
drawTime, drawTime,
drawContext, drawContext,
editPattern: (pat, id) => { editPattern: (pat, id) => {
if (onTrigger) { if (onSchedule) {
pat = pat.onTrigger(onTrigger, false); pat = pat.onSchedule(onSchedule, false);
} }
if (claviature) { if (claviature) {
pat = pat.onPaint((ctx, time, haps, drawTime) => { pat = pat.onPaint((ctx, time, haps, drawTime) => {
+1 -1
View File
@@ -36,7 +36,7 @@ const githubEditUrl = `${CONFIG.GITHUB_EDIT_URL}/${currentFile}`;
<Header currentPage={currentPage} /> <Header currentPage={currentPage} />
</header> </header>
<main class="relative pt-16"> <main class="relative pt-16">
<div class="h-full top-0 overflow-auto min-w-[300px] flex xl:justify-center pr-4 pl-4 md:pl-[300px] xl:pl-0"> <div class="h-full top-0 overflow-auto min-w-[300px] flex xl:justify-center pr-4 pl-4 md:pl-[300px] xl:pr-[300px]">
<aside title="Site Navigation" class="w-[300px] px-6 left-0 hidden md:block fixed h-full"> <aside title="Site Navigation" class="w-[300px] px-6 left-0 hidden md:block fixed h-full">
<LeftSidebar currentPage={currentPage} /> <LeftSidebar currentPage={currentPage} />
</aside> </aside>
+1 -1
View File
@@ -33,7 +33,7 @@ The signal chain in Strudel is as follows:
- This is where the main (volume) ADSR happens - This is where the main (volume) ADSR happens
- A lowpass filter (`lpf`) - A lowpass filter (`lpf`)
- A highpass filter (`hpf`) - A highpass filter (`hpf`)
- A bandpass filter (`bandpass`) - A bandpass filter (`bpf`)
- A vowel filter (`vowel`) - A vowel filter (`vowel`)
- Sample rate reduction (`coarse`) - Sample rate reduction (`coarse`)
- Bit crushing (`crush`) - Bit crushing (`crush`)
+8
View File
@@ -346,3 +346,11 @@ s increase by one semitone, i.e. sharp, works for steps of scales, note names
$: at the start of a line, defines a member of the stack. is the only stack name that should occur multiple names $: at the start of a line, defines a member of the stack. is the only stack name that should occur multiple names
_ before a stack name: mutes the stack, i.e. hush(), for example _$: s("bd"), see above for a different usage. _ before a stack name: mutes the stack, i.e. hush(), for example _$: s("bd"), see above for a different usage.
``` ```
## What is a 'hap'?
A 'hap' is a name for a thing that happens, e.g. `note("c3 -")` playing a note and then stopping after a while. Haps have a start and an end. Every separate sound that is produced by strudel is a hap on its own.
We would have called it 'event' if this particular name wasn't used by JavaScript to describe things that happen on html pages, like clicking on a button or hovering over a line of text.
Since strudel is written in JavaScript, this confusion of having the same name for different things is avoided by calling events 'haps'.
+8
View File
@@ -55,6 +55,14 @@ There is also `saw2`, `sine2`, `cosine2`, `tri2`, `square2` and `rand2` which ha
<JsDoc client:idle name="brandBy" h={0} /> <JsDoc client:idle name="brandBy" h={0} />
## time
<JsDoc client:idle name="time" h={0} />
## creating your own signal
<JsDoc client:idle name="signal" h={0} />
## mouseX ## mouseX
<JsDoc client:idle name="mousex" h={0} /> <JsDoc client:idle name="mousex" h={0} />
+6 -6
View File
@@ -12,6 +12,12 @@ import { JsDoc } from '../../docs/JsDoc';
These functions allow the use of scales other than your typical chromatic 12 based ones. These functions allow the use of scales other than your typical chromatic 12 based ones.
### xen(scaleOrRatios)
{/* TODO add explanation of EDO to documentation */}
<JsDoc client:idle name="Pattern.xen" h={0} />
### tune(scale) ### tune(scale)
{/* TODO (maybe): combine jsdoc things in tune.mjs with here */} {/* TODO (maybe): combine jsdoc things in tune.mjs with here */}
@@ -110,9 +116,3 @@ You can also give tune a list of frequencies to use as the scale:
523.2511306012 523.2511306012
]).mul(220).freq();`} ]).mul(220).freq();`}
/> />
### xen(scaleOrRatios)
{/* TODO add explanation of EDO to documentation */}
<JsDoc client:idle name="Pattern.xen" h={0} />
@@ -186,9 +186,9 @@ These functions are more low level, probably not needed by the live coder.
# Other # Other
## onTrigger ## onSchedule
<JsDoc client:idle name="Pattern#onTrigger" h={0} /> <JsDoc client:idle name="Pattern#onSchedule" h={0} />
## log ## log
@@ -8,7 +8,7 @@ import { JsDoc } from '../../docs/JsDoc';
# Patterns # Patterns
Patterns are the essence of Tidal. Its patterns are abstract entities that represent flows of time as functions, adapting a technique called pure functional reactive programming. Taking a time span as its input, a Pattern can output a set of events that happen within that time span. It depends on the structure of the Pattern how the events are located in time. Patterns are the essence of Uzu languages such as Strudel and Tidal. Its patterns are abstract entities that represent flows of time as functions, adapting a technique called pure functional reactive programming. Taking a time span as its input, a Pattern can output a set of events that happen within that time span. It depends on the structure of the Pattern how the events are located in time.
From now on, this process of generating events from a time span will be called **querying**. From now on, this process of generating events from a time span will be called **querying**.
Example: Example:
+6 -10
View File
@@ -17,7 +17,7 @@ Besides a UI for playback control and meta information, the main part of the REP
1. The user writes and updates code. Each update transpiles and evaluates it to create a `Pattern` instance 1. The user writes and updates code. Each update transpiles and evaluates it to create a `Pattern` instance
2. While the REPL is running, the `Scheduler` queries the active `Pattern` by a regular interval, generating `Events` (also known as `Haps` in Strudel) for the next time span. 2. While the REPL is running, the `Scheduler` queries the active `Pattern` by a regular interval, generating `Events` (also known as `Haps` in Strudel) for the next time span.
3. For each scheduling tick, all generated `Events` are triggered by calling their `onTrigger` method, which is set by the output. 3. For each scheduling tick, all generated `Events` are triggered by calling their `onSchedule` method, which is set by the output.
<img src="https://codeberg.org/uzu/strudel/raw/branch/talk/talk/public/strudelflow.png" width="600" /> <img src="https://codeberg.org/uzu/strudel/raw/branch/talk/talk/public/strudelflow.png" width="600" />
@@ -134,10 +134,7 @@ let minLatency = 0.1; // min time before a hap should trigger
setInterval(() => { setInterval(() => {
const haps = pattern.queryArc(time, time + interval); const haps = pattern.queryArc(time, time + interval);
time += interval; // increment time time += interval; // increment time
haps.forEach((hap) => { haps.forEach((hap) => onSchedule(hap, hap.whole.begin + minLatency));
const deadline = hap.whole.begin - time + minLatency;
onTrigger(hap, deadline, duration);
});
}, interval * 1000); // query each "interval" seconds }, interval * 1000); // query each "interval" seconds
``` ```
@@ -181,20 +178,19 @@ This example creates the custom control parameters `x` and `y` which are then us
### Outputs ### Outputs
Now that we know how the value of an event is manipulated using control parameters, we can look at how outputs can use that value to generate anything. The scheduler above was calling the `onTrigger` function which is used to implement the output. A very simple version of the web audio output could look like this: Now that we know how the value of an hap is manipulated using control parameters, we can look at how outputs can use that value to generate anything. The scheduler above was calling the `onSchedule` function which is used to implement the output. A very simple version of the web audio output could look like this:
```js ```js
function onTrigger(hap, deadline, duration) { function onSchedule(hap, time, cps) {
const { note } = hap.value; const { note } = hap.value;
const time = getAudioContext().currentTime + deadline;
const o = getAudioContext().createOscillator(); const o = getAudioContext().createOscillator();
o.frequency.value = getFreq(note); o.frequency.value = getFreq(note);
o.start(time); o.start(time);
o.stop(time + event.duration); o.stop(time + hap.duration / cps);
o.connect(getAudioContext().destination); o.connect(getAudioContext().destination);
} }
``` ```
The above example will create an `OscillatorNode` for each event, where the frequency is controlled by the `note` param. In essence, this is how the WebAudio API output of Strudel works, only with many more parameters to control synths, samples and effects. The above example will create an `OscillatorNode` for each hap, where the frequency is controlled by the `note` param. In essence, this is how the WebAudio API output of Strudel works, only with many more parameters to control synths, samples and effects.
I want to help, how do I contribute to the [Docs](/technical-manual/docs)? I want to help, how do I contribute to the [Docs](/technical-manual/docs)?
+2 -2
View File
@@ -628,7 +628,7 @@ export const debugAudiograph = async (argOptions = {}) => {
/*global all*/ /*global all*/
all((pat) => all((pat) =>
pat.onTrigger(async (hap, duration, cps, t) => { pat.onSchedule(async (hap, t, cps) => {
hap_count++; hap_count++;
const key = Object.entries(hap.value) const key = Object.entries(hap.value)
.map((param) => param.join('/')) .map((param) => param.join('/'))
@@ -643,7 +643,7 @@ export const debugAudiograph = async (argOptions = {}) => {
return; return;
} }
await webaudioOutput(hap, t, hap.duration / cps, cps, t); await webaudioOutput(hap, t, cps);
if (hapsBatch && hap_count % hapsBatch === 0) drawDiagram(); if (hapsBatch && hap_count % hapsBatch === 0) drawDiagram();
}), }),
+2 -2
View File
@@ -68,7 +68,7 @@ const initialCode = `// LOADING`;
export function useReplContext() { export function useReplContext() {
const { isSyncEnabled, audioEngineTarget, prebakeScript, includePrebakeScriptInShare } = useSettings(); const { isSyncEnabled, audioEngineTarget, prebakeScript, includePrebakeScriptInShare } = useSettings();
const shouldUseWebaudio = audioEngineTarget !== audioEngineTargets.osc; const shouldUseWebaudio = audioEngineTarget !== audioEngineTargets.osc;
const defaultOutput = shouldUseWebaudio ? webaudioOutput : superdirtOutput; const onSchedule = shouldUseWebaudio ? webaudioOutput : superdirtOutput;
const getTime = shouldUseWebaudio ? getAudioContextCurrentTime : getPerformanceTimeSeconds; const getTime = shouldUseWebaudio ? getAudioContextCurrentTime : getPerformanceTimeSeconds;
const init = useCallback(() => { const init = useCallback(() => {
setActivePattern(getViewingPatternData().id); setActivePattern(getViewingPatternData().id);
@@ -76,7 +76,7 @@ export function useReplContext() {
const drawContext = getDrawContext(); const drawContext = getDrawContext();
const editor = new StrudelMirror({ const editor = new StrudelMirror({
sync: isSyncEnabled, sync: isSyncEnabled,
defaultOutput, onSchedule,
getTime, getTime,
setInterval, setInterval,
clearInterval, clearInterval,
+1 -1
View File
@@ -138,7 +138,7 @@ export const setIsZen = (active) => settingsMap.setKey('isZen', !!active);
const patternSetting = (key) => const patternSetting = (key) =>
register(key, (value, pat) => register(key, (value, pat) =>
pat.onTrigger(() => { pat.onSchedule(() => {
value = Array.isArray(value) ? value.join(' ') : value; value = Array.isArray(value) ? value.join(' ') : value;
if (value !== settingsMap.get()[key]) { if (value !== settingsMap.get()[key]) {
settingsMap.setKey(key, value); settingsMap.setKey(key, value);