mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-23 13:42:56 -04:00
Compare commits
144 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 16b5faf333 | |||
| e01621d0cc | |||
| 196f70ab6b | |||
| 1d4f2b81d8 | |||
| d28c8dac8e | |||
| eb18648a2a | |||
| 0ec9826de5 | |||
| af7855402f | |||
| 68b1cb87ca | |||
| 7a3e0fd3fe | |||
| 4061dd4cf8 | |||
| b523205590 | |||
| 956598da18 | |||
| e011f3b982 | |||
| cb591815ed | |||
| 6a4fd27f5b | |||
| d30df8d4cf | |||
| e10cd22c26 | |||
| 18dc4944bf | |||
| a6008e2700 | |||
| 2f8cabbb6b | |||
| ec26392509 | |||
| 30cd486f37 | |||
| cde0520ed9 | |||
| 405338e4f0 | |||
| afbcaa54d0 | |||
| 6a1c6081e0 | |||
| 3c4ba667c8 | |||
| 7fcb5262b8 | |||
| b824c45ba4 | |||
| e47e6c87fc | |||
| 69b5ba89cb | |||
| 33a51761ce | |||
| f4b1baac87 | |||
| d217119391 | |||
| d5d6284a90 | |||
| fb790de26c | |||
| ec6f6a1f1c | |||
| a126bc9b86 | |||
| a5c7fcc136 | |||
| d70b758065 | |||
| 9fdba6374c | |||
| af90baea33 | |||
| 692359309f | |||
| d65b908bda | |||
| 743e239e52 | |||
| f44a96a5ce | |||
| b94a686394 | |||
| 728552b669 | |||
| 8b427bc1dd | |||
| ae47d767ce | |||
| ab64451d65 | |||
| 89da9bbdb0 | |||
| 2bc208d8f5 | |||
| cfeff26d42 | |||
| e60ecba393 | |||
| f0d49e0e22 | |||
| 7ee6b933cc | |||
| a3947d1ccf | |||
| e86850b6d8 | |||
| 89a6efb69e | |||
| 31c694b596 | |||
| 7a4babf6e1 | |||
| d5e5ab83ac | |||
| 20016fd9aa | |||
| 132a124644 | |||
| 4caacb0de1 | |||
| 8325b4cfc4 | |||
| 3b2df0b725 | |||
| f80fdbc38a | |||
| 3a7b237765 | |||
| f5af55b63b | |||
| d5dfbd7aa4 | |||
| 6221099af8 | |||
| 22f1512697 | |||
| 6c0d550b6b | |||
| cf12e3e1d8 | |||
| 9a05a679aa | |||
| 321e76166a | |||
| bde17594ce | |||
| 7bbb653963 | |||
| 30e672a027 | |||
| f7f3aa7668 | |||
| adc3a9ac6c | |||
| 2518b2aed2 | |||
| 1d5f3a4f30 | |||
| 2efd56e331 | |||
| 5fa6cb4653 | |||
| 19fd0fc649 | |||
| e72e26eb81 | |||
| 1c4b05d55f | |||
| d453c861d3 | |||
| f637b8b0dd | |||
| 06bf17f444 | |||
| 21543956c6 | |||
| 4758effdc6 | |||
| 6f58e74bea | |||
| 907f03f3bf | |||
| 0435711051 | |||
| 8f6718b9e8 | |||
| aa77255d98 | |||
| 7267990e20 | |||
| 387e5db0fe | |||
| d9f63b8dfa | |||
| 8ed72b4573 | |||
| 74488f8480 | |||
| 8144ec46bd | |||
| dd2c808cd7 | |||
| 389a63a850 | |||
| 654c1a86f0 | |||
| 99beb4454d | |||
| 30cc46ea66 | |||
| ac3f477f30 | |||
| ea7dc7a732 | |||
| 573cd1ae41 | |||
| 641bd2f356 | |||
| f5ed6dbe79 | |||
| 7a3bea6f90 | |||
| cc10122b71 | |||
| b66c0d18b3 | |||
| 0331c91b80 | |||
| 4d5ebfc204 | |||
| 921cb17d33 | |||
| 832fd04a3d | |||
| bfa8fa3c75 | |||
| 91799f2014 | |||
| 4c49cd9297 | |||
| 70d2da81d8 | |||
| 47c85f8540 | |||
| fd03d1fdfb | |||
| 35016b5847 | |||
| 0fe7edc157 | |||
| d6de160224 | |||
| cc4452ce58 | |||
| bd73d96847 | |||
| 7cf9db4872 | |||
| c9381a11f9 | |||
| f4cf77f5c6 | |||
| e4070b86f8 | |||
| 9eeb6da3e6 | |||
| 869141b0a3 | |||
| ea017e7622 | |||
| 763f900c69 | |||
| 7f103e77e3 |
@@ -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)
|
||||||
+222
-18
@@ -72,6 +72,27 @@ export function registerControl(names, ...aliases) {
|
|||||||
return bag;
|
return bag;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function registerMultiControl(names, maxControls, ...aliases) {
|
||||||
|
names = Array.isArray(names) ? names : [names];
|
||||||
|
let bag = {};
|
||||||
|
for (let i = 1; i <= maxControls; i++) {
|
||||||
|
let theseAliases = [...aliases];
|
||||||
|
let theseNames = [...names];
|
||||||
|
if (i === 1) {
|
||||||
|
// adds e.g. fm1 as an alias for fm
|
||||||
|
const aliases1 = theseAliases.map((a) => `${a}1`);
|
||||||
|
const names1 = theseNames.map((n) => `${n}1`);
|
||||||
|
theseAliases = theseAliases.concat(aliases1).concat(names1);
|
||||||
|
} else {
|
||||||
|
theseAliases = theseAliases.map((a) => `${a}${i}`);
|
||||||
|
theseNames = theseNames.map((n) => `${n}${i}`);
|
||||||
|
}
|
||||||
|
const subBag = registerControl(theseNames, ...theseAliases);
|
||||||
|
bag = { ...bag, ...subBag };
|
||||||
|
}
|
||||||
|
return bag;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Select a sound / sample by name. When using mininotation, you can also optionally supply 'n' and 'gain' parameters
|
* Select a sound / sample by name. When using mininotation, you can also optionally supply 'n' and 'gain' parameters
|
||||||
* separated by ':'.
|
* separated by ':'.
|
||||||
@@ -435,6 +456,9 @@ export const { attack, att } = registerControl('attack', 'att');
|
|||||||
* Whole numbers and simple ratios sound more natural,
|
* Whole numbers and simple ratios sound more natural,
|
||||||
* while decimal numbers and complex ratios sound metallic.
|
* while decimal numbers and complex ratios sound metallic.
|
||||||
*
|
*
|
||||||
|
* A number may be added afterwards to control the harmonicity of
|
||||||
|
* any of the 8 individual FMs (e.g. `fmh2`)
|
||||||
|
*
|
||||||
* @name fmh
|
* @name fmh
|
||||||
* @param {number | Pattern} harmonicity
|
* @param {number | Pattern} harmonicity
|
||||||
* @example
|
* @example
|
||||||
@@ -444,25 +468,40 @@ export const { attack, att } = registerControl('attack', 'att');
|
|||||||
* ._scope()
|
* ._scope()
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export const { fmh } = registerControl(['fmh', 'fmi'], 'fmh');
|
export const { fmh, fmh1, fmh2, fmh3, fmh4, fmh5, fmh6, fmh7, fmh8 } = registerMultiControl(['fmh', 'fmi'], 8, 'fmh');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the Frequency Modulation of the synth.
|
* Sets the Frequency Modulation of the synth.
|
||||||
* Controls the modulation index, which defines the brightness of the sound.
|
* Controls the modulation index, which defines the brightness of the sound.
|
||||||
*
|
*
|
||||||
* @name fm
|
* A number may be added afterwards to control the modulation index of
|
||||||
|
* any of the 8 individual FMs (e.g. `fm3`). Also, FMs may be routed into
|
||||||
|
* each other with matrix commands like `fm13`, which would send `fm1` back into
|
||||||
|
* `fm3`
|
||||||
|
*
|
||||||
|
* @name fmi
|
||||||
* @param {number | Pattern} brightness modulation index
|
* @param {number | Pattern} brightness modulation index
|
||||||
* @synonyms fmi
|
* @synonyms fm
|
||||||
* @example
|
* @example
|
||||||
* note("c e g b g e")
|
* note("c e g b g e")
|
||||||
* .fm("<0 1 2 8 32>")
|
* .fm("<0 1 2 8 32>")
|
||||||
* ._scope()
|
* ._scope()
|
||||||
|
* @example
|
||||||
|
* s("sine").note("F1").seg(8)
|
||||||
|
* .fm(4).fm2(rand.mul(4)).fm3(saw.mul(8).slow(8))
|
||||||
|
* .fmh(1.06).fmh2(10).fmh3(0.1)
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export const { fmi, fm } = registerControl(['fmi', 'fmh'], 'fm');
|
export const { fmi, fmi1, fmi2, fmi3, fmi4, fmi5, fmi6, fmi7, fmi8, fm, fm1, fm2, fm3, fm4, fm5, fm6, fm7, fm8 } =
|
||||||
|
registerMultiControl(['fmi', 'fmh'], 8, 'fm');
|
||||||
|
|
||||||
// fm envelope
|
// fm envelope
|
||||||
/**
|
/**
|
||||||
* Ramp type of fm envelope. Exp might be a bit broken..
|
* Ramp type of fm envelope. Exp might be a bit broken..
|
||||||
*
|
*
|
||||||
|
* A number may be added afterwards to control the envelope of
|
||||||
|
* any of the 8 individual FMs (e.g. `fmenv4`)
|
||||||
|
*
|
||||||
* @name fmenv
|
* @name fmenv
|
||||||
* @param {number | Pattern} type lin | exp
|
* @param {number | Pattern} type lin | exp
|
||||||
* @example
|
* @example
|
||||||
@@ -474,11 +513,19 @@ export const { fmi, fm } = registerControl(['fmi', 'fmh'], 'fm');
|
|||||||
* ._scope()
|
* ._scope()
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export const { fmenv } = registerControl('fmenv');
|
export const { fmenv, fmenv1, fmenv2, fmenv3, fmenv4, fmenv5, fmenv6, fmenv7, fmenv8 } = registerMultiControl(
|
||||||
|
'fmenv',
|
||||||
|
8,
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attack time for the FM envelope: time it takes to reach maximum modulation
|
* Attack time for the FM envelope: time it takes to reach maximum modulation
|
||||||
*
|
*
|
||||||
|
* A number may be added afterwards to control the attack of the envelope of
|
||||||
|
* any of the 8 individual FMs (e.g. `fmatt5`)
|
||||||
|
*
|
||||||
* @name fmattack
|
* @name fmattack
|
||||||
|
* @synonyms fmatt
|
||||||
* @param {number | Pattern} time attack time
|
* @param {number | Pattern} time attack time
|
||||||
* @example
|
* @example
|
||||||
* note("c e g b g e")
|
* note("c e g b g e")
|
||||||
@@ -487,11 +534,33 @@ export const { fmenv } = registerControl('fmenv');
|
|||||||
* ._scope()
|
* ._scope()
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export const { fmattack } = registerControl('fmattack');
|
export const {
|
||||||
|
fmattack,
|
||||||
|
fmattack1,
|
||||||
|
fmattack2,
|
||||||
|
fmattack3,
|
||||||
|
fmattack4,
|
||||||
|
fmattack5,
|
||||||
|
fmattack6,
|
||||||
|
fmattack7,
|
||||||
|
fmattack8,
|
||||||
|
fmatt,
|
||||||
|
fmatt1,
|
||||||
|
fmatt2,
|
||||||
|
fmatt3,
|
||||||
|
fmatt4,
|
||||||
|
fmatt5,
|
||||||
|
fmatt6,
|
||||||
|
fmatt7,
|
||||||
|
fmatt8,
|
||||||
|
} = registerMultiControl('fmattack', 8, 'fmatt');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Waveform of the fm modulator
|
* Waveform of the fm modulator
|
||||||
*
|
*
|
||||||
|
* A number may be added afterwards to control the waveform
|
||||||
|
* any of the 8 individual FMs (e.g. `fmwave6`)
|
||||||
|
*
|
||||||
* @name fmwave
|
* @name fmwave
|
||||||
* @param {number | Pattern} wave waveform
|
* @param {number | Pattern} wave waveform
|
||||||
* @example
|
* @example
|
||||||
@@ -500,12 +569,19 @@ export const { fmattack } = registerControl('fmattack');
|
|||||||
* n("0 1 2 3".fast(4)).chord("<Dm Am F G>").voicing().s("sawtooth").fmwave("brown").fm(.6)
|
* n("0 1 2 3".fast(4)).chord("<Dm Am F G>").voicing().s("sawtooth").fmwave("brown").fm(.6)
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export const { fmwave } = registerControl('fmwave');
|
export const { fmwave, fmwave1, fmwave2, fmwave3, fmwave4, fmwave5, fmwave6, fmwave7, fmwave8 } = registerMultiControl(
|
||||||
|
'fmwave',
|
||||||
|
8,
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decay time for the FM envelope: seconds until the sustain level is reached after the attack phase.
|
* Decay time for the FM envelope: seconds until the sustain level is reached after the attack phase.
|
||||||
*
|
*
|
||||||
|
* A number may be added afterwards to control the decay of the envelope of
|
||||||
|
* any of the 8 individual FMs (e.g. `fmdec6`)
|
||||||
|
*
|
||||||
* @name fmdecay
|
* @name fmdecay
|
||||||
|
* @synonyms fmdec
|
||||||
* @param {number | Pattern} time decay time
|
* @param {number | Pattern} time decay time
|
||||||
* @example
|
* @example
|
||||||
* note("c e g b g e")
|
* note("c e g b g e")
|
||||||
@@ -515,11 +591,35 @@ export const { fmwave } = registerControl('fmwave');
|
|||||||
* ._scope()
|
* ._scope()
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export const { fmdecay } = registerControl('fmdecay');
|
export const {
|
||||||
|
fmdecay,
|
||||||
|
fmdecay1,
|
||||||
|
fmdecay2,
|
||||||
|
fmdecay3,
|
||||||
|
fmdecay4,
|
||||||
|
fmdecay5,
|
||||||
|
fmdecay6,
|
||||||
|
fmdecay7,
|
||||||
|
fmdecay8,
|
||||||
|
fmdec,
|
||||||
|
fmdec1,
|
||||||
|
fmdec2,
|
||||||
|
fmdec3,
|
||||||
|
fmdec4,
|
||||||
|
fmdec5,
|
||||||
|
fmdec6,
|
||||||
|
fmdec7,
|
||||||
|
fmdec8,
|
||||||
|
} = registerMultiControl('fmdecay', 8, 'fmdec');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sustain level for the FM envelope: how much modulation is applied after the decay phase
|
* Sustain level for the FM envelope: how much modulation is applied after the decay phase
|
||||||
*
|
*
|
||||||
|
* A number may be added afterwards to control the sustain of the envelope of
|
||||||
|
* any of the 8 individual FMs (e.g. `fmsus7`)
|
||||||
|
*
|
||||||
* @name fmsustain
|
* @name fmsustain
|
||||||
|
* @synonyms fmsus
|
||||||
* @param {number | Pattern} level sustain level
|
* @param {number | Pattern} level sustain level
|
||||||
* @example
|
* @example
|
||||||
* note("c e g b g e")
|
* note("c e g b g e")
|
||||||
@@ -529,10 +629,69 @@ export const { fmdecay } = registerControl('fmdecay');
|
|||||||
* ._scope()
|
* ._scope()
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export const { fmsustain } = registerControl('fmsustain');
|
export const {
|
||||||
// these are not really useful... skipping for now
|
fmsustain,
|
||||||
export const { fmrelease } = registerControl('fmrelease');
|
fmsustain1,
|
||||||
export const { fmvelocity } = registerControl('fmvelocity');
|
fmsustain2,
|
||||||
|
fmsustain3,
|
||||||
|
fmsustain4,
|
||||||
|
fmsustain5,
|
||||||
|
fmsustain6,
|
||||||
|
fmsustain7,
|
||||||
|
fmsustain8,
|
||||||
|
fmsus,
|
||||||
|
fmsus1,
|
||||||
|
fmsus2,
|
||||||
|
fmsus3,
|
||||||
|
fmsus4,
|
||||||
|
fmsus5,
|
||||||
|
fmsus6,
|
||||||
|
fmsus7,
|
||||||
|
fmsus8,
|
||||||
|
} = registerMultiControl('fmsustain', 8, 'fmsus');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Release time for the FM envelope: how much modulation is applied after the note is released
|
||||||
|
*
|
||||||
|
* A number may be added afterwards to control the release of the envelope of
|
||||||
|
* any of the 8 individual FMs (e.g. `fmrel8`)
|
||||||
|
*
|
||||||
|
* @name fmrelease
|
||||||
|
* @synonyms fmrel
|
||||||
|
* @param {number | Pattern} time release time
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export const {
|
||||||
|
fmrelease,
|
||||||
|
fmrelease1,
|
||||||
|
fmrelease2,
|
||||||
|
fmrelease3,
|
||||||
|
fmrelease4,
|
||||||
|
fmrelease5,
|
||||||
|
fmrelease6,
|
||||||
|
fmrelease7,
|
||||||
|
fmrelease8,
|
||||||
|
fmrel,
|
||||||
|
fmrel1,
|
||||||
|
fmrel2,
|
||||||
|
fmrel3,
|
||||||
|
fmrel4,
|
||||||
|
fmrel5,
|
||||||
|
fmrel6,
|
||||||
|
fmrel7,
|
||||||
|
fmrel8,
|
||||||
|
} = registerMultiControl('fmrelease', 8, 'fmrel');
|
||||||
|
|
||||||
|
// FM Matrix
|
||||||
|
// Note: we do not declare top-level exports here since it would add
|
||||||
|
// ~162 more explicit exports. This is likely fine as the most common use-case would be to at least
|
||||||
|
// declare one other FM prior to utilizing the matrix functionality, but if we ever decide we need it,
|
||||||
|
// TODO to add it explicitly / go with the globalThis approach
|
||||||
|
for (let i = 0; i <= 8; i++) {
|
||||||
|
for (let j = 0; j <= 8; j++) {
|
||||||
|
registerControl(`fmi${i}${j}`, `fm${i}${j}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Select the sound bank to use. To be used together with `s`. The bank name (+ "_") will be prepended to the value of `s`.
|
* Select the sound bank to use. To be used together with `s`. The bank name (+ "_") will be prepended to the value of `s`.
|
||||||
@@ -1294,6 +1453,8 @@ export const { fanchor } = registerControl('fanchor');
|
|||||||
*
|
*
|
||||||
* @name lprate
|
* @name lprate
|
||||||
* @param {number | Pattern} rate rate in hertz
|
* @param {number | Pattern} rate rate in hertz
|
||||||
|
* @example
|
||||||
|
* note("<c c c# c c c4>*16").s("sawtooth").lpf(600).lprate("<4 8 2 1>")
|
||||||
*/
|
*/
|
||||||
export const { lprate } = registerControl('lprate');
|
export const { lprate } = registerControl('lprate');
|
||||||
|
|
||||||
@@ -1302,6 +1463,8 @@ export const { lprate } = registerControl('lprate');
|
|||||||
*
|
*
|
||||||
* @name lpsync
|
* @name lpsync
|
||||||
* @param {number | Pattern} rate rate in cycles
|
* @param {number | Pattern} rate rate in cycles
|
||||||
|
* @example
|
||||||
|
* note("<c c c# c c c4>*16").s("sawtooth").lpf(600).lpsync("<4 8 2 1>")
|
||||||
*/
|
*/
|
||||||
export const { lpsync } = registerControl('lpsync');
|
export const { lpsync } = registerControl('lpsync');
|
||||||
|
|
||||||
@@ -1310,8 +1473,23 @@ export const { lpsync } = registerControl('lpsync');
|
|||||||
*
|
*
|
||||||
* @name lpdepth
|
* @name lpdepth
|
||||||
* @param {number | Pattern} depth depth of modulation
|
* @param {number | Pattern} depth depth of modulation
|
||||||
|
* @example
|
||||||
|
* note("<c c c# c c c4>*16").s("sawtooth").lpf(600).lpdepth("<1 .5 1.8 0>")
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const { lpdepth } = registerControl('lpdepth');
|
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("<c c c# c c c4>*16").s("sawtooth").lpf(600).lpdepthfrequency("<200 500 100 0>")
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const { lpdepthfrequency } = registerControl('lpdepthfrequency', 'lpdepthfreq');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shape of the LFO for the lowpass filter
|
* Shape of the LFO for the lowpass filter
|
||||||
@@ -1361,6 +1539,19 @@ export const { bpsync } = registerControl('bpsync');
|
|||||||
*/
|
*/
|
||||||
export const { bpdepth } = registerControl('bpdepth');
|
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("<c c c# c c c4>*16").s("sawtooth").lpf(600).bpdepthfrequency("<200 500 100 0>")
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const { bpdepthfrequency } = registerControl('bpdepthfrequency', 'bpdepthfreq');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shape of the LFO for the bandpass filter
|
* Shape of the LFO for the bandpass filter
|
||||||
*
|
*
|
||||||
@@ -1407,7 +1598,20 @@ export const { hpsync } = registerControl('hpsync');
|
|||||||
* @name hpdepth
|
* @name hpdepth
|
||||||
* @param {number | Pattern} depth depth of modulation
|
* @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("<c c c# c c c4>*16").s("sawtooth").lpf(600).hpdepthfrequency("<200 500 100 0>")
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const { hpdepthfrequency } = registerControl('hpdepthfrequency', 'hpdepthfreq');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shape of the LFO for the highpass filter
|
* Shape of the LFO for the highpass filter
|
||||||
@@ -1820,12 +2024,12 @@ export const { nudge } = registerControl('nudge');
|
|||||||
* Sets the default octave of a synth.
|
* Sets the default octave of a synth.
|
||||||
*
|
*
|
||||||
* @name octave
|
* @name octave
|
||||||
|
* @synonyms oct
|
||||||
* @param {number | Pattern} octave octave number
|
* @param {number | Pattern} octave octave number
|
||||||
* @example
|
* @example
|
||||||
* n("0,4,7").s('supersquare').octave("<3 4 5 6>").osc()
|
* n("0,4,7").scale("F:minor").s('supersaw').octave("<0 1 2 3>")
|
||||||
* @superDirtOnly
|
|
||||||
*/
|
*/
|
||||||
export const { octave } = registerControl('octave');
|
export const { octave, oct } = registerControl('octave', 'oct');
|
||||||
|
|
||||||
// ['ophatdecay'],
|
// ['ophatdecay'],
|
||||||
// TODO: example
|
// TODO: example
|
||||||
@@ -1833,6 +2037,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.
|
* An `orbit` is a global parameter context for patterns. Patterns with the same orbit will share the same global effects.
|
||||||
*
|
*
|
||||||
* @name orbit
|
* @name orbit
|
||||||
|
* @synonyms o
|
||||||
* @param {number | Pattern} number
|
* @param {number | Pattern} number
|
||||||
* @example
|
* @example
|
||||||
* stack(
|
* stack(
|
||||||
@@ -1840,7 +2045,7 @@ export const { octave } = registerControl('octave');
|
|||||||
* s("~ sd ~ sd").delay(.5).delaytime(.125).orbit(2)
|
* 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
|
// 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');
|
export const { overgain } = registerControl('overgain');
|
||||||
// TODO: what is this? not found in tidal doc. Similar to above, but limited to 1
|
// TODO: what is this? not found in tidal doc. Similar to above, but limited to 1
|
||||||
@@ -2264,7 +2469,6 @@ export const { tsdelay } = registerControl('tsdelay');
|
|||||||
export const { real } = registerControl('real');
|
export const { real } = registerControl('real');
|
||||||
export const { imag } = registerControl('imag');
|
export const { imag } = registerControl('imag');
|
||||||
export const { enhance } = registerControl('enhance');
|
export const { enhance } = registerControl('enhance');
|
||||||
export const { partials } = registerControl('partials');
|
|
||||||
export const { comb } = registerControl('comb');
|
export const { comb } = registerControl('comb');
|
||||||
export const { smear } = registerControl('smear');
|
export const { smear } = registerControl('smear');
|
||||||
export const { scram } = registerControl('scram');
|
export const { scram } = registerControl('scram');
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import {
|
|||||||
stringifyValues,
|
stringifyValues,
|
||||||
} from './util.mjs';
|
} from './util.mjs';
|
||||||
import drawLine from './drawLine.mjs';
|
import drawLine from './drawLine.mjs';
|
||||||
import { logger } from './logger.mjs';
|
import { errorLogger, logger } from './logger.mjs';
|
||||||
|
|
||||||
let stringParser;
|
let stringParser;
|
||||||
|
|
||||||
@@ -414,7 +414,7 @@ export class Pattern {
|
|||||||
try {
|
try {
|
||||||
return this.query(new State(new TimeSpan(begin, end), controls));
|
return this.query(new State(new TimeSpan(begin, end), controls));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger(`[query]: ${err.message}`, 'error');
|
errorLogger(err, 'query');
|
||||||
return [];
|
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
|
* @name rev
|
||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
@@ -2266,6 +2266,23 @@ export const rev = register(
|
|||||||
true,
|
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
|
/** Like press, but allows you to specify the amount by which each
|
||||||
* event is shifted. pressBy(0.5) is the same as press, while
|
* event is shifted. pressBy(0.5) is the same as press, while
|
||||||
* pressBy(1/3) shifts each event by a third of its timespan.
|
* pressBy(1/3) shifts each event by a third of its timespan.
|
||||||
@@ -3624,3 +3641,66 @@ for (const name of distAlgoNames) {
|
|||||||
return this.distort(argsPat);
|
return this.distort(argsPat);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turns a list of patterns into a single pattern which outputs list-values
|
||||||
|
*
|
||||||
|
* @name parray
|
||||||
|
* @returns Pattern
|
||||||
|
*/
|
||||||
|
export const parray = (pats) => {
|
||||||
|
const pack = (...xs) => xs;
|
||||||
|
let acc = pure(curry(pack, null, pats.length));
|
||||||
|
for (const p of pats) acc = acc.appBoth(reify(p));
|
||||||
|
return acc;
|
||||||
|
};
|
||||||
|
|
||||||
|
const _ensureListPattern = (list) => {
|
||||||
|
if (Array.isArray(list)) {
|
||||||
|
return parray(list);
|
||||||
|
}
|
||||||
|
return reify(list);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scale the magnitude of the harmonics of one of the core synths ('sine', 'tri', 'saw', ..)
|
||||||
|
*
|
||||||
|
* Can also be used to create a new synth via `s('user').partials(...)`
|
||||||
|
*
|
||||||
|
* @name partials
|
||||||
|
* @param {number[] | Pattern} magnitudes List of [0, 1] magnitudes for partials. 0th entry is the fundamental harmonic (i.e. DC offset is skipped)
|
||||||
|
* @example
|
||||||
|
* s("user").seg(16).n(irand(8)).scale("A:major")
|
||||||
|
* .partials([1, 0, 1, 0, 0, 1])
|
||||||
|
* @example
|
||||||
|
* s("saw").seg(8).n(irand(12)).scale("G#:minor")
|
||||||
|
* .partials(binaryL(irand(256).add("1")))
|
||||||
|
*/
|
||||||
|
Pattern.prototype.partials = function (list) {
|
||||||
|
return this.withValue((v) => (l) => ({ ...v, partials: l })).appLeft(_ensureListPattern(list));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Also create a top-level function
|
||||||
|
export const partials = (list) => {
|
||||||
|
return _ensureListPattern(list).as('partials');
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rotates the harmonics of one of the core synths ('sine', 'tri', 'saw', 'user', ..) by a list of phases
|
||||||
|
*
|
||||||
|
* @name phases
|
||||||
|
* @param {number[] | Pattern} phases List of [0, 1) phases for partials. 0th entry is the fundamental phase (i.e. DC offset is skipped)
|
||||||
|
* @example
|
||||||
|
* // Phase cancellation
|
||||||
|
* s("saw").seg(8).n(irand(12)).scale("G#1:minor")
|
||||||
|
* .partials(partials([1, 1, 1]))
|
||||||
|
* .superimpose(x => x.phases([0.5, 0.5, 0.5]))
|
||||||
|
*/
|
||||||
|
Pattern.prototype.phases = function (list) {
|
||||||
|
return this.withValue((v) => (l) => ({ ...v, phases: l })).appLeft(_ensureListPattern(list));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Also create a top-level function
|
||||||
|
export const phases = (list) => {
|
||||||
|
return _ensureListPattern(list).as('phases');
|
||||||
|
};
|
||||||
|
|||||||
+17
-7
@@ -152,9 +152,9 @@ export function repl({
|
|||||||
// allows muting a pattern x with x_ or _x
|
// allows muting a pattern x with x_ or _x
|
||||||
return silence;
|
return silence;
|
||||||
}
|
}
|
||||||
if (id === '$') {
|
if (id.includes('$')) {
|
||||||
// allows adding anonymous patterns with $:
|
// allows adding anonymous patterns with $:
|
||||||
id = `$${anonymousIndex}`;
|
id = `${id}${anonymousIndex}`;
|
||||||
anonymousIndex++;
|
anonymousIndex++;
|
||||||
}
|
}
|
||||||
pPatterns[id] = this;
|
pPatterns[id] = this;
|
||||||
@@ -215,8 +215,19 @@ export function repl({
|
|||||||
let { pattern, meta } = await _evaluate(code, transpiler, transpilerOptions);
|
let { pattern, meta } = await _evaluate(code, transpiler, transpilerOptions);
|
||||||
if (Object.keys(pPatterns).length) {
|
if (Object.keys(pPatterns).length) {
|
||||||
let patterns = [];
|
let patterns = [];
|
||||||
|
let soloActive = false;
|
||||||
for (const [key, value] of Object.entries(pPatterns)) {
|
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) {
|
if (eachTransform) {
|
||||||
// Explicit lambda so only element (not index and array) are passed
|
// Explicit lambda so only element (not index and array) are passed
|
||||||
@@ -227,14 +238,13 @@ export function repl({
|
|||||||
pattern = eachTransform(pattern);
|
pattern = eachTransform(pattern);
|
||||||
}
|
}
|
||||||
if (allTransforms.length) {
|
if (allTransforms.length) {
|
||||||
for (let i in allTransforms) {
|
for (const transform of allTransforms) {
|
||||||
pattern = allTransforms[i](pattern);
|
pattern = transform(pattern);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isPattern(pattern)) {
|
if (!isPattern(pattern)) {
|
||||||
const message = `got "${typeof evaluated}" instead of pattern`;
|
pattern = silence;
|
||||||
throw new Error(message + (typeof evaluated === 'function' ? ', did you forget to call a function?' : '.'));
|
|
||||||
}
|
}
|
||||||
logger(`[eval] code updated`);
|
logger(`[eval] code updated`);
|
||||||
pattern = await setPattern(pattern, autostart);
|
pattern = await setPattern(pattern, autostart);
|
||||||
|
|||||||
@@ -228,7 +228,7 @@ const timeToRands = (t, n) => timeToRandsPrime(timeToIntSeed(t), n);
|
|||||||
export const run = (n) => saw.range(0, n).round().segment(n);
|
export const run = (n) => saw.range(0, n).round().segment(n);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a pattern from a binary number.
|
* Creates a binary pattern from a number.
|
||||||
*
|
*
|
||||||
* @name binary
|
* @name binary
|
||||||
* @param {number} n - input number to convert to binary
|
* @param {number} n - input number to convert to binary
|
||||||
@@ -242,7 +242,7 @@ export const binary = (n) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a pattern from a binary number, padded to n bits long.
|
* Creates a binary pattern from a number, padded to n bits long.
|
||||||
*
|
*
|
||||||
* @name binaryN
|
* @name binaryN
|
||||||
* @param {number} n - input number to convert to binary
|
* @param {number} n - input number to convert to binary
|
||||||
@@ -258,6 +258,51 @@ export const binaryN = (n, nBits = 16) => {
|
|||||||
return reify(n).segment(nBits).brshift(bitPos).band(pure(1));
|
return reify(n).segment(nBits).brshift(bitPos).band(pure(1));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a binary list pattern from a number.
|
||||||
|
*
|
||||||
|
* @name binaryL
|
||||||
|
* @param {number} n - input number to convert to binary
|
||||||
|
* s("saw").seg(8)
|
||||||
|
* .partials(binaryL(irand(4096).add(1)))
|
||||||
|
*/
|
||||||
|
export const binaryL = (n) => {
|
||||||
|
const nBits = reify(n).log2(0).floor().add(1);
|
||||||
|
return binaryNL(n, nBits);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a binary list pattern from a number, padded to n bits long.
|
||||||
|
*
|
||||||
|
* @name binaryNL
|
||||||
|
* @param {number} n - input number to convert to binary
|
||||||
|
* @param {number} nBits - pattern length, defaults to 16
|
||||||
|
*/
|
||||||
|
export const binaryNL = (n, nBits = 16) => {
|
||||||
|
return reify(n)
|
||||||
|
.withValue((v) => (bits) => {
|
||||||
|
const bList = [];
|
||||||
|
for (let i = bits - 1; i >= 0; i--) {
|
||||||
|
bList.push((v >> i) & 1);
|
||||||
|
}
|
||||||
|
return bList;
|
||||||
|
})
|
||||||
|
.appLeft(reify(nBits));
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a list of random numbers of the given length
|
||||||
|
*
|
||||||
|
* @name randL
|
||||||
|
* @param {number} n Number of random numbers to sample
|
||||||
|
* @example
|
||||||
|
* s("saw").seg(16).n(irand(12)).scale("F1:minor")
|
||||||
|
* .partials(randL(8))
|
||||||
|
*/
|
||||||
|
export const randL = (n) => {
|
||||||
|
return signal((t) => (nVal) => timeToRands(t, nVal).map(Math.abs)).appLeft(reify(n));
|
||||||
|
};
|
||||||
|
|
||||||
export const randrun = (n) => {
|
export const randrun = (n) => {
|
||||||
return signal((t) => {
|
return signal((t) => {
|
||||||
// Without adding 0.5, the first cycle is always 0,1,2,3,...
|
// Without adding 0.5, the first cycle is always 0,1,2,3,...
|
||||||
@@ -479,7 +524,7 @@ export const wchoose = (...pairs) => wchooseWith(rand, ...pairs);
|
|||||||
* @example
|
* @example
|
||||||
* wchooseCycles(["bd",10], ["hh",1], ["sd",1]).s().fast(8)
|
* wchooseCycles(["bd",10], ["hh",1], ["sd",1]).s().fast(8)
|
||||||
* @example
|
* @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
|
* @example
|
||||||
* // The probability can itself be a pattern
|
* // The probability can itself be a pattern
|
||||||
* wchooseCycles(["bd(3,8)","<5 0>"], ["hh hh hh",3]).fast(4).s()
|
* wchooseCycles(["bd(3,8)","<5 0>"], ["hh hh hh",3]).fast(4).s()
|
||||||
|
|||||||
@@ -586,6 +586,18 @@ describe('Pattern', () => {
|
|||||||
.map((a) => a.value),
|
.map((a) => a.value),
|
||||||
).toStrictEqual(['c', 'b', 'a']);
|
).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()', () => {
|
describe('sequence()', () => {
|
||||||
it('Can work like fastcat', () => {
|
it('Can work like fastcat', () => {
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ export async function initHydra(options = {}) {
|
|||||||
hydra.synth.s0.init({ src: canvas });
|
hydra.synth.s0.init({ src: canvas });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return hydra;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function clearHydra() {
|
export function clearHydra() {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import readline from 'readline';
|
|||||||
import os from 'os';
|
import os from 'os';
|
||||||
|
|
||||||
const LOG = !!process.env.LOG || false;
|
const LOG = !!process.env.LOG || false;
|
||||||
|
const PORT = process.env.PORT || 5432;
|
||||||
const VALID_AUDIO_EXTENSIONS = ['wav', 'mp3', 'ogg'];
|
const VALID_AUDIO_EXTENSIONS = ['wav', 'mp3', 'ogg'];
|
||||||
|
|
||||||
const isAudioFile = (f) => {
|
const isAudioFile = (f) => {
|
||||||
@@ -54,7 +55,6 @@ async function getBanks(directory, flat = false) {
|
|||||||
banks[bank].push(subDir);
|
banks[bank].push(subDir);
|
||||||
return subDir;
|
return subDir;
|
||||||
});
|
});
|
||||||
banks._base = `http://localhost:5432`;
|
|
||||||
return { banks, files };
|
return { banks, files };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,8 +134,6 @@ const server = http.createServer(async (req, res) => {
|
|||||||
readStream.pipe(res);
|
readStream.pipe(res);
|
||||||
});
|
});
|
||||||
|
|
||||||
// eslint-disable-next-line
|
|
||||||
const PORT = process.env.PORT || 5432;
|
|
||||||
const IP_ADDRESS = '0.0.0.0';
|
const IP_ADDRESS = '0.0.0.0';
|
||||||
let IP;
|
let IP;
|
||||||
const networkInterfaces = os.networkInterfaces();
|
const networkInterfaces = os.networkInterfaces();
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
getADSRValues,
|
getADSRValues,
|
||||||
getPitchEnvelope,
|
getPitchEnvelope,
|
||||||
getVibratoOscillator,
|
getVibratoOscillator,
|
||||||
|
onceEnded,
|
||||||
} from '@strudel/webaudio';
|
} from '@strudel/webaudio';
|
||||||
import gm from './gm.mjs';
|
import gm from './gm.mjs';
|
||||||
|
|
||||||
@@ -170,12 +171,12 @@ export function registerSoundfonts() {
|
|||||||
|
|
||||||
bufferSource.stop(envEnd);
|
bufferSource.stop(envEnd);
|
||||||
const stop = (releaseTime) => {};
|
const stop = (releaseTime) => {};
|
||||||
bufferSource.onended = () => {
|
onceEnded(bufferSource, () => {
|
||||||
bufferSource.disconnect();
|
bufferSource.disconnect();
|
||||||
vibratoOscillator?.stop();
|
vibratoOscillator?.stop();
|
||||||
node.disconnect();
|
node.disconnect();
|
||||||
onended();
|
onended();
|
||||||
};
|
});
|
||||||
return { node, stop };
|
return { node, stop };
|
||||||
},
|
},
|
||||||
{ type: 'soundfont', prebake: true, fonts },
|
{ type: 'soundfont', prebake: true, fonts },
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ superdough({ s: 'bd', delay: 0.5 }, 0, 1);
|
|||||||
- `decay`: seconds of decay phase
|
- `decay`: seconds of decay phase
|
||||||
- `sustain`: gain of sustain phase
|
- `sustain`: gain of sustain phase
|
||||||
- `release`: seconds of release phase
|
- `release`: seconds of release phase
|
||||||
- `deadline`: seconds until the sound should play (0 = immediate)
|
- `deadline`: seconds from audio context initialization before playing the sound (getAudioContextCurrentTime() = immediate)
|
||||||
- `duration`: seconds the sound should last. optional for one shot samples, required for synth sounds
|
- `duration`: seconds the sound should last. optional for one shot samples, required for synth sounds
|
||||||
|
|
||||||
### registerSynthSounds()
|
### registerSynthSounds()
|
||||||
|
|||||||
+176
-80
@@ -1,7 +1,7 @@
|
|||||||
import { getAudioContext } from './audioContext.mjs';
|
import { getAudioContext } from './audioContext.mjs';
|
||||||
import { clamp, nanFallback, midiToFreq, noteToMidi } from './util.mjs';
|
|
||||||
import { getNoiseBuffer } from './noise.mjs';
|
|
||||||
import { logger } from './logger.mjs';
|
import { logger } from './logger.mjs';
|
||||||
|
import { getNoiseBuffer } from './noise.mjs';
|
||||||
|
import { clamp, nanFallback, midiToFreq, noteToMidi } from './util.mjs';
|
||||||
|
|
||||||
export const noises = ['pink', 'white', 'brown', 'crackle'];
|
export const noises = ['pink', 'white', 'brown', 'crackle'];
|
||||||
|
|
||||||
@@ -191,8 +191,7 @@ export function applyParameterModulators(audioContext, param, start, end, envelo
|
|||||||
const lfo = getParamLfo(audioContext, param, start, end, lfoValues);
|
const lfo = getParamLfo(audioContext, param, start, end, lfoValues);
|
||||||
return { lfo, disconnect: () => lfo?.disconnect() };
|
return { lfo, disconnect: () => lfo?.disconnect() };
|
||||||
}
|
}
|
||||||
|
export function createFilter(context, start, end, params, cps, cycle) {
|
||||||
export function createFilter(context, start, end, params, cps) {
|
|
||||||
let {
|
let {
|
||||||
frequency,
|
frequency,
|
||||||
anchor,
|
anchor,
|
||||||
@@ -202,12 +201,14 @@ export function createFilter(context, start, end, params, cps) {
|
|||||||
q = 1,
|
q = 1,
|
||||||
drive = 0.69,
|
drive = 0.69,
|
||||||
depth,
|
depth,
|
||||||
|
depthfrequency,
|
||||||
dcoffset = -0.5,
|
dcoffset = -0.5,
|
||||||
skew,
|
skew,
|
||||||
shape,
|
shape,
|
||||||
rate,
|
rate,
|
||||||
sync,
|
sync,
|
||||||
} = params;
|
} = params;
|
||||||
|
|
||||||
let frequencyParam, filter;
|
let frequencyParam, filter;
|
||||||
if (model === 'ladder') {
|
if (model === 'ladder') {
|
||||||
filter = getWorklet(context, 'ladder-processor', { frequency, q, drive });
|
filter = getWorklet(context, 'ladder-processor', { frequency, q, drive });
|
||||||
@@ -238,9 +239,27 @@ export function createFilter(context, start, end, params, cps) {
|
|||||||
if (sync != null) {
|
if (sync != null) {
|
||||||
rate = cps * sync;
|
rate = cps * sync;
|
||||||
}
|
}
|
||||||
const lfoValues = { depth, dcoffset, skew, shape, frequency: rate };
|
const hasLFO = [depth, depthfrequency, skew, shape, rate].some((v) => v !== undefined);
|
||||||
getParamLfo(context, frequencyParam, start, end, lfoValues);
|
let lfo;
|
||||||
return filter;
|
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
|
// stays 1 until .5, then fades out
|
||||||
@@ -262,7 +281,15 @@ export function drywet(dry, wet, wetAmount = 0) {
|
|||||||
let mix = ac.createGain();
|
let mix = ac.createGain();
|
||||||
dry_gain.connect(mix);
|
dry_gain.connect(mix);
|
||||||
wet_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'];
|
let curves = ['linear', 'exponential'];
|
||||||
@@ -297,6 +324,10 @@ export function getVibratoOscillator(param, value, t) {
|
|||||||
gain.gain.value = vibmod * 100;
|
gain.gain.value = vibmod * 100;
|
||||||
vibratoOscillator.connect(gain);
|
vibratoOscillator.connect(gain);
|
||||||
gain.connect(param);
|
gain.connect(param);
|
||||||
|
vibratoOscillator.onended = () => {
|
||||||
|
gain.disconnect(param);
|
||||||
|
vibratoOscillator.disconnect(gain);
|
||||||
|
};
|
||||||
vibratoOscillator.start(t);
|
vibratoOscillator.start(t);
|
||||||
return vibratoOscillator;
|
return vibratoOscillator;
|
||||||
}
|
}
|
||||||
@@ -318,25 +349,17 @@ export function webAudioTimeout(audioContext, onComplete, startTime, stopTime) {
|
|||||||
constantNode.connect(zeroGain);
|
constantNode.connect(zeroGain);
|
||||||
|
|
||||||
// Schedule the `onComplete` callback to occur at `stopTime`
|
// Schedule the `onComplete` callback to occur at `stopTime`
|
||||||
constantNode.onended = () => {
|
onceEnded(constantNode, () => {
|
||||||
// Ensure garbage collection
|
releaseAudioNode(zeroGain);
|
||||||
try {
|
releaseAudioNode(constantNode);
|
||||||
zeroGain.disconnect();
|
|
||||||
} catch {
|
|
||||||
// pass
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
constantNode.disconnect();
|
|
||||||
} catch {
|
|
||||||
// pass
|
|
||||||
}
|
|
||||||
onComplete();
|
onComplete();
|
||||||
};
|
});
|
||||||
constantNode.start(startTime);
|
constantNode.start(startTime);
|
||||||
constantNode.stop(stopTime);
|
constantNode.stop(stopTime);
|
||||||
return constantNode;
|
return constantNode;
|
||||||
}
|
}
|
||||||
const mod = (freq, range = 1, type = 'sine') => {
|
|
||||||
|
const mod = (freq, type = 'sine') => {
|
||||||
const ctx = getAudioContext();
|
const ctx = getAudioContext();
|
||||||
let osc;
|
let osc;
|
||||||
if (noises.includes(type)) {
|
if (noises.includes(type)) {
|
||||||
@@ -348,64 +371,89 @@ const mod = (freq, range = 1, type = 'sine') => {
|
|||||||
osc.type = type;
|
osc.type = type;
|
||||||
osc.frequency.value = freq;
|
osc.frequency.value = freq;
|
||||||
}
|
}
|
||||||
|
|
||||||
osc.start();
|
osc.start();
|
||||||
const g = new GainNode(ctx, { gain: range });
|
return osc;
|
||||||
osc.connect(g); // -range, range
|
|
||||||
return { node: g, stop: (t) => osc.stop(t) };
|
|
||||||
};
|
};
|
||||||
const fm = (frequencyparam, harmonicityRatio, modulationIndex, wave = 'sine') => {
|
|
||||||
|
const fm = (frequencyparam, harmonicityRatio, wave = 'sine') => {
|
||||||
const carrfreq = frequencyparam.value;
|
const carrfreq = frequencyparam.value;
|
||||||
const modfreq = carrfreq * harmonicityRatio;
|
const modfreq = carrfreq * harmonicityRatio;
|
||||||
const modgain = modfreq * modulationIndex;
|
return { osc: mod(modfreq, wave), freq: modfreq };
|
||||||
return mod(modfreq, modgain, wave);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function applyFM(param, value, begin) {
|
export function applyFM(param, value, begin) {
|
||||||
const {
|
const ac = getAudioContext();
|
||||||
fmh: fmHarmonicity = 1,
|
const toStop = []; // fm oscillators we will expose `stop` for
|
||||||
fmi: fmModulationIndex,
|
const fms = {};
|
||||||
fmenv: fmEnvelopeType = 'exp',
|
// Matrix
|
||||||
fmattack: fmAttack,
|
for (let i = 1; i <= 8; i++) {
|
||||||
fmdecay: fmDecay,
|
for (let j = 0; j <= 8; j++) {
|
||||||
fmsustain: fmSustain,
|
let control;
|
||||||
fmrelease: fmRelease,
|
if (i === j + 1) {
|
||||||
fmvelocity: fmVelocity,
|
// Standard fm3 -> fm2 -> fm1 -> param usage
|
||||||
fmwave: fmWaveform = 'sine',
|
const iS = i === 1 ? '' : i;
|
||||||
duration,
|
control = `fmi${iS}`;
|
||||||
} = value;
|
} else {
|
||||||
let modulator;
|
control = `fmi${i}${j}`;
|
||||||
let stop = () => {};
|
}
|
||||||
|
const amt = value[control];
|
||||||
if (fmModulationIndex) {
|
if (!amt) continue;
|
||||||
const ac = getAudioContext();
|
let io = [];
|
||||||
const envGain = ac.createGain();
|
for (let [isMod, idx] of [
|
||||||
const fmmod = fm(param, fmHarmonicity, fmModulationIndex, fmWaveform);
|
[true, i], // source
|
||||||
|
[false, j], // target
|
||||||
modulator = fmmod.node;
|
]) {
|
||||||
stop = fmmod.stop;
|
if (idx === 0) {
|
||||||
if (![fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity].some((v) => v !== undefined)) {
|
io.push(param);
|
||||||
// no envelope by default
|
continue;
|
||||||
modulator.connect(param);
|
}
|
||||||
} else {
|
if (!fms[idx]) {
|
||||||
const [attack, decay, sustain, release] = getADSRValues([fmAttack, fmDecay, fmSustain, fmRelease]);
|
const idxS = idx === 1 ? '' : idx;
|
||||||
const holdEnd = begin + duration;
|
const { osc, freq } = fm(param, value[`fmh${idxS}`] ?? 1, value[`fmwave${idxS}`] ?? 'sine');
|
||||||
getParamADSR(
|
toStop.push(osc);
|
||||||
envGain.gain,
|
const toCleanup = [osc]; // nodes we want to cleanup after oscillator `stop`
|
||||||
attack,
|
const adsr = ['attack', 'decay', 'sustain', 'release'].map((s) => value[`fm${s}${idxS}`]);
|
||||||
decay,
|
let output = osc;
|
||||||
sustain,
|
if (adsr.some((v) => v !== undefined)) {
|
||||||
release,
|
const envGain = ac.createGain();
|
||||||
0,
|
const [attack, decay, sustain, release] = getADSRValues(adsr);
|
||||||
1,
|
const holdEnd = begin + value.duration;
|
||||||
begin,
|
const fmEnvelopeType = value[`fmenv${idxS}`] ?? 'exp';
|
||||||
holdEnd,
|
getParamADSR(
|
||||||
fmEnvelopeType === 'exp' ? 'exponential' : 'linear',
|
envGain.gain,
|
||||||
);
|
attack,
|
||||||
modulator.connect(envGain);
|
decay,
|
||||||
envGain.connect(param);
|
sustain,
|
||||||
|
release,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
begin,
|
||||||
|
holdEnd,
|
||||||
|
fmEnvelopeType === 'exp' ? 'exponential' : 'linear',
|
||||||
|
);
|
||||||
|
toCleanup.push(envGain);
|
||||||
|
output = osc.connect(envGain);
|
||||||
|
}
|
||||||
|
fms[idx] = { input: osc.frequency, output, freq, osc, toCleanup };
|
||||||
|
}
|
||||||
|
const { input, output, freq, osc, toCleanup } = fms[idx];
|
||||||
|
const g = gainNode(amt * freq);
|
||||||
|
io.push(isMod ? output.connect(g) : input);
|
||||||
|
cleanupOnEnd(osc, [...toCleanup, g]);
|
||||||
|
}
|
||||||
|
if (!io[1]) {
|
||||||
|
logger(
|
||||||
|
`[superdough] control ${control} failed to connect FM ${i} to target ${j} due to missing frequency parameter (likely because fm${j} is noise)`,
|
||||||
|
'warning',
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
io[0].connect(io[1]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { stop };
|
return {
|
||||||
|
stop: (t) => toStop.forEach((m) => m?.stop(t)),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Saturation curves
|
// Saturation curves
|
||||||
@@ -505,7 +553,7 @@ export const getDistortion = (distort, postgain, algorithm) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const getFrequencyFromValue = (value, defaultNote = 36) => {
|
export const getFrequencyFromValue = (value, defaultNote = 36) => {
|
||||||
let { note, freq } = value;
|
let { note, freq, octave = 0 } = value;
|
||||||
note = note || defaultNote;
|
note = note || defaultNote;
|
||||||
if (typeof note === 'string') {
|
if (typeof note === 'string') {
|
||||||
note = noteToMidi(note); // e.g. c3 => 48
|
note = noteToMidi(note); // e.g. c3 => 48
|
||||||
@@ -514,14 +562,62 @@ export const getFrequencyFromValue = (value, defaultNote = 36) => {
|
|||||||
if (!freq && typeof note === 'number') {
|
if (!freq && typeof note === 'number') {
|
||||||
freq = midiToFreq(note); // + 48);
|
freq = midiToFreq(note); // + 48);
|
||||||
}
|
}
|
||||||
|
freq *= Math.pow(2, octave);
|
||||||
return Number(freq);
|
return Number(freq);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const destroyAudioWorkletNode = (node) => {
|
// This helper should be used instead of the `node.onended = callback` pattern
|
||||||
if (node == null) {
|
// It adds a mechanism to help minimize gc retention
|
||||||
return;
|
export const onceEnded = (node, callback) => {
|
||||||
}
|
const onended = callback;
|
||||||
node.disconnect();
|
node.onended = function cleanup() {
|
||||||
node.parameters.get('end')?.setValueAtTime(0, 0);
|
onended && onended();
|
||||||
|
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();
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Once the `anchor` node has ended, release all nodes in `toCleanup`
|
||||||
|
export const cleanupOnEnd = (anchor, toCleanup) => {
|
||||||
|
onceEnded(anchor, () => toCleanup.forEach((n) => releaseAudioNode(n)));
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -65,8 +65,9 @@ export function getNoiseOscillator(type = 'white', t, density = 0.02) {
|
|||||||
export function getNoiseMix(inputNode, wet, t) {
|
export function getNoiseMix(inputNode, wet, t) {
|
||||||
const noiseOscillator = getNoiseOscillator('pink', t);
|
const noiseOscillator = getNoiseOscillator('pink', t);
|
||||||
const noiseMix = drywet(inputNode, noiseOscillator.node, wet);
|
const noiseMix = drywet(inputNode, noiseOscillator.node, wet);
|
||||||
|
noiseOscillator.node.onended = () => noiseMix.onended();
|
||||||
return {
|
return {
|
||||||
node: noiseMix,
|
node: noiseMix.node,
|
||||||
stop: (time) => noiseOscillator?.stop(time),
|
stop: (time) => noiseOscillator?.stop(time),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
import { getCommonSampleInfo } from './util.mjs';
|
import { getBaseURL, getCommonSampleInfo } from './util.mjs';
|
||||||
import { registerSound, registerWaveTable } from './index.mjs';
|
import { registerSound, registerWaveTable } from './index.mjs';
|
||||||
import { getAudioContext } from './audioContext.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';
|
import { logger } from './logger.mjs';
|
||||||
|
|
||||||
const bufferCache = {}; // string: Promise<ArrayBuffer>
|
const bufferCache = {}; // string: Promise<ArrayBuffer>
|
||||||
@@ -211,7 +218,7 @@ export async function fetchSampleMap(url) {
|
|||||||
// not a browser
|
// not a browser
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const base = url.split('/').slice(0, -1).join('/');
|
const base = getBaseURL(url);
|
||||||
if (typeof fetch === 'undefined') {
|
if (typeof fetch === 'undefined') {
|
||||||
// skip fetch when in node / testing
|
// skip fetch when in node / testing
|
||||||
return;
|
return;
|
||||||
@@ -236,12 +243,6 @@ export async function fetchSampleMap(url) {
|
|||||||
* sd: '808sd/SD0010.WAV'
|
* sd: '808sd/SD0010.WAV'
|
||||||
* }, 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/');
|
* }, 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/');
|
||||||
* s("[bd ~]*2, [~ hh]*2, ~ sd")
|
* 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 = {}) => {
|
export const samples = async (sampleMap, baseUrl = sampleMap._base || '', options = {}) => {
|
||||||
@@ -286,17 +287,19 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
|
|||||||
|
|
||||||
const { bufferSource, sliceDuration, offset } = await getSampleBufferSource(value, 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) {
|
if (!bufferSource) {
|
||||||
logger(`[sampler] could not load "${s}:${n}"`, 'error');
|
logger(`[sampler] could not load "${s}:${n}"`, 'error');
|
||||||
return;
|
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
|
// vibrato
|
||||||
let vibratoOscillator = getVibratoOscillator(bufferSource.detune, value, t);
|
let vibratoOscillator = getVibratoOscillator(bufferSource.detune, value, t);
|
||||||
|
|
||||||
@@ -319,13 +322,13 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
|
|||||||
|
|
||||||
const out = ac.createGain(); // we need a separate gain for the cutgroups because firefox...
|
const out = ac.createGain(); // we need a separate gain for the cutgroups because firefox...
|
||||||
node.connect(out);
|
node.connect(out);
|
||||||
bufferSource.onended = function () {
|
onceEnded(bufferSource, function () {
|
||||||
bufferSource.disconnect();
|
bufferSource.disconnect();
|
||||||
vibratoOscillator?.stop();
|
vibratoOscillator?.stop();
|
||||||
node.disconnect();
|
node.disconnect();
|
||||||
out.disconnect();
|
out.disconnect();
|
||||||
onended();
|
onended();
|
||||||
};
|
});
|
||||||
let envEnd = holdEnd + release + 0.01;
|
let envEnd = holdEnd + release + 0.01;
|
||||||
bufferSource.stop(envEnd);
|
bufferSource.stop(envEnd);
|
||||||
const stop = (endTime) => {
|
const stop = (endTime) => {
|
||||||
|
|||||||
@@ -263,8 +263,8 @@ let audioReady;
|
|||||||
export async function initAudioOnFirstClick(options) {
|
export async function initAudioOnFirstClick(options) {
|
||||||
if (!audioReady) {
|
if (!audioReady) {
|
||||||
audioReady = new Promise((resolve) => {
|
audioReady = new Promise((resolve) => {
|
||||||
document.addEventListener('click', async function listener() {
|
document.addEventListener('mousedown', async function listener() {
|
||||||
document.removeEventListener('click', listener);
|
document.removeEventListener('mousedown', listener);
|
||||||
await initAudio(options);
|
await initAudio(options);
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
@@ -274,7 +274,7 @@ export async function initAudioOnFirstClick(options) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let controller;
|
let controller;
|
||||||
function getSuperdoughAudioController() {
|
export function getSuperdoughAudioController() {
|
||||||
if (controller == null) {
|
if (controller == null) {
|
||||||
controller = new SuperdoughAudioController(getAudioContext());
|
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) {
|
function getPhaser(time, end, frequency = 1, depth = 0.5, centerFrequency = 1000, sweep = 2000) {
|
||||||
const ac = getAudioContext();
|
const ac = getAudioContext();
|
||||||
const lfoGain = getLfo(ac, time, end, { frequency, depth: sweep * 2 });
|
const lfo = getLfo(ac, time, end, { frequency, depth: sweep * 2 });
|
||||||
|
|
||||||
//filters
|
//filters
|
||||||
const numStages = 2; //num of filters in series
|
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.frequency.value = centerFrequency + fOffset;
|
||||||
filter.Q.value = 2 - Math.min(Math.max(depth * 2, 0), 1.9);
|
filter.Q.value = 2 - Math.min(Math.max(depth * 2, 0), 1.9);
|
||||||
|
|
||||||
lfoGain.connect(filter.detune);
|
lfo.connect(filter.detune);
|
||||||
fOffset += 282;
|
fOffset += 282;
|
||||||
if (i > 0) {
|
if (i > 0) {
|
||||||
filterChain[i - 1].connect(filter);
|
filterChain[i - 1].connect(filter);
|
||||||
}
|
}
|
||||||
filterChain.push(filter);
|
filterChain.push(filter);
|
||||||
}
|
}
|
||||||
return filterChain[filterChain.length - 1];
|
return { phaser: filterChain[filterChain.length - 1], lfo };
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFilterType(ftype) {
|
function getFilterType(ftype) {
|
||||||
@@ -413,7 +413,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
|||||||
release = 0,
|
release = 0,
|
||||||
|
|
||||||
//phaser
|
//phaser
|
||||||
phaserrate: phaser,
|
phaserrate,
|
||||||
phaserdepth = getDefaultValue('phaserdepth'),
|
phaserdepth = getDefaultValue('phaserdepth'),
|
||||||
phasersweep,
|
phasersweep,
|
||||||
phasercenter,
|
phasercenter,
|
||||||
@@ -553,16 +553,21 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
|||||||
rate: 'lprate',
|
rate: 'lprate',
|
||||||
sync: 'lpsync',
|
sync: 'lpsync',
|
||||||
depth: 'lpdepth',
|
depth: 'lpdepth',
|
||||||
|
depthfrequency: 'lpdepthfrequency',
|
||||||
shape: 'lpshape',
|
shape: 'lpshape',
|
||||||
dcoffset: 'lpdc',
|
dcoffset: 'lpdc',
|
||||||
skew: 'lpskew',
|
skew: 'lpskew',
|
||||||
};
|
};
|
||||||
const lpParams = pickAndRename(value, lpMap);
|
const lpParams = pickAndRename(value, lpMap);
|
||||||
lpParams.type = 'lowpass';
|
lpParams.type = 'lowpass';
|
||||||
let lp = () => createFilter(ac, t, end, lpParams, cps);
|
const lp = () => createFilter(ac, t, end, lpParams, cps, cycle);
|
||||||
chain.push(lp());
|
const { filter: lpf1, lfo: lfo1 } = lp();
|
||||||
|
chain.push(lpf1);
|
||||||
|
lfo1 && audioNodes.push(lfo1);
|
||||||
if (ftype === '24db') {
|
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',
|
rate: 'hprate',
|
||||||
sync: 'hpsync',
|
sync: 'hpsync',
|
||||||
depth: 'hpdepth',
|
depth: 'hpdepth',
|
||||||
|
depthfrequency: 'hpdepthfrequency',
|
||||||
shape: 'hpshape',
|
shape: 'hpshape',
|
||||||
dcoffset: 'hpdc',
|
dcoffset: 'hpdc',
|
||||||
skew: 'hpskew',
|
skew: 'hpskew',
|
||||||
};
|
};
|
||||||
const hpParams = pickAndRename(value, hpMap);
|
const hpParams = pickAndRename(value, hpMap);
|
||||||
hpParams.type = 'highpass';
|
hpParams.type = 'highpass';
|
||||||
let hp = () => createFilter(ac, t, end, hpParams, cps);
|
const hp = () => createFilter(ac, t, end, hpParams, cps, cycle);
|
||||||
chain.push(hp());
|
const { filter: hpf1, lfo: lfo1 } = hp();
|
||||||
|
chain.push(hpf1);
|
||||||
|
lfo1 && audioNodes.push(lfo1);
|
||||||
if (ftype === '24db') {
|
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',
|
rate: 'bprate',
|
||||||
sync: 'bpsync',
|
sync: 'bpsync',
|
||||||
depth: 'bpdepth',
|
depth: 'bpdepth',
|
||||||
|
depthfrequency: 'bpdepthfrequency',
|
||||||
shape: 'bpshape',
|
shape: 'bpshape',
|
||||||
dcoffset: 'bpdc',
|
dcoffset: 'bpdc',
|
||||||
skew: 'bpskew',
|
skew: 'bpskew',
|
||||||
};
|
};
|
||||||
const bpParams = pickAndRename(value, bpMap);
|
const bpParams = pickAndRename(value, bpMap);
|
||||||
bpParams.type = 'bandpass';
|
bpParams.type = 'bandpass';
|
||||||
let bp = () => createFilter(ac, t, end, bpParams, cps);
|
const bp = () => createFilter(ac, t, end, bpParams, cps, cycle);
|
||||||
chain.push(bp());
|
const { filter: bpf1, lfo: lfo1 } = bp();
|
||||||
|
chain.push(bpf1);
|
||||||
|
lfo1 && audioNodes.push(lfo1);
|
||||||
if (ftype === '24db') {
|
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,
|
curve: 1.5,
|
||||||
});
|
});
|
||||||
lfo.connect(amGain.gain);
|
lfo.connect(amGain.gain);
|
||||||
|
audioNodes.push(lfo);
|
||||||
chain.push(amGain);
|
chain.push(amGain);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -680,9 +696,10 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
|||||||
chain.push(panner);
|
chain.push(panner);
|
||||||
}
|
}
|
||||||
// phaser
|
// phaser
|
||||||
if (phaser !== undefined && phaserdepth > 0) {
|
if (phaserrate !== undefined && phaserdepth > 0) {
|
||||||
const phaserFX = getPhaser(t, endWithRelease, phaser, phaserdepth, phasercenter, phasersweep);
|
const { phaser, lfo } = getPhaser(t, endWithRelease, phaserrate, phaserdepth, phasercenter, phasersweep);
|
||||||
chain.push(phaserFX);
|
audioNodes.push(lfo);
|
||||||
|
chain.push(phaser);
|
||||||
}
|
}
|
||||||
|
|
||||||
// last gain
|
// last gain
|
||||||
@@ -692,7 +709,8 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
|||||||
// delay
|
// delay
|
||||||
if (delay > 0 && delaytime > 0 && delayfeedback > 0) {
|
if (delay > 0 && delaytime > 0 && delayfeedback > 0) {
|
||||||
orbitBus.getDelay(delaytime, delayfeedback, t);
|
orbitBus.getDelay(delaytime, delayfeedback, t);
|
||||||
orbitBus.sendDelay(post, delay);
|
const send = orbitBus.sendDelay(post, delay);
|
||||||
|
audioNodes.push(send);
|
||||||
}
|
}
|
||||||
// reverb
|
// reverb
|
||||||
if (room > 0) {
|
if (room > 0) {
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ export class Orbit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sendDelay(node, amount) {
|
sendDelay(node, amount) {
|
||||||
effectSend(node, this.delayNode, amount);
|
return effectSend(node, this.delayNode, amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
duck(t, onsettime = 0, attacktime = 0.1, depth = 1) {
|
duck(t, onsettime = 0, attacktime = 0.1, depth = 1) {
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { registerSound, soundMap } from './superdough.mjs';
|
|||||||
import { getAudioContext } from './audioContext.mjs';
|
import { getAudioContext } from './audioContext.mjs';
|
||||||
import {
|
import {
|
||||||
applyFM,
|
applyFM,
|
||||||
destroyAudioWorkletNode,
|
|
||||||
gainNode,
|
gainNode,
|
||||||
getADSRValues,
|
getADSRValues,
|
||||||
getFrequencyFromValue,
|
getFrequencyFromValue,
|
||||||
@@ -13,11 +12,13 @@ import {
|
|||||||
getVibratoOscillator,
|
getVibratoOscillator,
|
||||||
getWorklet,
|
getWorklet,
|
||||||
noises,
|
noises,
|
||||||
|
releaseAudioNode,
|
||||||
webAudioTimeout,
|
webAudioTimeout,
|
||||||
} from './helpers.mjs';
|
} from './helpers.mjs';
|
||||||
|
import { logger } from './logger.mjs';
|
||||||
import { getNoiseMix, getNoiseOscillator } from './noise.mjs';
|
import { getNoiseMix, getNoiseOscillator } from './noise.mjs';
|
||||||
|
|
||||||
const waveforms = ['triangle', 'square', 'sawtooth', 'sine'];
|
const waveforms = ['triangle', 'square', 'sawtooth', 'sine', 'user'];
|
||||||
const waveformAliases = [
|
const waveformAliases = [
|
||||||
['tri', 'triangle'],
|
['tri', 'triangle'],
|
||||||
['sqr', 'square'],
|
['sqr', 'square'],
|
||||||
@@ -47,19 +48,17 @@ export function registerSynthSounds() {
|
|||||||
[0.001, 0.05, 0.6, 0.01],
|
[0.001, 0.05, 0.6, 0.01],
|
||||||
);
|
);
|
||||||
|
|
||||||
let sound = getOscillator(s, t, value);
|
|
||||||
let { node: o, stop, triggerRelease } = sound;
|
|
||||||
|
|
||||||
// turn down
|
// turn down
|
||||||
const g = gainNode(0.3);
|
const g = gainNode(0.3);
|
||||||
|
|
||||||
const { duration } = value;
|
let sound = getOscillator(s, t, value, () => {
|
||||||
|
|
||||||
o.onended = () => {
|
|
||||||
o.disconnect();
|
|
||||||
g.disconnect();
|
g.disconnect();
|
||||||
onended();
|
onended();
|
||||||
};
|
});
|
||||||
|
|
||||||
|
let { node: o, stop, triggerRelease } = sound;
|
||||||
|
|
||||||
|
const { duration } = value;
|
||||||
|
|
||||||
const envGain = gainNode(1);
|
const envGain = gainNode(1);
|
||||||
let node = o.connect(g).connect(envGain);
|
let node = o.connect(g).connect(envGain);
|
||||||
@@ -193,8 +192,7 @@ export function registerSynthSounds() {
|
|||||||
let timeoutNode = webAudioTimeout(
|
let timeoutNode = webAudioTimeout(
|
||||||
ac,
|
ac,
|
||||||
() => {
|
() => {
|
||||||
destroyAudioWorkletNode(o);
|
releaseAudioNode(o);
|
||||||
envGain.disconnect();
|
|
||||||
onended();
|
onended();
|
||||||
fm?.stop();
|
fm?.stop();
|
||||||
vibratoOscillator?.stop();
|
vibratoOscillator?.stop();
|
||||||
@@ -271,8 +269,7 @@ export function registerSynthSounds() {
|
|||||||
let timeoutNode = webAudioTimeout(
|
let timeoutNode = webAudioTimeout(
|
||||||
ac,
|
ac,
|
||||||
() => {
|
() => {
|
||||||
destroyAudioWorkletNode(o);
|
releaseAudioNode(o);
|
||||||
envGain.disconnect();
|
|
||||||
onended();
|
onended();
|
||||||
},
|
},
|
||||||
begin,
|
begin,
|
||||||
@@ -345,9 +342,8 @@ export function registerSynthSounds() {
|
|||||||
let timeoutNode = webAudioTimeout(
|
let timeoutNode = webAudioTimeout(
|
||||||
ac,
|
ac,
|
||||||
() => {
|
() => {
|
||||||
destroyAudioWorkletNode(o);
|
releaseAudioNode(o);
|
||||||
destroyAudioWorkletNode(lfo);
|
releaseAudioNode(lfo);
|
||||||
envGain.disconnect();
|
|
||||||
onended();
|
onended();
|
||||||
fm?.stop();
|
fm?.stop();
|
||||||
vibratoOscillator?.stop();
|
vibratoOscillator?.stop();
|
||||||
@@ -414,9 +410,13 @@ export function registerSynthSounds() {
|
|||||||
waveformAliases.forEach(([alias, actual]) => soundMap.set({ ...soundMap.get(), [alias]: soundMap.get()[actual] }));
|
waveformAliases.forEach(([alias, actual]) => soundMap.set({ ...soundMap.get(), [alias]: soundMap.get()[actual] }));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function waveformN(partials, type) {
|
const PI2 = 2 * Math.PI;
|
||||||
const real = new Float32Array(partials + 1);
|
export function waveformN(partials, phases, type) {
|
||||||
const imag = new Float32Array(partials + 1);
|
const isList = typeof partials === 'object';
|
||||||
|
partials = isList ? partials : new Float32Array(partials).fill(1);
|
||||||
|
const len = partials.length;
|
||||||
|
const real = new Float32Array(len + 1);
|
||||||
|
const imag = new Float32Array(len + 1);
|
||||||
const ac = getAudioContext();
|
const ac = getAudioContext();
|
||||||
const osc = ac.createOscillator();
|
const osc = ac.createOscillator();
|
||||||
|
|
||||||
@@ -424,20 +424,29 @@ export function waveformN(partials, type) {
|
|||||||
sawtooth: (n) => [0, -1 / n],
|
sawtooth: (n) => [0, -1 / n],
|
||||||
square: (n) => [0, n % 2 === 0 ? 0 : 1 / n],
|
square: (n) => [0, n % 2 === 0 ? 0 : 1 / n],
|
||||||
triangle: (n) => [n % 2 === 0 ? 0 : 1 / (n * n), 0],
|
triangle: (n) => [n % 2 === 0 ? 0 : 1 / (n * n), 0],
|
||||||
|
user: (_n) => [0, 1],
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!terms[type]) {
|
if (!terms[type]) {
|
||||||
throw new Error(`unknown wave type ${type}`);
|
throw new Error(`unknown wave type ${type}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
real[0] = 0; // dc offset
|
for (let n = 0; n < len; n++) {
|
||||||
imag[0] = 0;
|
const mag = partials[n];
|
||||||
let n = 1;
|
const [r, i] = terms[type](n + 1); // we skip n === 0 as this is dc offset
|
||||||
while (n <= partials) {
|
const phase = phases?.[n] ?? 0;
|
||||||
const [r, i] = terms[type](n);
|
// Scale by `partials`
|
||||||
real[n] = r;
|
let R = r * mag;
|
||||||
imag[n] = i;
|
let I = i * mag;
|
||||||
n++;
|
// Apply rotation by the phase
|
||||||
|
if (phase !== 0) {
|
||||||
|
const c = Math.cos(PI2 * phase);
|
||||||
|
const s = Math.sin(PI2 * phase);
|
||||||
|
R = c * R - s * I;
|
||||||
|
I = s * R + c * I;
|
||||||
|
}
|
||||||
|
real[n + 1] = R;
|
||||||
|
imag[n + 1] = I;
|
||||||
}
|
}
|
||||||
|
|
||||||
const wave = ac.createPeriodicWave(real, imag);
|
const wave = ac.createPeriodicWave(real, imag);
|
||||||
@@ -446,21 +455,28 @@ export function waveformN(partials, type) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// expects one of waveforms as s
|
// expects one of waveforms as s
|
||||||
export function getOscillator(s, t, value) {
|
export function getOscillator(s, t, value, onended) {
|
||||||
let { n: partials, duration, noise = 0 } = value;
|
const { duration, noise = 0 } = value;
|
||||||
|
const partials = value.partials ?? value.n;
|
||||||
let o;
|
let o;
|
||||||
|
if (s === 'user' && !partials) {
|
||||||
|
logger(
|
||||||
|
`[superdough] Synth 'user' was selected, but partials not specified. Defaulting to triangle. Use pat.partials to setup custom waveform`,
|
||||||
|
);
|
||||||
|
s = 'triangle';
|
||||||
|
}
|
||||||
|
s = s === 'user' && !partials ? 'triangle' : s;
|
||||||
// If no partials are given, use stock waveforms
|
// If no partials are given, use stock waveforms
|
||||||
if (!partials || s === 'sine') {
|
if (!partials || partials?.length === 0 || s === 'sine') {
|
||||||
o = getAudioContext().createOscillator();
|
o = getAudioContext().createOscillator();
|
||||||
o.type = s || 'triangle';
|
o.type = s || 'triangle';
|
||||||
}
|
}
|
||||||
// generate custom waveform if partials are given
|
// generate custom waveform if partials are given
|
||||||
else {
|
else {
|
||||||
o = waveformN(partials, s);
|
o = waveformN(partials, value.phases, s);
|
||||||
}
|
}
|
||||||
// set frequency
|
// set frequency
|
||||||
o.frequency.value = getFrequencyFromValue(value);
|
o.frequency.value = getFrequencyFromValue(value);
|
||||||
o.start(t);
|
|
||||||
|
|
||||||
let vibratoOscillator = getVibratoOscillator(o.detune, value, t);
|
let vibratoOscillator = getVibratoOscillator(o.detune, value, t);
|
||||||
|
|
||||||
@@ -473,6 +489,13 @@ export function getOscillator(s, t, value) {
|
|||||||
noiseMix = getNoiseMix(o, noise, t);
|
noiseMix = getNoiseMix(o, noise, t);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
o.onended = () => {
|
||||||
|
noiseMix || o.disconnect();
|
||||||
|
noiseMix?.node.disconnect();
|
||||||
|
onended();
|
||||||
|
};
|
||||||
|
o.start(t);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
node: noiseMix?.node || o,
|
node: noiseMix?.node || o,
|
||||||
stop: (time) => {
|
stop: (time) => {
|
||||||
|
|||||||
@@ -114,3 +114,13 @@ export function getCommonSampleInfo(hapValue, bank) {
|
|||||||
export const pickAndRename = (source, map) => {
|
export const pickAndRename = (source, map) => {
|
||||||
return Object.fromEntries(Object.entries(map).map(([newKey, oldKey]) => [newKey, source[oldKey]]));
|
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('/');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -45,7 +45,8 @@ if (typeof GainNode !== 'undefined') {
|
|||||||
throw new Error('vowel: unknown vowel ' + letter);
|
throw new Error('vowel: unknown vowel ' + letter);
|
||||||
}
|
}
|
||||||
const { gains, qs, freqs } = vowelFormant[letter];
|
const { gains, qs, freqs } = vowelFormant[letter];
|
||||||
const makeupGain = ac.createGain();
|
this.makeupGain = ac.createGain();
|
||||||
|
this.audioNodes = [];
|
||||||
for (let i = 0; i < 5; i++) {
|
for (let i = 0; i < 5; i++) {
|
||||||
const gain = ac.createGain();
|
const gain = ac.createGain();
|
||||||
gain.gain.value = gains[i];
|
gain.gain.value = gains[i];
|
||||||
@@ -53,14 +54,25 @@ if (typeof GainNode !== 'undefined') {
|
|||||||
filter.type = 'bandpass';
|
filter.type = 'bandpass';
|
||||||
filter.Q.value = qs[i];
|
filter.Q.value = qs[i];
|
||||||
filter.frequency.value = freqs[i];
|
filter.frequency.value = freqs[i];
|
||||||
this.connect(filter);
|
super.connect(filter);
|
||||||
filter.connect(gain);
|
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.makeupGain.gain.value = 8; // how much makeup gain to add?
|
||||||
this.connect = (target) => makeupGain.connect(target);
|
|
||||||
return this;
|
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) {
|
AudioContext.prototype.createVowelFilter = function (letter) {
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import { getAudioContext, registerSound } from './index.mjs';
|
import { getAudioContext, registerSound } from './index.mjs';
|
||||||
import { getCommonSampleInfo } from './util.mjs';
|
import { getBaseURL, getCommonSampleInfo } from './util.mjs';
|
||||||
import {
|
import {
|
||||||
applyFM,
|
applyFM,
|
||||||
applyParameterModulators,
|
applyParameterModulators,
|
||||||
destroyAudioWorkletNode,
|
|
||||||
getADSRValues,
|
getADSRValues,
|
||||||
getFrequencyFromValue,
|
getFrequencyFromValue,
|
||||||
getParamADSR,
|
getParamADSR,
|
||||||
getPitchEnvelope,
|
getPitchEnvelope,
|
||||||
getVibratoOscillator,
|
getVibratoOscillator,
|
||||||
getWorklet,
|
getWorklet,
|
||||||
|
releaseAudioNode,
|
||||||
webAudioTimeout,
|
webAudioTimeout,
|
||||||
} from './helpers.mjs';
|
} from './helpers.mjs';
|
||||||
import { logger } from './logger.mjs';
|
import { logger } from './logger.mjs';
|
||||||
@@ -190,6 +190,7 @@ export const tables = async (url, frameLen, json, options = {}) => {
|
|||||||
if (url.startsWith('local:')) {
|
if (url.startsWith('local:')) {
|
||||||
url = `http://localhost:5432`;
|
url = `http://localhost:5432`;
|
||||||
}
|
}
|
||||||
|
const base = getBaseURL(url);
|
||||||
if (typeof fetch !== 'function') {
|
if (typeof fetch !== 'function') {
|
||||||
// not a browser
|
// not a browser
|
||||||
return;
|
return;
|
||||||
@@ -200,7 +201,7 @@ export const tables = async (url, frameLen, json, options = {}) => {
|
|||||||
}
|
}
|
||||||
return fetch(url)
|
return fetch(url)
|
||||||
.then((res) => res.json())
|
.then((res) => res.json())
|
||||||
.then((json) => _processTables(json, url, frameLen, options))
|
.then((json) => _processTables(json, base, frameLen, options))
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
throw new Error(`error loading "${url}"`);
|
throw new Error(`error loading "${url}"`);
|
||||||
@@ -318,10 +319,9 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) {
|
|||||||
const timeoutNode = webAudioTimeout(
|
const timeoutNode = webAudioTimeout(
|
||||||
ac,
|
ac,
|
||||||
() => {
|
() => {
|
||||||
destroyAudioWorkletNode(source);
|
releaseAudioNode(source);
|
||||||
vibratoOscillator?.stop();
|
vibratoOscillator?.stop();
|
||||||
fm?.stop();
|
fm?.stop();
|
||||||
node.disconnect();
|
|
||||||
wtPosModulators?.disconnect();
|
wtPosModulators?.disconnect();
|
||||||
wtWarpModulators?.disconnect();
|
wtWarpModulators?.disconnect();
|
||||||
onended();
|
onended();
|
||||||
|
|||||||
@@ -508,13 +508,14 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
process(_input, outputs, params) {
|
process(_input, outputs, params) {
|
||||||
if (currentTime <= params.begin[0]) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (currentTime >= params.end[0]) {
|
if (currentTime >= params.end[0]) {
|
||||||
// this.port.postMessage({ type: 'onended' });
|
// should terminate
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (currentTime <= params.begin[0]) {
|
||||||
|
// keep alive
|
||||||
|
return true;
|
||||||
|
}
|
||||||
const output = outputs[0];
|
const output = outputs[0];
|
||||||
const voices = params.voices[0]; // k-rate
|
const voices = params.voices[0]; // k-rate
|
||||||
for (let i = 0; i < output[0].length; i++) {
|
for (let i = 0; i < output[0].length; i++) {
|
||||||
@@ -532,7 +533,7 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
|
|||||||
const freqVoice = applySemitoneDetuneToFrequency(freq, detuner(n));
|
const freqVoice = applySemitoneDetuneToFrequency(freq, detuner(n));
|
||||||
// We must wrap this here because it is passed into sawblep below which
|
// We must wrap this here because it is passed into sawblep below which
|
||||||
// has domain [0, 1]
|
// has domain [0, 1]
|
||||||
const dt = ffrac(freqVoice * INVSR);
|
const dt = frac(freqVoice * INVSR);
|
||||||
this.phase[n] = this.phase[n] ?? Math.random();
|
this.phase[n] = this.phase[n] ?? Math.random();
|
||||||
const v = waveshapes.sawblep(this.phase[n], dt);
|
const v = waveshapes.sawblep(this.phase[n], dt);
|
||||||
|
|
||||||
@@ -543,8 +544,9 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
|
|||||||
if (pn >= 1.0) pn -= 1.0;
|
if (pn >= 1.0) pn -= 1.0;
|
||||||
this.phase[n] = pn;
|
this.phase[n] = pn;
|
||||||
// invert right and left gain
|
// invert right and left gain
|
||||||
|
const tmp = gainL;
|
||||||
gainL = gainR;
|
gainL = gainR;
|
||||||
gainR = gainL;
|
gainR = tmp;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -950,6 +952,102 @@ class ByteBeatProcessor extends AudioWorkletProcessor {
|
|||||||
|
|
||||||
registerProcessor('byte-beat-processor', ByteBeatProcessor);
|
registerProcessor('byte-beat-processor', ByteBeatProcessor);
|
||||||
|
|
||||||
|
class EnvelopeProcessor extends 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 },
|
||||||
|
{ name: 'release', defaultValue: 0.1, minValue: 0 },
|
||||||
|
{ name: 'attackCurve', defaultValue: 0, minValue: -1, maxValue: 1 },
|
||||||
|
{ name: 'decayCurve', defaultValue: 0, minValue: -1, maxValue: 1 },
|
||||||
|
{ name: 'releaseCurve', defaultValue: 0, minValue: -1, maxValue: 1 },
|
||||||
|
{ name: 'peak', defaultValue: 1 },
|
||||||
|
{ name: 'retrigger', defaultValue: 1, minValue: 0, maxValue: 1 },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.val = 0;
|
||||||
|
this.segIdx = 0;
|
||||||
|
this.state = 0;
|
||||||
|
this.beginTime = 0;
|
||||||
|
this.endTime = 0;
|
||||||
|
this.attackStart = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
_warp(phase, curvature, strength = 8) {
|
||||||
|
if (phase === 0 || phase === 1) return phase; // fast exit
|
||||||
|
if (curvature > 0) {
|
||||||
|
// snappier
|
||||||
|
const exp = 1 + strength * curvature;
|
||||||
|
return 1 - Math.pow(1 - phase, exp);
|
||||||
|
} else {
|
||||||
|
// more calm
|
||||||
|
const exp = 1 - strength * curvature;
|
||||||
|
return Math.pow(phase, exp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_advance(start, target, time, curvature) {
|
||||||
|
if (time === 0 || start === target) {
|
||||||
|
this.val = target;
|
||||||
|
} else {
|
||||||
|
// We compute our progress through this section of the envelope in time
|
||||||
|
// as a `phase` value, which is warped by the curvature, and then used
|
||||||
|
// to compute the value of the envelope at that time
|
||||||
|
const phase = Math.min(1, (currentTime - this.beginTime) / time);
|
||||||
|
const phaseWarped = this._warp(phase, curvature);
|
||||||
|
this.val = start + (target - start) * phaseWarped;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
process(_inputs, outputs, params) {
|
||||||
|
const out = outputs[0][0];
|
||||||
|
if (!out) return true;
|
||||||
|
const begin = pv(params.begin, 0);
|
||||||
|
const retrigger = pv(params.retrigger, 0) >= 0.5; // convert to bool
|
||||||
|
if (begin !== this.beginTime && (this.state === 0 || retrigger)) {
|
||||||
|
// triggered
|
||||||
|
this.beginTime = begin;
|
||||||
|
this.state = 1;
|
||||||
|
this.endTime = pv(params.end, 0);
|
||||||
|
this.attackStart = this.val;
|
||||||
|
}
|
||||||
|
const susTime = this.endTime - this.beginTime;
|
||||||
|
for (let i = 0; i < out.length; i++) {
|
||||||
|
const attack = pv(params.attack, i);
|
||||||
|
const decay = pv(params.decay, i);
|
||||||
|
const sustain = pv(params.sustain, i);
|
||||||
|
const release = pv(params.release, i);
|
||||||
|
const aCurve = pv(params.attackCurve, i);
|
||||||
|
const dCurve = pv(params.decayCurve, i);
|
||||||
|
const rCurve = pv(params.releaseCurve, i);
|
||||||
|
const peak = pv(params.peak, i);
|
||||||
|
const states = [
|
||||||
|
{ time: Number.POSITIVE_INFINITY, start: 0, target: 0 }, // idle
|
||||||
|
{ time: attack, start: this.attackStart, target: 1, curve: aCurve },
|
||||||
|
{ time: attack + decay, start: 1, target: sustain, curve: dCurve },
|
||||||
|
{ time: susTime, start: sustain, target: sustain },
|
||||||
|
{ time: susTime + release, start: sustain, target: 0, curve: rCurve },
|
||||||
|
];
|
||||||
|
let { time, start, target, curve } = states[this.state];
|
||||||
|
this._advance(start, target, time, curve);
|
||||||
|
while (currentTime - this.beginTime >= time) {
|
||||||
|
this.state = (this.state + 1) % states.length;
|
||||||
|
time = states[this.state].time;
|
||||||
|
}
|
||||||
|
out[i] = this.val * peak;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
registerProcessor('envelope-processor', EnvelopeProcessor);
|
||||||
|
|
||||||
export const WarpMode = Object.freeze({
|
export const WarpMode = Object.freeze({
|
||||||
NONE: 0,
|
NONE: 0,
|
||||||
ASYM: 1,
|
ASYM: 1,
|
||||||
@@ -1242,7 +1340,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
|||||||
const tablePos = clamp(pv(parameters.position, i), 0, 1);
|
const tablePos = clamp(pv(parameters.position, i), 0, 1);
|
||||||
const idx = tablePos * (this.numFrames - 1);
|
const idx = tablePos * (this.numFrames - 1);
|
||||||
const fIdx = idx | 0;
|
const fIdx = idx | 0;
|
||||||
const frac = idx - fIdx;
|
const interpT = idx - fIdx;
|
||||||
const warpAmount = clamp(pv(parameters.warp, i), 0, 1);
|
const warpAmount = clamp(pv(parameters.warp, i), 0, 1);
|
||||||
const warpMode = pv(parameters.warpMode, i);
|
const warpMode = pv(parameters.warpMode, i);
|
||||||
const phaseRand = clamp(pv(parameters.phaserand, i), 0, 1);
|
const phaseRand = clamp(pv(parameters.phaserand, i), 0, 1);
|
||||||
@@ -1272,13 +1370,13 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
|||||||
const ph = this._warpPhase(this.phase[n], warpAmount, warpMode);
|
const ph = this._warpPhase(this.phase[n], warpAmount, warpMode);
|
||||||
const s0 = this._sampleFrame(table[fIdx], ph);
|
const s0 = this._sampleFrame(table[fIdx], ph);
|
||||||
const s1 = this._sampleFrame(table[Math.min(this.numFrames - 1, fIdx + 1)], 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) {
|
if (warpMode === WarpMode.FLIP && this.phase[n] < warpAmount) {
|
||||||
s = -s;
|
s = -s;
|
||||||
}
|
}
|
||||||
outL[i] += s * gainL * normalizer;
|
outL[i] += s * gainL * normalizer;
|
||||||
outR[i] += s * gainR * normalizer;
|
outR[i] += s * gainR * normalizer;
|
||||||
this.phase[n] = ffrac(this.phase[n] + dPhase);
|
this.phase[n] = frac(this.phase[n] + dPhase);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { midiToFreq, noteToMidi } from './util.mjs';
|
|||||||
import { registerSound } from './superdough.mjs';
|
import { registerSound } from './superdough.mjs';
|
||||||
import { getAudioContext } from './audioContext.mjs';
|
import { getAudioContext } from './audioContext.mjs';
|
||||||
import { buildSamples } from './zzfx_fork.mjs';
|
import { buildSamples } from './zzfx_fork.mjs';
|
||||||
|
import { onceEnded, releaseAudioNode } from './helpers.mjs';
|
||||||
|
|
||||||
export const getZZFX = (value, t) => {
|
export const getZZFX = (value, t) => {
|
||||||
let {
|
let {
|
||||||
@@ -83,10 +84,10 @@ export function registerZZFXSounds() {
|
|||||||
wave,
|
wave,
|
||||||
(t, value, onended) => {
|
(t, value, onended) => {
|
||||||
const { node: o } = getZZFX({ s: wave, ...value }, t);
|
const { node: o } = getZZFX({ s: wave, ...value }, t);
|
||||||
o.onended = () => {
|
onceEnded(o, () => {
|
||||||
o.disconnect();
|
releaseAudioNode(o);
|
||||||
onended();
|
onended();
|
||||||
};
|
});
|
||||||
return {
|
return {
|
||||||
node: o,
|
node: o,
|
||||||
stop: () => {},
|
stop: () => {},
|
||||||
|
|||||||
@@ -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 { 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';
|
import { stepInNamedScale, nearestNumberIndex } from './tonleiter.mjs';
|
||||||
|
|
||||||
const octavesInterval = (octaves) => (octaves <= 0 ? -1 : 1) + octaves * 7 + 'P';
|
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));
|
if (offset != 0) scaleNote = Note.transpose(scaleNote, Interval.fromSemitones(offset));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger(`[tonal] ${err.message}`, 'error');
|
errorLogger(err, 'tonal');
|
||||||
return; // will be removed
|
return; // will be removed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"name": "dbpatch",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csv": "^6.3.11"
|
"csv": "^6.3.11"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
*/
|
||||||
@@ -48,28 +48,94 @@ You can also use the `crackle` type to play some subtle noise crackles. You can
|
|||||||
|
|
||||||
### Additive Synthesis
|
### Additive Synthesis
|
||||||
|
|
||||||
To tame the harsh sound of the basic waveforms, we can set the `n` control to limit the overtones of the waveform:
|
Periodic waveforms are composed of several [harmonics](https://en.wikipedia.org/wiki/Harmonic) above a fundamental frequency, lying at integer multiples. These overtones combine to give a sound its unique timbral quality.
|
||||||
|
|
||||||
|
For the basic waveforms, we offer you control over these harmonics with the `partials` and `phases` functions.
|
||||||
|
|
||||||
|
#### Partials
|
||||||
|
|
||||||
|
`partials` refers to the magnitude of each harmonic relative to the fundamental frequency. They can thus be used to spectrally filter these waveforms and tame some of their harshness:
|
||||||
|
|
||||||
<MiniRepl
|
<MiniRepl
|
||||||
client:idle
|
client:idle
|
||||||
tune={`note("c2 <eb2 <g2 g1>>".fast(2))
|
tune={`note("c2 <eb2 <g2 g1>>".fast(2))
|
||||||
.sound("sawtooth")
|
.sound("sawtooth")
|
||||||
.n("<32 16 8 4>")
|
.partials([1, 1, "<1 0>", "<1 0>", "<1 0>", "<1 0>", "<1 0>"])
|
||||||
._scope()`}
|
._scope()`}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
When the `n` control is used on a basic waveform, it defines the number of harmonic partials the sound is getting.
|
`partials` can also be used to construct _new_ waveforms not present in our basic set with the 'user' sound source:
|
||||||
You can also set `n` directly in mini notation with `sound`:
|
|
||||||
|
|
||||||
<MiniRepl
|
<MiniRepl
|
||||||
client:idle
|
client:idle
|
||||||
tune={`note("c2 <eb2 <g2 g1>>".fast(2))
|
tune={`note("c2 <eb2 <g2 g1>>".fast(2))
|
||||||
.sound("sawtooth:<32 16 8 4>")
|
.sound("user")
|
||||||
|
.partials([1, 0, 0.3, 0, 0.1, 0, 0, 0.3])
|
||||||
._scope()`}
|
._scope()`}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
Note for tidal users: `n` in tidal is synonymous to `note` for synths only.
|
We may algorithmically construct lists of magnitudes with Javascript code like:
|
||||||
In strudel, this is not the case, where `n` will always change timbre, be it though different samples or different waveforms.
|
|
||||||
|
<MiniRepl
|
||||||
|
client:idle
|
||||||
|
tune={`const numHarmonics = 22;
|
||||||
|
note("c2 <eb2 <g2 g1>>".fast(2))
|
||||||
|
.sound("saw")
|
||||||
|
.partials(new Array(numHarmonics).fill(1))
|
||||||
|
._scope()`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
which acts as a spectral filter. Or:
|
||||||
|
|
||||||
|
<MiniRepl
|
||||||
|
client:idle
|
||||||
|
tune={`note("c2 <eb2 <g2 g1>>").fast(2)
|
||||||
|
.sound("user")
|
||||||
|
.partials(new Array(50).fill(0)
|
||||||
|
.map((_, idx) => ((-1) ** (idx + 1)) / (idx + 1))
|
||||||
|
)
|
||||||
|
._scope()`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
which recovers a familiar waveform.
|
||||||
|
|
||||||
|
`partials` is also compatible with pattern functions designed to produce lists, like `randL` or `binaryL`:
|
||||||
|
|
||||||
|
<MiniRepl
|
||||||
|
client:idle
|
||||||
|
tune={`note("c2 <eb2 <g2 g1>>").fast(2)
|
||||||
|
.sound("user")
|
||||||
|
.partials(randL(10))
|
||||||
|
._scope()`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
and with lists _of_ patterns:
|
||||||
|
|
||||||
|
<MiniRepl
|
||||||
|
client:idle
|
||||||
|
tune={`note("c2 <eb2 <g2 g1>>".fast(4))
|
||||||
|
.sound("user")
|
||||||
|
.partials([1, 0, "0 1", "0 1 0.3", rand])
|
||||||
|
._scope()`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
Note that the first value in the `partials` array controls the magnitude of the fundamental harmonic rather than the DC offset, which is fixed at 0.
|
||||||
|
|
||||||
|
#### Phases
|
||||||
|
|
||||||
|
Earlier, we mentioned that periodic waveforms can be broken into a set of harmonics above a fundamental frequency. Each harmonic has two defining properties: its magnitude (how loud it is) and its phase, which determines where in its cycle that sine wave starts when the waveform is built.
|
||||||
|
|
||||||
|
These phases too can be declared in Strudel and can give your sounds interesting depth.
|
||||||
|
|
||||||
|
<MiniRepl
|
||||||
|
client:idle
|
||||||
|
tune={`s("saw").seg(16).n(irand(12)).scale("F1:minor")
|
||||||
|
.penv(48).panchor(0).pdec(0.05)
|
||||||
|
.delay(0.25).room(0.25)
|
||||||
|
.compressor(-20).vib(0.3)
|
||||||
|
.partials(randL(200))
|
||||||
|
.phases(randL(200))`}
|
||||||
|
/>
|
||||||
|
|
||||||
## Vibrato
|
## Vibrato
|
||||||
|
|
||||||
|
|||||||
@@ -8,3 +8,34 @@ export function ActionButton({ children, label, labelIsHidden, className, ...but
|
|||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function SpecialActionButton(props) {
|
||||||
|
const { className, ...buttonProps } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ActionButton
|
||||||
|
{...buttonProps}
|
||||||
|
className={cx('bg-background p-2 max-w-[300px] rounded-md hover:opacity-50', className)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ActionInput({ label, className, ...props }) {
|
||||||
|
return (
|
||||||
|
<label className={cx('inline-flex items-center cursor-pointer', className)}>
|
||||||
|
<input {...props} className="sr-only peer" />
|
||||||
|
|
||||||
|
<span className="inline-flex items-center peer-hover:opacity-50">{label}</span>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SpecialActionInput({ className, ...props }) {
|
||||||
|
return (
|
||||||
|
<ActionInput
|
||||||
|
{...props}
|
||||||
|
className={className}
|
||||||
|
label={<span className="bg-background p-2 max-w-[300px] rounded-md">{props.label}</span>}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<SpecialActionInput
|
||||||
|
type="file"
|
||||||
|
label="import prebake script"
|
||||||
|
accept=".strudel"
|
||||||
|
onChange={(e) => importScript(e.target.files[0])}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -7,6 +7,8 @@ import { AudioDeviceSelector } from './AudioDeviceSelector.jsx';
|
|||||||
import { AudioEngineTargetSelector } from './AudioEngineTargetSelector.jsx';
|
import { AudioEngineTargetSelector } from './AudioEngineTargetSelector.jsx';
|
||||||
import { confirmDialog } from '../../util.mjs';
|
import { confirmDialog } from '../../util.mjs';
|
||||||
import { DEFAULT_MAX_POLYPHONY, setMaxPolyphony, setMultiChannelOrbits } from '@strudel/webaudio';
|
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 }) {
|
function Checkbox({ label, value, onChange, disabled = false }) {
|
||||||
return (
|
return (
|
||||||
@@ -113,6 +115,7 @@ export function SettingsTab({ started }) {
|
|||||||
isTabIndentationEnabled,
|
isTabIndentationEnabled,
|
||||||
isMultiCursorEnabled,
|
isMultiCursorEnabled,
|
||||||
patternAutoStart,
|
patternAutoStart,
|
||||||
|
includePrebakeScriptInShare,
|
||||||
} = useSettings();
|
} = useSettings();
|
||||||
const shouldAlwaysSync = isUdels();
|
const shouldAlwaysSync = isUdels();
|
||||||
const canChangeAudioDevice = AudioContext.prototype.setSinkId != null;
|
const canChangeAudioDevice = AudioContext.prototype.setSinkId != null;
|
||||||
@@ -204,6 +207,15 @@ export function SettingsTab({ started }) {
|
|||||||
/>
|
/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</div>
|
</div>
|
||||||
|
<FormItem label="Prebake">
|
||||||
|
<ImportPrebakeScriptButton />
|
||||||
|
<Checkbox
|
||||||
|
label="Include prebake script in share"
|
||||||
|
onChange={(cbEvent) => settingsMap.setKey('includePrebakeScriptInShare', cbEvent.target.checked)}
|
||||||
|
value={includePrebakeScriptInShare}
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
|
||||||
<FormItem label="Keybindings">
|
<FormItem label="Keybindings">
|
||||||
<ButtonGroup
|
<ButtonGroup
|
||||||
value={keybindings}
|
value={keybindings}
|
||||||
@@ -313,8 +325,7 @@ export function SettingsTab({ started }) {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
<FormItem label="Zen Mode">Try clicking the logo in the top left!</FormItem>
|
<FormItem label="Zen Mode">Try clicking the logo in the top left!</FormItem>
|
||||||
<FormItem label="Reset Settings">
|
<FormItem label="Reset Settings">
|
||||||
<button
|
<SpecialActionButton
|
||||||
className="bg-background p-2 max-w-[300px] rounded-md hover:opacity-50"
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
confirmDialog('Sure?').then((r) => {
|
confirmDialog('Sure?').then((r) => {
|
||||||
if (r) {
|
if (r) {
|
||||||
@@ -325,7 +336,7 @@ export function SettingsTab({ started }) {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
restore default settings
|
restore default settings
|
||||||
</button>
|
</SpecialActionButton>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ import { prebake } from '@src/repl/prebake.mjs';
|
|||||||
const getSamples = (samples) =>
|
const getSamples = (samples) =>
|
||||||
Array.isArray(samples) ? samples.length : typeof samples === 'object' ? Object.values(samples).length : 1;
|
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() {
|
export function SoundsTab() {
|
||||||
const sounds = useStore(soundMap);
|
const sounds = useStore(soundMap);
|
||||||
|
|
||||||
@@ -56,9 +60,7 @@ export function SoundsTab() {
|
|||||||
|
|
||||||
// holds mutable ref to current triggered sound
|
// holds mutable ref to current triggered sound
|
||||||
const trigRef = useRef();
|
const trigRef = useRef();
|
||||||
|
const numRef = useRef(0);
|
||||||
// Used to cycle through sound previews on banks with multiple sounds
|
|
||||||
let soundPreviewIdx = 0;
|
|
||||||
|
|
||||||
// stop current sound on mouseup
|
// stop current sound on mouseup
|
||||||
useEvent('mouseup', () => {
|
useEvent('mouseup', () => {
|
||||||
@@ -66,6 +68,14 @@ export function SoundsTab() {
|
|||||||
trigRef.current = undefined;
|
trigRef.current = undefined;
|
||||||
ref?.stop?.(getAudioContext().currentTime + 0.01);
|
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 (
|
return (
|
||||||
<div id="sounds-tab" className="px-4 flex gap-2 flex-col w-full h-full text-foreground">
|
<div id="sounds-tab" className="px-4 flex gap-2 flex-col w-full h-full text-foreground">
|
||||||
<Textbox placeholder="Search" value={search} onChange={(v) => setSearch(v)} />
|
<Textbox placeholder="Search" value={search} onChange={(v) => setSearch(v)} />
|
||||||
@@ -115,25 +125,34 @@ export function SoundsTab() {
|
|||||||
const params = {
|
const params = {
|
||||||
note: ['synth', 'soundfont'].includes(data.type) ? 'a3' : undefined,
|
note: ['synth', 'soundfont'].includes(data.type) ? 'a3' : undefined,
|
||||||
s: name,
|
s: name,
|
||||||
n: soundPreviewIdx,
|
n: numRef.current,
|
||||||
clip: 1,
|
clip: 1,
|
||||||
release: 0.5,
|
release: 0.5,
|
||||||
sustain: 1,
|
sustain: 1,
|
||||||
duration: 0.5,
|
duration: 0.5,
|
||||||
};
|
};
|
||||||
soundPreviewIdx++;
|
|
||||||
const onended = () => trigRef.current?.node?.disconnect();
|
const onended = () => trigRef.current?.node?.disconnect();
|
||||||
try {
|
// Attempt to play the sample and retry every 200ms until 10 attempts have been reached
|
||||||
// Pre-load the sample by calling onTrigger with a future time
|
let errMsg;
|
||||||
// This triggers the loading but schedules playback for later
|
for (let attempt = 0; attempt < 10; attempt++) {
|
||||||
const time = ctx.currentTime + 0.5; // Give 500ms for loading
|
try {
|
||||||
const ref = await onTrigger(time, params, onended);
|
// Pre-load the sample by calling onTrigger with a future time
|
||||||
trigRef.current = ref;
|
// This triggers the loading but schedules playback for later
|
||||||
if (ref?.node) {
|
const time = ctx.currentTime + 0.05; // Give 50ms for loading
|
||||||
connectToDestination(ref.node);
|
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);
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { aliasBank, registerSynthSounds, registerZZFXSounds, samples } from '@st
|
|||||||
import { registerSamplesFromDB } from './idbutils.mjs';
|
import { registerSamplesFromDB } from './idbutils.mjs';
|
||||||
import './piano.mjs';
|
import './piano.mjs';
|
||||||
import './files.mjs';
|
import './files.mjs';
|
||||||
|
import { settingsMap } from '@src/settings.mjs';
|
||||||
|
import { evaluate } from '@strudel/transpiler';
|
||||||
|
|
||||||
const { BASE_URL } = import.meta.env;
|
const { BASE_URL } = import.meta.env;
|
||||||
const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL;
|
const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL;
|
||||||
|
|||||||
@@ -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 { code2hash, getPerformanceTimeSeconds, logger, silence } from '@strudel/core';
|
||||||
import { getDrawContext } from '@strudel/draw';
|
import { getDrawContext } from '@strudel/draw';
|
||||||
import { transpiler } from '@strudel/transpiler';
|
import { evaluate, transpiler } from '@strudel/transpiler';
|
||||||
import {
|
import {
|
||||||
getAudioContextCurrentTime,
|
getAudioContextCurrentTime,
|
||||||
webaudioOutput,
|
webaudioOutput,
|
||||||
@@ -63,11 +63,10 @@ async function getModule(name) {
|
|||||||
const initialCode = `// LOADING`;
|
const initialCode = `// LOADING`;
|
||||||
|
|
||||||
export function useReplContext() {
|
export function useReplContext() {
|
||||||
const { isSyncEnabled, audioEngineTarget } = useSettings();
|
const { isSyncEnabled, audioEngineTarget, prebakeScript, includePrebakeScriptInShare } = useSettings();
|
||||||
const shouldUseWebaudio = audioEngineTarget !== audioEngineTargets.osc;
|
const shouldUseWebaudio = audioEngineTarget !== audioEngineTargets.osc;
|
||||||
const defaultOutput = shouldUseWebaudio ? webaudioOutput : superdirtOutput;
|
const defaultOutput = shouldUseWebaudio ? webaudioOutput : superdirtOutput;
|
||||||
const getTime = shouldUseWebaudio ? getAudioContextCurrentTime : getPerformanceTimeSeconds;
|
const getTime = shouldUseWebaudio ? getAudioContextCurrentTime : getPerformanceTimeSeconds;
|
||||||
|
|
||||||
const init = useCallback(() => {
|
const init = useCallback(() => {
|
||||||
const drawTime = [-2, 2];
|
const drawTime = [-2, 2];
|
||||||
const drawContext = getDrawContext();
|
const drawContext = getDrawContext();
|
||||||
@@ -84,7 +83,12 @@ export function useReplContext() {
|
|||||||
pattern: silence,
|
pattern: silence,
|
||||||
drawTime,
|
drawTime,
|
||||||
drawContext,
|
drawContext,
|
||||||
prebake: async () => Promise.all([modulesLoading, presets]),
|
prebake: async () =>
|
||||||
|
Promise.all([modulesLoading, presets]).then(() => {
|
||||||
|
if (prebakeScript?.length) {
|
||||||
|
return evaluate(prebakeScript ?? '');
|
||||||
|
}
|
||||||
|
}),
|
||||||
onUpdateState: (state) => {
|
onUpdateState: (state) => {
|
||||||
setReplState({ ...state });
|
setReplState({ ...state });
|
||||||
},
|
},
|
||||||
@@ -214,7 +218,13 @@ export function useReplContext() {
|
|||||||
editorRef.current.repl.evaluate(code);
|
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 = {
|
const context = {
|
||||||
started,
|
started,
|
||||||
pending,
|
pending,
|
||||||
|
|||||||
@@ -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 { settingPatterns } from '../settings.mjs';
|
||||||
import { setVersionDefaults } from '@strudel/webaudio';
|
import { setVersionDefaults } from '@strudel/webaudio';
|
||||||
import { getMetadata } from '../metadata_parser';
|
import { getMetadata } from '../metadata_parser';
|
||||||
import { isTauri } from '../tauri.mjs';
|
import { isTauri } from '../tauri.mjs';
|
||||||
import './Repl.css';
|
import './Repl.css';
|
||||||
import { createClient } from '@supabase/supabase-js';
|
import { createClient } from '@supabase/supabase-js';
|
||||||
import { nanoid } from 'nanoid';
|
|
||||||
import { writeText } from '@tauri-apps/plugin-clipboard-manager';
|
import { writeText } from '@tauri-apps/plugin-clipboard-manager';
|
||||||
import { $featuredPatterns /* , loadDBPatterns */ } from '@src/user_pattern_utils.mjs';
|
import { $featuredPatterns /* , loadDBPatterns */ } from '@src/user_pattern_utils.mjs';
|
||||||
|
|
||||||
@@ -109,9 +108,8 @@ export function confirmDialog(msg) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let lastShared;
|
|
||||||
|
|
||||||
//RIP due to SPAM
|
//RIP due to SPAM
|
||||||
|
// let lastShared;
|
||||||
// export async function shareCode(codeToShare) {
|
// export async function shareCode(codeToShare) {
|
||||||
// // const codeToShare = activeCode || code;
|
// // const codeToShare = activeCode || code;
|
||||||
// if (lastShared === codeToShare) {
|
// if (lastShared === codeToShare) {
|
||||||
@@ -146,9 +144,10 @@ let lastShared;
|
|||||||
// });
|
// });
|
||||||
// }
|
// }
|
||||||
|
|
||||||
export async function shareCode() {
|
export async function shareCode(codeToShare) {
|
||||||
try {
|
try {
|
||||||
const shareUrl = window.location.href;
|
const hash = '#' + code2hash(codeToShare);
|
||||||
|
const shareUrl = window.location.origin + window.location.pathname + hash;
|
||||||
if (isTauri()) {
|
if (isTauri()) {
|
||||||
await writeText(shareUrl);
|
await writeText(shareUrl);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -45,11 +45,13 @@ export const defaultSettings = {
|
|||||||
isPanelOpen: true,
|
isPanelOpen: true,
|
||||||
togglePanelTrigger: 'click', //click | hover
|
togglePanelTrigger: 'click', //click | hover
|
||||||
userPatterns: '{}',
|
userPatterns: '{}',
|
||||||
|
prebakeScript: '',
|
||||||
audioEngineTarget: audioEngineTargets.webaudio,
|
audioEngineTarget: audioEngineTargets.webaudio,
|
||||||
isButtonRowHidden: false,
|
isButtonRowHidden: false,
|
||||||
isCSSAnimationDisabled: false,
|
isCSSAnimationDisabled: false,
|
||||||
maxPolyphony: 128,
|
maxPolyphony: 128,
|
||||||
multiChannelOrbits: false,
|
multiChannelOrbits: false,
|
||||||
|
includePrebakeScriptInShare: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
let search = null;
|
let search = null;
|
||||||
@@ -96,6 +98,7 @@ export function useSettings() {
|
|||||||
isPanelOpen: parseBoolean(state.isPanelOpen),
|
isPanelOpen: parseBoolean(state.isPanelOpen),
|
||||||
userPatterns: userPatterns,
|
userPatterns: userPatterns,
|
||||||
multiChannelOrbits: parseBoolean(state.multiChannelOrbits),
|
multiChannelOrbits: parseBoolean(state.multiChannelOrbits),
|
||||||
|
includePrebakeScriptInShare: parseBoolean(state.includePrebakeScriptInShare),
|
||||||
patternAutoStart: isUdels()
|
patternAutoStart: isUdels()
|
||||||
? false
|
? false
|
||||||
: state.patternAutoStart === undefined
|
: 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 setPanelPinned = (bool) => settingsMap.setKey('isPanelPinned', bool);
|
||||||
export const setIsPanelOpened = (bool) => settingsMap.setKey('isPanelOpen', 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);
|
export const setIsZen = (active) => settingsMap.setKey('isZen', !!active);
|
||||||
|
|
||||||
const patternSetting = (key) =>
|
const patternSetting = (key) =>
|
||||||
|
|||||||
Reference in New Issue
Block a user