diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..a13e379d9 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,88 @@ +# changelog + +NOTE: you can generate this with `node warm.js`. it might still not be perfectly sorted... + +## november 2025 + +- 2025-11-27T22:03:53+01:00 [perf] in `noise`, let noiseMix do the disconnect when it exists by jeromew in: [#1783](https://codeberg.org/uzu/strudel/pulls/1783) +- 2025-11-27T22:01:01+01:00 Bug Fix: Retries for sounds tab by glossing in: [#1754](https://codeberg.org/uzu/strudel/pulls/1754) +- 2025-11-27T20:36:46+01:00 [hydra] return the hydra object when await initHydra(..) is called by jeromew in: [#1784](https://codeberg.org/uzu/strudel/pulls/1784) +- 2025-11-27T20:36:14+01:00 Use errorLogger for query and tonal errors by glossing in: [#1782](https://codeberg.org/uzu/strudel/pulls/1782) +- 2025-11-27T20:27:10+01:00 [perf] level 5 `connect-leak` on `vowel` by jeromew in: [#1779](https://codeberg.org/uzu/strudel/pulls/1779) +- 2025-11-26T08:23:38+01:00 feat: add prebake script import button for loading .strudel files by daslyfe in: [#1774](https://codeberg.org/uzu/strudel/pulls/1774) +- 2025-11-26T07:39:28+01:00 Fix Sampler port trampling by Dayglo in: [#1717](https://codeberg.org/uzu/strudel/pulls/1717) +- 2025-11-25T20:59:09+01:00 Feat: Hook up octave and fix example by glossing in: [#1773](https://codeberg.org/uzu/strudel/pulls/1773) +- 2025-11-25T20:48:09+01:00 [perf] fix `connect-leak` in `tremolo` param by jeromew in: [#1780](https://codeberg.org/uzu/strudel/pulls/1780) +- 2025-11-24T17:51:20+01:00 filter modulation improvements! by daslyfe in: [#1769](https://codeberg.org/uzu/strudel/pulls/1769) +- 2025-11-24T01:20:53+01:00 Add o as a synonym for orbit by daslyfe in: [#1766](https://codeberg.org/uzu/strudel/pulls/1766) +- 2025-11-23T22:23:26+01:00 [perf] fix `connect-leak` in `fm` modulation by jeromew in: [#1758](https://codeberg.org/uzu/strudel/pulls/1758) +- 2025-11-23T09:47:24+01:00 Fix interoperability issue between `all` and `await initHydra()` by jeromew in: [#1663](https://codeberg.org/uzu/strudel/pulls/1663) +- 2025-11-23T04:23:06+01:00 Bug fix: Swap l/r gains with temp variable by glossing in: [#1768](https://codeberg.org/uzu/strudel/pulls/1768) +- 2025-11-23T00:52:28+01:00 FIX: prevent LFO filter modulation from popping from negative values by daslyfe in: [#1767](https://codeberg.org/uzu/strudel/pulls/1767) +- 2025-11-21T01:56:11+01:00 Bug Fix: Use frac due to negative frequencies from FM by glossing in: [#1759](https://codeberg.org/uzu/strudel/pulls/1759) +- 2025-11-20T19:48:16+01:00 [perf] fix `connect-leak` added by #1742 when noise() is not used by jeromew in: [#1757](https://codeberg.org/uzu/strudel/pulls/1757) +- 2025-11-20T14:51:00+01:00 [perf] fix `connect-leak` in `delay` effect by jeromew in: [#1755](https://codeberg.org/uzu/strudel/pulls/1755) +- 2025-11-20T14:49:41+01:00 [perf] fix `connect leak` when .noise() is in the mix by jeromew in: [#1742](https://codeberg.org/uzu/strudel/pulls/1742) +- 2025-11-18T23:52:45+01:00 wchooseCycles has now notes in an example by scrappy_fiddler in: [#1748](https://codeberg.org/uzu/strudel/pulls/1748) +- 2025-11-17T05:31:54+01:00 Feature: Partials by glossing in: [#1659](https://codeberg.org/uzu/strudel/pulls/1659) +- 2025-11-16T22:06:14+01:00 README: update superdough documentation by TristanMlct in: [#1741](https://codeberg.org/uzu/strudel/pulls/1741) +- 2025-11-16T21:08:54+01:00 Worklet optimizations by glossing in: [#1730](https://codeberg.org/uzu/strudel/pulls/1730) +- 2025-11-15T14:59:30+01:00 [perf] disconnect Offline AudioNode connections in generateReverb by jeromew in: [#1740](https://codeberg.org/uzu/strudel/pulls/1740) +- 2025-11-15T14:57:47+01:00 [perf] disconnect `send` effect AudioNode when `room` is used by jeromew in: [#1736](https://codeberg.org/uzu/strudel/pulls/1736) +- 2025-11-12T21:22:34+01:00 [supradough] fix: unstable filter by froos in: [#1593](https://codeberg.org/uzu/strudel/pulls/1593) +- 2025-11-12T21:12:07+01:00 Fix link syntax in `project-start` by Kissaki in: [#1724](https://codeberg.org/uzu/strudel/pulls/1724) +- 2025-11-12T21:06:13+01:00 Fix web README sample code by Kissaki in: [#1725](https://codeberg.org/uzu/strudel/pulls/1725) +- 2025-11-12T20:59:39+01:00 Fix typo: 'studel' -> 'strudel' by drhayes in: [#1726](https://codeberg.org/uzu/strudel/pulls/1726) +- 2025-11-12T15:23:41+01:00 fix for node 24 support in tests - #1718 by ausav in: [#1719](https://codeberg.org/uzu/strudel/pulls/1719) +- 2025-11-11T09:00:49+01:00 soundAlias example fix by PepsiiMan in: [#1720](https://codeberg.org/uzu/strudel/pulls/1720) +- 2025-11-10T08:18:36+01:00 fix: Make Supradough package build by munshkr in: [#1711](https://codeberg.org/uzu/strudel/pulls/1711) +- 2025-11-10T08:18:00+01:00 added docs for spaces in scale names by ondras in: [#1715](https://codeberg.org/uzu/strudel/pulls/1715) +- 2025-11-10T02:44:01+01:00 Feature: LFOs for Filters by glossing in: [#1636](https://codeberg.org/uzu/strudel/pulls/1636) +- 2025-11-05T17:07:00+01:00 improvement: sync midi messages to audio context clock by daslyfe in: [#1708](https://codeberg.org/uzu/strudel/pulls/1708) +- 2025-11-04T19:36:14+01:00 Add stick button mappings to gamepad implementation and improve docs by kaosuryoko in: [#1696](https://codeberg.org/uzu/strudel/pulls/1696) +- 2025-11-04T19:29:35+01:00 Refactor sound stopping and triggering logic in SoundsTab component by IJOL in: [#1688](https://codeberg.org/uzu/strudel/pulls/1688) +- 2025-11-04T19:13:21+01:00 Add setting to toggle pattern auto-start on pattern change by moumar in: [#1690](https://codeberg.org/uzu/strudel/pulls/1690) +- 2025-11-04T18:58:50+01:00 Voicings JSDoc by sharkeys_lunchbox in: [#1686](https://codeberg.org/uzu/strudel/pulls/1686) +- 2025-11-01T07:50:40+01:00 FIX: Loading local samples uses too much memory by daslyfe in: [#1706](https://codeberg.org/uzu/strudel/pulls/1706) + + +## october 2025 + +- 2025-10-28T22:58:24+01:00 Bug Fix: Handle scale-for-notes when n also supplied by glossing in: [#1625](https://codeberg.org/uzu/strudel/pulls/1625) +- 2025-10-28T22:51:16+01:00 Repurpose vim shortcuts for usability by dtricks in: [#1624](https://codeberg.org/uzu/strudel/pulls/1624) +- 2025-10-28T22:35:21+01:00 Add support for euclidian in mondo with `bd&3:8` by TristanCacqueray in: [#1630](https://codeberg.org/uzu/strudel/pulls/1630) +- 2025-10-27T10:42:07+01:00 Use bunny cdn for all samples and json files by yaxu in: [#1701](https://codeberg.org/uzu/strudel/pulls/1701) +- 2025-10-26T22:52:54+01:00 degithub - switch some samples to bunnycdn by yaxu in: [#1697](https://codeberg.org/uzu/strudel/pulls/1697) +- 2025-10-26T17:09:22+01:00 Fix ZZFX example by moumar in: [#1685](https://codeberg.org/uzu/strudel/pulls/1685) +- 2025-10-23T15:56:04+02:00 Make osc port and host configurable. Changes dependencies. by yaxu in: [#1682](https://codeberg.org/uzu/strudel/pulls/1682) +- 2025-10-23T03:47:31+02:00 Adds back shape to superdough by glossing in: [#1672](https://codeberg.org/uzu/strudel/pulls/1672) +- 2025-10-22T22:15:19+02:00 Fix sampler.mjs githubPath by jeromew in: [#1684](https://codeberg.org/uzu/strudel/pulls/1684) +- 2025-10-22T16:20:50+02:00 Fix onPaint for widgets by yaxu in: [#1658](https://codeberg.org/uzu/strudel/pulls/1658) +- 2025-10-22T15:19:12+02:00 Fix a bug introduced by #4e17cfbdd6 by milliganf in: [#1679](https://codeberg.org/uzu/strudel/pulls/1679) +- 2025-10-18T04:29:20+02:00 Bug Fix: Wavetable: phase wrapping at 1 and detune by glossing in: [#1620](https://codeberg.org/uzu/strudel/pulls/1620) +- 2025-10-16T20:26:28+02:00 github samples: default to "samples" if repository is not specified by prezmop in: [#1644](https://codeberg.org/uzu/strudel/pulls/1644) +- 2025-10-16T17:33:39+02:00 Docs: add example of custom chained function by dariusk in: [#1642](https://codeberg.org/uzu/strudel/pulls/1642) +- 2025-10-14T12:39:48+02:00 textbox by yaxu in: [#1650](https://codeberg.org/uzu/strudel/pulls/1650) +- 2025-10-13T07:05:57+02:00 Feature: Wavetable FM by glossing in: [#1623](https://codeberg.org/uzu/strudel/pulls/1623) +- 2025-10-12T20:43:42+02:00 fix: repair REPL sample sources and URL concat (#1640) by erikfox in: [#1646](https://codeberg.org/uzu/strudel/pulls/1646) +- 2025-10-10T11:25:02+02:00 Add control-metadata by yaxu in: [#1634](https://codeberg.org/uzu/strudel/pulls/1634) +- 2025-10-07T22:59:39+02:00 Fix MQTT: change the trigger handler to match new hap by jurrchen in: [#1629](https://codeberg.org/uzu/strudel/pulls/1629) +- 2025-10-05T21:42:19+02:00 Fix case sensitivity for synonym search by vvolhejn in: [#1618](https://codeberg.org/uzu/strudel/pulls/1618) +- 2025-10-01T08:56:35+02:00 Optimize wavetable synth by glossing in: [#1613](https://codeberg.org/uzu/strudel/pulls/1613) +- 2025-10-01T02:02:33+02:00 fix: pattern switching by daslyfe in: [#1616](https://codeberg.org/uzu/strudel/pulls/1616) + +## September 2025 + +- 2025-09-29T02:10:54+02:00 fix wavetable JSON by daslyfe in: [#1611](https://codeberg.org/uzu/strudel/pulls/1611) +- 2025-09-28T21:28:16+02:00 rename wavetable controls to match existing naming conventions by daslyfe in: [#1610](https://codeberg.org/uzu/strudel/pulls/1610) +- 2025-09-27T23:28:13+02:00 Feature: Wavetable synth improvements by glossing in: [#1607](https://codeberg.org/uzu/strudel/pulls/1607) +- 2025-09-27T21:43:10+02:00 wavetable improvements by daslyfe in: [#1608](https://codeberg.org/uzu/strudel/pulls/1608) +- 2025-09-26T08:48:46+02:00 create orbit based DJ filter by daslyfe in: [#1603](https://codeberg.org/uzu/strudel/pulls/1603) +- 2025-09-23T13:28:20+02:00 fix: time signal by daslyfe in: [#1583](https://codeberg.org/uzu/strudel/pulls/1583) +- 2025-09-21T16:16:06+02:00 mondo: fix all + setcpm + setcps by froos in: [#1600](https://codeberg.org/uzu/strudel/pulls/1600) +- 2025-09-19T21:59:43+02:00 fix: osc error message by froos in: [#1597](https://codeberg.org/uzu/strudel/pulls/1597) +- 2025-09-18T21:09:56+02:00 osc: --debug flag to see incoming messages by froos in: [#1595](https://codeberg.org/uzu/strudel/pulls/1595) +- 2025-09-17T14:12:10+02:00 simplify osc usage by froos in: [#1588](https://codeberg.org/uzu/strudel/pulls/1588) +- 2025-09-16T16:59:58+02:00 dev: logger errors to console in dev mode by daslyfe in: [#1585](https://codeberg.org/uzu/strudel/pulls/1585) +- 2025-09-15T22:52:14+02:00 add tic80 font by froos in: [#1579](https://codeberg.org/uzu/strudel/pulls/1579) +- 2025-09-15T00:00:19+02:00 supradough: fix delay + add some jsdoc types by froos in: [#1578](https://codeberg.org/uzu/strudel/pulls/1578) \ No newline at end of file diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index 854f48ffb..8a848fec0 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -1366,6 +1366,8 @@ export const { fanchor } = registerControl('fanchor'); * * @name lprate * @param {number | Pattern} rate rate in hertz + * @example + * note("*16").s("sawtooth").lpf(600).lprate("<4 8 2 1>") */ export const { lprate } = registerControl('lprate'); @@ -1374,6 +1376,8 @@ export const { lprate } = registerControl('lprate'); * * @name lpsync * @param {number | Pattern} rate rate in cycles + * @example + * note("*16").s("sawtooth").lpf(600).lpsync("<4 8 2 1>") */ export const { lpsync } = registerControl('lpsync'); @@ -1382,8 +1386,23 @@ export const { lpsync } = registerControl('lpsync'); * * @name lpdepth * @param {number | Pattern} depth depth of modulation + * @example + * note("*16").s("sawtooth").lpf(600).lpdepth("<1 .5 1.8 0>") */ + export const { lpdepth } = registerControl('lpdepth'); +/** + * Depth of the LFO for the lowpass filter, in HZ + * + * @name lpdepthfrequency + * @synonyms + * lpdethfreq + * @param {number | Pattern} depth depth of modulation + * @example + * note("*16").s("sawtooth").lpf(600).lpdepthfrequency("<200 500 100 0>") + */ + +export const { lpdepthfrequency } = registerControl('lpdepthfrequency', 'lpdepthfreq'); /** * Shape of the LFO for the lowpass filter @@ -1433,6 +1452,19 @@ export const { bpsync } = registerControl('bpsync'); */ export const { bpdepth } = registerControl('bpdepth'); +/** + * Depth of the LFO for the bandpass filter, in HZ + * + * @name bpdepthfrequency + * @synonyms + * bpdethfreq + * @param {number | Pattern} depth depth of modulation + * @example + * note("*16").s("sawtooth").lpf(600).bpdepthfrequency("<200 500 100 0>") + */ + +export const { bpdepthfrequency } = registerControl('bpdepthfrequency', 'bpdepthfreq'); + /** * Shape of the LFO for the bandpass filter * @@ -1479,7 +1511,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 + * hpdethfreq + * @param {number | Pattern} depth depth of modulation + * @example + * note("*16").s("sawtooth").lpf(600).hpdepthfrequency("<200 500 100 0>") + */ + +export const { hpdepthfrequency } = registerControl('hpdepthfrequency', 'hpdepthfreq'); /** * Shape of the LFO for the highpass filter @@ -1892,12 +1937,12 @@ export const { nudge } = registerControl('nudge'); * Sets the default octave of a synth. * * @name octave + * @synonyms oct * @param {number | Pattern} octave octave number * @example - * n("0,4,7").s('supersquare').octave("<3 4 5 6>").osc() - * @superDirtOnly + * n("0,4,7").scale("F:minor").s('supersaw').octave("<0 1 2 3>") */ -export const { octave } = registerControl('octave'); +export const { octave, oct } = registerControl('octave', 'oct'); // ['ophatdecay'], // TODO: example @@ -1905,6 +1950,7 @@ export const { octave } = registerControl('octave'); * An `orbit` is a global parameter context for patterns. Patterns with the same orbit will share the same global effects. * * @name orbit + * @synonyms o * @param {number | Pattern} number * @example * stack( @@ -1912,7 +1958,7 @@ export const { octave } = registerControl('octave'); * s("~ sd ~ sd").delay(.5).delaytime(.125).orbit(2) * ) */ -export const { orbit } = registerControl('orbit'); +export const { orbit } = registerControl('orbit', 'o'); // TODO: what is this? not found in tidal doc Answer: gain is limited to maximum of 2. This allows you to go over that export const { overgain } = registerControl('overgain'); // TODO: what is this? not found in tidal doc. Similar to above, but limited to 1 diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index 4737f0db0..c2e5ca1bc 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -25,7 +25,7 @@ import { stringifyValues, } from './util.mjs'; import drawLine from './drawLine.mjs'; -import { logger } from './logger.mjs'; +import { errorLogger, logger } from './logger.mjs'; let stringParser; @@ -414,7 +414,7 @@ export class Pattern { try { return this.query(new State(new TimeSpan(begin, end), controls)); } catch (err) { - logger(`[query]: ${err.message}`, 'error'); + errorLogger(err, 'query'); return []; } } @@ -2234,7 +2234,7 @@ export const brak = register('brak', function (pat) { }); /** - * Reverse all haps in a pattern + * Reverse all cycles in a pattern. See also `revv` for reversing a whole pattern. * * @name rev * @memberof Pattern @@ -2266,6 +2266,23 @@ export const rev = register( true, ); +/** + * Reverse a whole pattern. See also `rev` for reversing each cycle. + * + * @name revv + * @memberof Pattern + * @returns Pattern + * @example + * // This is the same as `<[g e] [d c]>`. If `rev()` is used, you get + * // the same as `<[d c] [g e]>`, where each cycle reverses, but the order of + * // cycles stays the same. + * note("<[c d] [e g]>").revv() + */ +export const revv = register('revv', function (pat) { + const negateSpan = (span) => new TimeSpan(Fraction(0).sub(span.end), Fraction(0).sub(span.begin)); + return pat.withQuerySpan(negateSpan).withHapSpan(negateSpan); +}); + /** Like press, but allows you to specify the amount by which each * event is shifted. pressBy(0.5) is the same as press, while * pressBy(1/3) shifts each event by a third of its timespan. diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index 171697eb9..4d59f5990 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -152,9 +152,9 @@ export function repl({ // allows muting a pattern x with x_ or _x return silence; } - if (id === '$') { + if (id.includes('$')) { // allows adding anonymous patterns with $: - id = `$${anonymousIndex}`; + id = `${id}${anonymousIndex}`; anonymousIndex++; } pPatterns[id] = this; @@ -215,8 +215,19 @@ export function repl({ let { pattern, meta } = await _evaluate(code, transpiler, transpilerOptions); if (Object.keys(pPatterns).length) { let patterns = []; + let soloActive = false; for (const [key, value] of Object.entries(pPatterns)) { - patterns.push(value.withState((state) => state.setControls({ id: key }))); + // handle soloed patterns ex: S$: s("bd!4") + const isSolod = key.length > 1 && key.startsWith('S'); + if (isSolod && soloActive === false) { + // first time we see a soloed pattern, clear existing patterns + patterns = []; + soloActive = true; + } + if (!soloActive || (soloActive && isSolod)) { + const valWithState = value.withState((state) => state.setControls({ id: key })); + patterns.push(valWithState); + } } if (eachTransform) { // Explicit lambda so only element (not index and array) are passed @@ -227,14 +238,13 @@ export function repl({ pattern = eachTransform(pattern); } if (allTransforms.length) { - for (let i in allTransforms) { - pattern = allTransforms[i](pattern); + for (const transform of allTransforms) { + pattern = transform(pattern); } } if (!isPattern(pattern)) { - const message = `got "${typeof evaluated}" instead of pattern`; - throw new Error(message + (typeof evaluated === 'function' ? ', did you forget to call a function?' : '.')); + pattern = silence; } logger(`[eval] code updated`); pattern = await setPattern(pattern, autostart); diff --git a/packages/core/signal.mjs b/packages/core/signal.mjs index 604d1c805..62fc26ad8 100644 --- a/packages/core/signal.mjs +++ b/packages/core/signal.mjs @@ -524,7 +524,7 @@ export const wchoose = (...pairs) => wchooseWith(rand, ...pairs); * @example * wchooseCycles(["bd",10], ["hh",1], ["sd",1]).s().fast(8) * @example - * wchooseCycles(["bd bd bd",5], ["hh hh hh",3], ["sd sd sd",1]).fast(4).s() + * wchooseCycles(["c c c",5], ["a a a",3], ["f f f",1]).fast(4).note() * @example * // The probability can itself be a pattern * wchooseCycles(["bd(3,8)","<5 0>"], ["hh hh hh",3]).fast(4).s() diff --git a/packages/core/test/pattern.test.mjs b/packages/core/test/pattern.test.mjs index 1df5c8776..408ff274b 100644 --- a/packages/core/test/pattern.test.mjs +++ b/packages/core/test/pattern.test.mjs @@ -586,6 +586,18 @@ describe('Pattern', () => { .map((a) => a.value), ).toStrictEqual(['c', 'b', 'a']); }); + it('Does not reverse the order of cycles', () => { + expect(fastcat('a', 'b', 'c', 'd').slow(2).rev().fast(2).sortHapsByPart().firstCycle()).toStrictEqual( + fastcat('b', 'a', 'd', 'c').firstCycle(), + ); + }); + }); + describe('revv()', () => { + it('Does reverse the order of cycles', () => { + expect(fastcat('a', 'b', 'c', 'd').slow(2).revv().fast(2).sortHapsByPart().firstCycle()).toStrictEqual( + fastcat('d', 'c', 'b', 'a').firstCycle(), + ); + }); }); describe('sequence()', () => { it('Can work like fastcat', () => { diff --git a/packages/hydra/hydra.mjs b/packages/hydra/hydra.mjs index 2cb265c2f..c13410121 100644 --- a/packages/hydra/hydra.mjs +++ b/packages/hydra/hydra.mjs @@ -35,6 +35,7 @@ export async function initHydra(options = {}) { hydra.synth.s0.init({ src: canvas }); } } + return hydra; } export function clearHydra() { diff --git a/packages/sampler/sample-server.mjs b/packages/sampler/sample-server.mjs index 31b83f5a3..1d5a8e7a6 100644 --- a/packages/sampler/sample-server.mjs +++ b/packages/sampler/sample-server.mjs @@ -9,6 +9,7 @@ import readline from 'readline'; import os from 'os'; const LOG = !!process.env.LOG || false; +const PORT = process.env.PORT || 5432; const VALID_AUDIO_EXTENSIONS = ['wav', 'mp3', 'ogg']; const isAudioFile = (f) => { @@ -54,7 +55,6 @@ async function getBanks(directory, flat = false) { banks[bank].push(subDir); return subDir; }); - banks._base = `http://localhost:5432`; return { banks, files }; } @@ -134,8 +134,6 @@ const server = http.createServer(async (req, res) => { readStream.pipe(res); }); -// eslint-disable-next-line -const PORT = process.env.PORT || 5432; const IP_ADDRESS = '0.0.0.0'; let IP; const networkInterfaces = os.networkInterfaces(); diff --git a/packages/soundfonts/fontloader.mjs b/packages/soundfonts/fontloader.mjs index 427f57e31..ca9e45d51 100644 --- a/packages/soundfonts/fontloader.mjs +++ b/packages/soundfonts/fontloader.mjs @@ -6,6 +6,7 @@ import { getADSRValues, getPitchEnvelope, getVibratoOscillator, + onceEnded, } from '@strudel/webaudio'; import gm from './gm.mjs'; @@ -170,12 +171,12 @@ export function registerSoundfonts() { bufferSource.stop(envEnd); const stop = (releaseTime) => {}; - bufferSource.onended = () => { + onceEnded(bufferSource, () => { bufferSource.disconnect(); vibratoOscillator?.stop(); node.disconnect(); onended(); - }; + }); return { node, stop }; }, { type: 'soundfont', prebake: true, fonts }, diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index 0ab43791d..073ed00bd 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -191,8 +191,7 @@ export function applyParameterModulators(audioContext, param, start, end, envelo const lfo = getParamLfo(audioContext, param, start, end, lfoValues); return { lfo, disconnect: () => lfo?.disconnect() }; } - -export function createFilter(context, start, end, params, cps) { +export function createFilter(context, start, end, params, cps, cycle) { let { frequency, anchor, @@ -202,12 +201,14 @@ export function createFilter(context, start, end, params, cps) { q = 1, drive = 0.69, depth, + depthfrequency, dcoffset = -0.5, skew, shape, rate, sync, } = params; + let frequencyParam, filter; if (model === 'ladder') { filter = getWorklet(context, 'ladder-processor', { frequency, q, drive }); @@ -238,9 +239,27 @@ export function createFilter(context, start, end, params, cps) { if (sync != null) { rate = cps * sync; } - const lfoValues = { depth, dcoffset, skew, shape, frequency: rate }; - getParamLfo(context, frequencyParam, start, end, lfoValues); - return filter; + const hasLFO = [depth, depthfrequency, skew, shape, rate].some((v) => v !== undefined); + let lfo; + if (hasLFO) { + depth = depth ?? 1; + const time = cycle / cps; + const modDepth = depthfrequency ?? (depth ?? 1) * frequency; + const lfoValues = { + depth: modDepth, + dcoffset, + skew, + shape, + frequency: rate ?? cps, + min: -frequency + 30, + max: 20000 - frequency, + time, + curve: 1, + }; + lfo = getParamLfo(context, frequencyParam, start, end, lfoValues); + } + + return { filter, lfo }; } // stays 1 until .5, then fades out @@ -262,7 +281,15 @@ export function drywet(dry, wet, wetAmount = 0) { let mix = ac.createGain(); dry_gain.connect(mix); wet_gain.connect(mix); - return mix; + return { + node: mix, + onended: () => { + dry_gain.disconnect(mix); + wet_gain.disconnect(mix); + dry.disconnect(dry_gain); + wet.disconnect(wet_gain); + }, + }; } let curves = ['linear', 'exponential']; @@ -297,6 +324,10 @@ export function getVibratoOscillator(param, value, t) { gain.gain.value = vibmod * 100; vibratoOscillator.connect(gain); gain.connect(param); + vibratoOscillator.onended = () => { + gain.disconnect(param); + vibratoOscillator.disconnect(gain); + }; vibratoOscillator.start(t); return vibratoOscillator; } @@ -318,20 +349,11 @@ export function webAudioTimeout(audioContext, onComplete, startTime, stopTime) { constantNode.connect(zeroGain); // Schedule the `onComplete` callback to occur at `stopTime` - constantNode.onended = () => { - // Ensure garbage collection - try { - zeroGain.disconnect(); - } catch { - // pass - } - try { - constantNode.disconnect(); - } catch { - // pass - } + onceEnded(constantNode, () => { + releaseAudioNode(zeroGain); + releaseAudioNode(constantNode); onComplete(); - }; + }); constantNode.start(startTime); constantNode.stop(stopTime); return constantNode; @@ -429,6 +451,11 @@ export function applyFM(param, value, begin) { } io[0].connect(io[1]); } + fmmod.osc.onended = () => { + envGain.disconnect(); + modulator.disconnect(); + fmmod.osc.disconnect(); + }; } return { stop: (t) => toStop.forEach((m) => m?.stop(t)), @@ -532,7 +559,7 @@ export const getDistortion = (distort, postgain, algorithm) => { }; export const getFrequencyFromValue = (value, defaultNote = 36) => { - let { note, freq } = value; + let { note, freq, octave = 0 } = value; note = note || defaultNote; if (typeof note === 'string') { note = noteToMidi(note); // e.g. c3 => 48 @@ -541,16 +568,60 @@ export const getFrequencyFromValue = (value, defaultNote = 36) => { if (!freq && typeof note === 'number') { freq = midiToFreq(note); // + 48); } - + freq *= Math.pow(2, octave); return Number(freq); }; -export const destroyAudioWorkletNode = (node) => { - if (node == null) { - return; +// This helper should be used instead of the `node.onended = callback` pattern +// It adds a mechanism to help minimize gc retention +export const onceEnded = (node, callback) => { + let onended = callback; + node.onended = function cleanup() { + onended && onended(); + onended = null; + this.onended = null; + }; +}; + +export const releaseAudioNode = (node) => { + if (node == null) return; + + // check we received an AudioNode + if (!(node instanceof AudioNode)) { + throw new Error('releaseAudioNode can only release an AudioNode'); } + + // https://developer.mozilla.org/en-US/docs/Web/API/AudioNode/disconnect node.disconnect(); - node.parameters.get('end')?.setValueAtTime(0, 0); + + // make sure all AudioScheduledSourceNodes are in a stopped state + // https://developer.mozilla.org/en-US/docs/Web/API/AudioScheduledSourceNode + if (node instanceof AudioScheduledSourceNode) { + if (node.onended && node.onended.name !== 'cleanup') { + logger( + `[superdough] Deprecation warning: it seems your code path is setting 'node.onended = callback' instead of using the onceEnded helper`, + ); + } + try { + node.stop(); + } catch (e) { + // At the stage, `start` was not called on the node + // but an `onended` callback releasing resources may exist + // and we want it to fire : + // - we force a start/stop cycle so that `onended` gets called + // - we `lock` the node so that no-one can start it + node.start(node.context.currentTime + 5); // will never happen + node.stop(); + } + } + + // https://www.w3.org/TR/webaudio-1.1/#AudioNode-actively-processing + // An AudioWorkletNode is actively processing when its AudioWorkletProcessor's [[callable process]] + // 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) { + node.parameters.get('end')?.setValueAtTime(0, 0); + } }; export const cleanupNode = (node, time) => { diff --git a/packages/superdough/noise.mjs b/packages/superdough/noise.mjs index 816dd252b..3e41515df 100644 --- a/packages/superdough/noise.mjs +++ b/packages/superdough/noise.mjs @@ -65,8 +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); + noiseOscillator.node.onended = () => noiseMix.onended(); return { - node: noiseMix, + node: noiseMix.node, stop: (time) => noiseOscillator?.stop(time), }; } diff --git a/packages/superdough/sampler.mjs b/packages/superdough/sampler.mjs index 84bac3582..1e8c786bf 100644 --- a/packages/superdough/sampler.mjs +++ b/packages/superdough/sampler.mjs @@ -1,7 +1,14 @@ -import { getCommonSampleInfo } from './util.mjs'; +import { getBaseURL, getCommonSampleInfo } from './util.mjs'; import { registerSound, registerWaveTable } from './index.mjs'; import { getAudioContext } from './audioContext.mjs'; -import { getADSRValues, getParamADSR, getPitchEnvelope, getVibratoOscillator } from './helpers.mjs'; +import { + getADSRValues, + getParamADSR, + getPitchEnvelope, + getVibratoOscillator, + onceEnded, + releaseAudioNode, +} from './helpers.mjs'; import { logger } from './logger.mjs'; const bufferCache = {}; // string: Promise @@ -211,7 +218,7 @@ export async function fetchSampleMap(url) { // not a browser return; } - const base = url.split('/').slice(0, -1).join('/'); + const base = getBaseURL(url); if (typeof fetch === 'undefined') { // skip fetch when in node / testing return; @@ -286,17 +293,19 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) { const { bufferSource, sliceDuration, offset } = await getSampleBufferSource(value, bank, resolveUrl); - // asny stuff above took too long? - if (ac.currentTime > t) { - logger(`[sampler] still loading sound "${s}:${n}"`, 'highlight'); - // console.warn('sample still loading:', s, n); - return; - } if (!bufferSource) { logger(`[sampler] could not load "${s}:${n}"`, 'error'); return; } + // async stuff above took too long? + if (ac.currentTime > t) { + logger(`[sampler] loading sound "${s}:${n}" took too long`, 'highlight'); + // AudioBufferSourceNode will never be used. discard it + releaseAudioNode(bufferSource); + return; + } + // vibrato let vibratoOscillator = getVibratoOscillator(bufferSource.detune, value, t); @@ -319,13 +328,13 @@ 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); - bufferSource.onended = function () { + onceEnded(bufferSource, function () { bufferSource.disconnect(); vibratoOscillator?.stop(); node.disconnect(); out.disconnect(); onended(); - }; + }); let envEnd = holdEnd + release + 0.01; bufferSource.stop(envEnd); const stop = (endTime) => { diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 65c444120..6f610d514 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -263,8 +263,8 @@ let audioReady; export async function initAudioOnFirstClick(options) { if (!audioReady) { audioReady = new Promise((resolve) => { - document.addEventListener('click', async function listener() { - document.removeEventListener('click', listener); + document.addEventListener('mousedown', async function listener() { + document.removeEventListener('mousedown', listener); await initAudio(options); resolve(); }); @@ -274,7 +274,7 @@ export async function initAudioOnFirstClick(options) { } let controller; -function getSuperdoughAudioController() { +export function getSuperdoughAudioController() { if (controller == null) { controller = new SuperdoughAudioController(getAudioContext()); } @@ -287,7 +287,7 @@ export function connectToDestination(input, channels) { function getPhaser(time, end, frequency = 1, depth = 0.5, centerFrequency = 1000, sweep = 2000) { const ac = getAudioContext(); - const lfoGain = getLfo(ac, time, end, { frequency, depth: sweep * 2 }); + const lfo = getLfo(ac, time, end, { frequency, depth: sweep * 2 }); //filters const numStages = 2; //num of filters in series @@ -300,14 +300,14 @@ function getPhaser(time, end, frequency = 1, depth = 0.5, centerFrequency = 1000 filter.frequency.value = centerFrequency + fOffset; filter.Q.value = 2 - Math.min(Math.max(depth * 2, 0), 1.9); - lfoGain.connect(filter.detune); + lfo.connect(filter.detune); fOffset += 282; if (i > 0) { filterChain[i - 1].connect(filter); } filterChain.push(filter); } - return filterChain[filterChain.length - 1]; + return { phaser: filterChain[filterChain.length - 1], lfo }; } function getFilterType(ftype) { @@ -413,7 +413,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) release = 0, //phaser - phaserrate: phaser, + phaserrate, phaserdepth = getDefaultValue('phaserdepth'), phasersweep, phasercenter, @@ -553,16 +553,21 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) rate: 'lprate', sync: 'lpsync', depth: 'lpdepth', + depthfrequency: 'lpdepthfrequency', shape: 'lpshape', dcoffset: 'lpdc', skew: 'lpskew', }; const lpParams = pickAndRename(value, lpMap); lpParams.type = 'lowpass'; - let lp = () => createFilter(ac, t, end, lpParams, cps); - chain.push(lp()); + const lp = () => createFilter(ac, t, end, lpParams, cps, cycle); + const { filter: lpf1, lfo: lfo1 } = lp(); + chain.push(lpf1); + lfo1 && audioNodes.push(lfo1); if (ftype === '24db') { - chain.push(lp()); + const { filter: lpf2, lfo: lfo2 } = lp(); + chain.push(lpf2); + lfo2 && audioNodes.push(lfo2); } } @@ -581,16 +586,21 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) rate: 'hprate', sync: 'hpsync', depth: 'hpdepth', + depthfrequency: 'hpdepthfrequency', shape: 'hpshape', dcoffset: 'hpdc', skew: 'hpskew', }; const hpParams = pickAndRename(value, hpMap); hpParams.type = 'highpass'; - let hp = () => createFilter(ac, t, end, hpParams, cps); - chain.push(hp()); + const hp = () => createFilter(ac, t, end, hpParams, cps, cycle); + const { filter: hpf1, lfo: lfo1 } = hp(); + chain.push(hpf1); + lfo1 && audioNodes.push(lfo1); if (ftype === '24db') { - chain.push(hp()); + const { filter: hpf2, lfo: lfo2 } = hp(); + chain.push(hpf2); + lfo2 && audioNodes.push(lfo2); } } @@ -609,16 +619,21 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) rate: 'bprate', sync: 'bpsync', depth: 'bpdepth', + depthfrequency: 'bpdepthfrequency', shape: 'bpshape', dcoffset: 'bpdc', skew: 'bpskew', }; const bpParams = pickAndRename(value, bpMap); bpParams.type = 'bandpass'; - let bp = () => createFilter(ac, t, end, bpParams, cps); - chain.push(bp()); + const bp = () => createFilter(ac, t, end, bpParams, cps, cycle); + const { filter: bpf1, lfo: lfo1 } = bp(); + chain.push(bpf1); + lfo1 && audioNodes.push(lfo1); if (ftype === '24db') { - chain.push(bp()); + const { filter: bpf2, lfo: lfo2 } = bp(); + chain.push(bpf2); + lfo2 && audioNodes.push(lfo2); } } @@ -665,6 +680,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) curve: 1.5, }); lfo.connect(amGain.gain); + audioNodes.push(lfo); chain.push(amGain); } @@ -680,9 +696,10 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) chain.push(panner); } // phaser - if (phaser !== undefined && phaserdepth > 0) { - const phaserFX = getPhaser(t, endWithRelease, phaser, phaserdepth, phasercenter, phasersweep); - chain.push(phaserFX); + if (phaserrate !== undefined && phaserdepth > 0) { + const { phaser, lfo } = getPhaser(t, endWithRelease, phaserrate, phaserdepth, phasercenter, phasersweep); + audioNodes.push(lfo); + chain.push(phaser); } // last gain @@ -692,7 +709,8 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) // delay if (delay > 0 && delaytime > 0 && delayfeedback > 0) { orbitBus.getDelay(delaytime, delayfeedback, t); - orbitBus.sendDelay(post, delay); + const send = orbitBus.sendDelay(post, delay); + audioNodes.push(send); } // reverb if (room > 0) { diff --git a/packages/superdough/superdoughoutput.mjs b/packages/superdough/superdoughoutput.mjs index a4235efd0..d8ead7d06 100644 --- a/packages/superdough/superdoughoutput.mjs +++ b/packages/superdough/superdoughoutput.mjs @@ -82,7 +82,7 @@ export class Orbit { } sendDelay(node, amount) { - effectSend(node, this.delayNode, amount); + return effectSend(node, this.delayNode, amount); } duck(t, onsettime = 0, attacktime = 0.1, depth = 1) { diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index 10df1776b..b02354465 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -3,7 +3,6 @@ import { registerSound, soundMap } from './superdough.mjs'; import { getAudioContext } from './audioContext.mjs'; import { applyFM, - destroyAudioWorkletNode, gainNode, getADSRValues, getFrequencyFromValue, @@ -13,6 +12,7 @@ import { getVibratoOscillator, getWorklet, noises, + releaseAudioNode, webAudioTimeout, } from './helpers.mjs'; import { logger } from './logger.mjs'; @@ -48,19 +48,17 @@ export function registerSynthSounds() { [0.001, 0.05, 0.6, 0.01], ); - let sound = getOscillator(s, t, value); - let { node: o, stop, triggerRelease } = sound; - // turn down const g = gainNode(0.3); - const { duration } = value; - - o.onended = () => { - o.disconnect(); + let sound = getOscillator(s, t, value, () => { g.disconnect(); onended(); - }; + }); + + let { node: o, stop, triggerRelease } = sound; + + const { duration } = value; const envGain = gainNode(1); let node = o.connect(g).connect(envGain); @@ -194,8 +192,7 @@ export function registerSynthSounds() { let timeoutNode = webAudioTimeout( ac, () => { - destroyAudioWorkletNode(o); - envGain.disconnect(); + releaseAudioNode(o); onended(); fm?.stop(); vibratoOscillator?.stop(); @@ -272,8 +269,7 @@ export function registerSynthSounds() { let timeoutNode = webAudioTimeout( ac, () => { - destroyAudioWorkletNode(o); - envGain.disconnect(); + releaseAudioNode(o); onended(); }, begin, @@ -346,9 +342,8 @@ export function registerSynthSounds() { let timeoutNode = webAudioTimeout( ac, () => { - destroyAudioWorkletNode(o); - destroyAudioWorkletNode(lfo); - envGain.disconnect(); + releaseAudioNode(o); + releaseAudioNode(lfo); onended(); fm?.stop(); vibratoOscillator?.stop(); @@ -460,7 +455,7 @@ export function waveformN(partials, phases, type) { } // expects one of waveforms as s -export function getOscillator(s, t, value) { +export function getOscillator(s, t, value, onended) { const { duration, noise = 0 } = value; const partials = value.partials ?? value.n; let o; @@ -482,7 +477,6 @@ export function getOscillator(s, t, value) { } // set frequency o.frequency.value = getFrequencyFromValue(value); - o.start(t); let vibratoOscillator = getVibratoOscillator(o.detune, value, t); @@ -495,6 +489,13 @@ export function getOscillator(s, t, value) { noiseMix = getNoiseMix(o, noise, t); } + o.onended = () => { + noiseMix || o.disconnect(); + noiseMix?.node.disconnect(); + onended(); + }; + o.start(t); + return { node: noiseMix?.node || o, stop: (time) => { diff --git a/packages/superdough/util.mjs b/packages/superdough/util.mjs index 44ec79f77..48830541a 100644 --- a/packages/superdough/util.mjs +++ b/packages/superdough/util.mjs @@ -114,3 +114,13 @@ export function getCommonSampleInfo(hapValue, bank) { export const pickAndRename = (source, map) => { return Object.fromEntries(Object.entries(map).map(([newKey, oldKey]) => [newKey, source[oldKey]])); }; + +export const getBaseURL = (url) => { + try { + // For real URLs + return new URL('.', new URL(url)).href.replace(/\/$/, ''); // removes trailing slash + } catch { + // For pseudo URLS + return url.split('/').slice(0, -1).join('/'); + } +}; diff --git a/packages/superdough/vowel.mjs b/packages/superdough/vowel.mjs index 3f30aef1e..fda0c6479 100644 --- a/packages/superdough/vowel.mjs +++ b/packages/superdough/vowel.mjs @@ -45,7 +45,8 @@ if (typeof GainNode !== 'undefined') { throw new Error('vowel: unknown vowel ' + letter); } const { gains, qs, freqs } = vowelFormant[letter]; - const makeupGain = ac.createGain(); + this.makeupGain = ac.createGain(); + this.audioNodes = []; for (let i = 0; i < 5; i++) { const gain = ac.createGain(); gain.gain.value = gains[i]; @@ -53,14 +54,25 @@ if (typeof GainNode !== 'undefined') { filter.type = 'bandpass'; filter.Q.value = qs[i]; filter.frequency.value = freqs[i]; - this.connect(filter); + super.connect(filter); filter.connect(gain); - gain.connect(makeupGain); + this.audioNodes.push(filter); + gain.connect(this.makeupGain); + this.audioNodes.push(gain); } - makeupGain.gain.value = 8; // how much makeup gain to add? - this.connect = (target) => makeupGain.connect(target); + this.makeupGain.gain.value = 8; // how much makeup gain to add? return this; } + connect(target) { + this.makeupGain.connect(target); + } + disconnect() { + this.makeupGain.disconnect(); + this.audioNodes.forEach((n) => n.disconnect()); + super.disconnect(); + this.makeupGain = null; + this.audioNodes = null; + } } AudioContext.prototype.createVowelFilter = function (letter) { diff --git a/packages/superdough/wavetable.mjs b/packages/superdough/wavetable.mjs index 01d4eb838..e9d810414 100644 --- a/packages/superdough/wavetable.mjs +++ b/packages/superdough/wavetable.mjs @@ -1,15 +1,15 @@ import { getAudioContext, registerSound } from './index.mjs'; -import { getCommonSampleInfo } from './util.mjs'; +import { getBaseURL, getCommonSampleInfo } from './util.mjs'; import { applyFM, applyParameterModulators, - destroyAudioWorkletNode, getADSRValues, getFrequencyFromValue, getParamADSR, getPitchEnvelope, getVibratoOscillator, getWorklet, + releaseAudioNode, webAudioTimeout, } from './helpers.mjs'; import { logger } from './logger.mjs'; @@ -190,6 +190,7 @@ export const tables = async (url, frameLen, json, options = {}) => { if (url.startsWith('local:')) { url = `http://localhost:5432`; } + const base = getBaseURL(url); if (typeof fetch !== 'function') { // not a browser return; @@ -200,7 +201,7 @@ export const tables = async (url, frameLen, json, options = {}) => { } return fetch(url) .then((res) => res.json()) - .then((json) => _processTables(json, url, frameLen, options)) + .then((json) => _processTables(json, base, frameLen, options)) .catch((error) => { console.error(error); throw new Error(`error loading "${url}"`); @@ -318,10 +319,9 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) { const timeoutNode = webAudioTimeout( ac, () => { - destroyAudioWorkletNode(source); + releaseAudioNode(source); vibratoOscillator?.stop(); fm?.stop(); - node.disconnect(); wtPosModulators?.disconnect(); wtWarpModulators?.disconnect(); onended(); diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 359bc833a..2f75453b9 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -508,13 +508,14 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor { ]; } process(_input, outputs, params) { - if (currentTime <= params.begin[0]) { - return true; - } if (currentTime >= params.end[0]) { - // this.port.postMessage({ type: 'onended' }); + // should terminate return false; } + if (currentTime <= params.begin[0]) { + // keep alive + return true; + } const output = outputs[0]; const voices = params.voices[0]; // k-rate for (let i = 0; i < output[0].length; i++) { @@ -532,7 +533,7 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor { const freqVoice = applySemitoneDetuneToFrequency(freq, detuner(n)); // We must wrap this here because it is passed into sawblep below which // has domain [0, 1] - const dt = ffrac(freqVoice * INVSR); + const dt = frac(freqVoice * INVSR); this.phase[n] = this.phase[n] ?? Math.random(); const v = waveshapes.sawblep(this.phase[n], dt); @@ -543,8 +544,9 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor { if (pn >= 1.0) pn -= 1.0; this.phase[n] = pn; // invert right and left gain + const tmp = gainL; gainL = gainR; - gainR = gainL; + gainR = tmp; } } return true; @@ -1338,7 +1340,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { const tablePos = clamp(pv(parameters.position, i), 0, 1); const idx = tablePos * (this.numFrames - 1); const fIdx = idx | 0; - const frac = idx - fIdx; + const interpT = idx - fIdx; const warpAmount = clamp(pv(parameters.warp, i), 0, 1); const warpMode = pv(parameters.warpMode, i); const phaseRand = clamp(pv(parameters.phaserand, i), 0, 1); @@ -1368,13 +1370,13 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor { 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); - let s = s0 + (s1 - s0) * frac; + let s = lerp(s0, s1, interpT); if (warpMode === WarpMode.FLIP && this.phase[n] < warpAmount) { s = -s; } outL[i] += s * gainL * normalizer; outR[i] += s * gainR * normalizer; - this.phase[n] = ffrac(this.phase[n] + dPhase); + this.phase[n] = frac(this.phase[n] + dPhase); } } return true; diff --git a/packages/superdough/zzfx.mjs b/packages/superdough/zzfx.mjs index 32db0395a..4a4c58c9c 100644 --- a/packages/superdough/zzfx.mjs +++ b/packages/superdough/zzfx.mjs @@ -3,6 +3,7 @@ import { midiToFreq, noteToMidi } from './util.mjs'; import { registerSound } from './superdough.mjs'; import { getAudioContext } from './audioContext.mjs'; import { buildSamples } from './zzfx_fork.mjs'; +import { onceEnded, releaseAudioNode } from './helpers.mjs'; export const getZZFX = (value, t) => { let { @@ -83,10 +84,10 @@ export function registerZZFXSounds() { wave, (t, value, onended) => { const { node: o } = getZZFX({ s: wave, ...value }, t); - o.onended = () => { - o.disconnect(); + onceEnded(o, () => { + releaseAudioNode(o); onended(); - }; + }); return { node: o, stop: () => {}, diff --git a/packages/tonal/tonal.mjs b/packages/tonal/tonal.mjs index 427a66f4c..951f0f7e6 100644 --- a/packages/tonal/tonal.mjs +++ b/packages/tonal/tonal.mjs @@ -5,7 +5,16 @@ This program is free software: you can redistribute it and/or modify it under th */ import { Note, Interval, Scale } from '@tonaljs/tonal'; -import { register, _mod, logger, isNote, noteToMidi, removeUndefineds, getAccidentalsOffset } from '@strudel/core'; +import { + _mod, + errorLogger, + getAccidentalsOffset, + isNote, + logger, + noteToMidi, + register, + removeUndefineds, +} from '@strudel/core'; import { stepInNamedScale, nearestNumberIndex } from './tonleiter.mjs'; const octavesInterval = (octaves) => (octaves <= 0 ? -1 : 1) + octaves * 7 + 'P'; @@ -294,7 +303,7 @@ export const scale = register( } if (offset != 0) scaleNote = Note.transpose(scaleNote, Interval.fromSemitones(offset)); } catch (err) { - logger(`[tonal] ${err.message}`, 'error'); + errorLogger(err, 'tonal'); return; // will be removed } } diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 536222469..5c76f4a89 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -1391,6 +1391,75 @@ exports[`runs examples > example "bpdecay" example index 0 1`] = ` ] `; +exports[`runs examples > example "bpdepthfrequency" example index 0 1`] = ` +[ + "[ 0/1 → 1/16 | note:c s:sawtooth cutoff:600 bpdepthfrequency:200 ]", + "[ 1/16 → 1/8 | note:c s:sawtooth cutoff:600 bpdepthfrequency:200 ]", + "[ 1/8 → 3/16 | note:c# s:sawtooth cutoff:600 bpdepthfrequency:200 ]", + "[ 3/16 → 1/4 | note:c s:sawtooth cutoff:600 bpdepthfrequency:200 ]", + "[ 1/4 → 5/16 | note:c s:sawtooth cutoff:600 bpdepthfrequency:200 ]", + "[ 5/16 → 3/8 | note:c4 s:sawtooth cutoff:600 bpdepthfrequency:200 ]", + "[ 3/8 → 7/16 | note:c s:sawtooth cutoff:600 bpdepthfrequency:200 ]", + "[ 7/16 → 1/2 | note:c s:sawtooth cutoff:600 bpdepthfrequency:200 ]", + "[ 1/2 → 9/16 | note:c# s:sawtooth cutoff:600 bpdepthfrequency:200 ]", + "[ 9/16 → 5/8 | note:c s:sawtooth cutoff:600 bpdepthfrequency:200 ]", + "[ 5/8 → 11/16 | note:c s:sawtooth cutoff:600 bpdepthfrequency:200 ]", + "[ 11/16 → 3/4 | note:c4 s:sawtooth cutoff:600 bpdepthfrequency:200 ]", + "[ 3/4 → 13/16 | note:c s:sawtooth cutoff:600 bpdepthfrequency:200 ]", + "[ 13/16 → 7/8 | note:c s:sawtooth cutoff:600 bpdepthfrequency:200 ]", + "[ 7/8 → 15/16 | note:c# s:sawtooth cutoff:600 bpdepthfrequency:200 ]", + "[ 15/16 → 1/1 | note:c s:sawtooth cutoff:600 bpdepthfrequency:200 ]", + "[ 1/1 → 17/16 | note:c s:sawtooth cutoff:600 bpdepthfrequency:500 ]", + "[ 17/16 → 9/8 | note:c4 s:sawtooth cutoff:600 bpdepthfrequency:500 ]", + "[ 9/8 → 19/16 | note:c s:sawtooth cutoff:600 bpdepthfrequency:500 ]", + "[ 19/16 → 5/4 | note:c s:sawtooth cutoff:600 bpdepthfrequency:500 ]", + "[ 5/4 → 21/16 | note:c# s:sawtooth cutoff:600 bpdepthfrequency:500 ]", + "[ 21/16 → 11/8 | note:c s:sawtooth cutoff:600 bpdepthfrequency:500 ]", + "[ 11/8 → 23/16 | note:c s:sawtooth cutoff:600 bpdepthfrequency:500 ]", + "[ 23/16 → 3/2 | note:c4 s:sawtooth cutoff:600 bpdepthfrequency:500 ]", + "[ 3/2 → 25/16 | note:c s:sawtooth cutoff:600 bpdepthfrequency:500 ]", + "[ 25/16 → 13/8 | note:c s:sawtooth cutoff:600 bpdepthfrequency:500 ]", + "[ 13/8 → 27/16 | note:c# s:sawtooth cutoff:600 bpdepthfrequency:500 ]", + "[ 27/16 → 7/4 | note:c s:sawtooth cutoff:600 bpdepthfrequency:500 ]", + "[ 7/4 → 29/16 | note:c s:sawtooth cutoff:600 bpdepthfrequency:500 ]", + "[ 29/16 → 15/8 | note:c4 s:sawtooth cutoff:600 bpdepthfrequency:500 ]", + "[ 15/8 → 31/16 | note:c s:sawtooth cutoff:600 bpdepthfrequency:500 ]", + "[ 31/16 → 2/1 | note:c s:sawtooth cutoff:600 bpdepthfrequency:500 ]", + "[ 2/1 → 33/16 | note:c# s:sawtooth cutoff:600 bpdepthfrequency:100 ]", + "[ 33/16 → 17/8 | note:c s:sawtooth cutoff:600 bpdepthfrequency:100 ]", + "[ 17/8 → 35/16 | note:c s:sawtooth cutoff:600 bpdepthfrequency:100 ]", + "[ 35/16 → 9/4 | note:c4 s:sawtooth cutoff:600 bpdepthfrequency:100 ]", + "[ 9/4 → 37/16 | note:c s:sawtooth cutoff:600 bpdepthfrequency:100 ]", + "[ 37/16 → 19/8 | note:c s:sawtooth cutoff:600 bpdepthfrequency:100 ]", + "[ 19/8 → 39/16 | note:c# s:sawtooth cutoff:600 bpdepthfrequency:100 ]", + "[ 39/16 → 5/2 | note:c s:sawtooth cutoff:600 bpdepthfrequency:100 ]", + "[ 5/2 → 41/16 | note:c s:sawtooth cutoff:600 bpdepthfrequency:100 ]", + "[ 41/16 → 21/8 | note:c4 s:sawtooth cutoff:600 bpdepthfrequency:100 ]", + "[ 21/8 → 43/16 | note:c s:sawtooth cutoff:600 bpdepthfrequency:100 ]", + "[ 43/16 → 11/4 | note:c s:sawtooth cutoff:600 bpdepthfrequency:100 ]", + "[ 11/4 → 45/16 | note:c# s:sawtooth cutoff:600 bpdepthfrequency:100 ]", + "[ 45/16 → 23/8 | note:c s:sawtooth cutoff:600 bpdepthfrequency:100 ]", + "[ 23/8 → 47/16 | note:c s:sawtooth cutoff:600 bpdepthfrequency:100 ]", + "[ 47/16 → 3/1 | note:c4 s:sawtooth cutoff:600 bpdepthfrequency:100 ]", + "[ 3/1 → 49/16 | note:c s:sawtooth cutoff:600 bpdepthfrequency:0 ]", + "[ 49/16 → 25/8 | note:c s:sawtooth cutoff:600 bpdepthfrequency:0 ]", + "[ 25/8 → 51/16 | note:c# s:sawtooth cutoff:600 bpdepthfrequency:0 ]", + "[ 51/16 → 13/4 | note:c s:sawtooth cutoff:600 bpdepthfrequency:0 ]", + "[ 13/4 → 53/16 | note:c s:sawtooth cutoff:600 bpdepthfrequency:0 ]", + "[ 53/16 → 27/8 | note:c4 s:sawtooth cutoff:600 bpdepthfrequency:0 ]", + "[ 27/8 → 55/16 | note:c s:sawtooth cutoff:600 bpdepthfrequency:0 ]", + "[ 55/16 → 7/2 | note:c s:sawtooth cutoff:600 bpdepthfrequency:0 ]", + "[ 7/2 → 57/16 | note:c# s:sawtooth cutoff:600 bpdepthfrequency:0 ]", + "[ 57/16 → 29/8 | note:c s:sawtooth cutoff:600 bpdepthfrequency:0 ]", + "[ 29/8 → 59/16 | note:c s:sawtooth cutoff:600 bpdepthfrequency:0 ]", + "[ 59/16 → 15/4 | note:c4 s:sawtooth cutoff:600 bpdepthfrequency:0 ]", + "[ 15/4 → 61/16 | note:c s:sawtooth cutoff:600 bpdepthfrequency:0 ]", + "[ 61/16 → 31/8 | note:c s:sawtooth cutoff:600 bpdepthfrequency:0 ]", + "[ 31/8 → 63/16 | note:c# s:sawtooth cutoff:600 bpdepthfrequency:0 ]", + "[ 63/16 → 4/1 | note:c s:sawtooth cutoff:600 bpdepthfrequency:0 ]", +] +`; + exports[`runs examples > example "bpenv" example index 0 1`] = ` [ "[ 0/1 → 1/4 | note:c2 s:sawtooth bandf:500 bpattack:0.5 bpenv:4 ]", @@ -4823,6 +4892,75 @@ exports[`runs examples > example "hpdecay" example index 0 1`] = ` ] `; +exports[`runs examples > example "hpdepthfrequency" example index 0 1`] = ` +[ + "[ 0/1 → 1/16 | note:c s:sawtooth cutoff:600 hpdepthfrequency:200 ]", + "[ 1/16 → 1/8 | note:c s:sawtooth cutoff:600 hpdepthfrequency:200 ]", + "[ 1/8 → 3/16 | note:c# s:sawtooth cutoff:600 hpdepthfrequency:200 ]", + "[ 3/16 → 1/4 | note:c s:sawtooth cutoff:600 hpdepthfrequency:200 ]", + "[ 1/4 → 5/16 | note:c s:sawtooth cutoff:600 hpdepthfrequency:200 ]", + "[ 5/16 → 3/8 | note:c4 s:sawtooth cutoff:600 hpdepthfrequency:200 ]", + "[ 3/8 → 7/16 | note:c s:sawtooth cutoff:600 hpdepthfrequency:200 ]", + "[ 7/16 → 1/2 | note:c s:sawtooth cutoff:600 hpdepthfrequency:200 ]", + "[ 1/2 → 9/16 | note:c# s:sawtooth cutoff:600 hpdepthfrequency:200 ]", + "[ 9/16 → 5/8 | note:c s:sawtooth cutoff:600 hpdepthfrequency:200 ]", + "[ 5/8 → 11/16 | note:c s:sawtooth cutoff:600 hpdepthfrequency:200 ]", + "[ 11/16 → 3/4 | note:c4 s:sawtooth cutoff:600 hpdepthfrequency:200 ]", + "[ 3/4 → 13/16 | note:c s:sawtooth cutoff:600 hpdepthfrequency:200 ]", + "[ 13/16 → 7/8 | note:c s:sawtooth cutoff:600 hpdepthfrequency:200 ]", + "[ 7/8 → 15/16 | note:c# s:sawtooth cutoff:600 hpdepthfrequency:200 ]", + "[ 15/16 → 1/1 | note:c s:sawtooth cutoff:600 hpdepthfrequency:200 ]", + "[ 1/1 → 17/16 | note:c s:sawtooth cutoff:600 hpdepthfrequency:500 ]", + "[ 17/16 → 9/8 | note:c4 s:sawtooth cutoff:600 hpdepthfrequency:500 ]", + "[ 9/8 → 19/16 | note:c s:sawtooth cutoff:600 hpdepthfrequency:500 ]", + "[ 19/16 → 5/4 | note:c s:sawtooth cutoff:600 hpdepthfrequency:500 ]", + "[ 5/4 → 21/16 | note:c# s:sawtooth cutoff:600 hpdepthfrequency:500 ]", + "[ 21/16 → 11/8 | note:c s:sawtooth cutoff:600 hpdepthfrequency:500 ]", + "[ 11/8 → 23/16 | note:c s:sawtooth cutoff:600 hpdepthfrequency:500 ]", + "[ 23/16 → 3/2 | note:c4 s:sawtooth cutoff:600 hpdepthfrequency:500 ]", + "[ 3/2 → 25/16 | note:c s:sawtooth cutoff:600 hpdepthfrequency:500 ]", + "[ 25/16 → 13/8 | note:c s:sawtooth cutoff:600 hpdepthfrequency:500 ]", + "[ 13/8 → 27/16 | note:c# s:sawtooth cutoff:600 hpdepthfrequency:500 ]", + "[ 27/16 → 7/4 | note:c s:sawtooth cutoff:600 hpdepthfrequency:500 ]", + "[ 7/4 → 29/16 | note:c s:sawtooth cutoff:600 hpdepthfrequency:500 ]", + "[ 29/16 → 15/8 | note:c4 s:sawtooth cutoff:600 hpdepthfrequency:500 ]", + "[ 15/8 → 31/16 | note:c s:sawtooth cutoff:600 hpdepthfrequency:500 ]", + "[ 31/16 → 2/1 | note:c s:sawtooth cutoff:600 hpdepthfrequency:500 ]", + "[ 2/1 → 33/16 | note:c# s:sawtooth cutoff:600 hpdepthfrequency:100 ]", + "[ 33/16 → 17/8 | note:c s:sawtooth cutoff:600 hpdepthfrequency:100 ]", + "[ 17/8 → 35/16 | note:c s:sawtooth cutoff:600 hpdepthfrequency:100 ]", + "[ 35/16 → 9/4 | note:c4 s:sawtooth cutoff:600 hpdepthfrequency:100 ]", + "[ 9/4 → 37/16 | note:c s:sawtooth cutoff:600 hpdepthfrequency:100 ]", + "[ 37/16 → 19/8 | note:c s:sawtooth cutoff:600 hpdepthfrequency:100 ]", + "[ 19/8 → 39/16 | note:c# s:sawtooth cutoff:600 hpdepthfrequency:100 ]", + "[ 39/16 → 5/2 | note:c s:sawtooth cutoff:600 hpdepthfrequency:100 ]", + "[ 5/2 → 41/16 | note:c s:sawtooth cutoff:600 hpdepthfrequency:100 ]", + "[ 41/16 → 21/8 | note:c4 s:sawtooth cutoff:600 hpdepthfrequency:100 ]", + "[ 21/8 → 43/16 | note:c s:sawtooth cutoff:600 hpdepthfrequency:100 ]", + "[ 43/16 → 11/4 | note:c s:sawtooth cutoff:600 hpdepthfrequency:100 ]", + "[ 11/4 → 45/16 | note:c# s:sawtooth cutoff:600 hpdepthfrequency:100 ]", + "[ 45/16 → 23/8 | note:c s:sawtooth cutoff:600 hpdepthfrequency:100 ]", + "[ 23/8 → 47/16 | note:c s:sawtooth cutoff:600 hpdepthfrequency:100 ]", + "[ 47/16 → 3/1 | note:c4 s:sawtooth cutoff:600 hpdepthfrequency:100 ]", + "[ 3/1 → 49/16 | note:c s:sawtooth cutoff:600 hpdepthfrequency:0 ]", + "[ 49/16 → 25/8 | note:c s:sawtooth cutoff:600 hpdepthfrequency:0 ]", + "[ 25/8 → 51/16 | note:c# s:sawtooth cutoff:600 hpdepthfrequency:0 ]", + "[ 51/16 → 13/4 | note:c s:sawtooth cutoff:600 hpdepthfrequency:0 ]", + "[ 13/4 → 53/16 | note:c s:sawtooth cutoff:600 hpdepthfrequency:0 ]", + "[ 53/16 → 27/8 | note:c4 s:sawtooth cutoff:600 hpdepthfrequency:0 ]", + "[ 27/8 → 55/16 | note:c s:sawtooth cutoff:600 hpdepthfrequency:0 ]", + "[ 55/16 → 7/2 | note:c s:sawtooth cutoff:600 hpdepthfrequency:0 ]", + "[ 7/2 → 57/16 | note:c# s:sawtooth cutoff:600 hpdepthfrequency:0 ]", + "[ 57/16 → 29/8 | note:c s:sawtooth cutoff:600 hpdepthfrequency:0 ]", + "[ 29/8 → 59/16 | note:c s:sawtooth cutoff:600 hpdepthfrequency:0 ]", + "[ 59/16 → 15/4 | note:c4 s:sawtooth cutoff:600 hpdepthfrequency:0 ]", + "[ 15/4 → 61/16 | note:c s:sawtooth cutoff:600 hpdepthfrequency:0 ]", + "[ 61/16 → 31/8 | note:c s:sawtooth cutoff:600 hpdepthfrequency:0 ]", + "[ 31/8 → 63/16 | note:c# s:sawtooth cutoff:600 hpdepthfrequency:0 ]", + "[ 63/16 → 4/1 | note:c s:sawtooth cutoff:600 hpdepthfrequency:0 ]", +] +`; + exports[`runs examples > example "hpenv" example index 0 1`] = ` [ "[ 0/1 → 1/4 | note:c2 s:sawtooth hcutoff:500 hpattack:0.5 hpenv:4 ]", @@ -6006,6 +6144,144 @@ exports[`runs examples > example "lpdecay" example index 0 1`] = ` ] `; +exports[`runs examples > example "lpdepth" example index 0 1`] = ` +[ + "[ 0/1 → 1/16 | note:c s:sawtooth cutoff:600 lpdepth:1 ]", + "[ 1/16 → 1/8 | note:c s:sawtooth cutoff:600 lpdepth:1 ]", + "[ 1/8 → 3/16 | note:c# s:sawtooth cutoff:600 lpdepth:1 ]", + "[ 3/16 → 1/4 | note:c s:sawtooth cutoff:600 lpdepth:1 ]", + "[ 1/4 → 5/16 | note:c s:sawtooth cutoff:600 lpdepth:1 ]", + "[ 5/16 → 3/8 | note:c4 s:sawtooth cutoff:600 lpdepth:1 ]", + "[ 3/8 → 7/16 | note:c s:sawtooth cutoff:600 lpdepth:1 ]", + "[ 7/16 → 1/2 | note:c s:sawtooth cutoff:600 lpdepth:1 ]", + "[ 1/2 → 9/16 | note:c# s:sawtooth cutoff:600 lpdepth:1 ]", + "[ 9/16 → 5/8 | note:c s:sawtooth cutoff:600 lpdepth:1 ]", + "[ 5/8 → 11/16 | note:c s:sawtooth cutoff:600 lpdepth:1 ]", + "[ 11/16 → 3/4 | note:c4 s:sawtooth cutoff:600 lpdepth:1 ]", + "[ 3/4 → 13/16 | note:c s:sawtooth cutoff:600 lpdepth:1 ]", + "[ 13/16 → 7/8 | note:c s:sawtooth cutoff:600 lpdepth:1 ]", + "[ 7/8 → 15/16 | note:c# s:sawtooth cutoff:600 lpdepth:1 ]", + "[ 15/16 → 1/1 | note:c s:sawtooth cutoff:600 lpdepth:1 ]", + "[ 1/1 → 17/16 | note:c s:sawtooth cutoff:600 lpdepth:0.5 ]", + "[ 17/16 → 9/8 | note:c4 s:sawtooth cutoff:600 lpdepth:0.5 ]", + "[ 9/8 → 19/16 | note:c s:sawtooth cutoff:600 lpdepth:0.5 ]", + "[ 19/16 → 5/4 | note:c s:sawtooth cutoff:600 lpdepth:0.5 ]", + "[ 5/4 → 21/16 | note:c# s:sawtooth cutoff:600 lpdepth:0.5 ]", + "[ 21/16 → 11/8 | note:c s:sawtooth cutoff:600 lpdepth:0.5 ]", + "[ 11/8 → 23/16 | note:c s:sawtooth cutoff:600 lpdepth:0.5 ]", + "[ 23/16 → 3/2 | note:c4 s:sawtooth cutoff:600 lpdepth:0.5 ]", + "[ 3/2 → 25/16 | note:c s:sawtooth cutoff:600 lpdepth:0.5 ]", + "[ 25/16 → 13/8 | note:c s:sawtooth cutoff:600 lpdepth:0.5 ]", + "[ 13/8 → 27/16 | note:c# s:sawtooth cutoff:600 lpdepth:0.5 ]", + "[ 27/16 → 7/4 | note:c s:sawtooth cutoff:600 lpdepth:0.5 ]", + "[ 7/4 → 29/16 | note:c s:sawtooth cutoff:600 lpdepth:0.5 ]", + "[ 29/16 → 15/8 | note:c4 s:sawtooth cutoff:600 lpdepth:0.5 ]", + "[ 15/8 → 31/16 | note:c s:sawtooth cutoff:600 lpdepth:0.5 ]", + "[ 31/16 → 2/1 | note:c s:sawtooth cutoff:600 lpdepth:0.5 ]", + "[ 2/1 → 33/16 | note:c# s:sawtooth cutoff:600 lpdepth:1.8 ]", + "[ 33/16 → 17/8 | note:c s:sawtooth cutoff:600 lpdepth:1.8 ]", + "[ 17/8 → 35/16 | note:c s:sawtooth cutoff:600 lpdepth:1.8 ]", + "[ 35/16 → 9/4 | note:c4 s:sawtooth cutoff:600 lpdepth:1.8 ]", + "[ 9/4 → 37/16 | note:c s:sawtooth cutoff:600 lpdepth:1.8 ]", + "[ 37/16 → 19/8 | note:c s:sawtooth cutoff:600 lpdepth:1.8 ]", + "[ 19/8 → 39/16 | note:c# s:sawtooth cutoff:600 lpdepth:1.8 ]", + "[ 39/16 → 5/2 | note:c s:sawtooth cutoff:600 lpdepth:1.8 ]", + "[ 5/2 → 41/16 | note:c s:sawtooth cutoff:600 lpdepth:1.8 ]", + "[ 41/16 → 21/8 | note:c4 s:sawtooth cutoff:600 lpdepth:1.8 ]", + "[ 21/8 → 43/16 | note:c s:sawtooth cutoff:600 lpdepth:1.8 ]", + "[ 43/16 → 11/4 | note:c s:sawtooth cutoff:600 lpdepth:1.8 ]", + "[ 11/4 → 45/16 | note:c# s:sawtooth cutoff:600 lpdepth:1.8 ]", + "[ 45/16 → 23/8 | note:c s:sawtooth cutoff:600 lpdepth:1.8 ]", + "[ 23/8 → 47/16 | note:c s:sawtooth cutoff:600 lpdepth:1.8 ]", + "[ 47/16 → 3/1 | note:c4 s:sawtooth cutoff:600 lpdepth:1.8 ]", + "[ 3/1 → 49/16 | note:c s:sawtooth cutoff:600 lpdepth:0 ]", + "[ 49/16 → 25/8 | note:c s:sawtooth cutoff:600 lpdepth:0 ]", + "[ 25/8 → 51/16 | note:c# s:sawtooth cutoff:600 lpdepth:0 ]", + "[ 51/16 → 13/4 | note:c s:sawtooth cutoff:600 lpdepth:0 ]", + "[ 13/4 → 53/16 | note:c s:sawtooth cutoff:600 lpdepth:0 ]", + "[ 53/16 → 27/8 | note:c4 s:sawtooth cutoff:600 lpdepth:0 ]", + "[ 27/8 → 55/16 | note:c s:sawtooth cutoff:600 lpdepth:0 ]", + "[ 55/16 → 7/2 | note:c s:sawtooth cutoff:600 lpdepth:0 ]", + "[ 7/2 → 57/16 | note:c# s:sawtooth cutoff:600 lpdepth:0 ]", + "[ 57/16 → 29/8 | note:c s:sawtooth cutoff:600 lpdepth:0 ]", + "[ 29/8 → 59/16 | note:c s:sawtooth cutoff:600 lpdepth:0 ]", + "[ 59/16 → 15/4 | note:c4 s:sawtooth cutoff:600 lpdepth:0 ]", + "[ 15/4 → 61/16 | note:c s:sawtooth cutoff:600 lpdepth:0 ]", + "[ 61/16 → 31/8 | note:c s:sawtooth cutoff:600 lpdepth:0 ]", + "[ 31/8 → 63/16 | note:c# s:sawtooth cutoff:600 lpdepth:0 ]", + "[ 63/16 → 4/1 | note:c s:sawtooth cutoff:600 lpdepth:0 ]", +] +`; + +exports[`runs examples > example "lpdepthfrequency" example index 0 1`] = ` +[ + "[ 0/1 → 1/16 | note:c s:sawtooth cutoff:600 lpdepthfrequency:200 ]", + "[ 1/16 → 1/8 | note:c s:sawtooth cutoff:600 lpdepthfrequency:200 ]", + "[ 1/8 → 3/16 | note:c# s:sawtooth cutoff:600 lpdepthfrequency:200 ]", + "[ 3/16 → 1/4 | note:c s:sawtooth cutoff:600 lpdepthfrequency:200 ]", + "[ 1/4 → 5/16 | note:c s:sawtooth cutoff:600 lpdepthfrequency:200 ]", + "[ 5/16 → 3/8 | note:c4 s:sawtooth cutoff:600 lpdepthfrequency:200 ]", + "[ 3/8 → 7/16 | note:c s:sawtooth cutoff:600 lpdepthfrequency:200 ]", + "[ 7/16 → 1/2 | note:c s:sawtooth cutoff:600 lpdepthfrequency:200 ]", + "[ 1/2 → 9/16 | note:c# s:sawtooth cutoff:600 lpdepthfrequency:200 ]", + "[ 9/16 → 5/8 | note:c s:sawtooth cutoff:600 lpdepthfrequency:200 ]", + "[ 5/8 → 11/16 | note:c s:sawtooth cutoff:600 lpdepthfrequency:200 ]", + "[ 11/16 → 3/4 | note:c4 s:sawtooth cutoff:600 lpdepthfrequency:200 ]", + "[ 3/4 → 13/16 | note:c s:sawtooth cutoff:600 lpdepthfrequency:200 ]", + "[ 13/16 → 7/8 | note:c s:sawtooth cutoff:600 lpdepthfrequency:200 ]", + "[ 7/8 → 15/16 | note:c# s:sawtooth cutoff:600 lpdepthfrequency:200 ]", + "[ 15/16 → 1/1 | note:c s:sawtooth cutoff:600 lpdepthfrequency:200 ]", + "[ 1/1 → 17/16 | note:c s:sawtooth cutoff:600 lpdepthfrequency:500 ]", + "[ 17/16 → 9/8 | note:c4 s:sawtooth cutoff:600 lpdepthfrequency:500 ]", + "[ 9/8 → 19/16 | note:c s:sawtooth cutoff:600 lpdepthfrequency:500 ]", + "[ 19/16 → 5/4 | note:c s:sawtooth cutoff:600 lpdepthfrequency:500 ]", + "[ 5/4 → 21/16 | note:c# s:sawtooth cutoff:600 lpdepthfrequency:500 ]", + "[ 21/16 → 11/8 | note:c s:sawtooth cutoff:600 lpdepthfrequency:500 ]", + "[ 11/8 → 23/16 | note:c s:sawtooth cutoff:600 lpdepthfrequency:500 ]", + "[ 23/16 → 3/2 | note:c4 s:sawtooth cutoff:600 lpdepthfrequency:500 ]", + "[ 3/2 → 25/16 | note:c s:sawtooth cutoff:600 lpdepthfrequency:500 ]", + "[ 25/16 → 13/8 | note:c s:sawtooth cutoff:600 lpdepthfrequency:500 ]", + "[ 13/8 → 27/16 | note:c# s:sawtooth cutoff:600 lpdepthfrequency:500 ]", + "[ 27/16 → 7/4 | note:c s:sawtooth cutoff:600 lpdepthfrequency:500 ]", + "[ 7/4 → 29/16 | note:c s:sawtooth cutoff:600 lpdepthfrequency:500 ]", + "[ 29/16 → 15/8 | note:c4 s:sawtooth cutoff:600 lpdepthfrequency:500 ]", + "[ 15/8 → 31/16 | note:c s:sawtooth cutoff:600 lpdepthfrequency:500 ]", + "[ 31/16 → 2/1 | note:c s:sawtooth cutoff:600 lpdepthfrequency:500 ]", + "[ 2/1 → 33/16 | note:c# s:sawtooth cutoff:600 lpdepthfrequency:100 ]", + "[ 33/16 → 17/8 | note:c s:sawtooth cutoff:600 lpdepthfrequency:100 ]", + "[ 17/8 → 35/16 | note:c s:sawtooth cutoff:600 lpdepthfrequency:100 ]", + "[ 35/16 → 9/4 | note:c4 s:sawtooth cutoff:600 lpdepthfrequency:100 ]", + "[ 9/4 → 37/16 | note:c s:sawtooth cutoff:600 lpdepthfrequency:100 ]", + "[ 37/16 → 19/8 | note:c s:sawtooth cutoff:600 lpdepthfrequency:100 ]", + "[ 19/8 → 39/16 | note:c# s:sawtooth cutoff:600 lpdepthfrequency:100 ]", + "[ 39/16 → 5/2 | note:c s:sawtooth cutoff:600 lpdepthfrequency:100 ]", + "[ 5/2 → 41/16 | note:c s:sawtooth cutoff:600 lpdepthfrequency:100 ]", + "[ 41/16 → 21/8 | note:c4 s:sawtooth cutoff:600 lpdepthfrequency:100 ]", + "[ 21/8 → 43/16 | note:c s:sawtooth cutoff:600 lpdepthfrequency:100 ]", + "[ 43/16 → 11/4 | note:c s:sawtooth cutoff:600 lpdepthfrequency:100 ]", + "[ 11/4 → 45/16 | note:c# s:sawtooth cutoff:600 lpdepthfrequency:100 ]", + "[ 45/16 → 23/8 | note:c s:sawtooth cutoff:600 lpdepthfrequency:100 ]", + "[ 23/8 → 47/16 | note:c s:sawtooth cutoff:600 lpdepthfrequency:100 ]", + "[ 47/16 → 3/1 | note:c4 s:sawtooth cutoff:600 lpdepthfrequency:100 ]", + "[ 3/1 → 49/16 | note:c s:sawtooth cutoff:600 lpdepthfrequency:0 ]", + "[ 49/16 → 25/8 | note:c s:sawtooth cutoff:600 lpdepthfrequency:0 ]", + "[ 25/8 → 51/16 | note:c# s:sawtooth cutoff:600 lpdepthfrequency:0 ]", + "[ 51/16 → 13/4 | note:c s:sawtooth cutoff:600 lpdepthfrequency:0 ]", + "[ 13/4 → 53/16 | note:c s:sawtooth cutoff:600 lpdepthfrequency:0 ]", + "[ 53/16 → 27/8 | note:c4 s:sawtooth cutoff:600 lpdepthfrequency:0 ]", + "[ 27/8 → 55/16 | note:c s:sawtooth cutoff:600 lpdepthfrequency:0 ]", + "[ 55/16 → 7/2 | note:c s:sawtooth cutoff:600 lpdepthfrequency:0 ]", + "[ 7/2 → 57/16 | note:c# s:sawtooth cutoff:600 lpdepthfrequency:0 ]", + "[ 57/16 → 29/8 | note:c s:sawtooth cutoff:600 lpdepthfrequency:0 ]", + "[ 29/8 → 59/16 | note:c s:sawtooth cutoff:600 lpdepthfrequency:0 ]", + "[ 59/16 → 15/4 | note:c4 s:sawtooth cutoff:600 lpdepthfrequency:0 ]", + "[ 15/4 → 61/16 | note:c s:sawtooth cutoff:600 lpdepthfrequency:0 ]", + "[ 61/16 → 31/8 | note:c s:sawtooth cutoff:600 lpdepthfrequency:0 ]", + "[ 31/8 → 63/16 | note:c# s:sawtooth cutoff:600 lpdepthfrequency:0 ]", + "[ 63/16 → 4/1 | note:c s:sawtooth cutoff:600 lpdepthfrequency:0 ]", +] +`; + exports[`runs examples > example "lpenv" example index 0 1`] = ` [ "[ 0/1 → 1/4 | note:c2 s:sawtooth cutoff:300 lpattack:0.5 lpenv:4 ]", @@ -6194,6 +6470,75 @@ exports[`runs examples > example "lpq" example index 0 1`] = ` ] `; +exports[`runs examples > example "lprate" example index 0 1`] = ` +[ + "[ 0/1 → 1/16 | note:c s:sawtooth cutoff:600 lprate:4 ]", + "[ 1/16 → 1/8 | note:c s:sawtooth cutoff:600 lprate:4 ]", + "[ 1/8 → 3/16 | note:c# s:sawtooth cutoff:600 lprate:4 ]", + "[ 3/16 → 1/4 | note:c s:sawtooth cutoff:600 lprate:4 ]", + "[ 1/4 → 5/16 | note:c s:sawtooth cutoff:600 lprate:4 ]", + "[ 5/16 → 3/8 | note:c4 s:sawtooth cutoff:600 lprate:4 ]", + "[ 3/8 → 7/16 | note:c s:sawtooth cutoff:600 lprate:4 ]", + "[ 7/16 → 1/2 | note:c s:sawtooth cutoff:600 lprate:4 ]", + "[ 1/2 → 9/16 | note:c# s:sawtooth cutoff:600 lprate:4 ]", + "[ 9/16 → 5/8 | note:c s:sawtooth cutoff:600 lprate:4 ]", + "[ 5/8 → 11/16 | note:c s:sawtooth cutoff:600 lprate:4 ]", + "[ 11/16 → 3/4 | note:c4 s:sawtooth cutoff:600 lprate:4 ]", + "[ 3/4 → 13/16 | note:c s:sawtooth cutoff:600 lprate:4 ]", + "[ 13/16 → 7/8 | note:c s:sawtooth cutoff:600 lprate:4 ]", + "[ 7/8 → 15/16 | note:c# s:sawtooth cutoff:600 lprate:4 ]", + "[ 15/16 → 1/1 | note:c s:sawtooth cutoff:600 lprate:4 ]", + "[ 1/1 → 17/16 | note:c s:sawtooth cutoff:600 lprate:8 ]", + "[ 17/16 → 9/8 | note:c4 s:sawtooth cutoff:600 lprate:8 ]", + "[ 9/8 → 19/16 | note:c s:sawtooth cutoff:600 lprate:8 ]", + "[ 19/16 → 5/4 | note:c s:sawtooth cutoff:600 lprate:8 ]", + "[ 5/4 → 21/16 | note:c# s:sawtooth cutoff:600 lprate:8 ]", + "[ 21/16 → 11/8 | note:c s:sawtooth cutoff:600 lprate:8 ]", + "[ 11/8 → 23/16 | note:c s:sawtooth cutoff:600 lprate:8 ]", + "[ 23/16 → 3/2 | note:c4 s:sawtooth cutoff:600 lprate:8 ]", + "[ 3/2 → 25/16 | note:c s:sawtooth cutoff:600 lprate:8 ]", + "[ 25/16 → 13/8 | note:c s:sawtooth cutoff:600 lprate:8 ]", + "[ 13/8 → 27/16 | note:c# s:sawtooth cutoff:600 lprate:8 ]", + "[ 27/16 → 7/4 | note:c s:sawtooth cutoff:600 lprate:8 ]", + "[ 7/4 → 29/16 | note:c s:sawtooth cutoff:600 lprate:8 ]", + "[ 29/16 → 15/8 | note:c4 s:sawtooth cutoff:600 lprate:8 ]", + "[ 15/8 → 31/16 | note:c s:sawtooth cutoff:600 lprate:8 ]", + "[ 31/16 → 2/1 | note:c s:sawtooth cutoff:600 lprate:8 ]", + "[ 2/1 → 33/16 | note:c# s:sawtooth cutoff:600 lprate:2 ]", + "[ 33/16 → 17/8 | note:c s:sawtooth cutoff:600 lprate:2 ]", + "[ 17/8 → 35/16 | note:c s:sawtooth cutoff:600 lprate:2 ]", + "[ 35/16 → 9/4 | note:c4 s:sawtooth cutoff:600 lprate:2 ]", + "[ 9/4 → 37/16 | note:c s:sawtooth cutoff:600 lprate:2 ]", + "[ 37/16 → 19/8 | note:c s:sawtooth cutoff:600 lprate:2 ]", + "[ 19/8 → 39/16 | note:c# s:sawtooth cutoff:600 lprate:2 ]", + "[ 39/16 → 5/2 | note:c s:sawtooth cutoff:600 lprate:2 ]", + "[ 5/2 → 41/16 | note:c s:sawtooth cutoff:600 lprate:2 ]", + "[ 41/16 → 21/8 | note:c4 s:sawtooth cutoff:600 lprate:2 ]", + "[ 21/8 → 43/16 | note:c s:sawtooth cutoff:600 lprate:2 ]", + "[ 43/16 → 11/4 | note:c s:sawtooth cutoff:600 lprate:2 ]", + "[ 11/4 → 45/16 | note:c# s:sawtooth cutoff:600 lprate:2 ]", + "[ 45/16 → 23/8 | note:c s:sawtooth cutoff:600 lprate:2 ]", + "[ 23/8 → 47/16 | note:c s:sawtooth cutoff:600 lprate:2 ]", + "[ 47/16 → 3/1 | note:c4 s:sawtooth cutoff:600 lprate:2 ]", + "[ 3/1 → 49/16 | note:c s:sawtooth cutoff:600 lprate:1 ]", + "[ 49/16 → 25/8 | note:c s:sawtooth cutoff:600 lprate:1 ]", + "[ 25/8 → 51/16 | note:c# s:sawtooth cutoff:600 lprate:1 ]", + "[ 51/16 → 13/4 | note:c s:sawtooth cutoff:600 lprate:1 ]", + "[ 13/4 → 53/16 | note:c s:sawtooth cutoff:600 lprate:1 ]", + "[ 53/16 → 27/8 | note:c4 s:sawtooth cutoff:600 lprate:1 ]", + "[ 27/8 → 55/16 | note:c s:sawtooth cutoff:600 lprate:1 ]", + "[ 55/16 → 7/2 | note:c s:sawtooth cutoff:600 lprate:1 ]", + "[ 7/2 → 57/16 | note:c# s:sawtooth cutoff:600 lprate:1 ]", + "[ 57/16 → 29/8 | note:c s:sawtooth cutoff:600 lprate:1 ]", + "[ 29/8 → 59/16 | note:c s:sawtooth cutoff:600 lprate:1 ]", + "[ 59/16 → 15/4 | note:c4 s:sawtooth cutoff:600 lprate:1 ]", + "[ 15/4 → 61/16 | note:c s:sawtooth cutoff:600 lprate:1 ]", + "[ 61/16 → 31/8 | note:c s:sawtooth cutoff:600 lprate:1 ]", + "[ 31/8 → 63/16 | note:c# s:sawtooth cutoff:600 lprate:1 ]", + "[ 63/16 → 4/1 | note:c s:sawtooth cutoff:600 lprate:1 ]", +] +`; + exports[`runs examples > example "lprelease" example index 0 1`] = ` [ "[ 0/1 → 1/4 | note:c2 s:sawtooth clip:0.5 cutoff:300 lpenv:4 lprelease:0.5 release:0.5 ]", @@ -6236,6 +6581,75 @@ exports[`runs examples > example "lpsustain" example index 0 1`] = ` ] `; +exports[`runs examples > example "lpsync" example index 0 1`] = ` +[ + "[ 0/1 → 1/16 | note:c s:sawtooth cutoff:600 lpsync:4 ]", + "[ 1/16 → 1/8 | note:c s:sawtooth cutoff:600 lpsync:4 ]", + "[ 1/8 → 3/16 | note:c# s:sawtooth cutoff:600 lpsync:4 ]", + "[ 3/16 → 1/4 | note:c s:sawtooth cutoff:600 lpsync:4 ]", + "[ 1/4 → 5/16 | note:c s:sawtooth cutoff:600 lpsync:4 ]", + "[ 5/16 → 3/8 | note:c4 s:sawtooth cutoff:600 lpsync:4 ]", + "[ 3/8 → 7/16 | note:c s:sawtooth cutoff:600 lpsync:4 ]", + "[ 7/16 → 1/2 | note:c s:sawtooth cutoff:600 lpsync:4 ]", + "[ 1/2 → 9/16 | note:c# s:sawtooth cutoff:600 lpsync:4 ]", + "[ 9/16 → 5/8 | note:c s:sawtooth cutoff:600 lpsync:4 ]", + "[ 5/8 → 11/16 | note:c s:sawtooth cutoff:600 lpsync:4 ]", + "[ 11/16 → 3/4 | note:c4 s:sawtooth cutoff:600 lpsync:4 ]", + "[ 3/4 → 13/16 | note:c s:sawtooth cutoff:600 lpsync:4 ]", + "[ 13/16 → 7/8 | note:c s:sawtooth cutoff:600 lpsync:4 ]", + "[ 7/8 → 15/16 | note:c# s:sawtooth cutoff:600 lpsync:4 ]", + "[ 15/16 → 1/1 | note:c s:sawtooth cutoff:600 lpsync:4 ]", + "[ 1/1 → 17/16 | note:c s:sawtooth cutoff:600 lpsync:8 ]", + "[ 17/16 → 9/8 | note:c4 s:sawtooth cutoff:600 lpsync:8 ]", + "[ 9/8 → 19/16 | note:c s:sawtooth cutoff:600 lpsync:8 ]", + "[ 19/16 → 5/4 | note:c s:sawtooth cutoff:600 lpsync:8 ]", + "[ 5/4 → 21/16 | note:c# s:sawtooth cutoff:600 lpsync:8 ]", + "[ 21/16 → 11/8 | note:c s:sawtooth cutoff:600 lpsync:8 ]", + "[ 11/8 → 23/16 | note:c s:sawtooth cutoff:600 lpsync:8 ]", + "[ 23/16 → 3/2 | note:c4 s:sawtooth cutoff:600 lpsync:8 ]", + "[ 3/2 → 25/16 | note:c s:sawtooth cutoff:600 lpsync:8 ]", + "[ 25/16 → 13/8 | note:c s:sawtooth cutoff:600 lpsync:8 ]", + "[ 13/8 → 27/16 | note:c# s:sawtooth cutoff:600 lpsync:8 ]", + "[ 27/16 → 7/4 | note:c s:sawtooth cutoff:600 lpsync:8 ]", + "[ 7/4 → 29/16 | note:c s:sawtooth cutoff:600 lpsync:8 ]", + "[ 29/16 → 15/8 | note:c4 s:sawtooth cutoff:600 lpsync:8 ]", + "[ 15/8 → 31/16 | note:c s:sawtooth cutoff:600 lpsync:8 ]", + "[ 31/16 → 2/1 | note:c s:sawtooth cutoff:600 lpsync:8 ]", + "[ 2/1 → 33/16 | note:c# s:sawtooth cutoff:600 lpsync:2 ]", + "[ 33/16 → 17/8 | note:c s:sawtooth cutoff:600 lpsync:2 ]", + "[ 17/8 → 35/16 | note:c s:sawtooth cutoff:600 lpsync:2 ]", + "[ 35/16 → 9/4 | note:c4 s:sawtooth cutoff:600 lpsync:2 ]", + "[ 9/4 → 37/16 | note:c s:sawtooth cutoff:600 lpsync:2 ]", + "[ 37/16 → 19/8 | note:c s:sawtooth cutoff:600 lpsync:2 ]", + "[ 19/8 → 39/16 | note:c# s:sawtooth cutoff:600 lpsync:2 ]", + "[ 39/16 → 5/2 | note:c s:sawtooth cutoff:600 lpsync:2 ]", + "[ 5/2 → 41/16 | note:c s:sawtooth cutoff:600 lpsync:2 ]", + "[ 41/16 → 21/8 | note:c4 s:sawtooth cutoff:600 lpsync:2 ]", + "[ 21/8 → 43/16 | note:c s:sawtooth cutoff:600 lpsync:2 ]", + "[ 43/16 → 11/4 | note:c s:sawtooth cutoff:600 lpsync:2 ]", + "[ 11/4 → 45/16 | note:c# s:sawtooth cutoff:600 lpsync:2 ]", + "[ 45/16 → 23/8 | note:c s:sawtooth cutoff:600 lpsync:2 ]", + "[ 23/8 → 47/16 | note:c s:sawtooth cutoff:600 lpsync:2 ]", + "[ 47/16 → 3/1 | note:c4 s:sawtooth cutoff:600 lpsync:2 ]", + "[ 3/1 → 49/16 | note:c s:sawtooth cutoff:600 lpsync:1 ]", + "[ 49/16 → 25/8 | note:c s:sawtooth cutoff:600 lpsync:1 ]", + "[ 25/8 → 51/16 | note:c# s:sawtooth cutoff:600 lpsync:1 ]", + "[ 51/16 → 13/4 | note:c s:sawtooth cutoff:600 lpsync:1 ]", + "[ 13/4 → 53/16 | note:c s:sawtooth cutoff:600 lpsync:1 ]", + "[ 53/16 → 27/8 | note:c4 s:sawtooth cutoff:600 lpsync:1 ]", + "[ 27/8 → 55/16 | note:c s:sawtooth cutoff:600 lpsync:1 ]", + "[ 55/16 → 7/2 | note:c s:sawtooth cutoff:600 lpsync:1 ]", + "[ 7/2 → 57/16 | note:c# s:sawtooth cutoff:600 lpsync:1 ]", + "[ 57/16 → 29/8 | note:c s:sawtooth cutoff:600 lpsync:1 ]", + "[ 29/8 → 59/16 | note:c s:sawtooth cutoff:600 lpsync:1 ]", + "[ 59/16 → 15/4 | note:c4 s:sawtooth cutoff:600 lpsync:1 ]", + "[ 15/4 → 61/16 | note:c s:sawtooth cutoff:600 lpsync:1 ]", + "[ 61/16 → 31/8 | note:c s:sawtooth cutoff:600 lpsync:1 ]", + "[ 31/8 → 63/16 | note:c# s:sawtooth cutoff:600 lpsync:1 ]", + "[ 63/16 → 4/1 | note:c s:sawtooth cutoff:600 lpsync:1 ]", +] +`; + exports[`runs examples > example "lrate" example index 0 1`] = ` [ "[ 0/1 → 1/1 | n:0 s:supersquare leslie:1 lrate:1 ]", @@ -6925,18 +7339,18 @@ exports[`runs examples > example "nrpv" example index 0 1`] = ` exports[`runs examples > example "octave" example index 0 1`] = ` [ - "[ 0/1 → 1/1 | n:0 s:supersquare octave:3 ]", - "[ 0/1 → 1/1 | n:4 s:supersquare octave:3 ]", - "[ 0/1 → 1/1 | n:7 s:supersquare octave:3 ]", - "[ 1/1 → 2/1 | n:0 s:supersquare octave:4 ]", - "[ 1/1 → 2/1 | n:4 s:supersquare octave:4 ]", - "[ 1/1 → 2/1 | n:7 s:supersquare octave:4 ]", - "[ 2/1 → 3/1 | n:0 s:supersquare octave:5 ]", - "[ 2/1 → 3/1 | n:4 s:supersquare octave:5 ]", - "[ 2/1 → 3/1 | n:7 s:supersquare octave:5 ]", - "[ 3/1 → 4/1 | n:0 s:supersquare octave:6 ]", - "[ 3/1 → 4/1 | n:4 s:supersquare octave:6 ]", - "[ 3/1 → 4/1 | n:7 s:supersquare octave:6 ]", + "[ 0/1 → 1/1 | note:F3 s:supersaw octave:0 ]", + "[ 0/1 → 1/1 | note:C4 s:supersaw octave:0 ]", + "[ 0/1 → 1/1 | note:F4 s:supersaw octave:0 ]", + "[ 1/1 → 2/1 | note:F3 s:supersaw octave:1 ]", + "[ 1/1 → 2/1 | note:C4 s:supersaw octave:1 ]", + "[ 1/1 → 2/1 | note:F4 s:supersaw octave:1 ]", + "[ 2/1 → 3/1 | note:F3 s:supersaw octave:2 ]", + "[ 2/1 → 3/1 | note:C4 s:supersaw octave:2 ]", + "[ 2/1 → 3/1 | note:F4 s:supersaw octave:2 ]", + "[ 3/1 → 4/1 | note:F3 s:supersaw octave:3 ]", + "[ 3/1 → 4/1 | note:C4 s:supersaw octave:3 ]", + "[ 3/1 → 4/1 | note:F4 s:supersaw octave:3 ]", ] `; @@ -8879,6 +9293,19 @@ exports[`runs examples > example "rev" example index 0 1`] = ` ] `; +exports[`runs examples > example "revv" example index 0 1`] = ` +[ + "[ 0/1 → 1/2 | note:g ]", + "[ 1/2 → 1/1 | note:e ]", + "[ 1/1 → 3/2 | note:d ]", + "[ 3/2 → 2/1 | note:c ]", + "[ 2/1 → 5/2 | note:g ]", + "[ 5/2 → 3/1 | note:e ]", + "[ 3/1 → 7/2 | note:d ]", + "[ 7/2 → 4/1 | note:c ]", +] +`; + exports[`runs examples > example "ribbon" example index 0 1`] = ` [ "[ 0/1 → 1/1 | note:d ]", @@ -12521,54 +12948,54 @@ exports[`runs examples > example "wchooseCycles" example index 0 1`] = ` exports[`runs examples > example "wchooseCycles" example index 1 1`] = ` [ - "[ 0/1 → 1/12 | s:bd ]", - "[ 1/12 → 1/6 | s:bd ]", - "[ 1/6 → 1/4 | s:bd ]", - "[ 1/4 → 1/3 | s:bd ]", - "[ 1/3 → 5/12 | s:bd ]", - "[ 5/12 → 1/2 | s:bd ]", - "[ 1/2 → 7/12 | s:sd ]", - "[ 7/12 → 2/3 | s:sd ]", - "[ 2/3 → 3/4 | s:sd ]", - "[ 3/4 → 5/6 | s:bd ]", - "[ 5/6 → 11/12 | s:bd ]", - "[ 11/12 → 1/1 | s:bd ]", - "[ 1/1 → 13/12 | s:bd ]", - "[ 13/12 → 7/6 | s:bd ]", - "[ 7/6 → 5/4 | s:bd ]", - "[ 5/4 → 4/3 | s:bd ]", - "[ 4/3 → 17/12 | s:bd ]", - "[ 17/12 → 3/2 | s:bd ]", - "[ 3/2 → 19/12 | s:hh ]", - "[ 19/12 → 5/3 | s:hh ]", - "[ 5/3 → 7/4 | s:hh ]", - "[ 7/4 → 11/6 | s:bd ]", - "[ 11/6 → 23/12 | s:bd ]", - "[ 23/12 → 2/1 | s:bd ]", - "[ 2/1 → 25/12 | s:hh ]", - "[ 25/12 → 13/6 | s:hh ]", - "[ 13/6 → 9/4 | s:hh ]", - "[ 9/4 → 7/3 | s:hh ]", - "[ 7/3 → 29/12 | s:hh ]", - "[ 29/12 → 5/2 | s:hh ]", - "[ 5/2 → 31/12 | s:bd ]", - "[ 31/12 → 8/3 | s:bd ]", - "[ 8/3 → 11/4 | s:bd ]", - "[ 11/4 → 17/6 | s:bd ]", - "[ 17/6 → 35/12 | s:bd ]", - "[ 35/12 → 3/1 | s:bd ]", - "[ 3/1 → 37/12 | s:bd ]", - "[ 37/12 → 19/6 | s:bd ]", - "[ 19/6 → 13/4 | s:bd ]", - "[ 13/4 → 10/3 | s:sd ]", - "[ 10/3 → 41/12 | s:sd ]", - "[ 41/12 → 7/2 | s:sd ]", - "[ 7/2 → 43/12 | s:hh ]", - "[ 43/12 → 11/3 | s:hh ]", - "[ 11/3 → 15/4 | s:hh ]", - "[ 15/4 → 23/6 | s:bd ]", - "[ 23/6 → 47/12 | s:bd ]", - "[ 47/12 → 4/1 | s:bd ]", + "[ 0/1 → 1/12 | note:c ]", + "[ 1/12 → 1/6 | note:c ]", + "[ 1/6 → 1/4 | note:c ]", + "[ 1/4 → 1/3 | note:c ]", + "[ 1/3 → 5/12 | note:c ]", + "[ 5/12 → 1/2 | note:c ]", + "[ 1/2 → 7/12 | note:f ]", + "[ 7/12 → 2/3 | note:f ]", + "[ 2/3 → 3/4 | note:f ]", + "[ 3/4 → 5/6 | note:c ]", + "[ 5/6 → 11/12 | note:c ]", + "[ 11/12 → 1/1 | note:c ]", + "[ 1/1 → 13/12 | note:c ]", + "[ 13/12 → 7/6 | note:c ]", + "[ 7/6 → 5/4 | note:c ]", + "[ 5/4 → 4/3 | note:c ]", + "[ 4/3 → 17/12 | note:c ]", + "[ 17/12 → 3/2 | note:c ]", + "[ 3/2 → 19/12 | note:a ]", + "[ 19/12 → 5/3 | note:a ]", + "[ 5/3 → 7/4 | note:a ]", + "[ 7/4 → 11/6 | note:c ]", + "[ 11/6 → 23/12 | note:c ]", + "[ 23/12 → 2/1 | note:c ]", + "[ 2/1 → 25/12 | note:a ]", + "[ 25/12 → 13/6 | note:a ]", + "[ 13/6 → 9/4 | note:a ]", + "[ 9/4 → 7/3 | note:a ]", + "[ 7/3 → 29/12 | note:a ]", + "[ 29/12 → 5/2 | note:a ]", + "[ 5/2 → 31/12 | note:c ]", + "[ 31/12 → 8/3 | note:c ]", + "[ 8/3 → 11/4 | note:c ]", + "[ 11/4 → 17/6 | note:c ]", + "[ 17/6 → 35/12 | note:c ]", + "[ 35/12 → 3/1 | note:c ]", + "[ 3/1 → 37/12 | note:c ]", + "[ 37/12 → 19/6 | note:c ]", + "[ 19/6 → 13/4 | note:c ]", + "[ 13/4 → 10/3 | note:f ]", + "[ 10/3 → 41/12 | note:f ]", + "[ 41/12 → 7/2 | note:f ]", + "[ 7/2 → 43/12 | note:a ]", + "[ 43/12 → 11/3 | note:a ]", + "[ 11/3 → 15/4 | note:a ]", + "[ 15/4 → 23/6 | note:c ]", + "[ 23/6 → 47/12 | note:c ]", + "[ 47/12 → 4/1 | note:c ]", ] `; diff --git a/tools/dbpatch/package.json b/tools/dbpatch/package.json index 3ee042ad9..21d79e199 100644 --- a/tools/dbpatch/package.json +++ b/tools/dbpatch/package.json @@ -1,4 +1,5 @@ { + "name": "dbpatch", "dependencies": { "csv": "^6.3.11" }, diff --git a/warm.js b/warm.js new file mode 100644 index 000000000..0d148dee9 --- /dev/null +++ b/warm.js @@ -0,0 +1,14 @@ +fetch('https://codeberg.org/api/v1/repos/uzu/strudel/pulls?state=closed&page=1') + .then((res) => res.json()) + .then((pulls) => { + const r = pulls + .filter((pull) => pull.merged) + .sort((a, b) => new Date(b.closed_at) - new Date(a.closed_at)) + .map((pull) => `${pull.closed_at} ${pull.title} by ${pull.user.login || '?'} in: [#${pull.number}](${pull.url}) `) + .join('\n'); + console.log(r); + }); + +/* + + */ diff --git a/website/src/repl/components/button/action-button.jsx b/website/src/repl/components/button/action-button.jsx index d589b7bed..de674b696 100644 --- a/website/src/repl/components/button/action-button.jsx +++ b/website/src/repl/components/button/action-button.jsx @@ -8,3 +8,34 @@ export function ActionButton({ children, label, labelIsHidden, className, ...but ); } + +export function SpecialActionButton(props) { + const { className, ...buttonProps } = props; + + return ( + + ); +} + +export function ActionInput({ label, className, ...props }) { + return ( + + ); +} + +export function SpecialActionInput({ className, ...props }) { + return ( + {props.label}} + /> + ); +} diff --git a/website/src/repl/components/panel/ImportPrebakeScriptButton.jsx b/website/src/repl/components/panel/ImportPrebakeScriptButton.jsx new file mode 100644 index 000000000..c997f6b6e --- /dev/null +++ b/website/src/repl/components/panel/ImportPrebakeScriptButton.jsx @@ -0,0 +1,29 @@ +import { errorLogger } from '@strudel/core'; +import { useSettings, storePrebakeScript } from '../../../settings.mjs'; +import { SpecialActionInput } from '../button/action-button'; + +async function importScript(script) { + const reader = new FileReader(); + reader.readAsText(script); + + reader.onload = () => { + const text = reader.result; + storePrebakeScript(text); + }; + + reader.onerror = () => { + errorLogger(new Error('failed to import prebake script'), 'importScript'); + }; +} +export function ImportPrebakeScriptButton() { + const settings = useSettings(); + + return ( + importScript(e.target.files[0])} + /> + ); +} diff --git a/website/src/repl/components/panel/SettingsTab.jsx b/website/src/repl/components/panel/SettingsTab.jsx index 85991f55c..f46fa661c 100644 --- a/website/src/repl/components/panel/SettingsTab.jsx +++ b/website/src/repl/components/panel/SettingsTab.jsx @@ -7,6 +7,8 @@ import { AudioDeviceSelector } from './AudioDeviceSelector.jsx'; import { AudioEngineTargetSelector } from './AudioEngineTargetSelector.jsx'; import { confirmDialog } from '../../util.mjs'; import { DEFAULT_MAX_POLYPHONY, setMaxPolyphony, setMultiChannelOrbits } from '@strudel/webaudio'; +import { ActionButton, SpecialActionButton } from '../button/action-button.jsx'; +import { ImportPrebakeScriptButton } from './ImportPrebakeScriptButton.jsx'; function Checkbox({ label, value, onChange, disabled = false }) { return ( @@ -113,6 +115,7 @@ export function SettingsTab({ started }) { isTabIndentationEnabled, isMultiCursorEnabled, patternAutoStart, + includePrebakeScriptInShare, } = useSettings(); const shouldAlwaysSync = isUdels(); const canChangeAudioDevice = AudioContext.prototype.setSinkId != null; @@ -204,6 +207,15 @@ export function SettingsTab({ started }) { /> + + + settingsMap.setKey('includePrebakeScriptInShare', cbEvent.target.checked)} + value={includePrebakeScriptInShare} + /> + + Try clicking the logo in the top left! - + ); diff --git a/website/src/repl/components/panel/SoundsTab.jsx b/website/src/repl/components/panel/SoundsTab.jsx index a9f10bf12..52fbf928c 100644 --- a/website/src/repl/components/panel/SoundsTab.jsx +++ b/website/src/repl/components/panel/SoundsTab.jsx @@ -14,6 +14,10 @@ import { prebake } from '@src/repl/prebake.mjs'; const getSamples = (samples) => Array.isArray(samples) ? samples.length : typeof samples === 'object' ? Object.values(samples).length : 1; +function wait(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + export function SoundsTab() { const sounds = useStore(soundMap); @@ -56,9 +60,7 @@ export function SoundsTab() { // holds mutable ref to current triggered sound const trigRef = useRef(); - - // Used to cycle through sound previews on banks with multiple sounds - let soundPreviewIdx = 0; + const numRef = useRef(0); // stop current sound on mouseup useEvent('mouseup', () => { @@ -66,6 +68,14 @@ export function SoundsTab() { trigRef.current = undefined; ref?.stop?.(getAudioContext().currentTime + 0.01); }); + useEvent('keydown', (e) => { + if (!isNaN(Number(e.key))) { + numRef.current = Number(e.key); + } + }); + useEvent('keyup', (e) => { + numRef.current = 0; + }); return (
setSearch(v)} /> @@ -115,25 +125,34 @@ export function SoundsTab() { const params = { note: ['synth', 'soundfont'].includes(data.type) ? 'a3' : undefined, s: name, - n: soundPreviewIdx, + n: numRef.current, clip: 1, release: 0.5, sustain: 1, duration: 0.5, }; - soundPreviewIdx++; const onended = () => trigRef.current?.node?.disconnect(); - try { - // Pre-load the sample by calling onTrigger with a future time - // This triggers the loading but schedules playback for later - const time = ctx.currentTime + 0.5; // Give 500ms for loading - const ref = await onTrigger(time, params, onended); - trigRef.current = ref; - if (ref?.node) { - connectToDestination(ref.node); + // Attempt to play the sample and retry every 200ms until 10 attempts have been reached + let errMsg; + for (let attempt = 0; attempt < 10; attempt++) { + try { + // Pre-load the sample by calling onTrigger with a future time + // This triggers the loading but schedules playback for later + const time = ctx.currentTime + 0.05; // Give 50ms for loading + const ref = await onTrigger(time, params, onended); + trigRef.current = ref; + if (ref?.node) { + connectToDestination(ref.node); + break; + } + } catch (err) { + errMsg = err; + } + if (attempt == 9) { + console.warn('Failed to trigger sound after 10 attempts' + (errMsg ? `: ${errMsg}` : '')); + } else { + await wait(200); } - } catch (err) { - console.warn('Failed to trigger sound:', err); } }} > diff --git a/website/src/repl/prebake.mjs b/website/src/repl/prebake.mjs index 8a6ccc7e0..53a2a6003 100644 --- a/website/src/repl/prebake.mjs +++ b/website/src/repl/prebake.mjs @@ -3,6 +3,8 @@ import { aliasBank, registerSynthSounds, registerZZFXSounds, samples } from '@st import { registerSamplesFromDB } from './idbutils.mjs'; import './piano.mjs'; import './files.mjs'; +import { settingsMap } from '@src/settings.mjs'; +import { evaluate } from '@strudel/transpiler'; const { BASE_URL } = import.meta.env; const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL; diff --git a/website/src/repl/useReplContext.jsx b/website/src/repl/useReplContext.jsx index ac30fe342..8dcb6b754 100644 --- a/website/src/repl/useReplContext.jsx +++ b/website/src/repl/useReplContext.jsx @@ -6,7 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th import { code2hash, getPerformanceTimeSeconds, logger, silence } from '@strudel/core'; import { getDrawContext } from '@strudel/draw'; -import { transpiler } from '@strudel/transpiler'; +import { evaluate, transpiler } from '@strudel/transpiler'; import { getAudioContextCurrentTime, webaudioOutput, @@ -63,11 +63,10 @@ async function getModule(name) { const initialCode = `// LOADING`; export function useReplContext() { - const { isSyncEnabled, audioEngineTarget } = useSettings(); + const { isSyncEnabled, audioEngineTarget, prebakeScript, includePrebakeScriptInShare } = useSettings(); const shouldUseWebaudio = audioEngineTarget !== audioEngineTargets.osc; const defaultOutput = shouldUseWebaudio ? webaudioOutput : superdirtOutput; const getTime = shouldUseWebaudio ? getAudioContextCurrentTime : getPerformanceTimeSeconds; - const init = useCallback(() => { const drawTime = [-2, 2]; const drawContext = getDrawContext(); @@ -84,7 +83,12 @@ export function useReplContext() { pattern: silence, drawTime, drawContext, - prebake: async () => Promise.all([modulesLoading, presets]), + prebake: async () => + Promise.all([modulesLoading, presets]).then(() => { + if (prebakeScript?.length) { + return evaluate(prebakeScript ?? ''); + } + }), onUpdateState: (state) => { setReplState({ ...state }); }, @@ -214,7 +218,13 @@ export function useReplContext() { editorRef.current.repl.evaluate(code); }; - const handleShare = async () => shareCode(replState.code); + const handleShare = async () => { + let code = replState.code; + if (includePrebakeScriptInShare) { + code = prebakeScript + '\n' + code; + } + shareCode(code); + }; const context = { started, pending, diff --git a/website/src/repl/util.mjs b/website/src/repl/util.mjs index ac27158f4..4a7cb26a8 100644 --- a/website/src/repl/util.mjs +++ b/website/src/repl/util.mjs @@ -1,11 +1,10 @@ -import { evalScope, hash2code, logger } from '@strudel/core'; +import { code2hash, evalScope, hash2code, logger } from '@strudel/core'; import { settingPatterns } from '../settings.mjs'; import { setVersionDefaults } from '@strudel/webaudio'; import { getMetadata } from '../metadata_parser'; import { isTauri } from '../tauri.mjs'; import './Repl.css'; import { createClient } from '@supabase/supabase-js'; -import { nanoid } from 'nanoid'; import { writeText } from '@tauri-apps/plugin-clipboard-manager'; import { $featuredPatterns /* , loadDBPatterns */ } from '@src/user_pattern_utils.mjs'; @@ -109,9 +108,8 @@ export function confirmDialog(msg) { }); } -let lastShared; - //RIP due to SPAM +// let lastShared; // export async function shareCode(codeToShare) { // // const codeToShare = activeCode || code; // if (lastShared === codeToShare) { @@ -146,9 +144,10 @@ let lastShared; // }); // } -export async function shareCode() { +export async function shareCode(codeToShare) { try { - const shareUrl = window.location.href; + const hash = '#' + code2hash(codeToShare); + const shareUrl = window.location.origin + window.location.pathname + hash; if (isTauri()) { await writeText(shareUrl); } else { diff --git a/website/src/settings.mjs b/website/src/settings.mjs index 7c62b0ded..8ec7d455a 100644 --- a/website/src/settings.mjs +++ b/website/src/settings.mjs @@ -45,11 +45,13 @@ export const defaultSettings = { isPanelOpen: true, togglePanelTrigger: 'click', //click | hover userPatterns: '{}', + prebakeScript: '', audioEngineTarget: audioEngineTargets.webaudio, isButtonRowHidden: false, isCSSAnimationDisabled: false, maxPolyphony: 128, multiChannelOrbits: false, + includePrebakeScriptInShare: true, }; let search = null; @@ -96,6 +98,7 @@ export function useSettings() { isPanelOpen: parseBoolean(state.isPanelOpen), userPatterns: userPatterns, multiChannelOrbits: parseBoolean(state.multiChannelOrbits), + includePrebakeScriptInShare: parseBoolean(state.includePrebakeScriptInShare), patternAutoStart: isUdels() ? false : state.patternAutoStart === undefined @@ -108,6 +111,8 @@ export const setActiveFooter = (tab) => settingsMap.setKey('activeFooter', tab); export const setPanelPinned = (bool) => settingsMap.setKey('isPanelPinned', bool); export const setIsPanelOpened = (bool) => settingsMap.setKey('isPanelOpen', bool); +export const storePrebakeScript = (script) => settingsMap.setKey('prebakeScript', script); + export const setIsZen = (active) => settingsMap.setKey('isZen', !!active); const patternSetting = (key) =>