Compare commits

..

11 Commits

Author SHA1 Message Date
Aria 3c407412c6 Merge branch 'glossing/basic-node-pool' into glossing/full-node-pool 2025-12-04 22:52:57 -06:00
Aria 4c99f4866b Use setvalueattime to pin values and prevent bleed 2025-12-04 22:50:28 -06:00
Aria 099288545e Working version 2025-12-04 22:12:54 -06:00
Aria 5738020ac2 Working version of defaulting 2025-12-04 21:41:52 -06:00
Aria 1478ff51a7 Defaulting 2025-12-04 21:33:33 -06:00
Aria 8314023c7a Mostly working - need to fix stretch and distort 2025-12-04 15:41:23 -06:00
Aria d50af71ba1 Merge branch 'main' into glossing/basic-node-pool 2025-12-04 14:20:41 -06:00
Aria 0664f90178 Remove old comment 2025-12-04 13:07:13 -06:00
Aria 4321814d36 Working version for compressor, filter, supersaw, wavetable 2025-12-04 13:04:33 -06:00
Aria 6610713018 Working version of supersaw, almost on wavetable 2025-12-04 12:40:28 -06:00
Aria 600ab0a83e WIP with compressors, filters, heavy worklets 2025-12-04 12:17:07 -06:00
25 changed files with 480 additions and 1207 deletions
+1 -4
View File
@@ -1,9 +1,6 @@
name: Build and Deploy to beta (warm.strudel.cc)
on:
push:
branches:
- main
on: [workflow_dispatch]
# Allow one concurrent deployment
concurrency:
+17 -22
View File
@@ -4,13 +4,20 @@ Thanks for wanting to contribute!!! There are many ways you can add value to thi
## Move to codeberg
Along with many other live coding projects, we have moved from Microsoft's Github platform to Codeberg for ethical reasons. Please don't fork the project back to github.
We are currently in the process of moving from github to codeberg -- not everything is working, please bear with us.
To update your local clone, you can run this command:
```
git remote set-url origin git@codeberg.org:uzu/strudel.git
```
## Communication Channels
To get in touch with the community, either
To get in touch with the contributors, either
- [join the Uzulang Discord Server](https://discord.com/invite/HGEdXmRkzT) and go to the strudel channels (Uzulangs are a family of live coding languages inspired by each other, including TidalCycles as well as Strudel)
- [join the Tidal Discord Channel](https://discord.com/invite/HGEdXmRkzT) and go to the #strudel channel
- Find related discussions on the [tidal club forum](https://club.tidalcycles.org/)
## Ask a Question
@@ -29,35 +36,22 @@ Use one of the Communication Channels listed above and drop us a line or two!
## Share Music
If you made some music with strudel, you can give back some love and share what you've done!
Your creation could also be part of the random selection in the REPL if you want.
Use one of the Communication Channels listed above.
(There used to be a random selection of contributed patterns in the REPL, but that is unfortunately disabled for now due to abuse)
## Improve the Docs
If you find some weak spots in the [docs](https://strudel.cc/workshop/getting-started/), you can edit each file directly on codeberg. There are "Edit this page" links in the right sidebar that take you to the right place.
If you find some weak spots in the [docs](https://strudel.cc/workshop/getting-started/),
you can edit each file directly on codeburg. (we are currently fixing the "Edit this page" links in the right sidebar)
## Propose a Feature
If you want a specific feature that is not part of strudel yet, feel free to use one of the communication channels above. Please bear in mind that this is a free/open source project, oriented around collaborative discussion. Maybe you even want to help with the implementation of that feature!
## Contribute a feature
Pull requests welcome! Consider starting with discussion or proof-of-concept first, rather than do loads of work on something and then find it doesn't fit the collective goals of the project, or something like that.
At the time of writing we have a PR backlog, and generally prioritise bugfixes and contributions that have arisen through community discussion.
### AI/LLM policy
Strudel is a project handmade by humans, with thought and nuance.
If you have used LLMs (so called 'AI'), please detail that in the pull request. We are still developing our response to the onslaught of LLM technology, but for practical and legal reasons are currently not accepting wholly LLM-generated code. We are also not accepting PRs that add LLM features to strudel itself.
There are #llm-chat and #llm-share channels on our discord. Please do not discuss or share LLM-related things outside of those channels.
If you want a specific feature that is not part of strudel yet, feel free to use one of the communication channels above.
Maybe you even want to help with the implementation of that feature!
## Report a Bug
If you've found a bug, or some behaviour that does not seem right, you are welcome to file an [issue](https://codeberg.org/uzu/strudel/issues).
Please check that it has not been reported before.
## Fix a Bug
@@ -124,7 +118,8 @@ There are also eslint extensions / plugins for most editors.
## Running all CI Checks
When opening a PR, the CI runner will (once approved by a human) check the code style and eslint, as well as run all tests. You can run the same check with `pnpm check`.
When opening a PR, the CI runner will automatically check the code style and eslint, as well as run all tests.
You can run the same check with `pnpm check`
## Package Workflow
+13 -30
View File
@@ -401,13 +401,12 @@ export const { accelerate } = registerControl('accelerate');
* Sets the velocity from 0 to 1. Is multiplied together with gain.
*
* @name velocity
* @synonyms vel
* @example
* s("hh*8")
* .gain(".4!2 1 .4!2 1 .4 1")
* .velocity(".4 1")
*/
export const { velocity, vel } = registerControl('velocity', 'vel');
export const { velocity } = registerControl('velocity');
/**
* Controls the gain by an exponential amount.
*
@@ -1483,13 +1482,14 @@ export const { lpdepth } = registerControl('lpdepth');
* Depth of the LFO for the lowpass filter, in HZ
*
* @name lpdepthfrequency
* @synonyms lpdepthfreq
* @synonyms
* lpdethfreq
* @param {number | Pattern} depth depth of modulation
* @example
* note("<c c c# c c c4>*16").s("sawtooth").lpf(600).lpdepthfrequency("<200 500 100 0>")
*/
export const { lpdepthfrequency, lpdepthfreq } = registerControl('lpdepthfrequency', 'lpdepthfreq');
export const { lpdepthfrequency } = registerControl('lpdepthfrequency', 'lpdepthfreq');
/**
* Shape of the LFO for the lowpass filter
@@ -1543,13 +1543,14 @@ export const { bpdepth } = registerControl('bpdepth');
* Depth of the LFO for the bandpass filter, in HZ
*
* @name bpdepthfrequency
* @synonyms bpdepthfreq
* @synonyms
* bpdethfreq
* @param {number | Pattern} depth depth of modulation
* @example
* note("<c c c# c c c4>*16").s("sawtooth").lpf(600).bpdepthfrequency("<200 500 100 0>")
*/
export const { bpdepthfrequency, bpdepthfreq } = registerControl('bpdepthfrequency', 'bpdepthfreq');
export const { bpdepthfrequency } = registerControl('bpdepthfrequency', 'bpdepthfreq');
/**
* Shape of the LFO for the bandpass filter
@@ -1597,19 +1598,20 @@ export const { hpsync } = registerControl('hpsync');
* @name hpdepth
* @param {number | Pattern} depth depth of modulation
*/
export const { hpdepth } = registerControl('hpdepth');
export const { hpdepth, hpdepthfreq } = registerControl('hpdepth');
/**
* Depth of the LFO for the hipass filter, in hz
*
* @name hpdepthfrequency
* @synonyms hpdepthfreq
* @synonyms
* hpdethfreq
* @param {number | Pattern} depth depth of modulation
* @example
* note("<c c c# c c c4>*16").s("sawtooth").lpf(600).hpdepthfrequency("<200 500 100 0>")
*/
export const { hpdepthfrequency, hpdepthfreq } = registerControl('hpdepthfrequency', 'hpdepthfreq');
export const { hpdepthfrequency } = registerControl('hpdepthfrequency', 'hpdepthfreq');
/**
* Shape of the LFO for the highpass filter
@@ -2753,13 +2755,8 @@ export const as = register('as', (mapping, pat) => {
mapping = Array.isArray(mapping) ? mapping : [mapping];
return pat.fmap((v) => {
v = Array.isArray(v) ? v : [v];
const entries = [];
for (let i = 0; i < mapping.length; ++i) {
if (v[i] !== undefined) {
entries.push([getControlName(mapping[i]), v[i]]);
}
}
return Object.fromEntries(entries);
v = Object.fromEntries(mapping.map((prop, i) => [getControlName(prop), v[i]]));
return v;
});
});
@@ -2791,17 +2788,3 @@ export const scrub = register(
},
false,
);
/**
* Transient shaper. Gives independent control over the emphasis on transients
* and sustains
*
* @name transient
* @param {number | Pattern} attack Emphasis on transients; between -1 (deaccentuate) and 1 (accentuate)
* @param {number | Pattern} sustain Emphasis on the sustains; between -1 (deaccentuate) and 1 (accentuate)
* @example
* s("bd").transient("<-1 -0.5 0 0.5 1>")
* @example
* s("hh*16").bank("tr909").transient("<-1:1 1:-1>")
*/
export const { transient } = registerControl(['transient', 'transsustain']);
-90
View File
@@ -1,90 +0,0 @@
/*
stateful.mjs - File of shame for stateful, impure and otherwise illegal pattern methods
Copyright (C) 2025 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/core/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/>.
*/
import { register, reify, Pattern } from './pattern.mjs';
let timelines = {};
export const reset_state = function () {
reset_timelines();
};
export const reset_timelines = function () {
timelines = {};
};
/***
* Allows you to switch a pattern between different 'timelines'. This is particularly useful when
* live coding, for example when you want to cue a pattern up to play from its start.
*
* Timelines are specified by number, so that if you had a pattern like
* `n("<0 1 2 3>").s("num").timeline(1)` playing, then changed the '1'
* to '2', it would always align '0' to the nearest cycle. You will likely want to trigger
* an evaluation a little bit before the cycle starts, to avoid missing events.
*
* After the first use, a timeline will continue with the same 'offset'. That is, if you change
* a pattern without changing its timeline number, it will stay on that timeline without resetting.
*
* Rather than incrementing a timeline to reset it, it's easier to negate it, e.g. by switching between `-2`
* and `2`. This is because when you negate a timeline it will always reset.
*
* You can also pattern the timeline if you want, to create strange resetting patterns.
* @param {number | Pattern} timeline The timeline that the pattern should play on.
* @example
* n("<0 1 2 3>(3,8)")
* .sound("num")
* // resets the timeline every two cycles, by negating the timeline.
* // in a lot of cases this will be edited by a human live coder
* // rather than patterned!
* .timeline("<2 -2>".slow(2))
*/
export const timeline = register(
'timeline',
function (tpat, pat) {
tpat = reify(tpat);
const f = function (state) {
// Is this called from the scheduler? (rather than from e.g. the visualiser)
const scheduler = !!state.controls.cyclist;
const timehaps = tpat.query(state);
const result = [];
for (const timehap of timehaps) {
const tlid = timehap.value;
let offset;
if (tlid === 0) {
offset = 0;
} else if (tlid in timelines) {
offset = timelines[tlid];
} else {
const timearc = timehap.wholeOrPart();
if (!scheduler || state.span.begin.lt(timearc.midpoint())) {
offset = timearc.begin;
} else {
// Sync to end of timearc if we first see it over halfway into its
// timespan. Allows 'cuing up' next timeline when live coding.
offset = timearc.end;
}
}
if (scheduler) {
// update state
timelines[tlid] = offset;
if (tlid !== 0) {
delete timelines[-tlid];
}
}
const pathaps = pat
.late(offset)
.query(state.setSpan(timehap.part))
.map((h) => h.setContext(h.combineContext(timehap)));
result.push(...pathaps);
}
return result;
};
return new Pattern(f, pat._steps);
},
false,
);
+9 -10
View File
@@ -1,6 +1,6 @@
/*
index.mjs - <short description TODO>
Copyright (C) 2025 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/core/index.mjs>
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/core/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/>.
*/
@@ -11,21 +11,20 @@ import createClock from './zyklus.mjs';
import { logger } from './logger.mjs';
export { Fraction, controls, createClock };
export * from './controls.mjs';
export * from './cyclist.mjs';
export * from './evaluate.mjs';
export * from './hap.mjs';
export * from './impure.mjs';
export * from './logger.mjs';
export * from './pattern.mjs';
export * from './pick.mjs';
export * from './repl.mjs';
export * from './signal.mjs';
export * from './speak.mjs';
export * from './pick.mjs';
export * from './state.mjs';
export * from './time.mjs';
export * from './timespan.mjs';
export * from './ui.mjs';
export * from './util.mjs';
export * from './speak.mjs';
export * from './evaluate.mjs';
export * from './repl.mjs';
export * from './cyclist.mjs';
export * from './logger.mjs';
export * from './time.mjs';
export * from './ui.mjs';
export { default as drawLine } from './drawLine.mjs';
// below won't work with runtime.mjs (json import fails)
/* import * as p from './package.json';
+6 -23
View File
@@ -574,8 +574,7 @@ export class Pattern {
* Returns a new Pattern, which only returns haps that meet the given test.
* @param {Function} hap_test - a function which returns false for haps to be removed from the pattern
* @returns Pattern
* @example
* s("bd*8").velocity(rand).filterHaps((h) => (h.whole.begin % 1) < h.value.velocity)
* @noAutocomplete
*/
filterHaps(hap_test) {
return new Pattern((state) => this.query(state).filter(hap_test));
@@ -586,11 +585,7 @@ export class Pattern {
* inside haps.
* @param {Function} value_test
* @returns Pattern
* @example
* const drums = s("bd sd bd sd")
* kick: drums.filterValues((v) => v.s === 'bd').duck(2)
* snare: drums.filterValues((v) => v.s === 'sd')
* bass: s("saw!4").note("G#1").lpf(80).lpenv(4).orbit(2)
* @noAutocomplete
*/
filterValues(value_test) {
return new Pattern((state) => this.query(state).filter((hap) => value_test(hap.value))).setSteps(this._steps);
@@ -1592,13 +1587,7 @@ export const func = curry((a, b) => reify(b).func(a));
*
* @param {string | string[]} name name of the function, or an array of names to be used as synonyms
* @param {function} func function with 1 or more params, where last is the current pattern
* @param {bool} patternify defaults to true; if set to false, you will have more control over the arguments to `func` as they will be
* in their raw form and it will be up to you to patternify them and/or query them for values
* @example
* const vlpf = register('vlpf', (freq, pat) => {
* return pat.fmap((v) => ({...v, cutoff: freq * (v.velocity ?? 1) }));
* })
* s("saw").seg(8).velocity(rand).vlpf(800)
* @noAutocomplete
*
*/
export function register(name, func, patternify = true, preserveSteps = false, join = (x) => x.innerJoin()) {
@@ -2687,13 +2676,8 @@ export const hsl = register('hsl', (h, s, l, pat) => {
/**
* Tags each Hap with an identifier. Good for filtering. The function populates Hap.context.tags (Array).
* @name tag
* @noAutocomplete
* @param {string} tag anything unique
* @example
* s("saw!16").note("F1")
* .lpf(tri.range(40, 80).slow(4)).lpenv(5).lpq(4).lpd(0.15)
* .when(rand.late(0.1).gte(0.5), x => x.transpose("12").tag('altered'))
* .when(rand.late(0.2).gte(0.5), x => x.s("square").tag('altered'))
* .when("<0 1>", x => x.filter((hap) => hap.hasTag('altered')))
*/
Pattern.prototype.tag = function (tag) {
return this.withContext((ctx) => ({ ...ctx, tags: (ctx.tags || []).concat([tag]) }));
@@ -2704,16 +2688,15 @@ Pattern.prototype.tag = function (tag) {
* @name filter
* @param {Function} test function to test Hap
* @example
* s("hh!7 oh").filter(hap => hap.value.s === 'hh')
* s("hh!7 oh").filter(hap => hap.value.s==='hh')
*/
export const filter = register('filter', (test, pat) => pat.withHaps((haps) => haps.filter(test)));
/**
* Filters haps by their begin time
* @name filterWhen
* @noAutocomplete
* @param {Function} test function to test Hap.whole.begin
* @example
* oneCycle: s("bd*4").filterWhen((t) => t < 1)
*/
export const filterWhen = register('filterWhen', (test, pat) => pat.filter((h) => test(h.whole.begin)));
-4
View File
@@ -5,7 +5,6 @@ import { errorLogger, logger } from './logger.mjs';
import { setTime } from './time.mjs';
import { evalScope } from './evaluate.mjs';
import { register, Pattern, isPattern, silence, stack } from './pattern.mjs';
import { reset_state } from './impure.mjs';
export function repl({
defaultOutput,
@@ -53,9 +52,6 @@ export function repl({
onToggle: (started) => {
updateState({ started });
onToggle?.(started);
if (!started) {
reset_state();
}
},
setInterval,
clearInterval,
-47
View File
@@ -882,50 +882,3 @@ export const whenKey = register('whenKey', function (input, func, pat) {
export const keyDown = register('keyDown', function (pat) {
return pat.fmap(_keyDown);
});
/**
* A pattern measuring the duration of events,
* in cycles per event. `cyclesPer` doesn't have structure itself, but takes structure, and therefore
* event durations, from the pattern that it is combined with.
* For example `cyclesPer.struct("1 1 [1 1] 1")` would give the same as `"0.25 0.25 [0.125 0.125] 0.25"`.
* See also its reciprocal, `per`, also known as `perCycle`.
* @example
* // Shorter events are lower in pitch
* sound("saw saw [saw saw] saw")
* .note(cyclesPer.range(50, 100))
* @example
* sound("bd sd [bd bd] sd*4 [- sd] [bd [bd bd]]")
* .note(cyclesPer.add(20))
*/
export const cyclesPer = new Pattern(function (state) {
return [new Hap(undefined, state.span, state.span.duration)];
});
/**
* A pattern measuring the 'shortness' of events, or in other words, the duration of pattern events,
* in events per cycle. `per` doesn't have structure itself, but takes structure, and therefore
* event durations, from the pattern that it is combined with.
* For example `per.struct("1 1 [1 1] 1")` would give the same as `"4 4 [8 8] 4"`.
* See also its reciprocal, `cyclesPer`.
* @synonyms perCycle
* @example
* // Shorter events are more distorted
* n("0 0*2 0 0*2 0 [0 0 0]@2").sound("bd")
* .distort(per.div(2))
*/
export const per = new Pattern(function (state) {
return [new Hap(undefined, state.span, Fraction(1).div(state.span.duration))];
});
export const perCycle = per;
/**
* Like `per` but measures the shortness of events according to an exponential curve. In
* particular, where the event duration halves, the
* returned value increases by one. `perx.struct("1 1 [1 [1 1]] 1")` would therefore be
* the same as `"3 3 [4 [5 5]] 3"`.
*/
export const perx = new Pattern(function (state) {
const n = Fraction(1).div(state.span.duration);
return [new Hap(undefined, state.span, Math.log(n) / Math.log(2) + 1)];
});
+14
View File
@@ -740,6 +740,20 @@ describe('Pattern', () => {
);
});
});
describe('signal()', () => {
it('Can make saw/saw2', () => {
expect(saw.struct(true, true, true, true).firstCycle()).toStrictEqual(
sequence(0, 1 / 4, 1 / 2, 3 / 4).firstCycle(),
);
expect(saw2.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(-1, -0.5, 0, 0.5).firstCycle());
});
it('Can make isaw/isaw2', () => {
expect(isaw.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(1, 0.75, 0.5, 0.25).firstCycle());
expect(isaw2.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(1, 0.5, 0, -0.5).firstCycle());
});
});
describe('_setContext()', () => {
it('Can set the hap context', () => {
expect(
-61
View File
@@ -1,61 +0,0 @@
/*
signal.test.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/core/test/pattern.test.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/>.
*/
import Fraction from 'fraction.js';
import { describe, it, expect, vi } from 'vitest';
import { saw, saw2, isaw, isaw2, per, perx, cyclesPer } from '../signal.mjs';
import { fastcat, sequence, State, TimeSpan, Hap } from '../index.mjs';
const st = (begin, end) => new State(ts(begin, end));
const ts = (begin, end) => new TimeSpan(Fraction(begin), Fraction(end));
const hap = (whole, part, value, context = {}) => new Hap(whole, part, value, context);
const third = Fraction(1, 3);
const twothirds = Fraction(2, 3);
const sameFirst = (a, b) => {
return expect(a.sortHapsByPart().firstCycle()).toStrictEqual(b.sortHapsByPart().firstCycle());
};
describe('signal()', () => {
it('Can make saw/saw2', () => {
expect(saw.struct(true, true, true, true).firstCycle()).toStrictEqual(
sequence(0, 1 / 4, 1 / 2, 3 / 4).firstCycle(),
);
expect(saw2.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(-1, -0.5, 0, 0.5).firstCycle());
});
it('Can make isaw/isaw2', () => {
expect(isaw.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(1, 0.75, 0.5, 0.25).firstCycle());
expect(isaw2.struct(true, true, true, true).firstCycle()).toStrictEqual(sequence(1, 0.5, 0, -0.5).firstCycle());
});
});
describe('cyclesPer', () => {
it('gives cycles per hap', () => {
sameFirst(
cyclesPer.struct(true, true, true, fastcat(true, true)),
sequence(0.25, 0.25, 0.25, fastcat(0.125, 0.125)).fmap(Fraction),
);
});
});
describe('per', () => {
it('gives haps per cycle', () => {
sameFirst(per.struct(true, true, true, fastcat(true, true)), sequence(4, 4, 4, fastcat(8, 8)).fmap(Fraction));
});
});
describe('perx', () => {
it('gives exponential haps per cycle', () => {
sameFirst(
perx.struct(true, true, true, fastcat(true, fastcat(true, true))),
sequence(3, 3, 3, fastcat(4, fastcat(5, 5))),
);
});
});
+9 -26
View File
@@ -479,24 +479,14 @@ Pattern.prototype.midi = function (midiport, options = {}) {
let listeners = {};
const refs = {};
const refsByChan = {};
/**
* MIDI input: Opens a MIDI input port to receive MIDI control change messages.
*
* The output is a function that accepts a midi cc value to query as well as (optionally) a midi channel
* @param {string | number} input MIDI device name or index defaulting to 0
* @returns {function(number, number=): Pattern} A function from (cc, channel?) to a pattern.
* When queried, the pattern will produces the most recently received midi value (normalized to 0 to 1)
* that came through that cc number (and channel, if provided)
* @returns {Function}
* @example
* const cc = await midin('IAC Driver Bus 1')
* let cc = await midin('IAC Driver Bus 1')
* note("c a f e").lpf(cc(0).range(0, 1000)).lpq(cc(1).range(0, 10)).sound("sawtooth")
* @example
* const allCC = await midin('IAC Driver Bus 1')
* const cc = (ccNum) => allCC(ccNum, 2) // just channel 2
* note("c a f e").s("saw")
* .when(cc(0).gt(0), x => x.postgain(0))
*/
export async function midin(input) {
if (isPattern(input)) {
@@ -521,24 +511,17 @@ export async function midin(input) {
}`,
);
}
refs[input] ??= {};
refsByChan[input] ??= {};
const cc = (cc, chan) => {
if (chan !== undefined) {
return ref(() => refsByChan[input][cc]?.[chan] || 0);
}
return ref(() => refs[input][cc] || 0);
};
// ensure refs for this input are initialized
if (!refs[input]) {
refs[input] = {};
}
const cc = (cc) => ref(() => refs[input][cc] || 0);
listeners[input] && device.removeListener('midimessage', listeners[input]);
listeners[input] = (e) => {
const ccNum = e.dataBytes[0];
const cc = e.dataBytes[0];
const v = e.dataBytes[1];
const chan = e.message.channel;
const scaled = v / 127;
refsByChan[input][ccNum] ??= {};
refsByChan[input][ccNum][chan] = scaled;
refs[input][ccNum] = scaled;
refs[input] && (refs[input][cc] = v / 127);
};
device.addListener('midimessage', listeners[input]);
return cc;
+3 -3
View File
@@ -7,7 +7,6 @@ import {
getPitchEnvelope,
getVibratoOscillator,
onceEnded,
releaseAudioNode,
} from '@strudel/webaudio';
import gm from './gm.mjs';
@@ -173,8 +172,9 @@ export function registerSoundfonts() {
bufferSource.stop(envEnd);
const stop = (releaseTime) => {};
onceEnded(bufferSource, () => {
releaseAudioNode(bufferSource);
releaseAudioNode(vibratoOscillator);
bufferSource.disconnect();
vibratoOscillator?.stop();
node.disconnect();
onended();
});
return { node, stop };
+9
View File
@@ -1,3 +1,12 @@
/*
audioContext.mjs - Audio Context manager
Sets up a common and accessible audio context for all superdough operations
Copyright (C) 2025 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/superdough/audiocontext.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/>.
*/
let audioContext;
export const setDefaultAudioContext = () => {
+36 -21
View File
@@ -1,6 +1,7 @@
import { getAudioContext } from './audioContext.mjs';
import { logger } from './logger.mjs';
import { getNoiseBuffer } from './noise.mjs';
import { getNodeFromPool, markWorkletAsDead } from './nodePools.mjs';
import { clamp, nanFallback, midiToFreq, noteToMidi } from './util.mjs';
export const noises = ['pink', 'white', 'brown', 'crackle'];
@@ -36,6 +37,16 @@ export function getWorklet(ac, processor, params, config) {
return node;
}
export function getPooledWorklet(ac, processor, params, constructorOptions, reInitOptions) {
const node = getNodeFromPool(processor, () => new AudioWorkletNode(ac, processor, constructorOptions), params);
node.port.postMessage({ type: 'initialize', payload: reInitOptions });
node.port.onmessage = (e) => {
if (e.data.type === 'died') markWorkletAsDead(node);
node.port.onmessage = null;
};
return node;
}
export const getParamADSR = (
param,
attack,
@@ -124,10 +135,11 @@ export function getLfo(audioContext, begin, end, properties = {}) {
...props,
};
return getWorklet(audioContext, 'lfo-processor', lfoprops);
return getPooledWorklet(audioContext, 'lfo-processor', lfoprops);
}
export function getCompressor(ac, threshold, ratio, knee, attack, release) {
const node = getNodeFromPool('compressor', () => new DynamicsCompressorNode(ac, {}));
const options = {
threshold: threshold ?? -3,
ratio: ratio ?? 10,
@@ -135,7 +147,11 @@ export function getCompressor(ac, threshold, ratio, knee, attack, release) {
attack: attack ?? 0.005,
release: release ?? 0.05,
};
return new DynamicsCompressorNode(ac, options);
const now = ac.currentTime;
Object.entries(options).forEach(([key, value]) => {
node[key].setValueAtTime(value, now);
});
return node;
}
// changes the default values of the envelope based on what parameters the user has defined
@@ -189,7 +205,7 @@ export function applyParameterModulators(audioContext, param, start, end, envelo
getParamADSR(param, attack, decay, sustain, release, min, max, start, holdEnd, curve);
}
const lfo = getParamLfo(audioContext, param, start, end, lfoValues);
return lfo;
return { lfo, disconnect: () => lfo?.disconnect() };
}
export function createFilter(context, start, end, params, cps, cycle) {
let {
@@ -211,13 +227,16 @@ export function createFilter(context, start, end, params, cps, cycle) {
let frequencyParam, filter;
if (model === 'ladder') {
filter = getWorklet(context, 'ladder-processor', { frequency, q, drive });
filter = getPooledWorklet(context, 'ladder-processor', { frequency, q, drive });
frequencyParam = filter.parameters.get('frequency');
} else {
filter = context.createBiquadFilter();
const factory = () => context.createBiquadFilter();
filter = getNodeFromPool('filter', factory);
filter.type = type;
filter.Q.value = q;
filter.frequency.value = frequency;
const now = context.currentTime;
Object.entries({ Q: q, frequency }).forEach(([key, value]) => {
filter[key].setValueAtTime(value, now);
});
frequencyParam = filter.frequency;
}
const envelopeValues = [params.attack, params.decay, params.sustain, params.release];
@@ -283,12 +302,9 @@ export function drywet(dry, wet, wetAmount = 0) {
wet_gain.connect(mix);
return {
node: mix,
teardown: () => {
releaseAudioNode(dry_gain);
releaseAudioNode(wet_gain);
// it is not the responsability of drywet
// to call `releaseAudioNode` on
// the 2 external args dry and wet
onended: () => {
dry_gain.disconnect(mix);
wet_gain.disconnect(mix);
dry.disconnect(dry_gain);
wet.disconnect(wet_gain);
},
@@ -327,10 +343,10 @@ export function getVibratoOscillator(param, value, t) {
gain.gain.value = vibmod * 100;
vibratoOscillator.connect(gain);
gain.connect(param);
onceEnded(vibratoOscillator, () => {
releaseAudioNode(gain);
releaseAudioNode(vibratoOscillator);
});
vibratoOscillator.onended = () => {
gain.disconnect(param);
vibratoOscillator.disconnect(gain);
};
vibratoOscillator.start(t);
return vibratoOscillator;
}
@@ -551,8 +567,9 @@ export const getDistortionAlgorithm = (algo) => {
return distortionAlgorithms[name];
};
export const getDistortion = (distort, postgain, algorithm) => {
return getWorklet(getAudioContext(), 'distort-processor', { distort, postgain }, { processorOptions: { algorithm } });
export const getDistortion = (fullParams) => {
const { algorithm, ...params } = fullParams;
return getPooledWorklet(getAudioContext(), 'distort-processor', params, {}, { algorithm });
};
export const getFrequencyFromValue = (value, defaultNote = 36) => {
@@ -616,8 +633,6 @@ export const releaseAudioNode = (node) => {
// returns true and either its active source flag is true or
// any AudioNode connected to one of its inputs is actively processing.
if (node instanceof AudioWorkletNode) {
// while `end` is not native to the web audio API, it is common practice in superdough
// to use that param in the worklets to trigger returning false from the processor
node.parameters.get('end')?.setValueAtTime(0, 0);
}
};
+71
View File
@@ -0,0 +1,71 @@
/*
nodePools.mjs - Helper functions related to pooling and re-using audio nodes
Copyright (C) 2025 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/superdough/nodePools.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/>.
*/
const nodePools = new Map();
const POOL_KEY = Symbol('nodePoolKey');
const IS_WORKLET_DEAD = Symbol('nodePoolIsWorkletDead');
const MAX_POOL_SIZE = 64;
export const isPoolable = (node) => !!node[POOL_KEY];
export const releaseNodeToPool = (node) => {
node.disconnect();
if (node instanceof AudioScheduledSourceNode) {
// not reusable
return;
}
if (node[IS_WORKLET_DEAD]) {
// Worklet already terminated, don't pool it
return;
}
const key = node[POOL_KEY];
if (key == null) return;
const pool = nodePools.get(key) ?? [];
if (pool.length < MAX_POOL_SIZE) {
pool.push(new WeakRef(node));
nodePools.set(key, pool);
}
};
export const markWorkletAsDead = (worklet) => (worklet[IS_WORKLET_DEAD] = true);
// Attempt to get node from the pool. If this fails, fall back
// to building it with the factory
export const getNodeFromPool = (key, factory, params = {}) => {
const pool = nodePools.get(key) ?? [];
let node;
while (pool.length) {
const ref = pool.pop();
node = ref?.deref();
if (node != null && !node[IS_WORKLET_DEAD]) break;
}
if (node == null || node[IS_WORKLET_DEAD]) {
node = factory();
}
node[POOL_KEY] = key;
const paramMap = new Map();
if (node instanceof AudioWorkletNode) {
for (const [name, param] of node.parameters.entries()) {
paramMap.set(name, param);
}
} else {
for (const name of Object.getOwnPropertyNames(node)) {
const value = node[name];
if (value instanceof AudioParam) {
paramMap.set(name, value);
}
}
}
const now = node.context?.currentTime ?? 0;
paramMap.forEach((param, name) => {
param.cancelScheduledValues(now);
// Set values from `params` or restore defaults
const target = params[name] !== undefined ? params[name] : param.defaultValue;
param.setValueAtTime(target, now);
});
return node;
};
+2 -5
View File
@@ -1,4 +1,4 @@
import { drywet, onceEnded, releaseAudioNode } from './helpers.mjs';
import { drywet } from './helpers.mjs';
import { getAudioContext } from './audioContext.mjs';
let noiseCache = {};
@@ -65,12 +65,9 @@ export function getNoiseOscillator(type = 'white', t, density = 0.02) {
export function getNoiseMix(inputNode, wet, t) {
const noiseOscillator = getNoiseOscillator('pink', t);
const noiseMix = drywet(inputNode, noiseOscillator.node, wet);
onceEnded(noiseOscillator.node, () => {
releaseAudioNode(noiseOscillator.node);
});
noiseOscillator.node.onended = () => noiseMix.onended();
return {
node: noiseMix.node,
stop: (time) => noiseOscillator?.stop(time),
teardown: noiseMix.teardown,
};
}
+6 -14
View File
@@ -1,22 +1,23 @@
'use strict';
import { makeReusable } from './worklets-common.mjs';
// sourced from https://github.com/olvb/phaze/tree/master?tab=readme-ov-file
const WEBAUDIO_BLOCK_SIZE = 128;
/** Overlap-Add Node */
class OLAProcessor extends AudioWorkletProcessor {
class OLAProcessor extends makeReusable(AudioWorkletProcessor) {
constructor(options) {
super(options);
this.started = false;
this.nbInputs = options.numberOfInputs;
this.nbOutputs = options.numberOfOutputs;
this.blockSize = options.processorOptions.blockSize;
// TODO for now, the only support hop size is the size of a web audio block
this.hopSize = WEBAUDIO_BLOCK_SIZE;
this.nbOverlaps = this.blockSize / this.hopSize;
}
initialize() {
// pre-allocate input buffers (will be reallocated if needed)
this.inputBuffers = new Array(this.nbInputs);
this.inputBuffersHead = new Array(this.nbInputs);
@@ -54,7 +55,6 @@ class OLAProcessor extends AudioWorkletProcessor {
allocateInputChannels(inputIndex, nbChannels) {
// allocate input buffers
this.inputBuffers[inputIndex] = new Array(nbChannels);
for (let i = 0; i < nbChannels; i++) {
this.inputBuffers[inputIndex][i] = new Float32Array(this.blockSize + WEBAUDIO_BLOCK_SIZE);
@@ -157,15 +157,8 @@ class OLAProcessor extends AudioWorkletProcessor {
}
}
process(inputs, outputs, params) {
const input = inputs[0];
const hasInput = !(input[0] === undefined);
if (this.started && !hasInput) {
return false;
}
this.started = hasInput;
processActive(inputs, outputs, params) {
this.reallocateChannelsIfNeeded(inputs, outputs);
this.readInputs(inputs);
this.shiftInputBuffers();
this.prepareInputBuffersToSend();
@@ -173,7 +166,6 @@ class OLAProcessor extends AudioWorkletProcessor {
this.handleOutputBuffersToRetrieve();
this.writeOutputs(outputs);
this.shiftOutputBuffers();
return true;
}
+10 -4
View File
@@ -243,6 +243,12 @@ export async function fetchSampleMap(url) {
* sd: '808sd/SD0010.WAV'
* }, 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/');
* s("[bd ~]*2, [~ hh]*2, ~ sd")
* @example
* samples('shabda:noise,chimp:2')
* s("noise <chimp:0*2 chimp:1>")
* @example
* samples('shabda/speech/fr-FR/f:chocolat')
* s("chocolat*4")
*/
export const samples = async (sampleMap, baseUrl = sampleMap._base || '', options = {}) => {
@@ -323,10 +329,10 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
const out = ac.createGain(); // we need a separate gain for the cutgroups because firefox...
node.connect(out);
onceEnded(bufferSource, function () {
releaseAudioNode(bufferSource);
releaseAudioNode(vibratoOscillator);
releaseAudioNode(node);
releaseAudioNode(out);
bufferSource.disconnect();
vibratoOscillator?.stop();
node.disconnect();
out.disconnect();
onended();
});
let envEnd = holdEnd + release + 0.01;
+22 -32
View File
@@ -11,14 +11,14 @@ import { nanFallback, _mod, cycleToSeconds, pickAndRename } from './util.mjs';
import workletsUrl from './worklets.mjs?audioworklet';
import {
createFilter,
effectSend,
gainNode,
getCompressor,
getDistortion,
getLfo,
getWorklet,
effectSend,
releaseAudioNode,
getPooledWorklet,
} from './helpers.mjs';
import { getNodeFromPool, isPoolable, releaseNodeToPool } from './nodePools.mjs';
import { map } from 'nanostores';
import { logger } from './logger.mjs';
import { loadBuffer } from './sampler.mjs';
@@ -299,11 +299,11 @@ function getPhaser(time, end, frequency = 1, depth = 0.5, centerFrequency = 1000
const lfo = getLfo(ac, time, end, { frequency, depth: sweep * 2 });
//filters
const numStages = 1; //num of filters in series
let fOffset = 282; //for backward compat in #1800
const numStages = 2; //num of filters in series
let fOffset = 0;
const filterChain = [];
for (let i = 0; i < numStages; i++) {
const filter = ac.createBiquadFilter();
const filter = getNodeFromPool('filter', () => ac.createBiquadFilter());
filter.type = 'notch';
filter.gain.value = 1;
filter.frequency.value = centerFrequency + fOffset;
@@ -311,9 +311,12 @@ function getPhaser(time, end, frequency = 1, depth = 0.5, centerFrequency = 1000
lfo.connect(filter.detune);
fOffset += 282;
if (i > 0) {
filterChain[i - 1].connect(filter);
}
filterChain.push(filter);
}
return { filterChain, lfo };
return { phaser: filterChain[filterChain.length - 1], lfo };
}
function getFilterType(ftype) {
@@ -457,8 +460,6 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
compressorKnee,
compressorAttack,
compressorRelease,
transient,
transsustain,
} = value;
delaytime = delaytime ?? cycleToSeconds(delaysync, cps);
@@ -514,7 +515,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
} else if (getSound(s)) {
const { onTrigger } = getSound(s);
const onEnded = () => {
audioNodes.forEach((n) => releaseAudioNode(n));
audioNodes.forEach((n) => (isPoolable(n) ? releaseNodeToPool(n) : n?.disconnect()));
activeSoundSources.delete(chainID);
};
const soundHandle = await onTrigger(t, value, onEnded, cps);
@@ -538,23 +539,9 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
}
const chain = []; // audio nodes that will be connected to each other sequentially
chain.push(sourceNode);
stretch !== undefined && chain.push(getWorklet(ac, 'phase-vocoder-processor', { pitchFactor: stretch }));
transient !== undefined &&
stretch !== undefined &&
chain.push(
getWorklet(
ac,
'transient-processor',
{},
{
processorOptions: {
attack: transient,
sustain: transsustain,
begin: t,
end: endWithRelease,
},
},
),
getPooledWorklet(ac, 'phase-vocoder-processor', { pitchFactor: stretch, begin: t, end: endWithRelease }),
);
// gain stage
@@ -668,10 +655,13 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
}
// effects
coarse !== undefined && chain.push(getWorklet(ac, 'coarse-processor', { coarse }));
crush !== undefined && chain.push(getWorklet(ac, 'crush-processor', { crush }));
shape !== undefined && chain.push(getWorklet(ac, 'shape-processor', { shape, postgain: shapevol }));
distort !== undefined && chain.push(getDistortion(distort, distortvol, distorttype));
coarse !== undefined &&
chain.push(getPooledWorklet(ac, 'coarse-processor', { coarse, begin: t, end: endWithRelease }));
crush !== undefined && chain.push(getPooledWorklet(ac, 'crush-processor', { crush, begin: t, end: endWithRelease }));
shape !== undefined &&
chain.push(getPooledWorklet(ac, 'shape-processor', { shape, postgain: shapevol, begin: t, end: endWithRelease }));
distort !== undefined &&
chain.push(getDistortion({ distort, postgain: distortvol, algorithm: distorttype, begin: t, end: endWithRelease }));
if (tremolosync != null) {
tremolo = cps * tremolosync;
@@ -722,9 +712,9 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
}
// phaser
if (phaserrate !== undefined && phaserdepth > 0) {
const { filterChain, lfo } = getPhaser(t, endWithRelease, phaserrate, phaserdepth, phasercenter, phasersweep);
const { phaser, lfo } = getPhaser(t, endWithRelease, phaserrate, phaserdepth, phasercenter, phasersweep);
audioNodes.push(lfo);
chain.push(...filterChain);
chain.push(phaser);
}
// last gain
+9
View File
@@ -1,3 +1,12 @@
/*
superdoughoutput.mjs - Output controller for superdough
Handles setting up and mixing to the outputs as well as all global (orbit) effects
Copyright (C) 2025 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/superdough/superdoughoutput.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/>.
*/
import { effectSend, getWorklet, webAudioTimeout } from './helpers.mjs';
import { errorLogger } from './logger.mjs';
import { clamp } from './util.mjs';
+30 -39
View File
@@ -9,15 +9,15 @@ import {
getLfo,
getParamADSR,
getPitchEnvelope,
getPooledWorklet,
getVibratoOscillator,
getWorklet,
noises,
onceEnded,
releaseAudioNode,
webAudioTimeout,
} from './helpers.mjs';
import { logger } from './logger.mjs';
import { getNoiseMix, getNoiseOscillator } from './noise.mjs';
import { releaseNodeToPool } from './nodePools.mjs';
const waveforms = ['triangle', 'square', 'sawtooth', 'sine', 'user'];
const waveformAliases = [
@@ -53,7 +53,7 @@ export function registerSynthSounds() {
const g = gainNode(0.3);
let sound = getOscillator(s, t, value, () => {
releaseAudioNode(g);
g.disconnect();
onended();
});
@@ -111,15 +111,15 @@ export function registerSynthSounds() {
const mix = gainNode(mixGain);
onceEnded(o, () => {
releaseAudioNode(o);
releaseAudioNode(g);
releaseAudioNode(sat);
releaseAudioNode(noise.node);
releaseAudioNode(noiseGain);
releaseAudioNode(mix);
o.onended = () => {
o.disconnect();
g.disconnect();
sat.disconnect();
noise.node.disconnect();
noiseGain.disconnect();
mix.disconnect();
onended();
});
};
const node = o.connect(sat).connect(g).connect(mix);
noise.node.connect(noiseGain).connect(mix);
@@ -165,22 +165,14 @@ export function registerSynthSounds() {
const end = holdend + release + 0.01;
const voices = clamp(unison, 1, 100);
let panspread = voices > 1 ? clamp(spread, 0, 1) : 0;
let o = getWorklet(
ac,
'supersaw-oscillator',
{
frequency,
begin,
end,
freqspread: detune,
voices,
panspread,
},
{
outputChannelCount: [2],
},
);
const params = {
frequency,
begin,
end,
freqspread: detune,
panspread,
};
const o = getPooledWorklet(ac, 'supersaw-oscillator', params, { outputChannelCount: [2] }, { voices });
const gainAdjustment = 1 / Math.sqrt(voices);
getPitchEnvelope(o.parameters.get('detune'), value, begin, holdend);
const vibratoOscillator = getVibratoOscillator(o.parameters.get('detune'), value, begin);
@@ -193,7 +185,7 @@ export function registerSynthSounds() {
let timeoutNode = webAudioTimeout(
ac,
() => {
releaseAudioNode(o);
releaseNodeToPool(o);
onended();
fm?.stop();
vibratoOscillator?.stop();
@@ -247,7 +239,7 @@ export function registerSynthSounds() {
const holdend = begin + duration;
const end = holdend + release + 0.01;
let o = getWorklet(
let o = getPooledWorklet(
ac,
'byte-beat-processor',
{
@@ -314,7 +306,7 @@ export function registerSynthSounds() {
);
const holdend = begin + duration;
const end = holdend + release + 0.01;
let o = getWorklet(
let o = getPooledWorklet(
ac,
'pulse-oscillator',
{
@@ -385,11 +377,11 @@ export function registerSynthSounds() {
const { duration } = value;
onceEnded(o, () => {
releaseAudioNode(o);
releaseAudioNode(g);
o.onended = () => {
o.disconnect();
g.disconnect();
onended();
});
};
const envGain = gainNode(1);
let node = o.connect(g).connect(envGain);
@@ -490,12 +482,11 @@ export function getOscillator(s, t, value, onended) {
noiseMix = getNoiseMix(o, noise, t);
}
onceEnded(o, () => {
noiseMix?.teardown();
releaseAudioNode(o);
releaseAudioNode(noiseMix?.node);
o.onended = () => {
noiseMix || o.disconnect();
noiseMix?.node.disconnect();
onended();
});
};
o.start(t);
return {
+19 -24
View File
@@ -7,11 +7,11 @@ import {
getFrequencyFromValue,
getParamADSR,
getPitchEnvelope,
getPooledWorklet,
getVibratoOscillator,
getWorklet,
releaseAudioNode,
webAudioTimeout,
} from './helpers.mjs';
import { releaseNodeToPool } from './nodePools.mjs';
import { logger } from './logger.mjs';
export const Warpmode = Object.freeze({
@@ -225,24 +225,19 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) {
}
const endWithRelease = holdEnd + release;
const envEnd = endWithRelease + 0.01;
const source = getWorklet(
ac,
'wavetable-oscillator-processor',
{
begin: t,
end: envEnd,
frequency,
freqspread: value.detune,
position: value.wt,
warp: value.warp,
warpMode: warpmode,
voices: Math.max(value.unison ?? 1, 1),
panspread: value.spread,
phaserand: (value.wtphaserand ?? value.unison > 1) ? 1 : 0,
},
{ outputChannelCount: [2] },
);
source.port.postMessage({ type: 'table', payload });
payload.voices = Math.max(value.unison ?? 1, 1);
const params = {
begin: t,
end: envEnd,
frequency,
freqspread: value.detune,
position: value.wt,
warp: value.warp,
warpMode: warpmode,
panspread: value.spread,
phaserand: (value.wtphaserand ?? value.unison > 1) ? 1 : 0,
};
const source = getPooledWorklet(ac, 'wavetable-oscillator-processor', params, { outputChannelCount: [2] }, payload);
if (ac.currentTime > t) {
logger(`[wavetable] still loading sound "${s}:${n}"`, 'highlight');
return;
@@ -319,11 +314,11 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) {
const timeoutNode = webAudioTimeout(
ac,
() => {
releaseAudioNode(source);
releaseAudioNode(vibratoOscillator);
releaseNodeToPool(source);
vibratoOscillator?.stop();
fm?.stop();
releaseAudioNode(wtPosModulators);
releaseAudioNode(wtWarpModulators);
wtPosModulators?.disconnect();
wtWarpModulators?.disconnect();
onended();
},
t,
+70
View File
@@ -0,0 +1,70 @@
/*
worklets-common.mjs - Common worklet code
Copyright (C) 2025 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/superdough/worklets-common.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/>.
*/
// Mixin to make class "reusable" (i.e. have a begin and end and properly handle `initialize` messages)
export const makeReusable = (Base) =>
class extends Base {
static get parameterDescriptors() {
return [
...(super.parameterDescriptors ?? []),
{
name: 'begin',
defaultValue: 0,
min: 0,
max: Number.POSITIVE_INFINITY,
automationRate: 'k-rate',
},
{
name: 'end',
defaultValue: 0,
min: 0,
max: Number.POSITIVE_INFINITY,
automationRate: 'k-rate',
},
];
}
constructor(options) {
super(options);
this.isAlive = true;
this.graceSeconds = options?.processorOptions?.graceSeconds ?? 0.5;
this.port.onmessage = (e) => {
const { type, payload } = e.data || {};
if (type === 'initialize') {
this.initialize(payload);
} else if (this.handlePortMessage) {
this.handlePortMessage(type, payload, e);
}
};
}
initialize(_options) {
// defined on subclasses
}
processActive(_inputs, _outputs, _params) {
// processing subclasses should do when we're between begin/end
return true;
}
process(inputs, outputs, params) {
const begin = params.begin[0];
const end = params.end[0];
if (currentTime >= end + this.graceSeconds) {
if (this.isAlive) {
this.port.postMessage({ type: 'died' });
this.isAlive = false;
}
return false;
}
if (currentTime < begin || currentTime >= end) {
return true;
}
return this.processActive(inputs, outputs, params);
}
};
+88 -356
View File
@@ -1,19 +1,17 @@
// coarse, crush, and shape processors adapted from dktr0's webdirt: https://github.com/dktr0/WebDirt/blob/5ce3d698362c54d6e1b68acc47eb2955ac62c793/dist/AudioWorklets.js
// LICENSE GNU General Public License v3.0 see https://github.com/dktr0/WebDirt/blob/main/LICENSE
// TOFIX: THIS FILE DOES NOT SUPPORT IMPORTS ON DEPOLYMENT
// TOFIX: THIS FILE DOES NOT SUPPORT IMPORTS ON DEPLOYMENT
import OLAProcessor from './ola-processor';
import FFT from './fft.js';
import { getDistortionAlgorithm } from './helpers.mjs';
import { makeReusable } from './worklets-common.mjs';
const blockSize = 128;
const PI = Math.PI;
const TWO_PI = 2 * PI;
const INVSR = 1 / sampleRate;
const timeToCoeff = (t) => 1 - Math.exp(-INVSR / t);
const dbToLin = (db) => Math.pow(10, db / 20);
const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
const mod = (n, m) => ((n % m) + m) % m;
const lerp = (a, b, n) => n * (b - a) + a;
@@ -111,12 +109,12 @@ const waveshapes = {
};
const waveShapeNames = Object.keys(waveshapes);
class LFOProcessor extends AudioWorkletProcessor {
class LFOProcessor extends makeReusable(AudioWorkletProcessor) {
static get parameterDescriptors() {
return [
{ name: 'begin', defaultValue: 0 },
...super.parameterDescriptors,
{ name: 'time', defaultValue: 0 },
{ name: 'end', defaultValue: 0 },
{ name: 'frequency', defaultValue: 0.5 },
{ name: 'skew', defaultValue: 0.5 },
{ name: 'depth', defaultValue: 1 },
@@ -129,9 +127,8 @@ class LFOProcessor extends AudioWorkletProcessor {
];
}
constructor() {
super();
this.phase;
initialize() {
this.phase = null;
}
incrementPhase(dt) {
@@ -141,15 +138,7 @@ class LFOProcessor extends AudioWorkletProcessor {
}
}
process(_inputs, outputs, parameters) {
const begin = parameters['begin'][0];
if (currentTime >= parameters.end[0]) {
return false;
}
if (currentTime <= begin) {
return true;
}
processActive(_inputs, outputs, parameters) {
const output = outputs[0];
const frequency = parameters['frequency'][0];
@@ -185,26 +174,14 @@ class LFOProcessor extends AudioWorkletProcessor {
}
registerProcessor('lfo-processor', LFOProcessor);
class CoarseProcessor extends AudioWorkletProcessor {
class CoarseProcessor extends makeReusable(AudioWorkletProcessor) {
static get parameterDescriptors() {
return [{ name: 'coarse', defaultValue: 1 }];
return [...super.parameterDescriptors, { name: 'coarse', defaultValue: 1 }];
}
constructor() {
super();
this.started = false;
}
process(inputs, outputs, parameters) {
processActive(inputs, outputs, parameters) {
const input = inputs[0];
const output = outputs[0];
const hasInput = !(input[0] === undefined);
if (this.started && !hasInput) {
return false;
}
this.started = hasInput;
let coarse = parameters.coarse[0] ?? 0;
coarse = Math.max(1, coarse);
for (let n = 0; n < blockSize; n++) {
@@ -217,26 +194,14 @@ class CoarseProcessor extends AudioWorkletProcessor {
}
registerProcessor('coarse-processor', CoarseProcessor);
class CrushProcessor extends AudioWorkletProcessor {
class CrushProcessor extends makeReusable(AudioWorkletProcessor) {
static get parameterDescriptors() {
return [{ name: 'crush', defaultValue: 0 }];
return [...super.parameterDescriptors, { name: 'crush', defaultValue: 0 }];
}
constructor() {
super();
this.started = false;
}
process(inputs, outputs, parameters) {
processActive(inputs, outputs, parameters) {
const input = inputs[0];
const output = outputs[0];
const hasInput = !(input[0] === undefined);
if (this.started && !hasInput) {
return false;
}
this.started = hasInput;
let crush = parameters.crush[0] ?? 8;
crush = Math.max(1, crush);
@@ -251,29 +216,14 @@ class CrushProcessor extends AudioWorkletProcessor {
}
registerProcessor('crush-processor', CrushProcessor);
class ShapeProcessor extends AudioWorkletProcessor {
class ShapeProcessor extends makeReusable(AudioWorkletProcessor) {
static get parameterDescriptors() {
return [
{ name: 'shape', defaultValue: 0 },
{ name: 'postgain', defaultValue: 1 },
];
return [...super.parameterDescriptors, { name: 'shape', defaultValue: 0 }, { name: 'postgain', defaultValue: 1 }];
}
constructor() {
super();
this.started = false;
}
process(inputs, outputs, parameters) {
processActive(inputs, outputs, parameters) {
const input = inputs[0];
const output = outputs[0];
const hasInput = !(input[0] === undefined);
if (this.started && !hasInput) {
return false;
}
this.started = hasInput;
let shape = parameters.shape[0];
shape = shape < 1 ? shape : 1.0 - 4e-10;
shape = (2.0 * shape) / (1.0 - shape);
@@ -357,17 +307,17 @@ class DJFProcessor extends AudioWorkletProcessor {
registerProcessor('djf-processor', DJFProcessor);
//adapted from https://github.com/TheBouteillacBear/webaudioworklet-wasm?tab=MIT-1-ov-file
class LadderProcessor extends AudioWorkletProcessor {
class LadderProcessor extends makeReusable(AudioWorkletProcessor) {
static get parameterDescriptors() {
return [
...super.parameterDescriptors,
{ name: 'frequency', defaultValue: 500 },
{ name: 'q', defaultValue: 1 },
{ name: 'drive', defaultValue: 0.69 },
];
}
constructor() {
super();
initialize(_options) {
this.started = false;
this.p0 = [0, 0];
this.p1 = [0, 0];
@@ -378,17 +328,9 @@ class LadderProcessor extends AudioWorkletProcessor {
this.p34 = [0, 0];
}
process(inputs, outputs, parameters) {
processActive(inputs, outputs, parameters) {
const input = inputs[0];
const output = outputs[0];
const hasInput = !(input[0] === undefined);
if (this.started && !hasInput) {
return false;
}
this.started = hasInput;
const resonance = parameters.q[0];
const drive = clamp(Math.exp(parameters.drive[0]), 0.1, 2000);
@@ -421,29 +363,18 @@ class LadderProcessor extends AudioWorkletProcessor {
}
registerProcessor('ladder-processor', LadderProcessor);
class DistortProcessor extends AudioWorkletProcessor {
class DistortProcessor extends makeReusable(AudioWorkletProcessor) {
static get parameterDescriptors() {
return [
{ name: 'distort', defaultValue: 0 },
{ name: 'postgain', defaultValue: 1 },
];
return [...super.parameterDescriptors, { name: 'distort', defaultValue: 0 }, { name: 'postgain', defaultValue: 1 }];
}
constructor({ processorOptions }) {
super();
this.started = false;
this.algorithm = getDistortionAlgorithm(processorOptions.algorithm);
initialize(options) {
this.algorithm = getDistortionAlgorithm(options?.algorithm);
}
process(inputs, outputs, parameters) {
processActive(inputs, outputs, parameters) {
const input = inputs[0];
const output = outputs[0];
const hasInput = !(input[0] === undefined);
if (this.started && !hasInput) {
return false;
}
this.started = hasInput;
for (let n = 0; n < blockSize; n++) {
const postgain = clamp(pv(parameters.postgain, n), 0.001, 1);
const shape = Math.expm1(pv(parameters.distort, n));
@@ -458,33 +389,15 @@ class DistortProcessor extends AudioWorkletProcessor {
registerProcessor('distort-processor', DistortProcessor);
// SUPERSAW
class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
constructor() {
super();
this.phase = [];
}
class SuperSawOscillatorProcessor extends makeReusable(AudioWorkletProcessor) {
static get parameterDescriptors() {
return [
{
name: 'begin',
defaultValue: 0,
max: Number.POSITIVE_INFINITY,
min: 0,
},
{
name: 'end',
defaultValue: 0,
max: Number.POSITIVE_INFINITY,
min: 0,
},
...super.parameterDescriptors,
{
name: 'frequency',
defaultValue: 440,
min: Number.EPSILON,
},
{
name: 'panspread',
defaultValue: 0.4,
@@ -501,26 +414,14 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
defaultValue: 0,
min: 0,
},
{
name: 'voices',
defaultValue: 5,
min: 1,
automationRate: 'k-rate',
},
];
}
process(_input, outputs, params) {
if (currentTime >= params.end[0]) {
// should terminate
return false;
}
if (currentTime <= params.begin[0]) {
// keep alive
return true;
}
initialize(options) {
this.phase = [];
this.voices = options?.voices ?? 5;
}
processActive(_input, outputs, params) {
const output = outputs[0];
const voices = params.voices[0]; // k-rate
for (let i = 0; i < output[0].length; i++) {
const detune = pv(params.detune, i);
const freqspread = pv(params.freqspread, i);
@@ -530,8 +431,8 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
let freq = pv(params.frequency, i);
// Main detuning
freq = applySemitoneDetuneToFrequency(freq, detune / 100);
const detuner = getDetuner(voices, freqspread);
for (let n = 0; n < voices; n++) {
const detuner = getDetuner(this.voices, freqspread);
for (let n = 0; n < this.voices; n++) {
// Individual voice detuning
const freqVoice = applySemitoneDetuneToFrequency(freq, detuner(n));
// We must wrap this here because it is passed into sawblep below which
@@ -575,12 +476,7 @@ function genHannWindow(length) {
class PhaseVocoderProcessor extends OLAProcessor {
static get parameterDescriptors() {
return [
{
name: 'pitchFactor',
defaultValue: 1.0,
},
];
return [...super.parameterDescriptors, { name: 'pitchFactor', defaultValue: 1.0 }];
}
constructor(options) {
@@ -588,9 +484,13 @@ class PhaseVocoderProcessor extends OLAProcessor {
blockSize: BUFFERED_BLOCK_SIZE,
};
super(options);
this.timeCursor = 0;
this.fftSize = this.blockSize;
this.invfftSize = 1 / this.fftSize;
}
initialize(options) {
super.initialize(options);
this.timeCursor = 0;
this.hannWindow = genHannWindow(this.fftSize);
// prepare FFT and pre-allocate buffers
this.fft = new FFT(this.fftSize);
@@ -721,9 +621,8 @@ class PhaseVocoderProcessor extends OLAProcessor {
registerProcessor('phase-vocoder-processor', PhaseVocoderProcessor);
// Adapted from https://www.musicdsp.org/en/latest/Effects/221-band-limited-pwm-generator.html
class PulseOscillatorProcessor extends AudioWorkletProcessor {
constructor() {
super();
class PulseOscillatorProcessor extends makeReusable(AudioWorkletProcessor) {
initialize() {
this.phi = -PI; // phase
this.Y0 = 0; // feedback memories
this.Y1 = 0;
@@ -735,20 +634,7 @@ class PulseOscillatorProcessor extends AudioWorkletProcessor {
static get parameterDescriptors() {
return [
{
name: 'begin',
defaultValue: 0,
max: Number.POSITIVE_INFINITY,
min: 0,
},
{
name: 'end',
defaultValue: 0,
max: Number.POSITIVE_INFINITY,
min: 0,
},
...super.parameterDescriptors,
{
name: 'frequency',
defaultValue: 440,
@@ -769,20 +655,10 @@ class PulseOscillatorProcessor extends AudioWorkletProcessor {
];
}
process(inputs, outputs, params) {
if (this.disconnected) {
return false;
}
if (currentTime <= params.begin[0]) {
return true;
}
if (currentTime >= params.end[0]) {
return false;
}
processActive(inputs, outputs, params) {
const output = outputs[0];
let env = 1,
dphi;
for (let i = 0; i < (output[0].length ?? 0); i++) {
const pw = (1 - clamp(pv(params.pulsewidth, i), -0.99, 0.99)) * PI;
const detune = pv(params.detune, i);
@@ -865,28 +741,23 @@ function getByteBeatFunc(codetext) {
return new Function(...mathParams, 't', `return 0,\n${codetext || 0};`).bind(globalThis, ...byteBeatHelperFuncs);
}
class ByteBeatProcessor extends AudioWorkletProcessor {
constructor() {
super();
this.port.onmessage = (event) => {
let { codeText } = event.data;
const { byteBeatStartTime } = event.data;
if (byteBeatStartTime != null) {
this.t = 0;
this.initialOffset = Math.floor(byteBeatStartTime);
}
//Optimization pulled from dollchan.net: https://github.com/Chasyxx/EnBeat_NEW, it seemed important
//Optimize code like eval(unescape(escape`XXXX`.replace(/u(..)/g,"$1%")))
codeText = codeText
.trim()
.replace(
/^eval\(unescape\(escape(?:`|\('|\("|\(`)(.*?)(?:`|'\)|"\)|`\)).replace\(\/u\(\.\.\)\/g,["'`]\$1%["'`]\)\)\)$/,
(match, m1) => unescape(escape(m1).replace(/u(..)/g, '$1%')),
);
this.func = getByteBeatFunc(codeText);
};
class ByteBeatProcessor extends makeReusable(AudioWorkletProcessor) {
initialize(options) {
let codeText = options?.codeText;
const byteBeatStartTime = options?.byteBeatStartTime;
if (byteBeatStartTime != null) {
this.t = 0;
this.initialOffset = Math.floor(byteBeatStartTime);
}
//Optimization pulled from dollchan.net: https://github.com/Chasyxx/EnBeat_NEW, it seemed important
//Optimize code like eval(unescape(escape`XXXX`.replace(/u(..)/g,"$1%")))
codeText = codeText
.trim()
.replace(
/^eval\(unescape\(escape(?:`|\('|\("|\(`)(.*?)(?:`|'\)|"\)|`\)).replace\(\/u\(\.\.\)\/g,["'`]\$1%["'`]\)\)\)$/,
(match, m1) => unescape(escape(m1).replace(/u(..)/g, '$1%')),
);
this.func = getByteBeatFunc(codeText);
this.initialOffset = 0;
this.t = null;
this.func = null;
@@ -894,12 +765,7 @@ class ByteBeatProcessor extends AudioWorkletProcessor {
static get parameterDescriptors() {
return [
{
name: 'begin',
defaultValue: 0,
max: Number.POSITIVE_INFINITY,
min: 0,
},
...super.parameterDescriptors,
{
name: 'frequency',
defaultValue: 440,
@@ -911,25 +777,10 @@ class ByteBeatProcessor extends AudioWorkletProcessor {
min: Number.NEGATIVE_INFINITY,
max: Number.POSITIVE_INFINITY,
},
{
name: 'end',
defaultValue: 0,
max: Number.POSITIVE_INFINITY,
min: 0,
},
];
}
process(inputs, outputs, params) {
if (this.disconnected) {
return false;
}
if (currentTime <= params.begin[0]) {
return true;
}
if (currentTime >= params.end[0]) {
return false;
}
processActive(inputs, outputs, params) {
if (this.t == null) {
this.t = params.begin[0] * sampleRate;
}
@@ -955,11 +806,9 @@ class ByteBeatProcessor extends AudioWorkletProcessor {
registerProcessor('byte-beat-processor', ByteBeatProcessor);
class EnvelopeProcessor extends AudioWorkletProcessor {
class EnvelopeProcessor extends makeReusable(AudioWorkletProcessor) {
static get parameterDescriptors() {
return [
{ name: 'begin', defaultValue: 0 },
{ name: 'end', defaultValue: 0 },
{ name: 'attack', defaultValue: 0.005, minValue: 0 },
{ name: 'decay', defaultValue: 0.14, minValue: 0 },
{ name: 'sustain', defaultValue: 0, minValue: 0, maxValue: 1 },
@@ -1119,56 +968,36 @@ function brownian(x, oct = 4) {
}
const tablesCache = {};
class WavetableOscillatorProcessor extends AudioWorkletProcessor {
class WavetableOscillatorProcessor extends makeReusable(AudioWorkletProcessor) {
static get parameterDescriptors() {
return [
{ name: 'begin', defaultValue: 0, min: 0, max: Number.POSITIVE_INFINITY },
{ name: 'end', defaultValue: 0, min: 0, max: Number.POSITIVE_INFINITY },
...super.parameterDescriptors,
{ name: 'frequency', defaultValue: 440, min: Number.EPSILON },
{ name: 'detune', defaultValue: 0 },
{ name: 'freqspread', defaultValue: 0.18, min: 0 },
{ name: 'position', defaultValue: 0, min: 0, max: 1 },
{ name: 'warp', defaultValue: 0, min: 0, max: 1 },
{ name: 'warpMode', defaultValue: 0 },
{ name: 'voices', defaultValue: 1, min: 1, automationRate: 'k-rate' },
{ name: 'panspread', defaultValue: 0.7, min: 0, max: 1 },
{ name: 'phaserand', defaultValue: 0, min: 0, max: 1 },
];
}
constructor(options) {
super(options);
this.frameLen = 0;
this.numFrames = 0;
initialize(options) {
this.table = null;
this.frameLen = null;
this.numFrames = null;
this.phase = [];
this.port.onmessage = (e) => {
const { type, payload } = e.data || {};
if (type === 'table') {
const key = payload.key;
this.frameLen = payload.frameLen;
if (!tablesCache[key]) {
const tables = [payload.frames];
let table = tables[0];
for (let level = 1; level < 1; level++) {
const nextLen = table.length >> 1;
const nextTable = table.map((frame) => {
const avg = new Float32Array(nextLen);
for (let i = 0; i < nextLen; i++) {
avg[i] = (frame[2 * i] + frame[2 * i + 1]) / 2;
}
return avg;
});
tables.push(nextTable);
table = nextTable;
if (nextLen <= 32) break;
}
tablesCache[key] = tables;
}
this.tables = tablesCache[key];
this.numFrames = this.tables[0].length;
if (options?.key) {
const key = options.key;
this.frameLen = options.frameLen;
if (!tablesCache[key]) {
tablesCache[key] = options.frames;
}
};
this.table = tablesCache[key];
this.numFrames = this.table.length;
}
this.voices = options?.voices ?? 1;
}
_mirror(x) {
@@ -1313,30 +1142,14 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
return a + (b - a) * frac;
}
_chooseMip(dphi) {
const approxHarm = clamp(dphi, 1e-6, 64);
let level = 0;
while (level + 1 < (this.tables?.length || 1) && approxHarm < this.tables[level][0].length / 8) {
level++;
}
return level;
}
process(_inputs, outputs, parameters) {
if (currentTime >= parameters.end[0]) {
return false;
}
if (currentTime <= parameters.begin[0]) {
return true;
}
processActive(_inputs, outputs, parameters) {
const outL = outputs[0][0];
const outR = outputs[0][1] || outputs[0][0];
if (!this.tables) {
if (!this.table) {
outL.fill(0);
if (outR !== outL) outR.set(outL);
return true;
}
const voices = parameters.voices[0]; // k-rate
for (let i = 0; i < outL.length; i++) {
const detune = pv(parameters.detune, i);
const freqspread = pv(parameters.freqspread, i);
@@ -1347,14 +1160,14 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
const warpAmount = clamp(pv(parameters.warp, i), 0, 1);
const warpMode = pv(parameters.warpMode, i);
const phaseRand = clamp(pv(parameters.phaserand, i), 0, 1);
const panspread = voices > 1 ? clamp(pv(parameters.panspread, i), 0, 1) : 0;
const panspread = this.voices > 1 ? clamp(pv(parameters.panspread, i), 0, 1) : 0;
const gain1 = Math.sqrt(0.5 - 0.5 * panspread);
const gain2 = Math.sqrt(0.5 + 0.5 * panspread);
let f = pv(parameters.frequency, i);
f = applySemitoneDetuneToFrequency(f, detune / 100); // overall detune
const normalizer = 1 / Math.sqrt(voices);
const detuner = getDetuner(voices, freqspread);
for (let n = 0; n < voices; n++) {
const normalizer = 1 / Math.sqrt(this.voices);
const detuner = getDetuner(this.voices, freqspread);
for (let n = 0; n < this.voices; n++) {
const isOdd = (n & 1) == 1;
let gainL = gain1;
let gainR = gain2;
@@ -1365,14 +1178,12 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
}
const fVoice = applySemitoneDetuneToFrequency(f, detuner(n)); // voice detune
const dPhase = fVoice * INVSR;
const level = this._chooseMip(dPhase);
const table = this.tables[level];
// warp phase then sample
this.phase[n] = this.phase[n] ?? Math.random() * phaseRand;
const ph = this._warpPhase(this.phase[n], warpAmount, warpMode);
const s0 = this._sampleFrame(table[fIdx], ph);
const s1 = this._sampleFrame(table[Math.min(this.numFrames - 1, fIdx + 1)], ph);
const s0 = this._sampleFrame(this.table[fIdx], ph);
const s1 = this._sampleFrame(this.table[Math.min(this.numFrames - 1, fIdx + 1)], ph);
let s = lerp(s0, s1, interpT);
if (warpMode === WarpMode.FLIP && this.phase[n] < warpAmount) {
s = -s;
@@ -1387,82 +1198,3 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
}
registerProcessor('wavetable-oscillator-processor', WavetableOscillatorProcessor);
class TransientProcessor extends AudioWorkletProcessor {
static get parameterDescriptors() {
return [];
}
constructor(options) {
super();
this.gainCoeff = timeToCoeff(0.2);
this.avgGain = 1;
let {
attackTime = 0.003,
sustainTime = 0.08,
attack = 0,
sustain = 0,
sensitivity = 0.1,
mix = 1,
begin = 0,
end = 0,
} = options.processorOptions;
attackTime = clamp(attackTime, 0.0005, 0.05);
sustainTime = clamp(sustainTime, 0.01, 0.5);
this.attackCoeff = timeToCoeff(attackTime);
this.sustainCoeff = timeToCoeff(sustainTime);
this.attackAmt = clamp(attack, -1, 1);
this.sustainAmt = clamp(sustain, -1, 1);
this.scaling = 0.5 + 5 * clamp(sensitivity, 0, 1);
this.mix = clamp(mix, 0, 1);
this.begin = begin;
this.end = end;
this.attackEnv = new Float32Array(2); // assume stereo
this.sustainEnv = new Float32Array(2);
}
process(inputs, outputs, _params) {
const input = inputs[0];
const output = outputs[0];
if (currentTime >= this.end) {
return false;
}
if (currentTime <= this.begin) {
return true;
}
const channels = input.length;
if (channels > this.attackEnv.length) {
this.attackEnv = new Float32Array(channels);
this.sustainEnv = new Float32Array(channels);
}
let avgGain = this.avgGain;
for (let ch = 0; ch < channels; ch++) {
let attEnv = this.attackEnv[ch];
let susEnv = this.sustainEnv[ch];
for (let n = 0; n < blockSize; n++) {
const sample = input[ch][n];
const x = Math.abs(sample);
attEnv = lerp(attEnv, x, this.attackCoeff);
susEnv = lerp(susEnv, x, this.sustainCoeff);
const peakiness = clamp((this.scaling * (attEnv - susEnv)) / (susEnv + 1e-6), -1.5, 1.5);
const attScale = peakiness > 0 ? peakiness : 0;
const susScale = peakiness < 0 ? -peakiness : 0;
const attackGain = dbToLin(this.attackAmt * attScale * 18);
const sustainGain = dbToLin(this.sustainAmt * susScale * 36);
const gain = clamp(attackGain * sustainGain, 0, 8);
avgGain = lerp(avgGain, gain, this.gainCoeff);
const makeup = avgGain > 1e-3 ? 1 / avgGain : 1;
const wet = sample * gain * makeup;
let y = lerp(sample, wet, this.mix);
y /= 1 + Math.abs(y); // soft clip
output[ch][n] = y;
}
this.attackEnv[ch] = attEnv;
this.sustainEnv[ch] = susEnv;
}
this.avgGain = avgGain;
return true;
}
}
registerProcessor('transient-processor', TransientProcessor);
+36 -392
View File
@@ -2539,84 +2539,6 @@ exports[`runs examples > example "cut" example index 0 1`] = `
]
`;
exports[`runs examples > example "cyclesPer" example index 0 1`] = `
[
"[ 0/1 → 1/4 | s:saw note:62.5 ]",
"[ 1/4 → 1/2 | s:saw note:62.5 ]",
"[ 1/2 → 5/8 | s:saw note:56.25 ]",
"[ 5/8 → 3/4 | s:saw note:56.25 ]",
"[ 3/4 → 1/1 | s:saw note:62.5 ]",
"[ 1/1 → 5/4 | s:saw note:62.5 ]",
"[ 5/4 → 3/2 | s:saw note:62.5 ]",
"[ 3/2 → 13/8 | s:saw note:56.25 ]",
"[ 13/8 → 7/4 | s:saw note:56.25 ]",
"[ 7/4 → 2/1 | s:saw note:62.5 ]",
"[ 2/1 → 9/4 | s:saw note:62.5 ]",
"[ 9/4 → 5/2 | s:saw note:62.5 ]",
"[ 5/2 → 21/8 | s:saw note:56.25 ]",
"[ 21/8 → 11/4 | s:saw note:56.25 ]",
"[ 11/4 → 3/1 | s:saw note:62.5 ]",
"[ 3/1 → 13/4 | s:saw note:62.5 ]",
"[ 13/4 → 7/2 | s:saw note:62.5 ]",
"[ 7/2 → 29/8 | s:saw note:56.25 ]",
"[ 29/8 → 15/4 | s:saw note:56.25 ]",
"[ 15/4 → 4/1 | s:saw note:62.5 ]",
]
`;
exports[`runs examples > example "cyclesPer" example index 1 1`] = `
[
"[ 0/1 → 1/6 | s:bd note:20.166666666666668 ]",
"[ 1/6 → 1/3 | s:sd note:20.166666666666668 ]",
"[ 1/3 → 5/12 | s:bd note:20.083333333333332 ]",
"[ 5/12 → 1/2 | s:bd note:20.083333333333332 ]",
"[ 1/2 → 13/24 | s:sd note:20.041666666666668 ]",
"[ 13/24 → 7/12 | s:sd note:20.041666666666668 ]",
"[ 7/12 → 5/8 | s:sd note:20.041666666666668 ]",
"[ 5/8 → 2/3 | s:sd note:20.041666666666668 ]",
"[ 3/4 → 5/6 | s:sd note:20.083333333333332 ]",
"[ 5/6 → 11/12 | s:bd note:20.083333333333332 ]",
"[ 11/12 → 23/24 | s:bd note:20.041666666666668 ]",
"[ 23/24 → 1/1 | s:bd note:20.041666666666668 ]",
"[ 1/1 → 7/6 | s:bd note:20.166666666666668 ]",
"[ 7/6 → 4/3 | s:sd note:20.166666666666668 ]",
"[ 4/3 → 17/12 | s:bd note:20.083333333333332 ]",
"[ 17/12 → 3/2 | s:bd note:20.083333333333332 ]",
"[ 3/2 → 37/24 | s:sd note:20.041666666666668 ]",
"[ 37/24 → 19/12 | s:sd note:20.041666666666668 ]",
"[ 19/12 → 13/8 | s:sd note:20.041666666666668 ]",
"[ 13/8 → 5/3 | s:sd note:20.041666666666668 ]",
"[ 7/4 → 11/6 | s:sd note:20.083333333333332 ]",
"[ 11/6 → 23/12 | s:bd note:20.083333333333332 ]",
"[ 23/12 → 47/24 | s:bd note:20.041666666666668 ]",
"[ 47/24 → 2/1 | s:bd note:20.041666666666668 ]",
"[ 2/1 → 13/6 | s:bd note:20.166666666666668 ]",
"[ 13/6 → 7/3 | s:sd note:20.166666666666668 ]",
"[ 7/3 → 29/12 | s:bd note:20.083333333333332 ]",
"[ 29/12 → 5/2 | s:bd note:20.083333333333332 ]",
"[ 5/2 → 61/24 | s:sd note:20.041666666666668 ]",
"[ 61/24 → 31/12 | s:sd note:20.041666666666668 ]",
"[ 31/12 → 21/8 | s:sd note:20.041666666666668 ]",
"[ 21/8 → 8/3 | s:sd note:20.041666666666668 ]",
"[ 11/4 → 17/6 | s:sd note:20.083333333333332 ]",
"[ 17/6 → 35/12 | s:bd note:20.083333333333332 ]",
"[ 35/12 → 71/24 | s:bd note:20.041666666666668 ]",
"[ 71/24 → 3/1 | s:bd note:20.041666666666668 ]",
"[ 3/1 → 19/6 | s:bd note:20.166666666666668 ]",
"[ 19/6 → 10/3 | s:sd note:20.166666666666668 ]",
"[ 10/3 → 41/12 | s:bd note:20.083333333333332 ]",
"[ 41/12 → 7/2 | s:bd note:20.083333333333332 ]",
"[ 7/2 → 85/24 | s:sd note:20.041666666666668 ]",
"[ 85/24 → 43/12 | s:sd note:20.041666666666668 ]",
"[ 43/12 → 29/8 | s:sd note:20.041666666666668 ]",
"[ 29/8 → 11/3 | s:sd note:20.041666666666668 ]",
"[ 15/4 → 23/6 | s:sd note:20.083333333333332 ]",
"[ 23/6 → 47/12 | s:bd note:20.083333333333332 ]",
"[ 47/12 → 95/24 | s:bd note:20.041666666666668 ]",
"[ 95/24 → 4/1 | s:bd note:20.041666666666668 ]",
]
`;
exports[`runs examples > example "decay" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:c3 decay:0.1 sustain:0 ]",
@@ -4121,70 +4043,6 @@ exports[`runs examples > example "filter" example index 0 1`] = `
]
`;
exports[`runs examples > example "filterHaps" example index 0 1`] = `
[
"[ 1/8 → 1/4 | s:bd velocity:0.6852155700325966 ]",
"[ 1/4 → 3/8 | s:bd velocity:0.36975969187915325 ]",
"[ 3/8 → 1/2 | s:bd velocity:0.40139251574873924 ]",
"[ 1/1 → 9/8 | s:bd velocity:0.5195421651005745 ]",
"[ 9/8 → 5/4 | s:bd velocity:0.6724895145744085 ]",
"[ 5/4 → 11/8 | s:bd velocity:0.7287282031029463 ]",
"[ 3/2 → 13/8 | s:bd velocity:0.6084080748260021 ]",
"[ 2/1 → 17/8 | s:bd velocity:0.9595271199941635 ]",
"[ 17/8 → 9/4 | s:bd velocity:0.9033902939409018 ]",
"[ 9/4 → 19/8 | s:bd velocity:0.34548251144587994 ]",
"[ 19/8 → 5/2 | s:bd velocity:0.8365066405385733 ]",
"[ 3/1 → 25/8 | s:bd velocity:0.21728911064565182 ]",
"[ 25/8 → 13/4 | s:bd velocity:0.34324387833476067 ]",
"[ 13/4 → 27/8 | s:bd velocity:0.9923650715500116 ]",
"[ 27/8 → 7/2 | s:bd velocity:0.40869180113077164 ]",
"[ 29/8 → 15/4 | s:bd velocity:0.9391485787928104 ]",
"[ 15/4 → 31/8 | s:bd velocity:0.8121673222631216 ]",
]
`;
exports[`runs examples > example "filterValues" example index 0 1`] = `
[
"[ 0/1 → 1/4 | s:saw note:G#1 cutoff:80 lpenv:4 orbit:2 ]",
"[ 1/4 → 1/2 | s:saw note:G#1 cutoff:80 lpenv:4 orbit:2 ]",
"[ 1/2 → 3/4 | s:saw note:G#1 cutoff:80 lpenv:4 orbit:2 ]",
"[ 3/4 → 1/1 | s:saw note:G#1 cutoff:80 lpenv:4 orbit:2 ]",
"[ 1/1 → 5/4 | s:saw note:G#1 cutoff:80 lpenv:4 orbit:2 ]",
"[ 5/4 → 3/2 | s:saw note:G#1 cutoff:80 lpenv:4 orbit:2 ]",
"[ 3/2 → 7/4 | s:saw note:G#1 cutoff:80 lpenv:4 orbit:2 ]",
"[ 7/4 → 2/1 | s:saw note:G#1 cutoff:80 lpenv:4 orbit:2 ]",
"[ 2/1 → 9/4 | s:saw note:G#1 cutoff:80 lpenv:4 orbit:2 ]",
"[ 9/4 → 5/2 | s:saw note:G#1 cutoff:80 lpenv:4 orbit:2 ]",
"[ 5/2 → 11/4 | s:saw note:G#1 cutoff:80 lpenv:4 orbit:2 ]",
"[ 11/4 → 3/1 | s:saw note:G#1 cutoff:80 lpenv:4 orbit:2 ]",
"[ 3/1 → 13/4 | s:saw note:G#1 cutoff:80 lpenv:4 orbit:2 ]",
"[ 13/4 → 7/2 | s:saw note:G#1 cutoff:80 lpenv:4 orbit:2 ]",
"[ 7/2 → 15/4 | s:saw note:G#1 cutoff:80 lpenv:4 orbit:2 ]",
"[ 15/4 → 4/1 | s:saw note:G#1 cutoff:80 lpenv:4 orbit:2 ]",
]
`;
exports[`runs examples > example "filterWhen" example index 0 1`] = `
[
"[ 0/1 → 1/4 | s:bd ]",
"[ 1/4 → 1/2 | s:bd ]",
"[ 1/2 → 3/4 | s:bd ]",
"[ 3/4 → 1/1 | s:bd ]",
"[ 1/1 → 5/4 | s:bd ]",
"[ 5/4 → 3/2 | s:bd ]",
"[ 3/2 → 7/4 | s:bd ]",
"[ 7/4 → 2/1 | s:bd ]",
"[ 2/1 → 9/4 | s:bd ]",
"[ 9/4 → 5/2 | s:bd ]",
"[ 5/2 → 11/4 | s:bd ]",
"[ 11/4 → 3/1 | s:bd ]",
"[ 3/1 → 13/4 | s:bd ]",
"[ 13/4 → 7/2 | s:bd ]",
"[ 7/2 → 15/4 | s:bd ]",
"[ 15/4 → 4/1 | s:bd ]",
]
`;
exports[`runs examples > example "firstOf" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:g3 ]",
@@ -7126,27 +6984,6 @@ exports[`runs examples > example "midin" example index 0 1`] = `
]
`;
exports[`runs examples > example "midin" example index 1 1`] = `
[
"[ 0/1 → 1/4 | note:c s:saw ]",
"[ 1/4 → 1/2 | note:a s:saw ]",
"[ 1/2 → 3/4 | note:f s:saw ]",
"[ 3/4 → 1/1 | note:e s:saw ]",
"[ 1/1 → 5/4 | note:c s:saw ]",
"[ 5/4 → 3/2 | note:a s:saw ]",
"[ 3/2 → 7/4 | note:f s:saw ]",
"[ 7/4 → 2/1 | note:e s:saw ]",
"[ 2/1 → 9/4 | note:c s:saw ]",
"[ 9/4 → 5/2 | note:a s:saw ]",
"[ 5/2 → 11/4 | note:f s:saw ]",
"[ 11/4 → 3/1 | note:e s:saw ]",
"[ 3/1 → 13/4 | note:c s:saw ]",
"[ 13/4 → 7/2 | note:a s:saw ]",
"[ 7/2 → 15/4 | note:f s:saw ]",
"[ 15/4 → 4/1 | note:e s:saw ]",
]
`;
exports[`runs examples > example "midiport" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:c midiport:0 ]",
@@ -7991,51 +7828,6 @@ exports[`runs examples > example "penv" example index 0 1`] = `
]
`;
exports[`runs examples > example "per" example index 0 1`] = `
[
"[ 0/1 → 1/7 | n:0 s:bd distort:3.5 ]",
"[ 1/7 → 3/14 | n:0 s:bd distort:7 ]",
"[ 3/14 → 2/7 | n:0 s:bd distort:7 ]",
"[ 2/7 → 3/7 | n:0 s:bd distort:3.5 ]",
"[ 3/7 → 1/2 | n:0 s:bd distort:7 ]",
"[ 1/2 → 4/7 | n:0 s:bd distort:7 ]",
"[ 4/7 → 5/7 | n:0 s:bd distort:3.5 ]",
"[ 5/7 → 17/21 | n:0 s:bd distort:5.25 ]",
"[ 17/21 → 19/21 | n:0 s:bd distort:5.25 ]",
"[ 19/21 → 1/1 | n:0 s:bd distort:5.25 ]",
"[ 1/1 → 8/7 | n:0 s:bd distort:3.5 ]",
"[ 8/7 → 17/14 | n:0 s:bd distort:7 ]",
"[ 17/14 → 9/7 | n:0 s:bd distort:7 ]",
"[ 9/7 → 10/7 | n:0 s:bd distort:3.5 ]",
"[ 10/7 → 3/2 | n:0 s:bd distort:7 ]",
"[ 3/2 → 11/7 | n:0 s:bd distort:7 ]",
"[ 11/7 → 12/7 | n:0 s:bd distort:3.5 ]",
"[ 12/7 → 38/21 | n:0 s:bd distort:5.25 ]",
"[ 38/21 → 40/21 | n:0 s:bd distort:5.25 ]",
"[ 40/21 → 2/1 | n:0 s:bd distort:5.25 ]",
"[ 2/1 → 15/7 | n:0 s:bd distort:3.5 ]",
"[ 15/7 → 31/14 | n:0 s:bd distort:7 ]",
"[ 31/14 → 16/7 | n:0 s:bd distort:7 ]",
"[ 16/7 → 17/7 | n:0 s:bd distort:3.5 ]",
"[ 17/7 → 5/2 | n:0 s:bd distort:7 ]",
"[ 5/2 → 18/7 | n:0 s:bd distort:7 ]",
"[ 18/7 → 19/7 | n:0 s:bd distort:3.5 ]",
"[ 19/7 → 59/21 | n:0 s:bd distort:5.25 ]",
"[ 59/21 → 61/21 | n:0 s:bd distort:5.25 ]",
"[ 61/21 → 3/1 | n:0 s:bd distort:5.25 ]",
"[ 3/1 → 22/7 | n:0 s:bd distort:3.5 ]",
"[ 22/7 → 45/14 | n:0 s:bd distort:7 ]",
"[ 45/14 → 23/7 | n:0 s:bd distort:7 ]",
"[ 23/7 → 24/7 | n:0 s:bd distort:3.5 ]",
"[ 24/7 → 7/2 | n:0 s:bd distort:7 ]",
"[ 7/2 → 25/7 | n:0 s:bd distort:7 ]",
"[ 25/7 → 26/7 | n:0 s:bd distort:3.5 ]",
"[ 26/7 → 80/21 | n:0 s:bd distort:5.25 ]",
"[ 80/21 → 82/21 | n:0 s:bd distort:5.25 ]",
"[ 82/21 → 4/1 | n:0 s:bd distort:5.25 ]",
]
`;
exports[`runs examples > example "perlin" example index 0 1`] = `
[
"[ 0/1 → 1/8 | s:hh cutoff:500 ]",
@@ -9309,43 +9101,6 @@ exports[`runs examples > example "ratio" example index 0 1`] = `
]
`;
exports[`runs examples > example "register" example index 0 1`] = `
[
"[ 0/1 → 1/8 | s:saw velocity:0 cutoff:0 ]",
"[ 1/8 → 1/4 | s:saw velocity:0.6852155700325966 cutoff:548.1724560260773 ]",
"[ 1/4 → 3/8 | s:saw velocity:0.36975969187915325 cutoff:295.8077535033226 ]",
"[ 3/8 → 1/2 | s:saw velocity:0.40139251574873924 cutoff:321.1140125989914 ]",
"[ 1/2 → 5/8 | s:saw velocity:0.2604806162416935 cutoff:208.3844929933548 ]",
"[ 5/8 → 3/4 | s:saw velocity:0.1356358677148819 cutoff:108.50869417190552 ]",
"[ 3/4 → 7/8 | s:saw velocity:0.19582648016512394 cutoff:156.66118413209915 ]",
"[ 7/8 → 1/1 | s:saw velocity:0.3976310808211565 cutoff:318.1048646569252 ]",
"[ 1/1 → 9/8 | s:saw velocity:0.5195421651005745 cutoff:415.6337320804596 ]",
"[ 9/8 → 5/4 | s:saw velocity:0.6724895145744085 cutoff:537.9916116595268 ]",
"[ 5/4 → 11/8 | s:saw velocity:0.7287282031029463 cutoff:582.982562482357 ]",
"[ 11/8 → 3/2 | s:saw velocity:0.18280375190079212 cutoff:146.2430015206337 ]",
"[ 3/2 → 13/8 | s:saw velocity:0.6084080748260021 cutoff:486.7264598608017 ]",
"[ 13/8 → 7/4 | s:saw velocity:0.49675471149384975 cutoff:397.4037691950798 ]",
"[ 7/4 → 15/8 | s:saw velocity:0.20473789609968662 cutoff:163.7903168797493 ]",
"[ 15/8 → 2/1 | s:saw velocity:0.5945571791380644 cutoff:475.6457433104515 ]",
"[ 2/1 → 17/8 | s:saw velocity:0.9595271199941635 cutoff:767.6216959953308 ]",
"[ 17/8 → 9/4 | s:saw velocity:0.9033902939409018 cutoff:722.7122351527214 ]",
"[ 9/4 → 19/8 | s:saw velocity:0.34548251144587994 cutoff:276.38600915670395 ]",
"[ 19/8 → 5/2 | s:saw velocity:0.8365066405385733 cutoff:669.2053124308586 ]",
"[ 5/2 → 21/8 | s:saw velocity:0.45861607417464256 cutoff:366.89285933971405 ]",
"[ 21/8 → 11/4 | s:saw velocity:0.16019348427653313 cutoff:128.1547874212265 ]",
"[ 11/4 → 23/8 | s:saw velocity:0.6332327704876661 cutoff:506.5862163901329 ]",
"[ 23/8 → 3/1 | s:saw velocity:0.01625417172908783 cutoff:13.003337383270264 ]",
"[ 3/1 → 25/8 | s:saw velocity:0.21728911064565182 cutoff:173.83128851652145 ]",
"[ 25/8 → 13/4 | s:saw velocity:0.34324387833476067 cutoff:274.59510266780853 ]",
"[ 13/4 → 27/8 | s:saw velocity:0.9923650715500116 cutoff:793.8920572400093 ]",
"[ 27/8 → 7/2 | s:saw velocity:0.40869180113077164 cutoff:326.9534409046173 ]",
"[ 7/2 → 29/8 | s:saw velocity:0.40994881466031075 cutoff:327.9590517282486 ]",
"[ 29/8 → 15/4 | s:saw velocity:0.9391485787928104 cutoff:751.3188630342484 ]",
"[ 15/4 → 31/8 | s:saw velocity:0.8121673222631216 cutoff:649.7338578104973 ]",
"[ 31/8 → 4/1 | s:saw velocity:0.7361665386706591 cutoff:588.9332309365273 ]",
]
`;
exports[`runs examples > example "release" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:c3 release:0 ]",
@@ -9967,6 +9722,42 @@ exports[`runs examples > example "samples" example index 1 1`] = `
]
`;
exports[`runs examples > example "samples" example index 2 1`] = `
[
"[ 0/1 → 1/2 | s:noise ]",
"[ 1/2 → 3/4 | s:chimp n:0 ]",
"[ 3/4 → 1/1 | s:chimp n:0 ]",
"[ 1/1 → 3/2 | s:noise ]",
"[ 3/2 → 2/1 | s:chimp n:1 ]",
"[ 2/1 → 5/2 | s:noise ]",
"[ 5/2 → 11/4 | s:chimp n:0 ]",
"[ 11/4 → 3/1 | s:chimp n:0 ]",
"[ 3/1 → 7/2 | s:noise ]",
"[ 7/2 → 4/1 | s:chimp n:1 ]",
]
`;
exports[`runs examples > example "samples" example index 3 1`] = `
[
"[ 0/1 → 1/4 | s:chocolat ]",
"[ 1/4 → 1/2 | s:chocolat ]",
"[ 1/2 → 3/4 | s:chocolat ]",
"[ 3/4 → 1/1 | s:chocolat ]",
"[ 1/1 → 5/4 | s:chocolat ]",
"[ 5/4 → 3/2 | s:chocolat ]",
"[ 3/2 → 7/4 | s:chocolat ]",
"[ 7/4 → 2/1 | s:chocolat ]",
"[ 2/1 → 9/4 | s:chocolat ]",
"[ 9/4 → 5/2 | s:chocolat ]",
"[ 5/2 → 11/4 | s:chocolat ]",
"[ 11/4 → 3/1 | s:chocolat ]",
"[ 3/1 → 13/4 | s:chocolat ]",
"[ 13/4 → 7/2 | s:chocolat ]",
"[ 7/2 → 15/4 | s:chocolat ]",
"[ 15/4 → 4/1 | s:chocolat ]",
]
`;
exports[`runs examples > example "saw" example index 0 1`] = `
[
"[ 0/1 → 1/8 | note:c3 clip:0 ]",
@@ -11971,75 +11762,6 @@ exports[`runs examples > example "sysexid" example index 0 1`] = `
]
`;
exports[`runs examples > example "tag" example index 0 1`] = `
[
"[ 0/1 → 1/16 | s:square note:F1 cutoff:40 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 1/16 → 1/8 | s:square note:F1 cutoff:41.25 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 1/8 → 3/16 | s:square note:F1 cutoff:42.5 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 3/16 → 1/4 | s:square note:F1 cutoff:43.75 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 1/4 → 5/16 | s:square note:F1 cutoff:45 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 5/16 → 3/8 | s:square note:F1 cutoff:46.25 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 3/8 → 7/16 | s:square note:F1 cutoff:47.5 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 7/16 → 1/2 | s:square note:F1 cutoff:48.75 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 1/2 → 9/16 | s:square note:F1 cutoff:50 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 9/16 → 5/8 | s:square note:F1 cutoff:51.25 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 5/8 → 11/16 | s:square note:F1 cutoff:52.5 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 11/16 → 3/4 | s:square note:F1 cutoff:53.75 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 3/4 → 13/16 | s:square note:F1 cutoff:55 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 13/16 → 7/8 | s:square note:F1 cutoff:56.25 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 7/8 → 15/16 | s:square note:F1 cutoff:57.5 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 15/16 → 1/1 | s:square note:F1 cutoff:58.75 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 1/1 → 17/16 | s:saw note:F2 cutoff:60 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 17/16 → 9/8 | s:saw note:F2 cutoff:61.25 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 9/8 → 19/16 | s:saw note:F2 cutoff:62.5 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 19/16 → 5/4 | s:saw note:F2 cutoff:63.75 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 5/4 → 21/16 | s:saw note:F2 cutoff:65 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 21/16 → 11/8 | s:saw note:F2 cutoff:66.25 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 11/8 → 23/16 | s:saw note:F2 cutoff:67.5 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 23/16 → 3/2 | s:saw note:F2 cutoff:68.75 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 3/2 → 25/16 | s:saw note:F2 cutoff:70 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 25/16 → 13/8 | s:saw note:F2 cutoff:71.25 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 13/8 → 27/16 | s:saw note:F2 cutoff:72.5 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 27/16 → 7/4 | s:saw note:F2 cutoff:73.75 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 7/4 → 29/16 | s:saw note:F2 cutoff:75 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 29/16 → 15/8 | s:saw note:F2 cutoff:76.25 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 15/8 → 31/16 | s:saw note:F2 cutoff:77.5 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 31/16 → 2/1 | s:saw note:F2 cutoff:78.75 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 2/1 → 33/16 | s:saw note:F1 cutoff:80 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 33/16 → 17/8 | s:saw note:F1 cutoff:78.75 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 17/8 → 35/16 | s:saw note:F1 cutoff:77.5 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 35/16 → 9/4 | s:saw note:F1 cutoff:76.25 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 9/4 → 37/16 | s:saw note:F1 cutoff:75 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 37/16 → 19/8 | s:saw note:F1 cutoff:73.75 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 19/8 → 39/16 | s:saw note:F1 cutoff:72.5 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 39/16 → 5/2 | s:saw note:F1 cutoff:71.25 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 5/2 → 41/16 | s:saw note:F1 cutoff:70 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 41/16 → 21/8 | s:saw note:F1 cutoff:68.75 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 21/8 → 43/16 | s:saw note:F1 cutoff:67.5 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 43/16 → 11/4 | s:saw note:F1 cutoff:66.25 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 11/4 → 45/16 | s:saw note:F1 cutoff:65 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 45/16 → 23/8 | s:saw note:F1 cutoff:63.75 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 23/8 → 47/16 | s:saw note:F1 cutoff:62.5 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 47/16 → 3/1 | s:saw note:F1 cutoff:61.25 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 3/1 → 49/16 | s:square note:F1 cutoff:60 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 49/16 → 25/8 | s:square note:F1 cutoff:58.75 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 25/8 → 51/16 | s:square note:F1 cutoff:57.5 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 51/16 → 13/4 | s:square note:F1 cutoff:56.25 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 13/4 → 53/16 | s:square note:F1 cutoff:55 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 53/16 → 27/8 | s:square note:F1 cutoff:53.75 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 27/8 → 55/16 | s:square note:F1 cutoff:52.5 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 55/16 → 7/2 | s:square note:F1 cutoff:51.25 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 7/2 → 57/16 | s:square note:F1 cutoff:50 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 57/16 → 29/8 | s:square note:F1 cutoff:48.75 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 29/8 → 59/16 | s:square note:F1 cutoff:47.5 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 59/16 → 15/4 | s:square note:F1 cutoff:46.25 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 15/4 → 61/16 | s:square note:F1 cutoff:45 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 61/16 → 31/8 | s:square note:F1 cutoff:43.75 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 31/8 → 63/16 | s:square note:F1 cutoff:42.5 lpenv:5 resonance:4 lpdecay:0.15 ]",
"[ 63/16 → 4/1 | s:square note:F1 cutoff:41.25 lpenv:5 resonance:4 lpdecay:0.15 ]",
]
`;
exports[`runs examples > example "take" example index 0 1`] = `
[
"[ 0/1 → 1/2 | s:bd ]",
@@ -12152,84 +11874,6 @@ exports[`runs examples > example "tour" example index 0 1`] = `
]
`;
exports[`runs examples > example "transient" example index 0 1`] = `
[
"[ 0/1 → 1/1 | s:bd transient:-1 ]",
"[ 1/1 → 2/1 | s:bd transient:-0.5 ]",
"[ 2/1 → 3/1 | s:bd transient:0 ]",
"[ 3/1 → 4/1 | s:bd transient:0.5 ]",
]
`;
exports[`runs examples > example "transient" example index 1 1`] = `
[
"[ 0/1 → 1/16 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
"[ 1/16 → 1/8 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
"[ 1/8 → 3/16 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
"[ 3/16 → 1/4 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
"[ 1/4 → 5/16 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
"[ 5/16 → 3/8 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
"[ 3/8 → 7/16 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
"[ 7/16 → 1/2 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
"[ 1/2 → 9/16 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
"[ 9/16 → 5/8 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
"[ 5/8 → 11/16 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
"[ 11/16 → 3/4 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
"[ 3/4 → 13/16 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
"[ 13/16 → 7/8 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
"[ 7/8 → 15/16 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
"[ 15/16 → 1/1 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
"[ 1/1 → 17/16 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
"[ 17/16 → 9/8 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
"[ 9/8 → 19/16 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
"[ 19/16 → 5/4 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
"[ 5/4 → 21/16 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
"[ 21/16 → 11/8 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
"[ 11/8 → 23/16 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
"[ 23/16 → 3/2 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
"[ 3/2 → 25/16 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
"[ 25/16 → 13/8 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
"[ 13/8 → 27/16 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
"[ 27/16 → 7/4 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
"[ 7/4 → 29/16 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
"[ 29/16 → 15/8 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
"[ 15/8 → 31/16 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
"[ 31/16 → 2/1 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
"[ 2/1 → 33/16 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
"[ 33/16 → 17/8 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
"[ 17/8 → 35/16 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
"[ 35/16 → 9/4 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
"[ 9/4 → 37/16 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
"[ 37/16 → 19/8 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
"[ 19/8 → 39/16 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
"[ 39/16 → 5/2 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
"[ 5/2 → 41/16 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
"[ 41/16 → 21/8 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
"[ 21/8 → 43/16 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
"[ 43/16 → 11/4 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
"[ 11/4 → 45/16 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
"[ 45/16 → 23/8 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
"[ 23/8 → 47/16 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
"[ 47/16 → 3/1 | s:hh bank:tr909 transient:-1 transsustain:1 ]",
"[ 3/1 → 49/16 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
"[ 49/16 → 25/8 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
"[ 25/8 → 51/16 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
"[ 51/16 → 13/4 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
"[ 13/4 → 53/16 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
"[ 53/16 → 27/8 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
"[ 27/8 → 55/16 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
"[ 55/16 → 7/2 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
"[ 7/2 → 57/16 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
"[ 57/16 → 29/8 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
"[ 29/8 → 59/16 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
"[ 59/16 → 15/4 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
"[ 15/4 → 61/16 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
"[ 61/16 → 31/8 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
"[ 31/8 → 63/16 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
"[ 63/16 → 4/1 | s:hh bank:tr909 transient:1 transsustain:-1 ]",
]
`;
exports[`runs examples > example "transpose" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:C2 ]",