diff --git a/.forgejo/workflows/test.yml b/.forgejo/workflows/test.yml index 90cb7e258..6b3759571 100644 --- a/.forgejo/workflows/test.yml +++ b/.forgejo/workflows/test.yml @@ -1,6 +1,6 @@ name: Strudel tests -on: [push] +on: [push, pull_request] jobs: build: @@ -19,7 +19,7 @@ jobs: - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - cache: 'pnpm' + cache: "pnpm" - run: pnpm install - run: pnpm run format-check - run: pnpm run lint diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f455741f5..a7f65b5bf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -150,6 +150,7 @@ Important: Always publish with `pnpm`, as `npm` does not support overriding main ## useful commands + ```sh #regenerate the test snapshots (ex: when updating or creating new pattern functions) pnpm snapshot @@ -160,6 +161,81 @@ pnpm run osc #build the standalone version pnpm tauri build ``` + +## version tag patching + +here's a little guide on how to patch patterns in the database to prevent breaking old patterns due to breaking changes in newer versions. + +the general tactic is to use `// @version x.y` to tag a pattern with a specific strudel version. when a pattern is evaluated, this metadata will de-activate any breaking changes that came after the specified version. +for example, in version 1.1, the default value for `fanchor` was changed from `0.5` to `0`. +if play a pattern that was made before that change, sounds that use filter evenlopes can sound very different, so by adding `// @version 1.0` will make it sound like it used to. +before releasing a new version with breaking changes, we can edit all patterns in the database, inserting the version tag they were created under: + +as an example, to release version 1.2, do the following: + +1. get date range + +```sh +# get date of last version: +git log -1 --format=%aI @strudel/core@1.1.0 +# 2024-05-31T23:07:26+02:00 + +# get date of current version: +git log -1 --format=%aI @strudel/core@1.2.0 +# 2025-05-01T12:39:24+02:00 +# might also use todays timestamp if version is not yet released +``` + +now we know, all patterns between these 2 dates have to receive a version tag (unless they already have one). + +2. get patterns in question + +```sql +SELECT * +FROM code_v1 +WHERE code NOT LIKE '%@version%' +AND created_at > '2024-05-31T23:07:26+02:00' +AND created_at < '2025-05-01T12:39:24+02:00' +ORDER BY created_at ASC; +``` + +this gives us all unversioned patterns that were saved between 1.1.0 and 1.2.0. in this case, it's 9373 patterns! + +3. insert version tags + +we are now ready to insert the version tag to these patterns. +before updating thousands of patterns, it's probably a good idea to test if a single one gets udpated: + +```sql +UPDATE code_v1 +SET code = code || E'\n// @version 1.1' +WHERE hash = 'Ns2sMB40yIw4'; +``` + +after [verifying](https://strudel.cc/?Ns2sMB40yIw4) that the version tag has been added, let's insert it everywhere: + +```sql +UPDATE code_v1 +SET code = code || E'\n// @version 1.1' +WHERE code NOT LIKE '%@version%' +AND created_at > '2024-05-31T23:07:26+02:00' +AND created_at < '2025-05-01T12:39:24+02:00' +``` + +4. verify + +we can verify that the edits worked by querying all patterns that contain the new version tag: + +```sql +SELECT * +FROM code_v1 +WHERE code LIKE '%@version 1.1%' +AND created_at > '2024-05-31T23:07:26+02:00' +AND created_at < '2025-05-01T12:39:24+02:00' +ORDER BY created_at ASC; +``` + + ## Have Fun Remember to have fun, and that this project is driven by the passion of volunteers! diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..b0c6618be --- /dev/null +++ b/Dockerfile @@ -0,0 +1,21 @@ +FROM node:24 + +WORKDIR /app + +RUN npm install pnpm --global + +COPY pnpm-workspace.yaml ./ +COPY package.json pnpm-lock.yaml ./ +COPY packages/ ./packages/ +COPY examples/ ./examples/ +RUN mkdir -p website/public +COPY website/package.json ./website/ + +RUN pnpm install + + +COPY . . + +EXPOSE 4321 + +CMD ["pnpm", "dev"] diff --git a/README.md b/README.md index d54302ac6..baaac82b2 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,6 @@ Live coding patterns on the web https://strudel.cc/ -Development is moving to https://codeberg.org/uzu/strudel - - Try it here: - Docs: - Technical Blog Post: @@ -38,13 +36,7 @@ Licensing info for the default sound banks can be found over on the [dough-sampl ## Contributing -There are many ways to contribute to this project! See [contribution guide](./CONTRIBUTING.md). - - - - - -Made with [contrib.rocks](https://contrib.rocks). +There are many ways to contribute to this project! See [contribution guide](./CONTRIBUTING.md). You can find the full list of contributors [here](https://codeberg.org/uzu/strudel/activity/contributors). ## Community @@ -53,3 +45,5 @@ There is a #strudel channel on the TidalCycles discord: The discord and forum is shared with the haskell (tidal) and python (vortex) siblings of this project. + +We also have a mastodon account: social.toplap.org/@strudel diff --git a/eslint.config.mjs b/eslint.config.mjs index 19d9bb390..c9ff40ca1 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -42,6 +42,7 @@ export default [ '**/hydra.mjs', '**/jsdoc-synonyms.js', 'packages/hs2js/src/hs2js.mjs', + 'packages/supradough/dough-export.mjs', '**/samples', ], }, @@ -83,4 +84,14 @@ export default [ ], }, }, + { + // Properties provided by AudioWorkletGlobalScope + files: ['packages/superdough/worklets.mjs'], + languageOptions: { + globals: { + currentTime: 'readonly', + sampleRate: 'readonly', + }, + }, + }, ]; diff --git a/jsdoc/jsdoc-synonyms.js b/jsdoc/jsdoc-synonyms.js index 0b52420bc..d59c8dac4 100644 --- a/jsdoc/jsdoc-synonyms.js +++ b/jsdoc/jsdoc-synonyms.js @@ -1,5 +1,5 @@ /* -jsdoc-synonyms.js - Add support for @synonym tag +jsdoc-synonyms.js - Add support for @synonyms tag Copyright (C) 2023 Strudel contributors - see This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ diff --git a/packages/codemirror/autocomplete.mjs b/packages/codemirror/autocomplete.mjs index 203ab8556..69aa3bc59 100644 --- a/packages/codemirror/autocomplete.mjs +++ b/packages/codemirror/autocomplete.mjs @@ -1,68 +1,122 @@ import jsdoc from '../../doc.json'; -// import { javascriptLanguage } from '@codemirror/lang-javascript'; import { autocompletion } from '@codemirror/autocomplete'; import { h } from './html'; -function plaintext(str) { +const escapeHtml = (str) => { const div = document.createElement('div'); div.innerText = str; return div.innerHTML; -} +}; -const getDocLabel = (doc) => doc.name || doc.longname; -const getInnerText = (html) => { - var div = document.createElement('div'); +const stripHtml = (html) => { + const div = document.createElement('div'); div.innerHTML = html; return div.textContent || div.innerText || ''; }; -export function Autocomplete({ doc, label }) { - return h`
-

${label || getDocLabel(doc)}

-${doc.description} -
    - ${doc.params?.map( - ({ name, type, description }) => - `
  • ${name} : ${type.names?.join(' | ')} ${description ? ` - ${getInnerText(description)}` : ''}
  • `, - )} -
-
- ${doc.examples?.map((example) => `
${plaintext(example)}
`)} -
-
`[0]; - /* -
 {
-  console.log('ola!');
-  navigator.clipboard.writeText(example);
-  e.stopPropagation();
-}}
->
-{example}
-
-*/ -} +const getDocLabel = (doc) => doc.name || doc.longname; -const jsdocCompletions = jsdoc.docs - .filter( - (doc) => - getDocLabel(doc) && - !getDocLabel(doc).startsWith('_') && - !['package'].includes(doc.kind) && - !['superdirtOnly', 'noAutocomplete'].some((tag) => doc.tags?.find((t) => t.originalTitle === tag)), - ) - // https://codemirror.net/docs/ref/#autocomplete.Completion - .map((doc) /*: Completion */ => ({ - label: getDocLabel(doc), - // detail: 'xxx', // An optional short piece of information to show (with a different style) after the label. - info: () => Autocomplete({ doc }), - type: 'function', // https://codemirror.net/docs/ref/#autocomplete.Completion.type - })); +const buildParamsList = (params) => + params?.length + ? ` +
+

Parameters

+
    + ${params + .map( + ({ name, type, description }) => ` +
  • + ${name} + ${type.names?.join(' | ')} + ${description ? `
    ${stripHtml(description)}
    ` : ''} +
  • + `, + ) + .join('')} +
+
+ ` + : ''; + +const buildExamples = (examples) => + examples?.length + ? ` +
+

Examples

+ ${examples + .map( + (example) => ` +
${escapeHtml(example)}
+ `, + ) + .join('')} +
+ ` + : ''; + +export const Autocomplete = (doc) => + h` +
+
+

${getDocLabel(doc)}

+ ${doc.synonyms_text ? `
Synonyms: ${doc.synonyms_text}
` : ''} + ${doc.description ? `
${doc.description}
` : ''} + ${buildParamsList(doc.params)} + ${buildExamples(doc.examples)} +
+
+`[0]; + +const isValidDoc = (doc) => { + const label = getDocLabel(doc); + return label && !label.startsWith('_') && !['package'].includes(doc.kind); +}; + +const hasExcludedTags = (doc) => + ['superdirtOnly', 'noAutocomplete'].some((tag) => doc.tags?.find((t) => t.originalTitle === tag)); + +export const getSynonymDoc = (doc, synonym) => { + const synonyms = doc.synonyms || []; + const docLabel = getDocLabel(doc); + // Swap `doc.name` in for `s` in the list of synonyms + const synonymsWithDoc = [docLabel, ...synonyms].filter((x) => x && x !== synonym); + return { + ...doc, + name: synonym, + longname: synonym, + synonyms: synonymsWithDoc, + synonyms_text: synonymsWithDoc.join(', '), + }; +}; + +const jsdocCompletions = (() => { + const seen = new Set(); // avoid repetition + const completions = []; + for (const doc of jsdoc.docs) { + if (!isValidDoc(doc) || hasExcludedTags(doc)) continue; + const docLabel = getDocLabel(doc); + // Remove duplicates + const synonyms = doc.synonyms || []; + let labels = [docLabel, ...synonyms]; + for (const label of labels) { + // https://codemirror.net/docs/ref/#autocomplete.Completion + if (label && !seen.has(label)) { + seen.add(label); + completions.push({ + label, + info: () => Autocomplete(getSynonymDoc(doc, label)), + type: 'function', // https://codemirror.net/docs/ref/#autocomplete.Completion.type + }); + } + } + } + return completions; +})(); + +export const strudelAutocomplete = (context) => { + const word = context.matchBefore(/\w*/); + if (word.from === word.to && !context.explicit) return null; -export const strudelAutocomplete = (context /* : CompletionContext */) => { - let word = context.matchBefore(/\w*/); - if (word.from == word.to && !context.explicit) return null; return { from: word.from, options: jsdocCompletions, @@ -74,11 +128,5 @@ export const strudelAutocomplete = (context /* : CompletionContext */) => { }; }; -export function isAutoCompletionEnabled(on) { - return on - ? [ - autocompletion({ override: [strudelAutocomplete] }), - //javascriptLanguage.data.of({ autocomplete: strudelAutocomplete }), - ] - : []; // autocompletion({ override: [] }) -} +export const isAutoCompletionEnabled = (enabled) => + enabled ? [autocompletion({ override: [strudelAutocomplete], closeOnBlur: false })] : []; diff --git a/packages/codemirror/basicSetup.mjs b/packages/codemirror/basicSetup.mjs new file mode 100644 index 000000000..02294b93a --- /dev/null +++ b/packages/codemirror/basicSetup.mjs @@ -0,0 +1,63 @@ +import { + keymap, + highlightSpecialChars, + drawSelection, + highlightActiveLine, + dropCursor, + rectangularSelection, + crosshairCursor, + lineNumbers, + highlightActiveLineGutter, +} from '@codemirror/view'; +import { + defaultHighlightStyle, + syntaxHighlighting, + bracketMatching, + foldGutter, + foldKeymap, +} from '@codemirror/language'; +import { defaultKeymap, history, historyKeymap } from '@codemirror/commands'; +import { searchKeymap, highlightSelectionMatches } from '@codemirror/search'; +import { completionKeymap, closeBracketsKeymap } from '@codemirror/autocomplete'; + +// Taken + slightly modified from https://github.com/codemirror/basic-setup/blob/main/src/codemirror.ts + +export const basicSetup = (() => [ + // lineNumbers(), + // highlightActiveLineGutter(), + highlightSpecialChars(), + history(), + // foldGutter(), + // drawSelection(), + dropCursor(), + // EditorState.allowMultipleSelections.of(true), + // indentOnInput(), + // syntaxHighlighting(defaultHighlightStyle, { fallback: true }), + // autocompletion(), + rectangularSelection(), + crosshairCursor(), + // highlightActiveLine(), + // highlightSelectionMatches(), + keymap.of([ + ...closeBracketsKeymap, + ...defaultKeymap, + // ...searchKeymap, + ...historyKeymap, + // ...foldKeymap, + // ...completionKeymap, + ]), +])(); + +/// A minimal set of extensions to create a functional editor. Only +/// includes [the default keymap](#commands.defaultKeymap), [undo +/// history](#commands.history), [special character +/// highlighting](#view.highlightSpecialChars), [custom selection +/// drawing](#view.drawSelection), and [default highlight +/// style](#language.defaultHighlightStyle). +export const minimalSetup = (() => [ + highlightSpecialChars(), + history(), + drawSelection(), + syntaxHighlighting(defaultHighlightStyle, { fallback: true }), + keymap.of([...defaultKeymap, ...historyKeymap]), +])(); diff --git a/packages/codemirror/codemirror.mjs b/packages/codemirror/codemirror.mjs index 193d96b55..4dc23996f 100644 --- a/packages/codemirror/codemirror.mjs +++ b/packages/codemirror/codemirror.mjs @@ -1,8 +1,8 @@ import { closeBrackets } from '@codemirror/autocomplete'; export { toggleComment, toggleBlockComment, toggleLineComment, toggleBlockCommentByLine } from '@codemirror/commands'; // import { search, highlightSelectionMatches } from '@codemirror/search'; -import { history } from '@codemirror/commands'; -import { javascript } from '@codemirror/lang-javascript'; +import { indentWithTab } from '@codemirror/commands'; +import { javascript, javascriptLanguage } from '@codemirror/lang-javascript'; import { defaultHighlightStyle, syntaxHighlighting, bracketMatching } from '@codemirror/language'; import { Compartment, EditorState, Prec } from '@codemirror/state'; import { @@ -24,6 +24,7 @@ import { initTheme, activateTheme, theme } from './themes.mjs'; import { sliderPlugin, updateSliderWidgets } from './slider.mjs'; import { widgetPlugin, updateWidgets } from './widget.mjs'; import { persistentAtom } from '@nanostores/persistent'; +import { basicSetup } from './basicSetup.mjs'; const extensions = { isLineWrappingEnabled: (on) => (on ? EditorView.lineWrapping : []), @@ -37,6 +38,14 @@ const extensions = { isActiveLineHighlighted: (on) => (on ? [highlightActiveLine(), highlightActiveLineGutter()] : []), isFlashEnabled, keybindings, + isTabIndentationEnabled: (on) => (on ? keymap.of([indentWithTab]) : []), + isMultiCursorEnabled: (on) => + on + ? [ + EditorState.allowMultipleSelections.of(true), + EditorView.clickAddsSelectionRange.of((ev) => ev.metaKey || ev.ctrlKey), + ] + : [], }; const compartments = Object.fromEntries(Object.keys(extensions).map((key) => [key, new Compartment()])); @@ -51,6 +60,8 @@ export const defaultSettings = { isFlashEnabled: true, isTooltipEnabled: false, isLineWrappingEnabled: false, + isTabIndentationEnabled: false, + isMultiCursorEnabled: false, theme: 'strudelTheme', fontFamily: 'monospace', fontSize: 18, @@ -75,13 +86,17 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo /* search(), highlightSelectionMatches(), */ ...initialSettings, + basicSetup, mondo ? [] : javascript(), + javascriptLanguage.data.of({ + closeBrackets: { brackets: ['(', '[', '{', "'", '"', '<'] }, + bracketMatching: { brackets: ['(', '[', '{', "'", '"', '<'] }, + }), sliderPlugin, widgetPlugin, // indentOnInput(), // works without. already brought with javascript extension? // bracketMatching(), // does not do anything syntaxHighlighting(defaultHighlightStyle), - history(), EditorView.updateListener.of((v) => onChange(v)), drawSelection({ cursorBlinkRate: 0 }), Prec.highest( diff --git a/packages/codemirror/html.mjs b/packages/codemirror/html.mjs index 527275ef6..f240059d3 100644 --- a/packages/codemirror/html.mjs +++ b/packages/codemirror/html.mjs @@ -1,6 +1,7 @@ -const parser = typeof DOMParser !== 'undefined' ? new DOMParser() : null; export let html = (string) => { - return parser?.parseFromString(string, 'text/html').querySelectorAll('*'); + const template = document.createElement('template'); + template.innerHTML = string.trim(); + return template.content.childNodes; }; let parseChunk = (chunk) => { if (Array.isArray(chunk)) return chunk.flat().join(''); diff --git a/packages/codemirror/keybindings.mjs b/packages/codemirror/keybindings.mjs index 6fe00eda1..ca5f34f4c 100644 --- a/packages/codemirror/keybindings.mjs +++ b/packages/codemirror/keybindings.mjs @@ -3,8 +3,9 @@ import { keymap, ViewPlugin } from '@codemirror/view'; // import { searchKeymap } from '@codemirror/search'; import { emacs } from '@replit/codemirror-emacs'; import { vim } from '@replit/codemirror-vim'; +// import { vim } from './vim_test.mjs'; import { vscodeKeymap } from '@replit/codemirror-vscode-keymap'; -import { defaultKeymap, historyKeymap } from '@codemirror/commands'; +import { defaultKeymap } from '@codemirror/commands'; const vscodePlugin = ViewPlugin.fromClass( class { @@ -21,11 +22,11 @@ const vscodeExtension = (options) => [vscodePlugin].concat(options ?? []); const keymaps = { vim, emacs, + codemirror: () => keymap.of(defaultKeymap), vscode: vscodeExtension, }; export function keybindings(name) { const active = keymaps[name]; - return [keymap.of(defaultKeymap), keymap.of(historyKeymap), active ? active() : []]; - // keymap.of(searchKeymap), + return [active ? Prec.high(active()) : []]; } diff --git a/packages/codemirror/package.json b/packages/codemirror/package.json index 4f8508c90..803b33f3d 100644 --- a/packages/codemirror/package.json +++ b/packages/codemirror/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/codemirror", - "version": "1.2.2", + "version": "1.2.5", "description": "Codemirror Extensions for Strudel", "main": "index.mjs", "publishConfig": { @@ -42,7 +42,7 @@ "@lezer/highlight": "^1.2.1", "@nanostores/persistent": "^0.10.2", "@replit/codemirror-emacs": "^6.1.0", - "@replit/codemirror-vim": "^6.2.1", + "@replit/codemirror-vim": "^6.3.0", "@replit/codemirror-vscode-keymap": "^6.0.2", "@strudel/core": "workspace:*", "@strudel/draw": "workspace:*", diff --git a/packages/codemirror/tooltip.mjs b/packages/codemirror/tooltip.mjs index f67e6d14a..d1d0479b2 100644 --- a/packages/codemirror/tooltip.mjs +++ b/packages/codemirror/tooltip.mjs @@ -1,6 +1,6 @@ import { hoverTooltip } from '@codemirror/view'; import jsdoc from '../../doc.json'; -import { Autocomplete } from './autocomplete.mjs'; +import { Autocomplete, getSynonymDoc } from './autocomplete.mjs'; const getDocLabel = (doc) => doc.name || doc.longname; @@ -52,10 +52,11 @@ export const strudelTooltip = hoverTooltip( let entry = jsdoc.docs.filter((doc) => getDocLabel(doc) === word)[0]; if (!entry) { // Try for synonyms - entry = jsdoc.docs.filter((doc) => doc.synonyms && doc.synonyms.includes(word))[0]; - if (!entry) { + const doc = jsdoc.docs.filter((doc) => doc.synonyms && doc.synonyms.includes(word))[0]; + if (!doc) { return null; } + entry = getSynonymDoc(doc, word); } return { @@ -66,7 +67,7 @@ export const strudelTooltip = hoverTooltip( create(view) { let dom = document.createElement('div'); dom.className = 'strudel-tooltip'; - const ac = Autocomplete({ doc: entry, label: word }); + const ac = Autocomplete(entry); dom.appendChild(ac); return { dom }; }, diff --git a/packages/core/bench/pattern.bench.mjs b/packages/core/bench/pattern.bench.mjs index 1b5be0b9c..56a849801 100644 --- a/packages/core/bench/pattern.bench.mjs +++ b/packages/core/bench/pattern.bench.mjs @@ -1,11 +1,11 @@ import { describe, bench } from 'vitest'; -import { calculateTactus, sequence, stack } from '../index.mjs'; +import { calculateSteps, sequence, stack } from '../index.mjs'; const pat64 = sequence(...Array(64).keys()); describe('steps', () => { - calculateTactus(true); + calculateSteps(true); bench( '+tactus', () => { @@ -14,7 +14,7 @@ describe('steps', () => { { time: 1000 }, ); - calculateTactus(false); + calculateSteps(false); bench( '-tactus', () => { @@ -25,7 +25,7 @@ describe('steps', () => { }); describe('stack', () => { - calculateTactus(true); + calculateSteps(true); bench( '+tactus', () => { @@ -34,7 +34,7 @@ describe('stack', () => { { time: 1000 }, ); - calculateTactus(false); + calculateSteps(false); bench( '-tactus', () => { @@ -43,4 +43,4 @@ describe('stack', () => { { time: 1000 }, ); }); -calculateTactus(true); +calculateSteps(true); diff --git a/packages/core/controls.mjs b/packages/core/controls.mjs index e4f4b206d..9137d96fa 100644 --- a/packages/core/controls.mjs +++ b/packages/core/controls.mjs @@ -87,10 +87,244 @@ export function registerControl(names, ...aliases) { */ export const { s, sound } = registerControl(['s', 'n', 'gain'], 'sound'); +/** + * Position in the wavetable of the wavetable oscillator + * + * @name wt + * @param {number | Pattern} position Position in the wavetable from 0 to 1 + * @synonyms wavetablePosition + * @example + * s("squelch").bank("wt_digital").seg(8).note("F1").wt("0 0.25 0.5 0.75 1") + */ +export const { wt, wavetablePosition } = registerControl('wt', 'wavetablePosition'); + +/** + * Amount of envelope applied wavetable oscillator's position envelope + * + * @name wtenv + * @param {number | Pattern} amount between 0 and 1 + */ +export const { wtenv } = registerControl('wtenv'); +/** + * Attack time of the wavetable oscillator's position envelope + * + * @name wtattack + * @synonyms wtatt + * @param {number | Pattern} time attack time in seconds + */ +export const { wtattack, wtatt } = registerControl('wtattack', 'wtatt'); + +/** + * Decay time of the wavetable oscillator's position envelope + * + * @name wtdecay + * @synonyms wtdec + * @param {number | Pattern} time decay time in seconds + */ +export const { wtdecay, wtdec } = registerControl('wtdecay', 'wtdec'); + +/** + * Sustain time of the wavetable oscillator's position envelope + * + * @name wtsustain + * @synonyms wtsus + * @param {number | Pattern} gain sustain level (0 to 1) + */ +export const { wtsustain, wtsus } = registerControl('wtsustain', 'wtsus'); + +/** + * Release time of the wavetable oscillator's position envelope + * + * @name wtrelease + * @synonyms wtrel + * @param {number | Pattern} time release time in seconds + */ +export const { wtrelease, wtrel } = registerControl('wtrelease', 'wtrel'); + +/** + * Rate of the LFO for the wavetable oscillator's position + * + * @name wtrate + * @param {number | Pattern} rate rate in hertz + */ +export const { wtrate } = registerControl('wtrate'); +/** + * cycle synced rate of the LFO for the wavetable oscillator's position + * + * @name wtsync + * @param {number | Pattern} rate rate in cycles + */ +export const { wtsync } = registerControl('wtsync'); + +/** + * Depth of the LFO for the wavetable oscillator's position + * + * @name wtdepth + * @param {number | Pattern} depth depth of modulation + */ +export const { wtdepth } = registerControl('wtdepth'); + +/** + * Shape of the LFO for the wavetable oscillator's position + * + * @name wtshape + * @param {number | Pattern} shape Shape of the lfo (0, 1, 2, ..) + */ +export const { wtshape } = registerControl('wtshape'); + +/** + * DC offset of the LFO for the wavetable oscillator's position + * + * @name wtdc + * @param {number | Pattern} dcoffset dc offset. set to 0 for unipolar + */ +export const { wtdc } = registerControl('wtdc'); + +/** + * Skew of the LFO for the wavetable oscillator's position + * + * @name wtskew + * @param {number | Pattern} skew How much to bend the LFO shape + */ +export const { wtskew } = registerControl('wtskew'); + +/** + * Amount of warp (alteration of the waveform) to apply to the wavetable oscillator + * + * @name warp + * @param {number | Pattern} amount Warp of the wavetable from 0 to 1 + * @synonyms wavetableWarp + * @example + * s("basique").bank("wt_digital").seg(8).note("F1").warp("0 0.25 0.5 0.75 1") + * .warpmode("spin") + */ +export const { warp, wavetableWarp } = registerControl('warp', 'wavetableWarp'); + +/** + * Attack time of the wavetable oscillator's warp envelope + * + * @name warpattack + * @synonyms warpatt + * @param {number | Pattern} time attack time in seconds + */ +export const { warpattack, warpatt } = registerControl('warpattack', 'warpatt'); + +/** + * Decay time of the wavetable oscillator's warp envelope + * + * @name warpdecay + * @synonyms warpdec + * @param {number | Pattern} time decay time in seconds + */ +export const { warpdecay, warpdec } = registerControl('warpdecay', 'warpdec'); + +/** + * Sustain time of the wavetable oscillator's warp envelope + * + * @name warpsustain + * @synonyms warpsus + * @param {number | Pattern} gain sustain level (0 to 1) + */ +export const { warpsustain, warpsus } = registerControl('warpsustain', 'warpsus'); + +/** + * Release time of the wavetable oscillator's warp envelope + * + * @name warprelease + * @synonyms warprel + * @param {number | Pattern} time release time in seconds + */ +export const { warprelease, warprel } = registerControl('warprelease', 'warprel'); + +/** + * Rate of the LFO for the wavetable oscillator's warp + * + * @name warprate + * @param {number | Pattern} rate rate in hertz + */ +export const { warprate } = registerControl('warprate'); + +/** + * Depth of the LFO for the wavetable oscillator's warp + * + * @name warpdepth + * @param {number | Pattern} depth depth of modulation + */ +export const { warpdepth } = registerControl('warpdepth'); + +/** + * Shape of the LFO for the wavetable oscillator's warp + * + * @name warpshape + * @param {number | Pattern} shape Shape of the lfo (0, 1, 2, ..) + */ +export const { warpshape } = registerControl('warpshape'); + +/** + * DC offset of the LFO for the wavetable oscillator's warp + * + * @name warpdc + * @param {number | Pattern} dcoffset dc offset. set to 0 for unipolar + */ +export const { warpdc } = registerControl('warpdc'); + +/** + * Skew of the LFO for the wavetable oscillator's warp + * + * @name warpskew + * @param {number | Pattern} skew How much to bend the LFO shape + */ +export const { warpskew } = registerControl('warpskew'); + +/** + * Type of warp (alteration of the waveform) to apply to the wavetable oscillator. + * + * The current options are: none, asym, bendp, bendm, bendmp, sync, quant, fold, pwm, orbit, + * spin, chaos, primes, binary, brownian, reciprocal, wormhole, logistic, sigmoid, fractal, flip + * + * @name warpmode + * @param {number | string | Pattern} mode Warp mode + * @synonyms wavetableWarpMode + * @example + * s("morgana").bank("wt_digital").seg(8).note("F1").warp("0 0.25 0.5 0.75 1") + * .warpmode("*2") + * + */ +export const { warpmode, wavetableWarpMode } = registerControl('warpmode', 'wavetableWarpMode'); + +/** + * Amount of randomness of the initial phase of the wavetable oscillator. + * + * @name wtphaserand + * @param {number | Pattern} amount Randomness of the initial phase. Between 0 (not random) and 1 (fully random) + * @synonyms wavetablePhaseRand + * @example + * s("basique").bank("wt_digital").seg(16).wtphaserand("<0 1>") + * + */ +export const { wtphaserand, wavetablePhaseRand } = registerControl('wtphaserand', 'wavetablePhaseRand'); + +/** + * Amount of envelope applied wavetable oscillator's position envelope + * + * @name warpenv + * @param {number | Pattern} amount between 0 and 1 + */ +export const { warpenv } = registerControl('warpenv'); + +/** + * cycle synced rate of the LFO for the wavetable warp position + * + * @name warpsync + * @param {number | Pattern} rate rate in cycles + */ +export const { warpsync } = registerControl('warpsync'); + /** * Define a custom webaudio node to use as a sound source. * * @name source + * @synonyms src * @param {function} getSource * @synonyms src * @@ -113,7 +347,7 @@ export const { n } = registerControl('n'); * * - a letter (a-g or A-G) * - optional accidentals (b or #) - * - optional octave number (0-9). Defaults to 3 + * - optional (possibly negative) octave number (0-9). Defaults to 3 * * Examples of valid note names: `c`, `bb`, `Bb`, `f#`, `c3`, `A4`, `Eb2`, `c#5` * @@ -126,6 +360,8 @@ export const { n } = registerControl('n'); * note("c4 a4 f4 e4") * @example * note("60 69 65 64") + * @example + * note("fbb1 a#0 cbbb-1 e##-2").sound("saw") */ export const { note } = registerControl(['note', 'n']); @@ -141,8 +377,8 @@ export const { note } = registerControl(['note', 'n']); */ export const { accelerate } = registerControl('accelerate'); /** - * * Sets the velocity from 0 to 1. Is multiplied together with gain. + * * @name velocity * @example * s("hh*8") @@ -264,6 +500,20 @@ export const { fmenv } = registerControl('fmenv'); * */ export const { fmattack } = registerControl('fmattack'); + +/** + * Waveform of the fm modulator + * + * @name fmwave + * @param {number | Pattern} wave waveform + * @example + * n("0 1 2 3".fast(4)).scale("d:minor").s("sine").fmwave("").fm(4).fmh(2.01) + * @example + * n("0 1 2 3".fast(4)).chord("").voicing().s("sawtooth").fmwave("brown").fm(.6) + * + */ +export const { fmwave } = registerControl('fmwave'); + /** * Decay time for the FM envelope: seconds until the sustain level is reached after the attack phase. * @@ -307,6 +557,17 @@ export const { fmvelocity } = registerControl('fmvelocity'); */ export const { bank } = registerControl('bank'); +/** + * mix control for the chorus effect + * + * @name chorus + * @param {string | Pattern} chorus mix amount between 0 and 1 + * @example + * note("d d a# a").s("sawtooth").chorus(.5) + * + */ +export const { chorus } = registerControl('chorus'); + // analyser node send amount 0 - 1 (used by scope) export const { analyze } = registerControl('analyze'); // fftSize of analyser @@ -318,6 +579,7 @@ export const { fft } = registerControl('fft'); * * @name decay * @param {number | Pattern} time decay time in seconds + * @synonyms dec * @example * note("c3 e3 f3 g3").decay("<.1 .2 .3 .4>").sustain(0) * @@ -374,7 +636,7 @@ export const { bandf, bpf, bp } = registerControl(['bandf', 'bandq', 'bpenv'], ' // ['bpq'], export const { bandq, bpq } = registerControl('bandq', 'bpq'); /** - * a pattern of numbers from 0 to 1. Skips the beginning of each sample, e.g. `0.25` to cut off the first quarter from each sample. + * A pattern of numbers from 0 to 1. Skips the beginning of each sample, e.g. `0.25` to cut off the first quarter from each sample. * * @memberof Pattern * @name begin @@ -435,7 +697,7 @@ export const { loopBegin, loopb } = registerControl('loopBegin', 'loopb'); */ export const { loopEnd, loope } = registerControl('loopEnd', 'loope'); /** - * bit crusher effect. + * Bit crusher effect. * * @name crush * @param {number | Pattern} depth between 1 (for drastic reduction in bit-depth) to 16 (for barely no reduction). @@ -446,7 +708,7 @@ export const { loopEnd, loope } = registerControl('loopEnd', 'loope'); // ['clhatdecay'], export const { crush } = registerControl('crush'); /** - * fake-resampling for lowering the sample rate. Caution: This effect seems to only work in chromium based browsers + * Fake-resampling for lowering the sample rate. Caution: This effect seems to only work in chromium based browsers * * @name coarse * @param {number | Pattern} factor 1 for original 2 for half, 3 for a third and so on. @@ -457,7 +719,80 @@ export const { crush } = registerControl('crush'); export const { coarse } = registerControl('coarse'); /** - * filter overdrive for supported filter types + * Modulate the amplitude of a sound with a continuous waveform + * + * @name tremolo + * @synonyms trem + * @param {number | Pattern} speed modulation speed in HZ + * @example + * note("d d d# d".fast(4)).s("supersaw").tremolo("<3 2 100> ").tremoloskew("<.5>") + * + */ +export const { tremolo } = registerControl(['tremolo', 'tremolodepth', 'tremoloskew', 'tremolophase'], 'trem'); + +/** + * Modulate the amplitude of a sound with a continuous waveform + * + * @name tremolosync + * @synonyms tremsync + * @param {number | Pattern} cycles modulation speed in cycles + * @example + * note("d d d# d".fast(4)).s("supersaw").tremolosync("4").tremoloskew("<1 .5 0>") + * + */ +export const { tremolosync } = registerControl( + ['tremolosync', 'tremolodepth', 'tremoloskew', 'tremolophase'], + 'tremsync', +); + +/** + * Depth of amplitude modulation + * + * @name tremolodepth + * @synonyms tremdepth + * @param {number | Pattern} depth + * @example + * note("a1 a1 a#1 a1".fast(4)).s("pulse").tremsync(4).tremolodepth("<1 2 .7>") + * + */ +export const { tremolodepth } = registerControl('tremolodepth', 'tremdepth'); +/** + * Alter the shape of the modulation waveform + * + * @name tremoloskew + * @synonyms tremskew + * @param {number | Pattern} amount between 0 & 1, the shape of the waveform + * @example + * note("{f a c e}%16").s("sawtooth").tremsync(4).tremoloskew("<.5 0 1>") + * + */ +export const { tremoloskew } = registerControl('tremoloskew', 'tremskew'); + +/** + * Alter the phase of the modulation waveform + * + * @name tremolophase + * @synonyms tremphase + * @param {number | Pattern} offset the offset in cycles of the modulation + * @example + * note("{f a c e}%16").s("sawtooth").tremsync(4).tremolophase("<0 .25 .66>") + * + */ +export const { tremolophase } = registerControl('tremolophase', 'tremphase'); + +/** + * Shape of amplitude modulation + * + * @name tremoloshape + * @synonyms tremshape + * @param {number | Pattern} shape tri | square | sine | saw | ramp + * @example + * note("{f g c d}%16").tremsync(4).tremoloshape("").s("sawtooth") + * + */ +export const { tremoloshape } = registerControl('tremoloshape', 'tremshape'); +/** + * Filter overdrive for supported filter types * * @name drive * @param {number | Pattern} amount @@ -467,6 +802,92 @@ export const { coarse } = registerControl('coarse'); */ export const { drive } = registerControl('drive'); +/** + * Modulate the amplitude of an orbit to create a "sidechain" like effect. + * + * Can be applied to multiple orbits with the ':' mininotation, e.g. `duckorbit("2:3")` + * + * @name duckorbit + * @synonyms duck + * @param {number | Pattern} orbit target orbit + * @example + * $: n(run(16)).scale("c:minor:pentatonic").s("sawtooth").delay(.7).orbit(2) + * $: s("bd:4!4").beat("0,4,8,11,14",16).duckorbit(2).duckattack(0.2).duckdepth(1) + * @example + * $: n(run(16)).scale("c:minor:pentatonic").s("sawtooth").delay(.7).orbit(2) + * $: s("hh*16").orbit(3) + * $: s("bd:4!4").beat("0,4,8,11,14",16).duckorbit("2:3").duckattack(0.2).duckdepth(1) + * + */ +export const { duck } = registerControl('duckorbit', 'duck'); + +/** + * The amount of ducking applied to target orbit + * + * Can vary across orbits with the ':' mininotation, e.g. `duckdepth("0.3:0.1")`. + * Note: this requires first applying the effect to multiple orbits with e.g. `duckorbit("2:3")`. + * + * @name duckdepth + * @param {number | Pattern} depth depth of modulation from 0 to 1 + * @example + * stack( n(run(8)).scale("c:minor").s("sawtooth").delay(.7).orbit(2), s("bd:4!4").beat("0,4,8,11,14",16).duckorbit(2).duckattack(0.2).duckdepth("<1 .9 .6 0>")) + * @example + * $: n(run(16)).scale("c:minor:pentatonic").s("sawtooth").delay(.7).orbit(2) + * $: s("hh*16").orbit(3) + * $: s("bd:4!4").beat("0,4,8,11,14",16).duckorbit("2:3").duckattack(0.2).duckdepth("1:0.5") + * + */ +export const { duckdepth } = registerControl('duckdepth'); + +/** + * The time required for the ducked signal(s) to reach their lowest volume. + * Can be used to prevent clicking or for creative rhythmic effects. + * + * Can vary across orbits with the ':' mininotation, e.g. `duckonset("0:0.003")`. + * Note: this requires first applying the effect to multiple orbits with e.g. `duckorbit("2:3")`. + * + * @name duckonset + * @synonyms duckons + * + * @param {number | Pattern} time The onset time in seconds + * @example + * // Clicks + * sound: freq("63.2388").s("sine").orbit(2).gain(4) + * duckerWithClick: s("bd*4").duckorbit(2).duckattack(0.3).duckonset(0).postgain(0) + * @example + * // No clicks + * sound: freq("63.2388").s("sine").orbit(2).gain(4) + * duckerWithoutClick: s("bd*4").duckorbit(2).duckattack(0.3).duckonset(0.01).postgain(0) + * @example + * // Rhythmic + * noise: s("pink").distort("2:1").orbit(4) // used rhythmically with 0.3 onset below + * hhat: s("hh*16").orbit(7) + * ducker: s("bd*4").bank("tr909").duckorbit("4:7").duckonset("0.3:0.003").duckattack(0.25) + * + */ +export const { duckonset } = registerControl('duckonset', 'duckons'); + +/** + * The time required for the ducked signal(s) to return to their normal volume. + * + * Can vary across orbits with the ':' mininotation, e.g. `duckonset("0:0.003")`. + * Note: this requires first applying the effect to multiple orbits with e.g. `duckorbit("2:3")`. + * + * @name duckattack + * @synonyms duckatt + * + * @param {number | Pattern} time The attack time in seconds + * @example + * sound: n(run(8)).scale("c:minor").s("sawtooth").delay(.7).orbit(2) + * ducker: s("bd:4!4").beat("0,4,8,11,14",16).duckorbit(2).duckattack("<0.2 0 0.4>").duckdepth(1) + * @example + * moreduck: n(run(8)).scale("c:minor").s("sawtooth").delay(.7).orbit(2) + * lessduck: s("hh*16").orbit(5) + * ducker: s("bd:4!4").beat("0,4,8,11,14",16).duckorbit("2:5").duckattack("0.4:0.1") + * + */ +export const { duckattack } = registerControl('duckattack', 'duckatt'); + /** * Create byte beats with custom expressions * @@ -507,7 +928,7 @@ export const { byteBeatStartTime, bbst } = registerControl('byteBeatStartTime', export const { channels, ch } = registerControl('channels', 'ch'); /** - * controls the pulsewidth of the pulse oscillator + * Controls the pulsewidth of the pulse oscillator * * @name pw * @param {number | Pattern} pulsewidth @@ -519,7 +940,7 @@ export const { channels, ch } = registerControl('channels', 'ch'); export const { pw } = registerControl(['pw', 'pwrate', 'pwsweep']); /** - * controls the lfo rate for the pulsewidth of the pulse oscillator + * Controls the lfo rate for the pulsewidth of the pulse oscillator * * @name pwrate * @param {number | Pattern} rate @@ -531,7 +952,7 @@ export const { pw } = registerControl(['pw', 'pwrate', 'pwsweep']); export const { pwrate } = registerControl('pwrate'); /** - * controls the lfo sweep for the pulsewidth of the pulse oscillator + * Controls the lfo sweep for the pulsewidth of the pulse oscillator * * @name pwsweep * @param {number | Pattern} sweep @@ -572,7 +993,7 @@ export const { phaserrate, ph, phaser } = registerControl( export const { phasersweep, phs } = registerControl('phasersweep', 'phs'); /** - * The center frequency of the phaser in HZ. Defaults to 1000 + * The center frequency of the phaser in HZ. Defaults to 1000 * * @name phasercenter * @synonyms phc @@ -589,7 +1010,7 @@ export const { phasercenter, phc } = registerControl('phasercenter', 'phc'); * The amount the signal is affected by the phaser effect. Defaults to 0.75 * * @name phaserdepth - * @synonyms phd + * @synonyms phd, phasdp * @param {number | Pattern} depth number between 0 and 1 * @example * n(run(8)).scale("D:pentatonic").s("sawtooth").release(0.5) @@ -600,7 +1021,7 @@ export const { phasercenter, phc } = registerControl('phasercenter', 'phc'); export const { phaserdepth, phd, phasdp } = registerControl('phaserdepth', 'phd', 'phasdp'); /** - * choose the channel the pattern is sent to in superdirt + * Choose the channel the pattern is sent to in superdirt * * @name channel * @param {number | Pattern} channel channel number @@ -952,7 +1373,7 @@ export const { resonance, lpq } = registerControl('resonance', 'lpq'); * @name djf * @param {number | Pattern} cutoff below 0.5 is low pass filter, above is high pass filter * @example - * n("0 3 7 [10,24]").s('superzow').octave(3).djf("<.5 .25 .5 .75>").osc() + * n(irand(16).seg(8)).scale("d:phrygian").s("supersaw").djf("<.5 .3 .2 .75>") * */ export const { djf } = registerControl('djf'); @@ -986,14 +1407,27 @@ export const { delay } = registerControl(['delay', 'delaytime', 'delayfeedback'] * */ export const { delayfeedback, delayfb, dfb } = registerControl('delayfeedback', 'delayfb', 'dfb'); + +/** + * Sets the level of the signal that is fed back into the delay. + * Caution: Values >= 1 will result in a signal that gets louder and louder! Don't do it + * + * @name delayfeedback + * @param {number | Pattern} feedback between 0 and 1 + * @synonyms delayfb, dfb + * @example + * s("bd").delay(.25).delayfeedback("<.25 .5 .75 1>") + * + */ +export const { delayspeed } = registerControl('delayspeed'); /** * Sets the time of the delay effect. * - * @name delaytime - * @param {number | Pattern} seconds between 0 and Infinity + * @name delayspeed + * @param {number | Pattern} delayspeed controls the pitch of the delay feedback * @synonyms delayt, dt * @example - * s("bd bd").delay(.25).delaytime("<.125 .25 .5 1>") + * note("d d a# a".fast(2)).s("sawtooth").delay(.8).delaytime(1/2).delayspeed("<2 .5 -1 -2>") * */ export const { delaytime, delayt, dt } = registerControl('delaytime', 'delayt', 'dt'); @@ -1071,6 +1505,7 @@ export const { dry } = registerControl('dry'); * Used when using `begin`/`end` or `chop`/`striate` and friends, to change the fade out time of the 'grain' envelope. * * @name fadeTime + * @synonyms fadeOutTime * @param {number | Pattern} time between 0 and 1 * @example * s("oh*4").end(.1).fadeTime("<0 .2 .4 .8>").osc() @@ -1405,6 +1840,29 @@ export const { roomfade, rfade } = registerControl('roomfade', 'rfade'); * */ export const { ir, iresponse } = registerControl(['ir', 'i'], 'iresponse'); + +/** + * Sets speed of the sample for the impulse response. + * @name irspeed + * @param {string | Pattern} speed + * @example + * samples('github:switchangel/pad') + * $: s("brk/2").fit().scrub(irand(16).div(16).seg(8)).ir("swpad:4").room(.2).irspeed("<2 1 .5>/2").irbegin(.5).roomsize(.5) + * + */ +export const { irspeed } = registerControl('irspeed'); + +/** + * Sets the beginning of the IR response sample + * @name irbegin + * @param {string | Pattern} begin between 0 and 1 + * @synonyms ir + * @example + * samples('github:switchangel/pad') + * $: s("brk/2").fit().scrub(irand(16).div(16).seg(8)).ir("swpad:4").room(.65).irspeed("-2").irbegin("<0 .5 .75>/2").roomsize(.6) + * + */ +export const { irbegin } = registerControl('irbegin'); /** * Sets the room size of the reverb, see `room`. * When this property is changed, the reverb will be recaculated, so only change this sparsely.. @@ -1441,19 +1899,52 @@ export const { roomsize, size, sz, rsize } = registerControl('roomsize', 'size', export const { shape } = registerControl(['shape', 'shapevol']); /** * Wave shaping distortion. CAUTION: it can get loud. - * Second option in optional array syntax (ex: ".9:.5") applies a postgain to the output. + * Second option in optional array syntax (ex: ".9:.5") applies a postgain to the output. Third option sets the waveshaping type. * Most useful values are usually between 0 and 10 (depending on source gain). If you are feeling adventurous, you can turn it up to 11 and beyond ;) * * @name distort * @synonyms dist - * @param {number | Pattern} distortion + * @param {number | Pattern} distortion amount of distortion to apply + * @param {number | Pattern} volume linear postgain of the distortion + * @param {number | string | Pattern} type type of distortion to apply * @example * s("bd sd [~ bd] sd,hh*8").distort("<0 2 3 10:.5>") * @example * note("d1!8").s("sine").penv(36).pdecay(.12).decay(.23).distort("8:.4") + * @example + * s("bd:4*4").bank("tr808").distort("3:0.5:diode") * */ -export const { distort, dist } = registerControl(['distort', 'distortvol'], 'dist'); +export const { distort, dist } = registerControl(['distort', 'distortvol', 'distorttype'], 'dist'); + +/** + * Postgain for waveshaping distortion. + * + * @name distortvol + * @synonyms distvol + * @param {number | Pattern} volume linear postgain of the distortion + * @example + * s("bd*4").bank("tr909").distort(2).distortvol(0.8) + */ +export const { distortvol } = registerControl('distortvol', 'distvol'); + +/** + * Type of waveshaping distortion to apply. + * + * @name distorttype + * @synonyms disttype + * @param {number | string | Pattern} type type of distortion to apply + * @example + * s("bd*4").bank("tr909").distort(2).distorttype("<0 1 2>") + * + * @example + * s("sine").note("F1*2").release(1) + * .penv(24).pdecay(0.05) + * .distort(rand.range(1, 8)) + * .distorttype("") + */ +export const { distorttype } = registerControl('distorttype', 'disttype'); + /** * Dynamics Compressor. The params are `compressor("threshold:ratio:knee:attack:release")` * More info [here](https://developer.mozilla.org/en-US/docs/Web/API/DynamicsCompressorNode?retiredLocale=de#instance_properties) @@ -1573,18 +2064,6 @@ export const { density } = registerControl('density'); // ['modwheel'], export const { expression } = registerControl('expression'); export const { sustainpedal } = registerControl('sustainpedal'); -/* // TODO: doesn't seem to do anything - * - * Tremolo Audio DSP effect - * - * @name tremolodepth - * @param {number | Pattern} depth between 0 and 1 - * @example - * n("0,4,7").tremolodepth("<0 .3 .6 .9>").osc() - * - */ -export const { tremolodepth, tremdp } = registerControl('tremolodepth', 'tremdp'); -export const { tremolorate, tremr } = registerControl('tremolorate', 'tremr'); export const { fshift } = registerControl('fshift'); export const { fshiftnote } = registerControl('fshiftnote'); @@ -1661,7 +2140,6 @@ export const { zmod } = registerControl('zmod'); // like crush but scaled differently export const { zcrush } = registerControl('zcrush'); export const { zdelay } = registerControl('zdelay'); -export const { tremolo } = registerControl('tremolo'); export const { zzfx } = registerControl('zzfx'); /** diff --git a/packages/core/cyclist.mjs b/packages/core/cyclist.mjs index f28dc604c..59e410c53 100644 --- a/packages/core/cyclist.mjs +++ b/packages/core/cyclist.mjs @@ -5,7 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th */ import createClock from './zyklus.mjs'; -import { logger } from './logger.mjs'; +import { errorLogger, logger } from './logger.mjs'; export class Cyclist { constructor({ @@ -57,7 +57,7 @@ export class Cyclist { } // query the pattern for events - const haps = this.pattern.queryArc(begin, end, { _cps: this.cps }); + const haps = this.pattern.queryArc(begin, end, { _cps: this.cps, cyclist: 'cyclist' }); haps.forEach((hap) => { if (hap.hasOnset()) { @@ -67,6 +67,7 @@ export class Cyclist { // the following line is dumb and only here for backwards compatibility // see https://codeberg.org/uzu/strudel/pulls/1004 const deadline = targetTime - phase; + // this onTrigger has another signature onTrigger?.(hap, deadline, duration, this.cps, targetTime); if (hap.value.cps !== undefined && this.cps != hap.value.cps) { this.cps = hap.value.cps; @@ -75,7 +76,7 @@ export class Cyclist { } }); } catch (e) { - logger(`[cyclist] error: ${e.message}`); + errorLogger(e); onError?.(e); } }, diff --git a/packages/core/euclid.mjs b/packages/core/euclid.mjs index 25e12b07c..44ab07f11 100644 --- a/packages/core/euclid.mjs +++ b/packages/core/euclid.mjs @@ -10,7 +10,7 @@ https://rohandrape.net/?t=hmt This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ -import { timeCat, register, silence } from './pattern.mjs'; +import { timeCat, register, silence, stack, pure, _morph } from './pattern.mjs'; import { rotate, flatten, splitAt, zipWith } from './util.mjs'; import Fraction, { lcm } from './fraction.mjs'; @@ -196,3 +196,26 @@ export const euclidLegato = register(['euclidLegato'], function (pulses, steps, export const euclidLegatoRot = register(['euclidLegatoRot'], function (pulses, steps, rotation, pat) { return _euclidLegato(pulses, steps, rotation, pat); }); + +/** + * A 'euclid' variant with an additional parameter that morphs the resulting + * rhythm from 0 (no morphing) to 1 (completely 'even'). For example + * `sound("bd").euclidish(3,8,0)` would be the same as + * `sound("bd").euclid(3,8)`, and `sound("bd").euclidish(3,8,1)` would be the + * same as `sound("bd bd bd")`. `sound("bd").euclidish(3,8,0.5)` would have a + * groove somewhere between. + * Inspired by the work of Malcom Braff. + * @name euclidish + * @synonyms eish + * @memberof Pattern + * @param {number} pulses the number of onsets + * @param {number} steps the number of steps to fill + * @param {number} groove exists between the extremes of 0 (straight euclidian) and 1 (straight pulse) + * @example + * sound("hh").euclidish(7,12,sine.slow(8)) + * .pan(sine.slow(8)) + */ +export const { euclidish, eish } = register(['euclidish', 'eish'], function (pulses, steps, perc, pat) { + const morphed = _morph(bjork(pulses, steps), new Array(pulses).fill(1), perc); + return pat.struct(morphed).setSteps(steps); +}); diff --git a/packages/core/fraction.mjs b/packages/core/fraction.mjs index 2e3bc68ea..076fbadf6 100644 --- a/packages/core/fraction.mjs +++ b/packages/core/fraction.mjs @@ -126,6 +126,8 @@ export const lcm = (...fractions) => { ); }; +export const isFraction = (x) => x instanceof Fraction; + fraction._original = Fraction; export default fraction; diff --git a/packages/core/hap.mjs b/packages/core/hap.mjs index a6e3c55ad..5f820d644 100644 --- a/packages/core/hap.mjs +++ b/packages/core/hap.mjs @@ -4,6 +4,7 @@ Copyright (C) 2022 Strudel contributors - see . */ import Fraction from './fraction.mjs'; +import { stringifyValues } from './util.mjs'; export class Hap { /* @@ -148,13 +149,7 @@ export class Hap { } showWhole(compact = false) { - return `${this.whole == undefined ? '~' : this.whole.show()}: ${ - typeof this.value === 'object' - ? compact - ? JSON.stringify(this.value).slice(1, -1).replaceAll('"', '').replaceAll(',', ' ') - : JSON.stringify(this.value) - : this.value - }`; + return `${this.whole == undefined ? '~' : this.whole.show()}: ${stringifyValues(this.value, compact)}`; } combineContext(b) { diff --git a/packages/core/logger.mjs b/packages/core/logger.mjs index e13bf86ca..6727c2422 100644 --- a/packages/core/logger.mjs +++ b/packages/core/logger.mjs @@ -4,6 +4,13 @@ let debounce = 1000, lastMessage, lastTime; +export function errorLogger(e, origin = 'cyclist') { + if (process.env.NODE_ENV === 'development') { + console.error(e); + } + logger(`[${origin}] error: ${e.message}`); +} + export function logger(message, type, data = {}) { let t = performance.now(); if (lastMessage === message && t - lastTime < debounce) { diff --git a/packages/core/neocyclist.mjs b/packages/core/neocyclist.mjs index 5c175dc1f..3e412074d 100644 --- a/packages/core/neocyclist.mjs +++ b/packages/core/neocyclist.mjs @@ -11,7 +11,6 @@ export class NeoCyclist { constructor({ onTrigger, onToggle, getTime }) { this.started = false; this.cps = 0.5; - this.lastTick = 0; // absolute time when last tick (clock callback) happened this.getTime = getTime; // get absolute time this.time_at_last_tick_message = 0; // the clock of the worker and the audio context clock can drift apart over time @@ -39,8 +38,7 @@ export class NeoCyclist { if (this.started === false) { return; } - - const haps = this.pattern.queryArc(begin, end, { _cps: this.cps }); + const haps = this.pattern.queryArc(begin, end, { _cps: this.cps, cyclist: 'neocyclist' }); haps.forEach((hap) => { if (hap.hasOnset()) { const timeUntilTrigger = cycleToSeconds(hap.whole.begin - this.cycle, this.cps); diff --git a/packages/core/package.json b/packages/core/package.json index f4170f2b5..7cf20cea7 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/core", - "version": "1.2.2", + "version": "1.2.4", "description": "Port of Tidal Cycles to JavaScript", "main": "index.mjs", "type": "module", diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index cbaf8a8a2..a85226a8f 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -5,7 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th */ import TimeSpan from './timespan.mjs'; -import Fraction, { lcm } from './fraction.mjs'; +import Fraction, { isFraction, lcm } from './fraction.mjs'; import Hap from './hap.mjs'; import State from './state.mjs'; import { unionWithObj } from './value.mjs'; @@ -21,6 +21,8 @@ import { numeralArgs, parseNumeral, pairs, + zipWith, + stringifyValues, } from './util.mjs'; import drawLine from './drawLine.mjs'; import { logger } from './logger.mjs'; @@ -96,10 +98,7 @@ export class Pattern { // runs func on query state withState(func) { - return this.withHaps((haps, state) => { - func(state); - return haps; - }); + return new Pattern((state) => this.query(func(state))); } /** @@ -852,14 +851,29 @@ export class Pattern { ); } - log(func = (_, hap) => `[hap] ${hap.showWhole(true)}`, getData = (_, hap) => ({ hap })) { + /** + * Writes the content of the current event to the console (visible in the side menu). + * @name log + * @memberof Pattern + * @example + * s("bd sd").log() + */ + log(func = (hap) => `[hap] ${hap.showWhole(true)}`, getData = (hap) => ({ hap })) { return this.onTrigger((...args) => { logger(func(...args), undefined, getData(...args)); }, false); } - logValues(func = id) { - return this.log((_, hap) => func(hap.value)); + /** + * A simplified version of `log` which writes all "values" (various configurable parameters) + * within the event to the console (visible in the side menu). + * @name logValues + * @memberof Pattern + * @example + * s("bd sd").gain("0.25 0.5 1").n("2 1 0").logValues() + */ + logValues(func = (value) => `[hap] ${stringifyValues(value, true)}`) { + return this.log((hap) => func(hap.value)); } ////////////////////////////////////////////////////////////////////// @@ -982,7 +996,7 @@ addToPrototype('weaveWith', function (t, ...funcs) { // compose matrix functions function _nonArrayObject(x) { - return !Array.isArray(x) && typeof x === 'object'; + return !Array.isArray(x) && typeof x === 'object' && !isFraction(x); } function _composeOp(a, b, func) { if (_nonArrayObject(a) || _nonArrayObject(b)) { @@ -1229,7 +1243,8 @@ export const silence = gap(1); /* Like silence, but with a 'steps' (relative duration) of 0 */ export const nothing = gap(0); -/** A discrete value that repeats once per cycle. +/** + * A discrete value that repeats once per cycle. * * @returns {Pattern} * @example @@ -1282,13 +1297,14 @@ export function sequenceP(pats) { return result; } -/** The given items are played at the same time at the same length. +/** + * The given items are played at the same time at the same length. * * @return {Pattern} * @synonyms polyrhythm, pr * @example * stack("g3", "b3", ["e4", "d4"]).note() - * // "g3,b3,[e4,d4]".note() + * // "g3,b3,[e4 d4]".note() * * @example * // As a chained function: @@ -1365,11 +1381,11 @@ export function stackBy(by, ...pats) { .setSteps(steps); } -/** Concatenation: combines a list of patterns, switching between them successively, one per cycle: - * - * synonyms: `cat` +/** + * Concatenation: combines a list of patterns, switching between them successively, one per cycle. * * @return {Pattern} + * @synonyms cat * @example * slowcat("e5", "b4", ["d5", "c5"]) * @@ -1569,7 +1585,7 @@ export const func = curry((a, b) => reify(b).func(a)); /** * Registers a new pattern method. The method is added to the Pattern class + the standalone function is returned from register. * - * @param {string} name name of the function + * @param {string | string[]} name name of the function, or an array of names to be used as synonyms * @param {function} func function with 1 or more params, where last is the current pattern * @noAutocomplete * @@ -2380,6 +2396,57 @@ export const stut = register('stut', function (times, feedback, time, pat) { return pat._echoWith(times, time, (pat, i) => pat.gain(Math.pow(feedback, i))); }); +export const applyN = register('applyN', function (n, func, p) { + let result = p; + for (let i = 0; i < n; i++) { + result = func(result); + } + return result; +}); + +/** + * The plyWith function repeats each event the given number of times, applying the given function to each event.\n + * @name plyWith + * @synonyms plywith + * @param {number} factor how many times to repeat + * @param {function} func function to apply, given the pattern + * @example + * "<0 [2 4]>" + * .plyWith(4, (p) => p.add(2)) + * .scale("C:minor").note() + */ +export const plyWith = register(['plyWith', 'plywith'], function (factor, func, pat) { + const result = pat + .fmap((x) => cat(...listRange(0, factor - 1).map((i) => applyN(i, func, x)))._fast(factor)) + .squeezeJoin(); + if (__steps) { + result._steps = Fraction(factor).mulmaybe(pat._steps); + } + return result; +}); + +/** + * The plyForEach function repeats each event the given number of times, applying the given function to each event. + * This version of ply uses the iteration index as an argument to the function, similar to echoWith. + * @name plyForEach + * @synonyms plyforeach + * @param {number} factor how many times to repeat + * @param {function} func function to apply, given the pattern and the iteration index + * @example + * "<0 [2 4]>" + * .plyForEach(4, (p,n) => p.add(n*2)) + * .scale("C:minor").note() + */ +export const plyForEach = register(['plyForEach', 'plyforeach'], function (factor, func, pat) { + const result = pat + .fmap((x) => cat(cat(pure(x), ...listRange(1, factor - 1).map((i) => func(pure(x), i))))._fast(factor)) + .squeezeJoin(); + if (__steps) { + result._steps = Fraction(factor).mulmaybe(pat._steps); + } + return result; +}); + /** * Divides a pattern into a given number of subdivisions, plays the subdivisions in order, but increments the starting subdivision each cycle. The pattern wraps to the first subdivision after the last subdivision is played. * @name iter @@ -2522,7 +2589,7 @@ export const { fastchunk, fastChunk } = register( /** * Like `chunk`, but the function is applied to a looped subcycle of the source pattern. * @name chunkInto - * @synonym chunkinto + * @synonyms chunkinto * @memberof Pattern * @example * sound("bd sd ht lt bd - cp lt").chunkInto(4, hurry(2)) @@ -2535,7 +2602,7 @@ export const { chunkinto, chunkInto } = register(['chunkinto', 'chunkInto'], fun /** * Like `chunkInto`, but moves backwards through the chunks. * @name chunkBackInto - * @synonym chunkbackinto + * @synonyms chunkbackinto * @memberof Pattern * @example * sound("bd sd ht lt bd - cp lt").chunkInto(4, hurry(2)) @@ -2565,7 +2632,7 @@ export const bypass = register( * Loops the pattern inside an `offset` for `cycles`. * If you think of the entire span of time in cycles as a ribbon, you can cut a single piece and loop it. * @name ribbon - * @synonym rib + * @synonyms rib * @param {number} offset start point of loop in cycles * @param {number} cycles loop length in cycles * @example @@ -3260,10 +3327,10 @@ export const slice = register( * @memberof Pattern * @returns Pattern * @example - * s("bd!8").onTriggerTime((hap) => {console.info(hap)}) + * s("bd!8").onTriggerTime((hap) => {console.log(hap)}) */ Pattern.prototype.onTriggerTime = function (func) { - return this.onTrigger((t_deprecate, hap, currentTime, cps = 1, targetTime) => { + return this.onTrigger((hap, currentTime, _cps, targetTime) => { const diff = targetTime - currentTime; window.setTimeout(() => { func(hap); @@ -3400,3 +3467,142 @@ export const { beat } = register( ['beat'], __beat((x) => x.innerJoin()), ); + +export const _morph = (from, to, by) => { + by = Fraction(by); + const dur = Fraction(1).div(from.length); + const positions = (list) => { + const result = []; + for (const [pos, value] of list.entries()) { + if (value) { + result.push([Fraction(pos).div(list.length), value]); + } + } + return result; + }; + const arcs = zipWith( + ([posa, valuea], [posb, valueb]) => { + const b = by.mul(posb - posa).add(posa); + const e = b.add(dur); + return new TimeSpan(b, e); + }, + positions(from), + positions(to), + ); + function query(state) { + const cycle = state.span.begin.sam(); + const cycleArc = state.span.cycleArc(); + const result = []; + for (const whole of arcs) { + const part = whole.intersection(cycleArc); + if (part !== undefined) { + result.push( + new Hap( + whole.withTime((x) => x.add(cycle)), + part.withTime((x) => x.add(cycle)), + true, + ), + ); + } + } + return result; + } + return new Pattern(query).splitQueries(); +}; + +/** + * Takes two binary rhythms represented as lists of 1s and 0s, and a number + * between 0 and 1 that morphs between them. The two lists should contain the same + * number of true values. + * @example + * sound("hh").struct(morph([1,0,1,0,1,0,1,0], // straight rhythm + * [1,1,0,1,0,1,0], // wonky rhythm + * 0.25 // creates a slightly wonky rhythm + * ) + * ) + * @example + * sound("hh").struct(morph("1:0:1:0:1:0:1:0", // straight rhythm + * "1:1:0:1:0:1:0", // wonky rhythm + * sine.slow(8) // slowly morph between the rhythms + * ) + * ) + */ +export const morph = (frompat, topat, bypat) => { + frompat = reify(frompat); + topat = reify(topat); + bypat = reify(bypat); + return frompat.innerBind((from) => topat.innerBind((to) => bypat.innerBind((by) => _morph(from, to, by)))); +}; + +/** + * Soft-clipping distortion + * + * @name soft + * @param {number | Pattern} distortion amount of distortion to apply + * @param {number | Pattern} volume linear postgain of the distortion + * + */ +/** + * Hard-clipping distortion + * + * @name hard + * @param {number | Pattern} distortion amount of distortion to apply + * @param {number | Pattern} volume linear postgain of the distortion + * + */ +/** + * Cubic polynomial distortion + * + * @name cubic + * @param {number | Pattern} distortion amount of distortion to apply + * @param {number | Pattern} volume linear postgain of the distortion + * + */ +/** + * Diode-emulating distortion + * + * @name diode + * @param {number | Pattern} distortion amount of distortion to apply + * @param {number | Pattern} volume linear postgain of the distortion + * + */ +/** + * Asymmetrical diode distortion + * + * @name asym + * @param {number | Pattern} distortion amount of distortion to apply + * @param {number | Pattern} volume linear postgain of the distortion + * + */ +/** + * Wavefolding distortion + * + * @name fold + * @param {number | Pattern} distortion amount of distortion to apply + * @param {number | Pattern} volume linear postgain of the distortion + * + */ +/** + * Wavefolding distortion composed with sinusoid + * + * @name sinefold + * @param {number | Pattern} distortion amount of distortion to apply + * @param {number | Pattern} volume linear postgain of the distortion + * + */ +/** + * Distortion via Chebyshev polynomials + * + * @name chebyshev + * @param {number | Pattern} distortion amount of distortion to apply + * @param {number | Pattern} volume linear postgain of the distortion + * + */ +const distAlgoNames = ['scurve', 'soft', 'hard', 'cubic', 'diode', 'asym', 'fold', 'sinefold', 'chebyshev']; +for (const name of distAlgoNames) { + // Add aliases for distortion algorithms + Pattern.prototype[name] = function (args) { + const argsPat = reify(args).fmap((v) => (Array.isArray(v) ? [...v, name] : [v, 1, name])); + return this.distort(argsPat); + }; +} diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index dcc5fcba4..171697eb9 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -1,7 +1,7 @@ import { NeoCyclist } from './neocyclist.mjs'; import { Cyclist } from './cyclist.mjs'; import { evaluate as _evaluate } from './evaluate.mjs'; -import { logger } from './logger.mjs'; +import { errorLogger, logger } from './logger.mjs'; import { setTime } from './time.mjs'; import { evalScope } from './evaluate.mjs'; import { register, Pattern, isPattern, silence, stack } from './pattern.mjs'; @@ -74,6 +74,14 @@ export function repl({ return silence; }; + // helper to get a patternified pure value out + function unpure(pat) { + if (pat._Pattern) { + return pat.__pure; + } + return pat; + } + const setPattern = async (pattern, autostart = true) => { pattern = editPattern?.(pattern) || pattern; await scheduler.setPattern(pattern, autostart); @@ -85,7 +93,10 @@ export function repl({ const start = () => scheduler.start(); const pause = () => scheduler.pause(); const toggle = () => scheduler.toggle(); - const setCps = (cps) => scheduler.setCps(cps); + const setCps = (cps) => { + scheduler.setCps(unpure(cps)); + return silence; + }; /** * Changes the global tempo to the given cycles per minute @@ -97,7 +108,10 @@ export function repl({ * setcpm(140/4) // =140 bpm in 4/4 * $: s("bd*4,[- sd]*2").bank('tr707') */ - const setCpm = (cpm) => scheduler.setCps(cpm / 60); + const setCpm = (cpm) => { + scheduler.setCps(unpure(cpm) / 60); + return silence; + }; // TODO - not documented as jsdoc examples as the test framework doesn't simulate enough context for `each` and `all`.. @@ -200,7 +214,10 @@ export function repl({ } let { pattern, meta } = await _evaluate(code, transpiler, transpilerOptions); if (Object.keys(pPatterns).length) { - let patterns = Object.values(pPatterns); + let patterns = []; + for (const [key, value] of Object.entries(pPatterns)) { + patterns.push(value.withState((state) => state.setControls({ id: key }))); + } if (eachTransform) { // Explicit lambda so only element (not index and array) are passed patterns = patterns.map((x) => eachTransform(x)); @@ -214,6 +231,7 @@ export function repl({ pattern = allTransforms[i](pattern); } } + if (!isPattern(pattern)) { const message = `got "${typeof evaluated}" instead of pattern`; throw new Error(message + (typeof evaluated === 'function' ? ', did you forget to call a function?' : '.')); @@ -245,6 +263,7 @@ export function repl({ export const getTrigger = ({ getTime, defaultOutput }) => async (hap, deadline, duration, cps, t) => { + // ^ this signature is different from hap.context.onTrigger, as set by Pattern.onTrigger(onTrigger) // TODO: get rid of deadline after https://codeberg.org/uzu/strudel/pulls/1004 try { if (!hap.context.onTrigger || !hap.context.dominantTrigger) { @@ -252,9 +271,9 @@ export const getTrigger = } if (hap.context.onTrigger) { // call signature of output / onTrigger is different... - await hap.context.onTrigger(getTime() + deadline, hap, getTime(), cps, t); + await hap.context.onTrigger(hap, getTime(), cps, t); } } catch (err) { - logger(`[cyclist] error: ${err.message}`, 'error'); + errorLogger(err, 'getTrigger'); } }; diff --git a/packages/core/signal.mjs b/packages/core/signal.mjs index c933e7fe2..6bd6fde67 100644 --- a/packages/core/signal.mjs +++ b/packages/core/signal.mjs @@ -264,7 +264,7 @@ export const randrun = (n) => { const rands = timeToRands(t.floor().add(0.5), n); const nums = rands .map((n, i) => [n, i]) - .sort((a, b) => a[0] > b[0] - a[0] < b[0]) + .sort((a, b) => (a[0] > b[0]) - (a[0] < b[0])) .map((x) => x[1]); const i = t.cyclePos().mul(n).floor() % n; return nums[i]; diff --git a/packages/core/speak.mjs b/packages/core/speak.mjs index 7e548a73b..e0f60184e 100644 --- a/packages/core/speak.mjs +++ b/packages/core/speak.mjs @@ -32,7 +32,7 @@ function triggerSpeech(words, lang, voice) { } export const speak = register('speak', function (lang, voice, pat) { - return pat.onTrigger((_, hap) => { + return pat.onTrigger((hap) => { triggerSpeech(hap.value, lang, voice); }); }); diff --git a/packages/core/state.mjs b/packages/core/state.mjs index 162dc7da9..8aa581954 100644 --- a/packages/core/state.mjs +++ b/packages/core/state.mjs @@ -19,9 +19,9 @@ export class State { return this.setSpan(func(this.span)); } - // Returns new State with different controls + // Returns new State with added controls. setControls(controls) { - return new State(this.span, controls); + return new State(this.span, { ...this.controls, ...controls }); } } diff --git a/packages/core/test/pattern.test.mjs b/packages/core/test/pattern.test.mjs index 93c4168c9..1df5c8776 100644 --- a/packages/core/test/pattern.test.mjs +++ b/packages/core/test/pattern.test.mjs @@ -6,7 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th import Fraction from 'fraction.js'; -import { describe, it, expect } from 'vitest'; +import { describe, it, expect, vi } from 'vitest'; import { TimeSpan, @@ -55,6 +55,8 @@ import { expand, } from '../index.mjs'; +import { log, logValues } from '../pattern.mjs'; + import { steady } from '../signal.mjs'; import { n, s } from '../controls.mjs'; @@ -1306,4 +1308,40 @@ describe('Pattern', () => { ); }); }); + describe('log', () => { + it('logs to console', () => { + const mockConsoleLog = vi.spyOn(console, 'log').mockImplementation(() => {}); + const pattern = pure('a').log(); + const haps = pattern.queryArc(0, 1); + + // Force a trigger + haps.forEach((hap) => { + hap.context?.onTrigger?.(hap); + }); + + expect(mockConsoleLog).toHaveBeenCalledWith( + '%c[hap] 0/1 → 1/1: a', + 'background-color: black;color:white;border-radius:15px', + ); + mockConsoleLog.mockRestore(); + }); + }); + describe('logValues', () => { + it('logs values to console', () => { + const mockConsoleLog = vi.spyOn(console, 'log').mockImplementation(() => {}); + const pattern = pure('a').note('c#').logValues(); + const haps = pattern.queryArc(0, 1); + + // Force a trigger + haps.forEach((hap) => { + hap.context?.onTrigger?.(hap); + }); + + expect(mockConsoleLog).toHaveBeenCalledWith( + '%c[hap] value:a note:c#', + 'background-color: black;color:white;border-radius:15px', + ); + mockConsoleLog.mockRestore(); + }); + }); }); diff --git a/packages/core/timespan.mjs b/packages/core/timespan.mjs index 0dbc74fc8..446156bbf 100644 --- a/packages/core/timespan.mjs +++ b/packages/core/timespan.mjs @@ -72,7 +72,7 @@ export class TimeSpan { } intersection(other) { - // Intersection of two timespans, returns None if they don't intersect. + // Intersection of two timespans, returns undefined if they don't intersect. const intersect_begin = this.begin.max(other.begin); const intersect_end = this.end.min(other.end); diff --git a/packages/core/util.mjs b/packages/core/util.mjs index 756fac8e8..ef3f1e961 100644 --- a/packages/core/util.mjs +++ b/packages/core/util.mjs @@ -8,12 +8,12 @@ import { logger } from './logger.mjs'; // returns true if the given string is a note export const isNoteWithOctave = (name) => /^[a-gA-G][#bs]*[0-9]$/.test(name); -export const isNote = (name) => /^[a-gA-G][#bsf]*[0-9]?$/.test(name); +export const isNote = (name) => /^[a-gA-G][#bsf]*-?[0-9]?$/.test(name); export const tokenizeNote = (note) => { if (typeof note !== 'string') { return []; } - const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bsf]*)([0-9]*)$/)?.slice(1) || []; + const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bsf]*)(-?[0-9]*)$/)?.slice(1) || []; if (!pc) { return []; } @@ -487,3 +487,13 @@ export function getCurrentKeyboardState() { // } // return lcm((x * y) / gcd(x, y), ...z); // }; + +// Takes values -- typically derived from events, i.e. `hap`s -- and renders them +// into a readable format +export function stringifyValues(value, compact = false) { + return typeof value === 'object' + ? compact + ? JSON.stringify(value).slice(1, -1).replaceAll('"', '').replaceAll(',', ' ') + : JSON.stringify(value) + : value; +} diff --git a/packages/csound/index.mjs b/packages/csound/index.mjs index 1eeaf6fa5..e44765227 100644 --- a/packages/csound/index.mjs +++ b/packages/csound/index.mjs @@ -23,7 +23,7 @@ export const csound = register('csound', (instrument, pat) => { instrument = instrument || 'triangle'; init(); // not async to support csound inside other patterns + to be able to call pattern methods after it // TODO: find a alternative way to wait for csound to load (to wait with first time playback) - return pat.onTrigger((time_deprecate, hap, currentTime, _cps, targetTime) => { + return pat.onTrigger((hap, currentTime, _cps, targetTime) => { if (!_csound) { logger('[csound] not loaded yet', 'warning'); return; @@ -142,7 +142,7 @@ export const csoundm = register('csoundm', (instrument, pat) => { p1 = `"${instrument}"`; } init(); // not async to support csound inside other patterns + to be able to call pattern methods after it - return pat.onTrigger((tidal_time, hap) => { + return pat.onTrigger((hap, currentTime, _cps, targetTime) => { if (!_csound) { logger('[csound] not loaded yet', 'warning'); return; @@ -151,7 +151,7 @@ export const csoundm = register('csoundm', (instrument, pat) => { throw new Error('csound only support objects as hap values'); } // Time in seconds counting from now. - const p2 = tidal_time - getAudioContext().currentTime; + const p2 = targetTime - currentTime; const p3 = hap.duration.valueOf() + 0; const frequency = getFrequency(hap); let { gain = 1, velocity = 0.9 } = hap.value; diff --git a/packages/csound/package.json b/packages/csound/package.json index 04a5ff246..90130a101 100644 --- a/packages/csound/package.json +++ b/packages/csound/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/csound", - "version": "1.2.3", + "version": "1.2.5", "description": "csound bindings for strudel", "main": "index.mjs", "type": "module", diff --git a/packages/desktopbridge/midibridge.mjs b/packages/desktopbridge/midibridge.mjs index 471c826ea..2436d8ad7 100644 --- a/packages/desktopbridge/midibridge.mjs +++ b/packages/desktopbridge/midibridge.mjs @@ -6,7 +6,7 @@ const OFF_MESSAGE = 0x80; const CC_MESSAGE = 0xb0; Pattern.prototype.midi = function (output) { - return this.onTrigger((time_deprecate, hap, currentTime, cps, targetTime) => { + return this.onTrigger((hap, currentTime, cps, targetTime) => { let { note, nrpnn, nrpv, ccn, ccv, velocity = 0.9, gain = 1 } = hap.value; //magic number to get audio engine to line up, can probably be calculated somehow const latencyMs = 34; diff --git a/packages/desktopbridge/oscbridge.mjs b/packages/desktopbridge/oscbridge.mjs index 9bead6d19..2568c78e5 100644 --- a/packages/desktopbridge/oscbridge.mjs +++ b/packages/desktopbridge/oscbridge.mjs @@ -4,7 +4,7 @@ import { Invoke } from './utils.mjs'; const collator = new ClockCollator({}); -export async function oscTriggerTauri(t_deprecate, hap, currentTime, cps = 1, targetTime) { +export async function oscTriggerTauri(hap, currentTime, cps = 1, targetTime) { const controls = parseControlsFromHap(hap, cps); const params = []; const timestamp = collator.calculateTimestamp(currentTime, targetTime); diff --git a/packages/draw/package.json b/packages/draw/package.json index ee1b8dd00..6a4c57540 100644 --- a/packages/draw/package.json +++ b/packages/draw/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/draw", - "version": "1.2.2", + "version": "1.2.4", "description": "Helpers for drawing with Strudel", "main": "index.mjs", "type": "module", diff --git a/packages/embed/package.json b/packages/embed/package.json index a0cc33de1..6a00ee904 100644 --- a/packages/embed/package.json +++ b/packages/embed/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/embed", - "version": "1.1.0", + "version": "1.1.1", "description": "Embeddable Web Component to load a Strudel REPL into an iframe", "main": "embed.js", "type": "module", diff --git a/packages/gamepad/package.json b/packages/gamepad/package.json index 3efb2e084..555eac03f 100644 --- a/packages/gamepad/package.json +++ b/packages/gamepad/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/gamepad", - "version": "1.2.2", + "version": "1.2.4", "description": "Gamepad Inputs for strudel", "main": "index.mjs", "type": "module", diff --git a/packages/hydra/package.json b/packages/hydra/package.json index b022de87d..7ba79d5d0 100644 --- a/packages/hydra/package.json +++ b/packages/hydra/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/hydra", - "version": "1.2.2", + "version": "1.2.4", "description": "Hydra integration for strudel", "main": "hydra.mjs", "type": "module", diff --git a/packages/midi/midi.mjs b/packages/midi/midi.mjs index af2dd3a62..ae983d200 100644 --- a/packages/midi/midi.mjs +++ b/packages/midi/midi.mjs @@ -333,7 +333,7 @@ Pattern.prototype.midi = function (midiport, options = {}) { logger(`Midi device disconnected! Available: ${getMidiDeviceNamesString(outputs)}`), }); - return this.onTrigger((time_deprecate, hap, currentTime, cps, targetTime) => { + return this.onTrigger((hap, currentTime, cps, targetTime) => { if (!WebMidi.enabled) { logger('Midi not enabled'); return; @@ -493,6 +493,9 @@ export async function midin(input) { otherInputs?.length ? `Also available: ${getMidiDeviceNamesString(otherInputs)}` : '' }`, ); + } + // ensure refs for this input are initialized + if (!refs[input]) { refs[input] = {}; } const cc = (cc) => ref(() => refs[input][cc] || 0); diff --git a/packages/midi/package.json b/packages/midi/package.json index 4efd329d8..2342cf7e9 100644 --- a/packages/midi/package.json +++ b/packages/midi/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/midi", - "version": "1.2.3", + "version": "1.2.5", "description": "Midi API for strudel", "main": "index.mjs", "type": "module", diff --git a/packages/mini/bench/mini.bench.mjs b/packages/mini/bench/mini.bench.mjs index 782ac86ba..e7471bf22 100644 --- a/packages/mini/bench/mini.bench.mjs +++ b/packages/mini/bench/mini.bench.mjs @@ -1,10 +1,10 @@ import { describe, bench } from 'vitest'; -import { calculateTactus } from '../../core/index.mjs'; +import { calculateSteps } from '../../core/index.mjs'; import { mini } from '../index.mjs'; describe('mini', () => { - calculateTactus(true); + calculateSteps(true); bench( '+tactus', () => { @@ -13,7 +13,7 @@ describe('mini', () => { { time: 1000 }, ); - calculateTactus(false); + calculateSteps(false); bench( '-tactus', () => { @@ -21,5 +21,5 @@ describe('mini', () => { }, { time: 1000 }, ); - calculateTactus(true); + calculateSteps(true); }); diff --git a/packages/mini/package.json b/packages/mini/package.json index 5d94301d4..6eeaab0da 100644 --- a/packages/mini/package.json +++ b/packages/mini/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/mini", - "version": "1.2.2", + "version": "1.2.4", "description": "Mini notation for strudel", "main": "index.mjs", "type": "module", diff --git a/packages/mondo/package.json b/packages/mondo/package.json index 277bddb1c..a59bfc344 100644 --- a/packages/mondo/package.json +++ b/packages/mondo/package.json @@ -1,6 +1,6 @@ { "name": "mondolang", - "version": "1.1.0", + "version": "1.1.1", "description": "a language for functional composition that translates to js", "main": "mondo.mjs", "type": "module", diff --git a/packages/mondough/mondough.mjs b/packages/mondough/mondough.mjs index 6e8278939..b7ee83787 100644 --- a/packages/mondough/mondough.mjs +++ b/packages/mondough/mondough.mjs @@ -42,6 +42,7 @@ lib['%'] = pace; lib['?'] = degradeBy; // todo: default 0.5 not working.. lib[':'] = tail; lib['..'] = range; +lib['def'] = () => silence; lib['or'] = (...children) => chooseIn(...children); // always has structure but is cyclewise.. e.g. "s oh*8.dec[.04 | .5]" //lib['or'] = (...children) => chooseOut(...children); // "s oh*8.dec[.04 | .5]" is better but "dec[.04 | .5].s oh*8" has no struct @@ -84,7 +85,7 @@ function evaluator(node, scope) { let pat; if (type === 'plain' && typeof variable !== 'undefined') { // some function names are not patternable, so we skip reification here - if (['!', 'extend', '@', 'expand', 'square', 'angle'].includes(value)) { + if (['!', 'extend', '@', 'expand', 'square', 'angle', 'all', 'setcpm', 'setcps'].includes(value)) { return variable; } pat = reify(variable); @@ -107,7 +108,7 @@ export function mondo(code, offset = 0) { return pat.markcss('color: var(--caret,--foreground);text-decoration:underline'); } -let getLocations = (code, offset) => runner.parser.get_locations(code, offset); +export let getLocations = (code, offset) => runner.parser.get_locations(code, offset); export const mondi = (str, offset) => { const code = `[${str}]`; diff --git a/packages/mondough/package.json b/packages/mondough/package.json index a034424c0..c81d76cb6 100644 --- a/packages/mondough/package.json +++ b/packages/mondough/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/mondo", - "version": "1.1.0", + "version": "1.1.4", "description": "mondo notation for strudel", "main": "mondough.mjs", "type": "module", diff --git a/packages/mondough/vite.config.js b/packages/mondough/vite.config.js index c46972e96..06c8c4559 100644 --- a/packages/mondough/vite.config.js +++ b/packages/mondough/vite.config.js @@ -1,5 +1,5 @@ import { defineConfig } from 'vite'; -//import { dependencies } from './package.json'; +import { dependencies } from './package.json'; import { resolve } from 'path'; // https://vitejs.dev/config/ @@ -12,7 +12,7 @@ export default defineConfig({ fileName: (ext) => ({ es: 'mondough.mjs' })[ext], }, rollupOptions: { - // external: [...Object.keys(dependencies)], + external: [...Object.keys(dependencies)], }, target: 'esnext', }, diff --git a/packages/motion/package.json b/packages/motion/package.json index a7db05680..57cac9cc8 100644 --- a/packages/motion/package.json +++ b/packages/motion/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/motion", - "version": "1.2.2", + "version": "1.2.4", "description": "DeviceMotion API for strudel", "main": "index.mjs", "type": "module", diff --git a/packages/mqtt/mqtt.mjs b/packages/mqtt/mqtt.mjs index aef01bd93..d74de342d 100644 --- a/packages/mqtt/mqtt.mjs +++ b/packages/mqtt/mqtt.mjs @@ -82,7 +82,7 @@ Pattern.prototype.mqtt = function ( cx.connect(props); } return this.withHap((hap) => { - const onTrigger = (t_deprecate, hap, currentTime, cps, targetTime) => { + const onTrigger = (hap, currentTime, cps, targetTime) => { let msg_topic = topic; if (!cx || !cx.isConnected()) { return; diff --git a/packages/mqtt/package.json b/packages/mqtt/package.json index f522e3354..2e32825fe 100644 --- a/packages/mqtt/package.json +++ b/packages/mqtt/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/mqtt", - "version": "1.2.2", + "version": "1.2.4", "description": "MQTT API for strudel", "main": "mqtt.mjs", "type": "module", diff --git a/packages/osc/README.md b/packages/osc/README.md index 3dc65e16e..0a7bfedcf 100644 --- a/packages/osc/README.md +++ b/packages/osc/README.md @@ -4,21 +4,13 @@ OSC output for strudel patterns! Currently only tested with super collider / sup ## Usage -OSC will only work if you run the REPL locally + the OSC server besides it: +Assuming you have [node.js](https://nodejs.org/) installed, you can run the osc bridge server via: -From the project root: - -```js -npm run repl +```sh +npx @strudel/osc ``` -and in a seperate shell: - -```js -npm run osc -``` - -This should give you +You should see something like: ```log osc client running on port 57120 @@ -26,14 +18,32 @@ osc server running on port 57121 websocket server running on port 8080 ``` -Now open Supercollider (with the super dirt startup file) +### --port -Now open the REPL and type: +By default it will use port 57120 for the osc client, which is what [superdirt](https://github.com/musikinformatik/SuperDirt) uses. You can change it via the `--port` option: -```js -s(" hh").osc() +```sh +npx @strudel/osc --port 7771 # classic dirt ``` -or just [click here](https://strudel.cc/#cygiPGJkIHNkPiBoaCIpLm9zYygp)... +### --debug + +To log all incoming osc messages, add the `--debug` flag: + +```sh +npx @strudel/osc --debug +``` + +## Usage in Strudel + +To test it in strudel, you have can use `all(osc)` to send all events through osc: + +```js +$: s("bd*4") + +all(osc) +``` + +[open in repl](https://strudel.cc/#JDogcygiYmQqNCIpCgphbGwob3NjKQ%3D%3D) You can read more about [how to use Superdirt with Strudel the Tutorial](https://strudel.cc/learn/input-output/#superdirt-api) diff --git a/packages/osc/osc.mjs b/packages/osc/osc.mjs index ac70b9e73..fe0691522 100644 --- a/packages/osc/osc.mjs +++ b/packages/osc/osc.mjs @@ -6,7 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th import OSC from 'osc-js'; -import { logger, parseNumeral, Pattern, isNote, noteToMidi, ClockCollator } from '@strudel/core'; +import { logger, parseNumeral, register, isNote, noteToMidi, ClockCollator } from '@strudel/core'; let connection; // Promise function connect() { @@ -60,7 +60,7 @@ export function parseControlsFromHap(hap, cps) { const collator = new ClockCollator({}); -export async function oscTrigger(t_deprecate, hap, currentTime, cps = 1, targetTime) { +export async function oscTrigger(hap, currentTime, cps = 1, targetTime) { const osc = await connect(); const controls = parseControlsFromHap(hap, cps); const keyvals = Object.entries(controls).flat(); @@ -81,6 +81,4 @@ export async function oscTrigger(t_deprecate, hap, currentTime, cps = 1, targetT * @memberof Pattern * @returns Pattern */ -Pattern.prototype.osc = function () { - return this.onTrigger(oscTrigger); -}; +export const osc = register('osc', (pat) => pat.onTrigger(oscTrigger)); diff --git a/packages/osc/package.json b/packages/osc/package.json index 7d19fbbfc..bc828d798 100644 --- a/packages/osc/package.json +++ b/packages/osc/package.json @@ -1,8 +1,9 @@ { "name": "@strudel/osc", - "version": "1.2.2", + "version": "1.2.10", "description": "OSC messaging for strudel", "main": "osc.mjs", + "bin": "./server.js", "type": "module", "publishConfig": { "main": "dist/index.mjs" diff --git a/packages/osc/server.js b/packages/osc/server.js index 75fc5b1c0..d7ec21c4b 100644 --- a/packages/osc/server.js +++ b/packages/osc/server.js @@ -1,3 +1,5 @@ +#!/usr/bin/env node + /* server.js - Copyright (C) 2022 Strudel contributors - see @@ -6,6 +8,19 @@ This program is free software: you can redistribute it and/or modify it under th import OSC from 'osc-js'; +const args = process.argv.slice(2); +function getArgValue(flag) { + const i = args.indexOf(flag); + if (i !== -1) { + const nextIsFlag = args[i + 1]?.startsWith('--') ?? true; + if (nextIsFlag) return true; + return args[i + 1]; + } +} + +let udpClientPort = Number(getArgValue('--port')) || 57120; +let debug = Number(getArgValue('--debug')) || 0; + const config = { receiver: 'ws', // @param {string} Where messages sent via 'send' method will be delivered to, 'ws' for Websocket clients, 'udp' for udp client udpServer: { @@ -17,7 +32,7 @@ const config = { }, udpClient: { host: 'localhost', // @param {string} Hostname of udp client for messaging - port: 57120, // @param {number} Port of udp client for messaging + port: udpClientPort, // @param {number} Port of udp client for messaging }, wsServer: { host: 'localhost', // @param {string} Hostname of WebSocket server @@ -27,8 +42,34 @@ const config = { const osc = new OSC({ plugin: new OSC.BridgePlugin(config) }); -osc.open(); // start a WebSocket server on port 8080 +if (debug) { + osc.on('*', (message) => { + const { address, args } = message; + let str = ''; + for (let i = 0; i < args.length; i += 2) { + str += `${args[i]}: ${args[i + 1]} `; + } + console.log(`${address} ${str}`); + }); +} + +osc.on('error', (message) => { + if (message.toString().includes('EADDRINUSE')) { + console.log(`------ ERROR ------- +a server is already running on port 57121! to stop it: +1. run "lsof -ti :57121 | xargs kill -9" (macos / linux) +2. re-run the osc server +`); + } else { + console.log(message); + } +}); + +osc.open(); console.log('osc client running on port', config.udpClient.port); console.log('osc server running on port', config.udpServer.port); console.log('websocket server running on port', config.wsServer.port); +if (debug) { + console.log('debug logs enabled. incoming messages will appear below'); +} diff --git a/packages/osc/superdirtoutput.js b/packages/osc/superdirtoutput.js index 3f48e66bd..a317af8f1 100644 --- a/packages/osc/superdirtoutput.js +++ b/packages/osc/superdirtoutput.js @@ -1,10 +1,10 @@ -import { oscTriggerTauri } from '../desktopbridge/oscbridge.mjs'; -import { isTauri } from '../desktopbridge/utils.mjs'; +/* import { oscTriggerTauri } from '../desktopbridge/oscbridge.mjs'; +import { isTauri } from '../desktopbridge/utils.mjs'; */ import { oscTrigger } from './osc.mjs'; -const trigger = isTauri() ? oscTriggerTauri : oscTrigger; +const trigger = /* isTauri() ? oscTriggerTauri : */ oscTrigger; export const superdirtOutput = (hap, deadline, hapDuration, cps, targetTime) => { const currentTime = performance.now() / 1000; - return trigger(null, hap, currentTime, cps, targetTime); + return trigger(hap, currentTime, cps, targetTime); }; diff --git a/packages/reference/package.json b/packages/reference/package.json index 8dc966cc2..634057e4c 100644 --- a/packages/reference/package.json +++ b/packages/reference/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/reference", - "version": "1.2.0", + "version": "1.2.1", "description": "Headless reference of all strudel functions", "main": "index.mjs", "type": "module", diff --git a/packages/repl/package.json b/packages/repl/package.json index bfa404c75..41a165c34 100644 --- a/packages/repl/package.json +++ b/packages/repl/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/repl", - "version": "1.2.3", + "version": "1.2.6", "description": "Strudel REPL as a Web Component", "module": "index.mjs", "publishConfig": { diff --git a/packages/repl/prebake.mjs b/packages/repl/prebake.mjs index dd0023fb5..26d875c2b 100644 --- a/packages/repl/prebake.mjs +++ b/packages/repl/prebake.mjs @@ -20,10 +20,13 @@ export async function prebake() { // import('@strudel/osc'), ); // load samples - const ds = 'https://raw.githubusercontent.com/felixroos/dough-samples/main/'; + const ds = 'https://raw.githubusercontent.com/felixroos/dough-samples/main'; // TODO: move this onto the strudel repo - const ts = 'https://raw.githubusercontent.com/todepond/samples/main/'; + const ts = 'https://raw.githubusercontent.com/todepond/samples/main'; + + const tc = 'https://raw.githubusercontent.com/tidalcycles/uzu-drumkit/main'; + await Promise.all([ modulesLoading, registerSynthSounds(), @@ -36,9 +39,9 @@ export async function prebake() { samples(`${ds}/tidal-drum-machines.json`), samples(`${ds}/piano.json`), samples(`${ds}/Dirt-Samples.json`), - samples(`${ds}/EmuSP12.json`), samples(`${ds}/vcsl.json`), samples(`${ds}/mridangam.json`), + samples(`${tc}/strudel.json`), ]); aliasBank(`${ts}/tidal-drum-machines-alias.json`); diff --git a/packages/sampler/README.md b/packages/sampler/README.md index c495c2add..1142176b7 100644 --- a/packages/sampler/README.md +++ b/packages/sampler/README.md @@ -20,3 +20,13 @@ samples('http://localhost:5432') LOG=1 npx @strudel/sampler # adds logging PORT=5555 npx @strudel/sampler # changes port ``` + +## static json + +when running with `--json`, you will simply get the json logged back: + +```sh +npx --yes @strudel/sampler --json > strudel.json +``` + +this is useful if you want to create a sample pack from the current folder. \ No newline at end of file diff --git a/packages/sampler/package.json b/packages/sampler/package.json index b0f27f86a..2bf0607b1 100644 --- a/packages/sampler/package.json +++ b/packages/sampler/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/sampler", - "version": "0.2.0", + "version": "0.2.3", "description": "", "keywords": [ "tidalcycles", diff --git a/packages/sampler/sample-server.mjs b/packages/sampler/sample-server.mjs index d1e56108f..31b83f5a3 100644 --- a/packages/sampler/sample-server.mjs +++ b/packages/sampler/sample-server.mjs @@ -1,22 +1,20 @@ #!/usr/bin/env node import cowsay from 'cowsay'; -import { createReadStream, existsSync } from 'fs'; +import { createReadStream, existsSync, writeFileSync } from 'fs'; import { readdir } from 'fs/promises'; import http from 'http'; -import { join, sep } from 'path'; +import { join, resolve, sep } from 'path'; +import readline from 'readline'; import os from 'os'; -// eslint-disable-next-line const LOG = !!process.env.LOG || false; +const VALID_AUDIO_EXTENSIONS = ['wav', 'mp3', 'ogg']; -console.log( - cowsay.say({ - text: 'welcome to @strudel/sampler', - e: 'oO', - T: 'U ', - }), -); +const isAudioFile = (f) => { + const ext = f.split('.').slice(-1)[0].toLowerCase(); + return VALID_AUDIO_EXTENSIONS.includes(ext); +}; async function getFilesInDirectory(directory) { let files = []; @@ -29,42 +27,90 @@ async function getFilesInDirectory(directory) { continue; } try { - const subFiles = (await getFilesInDirectory(fullPath)).filter((f) => - ['wav', 'mp3', 'ogg'].includes(f.split('.').slice(-1)[0].toLowerCase()), - ); + const subFiles = (await getFilesInDirectory(fullPath)).filter(isAudioFile); files = files.concat(subFiles); LOG && console.log(`${dirent.name} (${subFiles.length})`); } catch (err) { LOG && console.warn(`skipped due to error: ${fullPath}`); } } else { - files.push(fullPath); + isAudioFile(fullPath) && files.push(fullPath); } } return files; } -async function getBanks(directory) { +async function getBanks(directory, flat = false) { let files = await getFilesInDirectory(directory); let banks = {}; directory = directory.split(sep).join('/'); files = files.map((path) => { path = path.split(sep).join('/'); - const [bank] = path.split('/').slice(-2); + const subDir = path.replace(directory, ''); + const subDirFlat = subDir.replaceAll('/', '_').slice(1); // remove initial underscore + const subDirFlatStem = subDirFlat.replace(/\.[^.]+$/, ''); // remove extension + let bank = flat ? subDirFlatStem : path.split('/').slice(-2)[0]; banks[bank] = banks[bank] || []; - const relativeUrl = path.replace(directory, ''); - banks[bank].push(relativeUrl); - return relativeUrl; + banks[bank].push(subDir); + return subDir; }); banks._base = `http://localhost:5432`; return { banks, files }; } -// eslint-disable-next-line -const directory = process.cwd(); +const args = process.argv.slice(2); + +function getArgValue(flag) { + const i = args.indexOf(flag); + if (i !== -1) { + const nextIsFlag = args[i + 1]?.startsWith('--') ?? true; + if (nextIsFlag) return true; + return args[i + 1]; + } +} + +function getInput(query) { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + + return new Promise((resolve) => + rl.question(query, (response) => { + rl.close(); + resolve(response); + }), + ); +} + +let directory = getArgValue('--dir') || process.cwd(); +directory = resolve(directory); +if (args.includes('--json')) { + const { banks } = await getBanks(directory, getArgValue('--flat')); + const json = JSON.stringify(banks); + const outFile = resolve(directory, 'strudel.json'); + if (existsSync(outFile)) { + const answer = await getInput(`Warning: File already exists at ${outFile}. Overwrite? (y/N): `); + if (answer.toLowerCase() !== 'y') { + console.log('Aborted.'); + process.exit(0); + } + } + writeFileSync(outFile, json, 'utf8'); + console.log(`Wrote json to ${outFile}`); +} + +console.log( + cowsay.say({ + text: 'welcome to @strudel/sampler', + e: 'oO', + T: 'U ', + }), +); + const server = http.createServer(async (req, res) => { res.setHeader('Access-Control-Allow-Origin', '*'); - const { banks, files } = await getBanks(directory); + const { banks, files } = await getBanks(directory, getArgValue('--flat')); if (req.url === '/') { res.setHeader('Content-Type', 'application/json'); return res.end(JSON.stringify(banks)); @@ -72,7 +118,7 @@ const server = http.createServer(async (req, res) => { let subpath = decodeURIComponent(req.url); const filePath = join(directory, subpath.split('/').join(sep)); - //console.log('GET:', filePath); + // console.log('GET:', filePath); const isFound = existsSync(filePath); if (!isFound) { res.statusCode = 404; diff --git a/packages/serial/package.json b/packages/serial/package.json index c04a69cd0..9ed89cf2a 100644 --- a/packages/serial/package.json +++ b/packages/serial/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/serial", - "version": "1.2.2", + "version": "1.2.4", "description": "Webserial API for strudel", "main": "serial.mjs", "type": "module", diff --git a/packages/soundfonts/gm.mjs b/packages/soundfonts/gm.mjs index c0e57dcb0..d8b7687d7 100644 --- a/packages/soundfonts/gm.mjs +++ b/packages/soundfonts/gm.mjs @@ -537,7 +537,7 @@ export default { ], gm_synth_bass_1: [ // Synth Bass 1: Bass - '0380_Aspirin_sf2_file', + // '0380_Aspirin_sf2_file', // broken in safari https://codeberg.org/uzu/strudel/issues/1384 '0380_Chaos_sf2_file', '0380_FluidR3_GM_sf2_file', // 0380_GeneralUserGS_sf2_file // laut diff --git a/packages/soundfonts/package.json b/packages/soundfonts/package.json index 2c87a6e05..07e35674b 100644 --- a/packages/soundfonts/package.json +++ b/packages/soundfonts/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/soundfonts", - "version": "1.2.3", + "version": "1.2.5", "description": "Soundsfont support for strudel", "main": "index.mjs", "publishConfig": { diff --git a/packages/soundfonts/sfumato.mjs b/packages/soundfonts/sfumato.mjs index 6d60f80dd..4d5e7ef45 100644 --- a/packages/soundfonts/sfumato.mjs +++ b/packages/soundfonts/sfumato.mjs @@ -3,7 +3,7 @@ import { getAudioContext, registerSound } from '@strudel/webaudio'; import { loadSoundfont as _loadSoundfont, startPresetNote } from 'sfumato'; Pattern.prototype.soundfont = function (sf, n = 0) { - return this.onTrigger((time_deprecate, h, ct, cps, targetTime) => { + return this.onTrigger((h, ct, cps, targetTime) => { const ctx = getAudioContext(); const note = getPlayableNoteValue(h); const preset = sf.presets[n % sf.presets.length]; diff --git a/packages/superdough/README.md b/packages/superdough/README.md index 0c6e4f14c..f5947d83f 100644 --- a/packages/superdough/README.md +++ b/packages/superdough/README.md @@ -153,6 +153,7 @@ samples('github:tidalcycles/dirt-samples') The format is `github://`. +If `` and `` are not specified, they will default to `samples` and `main` respectively. It expects a `strudel.json` file to be present at the root of the given repository, which declares the sample paths in the repo. The format is also expected to be the same as explained above. diff --git a/packages/superdough/audioContext.mjs b/packages/superdough/audioContext.mjs new file mode 100644 index 000000000..71e01d57d --- /dev/null +++ b/packages/superdough/audioContext.mjs @@ -0,0 +1,18 @@ +let audioContext; + +export const setDefaultAudioContext = () => { + audioContext = new AudioContext(); + return audioContext; +}; + +export const getAudioContext = () => { + if (!audioContext) { + return setDefaultAudioContext(); + } + + return audioContext; +}; + +export function getAudioContextCurrentTime() { + return getAudioContext().currentTime; +} diff --git a/packages/superdough/dspworklet.mjs b/packages/superdough/dspworklet.mjs index ed5c1e7e0..aac08061e 100644 --- a/packages/superdough/dspworklet.mjs +++ b/packages/superdough/dspworklet.mjs @@ -1,4 +1,4 @@ -import { getAudioContext } from './superdough.mjs'; +import { getAudioContext } from './audioContext.mjs'; let worklet; export async function dspWorklet(ac, code) { @@ -74,6 +74,6 @@ export const dough = async (code) => { worklet.node.connect(ac.destination); }; -export function doughTrigger(time_deprecate, hap, currentTime, cps, targetTime) { +export function doughTrigger(hap, currentTime, cps, targetTime) { window.postMessage({ time: targetTime, dough: hap.value, currentTime, duration: hap.duration, cps }); } diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index 6bde69373..a471da9dd 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -1,5 +1,9 @@ -import { getAudioContext } from './superdough.mjs'; -import { clamp, nanFallback } from './util.mjs'; +import { getAudioContext } from './audioContext.mjs'; +import { clamp, nanFallback, midiToFreq, noteToMidi } from './util.mjs'; +import { getNoiseBuffer } from './noise.mjs'; +import { logger } from './logger.mjs'; + +export const noises = ['pink', 'white', 'brown', 'crackle']; export function gainNode(value) { const node = getAudioContext().createGain(); @@ -7,6 +11,13 @@ export function gainNode(value) { return node; } +export function effectSend(input, effect, wet) { + const send = gainNode(wet); + input.connect(send); + send.connect(effect); + return send; +} + const getSlope = (y1, y2, x1, x2) => { const denom = x2 - x1; if (denom === 0) { @@ -18,7 +29,9 @@ const getSlope = (y1, y2, x1, x2) => { export function getWorklet(ac, processor, params, config) { const node = new AudioWorkletNode(ac, processor, config); Object.entries(params).forEach(([key, value]) => { - node.parameters.get(key).value = value; + if (value !== undefined) { + node.parameters.get(key).value = value; + } }); return node; } @@ -85,6 +98,35 @@ export const getParamADSR = ( param[ramp](min, end + release); }; +function getModulationShapeInput(val) { + if (typeof val === 'number') { + return val % 5; + } + return { tri: 0, triangle: 0, sine: 1, ramp: 2, saw: 3, square: 4 }[val] ?? 0; +} + +export function getLfo(audioContext, begin, end, properties = {}) { + const { shape = 0, ...props } = properties; + const { dcoffset = -0.5, depth = 1 } = properties; + const lfoprops = { + frequency: 1, + depth, + skew: 0.5, + phaseoffset: 0, + time: begin, + begin, + end, + shape: getModulationShapeInput(shape), + dcoffset, + min: dcoffset * depth, + max: dcoffset * depth + depth, + curve: 1, + ...props, + }; + + return getWorklet(audioContext, 'lfo-processor', lfoprops); +} + export function getCompressor(ac, threshold, ratio, knee, attack, release) { const options = { threshold: threshold ?? -3, @@ -112,6 +154,41 @@ export const getADSRValues = (params, curve = 'linear', defaultValues) => { return [Math.max(a ?? 0, envmin), Math.max(d ?? 0, envmin), Math.min(sustain, envmax), Math.max(r ?? 0, releaseMin)]; }; +// helper utility for applying standard modulators to a parameter +export function applyParameterModulators(audioContext, param, start, end, envelopeValues, lfoValues) { + let { amount, offset, defaultAmount = 1, curve = 'linear', values, holdEnd, defaultValues } = envelopeValues; + + if (amount == null) { + const hasADSRParams = values.some((p) => p != null); + amount = hasADSRParams ? defaultAmount : 0; + } + + const min = offset ?? 0; + const max = amount + min; + const diff = Math.abs(max - min); + if (diff) { + const [attack, decay, sustain, release] = getADSRValues(values, curve, defaultValues); + getParamADSR(param, attack, decay, sustain, release, min, max, start, holdEnd, curve); + } + let lfo; + let { defaultDepth = 1, depth, dcoffset, ...getLfoInputs } = lfoValues; + + if (depth == null) { + const hasLFOParams = Object.values(getLfoInputs).some((v) => v != null); + depth = hasLFOParams ? defaultDepth : 0; + } + if (depth) { + lfo = getLfo(audioContext, start, end, { + depth, + dcoffset, + ...getLfoInputs, + }); + lfo.connect(param); + } + + return { lfo, disconnect: () => lfo?.disconnect() }; +} + export function createFilter(context, type, frequency, Q, att, dec, sus, rel, fenv, start, end, fanchor, model, drive) { const curve = 'exponential'; const [attack, decay, sustain, release] = getADSRValues([att, dec, sus, rel], curve, [0.005, 0.14, 0, 0.1]); @@ -171,7 +248,7 @@ let curves = ['linear', 'exponential']; export function getPitchEnvelope(param, value, t, holdEnd) { // envelope is active when any of these values is set const hasEnvelope = value.pattack ?? value.pdecay ?? value.psustain ?? value.prelease ?? value.penv; - if (!hasEnvelope) { + if (hasEnvelope === undefined) { return; } const penv = nanFallback(value.penv, 1, true); @@ -206,19 +283,46 @@ export function getVibratoOscillator(param, value, t) { // ConstantSource inherits AudioScheduledSourceNode, which has scheduling abilities // a bit of a hack, but it works very well :) export function webAudioTimeout(audioContext, onComplete, startTime, stopTime) { - const constantNode = audioContext.createConstantSource(); - constantNode.start(startTime); - constantNode.stop(stopTime); + const constantNode = new ConstantSourceNode(audioContext); + + // Certain browsers requires audio nodes to be connected in order for their onended events + // to fire, so we _mute it_ and then connect it to the destination + const zeroGain = gainNode(0); + zeroGain.connect(audioContext.destination); + constantNode.connect(zeroGain); + + // Schedule the `onComplete` callback to occur at `stopTime` constantNode.onended = () => { + // Ensure garbage collection + try { + zeroGain.disconnect(); + } catch { + // pass + } + try { + constantNode.disconnect(); + } catch { + // pass + } onComplete(); }; + constantNode.start(startTime); + constantNode.stop(stopTime); return constantNode; } const mod = (freq, range = 1, type = 'sine') => { const ctx = getAudioContext(); - const osc = ctx.createOscillator(); - osc.type = type; - osc.frequency.value = freq; + let osc; + if (noises.includes(type)) { + osc = ctx.createBufferSource(); + osc.buffer = getNoiseBuffer(type, 2); + osc.loop = true; + } else { + osc = ctx.createOscillator(); + osc.type = type; + osc.frequency.value = freq; + } + osc.start(); const g = new GainNode(ctx, { gain: range }); osc.connect(g); // -range, range @@ -253,7 +357,7 @@ export function applyFM(param, value, begin) { modulator = fmmod.node; stop = fmmod.stop; - if (![fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity].find((v) => v !== undefined)) { + if (![fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity].some((v) => v !== undefined)) { // no envelope by default modulator.connect(param); } else { @@ -277,3 +381,121 @@ export function applyFM(param, value, begin) { } return { stop }; } + +// Saturation curves + +const __squash = (x) => x / (1 + x); // [0, inf) to [0, 1) +const _mod = (n, m) => ((n % m) + m) % m; + +const _scurve = (x, k) => ((1 + k) * x) / (1 + k * Math.abs(x)); +const _soft = (x, k) => Math.tanh(x * (1 + k)); +const _hard = (x, k) => clamp((1 + k) * x, -1, 1); + +const _fold = (x, k) => { + // Closed form folding for audio rate + let y = (1 + 0.5 * k) * x; + const window = _mod(y + 1, 4); + return 1 - Math.abs(window - 2); +}; + +const _sineFold = (x, k) => Math.sin((Math.PI / 2) * _fold(x, k)); + +const _cubic = (x, k) => { + const t = __squash(Math.log1p(k)); + const cubic = (x - (t / 3) * x * x * x) / (1 - t / 3); // normalized to go from (-1, 1) + return _soft(cubic, k); +}; + +const _diode = (x, k, asym = false) => { + const g = 1 + 2 * k; // gain + const t = __squash(Math.log1p(k)); + const bias = 0.07 * t; + const pos = _soft(x + bias, 2 * k); + const neg = _soft(asym ? bias : -x + bias, 2 * k); + const y = pos - neg; + // We divide by the derivative at 0 so that the distortion is roughly + // the identity map near 0 => small values are preserved and undistorted + const sech = 1 / Math.cosh(g * bias); + const sech2 = sech * sech; // derivative of soft (i.e. tanh) is sech^2 + const denom = Math.max(1e-8, (asym ? 1 : 2) * g * sech2); // g from chain rule; 2 if both pos/neg have x + return _soft(y / denom, k); +}; + +const _asym = (x, k) => _diode(x, k, true); + +const _chebyshev = (x, k) => { + const kl = 10 * Math.log1p(k); + let tnm1 = 1; + let tnm2 = x; + let tn; + let y = 0; + for (let i = 1; i < 64; i++) { + if (i < 2) { + // Already set inital conditions + y += i == 0 ? tnm1 : tnm2; + continue; + } + tn = 2 * x * tnm1 - tnm2; // https://en.wikipedia.org/wiki/Chebyshev_polynomials#Recurrence_definition + tnm2 = tnm1; + tnm1 = tn; + if (i % 2 === 0) { + y += Math.min((1.3 * kl) / i, 2) * tn; + } + } + // Soft clip + return _soft(y, kl / 20); +}; + +export const distortionAlgorithms = { + scurve: _scurve, + soft: _soft, + hard: _hard, + cubic: _cubic, + diode: _diode, + asym: _asym, + fold: _fold, + sinefold: _sineFold, + chebyshev: _chebyshev, +}; +const _algoNames = Object.freeze(Object.keys(distortionAlgorithms)); + +export const getDistortionAlgorithm = (algo) => { + let index = algo; + if (typeof algo === 'string') { + index = _algoNames.indexOf(algo); + if (index === -1) { + logger(`[superdough] Could not find waveshaping algorithm ${algo}. + Available options are ${_algoNames.join(', ')}. + Defaulting to ${_algoNames[0]}.`); + index = 0; + } + } + const name = _algoNames[index % _algoNames.length]; // allow for wrapping if algo was a number + return distortionAlgorithms[name]; +}; + +export const getDistortion = (distort, postgain, algorithm) => { + return getWorklet(getAudioContext(), 'distort-processor', { distort, postgain }, { processorOptions: { algorithm } }); +}; + +export const getFrequencyFromValue = (value, defaultNote = 36) => { + let { note, freq } = value; + note = note || defaultNote; + if (typeof note === 'string') { + note = noteToMidi(note); // e.g. c3 => 48 + } + // get frequency + if (!freq && typeof note === 'number') { + freq = midiToFreq(note); // + 48); + } + + return Number(freq); +}; + +export const destroyAudioWorkletNode = (node) => { + if (node == null) { + return; + } + node.disconnect(); + node.parameters.get('end')?.setValueAtTime(0, 0); +}; diff --git a/packages/superdough/index.mjs b/packages/superdough/index.mjs index fd49fe338..a382bd15a 100644 --- a/packages/superdough/index.mjs +++ b/packages/superdough/index.mjs @@ -11,3 +11,5 @@ export * from './synth.mjs'; export * from './zzfx.mjs'; export * from './logger.mjs'; export * from './dspworklet.mjs'; +export * from './audioContext.mjs'; +export * from './wavetable.mjs'; diff --git a/packages/superdough/logger.mjs b/packages/superdough/logger.mjs index a20af1b3c..99fd0cf39 100644 --- a/packages/superdough/logger.mjs +++ b/packages/superdough/logger.mjs @@ -1,5 +1,12 @@ let log = (msg) => console.log(msg); +export function errorLogger(e, origin = 'superdough') { + if (process.env.NODE_ENV === 'development') { + console.error(e); + } + logger(`[${origin}] error: ${e.message}`); +} + export const logger = (...args) => log(...args); export const setLogger = (fn) => { diff --git a/packages/superdough/noise.mjs b/packages/superdough/noise.mjs index 247794702..816dd252b 100644 --- a/packages/superdough/noise.mjs +++ b/packages/superdough/noise.mjs @@ -1,10 +1,10 @@ import { drywet } from './helpers.mjs'; -import { getAudioContext } from './superdough.mjs'; +import { getAudioContext } from './audioContext.mjs'; let noiseCache = {}; // lazy generates noise buffers and keeps them forever -function getNoiseBuffer(type, density) { +export function getNoiseBuffer(type, density) { const ac = getAudioContext(); if (noiseCache[type]) { return noiseCache[type]; diff --git a/packages/superdough/package.json b/packages/superdough/package.json index 439b83718..a82117774 100644 --- a/packages/superdough/package.json +++ b/packages/superdough/package.json @@ -1,6 +1,6 @@ { "name": "superdough", - "version": "1.2.3", + "version": "1.2.5", "description": "simple web audio synth and sampler intended for live coding. inspired by superdirt and webdirt.", "main": "index.mjs", "type": "module", diff --git a/packages/superdough/reverb.mjs b/packages/superdough/reverb.mjs index 0f638ca80..2960b597c 100644 --- a/packages/superdough/reverb.mjs +++ b/packages/superdough/reverb.mjs @@ -1,7 +1,9 @@ import reverbGen from './reverbGen.mjs'; +import { clamp } from './util.mjs'; if (typeof AudioContext !== 'undefined') { - AudioContext.prototype.adjustLength = function (duration, buffer) { + AudioContext.prototype.adjustLength = function (duration, buffer, speed = 1, offsetAmount = 0) { + const sampleOffset = Math.floor(clamp(offsetAmount, 0, 1) * buffer.length); const newLength = buffer.sampleRate * duration; const newBuffer = this.createBuffer(buffer.numberOfChannels, buffer.length, buffer.sampleRate); for (let channel = 0; channel < buffer.numberOfChannels; channel++) { @@ -9,22 +11,30 @@ if (typeof AudioContext !== 'undefined') { let newData = newBuffer.getChannelData(channel); for (let i = 0; i < newLength; i++) { - newData[i] = oldData[i] || 0; + // loop the buffer around to prevent + let position = (sampleOffset + i * Math.abs(speed)) % oldData.length; + if (speed < 1) { + position = position * -1; + } + + newData[i] = oldData.at(position) || 0; } } return newBuffer; }; - AudioContext.prototype.createReverb = function (duration, fade, lp, dim, ir) { + AudioContext.prototype.createReverb = function (duration, fade, lp, dim, ir, irspeed, irbegin) { const convolver = this.createConvolver(); - convolver.generate = (d = 2, fade = 0.1, lp = 15000, dim = 1000, ir) => { + convolver.generate = (d = 2, fade = 0.1, lp = 15000, dim = 1000, ir, irspeed, irbegin) => { convolver.duration = d; convolver.fade = fade; convolver.lp = lp; convolver.dim = dim; convolver.ir = ir; + convolver.irspeed = irspeed; + convolver.irbegin = irbegin; if (ir) { - convolver.buffer = this.adjustLength(d, ir); + convolver.buffer = this.adjustLength(d, ir, irspeed, irbegin); } else { reverbGen.generateReverb( { @@ -41,7 +51,7 @@ if (typeof AudioContext !== 'undefined') { ); } }; - convolver.generate(duration, fade, lp, dim, ir); + convolver.generate(duration, fade, lp, dim, ir, irspeed, irbegin); return convolver; }; } diff --git a/packages/superdough/sampler.mjs b/packages/superdough/sampler.mjs index 18d1b7797..3232fa476 100644 --- a/packages/superdough/sampler.mjs +++ b/packages/superdough/sampler.mjs @@ -1,5 +1,6 @@ -import { noteToMidi, valueToMidi, getSoundIndex } from './util.mjs'; -import { getAudioContext, registerSound } from './index.mjs'; +import { getCommonSampleInfo } from './util.mjs'; +import { registerSound, registerWaveTable } from './index.mjs'; +import { getAudioContext } from './audioContext.mjs'; import { getADSRValues, getParamADSR, getPitchEnvelope, getVibratoOscillator } from './helpers.mjs'; import { logger } from './logger.mjs'; @@ -22,39 +23,16 @@ function humanFileSize(bytes, si) { return bytes.toFixed(1) + ' ' + units[u]; } -// deduces relevant info for sample loading from hap.value and sample definition -// it encapsulates the core sampler logic into a pure and synchronous function -// hapValue: Hap.value, bank: sample bank definition for sound "s" (values in strudel.json format) export function getSampleInfo(hapValue, bank) { - const { s, n = 0, speed = 1.0 } = hapValue; - let midi = valueToMidi(hapValue, 36); - let transpose = midi - 36; // C3 is middle C; - let sampleUrl; - let index = 0; - if (Array.isArray(bank)) { - index = getSoundIndex(n, bank.length); - sampleUrl = bank[index]; - } else { - const midiDiff = (noteA) => noteToMidi(noteA) - midi; - // object format will expect keys as notes - const closest = Object.keys(bank) - .filter((k) => !k.startsWith('_')) - .reduce( - (closest, key, j) => (!closest || Math.abs(midiDiff(key)) < Math.abs(midiDiff(closest)) ? key : closest), - null, - ); - transpose = -midiDiff(closest); // semitones to repitch - index = getSoundIndex(n, bank[closest].length); - sampleUrl = bank[closest][index]; - } - const label = `${s}:${index}`; + const { speed = 1.0 } = hapValue; + const { transpose, url, index, midi, label } = getCommonSampleInfo(hapValue, bank); let playbackRate = Math.abs(speed) * Math.pow(2, transpose / 12); - return { transpose, sampleUrl, index, midi, label, playbackRate }; + return { transpose, url, index, midi, label, playbackRate }; } // takes hapValue and returns buffer + playbackRate. export const getSampleBuffer = async (hapValue, bank, resolveUrl) => { - let { sampleUrl, label, playbackRate } = getSampleInfo(hapValue, bank); + let { url: sampleUrl, label, playbackRate } = getSampleInfo(hapValue, bank); if (resolveUrl) { sampleUrl = await resolveUrl(sampleUrl); } @@ -79,14 +57,14 @@ export const getSampleBufferSource = async (hapValue, bank, resolveUrl) => { bufferSource.buffer = buffer; bufferSource.playbackRate.value = playbackRate; - const { s, loopBegin = 0, loopEnd = 1, begin = 0, end = 1 } = hapValue; + const { loopBegin = 0, loopEnd = 1, begin = 0, end = 1 } = hapValue; // "The computation of the offset into the sound is performed using the sound buffer's natural sample rate, // rather than the current playback rate, so even if the sound is playing at twice its normal speed, // the midway point through a 10-second audio buffer is still 5." const offset = begin * bufferSource.buffer.duration; - const loop = s.startsWith('wt_') ? 1 : hapValue.loop; + const loop = hapValue.loop; if (loop) { bufferSource.loop = true; bufferSource.loopStart = loopBegin * bufferSource.buffer.duration - offset; @@ -143,13 +121,20 @@ function githubPath(base, subpath = '') { if (!base.startsWith('github:')) { throw new Error('expected "github:" at the start of pseudoUrl'); } - let [_, path] = base.split('github:'); + let path = base.slice('github:'.length); path = path.endsWith('/') ? path.slice(0, -1) : path; - if (path.split('/').length === 2) { - // assume main as default branch if none set - path += '/main'; + + let components = path.split('/'); + let user = components[0]; + let repo = components.length >= 2 ? components[1] : 'samples'; + let branch = components.length >= 3 ? components[2] : 'main'; + let other = components.slice(3); + if (subpath) { + other.push(subpath); } - return `https://raw.githubusercontent.com/${path}/${subpath}`; + other = other.join('/'); + + return `https://raw.githubusercontent.com/${user}/${repo}/${branch}/${other}`; } export const processSampleMap = (sampleMap, fn, baseUrl = sampleMap._base || '') => { @@ -196,6 +181,52 @@ function getSamplesPrefixHandler(url) { return; } +export async function fetchSampleMap(url) { + // check if custom prefix handler + const handler = getSamplesPrefixHandler(url); + if (handler) { + return handler(url); + } + url = resolveSpecialPaths(url); + if (url.startsWith('github:')) { + url = githubPath(url, 'strudel.json'); + } + if (url.startsWith('local:')) { + url = `http://localhost:5432`; + } + if (url.startsWith('shabda:')) { + let [_, path] = url.split('shabda:'); + url = `https://shabda.ndre.gr/${path}.json?strudel=1`; + } + if (url.startsWith('shabda/speech')) { + let [_, path] = url.split('shabda/speech'); + path = path.startsWith('/') ? path.substring(1) : path; + let [params, words] = path.split(':'); + let gender = 'f'; + let language = 'en-GB'; + if (params) { + [language, gender] = params.split('/'); + } + url = `https://shabda.ndre.gr/speech/${words}.json?gender=${gender}&language=${language}&strudel=1'`; + } + if (typeof fetch !== 'function') { + // not a browser + return; + } + const base = url.split('/').slice(0, -1).join('/'); + if (typeof fetch === 'undefined') { + // skip fetch when in node / testing + return; + } + const json = await fetch(url) + .then((res) => res.json()) + .catch((error) => { + console.error(error); + throw new Error(`error loading "${url}"`); + }); + return [json, json._base || base]; +} + /** * Loads a collection of samples to use with `s` * @example @@ -217,61 +248,16 @@ function getSamplesPrefixHandler(url) { export const samples = async (sampleMap, baseUrl = sampleMap._base || '', options = {}) => { if (typeof sampleMap === 'string') { - // check if custom prefix handler - const handler = getSamplesPrefixHandler(sampleMap); - if (handler) { - return handler(sampleMap); - } - sampleMap = resolveSpecialPaths(sampleMap); - if (sampleMap.startsWith('github:')) { - sampleMap = githubPath(sampleMap, 'strudel.json'); - } - if (sampleMap.startsWith('local:')) { - sampleMap = `http://localhost:5432`; - } - if (sampleMap.startsWith('shabda:')) { - let [_, path] = sampleMap.split('shabda:'); - sampleMap = `https://shabda.ndre.gr/${path}.json?strudel=1`; - } - if (sampleMap.startsWith('shabda/speech')) { - let [_, path] = sampleMap.split('shabda/speech'); - path = path.startsWith('/') ? path.substring(1) : path; - let [params, words] = path.split(':'); - let gender = 'f'; - let language = 'en-GB'; - if (params) { - [language, gender] = params.split('/'); - } - sampleMap = `https://shabda.ndre.gr/speech/${words}.json?gender=${gender}&language=${language}&strudel=1'`; - } - if (typeof fetch !== 'function') { - // not a browser - return; - } - const base = sampleMap.split('/').slice(0, -1).join('/'); - if (typeof fetch === 'undefined') { - // skip fetch when in node / testing - return; - } - return fetch(sampleMap) - .then((res) => res.json()) - .then((json) => samples(json, baseUrl || json._base || base, options)) - .catch((error) => { - console.error(error); - throw new Error(`error loading "${sampleMap}"`); - }); + const [json, base] = await fetchSampleMap(sampleMap); + return samples(json, baseUrl || base, options); } const { prebake, tag } = options; + processSampleMap( sampleMap, - (key, bank) => - registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, bank), { - type: 'sample', - samples: bank, - baseUrl, - prebake, - tag, - }), + (key, bank) => { + registerSampleSource(key, bank, { baseUrl, prebake, tag }); + }, baseUrl, ); }; @@ -361,3 +347,20 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) { return handle; } + +function registerSample(key, bank, params) { + registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, bank), { + type: 'sample', + samples: bank, + ...params, + }); +} + +export function registerSampleSource(key, bank, params) { + const isWavetable = key.startsWith('wt_'); + if (isWavetable) { + registerWaveTable(key, bank, params); + } else { + registerSample(key, bank, params); + } +} diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index ace667643..eec8d095b 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -7,12 +7,14 @@ This program is free software: you can redistribute it and/or modify it under th import './feedbackdelay.mjs'; import './reverb.mjs'; import './vowel.mjs'; -import { clamp, nanFallback, _mod, cycleToSeconds } from './util.mjs'; +import { nanFallback, _mod, cycleToSeconds } from './util.mjs'; import workletsUrl from './worklets.mjs?audioworklet'; -import { createFilter, gainNode, getCompressor, getWorklet } from './helpers.mjs'; +import { createFilter, gainNode, getCompressor, getDistortion, getLfo, getWorklet, effectSend } from './helpers.mjs'; import { map } from 'nanostores'; import { logger } from './logger.mjs'; import { loadBuffer } from './sampler.mjs'; +import { getAudioContext } from './audioContext.mjs'; +import { SuperdoughAudioController } from './superdoughoutput.mjs'; export const DEFAULT_MAX_POLYPHONY = 128; const DEFAULT_AUDIO_DEVICE_NAME = 'System Standard'; @@ -106,6 +108,19 @@ export async function aliasBank(...args) { } } +/** + * Register an alias for a sound. + * @param {string} original - The original sound name + * @param {string} alias - The alias to use for the sound + */ +export function soundAlias(original, alias) { + if (getSound(original) == null) { + logger('soundAlias: original sound not found'); + return; + } + soundMap.setKey(alias, getSound(original)); +} + export function getSound(s) { if (typeof s !== 'string') { console.warn(`getSound: expected string got "${s}". fall back to triangle`); @@ -140,6 +155,7 @@ const defaultDefaultValues = { phaserdepth: 0.75, shapevol: 1, distortvol: 1, + distorttype: 0, delay: 0, byteBeatExpression: '0', delayfeedback: 0.5, @@ -183,30 +199,17 @@ export function setVersionDefaults(version) { export const resetLoadedSounds = () => soundMap.set({}); -let audioContext; - -export const setDefaultAudioContext = () => { - audioContext = new AudioContext(); - return audioContext; -}; - -export const getAudioContext = () => { - if (!audioContext) { - return setDefaultAudioContext(); - } - - return audioContext; -}; - -export function getAudioContextCurrentTime() { - return getAudioContext().currentTime; +let externalWorklets = []; +export function registerWorklet(url) { + externalWorklets.push(url); } let workletsLoading; function loadWorklets() { if (!workletsLoading) { const audioCtx = getAudioContext(); - workletsLoading = audioCtx.audioWorklet.addModule(workletsUrl); + const allWorkletURLs = externalWorklets.concat([workletsUrl]); + workletsLoading = Promise.all(allWorkletURLs.map((workletURL) => audioCtx.audioWorklet.addModule(workletURL))); } return workletsLoading; @@ -272,79 +275,16 @@ export async function initAudioOnFirstClick(options) { return audioReady; } -let delays = {}; -const maxfeedback = 0.98; - -let channelMerger, destinationGain; -//update the output channel configuration to match user's audio device -export function initializeAudioOutput() { - const audioContext = getAudioContext(); - const maxChannelCount = audioContext.destination.maxChannelCount; - audioContext.destination.channelCount = maxChannelCount; - channelMerger = new ChannelMergerNode(audioContext, { numberOfInputs: audioContext.destination.channelCount }); - destinationGain = new GainNode(audioContext); - channelMerger.connect(destinationGain); - destinationGain.connect(audioContext.destination); +let controller; +function getSuperdoughAudioController() { + if (controller == null) { + controller = new SuperdoughAudioController(getAudioContext()); + } + return controller; } - -// input: AudioNode, channels: ?Array -export const connectToDestination = (input, channels = [0, 1]) => { - const ctx = getAudioContext(); - if (channelMerger == null) { - initializeAudioOutput(); - } - //This upmix can be removed if correct channel counts are set throughout the app, - // and then strudel could theoretically support surround sound audio files - const stereoMix = new StereoPannerNode(ctx); - input.connect(stereoMix); - - const splitter = new ChannelSplitterNode(ctx, { - numberOfOutputs: stereoMix.channelCount, - }); - stereoMix.connect(splitter); - channels.forEach((ch, i) => { - splitter.connect(channelMerger, i % stereoMix.channelCount, ch % ctx.destination.channelCount); - }); -}; - -export const panic = () => { - if (destinationGain == null) { - return; - } - destinationGain.gain.linearRampToValueAtTime(0, getAudioContext().currentTime + 0.01); - destinationGain = null; - channelMerger == null; -}; - -function getDelay(orbit, delaytime, delayfeedback, t, channels) { - if (delayfeedback > maxfeedback) { - //logger(`delayfeedback was clamped to ${maxfeedback} to save your ears`); - } - delayfeedback = clamp(delayfeedback, 0, 0.98); - if (!delays[orbit]) { - const ac = getAudioContext(); - const dly = ac.createFeedbackDelay(1, delaytime, delayfeedback); - dly.start?.(t); // for some reason, this throws when audion extension is installed.. - connectToDestination(dly, channels); - delays[orbit] = dly; - } - delays[orbit].delayTime.value !== delaytime && delays[orbit].delayTime.setValueAtTime(delaytime, t); - delays[orbit].feedback.value !== delayfeedback && delays[orbit].feedback.setValueAtTime(delayfeedback, t); - return delays[orbit]; -} - -export function getLfo(audioContext, time, end, properties = {}) { - return getWorklet(audioContext, 'lfo-processor', { - frequency: 1, - depth: 1, - skew: 0, - phaseoffset: 0, - time, - end, - shape: 1, - dcoffset: -0.5, - ...properties, - }); +export function connectToDestination(input, channels) { + const controller = getSuperdoughAudioController(); + controller.output.connectToDestination(input, channels); } function getPhaser(time, end, frequency = 1, depth = 0.5, centerFrequency = 1000, sweep = 2000) { @@ -378,33 +318,6 @@ function getFilterType(ftype) { return typeof ftype === 'number' ? filterTypes[Math.floor(_mod(ftype, filterTypes.length))] : ftype; } -let reverbs = {}; -let hasChanged = (now, before) => now !== undefined && now !== before; -function getReverb(orbit, duration, fade, lp, dim, ir, channels) { - // If no reverb has been created for a given orbit, create one - if (!reverbs[orbit]) { - const ac = getAudioContext(); - const reverb = ac.createReverb(duration, fade, lp, dim, ir); - connectToDestination(reverb, channels); - reverbs[orbit] = reverb; - } - if ( - hasChanged(duration, reverbs[orbit].duration) || - hasChanged(fade, reverbs[orbit].fade) || - hasChanged(lp, reverbs[orbit].lp) || - hasChanged(dim, reverbs[orbit].dim) || - reverbs[orbit].ir !== ir - ) { - // only regenerate when something has changed - // avoids endless regeneration on things like - // stack(s("a"), s("b").rsize(8)).room(.5) - // this only works when args may stay undefined until here - // setting default values breaks this - reverbs[orbit].generate(duration, fade, lp, dim, ir); - } - return reverbs[orbit]; -} - export let analysers = {}, analysersData = {}; @@ -437,16 +350,8 @@ export function getAnalyzerData(type = 'time', id = 1) { return analysersData[id]; } -function effectSend(input, effect, wet) { - const send = gainNode(wet); - input.connect(send); - send.connect(effect); - return send; -} - export function resetGlobalEffects() { - delays = {}; - reverbs = {}; + controller?.reset(); analysers = {}; analysersData = {}; } @@ -458,9 +363,11 @@ function mapChannelNumbers(channels) { return (Array.isArray(channels) ? channels : [channels]).map((ch) => ch - 1); } -export const superdough = async (value, t, hapDuration, cps = 0.5) => { +export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) => { + // new: t is always expected to be the absolute target onset time const ac = getAudioContext(); - t = typeof t === 'string' && t.startsWith('=') ? Number(t.slice(1)) : ac.currentTime + t; + const audioController = getSuperdoughAudioController(); + let { stretch } = value; if (stretch != null) { //account for phase vocoder latency @@ -486,6 +393,12 @@ export const superdough = async (value, t, hapDuration, cps = 0.5) => { } // destructure let { + tremolo, + tremolosync, + tremolodepth = 1, + tremoloskew, + tremolophase = 0, + tremoloshape, s = getDefaultValue('s'), bank, source, @@ -493,9 +406,15 @@ export const superdough = async (value, t, hapDuration, cps = 0.5) => { gainlinear, postgain = getDefaultValue('postgain'), density = getDefaultValue('density'), + duckorbit, + duckonset, + duckattack, + duckdepth, + djf, // filters fanchor = getDefaultValue('fanchor'), drive = 0.69, + release = 0, // low pass cutoff, lpenv, @@ -528,11 +447,14 @@ export const superdough = async (value, t, hapDuration, cps = 0.5) => { phasercenter, // coarse, + crush, + dry, shape, shapevol = getDefaultValue('shapevol'), distort, distortvol = getDefaultValue('distortvol'), + distorttype = getDefaultValue('distorttype'), pan, vowel, delay = getDefaultValue('delay'), @@ -546,6 +468,8 @@ export const superdough = async (value, t, hapDuration, cps = 0.5) => { roomdim, roomsize, ir, + irspeed, + irbegin, i = getDefaultValue('i'), velocity = getDefaultValue('velocity'), analyze, // analyser wet @@ -562,7 +486,12 @@ export const superdough = async (value, t, hapDuration, cps = 0.5) => { const orbitChannels = mapChannelNumbers( multiChannelOrbits && orbit > 0 ? [orbit * 2 - 1, orbit * 2] : getDefaultValue('channels'), ); + const channels = value.channels != null ? mapChannelNumbers(value.channels) : orbitChannels; + const orbitBus = audioController.getOrbit(orbit, channels); + if (duckorbit != null) { + audioController.duck(duckorbit, t, duckonset, duckattack, duckdepth); + } gain = applyGainCurve(nanFallback(gain, 1)); postgain = applyGainCurve(postgain); @@ -570,11 +499,14 @@ export const superdough = async (value, t, hapDuration, cps = 0.5) => { distortvol = applyGainCurve(distortvol); delay = applyGainCurve(delay); velocity = applyGainCurve(velocity); + tremolodepth = applyGainCurve(tremolodepth); gain *= velocity; // velocity currently only multiplies with gain. it might do other things in the future if (gainlinear != null) { gain *= gainlinear; } + const end = t + hapDuration; + const endWithRelease = end + release; const chainID = Math.round(Math.random() * 1000000); // oldest audio nodes will be destroyed if maximum polyphony is exceeded @@ -608,7 +540,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5) => { audioNodes.forEach((n) => n?.disconnect()); activeSoundSources.delete(chainID); }; - const soundHandle = await onTrigger(t, value, onEnded); + const soundHandle = await onTrigger(t, value, onEnded, cps); if (soundHandle) { sourceNode = soundHandle.node; @@ -649,7 +581,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5) => { lprelease, lpenv, t, - t + hapDuration, + end, fanchor, ftype, drive, @@ -673,7 +605,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5) => { hprelease, hpenv, t, - t + hapDuration, + end, fanchor, ); chain.push(hp()); @@ -684,20 +616,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5) => { if (bandf !== undefined) { let bp = () => - createFilter( - ac, - 'bandpass', - bandf, - bandq, - bpattack, - bpdecay, - bpsustain, - bprelease, - bpenv, - t, - t + hapDuration, - fanchor, - ); + createFilter(ac, 'bandpass', bandf, bandq, bpattack, bpdecay, bpsustain, bprelease, bpenv, t, end, fanchor); chain.push(bp()); if (ftype === '24db') { chain.push(bp()); @@ -712,8 +631,42 @@ export const superdough = async (value, t, hapDuration, cps = 0.5) => { // effects coarse !== undefined && chain.push(getWorklet(ac, 'coarse-processor', { coarse })); crush !== undefined && chain.push(getWorklet(ac, 'crush-processor', { crush })); - shape !== undefined && chain.push(getWorklet(ac, 'shape-processor', { shape, postgain: shapevol })); - distort !== undefined && chain.push(getWorklet(ac, 'distort-processor', { distort, postgain: distortvol })); + distort !== undefined && chain.push(getDistortion(distort, distortvol, distorttype)); + + if (tremolosync != null) { + tremolo = cps * tremolosync; + } + + if (value.wtPosSynced != null) { + value.wtPosRate /= cps; + } + + if (value.wtWarpSynced != null) { + value.wtWarpRate /= cps; + } + + if (tremolo !== undefined) { + // Allow clipping of modulator for more dynamic possiblities, and to prevent speaker overload + // EX: a triangle waveform will clip like this /-\ when the depth is above 1 + const gain = Math.max(1 - tremolodepth, 0); + const amGain = new GainNode(ac, { gain }); + + const time = cycle / cps; + const lfo = getLfo(ac, t, endWithRelease, { + skew: tremoloskew ?? (tremoloshape != null ? 0.5 : 1), + frequency: tremolo, + depth: tremolodepth, + time, + dcoffset: 0, + shape: tremoloshape, + phaseoffset: tremolophase, + min: 0, + max: 1, + curve: 1.5, + }); + lfo.connect(amGain.gain); + chain.push(amGain); + } compressorThreshold !== undefined && chain.push( @@ -728,24 +681,20 @@ export const superdough = async (value, t, hapDuration, cps = 0.5) => { } // phaser if (phaser !== undefined && phaserdepth > 0) { - const phaserFX = getPhaser(t, t + hapDuration, phaser, phaserdepth, phasercenter, phasersweep); + const phaserFX = getPhaser(t, endWithRelease, phaser, phaserdepth, phasercenter, phasersweep); chain.push(phaserFX); } // last gain const post = new GainNode(ac, { gain: postgain }); chain.push(post); - connectToDestination(post, channels); // delay - let delaySend; if (delay > 0 && delaytime > 0 && delayfeedback > 0) { - const delayNode = getDelay(orbit, delaytime, delayfeedback, t, orbitChannels); - delaySend = effectSend(post, delayNode, delay); - audioNodes.push(delaySend); + orbitBus.getDelay(delaytime, delayfeedback, t); + orbitBus.sendDelay(post, delay); } // reverb - let reverbSend; if (room > 0) { let roomIR; if (ir !== undefined) { @@ -758,18 +707,28 @@ export const superdough = async (value, t, hapDuration, cps = 0.5) => { } roomIR = await loadBuffer(url, ac, ir, 0); } - const reverbNode = getReverb(orbit, roomsize, roomfade, roomlp, roomdim, roomIR, orbitChannels); - reverbSend = effectSend(post, reverbNode, room); - audioNodes.push(reverbSend); + orbitBus.getReverb(roomsize, roomfade, roomlp, roomdim, roomIR, irspeed, irbegin); + orbitBus.sendReverb(post, room); + } + + if (djf != null) { + orbitBus.getDjf(djf, t); } // analyser - let analyserSend; if (analyze) { const analyserNode = getAnalyserById(analyze, 2 ** (fft + 5)); - analyserSend = effectSend(post, analyserNode, 1); + const analyserSend = effectSend(post, analyserNode, 1); audioNodes.push(analyserSend); } + if (dry != null) { + dry = applyGainCurve(dry); + const dryGain = new GainNode(ac, { gain: dry }); + chain.push(dryGain); + orbitBus.connectToOutput(dryGain); + } else { + orbitBus.connectToOutput(post); + } // connect chain elements together chain.slice(1).reduce((last, current) => last.connect(current), chain[0]); diff --git a/packages/superdough/superdoughoutput.mjs b/packages/superdough/superdoughoutput.mjs new file mode 100644 index 000000000..afe91e71e --- /dev/null +++ b/packages/superdough/superdoughoutput.mjs @@ -0,0 +1,209 @@ +import { effectSend, getWorklet, webAudioTimeout } from './helpers.mjs'; +import { errorLogger } from './logger.mjs'; +import { clamp } from './util.mjs'; + +let hasChanged = (now, before) => now !== undefined && now !== before; + +export class Orbit { + reverbNode; + delayNode; + output; + summingNode; + djfNode; + audioContext; + constructor(audioContext) { + this.audioContext = audioContext; + this.output = new GainNode(audioContext, { gain: 1, channelCount: 2, channelCountMode: 'explicit' }); + this.summingNode = new GainNode(audioContext, { gain: 1, channelCount: 2, channelCountMode: 'explicit' }); + this.summingNode.connect(this.output); + } + + disconnect() { + this.output.disconnect(); + this.summingNode.disconnect(); + this.delayNode?.disconnect(); + this.reverbNode?.disconnect(); + } + + getDjf(value, t = 0) { + if (this.djfNode == null) { + this.djfNode = getWorklet(this.audioContext, 'djf-processor', { value }); + this.summingNode.disconnect(); + this.summingNode.connect(this.djfNode); + this.djfNode.connect(this.output); + } + const val = this.djfNode.parameters.get('value'); + val.setValueAtTime(value, t); + } + + getDelay(delaytime = 0, feedback = 0.5, t) { + const maxfeedback = 0.98; + if (feedback > maxfeedback) { + //logger(`feedback was clamped to ${maxfeedback} to save your ears`); + } + feedback = clamp(feedback, 0, 0.98); + if (this.delayNode == null) { + this.delayNode = this.audioContext.createFeedbackDelay(1, delaytime, feedback); + this.delayNode.connect(this.summingNode); + this.delayNode.start?.(t); // for some reason, this throws when audion extension is installed.. + } + this.delayNode.delayTime.value !== delaytime && this.delayNode.delayTime.setValueAtTime(delaytime, t); + this.delayNode.feedback.value !== feedback && this.delayNode.feedback.setValueAtTime(feedback, t); + return this.delayNode; + } + + getReverb(duration, fade, lp, dim, ir, irspeed, irbegin) { + // If no reverb has been created for a given orbit, create one + if (this.reverbNode == null) { + this.reverbNode = this.audioContext.createReverb(duration, fade, lp, dim, ir, irspeed, irbegin); + this.reverbNode.connect(this.summingNode); + } + + if ( + hasChanged(duration, this.reverbNode.duration) || + hasChanged(fade, this.reverbNode.fade) || + hasChanged(lp, this.reverbNode.lp) || + hasChanged(dim, this.reverbNode.dim) || + hasChanged(irspeed, this.reverbNode.irspeed) || + hasChanged(irbegin, this.reverbNode.irbegin) || + this.reverbNode.ir !== ir + ) { + // only regenerate when something has changed + // avoids endless regeneration on things like + // stack(s("a"), s("b").rsize(8)).room(.5) + // this only works when args may stay undefined until here + // setting default values breaks this + this.reverbNode.generate(duration, fade, lp, dim, ir, irspeed, irbegin); + } + return this.reverbNode; + } + sendReverb(node, amount) { + effectSend(node, this.reverbNode, amount); + } + + sendDelay(node, amount) { + effectSend(node, this.delayNode, amount); + } + + duck(t, onsettime = 0, attacktime = 0.1, depth = 1) { + const onset = onsettime; + const attack = Math.max(attacktime, 0.002); + const gainParam = this.output.gain; + webAudioTimeout( + this.audioContext, + () => { + const now = this.audioContext.currentTime; + + // cancelScheduledValues and setValueAtTime together emulate cancelAndHoldAtTime + // on browsers which lack that method + const currVal = gainParam.value; + gainParam.cancelScheduledValues(now); + gainParam.setValueAtTime(currVal, now); + + const t0 = Math.max(t, now); // guard against now > t + const duckedVal = clamp(1 - Math.sqrt(depth), 0.01, currVal); + gainParam.exponentialRampToValueAtTime(duckedVal, t0 + onset); + gainParam.exponentialRampToValueAtTime(1, t0 + onset + attack); + }, + 0, + t - 0.01, + ); + } + + connectToOutput(node) { + node.connect(this.summingNode); + } +} + +export class SuperdoughOutput { + channelMerger; + destinationGain; + + constructor(audioContext) { + this.audioContext = audioContext; + this.initializeAudio(); + } + + initializeAudio() { + const audioContext = this.audioContext; + const maxChannelCount = audioContext.destination.maxChannelCount; + this.audioContext.destination.channelCount = maxChannelCount; + this.channelMerger = new ChannelMergerNode(audioContext, { numberOfInputs: audioContext.destination.channelCount }); + this.destinationGain = new GainNode(audioContext); + this.channelMerger.connect(this.destinationGain); + this.destinationGain.connect(audioContext.destination); + } + + reset() { + this.disconnect(); + this.initializeAudio(); + } + disconnect() { + this.channelMerger.disconnect(); + this.destinationGain.disconnect(); + this.destinationGain = null; + this.channelMerger = null; + } + connectToDestination = (input, channels = [0, 1]) => { + //This upmix can be removed if correct channel counts are set throughout the app, + // and then strudel could theoretically support surround sound audio files + const stereoMix = new StereoPannerNode(this.audioContext); + input.connect(stereoMix); + + const splitter = new ChannelSplitterNode(this.audioContext, { + numberOfOutputs: stereoMix.channelCount, + }); + stereoMix.connect(splitter); + channels.forEach((ch, i) => { + splitter.connect(this.channelMerger, i % stereoMix.channelCount, ch % this.audioContext.destination.channelCount); + }); + }; +} + +export class SuperdoughAudioController { + audioContext; + output; + nodes = {}; + + constructor(audioContext) { + this.audioContext = audioContext; + this.output = new SuperdoughOutput(audioContext); + } + + reset() { + Array.from(this.nodes).forEach((node) => { + node.disconnect(); + }); + this.nodes = {}; + this.output.reset(); + } + + duck(targetOrbits, t, onsettime = 0, attacktime = 0.1, depth = 1) { + const targetArr = [targetOrbits].flat(); + const onsetArr = [onsettime].flat(); + const attackArr = [attacktime].flat(); + const depthArr = [depth].flat(); + + targetArr.forEach((target, idx) => { + const orbit = this.nodes[target]; + + if (orbit == null) { + errorLogger(new Error(`duck target orbit ${target} does not exist`), 'superdough'); + return; + } + const onset = onsetArr[idx] ?? onsetArr[0]; + const attack = Math.max(attackArr[idx] ?? attackArr[0], 0.002); + const depth = depthArr[idx] ?? depthArr[0]; + + orbit.duck(t, onset, attack, depth); + }); + } + + getOrbit(orbitNum, channels) { + if (this.nodes[orbitNum] == null) { + this.nodes[orbitNum] = new Orbit(this.audioContext); + this.output.connectToDestination(this.nodes[orbitNum].output, channels); + } + return this.nodes[orbitNum]; + } +} diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index 88e14e5ab..e35b98806 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -1,38 +1,22 @@ -import { clamp, midiToFreq, noteToMidi } from './util.mjs'; -import { registerSound, getAudioContext, soundMap, getLfo } from './superdough.mjs'; +import { clamp } from './util.mjs'; +import { registerSound, soundMap } from './superdough.mjs'; +import { getAudioContext } from './audioContext.mjs'; import { applyFM, + destroyAudioWorkletNode, gainNode, getADSRValues, + getFrequencyFromValue, + getLfo, getParamADSR, getPitchEnvelope, getVibratoOscillator, - webAudioTimeout, getWorklet, + noises, + webAudioTimeout, } from './helpers.mjs'; import { getNoiseMix, getNoiseOscillator } from './noise.mjs'; -const getFrequencyFromValue = (value) => { - let { note, freq } = value; - note = note || 36; - if (typeof note === 'string') { - note = noteToMidi(note); // e.g. c3 => 48 - } - // get frequency - if (!freq && typeof note === 'number') { - freq = midiToFreq(note); // + 48); - } - - return Number(freq); -}; -function destroyAudioWorkletNode(node) { - if (node == null) { - return; - } - node.disconnect(); - node.parameters.get('end')?.setValueAtTime(0, 0); -} - const waveforms = ['triangle', 'square', 'sawtooth', 'sine']; const waveformAliases = [ ['tri', 'triangle'], @@ -40,7 +24,17 @@ const waveformAliases = [ ['saw', 'sawtooth'], ['sin', 'sine'], ]; -const noises = ['pink', 'white', 'brown', 'crackle']; + +function makeSaturationCurve(amount, n_samples) { + const k = typeof amount === 'number' ? amount : 50; + const curve = new Float32Array(n_samples); + + for (let i = 0; i < n_samples; i++) { + const x = (i * 2) / n_samples - 1; + curve[i] = Math.tanh(x * k); + } + return curve; +} export function registerSynthSounds() { [...waveforms].forEach((s) => { @@ -84,6 +78,75 @@ export function registerSynthSounds() { { type: 'synth', prebake: true }, ); }); + + registerSound( + 'sbd', + (t, value, onended) => { + const { duration, decay = 0.5, pdecay = 0.5, penv = 36, clip } = value; + const ctx = getAudioContext(); + const attackhold = 0.02; + const noiselvl = 1.2; + const noisedecay = 0.025; + const mixGain = 1; + + const o = ctx.createOscillator(); + o.type = 'triangle'; + o.frequency.value = getFrequencyFromValue(value, 29); + o.detune.setValueAtTime(penv * 100, 0); + o.detune.setValueAtTime(penv * 100, t); + o.detune.exponentialRampToValueAtTime(0.001, t + pdecay); + const g = gainNode(1); + g.gain.setValueAtTime(1, t + attackhold); + g.gain.exponentialRampToValueAtTime(0.001, t + attackhold + decay); + o.start(t); + + const noise = getNoiseOscillator('brown', t, 2); + const noiseGain = gainNode(1); + noiseGain.gain.setValueAtTime(noiselvl, t); + noiseGain.gain.exponentialRampToValueAtTime(0.001, t + noisedecay); + + const sat = new WaveShaperNode(ctx); + // tri to sine diode shaper emulation + sat.curve = makeSaturationCurve(2, ctx.sampleRate); + + const mix = gainNode(mixGain); + + o.onended = () => { + o.disconnect(); + g.disconnect(); + sat.disconnect(); + noise.node.disconnect(); + noiseGain.disconnect(); + mix.disconnect(); + onended(); + }; + + const node = o.connect(sat).connect(g).connect(mix); + noise.node.connect(noiseGain).connect(mix); + + const holdEnd = t + decay; + let end = holdEnd + 0.01; + if (clip != null) { + end = Math.min(t + clip * duration, end); + } + + // prevent clicking + mix.gain.setValueAtTime(mixGain, end - 0.01); + mix.gain.linearRampToValueAtTime(0, end); + + o.stop(end); + noise.stop(end); + + return { + node, + stop: (endTime) => { + o.stop(endTime); + }, + }; + }, + { type: 'synth', prebake: true }, + ); + registerSound( 'supersaw', (begin, value, onended) => { @@ -121,10 +184,7 @@ export function registerSynthSounds() { const gainAdjustment = 1 / Math.sqrt(voices); getPitchEnvelope(o.parameters.get('detune'), value, begin, holdend); const vibratoOscillator = getVibratoOscillator(o.parameters.get('detune'), value, begin); - // const fm = applyFM(o.parameters.get('frequency'), value, begin); - // https://codeberg.org/uzu/strudel/issues/1428 - // if you think about re-enabling this, please test with fm > 1 first - // it's like 10x gain, so it's really dangerous + const fm = applyFM(o.parameters.get('frequency'), value, begin); let envGain = gainNode(1); envGain = o.connect(envGain); @@ -136,7 +196,7 @@ export function registerSynthSounds() { destroyAudioWorkletNode(o); envGain.disconnect(); onended(); - // fm?.stop(); + fm?.stop(); vibratoOscillator?.stop(); }, begin, diff --git a/packages/superdough/util.mjs b/packages/superdough/util.mjs index ea63a16f0..475c05f63 100644 --- a/packages/superdough/util.mjs +++ b/packages/superdough/util.mjs @@ -7,7 +7,7 @@ export const tokenizeNote = (note) => { if (typeof note !== 'string') { return []; } - const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bsf]*)([0-9]*)$/)?.slice(1) || []; + const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bsf]*)(-?[0-9]*)$/)?.slice(1) || []; if (!pc) { return []; } @@ -72,3 +72,36 @@ export const getSoundIndex = (n, numSounds) => { export function cycleToSeconds(cycle, cps) { return cycle / cps; } + +export function secondsToCycle(t, cps) { + return t * cps; +} + +// deduces relevant info for sample loading from hap.value and sample definition +// it encapsulates the core sampler logic into a pure and synchronous function +// hapValue: Hap.value, bank: sample bank definition for sound "s" (values in strudel.json format) +export function getCommonSampleInfo(hapValue, bank) { + const { s, n = 0 } = hapValue; + let midi = valueToMidi(hapValue, 36); + let transpose = midi - 36; // C3 is middle C; + let url; + let index = 0; + if (Array.isArray(bank)) { + index = getSoundIndex(n, bank.length); + url = bank[index]; + } else { + const midiDiff = (noteA) => noteToMidi(noteA) - midi; + // object format will expect keys as notes + const closest = Object.keys(bank) + .filter((k) => !k.startsWith('_')) + .reduce( + (closest, key, j) => (!closest || Math.abs(midiDiff(key)) < Math.abs(midiDiff(closest)) ? key : closest), + null, + ); + transpose = -midiDiff(closest); // semitones to repitch + index = getSoundIndex(n, bank[closest].length); + url = bank[closest][index]; + } + const label = `${s}:${index}`; + return { transpose, url, index, midi, label }; +} diff --git a/packages/superdough/wavetable.mjs b/packages/superdough/wavetable.mjs new file mode 100644 index 000000000..01d4eb838 --- /dev/null +++ b/packages/superdough/wavetable.mjs @@ -0,0 +1,336 @@ +import { getAudioContext, registerSound } from './index.mjs'; +import { getCommonSampleInfo } from './util.mjs'; +import { + applyFM, + applyParameterModulators, + destroyAudioWorkletNode, + getADSRValues, + getFrequencyFromValue, + getParamADSR, + getPitchEnvelope, + getVibratoOscillator, + getWorklet, + webAudioTimeout, +} from './helpers.mjs'; +import { logger } from './logger.mjs'; + +export const Warpmode = Object.freeze({ + NONE: 0, + ASYM: 1, + MIRROR: 2, + BENDP: 3, + BENDM: 4, + BENDMP: 5, + SYNC: 6, + QUANT: 7, + FOLD: 8, + PWM: 9, + ORBIT: 10, + SPIN: 11, + CHAOS: 12, + PRIMES: 13, + BINARY: 14, + BROWNIAN: 15, + RECIPROCAL: 16, + WORMHOLE: 17, + LOGISTIC: 18, + SIGMOID: 19, + FRACTAL: 20, + FLIP: 21, +}); + +const seenKeys = new Set(); +async function getPayload(url, label, frameLen = 2048) { + const key = `${url},${frameLen}`; + if (!seenKeys.has(key)) { + const buf = await loadBuffer(url, label); + const ch0 = buf.getChannelData(0); + const total = ch0.length; + const numFrames = Math.max(1, Math.floor(total / frameLen)); + const frames = new Array(numFrames); + for (let i = 0; i < numFrames; i++) { + const start = i * frameLen; + frames[i] = ch0.subarray(start, start + frameLen); + } + seenKeys.add(key); + return { frames, frameLen, numFrames, key }; + } + return { frameLen, key }; // worklet will use the cached version +} + +function humanFileSize(bytes, si) { + var thresh = si ? 1000 : 1024; + if (bytes < thresh) return bytes + ' B'; + var units = si + ? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] + : ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']; + var u = -1; + do { + bytes /= thresh; + ++u; + } while (bytes >= thresh); + return bytes.toFixed(1) + ' ' + units[u]; +} + +// Extract the sample rate of a .wav file +function parseWavSampleRate(arrBuf) { + const dv = new DataView(arrBuf); + // Header is "RIFFWAVE", so 12 bytes + let p = 12; + // Look through chunks for the format header + // (they will always have an 8 byte header (id and size) followed by a payload) + while (p + 8 <= dv.byteLength) { + // Parse id + const id = String.fromCharCode(dv.getUint8(p), dv.getUint8(p + 1), dv.getUint8(p + 2), dv.getUint8(p + 3)); + // Parse chunk size + const size = dv.getUint32(p + 4, true); + if (id === 'fmt ') { + // The format chunk contains the sample rate after + // 8 bytes of header, 2 bytes of format tag, 2 bytes of num channels + // (for a total of 12) + return dv.getUint32(p + 12, true); + } + // Advance to next chunk + p += 8 + size + (size & 1); + } + return null; +} + +async function decodeAtNativeRate(arr) { + const sr = parseWavSampleRate(arr) || 44100; + const tempAC = new OfflineAudioContext(1, 1, sr); + return await tempAC.decodeAudioData(arr); +} + +const loadCache = {}; +const loadBuffer = (url, label) => { + url = url.replace('#', '%23'); + if (!loadCache[url]) { + logger(`[wavetable] load table ${label}..`, 'load-table', { url }); + const timestamp = Date.now(); + loadCache[url] = fetch(url) + .then((res) => res.arrayBuffer()) + .then(async (res) => { + const took = Date.now() - timestamp; + const size = humanFileSize(res.byteLength); + logger(`[wavetable] load table ${label}... done! loaded ${size} in ${took}ms`, 'loaded-table', { url }); + const decoded = await decodeAtNativeRate(res); + return decoded; + }); + } + return loadCache[url]; +}; + +function githubPath(base, subpath = '') { + if (!base.startsWith('github:')) { + throw new Error('expected "github:" at the start of pseudoUrl'); + } + let [_, path] = base.split('github:'); + path = path.endsWith('/') ? path.slice(0, -1) : path; + if (path.split('/').length === 2) { + // assume main as default branch if none set + path += '/main'; + } + return `https://raw.githubusercontent.com/${path}/${subpath}`; +} + +const _processTables = (json, baseUrl, frameLen, options = {}) => { + baseUrl = json._base || baseUrl; + return Object.entries(json).forEach(([key, tables]) => { + if (key === '_base') return false; + if (typeof tables === 'string') { + tables = [tables]; + } + if (typeof tables !== 'object') { + throw new Error('wrong json format for ' + key); + } + let resolvedUrl = baseUrl; + if (resolvedUrl.startsWith('github:')) { + resolvedUrl = githubPath(resolvedUrl, ''); + } + tables = tables + .map((t) => resolvedUrl + t) + .filter((t) => { + if (!t.toLowerCase().endsWith('.wav')) { + logger(`[wavetable] skipping ${t} -- wavetables must be ".wav" format`); + return false; + } + return true; + }); + if (tables.length) { + registerWaveTable(key, tables, { baseUrl, frameLen }); + } + }); +}; + +export function registerWaveTable(key, tables, params) { + registerSound( + key, + (t, hapValue, onended, cps) => { + return onTriggerSynth(t, hapValue, onended, tables, cps, params?.frameLen ?? 2048); + }, + { + type: 'wavetable', + tables, + ...params, + }, + ); +} + +/** + * Loads a collection of wavetables to use with `s` + * + * @name tables + */ +export const tables = async (url, frameLen, json, options = {}) => { + if (json !== undefined) return _processTables(json, url, frameLen); + if (url.startsWith('github:')) { + url = githubPath(url, 'strudel.json'); + } + if (url.startsWith('local:')) { + url = `http://localhost:5432`; + } + if (typeof fetch !== 'function') { + // not a browser + return; + } + if (typeof fetch === 'undefined') { + // skip fetch when in node / testing + return; + } + return fetch(url) + .then((res) => res.json()) + .then((json) => _processTables(json, url, frameLen, options)) + .catch((error) => { + console.error(error); + throw new Error(`error loading "${url}"`); + }); +}; + +export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) { + const { s, n = 0, duration, clip } = value; + const ac = getAudioContext(); + const [attack, decay, sustain, release] = getADSRValues([value.attack, value.decay, value.sustain, value.release]); + let { warpmode } = value; + if (typeof warpmode === 'string') { + warpmode = Warpmode[warpmode.toUpperCase()] ?? Warpmode.NONE; + } + const frequency = getFrequencyFromValue(value); + const { url, label } = getCommonSampleInfo(value, tables); + const payload = await getPayload(url, label, frameLen); + let holdEnd = t + duration; + if (clip !== undefined) { + holdEnd = Math.min(t + clip * duration, holdEnd); + } + const endWithRelease = holdEnd + release; + const envEnd = endWithRelease + 0.01; + const source = getWorklet( + ac, + 'wavetable-oscillator-processor', + { + begin: t, + end: envEnd, + frequency, + freqspread: value.detune, + position: value.wt, + warp: value.warp, + warpMode: warpmode, + voices: Math.max(value.unison ?? 1, 1), + panspread: value.spread, + phaserand: (value.wtphaserand ?? value.unison > 1) ? 1 : 0, + }, + { outputChannelCount: [2] }, + ); + source.port.postMessage({ type: 'table', payload }); + if (ac.currentTime > t) { + logger(`[wavetable] still loading sound "${s}:${n}"`, 'highlight'); + return; + } + const posADSRParams = [value.wtattack, value.wtdecay, value.wtsustain, value.wtrelease]; + const warpADSRParams = [value.warpattack, value.warpdecay, value.warpsustain, value.warprelease]; + const wtParams = source.parameters; + const positionParam = wtParams.get('position'); + const warpParam = wtParams.get('warp'); + + let wtrate = value.wtrate; + if (value.wtsync != null) { + wtrate = cps * value.wtsync; + } + + const wtPosModulators = applyParameterModulators( + ac, + positionParam, + t, + endWithRelease, + { + offset: value.wt, + amount: value.wtenv, + defaultAmount: 0.5, + shape: 'linear', + values: posADSRParams, + holdEnd, + defaultValues: [0, 0.5, 0, 0.1], + }, + { + frequency: wtrate, + depth: value.wtdepth, + defaultDepth: 0.5, + shape: value.wtshape, + skew: value.wtskew, + dcoffset: value.wtdc ?? 0, + }, + ); + + let warprate = value.warprate; + if (value.warpsync != null) { + warprate = warprate = cps * value.warpsync; + } + const wtWarpModulators = applyParameterModulators( + ac, + warpParam, + t, + endWithRelease, + { + offset: value.warp, + amount: value.warpenv, + defaultAmount: 0.5, + shape: 'linear', + values: warpADSRParams, + holdEnd, + defaultValues: [0, 0.5, 0, 0.1], + }, + { + frequency: warprate, + depth: value.warpdepth, + defaultDepth: 0.5, + shape: value.warpshape, + skew: value.warpskew, + dcoffset: value.warpdc ?? 0, + }, + ); + const vibratoOscillator = getVibratoOscillator(source.parameters.get('detune'), value, t); + const fm = applyFM(source.parameters.get('frequency'), value, t); + const envGain = ac.createGain(); + const node = source.connect(envGain); + getParamADSR(node.gain, attack, decay, sustain, release, 0, 0.3, t, holdEnd, 'linear'); + getPitchEnvelope(source.parameters.get('detune'), value, t, holdEnd); + const handle = { node, source }; + const timeoutNode = webAudioTimeout( + ac, + () => { + destroyAudioWorkletNode(source); + vibratoOscillator?.stop(); + fm?.stop(); + node.disconnect(); + wtPosModulators?.disconnect(); + wtWarpModulators?.disconnect(); + onended(); + }, + t, + envEnd, + ); + handle.stop = (time) => { + timeoutNode.stop(time); + }; + return handle; +} diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 6f526ec8f..7d7ef8e2a 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -4,22 +4,47 @@ import OLAProcessor from './ola-processor'; import FFT from './fft.js'; +import { getDistortionAlgorithm } from './helpers.mjs'; const clamp = (num, min, max) => Math.min(Math.max(num, min), max); -const _mod = (n, m) => ((n % m) + m) % m; +const mod = (n, m) => ((n % m) + m) % m; +const lerp = (a, b, n) => n * (b - a) + a; +const pv = (arr, n) => arr[n] ?? arr[0]; +const frac = (x) => x - Math.floor(x); +const ffloor = (x) => x | 0; // fast floor for non-negative +const getUnisonDetune = (unison, detune, voiceIndex) => { + if (unison < 2) { + return 0; + } + return lerp(-detune * 0.5, detune * 0.5, voiceIndex / (unison - 1)); +}; +const applySemitoneDetuneToFrequency = (frequency, detune) => { + return frequency * Math.pow(2, detune / 12); +}; + +// Restrict phase to the range [0, maxPhase) via wrapping +function wrapPhase(phase, maxPhase = 1) { + if (phase >= maxPhase) { + phase -= maxPhase; + } else if (phase < 0) { + phase += maxPhase; + } + return phase; +} const blockSize = 128; -// adjust waveshape to remove frequencies above nyquist to prevent aliasing +// Smooth waveshape near discontinuities to remove frequencies above Nyquist and prevent aliasing // referenced from https://www.kvraudio.com/forum/viewtopic.php?t=375517 function polyBlep(phase, dt) { - // 0 <= phase < 1 + dt = Math.min(dt, 1 - dt); + // Start of cycle if (phase < dt) { phase /= dt; // 2 * (phase - phase^2/2 - 0.5) return phase + phase - phase * phase - 1; } - // -1 < phase < 0 + // End of cycle else if (phase > 1 - dt) { phase = (phase - 1) / dt; // 2 * (phase^2/2 + phase + 0.5) @@ -31,7 +56,7 @@ function polyBlep(phase, dt) { return 0; } } - +// The order is important for dough integration const waveshapes = { tri(phase, skew = 0.5) { const x = 1 - skew; @@ -81,10 +106,12 @@ function getParamValue(block, param) { } return param[0]; } + const waveShapeNames = Object.keys(waveshapes); class LFOProcessor extends AudioWorkletProcessor { static get parameterDescriptors() { return [ + { name: 'begin', defaultValue: 0 }, { name: 'time', defaultValue: 0 }, { name: 'end', defaultValue: 0 }, { name: 'frequency', defaultValue: 0.5 }, @@ -92,7 +119,10 @@ class LFOProcessor extends AudioWorkletProcessor { { name: 'depth', defaultValue: 1 }, { name: 'phaseoffset', defaultValue: 0 }, { name: 'shape', defaultValue: 0 }, + { name: 'curve', defaultValue: 1 }, { name: 'dcoffset', defaultValue: 0 }, + { name: 'min', defaultValue: 0 }, + { name: 'max', defaultValue: 1 }, ]; } @@ -108,11 +138,14 @@ class LFOProcessor extends AudioWorkletProcessor { } } - process(inputs, outputs, parameters) { - // eslint-disable-next-line no-undef + process(_inputs, outputs, parameters) { + const begin = parameters['begin'][0]; if (currentTime >= parameters.end[0]) { return false; } + if (currentTime <= begin) { + return true; + } const output = outputs[0]; const frequency = parameters['frequency'][0]; @@ -122,20 +155,24 @@ class LFOProcessor extends AudioWorkletProcessor { const skew = parameters['skew'][0]; const phaseoffset = parameters['phaseoffset'][0]; + const curve = parameters['curve'][0]; + const dcoffset = parameters['dcoffset'][0]; + const min = parameters['min'][0]; + const max = parameters['max'][0]; const shape = waveShapeNames[parameters['shape'][0]]; const blockSize = output[0].length ?? 0; if (this.phase == null) { - this.phase = _mod(time * frequency + phaseoffset, 1); + this.phase = mod(time * frequency + phaseoffset, 1); } - // eslint-disable-next-line no-undef const dt = frequency / sampleRate; for (let n = 0; n < blockSize; n++) { for (let i = 0; i < output.length; i++) { - const modval = (waveshapes[shape](this.phase, skew) + dcoffset) * depth; - output[i][n] = modval; + let modval = (waveshapes[shape](this.phase, skew) + dcoffset) * depth; + modval = Math.pow(modval, curve); + output[i][n] = clamp(modval, min, max); } this.incrementPhase(dt); } @@ -249,6 +286,73 @@ class ShapeProcessor extends AudioWorkletProcessor { } registerProcessor('shape-processor', ShapeProcessor); +class TwoPoleFilter { + s0 = 0; + s1 = 0; + update(s, cutoff, resonance = 0) { + // Out of bound values can produce NaNs + resonance = clamp(resonance, 0, 1); + cutoff = clamp(cutoff, 0, sampleRate / 2 - 1); + const c = clamp(2 * Math.sin(cutoff * (_PI / sampleRate)), 0, 1.14); + const r = Math.pow(0.5, (resonance + 0.125) / 0.125); + const mrc = 1 - r * c; + this.s0 = mrc * this.s0 - c * this.s1 + c * s; // bpf + this.s1 = mrc * this.s1 + c * this.s0; // lpf + return this.s1; // return lpf by default + } +} + +class DJFProcessor extends AudioWorkletProcessor { + static get parameterDescriptors() { + return [{ name: 'value', defaultValue: 0.5 }]; + } + + constructor() { + super(); + this.filters = [new TwoPoleFilter(), new TwoPoleFilter()]; + } + + process(inputs, outputs, parameters) { + const input = inputs[0]; + const output = outputs[0]; + + const hasInput = !(input[0] === undefined); + this.started = hasInput; + + const value = clamp(parameters.value[0], 0, 1); + let filterType = 'none'; + let cutoff; + let v = 1; + if (value > 0.51) { + filterType = 'hipass'; + v = (value - 0.5) * 2; + } else if (value < 0.49) { + filterType = 'lopass'; + v = value * 2; + } + cutoff = Math.pow(v * 11, 4); + + for (let i = 0; i < input.length; i++) { + for (let n = 0; n < blockSize; n++) { + if (filterType == 'none') { + output[i][n] = input[i][n]; + } else { + this.filters[i].update(input[i][n], cutoff, 0.1); + if (filterType === 'lopass') { + output[i][n] = this.filters[i].s1; + } else if (filterType === 'hipass') { + output[i][n] = input[i][n] - this.filters[i].s1; + } else { + output[i][n] = input[i][n]; + } + } + } + } + return true; + } +} +registerProcessor('djf-processor', DJFProcessor); + function fast_tanh(x) { const x2 = x * x; return (x * (27.0 + x2)) / (27.0 + 9.0 * x2); @@ -291,7 +395,6 @@ class LadderProcessor extends AudioWorkletProcessor { const drive = clamp(Math.exp(parameters.drive[0]), 0.1, 2000); let cutoff = parameters.frequency[0]; - // eslint-disable-next-line no-undef cutoff = (cutoff * 2 * _PI) / sampleRate; cutoff = cutoff > 1 ? 1 : cutoff; @@ -328,9 +431,10 @@ class DistortProcessor extends AudioWorkletProcessor { ]; } - constructor() { + constructor({ processorOptions }) { super(); this.started = false; + this.algorithm = getDistortionAlgorithm(processorOptions.algorithm); } process(inputs, outputs, parameters) { @@ -342,13 +446,12 @@ class DistortProcessor extends AudioWorkletProcessor { return false; } this.started = hasInput; - - const shape = Math.expm1(parameters.distort[0]); - const postgain = Math.max(0.001, Math.min(1, parameters.postgain[0])); - for (let n = 0; n < blockSize; n++) { - for (let i = 0; i < input.length; i++) { - output[i][n] = (((1 + shape) * input[i][n]) / (1 + shape * Math.abs(input[i][n]))) * postgain; + const postgain = clamp(pv(parameters.postgain, n), 0.001, 1); + const shape = Math.expm1(pv(parameters.distort, n)); + for (let ch = 0; ch < input.length; ch++) { + const x = input[ch][n]; + output[ch][n] = postgain * this.algorithm(x, shape); } } return true; @@ -357,21 +460,6 @@ class DistortProcessor extends AudioWorkletProcessor { registerProcessor('distort-processor', DistortProcessor); // SUPERSAW -function lerp(a, b, n) { - return n * (b - a) + a; -} - -function getUnisonDetune(unison, detune, voiceIndex) { - if (unison < 2) { - return 0; - } - return lerp(-detune * 0.5, detune * 0.5, voiceIndex / (unison - 1)); -} - -function applySemitoneDetuneToFrequency(frequency, detune) { - return frequency * Math.pow(2, detune / 12); -} - class SuperSawOscillatorProcessor extends AudioWorkletProcessor { constructor() { super(); @@ -423,54 +511,48 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor { }, ]; } - process(input, outputs, params) { - // eslint-disable-next-line no-undef + process(_input, outputs, params) { if (currentTime <= params.begin[0]) { return true; } - // eslint-disable-next-line no-undef if (currentTime >= params.end[0]) { // this.port.postMessage({ type: 'onended' }); return false; } - let frequency = params.frequency[0]; - //apply detune in cents - frequency = frequency * Math.pow(2, params.detune[0] / 1200); const output = outputs[0]; - const voices = params.voices[0]; - const freqspread = params.freqspread[0]; - const panspread = params.panspread[0] * 0.5 + 0.5; - const gain1 = Math.sqrt(1 - panspread); - const gain2 = Math.sqrt(panspread); - for (let n = 0; n < voices; n++) { - const isOdd = (n & 1) == 1; - - //applies unison "spread" detune in semitones - const freq = applySemitoneDetuneToFrequency(frequency, getUnisonDetune(voices, freqspread, n)); - let gainL = gain1; - let gainR = gain2; - // invert right and left gain - if (isOdd) { - gainL = gain2; - gainR = gain1; - } - // eslint-disable-next-line no-undef - const dt = freq / sampleRate; - - for (let i = 0; i < output[0].length; i++) { + for (let i = 0; i < output[0].length; i++) { + const detune = pv(params.detune, i); + const voices = pv(params.voices, i); + const freqspread = pv(params.freqspread, i); + const panspread = pv(params.panspread, i) * 0.5 + 0.5; + const gain1 = Math.sqrt(1 - panspread); + const gain2 = Math.sqrt(panspread); + let freq = pv(params.frequency, i); + // Main detuning + freq = applySemitoneDetuneToFrequency(freq, detune / 100); + for (let n = 0; n < voices; n++) { + const isOdd = (n & 1) == 1; + let gainL = gain1; + let gainR = gain2; + // invert right and left gain + if (isOdd) { + gainL = gain2; + gainR = gain1; + } + // Individual voice detuning + const freqVoice = applySemitoneDetuneToFrequency(freq, getUnisonDetune(voices, freqspread, n)); + // We must wrap this here because it is passed into sawblep below which + // has domain [0, 1] + const dt = mod(freqVoice / sampleRate, 1); this.phase[n] = this.phase[n] ?? Math.random(); const v = waveshapes.sawblep(this.phase[n], dt); output[0][i] = output[0][i] + v * gainL; output[1][i] = output[1][i] + v * gainR; - this.phase[n] += dt; - - if (this.phase[n] > 1.0) { - this.phase[n] = this.phase[n] - 1; - } + this.phase[n] = wrapPhase(this.phase[n] + dt); } } return true; @@ -479,7 +561,7 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor { registerProcessor('supersaw-oscillator', SuperSawOscillatorProcessor); -// Phase Vocoder sourced from // sourced from https://github.com/olvb/phaze/tree/master?tab=readme-ov-file +// Phase Vocoder sourced from https://github.com/olvb/phaze/tree/master?tab=readme-ov-file const BUFFERED_BLOCK_SIZE = 2048; function genHannWindow(length) { @@ -894,3 +976,346 @@ class ByteBeatProcessor extends AudioWorkletProcessor { } registerProcessor('byte-beat-processor', ByteBeatProcessor); + +export const WarpMode = Object.freeze({ + NONE: 0, + ASYM: 1, + MIRROR: 2, + BENDP: 3, + BENDM: 4, + BENDMP: 5, + SYNC: 6, + QUANT: 7, + FOLD: 8, + PWM: 9, + ORBIT: 10, + SPIN: 11, + CHAOS: 12, + PRIMES: 13, + BINARY: 14, + BROWNIAN: 15, + RECIPROCAL: 16, + WORMHOLE: 17, + LOGISTIC: 18, + SIGMOID: 19, + FRACTAL: 20, + FLIP: 21, +}); + +function hash32(u) { + u = u + 0x7ed55d16 + (u << 12); + u = u ^ 0xc761c23c ^ (u >>> 19); + u = u + 0x165667b1 + (u << 5); + u = (u + 0xd3a2646c) ^ (u << 9); + u = u + 0xfd7046c5 + (u << 3); + u = u ^ 0xb55a4f09 ^ (u >>> 16); + return u >>> 0; +} +const hash01 = (i) => (hash32(i) >>> 8) / 0x01000000; + +function bitReverse(i, n) { + let r = 0; + for (let b = 0; b < n; b++) { + r = (r << 1) | (i & 1); + i >>>= 1; + } + return r; +} + +function noise(x) { + const i = Math.floor(x), + f = x - i; + const a = hash01(i), + b = hash01(i + 1); + return a + (b - a) * f; +} + +function brownian(x, oct = 4) { + let amp = 0.5, + sum = 0, + norm = 0, + freq = 1; + for (let o = 0; o < oct; o++) { + sum += amp * noise(x * freq); + norm += amp; + amp *= 0.5; + freq *= 2; + } + return (sum / norm) * 2 - 1; +} + +const tablesCache = {}; +class WavetableOscillatorProcessor extends AudioWorkletProcessor { + static get parameterDescriptors() { + return [ + { name: 'begin', defaultValue: 0, min: 0, max: Number.POSITIVE_INFINITY }, + { name: 'end', defaultValue: 0, min: 0, max: Number.POSITIVE_INFINITY }, + { name: 'frequency', defaultValue: 440, min: Number.EPSILON }, + { name: 'detune', defaultValue: 0 }, + { name: 'freqspread', defaultValue: 0.18, min: 0 }, + { name: 'position', defaultValue: 0, min: 0, max: 1 }, + { name: 'warp', defaultValue: 0, min: 0, max: 1 }, + { name: 'warpMode', defaultValue: 0 }, + { name: 'voices', defaultValue: 1, min: 1 }, + { name: 'panspread', defaultValue: 0.7, min: 0, max: 1 }, + { name: 'phaserand', defaultValue: 0, min: 0, max: 1 }, + ]; + } + + constructor(options) { + super(options); + this.frameLen = 0; + this.numFrames = 0; + this.phase = []; + this.invSR = 1 / sampleRate; + + this.port.onmessage = (e) => { + const { type, payload } = e.data || {}; + if (type === 'table') { + const key = payload.key; + this.frameLen = payload.frameLen; + if (!tablesCache[key]) { + const tables = [payload.frames]; + let table = tables[0]; + for (let level = 1; level < 1; level++) { + const nextLen = table.length >> 1; + const nextTable = table.map((frame) => { + const avg = new Float32Array(nextLen); + for (let i = 0; i < nextLen; i++) { + avg[i] = (frame[2 * i] + frame[2 * i + 1]) / 2; + } + return avg; + }); + tables.push(nextTable); + table = nextTable; + if (nextLen <= 32) break; + } + tablesCache[key] = tables; + } + this.tables = tablesCache[key]; + this.numFrames = this.tables[0].length; + } + }; + } + + _mirror(x) { + return 1 - Math.abs(2 * x - 1); + } + + _toBits(amt, min = 2, max = 12) { + const b = max + (min - max) * amt; + return { b, n: Math.round(Math.pow(2, b)) }; + } + + _warpPhase(phase, amt, mode) { + switch (mode) { + case WarpMode.NONE: { + return phase; + } + case WarpMode.ASYM: { + const a = 0.01 + 0.99 * amt; + return phase < a ? (0.5 * phase) / a : 0.5 + (0.5 * (phase - a)) / (1 - a); + } + case WarpMode.MIRROR: { + // Asym, then mirror + return this._mirror(this._warpPhase(phase, amt, WarpMode.ASYM)); + } + case WarpMode.BENDP: { + return Math.pow(phase, 1 + 3 * amt); + } + case WarpMode.BENDM: { + return Math.pow(phase, 1 / (1 + 3 * amt)); + } + case WarpMode.BENDMP: { + return amt < 0.5 ? this._warpPhase(phase, 1 - 2 * amt, 3) : this._warpPhase(phase, 2 * amt - 1, 2); + } + case WarpMode.SYNC: { + const syncRatio = Math.pow(16, amt * amt); + return (phase * syncRatio) % 1; + } + case WarpMode.QUANT: { + const { n } = this._toBits(amt); + return ffloor(phase * n) / n; + } + case WarpMode.FOLD: { + const K = 7; + const k = 1 + Math.max(1, Math.round(K * amt)); + return Math.abs(frac(k * phase) - 0.5) * 2; + } + case WarpMode.PWM: { + const w = clamp(0.5 + 0.49 * (2 * amt - 1), 0, 1); + if (phase < w) return (phase / w) * 0.5; + return 0.5 + ((phase - w) / (1 - w)) * 0.5; + } + case WarpMode.ORBIT: { + const depth = 0.5 * amt; + const n = 3; + return frac(phase + depth * Math.sin(2 * Math.PI * n * phase)); + } + case WarpMode.SPIN: { + const depth = 0.5 * amt; + const { n } = this._toBits(amt, 1, 6); + return frac(phase + depth * Math.sin(2 * Math.PI * n * phase)); + } + case WarpMode.CHAOS: { + const r = 3.7 + 0.3 * amt; + const logistic = r * phase * (1 - phase); + return clamp((1 - amt) * phase + amt * logistic, 0, 1); + } + case WarpMode.PRIMES: { + const isPrime = (n) => { + if (n < 2) return false; + if (n % 2 === 0) return n === 2; + for (let d = 3; d * d <= n; d += 2) if (n % d === 0) return false; + return true; + }; + let { n } = this._toBits(amt, 3); + while (!isPrime(n)) n++; + return ffloor(phase * n) / n; + } + case WarpMode.BINARY: { + let { b } = this._toBits(amt, 3); + b = Math.round(b); + const n = 1 << b; + const idx = ffloor(phase * n); + const ridx = bitReverse(idx, b); + return ridx / n; + } + case WarpMode.MODULAR: { + const { n } = this._toBits(amt); + const depth = 0.5 * amt; + const jump = frac(phase * n) / n; + return frac(phase + depth * jump); + } + case WarpMode.BROWNIAN: { + const disp = 0.25 * amt * brownian(64 * phase, 4); + return frac(phase + disp); + } + case WarpMode.RECIPROCAL: { + const g = 2 + 4 * amt; + const num = phase * g; + const den = phase + (1 - phase) * g; + const y = den > 1e-12 ? num / den : 0; + return clamp(y, 0, 1); + } + case WarpMode.WORMHOLE: { + const gap = clamp(0.8 * amt, 0, 1); + const a = 0.5 * (1 - gap); + const b = 0.5 * (1 + gap); + if (phase < a) return (phase / a) * 0.5; + if (phase > b) return 0.5 * (1 + (phase - b) / (1 - b)); + return 0.5; + } + case WarpMode.LOGISTIC: { + let x = phase; + const r = 3.6 + 0.4 * amt; + const iters = 1 + Math.round(2 * amt); + for (let i = 0; i < iters; i++) x = r * x * (1 - x); + return clamp(x, 0, 1); + } + case WarpMode.SIGMOID: { + const k = 1 + 10 * amt; + const x = phase - 0.5; + const y = 1 / (1 + Math.exp(-k * x)); + const y0 = 1 / (1 + Math.exp(0.5 * k)); + const y1 = 1 / (1 + Math.exp(-0.5 * k)); + return (y - y0) / (y1 - y0); + } + case WarpMode.FRACTAL: { + const d = 0.5 * Math.sin(2 * Math.PI * phase) * amt; + return frac(phase + d); + } + case WarpMode.FLIP: { + return phase; + } + default: + return phase; + } + } + + _sampleFrame(frame, phase) { + const len = frame.length; + const pos = phase * len; + let i = pos | 0; + if (i >= len) i = 0; // fast wrap + const frac = pos - i; + const a = frame[i]; + let i1 = i + 1; + if (i1 >= len) i1 = 0; + const b = frame[i1]; + return a + (b - a) * frac; + } + + _chooseMip(dphi) { + const approxHarm = clamp(dphi, 1e-6, 64); + let level = 0; + while (level + 1 < (this.tables?.length || 1) && approxHarm < this.tables[level][0].length / 8) { + level++; + } + return level; + } + + process(_inputs, outputs, parameters) { + if (currentTime >= parameters.end[0]) { + return false; + } + if (currentTime <= parameters.begin[0]) { + return true; + } + const outL = outputs[0][0]; + const outR = outputs[0][1] || outputs[0][0]; + if (!this.tables) { + outL.fill(0); + if (outR !== outL) outR.set(outL); + return true; + } + for (let i = 0; i < outL.length; i++) { + const detune = pv(parameters.detune, i); + const freqspread = pv(parameters.freqspread, i); + const tablePos = clamp(pv(parameters.position, i), 0, 1); + const idx = tablePos * (this.numFrames - 1); + const fIdx = idx | 0; + const frac = idx - fIdx; + const warpAmount = clamp(pv(parameters.warp, i), 0, 1); + const warpMode = pv(parameters.warpMode, i); + const voices = pv(parameters.voices, i); + const phaseRand = clamp(pv(parameters.phaserand, i), 0, 1); + const panspread = voices > 1 ? clamp(pv(parameters.panspread, i), 0, 1) : 0; + const gain1 = Math.sqrt(0.5 - 0.5 * panspread); + const gain2 = Math.sqrt(0.5 + 0.5 * panspread); + let f = pv(parameters.frequency, i); + f = applySemitoneDetuneToFrequency(f, detune / 100); // overall detune + const normalizer = 1 / Math.sqrt(voices); + for (let n = 0; n < voices; n++) { + const isOdd = (n & 1) == 1; + let gainL = gain1; + let gainR = gain2; + // invert right and left gain + if (isOdd) { + gainL = gain2; + gainR = gain1; + } + const fVoice = applySemitoneDetuneToFrequency(f, getUnisonDetune(voices, freqspread, n)); // voice detune + const dPhase = fVoice * this.invSR; + const level = this._chooseMip(dPhase); + const table = this.tables[level]; + + // warp phase then sample + this.phase[n] = this.phase[n] ?? Math.random() * phaseRand; + const ph = this._warpPhase(this.phase[n], warpAmount, warpMode); + const s0 = this._sampleFrame(table[fIdx], ph); + const s1 = this._sampleFrame(table[Math.min(this.numFrames - 1, fIdx + 1)], ph); + let s = s0 + (s1 - s0) * frac; + if (warpMode === WarpMode.FLIP && this.phase[n] < warpAmount) { + s = -s; + } + outL[i] += s * gainL * normalizer; + outR[i] += s * gainR * normalizer; + this.phase[n] = wrapPhase(this.phase[n] + dPhase); + } + } + return true; + } +} + +registerProcessor('wavetable-oscillator-processor', WavetableOscillatorProcessor); diff --git a/packages/superdough/zzfx.mjs b/packages/superdough/zzfx.mjs index a6af82609..32db0395a 100644 --- a/packages/superdough/zzfx.mjs +++ b/packages/superdough/zzfx.mjs @@ -1,6 +1,7 @@ //import { ZZFX } from 'zzfx'; import { midiToFreq, noteToMidi } from './util.mjs'; -import { registerSound, getAudioContext } from './superdough.mjs'; +import { registerSound } from './superdough.mjs'; +import { getAudioContext } from './audioContext.mjs'; import { buildSamples } from './zzfx_fork.mjs'; export const getZZFX = (value, t) => { diff --git a/packages/superdough/zzfx_fork.mjs b/packages/superdough/zzfx_fork.mjs index 7235d1bf8..f3ee6a051 100644 --- a/packages/superdough/zzfx_fork.mjs +++ b/packages/superdough/zzfx_fork.mjs @@ -1,4 +1,4 @@ -import { getAudioContext } from './superdough.mjs'; +import { getAudioContext } from './audioContext.mjs'; // https://github.com/KilledByAPixel/ZzFX/blob/master/ZzFX.js#L85C5-L180C6 // changes: replaced this.volume with 1 + using sampleRate from getAudioContext() diff --git a/packages/supradough/.gitignore b/packages/supradough/.gitignore new file mode 100644 index 000000000..d21cbdf3e --- /dev/null +++ b/packages/supradough/.gitignore @@ -0,0 +1 @@ +pattern.wav diff --git a/packages/supradough/README.md b/packages/supradough/README.md new file mode 100644 index 000000000..a8cfa84b3 --- /dev/null +++ b/packages/supradough/README.md @@ -0,0 +1,3 @@ +# supradough + +platform agnostic synth and sampler intended for live coding. a reimplementation of superdough. \ No newline at end of file diff --git a/packages/supradough/dough-export.mjs b/packages/supradough/dough-export.mjs new file mode 100644 index 000000000..bd4b530b3 --- /dev/null +++ b/packages/supradough/dough-export.mjs @@ -0,0 +1,123 @@ +// this is a poc of how a pattern can be rendered as a wav file using node +// run via: node dough-export.mjs +import fs from 'node:fs'; +import WavEncoder from 'wav-encoder'; +import { evalScope } from '@strudel/core'; +import { miniAllStrings } from '@strudel/mini'; +import { Dough } from './dough.mjs'; + +await evalScope( + import('@strudel/core'), + import('@strudel/mini'), + import('@strudel/tonal'), + // import('@strudel/tonal'), +); + +miniAllStrings(); // allows using single quotes for mini notation / skip transpilation + +let sampleRate = 48000, + cps = 0.4; + +/* await doughsamples('github:eddyflux/crate'); +await doughsamples('github:eddyflux/wax'); */ + +let pat = note('c,eb,g,') + .s('sine') + .press() + .add(note(24)) + .fmi(3) + .fmh(5.01) + .dec(0.4) + .delay('.6:<.12 .22>:.8') + .jux(press) + .rarely(add(note('12'))) + .lpf(400) + .lpq(0.2) + .lpd(0.4) + .lpenv(3) + .fmdecay(0.4) + .fmenv(1) + .postgain(0.6) + .stack(s('*8').dec(0.07).rarely(ply('2')).delay(0.5).hpf(sine.range(200, 2000).slow(4)).hpq(0.2)) + .stack( + s('[- white@3]*2') + .dec(0.4) + .hpf('<2000!3 <4000 8000>>*4') + .hpq(0.6) + .ply('<1 2>*4') + .postgain(0.5) + .delay(0.5) + .jux(rev) + .lpf(5000), + ) + .stack( + note('*2') + .s('square') + .lpf(sine.range(100, 300).slow(4)) + .lpe(1) + .segment(8) + .lpd(0.3) + .lpq(0.2) + .dec(0.2) + .speed('<1 2>') + .ply('<1 2>') + .postgain(1), + ) + .stack( + chord('') + .voicing() + .s('') + .clip(1) + .rel(0.4) + .vib('4:.2') + .gain(0.7) + .hpf(1200) + .fm(0.5) + .att(1) + .lpa(0.5) + .lpf(200) + .lpenv(4) + .chorus(0.8), + ) + .slow(1 / cps); + +let cycles = 30; +let seconds = cycles + 1; // 1s release tail +const haps = pat.queryArc(0, cycles); + +const dough = new Dough(sampleRate); + +console.log('spawn voices...'); +haps.forEach((hap) => { + hap.value._begin = Number(hap.whole.begin); + hap.value._duration = hap.duration /* / cps */; + dough.scheduleSpawn(hap.value); +}); +console.log(`render ${seconds}s long buffer, each dot is 1 second:`); +const buffers = [new Float32Array(seconds * sampleRate), new Float32Array(seconds * sampleRate)]; +let t = performance.now(); +while (dough.t <= buffers[0].length) { + dough.update(); + buffers[0][dough.t] = dough.out[0]; + buffers[1][dough.t] = dough.out[1]; + if (dough.t % sampleRate === 0) { + process.stdout.write('.'); + } +} +const took = (performance.now() - t) / 1000; +const load = (took / seconds) * 100; +const speed = (seconds / took).toFixed(2); +console.log(''); +console.log(`done! +rendered ${seconds}s in ${took.toFixed(2)}s +speed: ${speed}x +load: ${load.toFixed(2)}%`); + +const patternAudio = { + sampleRate, + channelData: buffers, +}; + +WavEncoder.encode(patternAudio).then((buffer) => { + fs.writeFileSync('pattern.wav', new Float32Array(buffer)); +}); diff --git a/packages/supradough/dough-worklet.mjs b/packages/supradough/dough-worklet.mjs new file mode 100644 index 000000000..18c316b2d --- /dev/null +++ b/packages/supradough/dough-worklet.mjs @@ -0,0 +1,39 @@ +import { Dough } from './dough.mjs'; + +const clamp = (num, min, max) => Math.min(Math.max(num, min), max); + +class DoughProcessor extends AudioWorkletProcessor { + constructor() { + super(); + this.dough = new Dough(sampleRate, currentTime); + this.port.onmessage = (event) => { + if (event.data.spawn) { + this.dough.scheduleSpawn(event.data.spawn); + } else if (event.data.sample) { + this.dough.loadSample(event.data.sample, event.data.channels, event.data.sampleRate); + } else if (event.data.samples) { + event.data.samples.forEach(([name, channels, sampleRate]) => { + this.dough.loadSample(name, channels, sampleRate); + }); + } else { + console.log('unrecognized event type', event.data); + } + }; + } + process(inputs, outputs, params) { + if (this.disconnected) { + return false; + } + const output = outputs[0]; + for (let i = 0; i < output[0].length; i++) { + this.dough.update(); + for (let c = 0; c < output.length; c++) { + //prevent speaker blowout via clipping if threshold exceeds + output[c][i] = clamp(this.dough.out[c], -1, 1); + } + } + return true; // keep the audio processing going + } +} + +registerProcessor('dough-processor', DoughProcessor); diff --git a/packages/supradough/dough.mjs b/packages/supradough/dough.mjs new file mode 100644 index 000000000..1cb9241ad --- /dev/null +++ b/packages/supradough/dough.mjs @@ -0,0 +1,1116 @@ +// this is dough, the superdough without dependencies +// @ts-check +// @ts-ignore ignore next line because sampleRate is unknown +const SAMPLE_RATE = typeof sampleRate !== 'undefined' ? sampleRate : 48000; +const PI_DIV_SR = Math.PI / SAMPLE_RATE; +const ISR = 1 / SAMPLE_RATE; + +let gainCurveFunc = (val) => Math.pow(val, 2); + +function applyGainCurve(val) { + return gainCurveFunc(val); +} + +/** + * Equal Power Crossfade function. + * Smoothly transitions between signals A and B, maintaining consistent perceived loudness. + * + * @param {number} a - Signal A (can be a single value or an array value in buffer processing). + * @param {number} b - Signal B (can be a single value or an array value in buffer processing). + * @param {number} m - Crossfade parameter (0.0 = all A, 1.0 = all B, 0.5 = equal mix). + * @returns {number} Crossfaded output value. + */ +function crossfade(a, b, m) { + const aGain = Math.sin((1 - m) * 0.5 * Math.PI); + const bGain = Math.sin(m * 0.5 * Math.PI); + return a * aGain + b * bGain; +} + +// function setGainCurve(newGainCurveFunc) { +// gainCurveFunc = newGainCurveFunc; +// } +// https://garten.salat.dev/audio-DSP/oscillators.html +export class SineOsc { + phase = 0; + update(freq) { + const value = Math.sin(this.phase * 2 * Math.PI); + this.phase = (this.phase + freq / SAMPLE_RATE) % 1; + return value; + } +} + +export class ZawOsc { + phase = 0; + update(freq) { + this.phase += ISR * freq; + return (this.phase % 1) * 2 - 1; + } +} + +function polyBlep(t, dt) { + // 0 <= t < 1 + if (t < dt) { + t /= dt; + // 2 * (t - t^2/2 - 0.5) + return t + t - t * t - 1; + } + // -1 < t < 0 + if (t > 1 - dt) { + t = (t - 1) / dt; + // 2 * (t^2/2 + t + 0.5) + return t * t + t + t + 1; + } + // 0 otherwise + return 0; +} + +export class SawOsc { + constructor(props = {}) { + this.phase = props.phase ?? 0; + } + update(freq) { + const dt = freq / SAMPLE_RATE; + let p = polyBlep(this.phase, dt); + let s = 2 * this.phase - 1 - p; + this.phase += dt; + if (this.phase > 1) { + this.phase -= 1; + } + return s; + } +} + +function getUnisonDetune(unison, detune, voiceIndex) { + if (unison < 2) { + return 0; + } + const lerp = (a, b, n) => { + return n * (b - a) + a; + }; + return lerp(-detune * 0.5, detune * 0.5, voiceIndex / (unison - 1)); +} +function applySemitoneDetuneToFrequency(frequency, detune) { + return frequency * Math.pow(2, detune / 12); +} +export class SupersawOsc { + constructor(props = {}) { + //TODO: figure out a good way to pass in these params + this.voices = props.voices ?? 5; + this.freqspread = props.freqspread ?? 0.2; + this.panspread = props.panspread ?? 0.4; + this.phase = new Float32Array(this.voices).map(() => Math.random()); + } + update(freq) { + const gain1 = Math.sqrt(1 - this.panspread); + const gain2 = Math.sqrt(this.panspread); + let sl = 0; + let sr = 0; + for (let n = 0; n < this.voices; n++) { + const freqAdjusted = applySemitoneDetuneToFrequency(freq, getUnisonDetune(this.voices, this.freqspread, n)); + const dt = freqAdjusted / SAMPLE_RATE; + const isOdd = (n & 1) == 1; + let gainL = gain1; + let gainR = gain2; + // invert right and left gain + if (isOdd) { + gainL = gain2; + gainR = gain1; + } + let p = polyBlep(this.phase[n], dt); + let s = 2 * this.phase[n] - 1 - p; + sl = sl + s * gainL; + sr = sr + s * gainL; + + this.phase[n] += dt; + if (this.phase[n] > 1) { + this.phase[n] -= 1; + } + } + + return sl + sr; + //TODO: make stereo + // return [sl, sr]; + } +} + +export class TriOsc { + phase = 0; + update(freq) { + this.phase += ISR * freq; + let phase = this.phase % 1; + let value = phase < 0.5 ? 2 * phase : 1 - 2 * (phase - 0.5); + return value * 2 - 1; + } +} + +export class TwoPoleFilter { + s0 = 0; + s1 = 0; + update(s, cutoff, resonance = 0) { + // Out of bound values can produce NaNs + resonance = Math.max(resonance, 0); + + cutoff = Math.min(cutoff, 20000); + const c = 2 * Math.sin(cutoff * PI_DIV_SR); + + const r = Math.pow(0.5, (resonance + 0.125) / 0.125); + const mrc = 1 - r * c; + + this.s0 = mrc * this.s0 - c * this.s1 + c * s; // bpf + this.s1 = mrc * this.s1 + c * this.s0; // lpf + return this.s1; // return lpf by default + } +} + +class PulseOsc { + constructor(phase = 0) { + this.phase = phase; + } + saw(offset, dt) { + let phase = (this.phase + offset) % 1; + let p = polyBlep(phase, dt); + return 2 * phase - 1 - p; + } + update(freq, pw = 0.5) { + const dt = freq / SAMPLE_RATE; + let pulse = this.saw(0, dt) - this.saw(pw, dt); + this.phase = (this.phase + dt) % 1; + return pulse + pw * 2 - 1; + } +} + +// non bandlimited (has aliasing) +export class PulzeOsc { + phase = 0; + update(freq, duty = 0.5) { + this.phase += ISR * freq; + let cyclePos = this.phase % 1; + return cyclePos < duty ? 1 : -1; + } +} + +export class Dust { + update = (density) => (Math.random() < density * ISR ? Math.random() : 0); +} + +export class WhiteNoise { + update() { + return Math.random() * 2 - 1; + } +} + +export class BrownNoise { + constructor() { + this.out = 0; + } + update() { + let white = Math.random() * 2 - 1; + this.out = (this.out + 0.02 * white) / 1.02; + return this.out; + } +} + +export class PinkNoise { + constructor() { + this.b0 = 0; + this.b1 = 0; + this.b2 = 0; + this.b3 = 0; + this.b4 = 0; + this.b5 = 0; + this.b6 = 0; + } + + update() { + const white = Math.random() * 2 - 1; + + this.b0 = 0.99886 * this.b0 + white * 0.0555179; + this.b1 = 0.99332 * this.b1 + white * 0.0750759; + this.b2 = 0.969 * this.b2 + white * 0.153852; + this.b3 = 0.8665 * this.b3 + white * 0.3104856; + this.b4 = 0.55 * this.b4 + white * 0.5329522; + this.b5 = -0.7616 * this.b5 - white * 0.016898; + + const pink = this.b0 + this.b1 + this.b2 + this.b3 + this.b4 + this.b5 + this.b6 + white * 0.5362; + this.b6 = white * 0.115926; + + return pink * 0.11; + } +} + +export class Impulse { + phase = 1; + update(freq) { + this.phase += ISR * freq; + let v = this.phase >= 1 ? 1 : 0; + this.phase = this.phase % 1; + return v; + } +} + +export class ClockDiv { + inSgn = true; + outSgn = true; + clockCnt = 0; + update(clock, factor) { + let curSgn = clock > 0; + if (this.inSgn != curSgn) { + this.clockCnt++; + if (this.clockCnt >= factor) { + this.clockCnt = 0; + this.outSgn = !this.outSgn; + } + } + + this.inSgn = curSgn; + return this.outSgn ? 1 : -1; + } +} + +export class Hold { + value = 0; + trigSgn = false; + update(input, trig) { + if (!this.trigSgn && trig > 0) this.value = input; + this.trigSgn = trig > 0; + return this.value; + } +} + +function lerp(x, y0, y1, exponent = 1) { + if (x <= 0) return y0; + if (x >= 1) return y1; + + let curvedX; + + if (exponent === 0) { + curvedX = x; // linear + } else if (exponent > 0) { + curvedX = Math.pow(x, exponent); // ease-in + } else { + curvedX = 1 - Math.pow(1 - x, -exponent); // ease-out + } + + return y0 + (y1 - y0) * curvedX; +} + +export class ADSR { + constructor(props = {}) { + this.state = 'off'; + this.startTime = 0; + this.startVal = 0; + this.decayCurve = props.decayCurve ?? 1; + } + + update(curTime, gate, attack, decay, susVal, release) { + switch (this.state) { + case 'off': { + if (gate > 0) { + this.state = 'attack'; + this.startTime = curTime; + this.startVal = 0; + } + return 0; + } + case 'attack': { + let time = curTime - this.startTime; + if (time > attack) { + this.state = 'decay'; + this.startTime = curTime; + return 1; + } + return lerp(time / attack, this.startVal, 1, 1); + } + case 'decay': { + let time = curTime - this.startTime; + let curVal = lerp(time / decay, 1, susVal, -this.decayCurve); + if (gate <= 0) { + this.state = 'release'; + this.startTime = curTime; + this.startVal = curVal; + return curVal; + } + if (time > decay) { + this.state = 'sustain'; + this.startTime = curTime; + return susVal; + } + return curVal; + } + case 'sustain': { + if (gate <= 0) { + this.state = 'release'; + this.startTime = curTime; + this.startVal = susVal; + } + return susVal; + } + case 'release': { + let time = curTime - this.startTime; + + if (time > release) { + this.state = 'off'; + return 0; + } + let curVal = lerp(time / release, this.startVal, 0, -this.decayCurve); + if (gate > 0) { + this.state = 'attack'; + this.startTime = curTime; + this.startVal = curVal; + } + return curVal; + } + } + throw 'invalid envelope state'; + } +} + +/* + impulse(1).ad(.1).mul(sine(200)) +.add(x=>x.delay(.1).mul(.8)) +.out()*/ +const MAX_DELAY_TIME = 10; +export class PitchDelay { + lpf = new TwoPoleFilter(); + constructor(_props = {}) { + this.buffer = new Float32Array(MAX_DELAY_TIME * SAMPLE_RATE); + this.writeIdx = 0; + this.readIdx = 0; + this.numSamples = 0; + } + write(s, delayTime) { + // Calculate how far in the past to read + this.numSamples = Math.min(Math.floor(SAMPLE_RATE * delayTime), this.buffer.length - 1); + this.writeIdx = (this.writeIdx + 1) % this.numSamples; + this.buffer[this.writeIdx] = s; + this.readIdx = this.writeIdx - this.numSamples + 1; + + // If past the start of the buffer, wrap around (Q: is this possible?) + if (this.readIdx < 0) this.readIdx += this.numSamples; + } + update(input, delayTime, speed = 1) { + this.write(input, delayTime); + let index = this.readIdx; + if (speed < 0) { + index = this.numSamples - Math.floor(Math.abs(this.readIdx * speed) % this.numSamples); + } else { + index = Math.floor(this.readIdx * speed) % this.numSamples; + } + const s = this.lpf.update(this.buffer[index], 0.9, 0); + + return s; + } +} + +export class Delay { + writeIdx = 0; + readIdx = 0; + buffer = new Float32Array(MAX_DELAY_TIME * SAMPLE_RATE); //.fill(0) + write(s, delayTime) { + this.writeIdx = (this.writeIdx + 1) % this.buffer.length; + this.buffer[this.writeIdx] = s; + // Calculate how far in the past to read + let numSamples = Math.min(Math.floor(SAMPLE_RATE * delayTime), this.buffer.length - 1); + this.readIdx = this.writeIdx - numSamples; + // If past the start of the buffer, wrap around + if (this.readIdx < 0) this.readIdx += this.buffer.length; + } + update(input, delayTime) { + this.write(input, delayTime); + return this.buffer[this.readIdx]; + } +} +//TODO: Figure out why clicking at the start off the buffer +export class Chorus { + delay = new Delay(); + modulator = new TriOsc(); + update(input, mix, delayTime, modulationFreq, modulationDepth) { + const m = this.modulator.update(modulationFreq) * modulationDepth; + const c = this.delay.update(input, delayTime * (1 + m)); + return crossfade(input, c, mix); + } +} + +export class Fold { + update(input = 0, rate = 0) { + if (rate < 0) rate = 0; + rate = rate + 1; + input = input * rate; + return 4 * (Math.abs(0.25 * input + 0.25 - Math.round(0.25 * input + 0.25)) - 0.25); + } +} + +export class Lag { + lagUnit = 4410; + s = 0; + update(input, rate) { + // Remap so the useful range is around [0, 1] + rate = rate * this.lagUnit; + if (rate < 1) rate = 1; + this.s += (1 / rate) * (input - this.s); + return this.s; + } +} + +export class Slew { + last = 0; + update(input, up, dn) { + const upStep = up * ISR; + const downStep = dn * ISR; + let delta = input - this.last; + if (delta > upStep) { + delta = upStep; + } else if (delta < -downStep) { + delta = -downStep; + } + this.last += delta; + return this.last; + } +} + +// overdrive style distortion (adapted from noisecraft) currently unused +export function applyDistortion(x, amount) { + amount = Math.min(Math.max(amount, 0), 1); + amount -= 0.01; + var k = (2 * amount) / (1 - amount); + var y = ((1 + k) * x) / (1 + k * Math.abs(x)); + return y; +} + +export class Sequence { + clockSgn = true; + step = 0; + first = true; + update(clock, ...ins) { + if (!this.clockSgn && clock > 0) { + this.step = (this.step + 1) % ins.length; + this.clockSgn = clock > 0; + return 0; // set first sample to zero to retrigger gates on step change... + } + this.clockSgn = clock > 0; + return ins[this.step]; + } +} + +// sample rate bit crusher +export class Coarse { + hold = 0; + t = 0; + update(input, coarse) { + if (this.t++ % coarse === 0) { + this.t = 0; + this.hold = input; + } + return this.hold; + } +} + +// amplitude bit crusher +export class Crush { + update(input, crush) { + crush = Math.max(1, crush); + const x = Math.pow(2, crush - 1); + return Math.round(input * x) / x; + } +} + +// this is the distort from superdough +export class Distort { + update(input, distort = 0, postgain = 1) { + postgain = Math.max(0.001, Math.min(1, postgain)); + const shape = Math.expm1(distort); + return (((1 + shape) * input) / (1 + shape * Math.abs(input))) * postgain; + } +} +// distortion could be expressed as a function, because it's stateless + +export class BufferPlayer { + static samples = new Map(); // string -> { channels, sampleRate } + buffer; // Float32Array + sampleRate; + pos = 0; + sampleFreq = note2freq(); + constructor(buffer, sampleRate, normalize) { + this.buffer = buffer; + this.sampleRate = sampleRate; + this.duration = this.buffer.length / this.sampleRate; + this.speed = SAMPLE_RATE / this.sampleRate; + if (normalize) { + // this will make the buffer last 1s if freq = sampleFreq + // it's useful to loop samples (e.g. fit function) + this.speed *= this.duration; + } + } + update(freq) { + if (this.pos >= this.buffer.length) { + return 0; + } + const speed = (freq / this.sampleFreq) * this.speed; + let s = this.buffer[Math.floor(this.pos)]; + this.pos = this.pos + speed; + return s; + } +} + +export function _rangex(sig, min, max) { + let logmin = Math.log(min); + let range = Math.log(max) - logmin; + const unipolar = (sig + 1) / 2; + return Math.exp(unipolar * range + logmin); +} + +// duplicate +export const getADSR = (params, curve = 'linear', defaultValues) => { + const envmin = curve === 'exponential' ? 0.001 : 0.001; + const releaseMin = 0.01; + const envmax = 1; + const [a, d, s, r] = params; + if (a == null && d == null && s == null && r == null) { + return defaultValues ?? [envmin, envmin, envmax, releaseMin]; + } + const sustain = s != null ? s : (a != null && d == null) || (a == null && d == null) ? envmax : envmin; + return [Math.max(a ?? 0, envmin), Math.max(d ?? 0, envmin), Math.min(sustain, envmax), Math.max(r ?? 0, releaseMin)]; +}; + +let shapes = { + sine: SineOsc, + saw: SawOsc, + zaw: ZawOsc, + sawtooth: SawOsc, + zawtooth: ZawOsc, + supersaw: SupersawOsc, + tri: TriOsc, + triangle: TriOsc, + pulse: PulseOsc, + square: PulseOsc, + pulze: PulzeOsc, + dust: Dust, + crackle: Dust, + impulse: Impulse, + white: WhiteNoise, + brown: BrownNoise, + pink: PinkNoise, +}; + +const defaultDefaultValues = { + chorus: 0, + note: 48, + s: 'triangle', + bank: '', + gain: 1, + postgain: 1, + velocity: 1, + density: '.03', + ftype: '12db', + fanchor: 0, + //resonance: 1, // superdough resonance is scaled differently + resonance: 0, + //hresonance: 1, // superdough resonance is scaled differently + hresonance: 0, + // bandq: 1, // superdough resonance is scaled differently + bandq: 0, + channels: [1, 2], + phaserdepth: 0.75, + shapevol: 1, + distortvol: 1, + delay: 0, + byteBeatExpression: '0', + delayfeedback: 0.5, + delayspeed: 1, + delaytime: 0.25, + orbit: 1, + i: 1, + fft: 8, + z: 'triangle', + pan: 0.5, + fmh: 1, + fmenv: 0, // differs from superdough + speed: 1, + pw: 0.5, +}; + +let getDefaultValue = (key) => defaultDefaultValues[key]; + +const chromas = { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 }; +const accs = { '#': 1, b: -1, s: 1, f: -1 }; +const note2midi = (note, defaultOctave = 3) => { + let [pc, acc = '', oct = ''] = + String(note) + .match(/^([a-gA-G])([#bsf]*)([0-9]*)$/) + ?.slice(1) || []; + if (!pc) { + throw new Error('not a note: "' + note + '"'); + } + const chroma = chromas[pc.toLowerCase()]; + const offset = acc?.split('').reduce((o, char) => o + accs[char], 0) || 0; + const octave = Number(oct || defaultOctave); + return (octave + 1) * 12 + chroma + offset; +}; +const midi2freq = (midi) => Math.pow(2, (midi - 69) / 12) * 440; +const note2freq = (note) => { + note = note || getDefaultValue('note'); + if (typeof note === 'string') { + note = note2midi(note, 3); // e.g. c3 => 48 + } + return midi2freq(note); +}; + +export class DoughVoice { + /** @type {number} */ + id = 0; + /** @type {number[]} */ + out = [0, 0]; + + /** @type {number | undefined} */ + attack; + /** @type {number | undefined} */ + decay; + /** @type {number | undefined} */ + sustain; + /** @type {number} */ + release; + /** @type {number} */ + _begin; + /** @type {number} */ + _duration; + + /** @type {any} */ + _sound; + /** @type {number} */ + _channels = 1; + /** @type {BufferPlayer[] | undefined} */ + _buffers; + /** @type {string | undefined} */ + unit; + + /** @type {ADSR | undefined} */ + _penv; + /** @type {number | undefined} */ + penv; + /** @type {number | undefined} */ + pattack; + /** @type {number | undefined} */ + pdecay; + /** @type {number | undefined} */ + psustain; + /** @type {number | undefined} */ + prelease; + + /** @type {number | undefined} */ + vib; + + _vib; + /** @type {number | undefined} */ + vibmod; + + /** @type {SineOsc | undefined} */ + _fm; + /** @type {number | undefined} */ + fmh; + /** @type {number | undefined} */ + fmi; + + /** @type {ADSR | undefined} */ + _fmenv; + /** @type {number | undefined} */ + fmattack; + /** @type {number | undefined} */ + fmdecay; + /** @type {number | undefined} */ + fmsustain; + /** @type {number | undefined} */ + fmrelease; + + /** @type {ADSR | undefined} */ + _lpenv; + lpenv; + /** @type {number | undefined} */ + lpattack; + /** @type {number | undefined} */ + lpdecay; + /** @type {number | undefined} */ + lpsustain; + /** @type {number | undefined} */ + lprelease; + + /** @type {ADSR | undefined} */ + _hpenv; + /** @type {number | undefined} */ + hpenv; + /** @type {number | undefined} */ + hpattack; + /** @type {number | undefined} */ + hpdecay; + /** @type {number | undefined} */ + hpsustain; + /** @type {number | undefined} */ + hprelease; + + /** @type {ADSR | undefined} */ + _bpenv; + /** @type {number | undefined} */ + bpenv; + /** @type {number | undefined} */ + bpattack; + /** @type {number | undefined} */ + bpdecay; + /** @type {number | undefined} */ + bpsustain; + /** @type {number | undefined} */ + bprelease; + + /** @type {number | undefined} */ + cutoff; + /** @type {number | undefined} */ + hcutoff; + /** @type {number | undefined} */ + bandf; + /** @type {number | undefined} */ + coarse; + /** @type {number | undefined} */ + crush; + /** @type {number | undefined} */ + distort; + + /** @type {number} */ + freq; + /** @type {string | undefined} */ + note; + + /** @type {TwoPoleFilter[] | null | undefined} */ + _lpf; + /** @type {TwoPoleFilter[] | null | undefined} */ + _hpf; + /** @type {TwoPoleFilter[] | null | undefined} */ + _bpf; + /** @type {Chorus[] | null | undefined} */ + _chorus; + /** @type {Coarse[] | null | undefined} */ + _coarse; + /** @type {Crush[] | null | undefined} */ + _crush; + /** @type {Distort[] | null | undefined} */ + _distort; + + /** + * @param {DoughVoice} value + */ + constructor(value) { + // mandatory controls + this.freq ??= note2freq(value.note); + this._begin = value._begin; + this._duration = value._duration; + this.release = value.release ?? 0; + // the rest.. we use $ for readability + let $ = this; + Object.assign($, value); + $.s = $.s ?? getDefaultValue('s'); + $.gain = applyGainCurve($.gain ?? getDefaultValue('gain')); + $.velocity = applyGainCurve($.velocity ?? getDefaultValue('velocity')); + $.postgain = applyGainCurve($.postgain ?? getDefaultValue('postgain')); + $.density = $.density ?? getDefaultValue('density'); + $.fanchor = $.fanchor ?? getDefaultValue('fanchor'); + $.drive = $.drive ?? 0.69; + $.phaserdepth = $.phaserdepth ?? getDefaultValue('phaserdepth'); + $.shapevol = applyGainCurve($.shapevol ?? getDefaultValue('shapevol')); + $.distortvol = applyGainCurve($.distortvol ?? getDefaultValue('distortvol')); + $.i = $.i ?? getDefaultValue('i'); + $.chorus = $.chorus ?? getDefaultValue('chorus'); + $.fft = $.fft ?? getDefaultValue('fft'); + $.pan = $.pan ?? getDefaultValue('pan'); + $.orbit = $.orbit ?? getDefaultValue('orbit'); + $.fmenv = $.fmenv ?? getDefaultValue('fmenv'); + $.resonance = $.resonance ?? getDefaultValue('resonance'); + $.hresonance = $.hresonance ?? getDefaultValue('hresonance'); + $.bandq = $.bandq ?? getDefaultValue('bandq'); + $.speed = $.speed ?? getDefaultValue('speed'); + $.pw = $.pw ?? getDefaultValue('pw'); + + [$.attack, $.decay, $.sustain, $.release] = getADSR([$.attack, $.decay, $.sustain, $.release]); + + $._holdEnd = $._begin + $._duration; // needed for gate + $._end = $._holdEnd + $.release + 0.01; // needed for despawn + + if ($.fmi && ($.s === 'saw' || $.s === 'sawtooth')) { + $.s = 'zaw'; // polyblepped saw when fm is applied + } + + if (shapes[$.s]) { + const SourceClass = shapes[$.s]; + $._sound = new SourceClass(); + $._channels = 1; + } else if (BufferPlayer.samples.has($.s)) { + const sample = BufferPlayer.samples.get($.s); + $._buffers = []; + $._channels = sample.channels.length; + for (let i = 0; i < $._channels; i++) { + $._buffers.push(new BufferPlayer(sample.channels[i], sample.sampleRate, $.unit === 'c')); // tbd unit === 'c' + } + } else { + console.warn('sound not loaded', $.s); + } + + if ($.penv) { + $._penv = new ADSR({ decayCurve: 4 }); + [$.pattack, $.pdecay, $.psustain, $.prelease] = getADSR([$.pattack, $.pdecay, $.psustain, $.prelease]); + } + + if ($.vib) { + $._vib = new SineOsc(); + $.vibmod = $.vibmod ?? getDefaultValue('vibmod'); + } + + if ($.fmi) { + $._fm = new SineOsc(); + $.fmh = $.fmh ?? getDefaultValue('fmh'); + if ($.fmenv) { + $._fmenv = new ADSR({ decayCurve: 2 }); + [$.fmattack, $.fmdecay, $.fmsustain, $.fmrelease] = getADSR([$.fmattack, $.fmdecay, $.fmsustain, $.fmrelease]); + } + } + + // gain envelope + $._adsr = new ADSR({ decayCurve: 2 }); + // delay + $.delay = applyGainCurve($.delay ?? getDefaultValue('delay')); + $.delayfeedback = $.delayfeedback ?? getDefaultValue('delayfeedback'); + $.delayspeed = $.delayspeed ?? getDefaultValue('delayspeed'); + $.delaytime = $.delaytime ?? getDefaultValue('delaytime'); + + // filter setup + if ($.lpenv) { + $._lpenv = new ADSR({ decayCurve: 4 }); + [$.lpattack, $.lpdecay, $.lpsustain, $.lprelease] = getADSR([$.lpattack, $.lpdecay, $.lpsustain, $.lprelease]); + } + if ($.hpenv) { + $._hpenv = new ADSR({ decayCurve: 4 }); + [$.hpattack, $.hpdecay, $.hpsustain, $.hprelease] = getADSR([$.hpattack, $.hpdecay, $.hpsustain, $.hprelease]); + } + if ($.bpenv) { + $._bpenv = new ADSR({ decayCurve: 4 }); + [$.bpattack, $.bpdecay, $.bpsustain, $.bprelease] = getADSR([$.bpattack, $.bpdecay, $.bpsustain, $.bprelease]); + } + + // channelwise effects setup + $._chorus = $.chorus ? [] : null; + $._lpf = $.cutoff ? [] : null; + $._hpf = $.hcutoff ? [] : null; + $._bpf = $.bandf ? [] : null; + $._coarse = $.coarse ? [] : null; + $._crush = $.crush ? [] : null; + $._distort = $.distort ? [] : null; + for (let i = 0; i < this._channels; i++) { + $._lpf?.push(new TwoPoleFilter()); + $._hpf?.push(new TwoPoleFilter()); + $._bpf?.push(new TwoPoleFilter()); + $._chorus?.push(new Chorus()); + $._coarse?.push(new Coarse()); + $._crush?.push(new Crush()); + $._distort?.push(new Distort()); + } + } + update(t) { + if (!this._sound && !this._buffers) { + return 0; + } + let gate = Number(t >= this._begin && t <= this._holdEnd); + + let freq = this.freq * this.speed; + + // frequency modulation + if (this._fm && this.fmh !== undefined && this.fmi !== undefined) { + let fmi = this.fmi; + if (this._fmenv) { + const env = this._fmenv.update(t, gate, this.fmattack, this.fmdecay, this.fmsustain, this.fmrelease); + fmi = this.fmenv * env * fmi; + } + const modfreq = freq * this.fmh; + const modgain = modfreq * fmi; + freq = freq + this._fm.update(modfreq) * modgain; + } + + // vibrato + if (this._vib && this.vibmod !== undefined) { + freq = freq * 2 ** ((this._vib.update(this.vib) * this.vibmod) / 12); + } + + // pitch envelope + if (this._penv && this.penv !== undefined) { + const env = this._penv.update(t, gate, this.pattack, this.pdecay, this.psustain, this.prelease); + freq = freq + env * this.penv; + } + + // filters + let lpf = this.cutoff; + if (lpf !== undefined && this._lpenv) { + const env = this._lpenv.update(t, gate, this.lpattack, this.lpdecay, this.lpsustain, this.lprelease); + lpf = this.lpenv * env * lpf + lpf; + } + let hpf = this.hcutoff; + if (hpf !== undefined && this._hpenv && this.hpenv !== undefined) { + const env = this._hpenv.update(t, gate, this.hpattack, this.hpdecay, this.hpsustain, this.hprelease); + hpf = 2 ** this.hpenv * env * hpf + hpf; + } + let bpf = this.bandf; + if (bpf !== undefined && this._bpenv && this.bpenv !== undefined) { + const env = this._bpenv.update(t, gate, this.bpattack, this.bpdecay, this.bpsustain, this.bprelease); + bpf = 2 ** this.bpenv * env * bpf + bpf; + } + // gain envelope + const env = this._adsr.update(t, gate, this.attack, this.decay, this.sustain, this.release); + + // channelwise dsp + for (let i = 0; i < this._channels; i++) { + // sound source + if (this._sound && this.s === 'pulse') { + this.out[i] = this._sound.update(freq, this.pw); + } else if (this._sound) { + this.out[i] = this._sound.update(freq); + } else if (this._buffers) { + this.out[i] = this._buffers[i].update(freq); + } + this.out[i] = this.out[i] * this.gain * this.velocity; + if (this._chorus) { + const c = this._chorus[i].update(this.out[i], this.chorus, 0.03 + 0.05 * i, 1, 0.11); + this.out[i] = c + this.out[i]; + } + + if (this._lpf) { + this._lpf[i].update(this.out[i], lpf, this.resonance); + this.out[i] = this._lpf[i].s1; + } + if (this._hpf) { + this._hpf[i].update(this.out[i], hpf, this.hresonance); + this.out[i] = this.out[i] - this._hpf[i].s1; + } + if (this._bpf) { + this._bpf[i].update(this.out[i], bpf, this.bandq); + this.out[i] = this._bpf[i].s0; + } + if (this._coarse) { + this.out[i] = this._coarse[i].update(this.out[i], this.coarse); + } + if (this._crush) { + this.out[i] = this._crush[i].update(this.out[i], this.crush); + } + if (this._distort) { + this.out[i] = this._distort[i].update(this.out[i], this.distort, this.distortvol); + } + this.out[i] = this.out[i] * env; + this.out[i] = this.out[i] * this.postgain; + if (!this._buffers) { + this.out[i] = this.out[i] * 0.2; // turn down waveform + } + } + if (this._channels === 1) { + this.out[1] = this.out[0]; + } + if (this.pan !== 0.5) { + const panpos = (this.pan * Math.PI) / 2; + this.out[0] = this.out[0] * Math.cos(panpos); + this.out[1] = this.out[1] * Math.sin(panpos); + } + } +} + +// this class is the interface to the "outer world" +// it handles spawning and despawning of DoughVoice's +export class Dough { + voices = []; // DoughVoice[] + vid = 0; + q = []; + out = [0, 0]; + delaysend = [0, 0]; + delaytime = getDefaultValue('delaytime'); + delayfeedback = getDefaultValue('delayfeedback'); + delayspeed = getDefaultValue('delayspeed'); + t = 0; + // sampleRate: number, currentTime: number (seconds) + constructor(sampleRate = 48000, currentTime = 0) { + this.sampleRate = sampleRate; + this.t = Math.floor(currentTime * sampleRate); // samples + // console.log('init dough', this.sampleRate, this.t); + this._delayL = new Delay(); + this._delayR = new Delay(); + } + loadSample(name, channels, sampleRate) { + BufferPlayer.samples.set(name, { channels, sampleRate }); + } + scheduleSpawn(value) { + if (value._begin === undefined) { + throw new Error('[dough]: scheduleSpawn expected _begin to be set'); + } + if (value._duration === undefined) { + throw new Error('[dough]: scheduleSpawn expected _duration to be set'); + } + value.sampleRate = this.sampleRate; + // convert seconds to samples + const time = Math.floor(value._begin * this.sampleRate); // set from supradough.mjs + this.schedule({ time, type: 'spawn', arg: value }); + } + spawn(value) { + value.id = this.vid++; + const voice = new DoughVoice(value); + this.voices.push(voice); + // console.log('spawn', voice.id, 'voices:', this.voices.length); + // schedule removal + const endTime = Math.ceil(voice._end * this.sampleRate); + this.schedule({ time: endTime /* + 48000 */, type: 'despawn', arg: voice.id }); + } + despawn(vid) { + this.voices = this.voices.filter((v) => v.id !== vid); + // console.log('despawn', vid, 'voices:', this.voices.length); + } + // schedules a function call with a single argument + // msg = {time:number,type:string, arg: any} + // the Dough method "type" will be called with "arg" at "time" + schedule(msg) { + if (!this.q.length) { + // if empty, just push + this.q.push(msg); + return; + } + // not empty + // find index where msg.time fits in + let i = 0; + while (i < this.q.length && this.q[i].time < msg.time) { + i++; + } + // this ensures q stays sorted by time, so we only need to check q[0] + this.q.splice(i, 0, msg); + } + // maybe update should be called once per block instead for perf reasons? + update() { + // go over q + while (this.q.length > 0 && this.q[0].time <= this.t) { + // console.log('schedule', this.q[0]); + // trigger due messages. q is sorted, so we only need to check q[0] + this[this.q[0].type](this.q[0].arg); // type is expected to be a Dough method + this.q.shift(); + } + // add active voices + this.out[0] = 0; + this.out[1] = 0; + for (let v = 0; v < this.voices.length; v++) { + this.voices[v].update(this.t / this.sampleRate); + this.out[0] += this.voices[v].out[0]; + this.out[1] += this.voices[v].out[1]; + if (this.voices[v].delay) { + this.delaysend[0] += this.voices[v].out[0] * this.voices[v].delay; + this.delaysend[1] += this.voices[v].out[1] * this.voices[v].delay; + this.delaytime = this.voices[v].delaytime; // we trust that these are initialized in the voice + this.delayspeed = this.voices[v].delayspeed; // we trust that these are initialized in the voice + this.delayfeedback = this.voices[v].delayfeedback; + } + } + // todo: how to change delaytime / delayfeedback from a voice? + const delayL = this._delayL.update(this.delaysend[0], this.delaytime); + const delayR = this._delayR.update(this.delaysend[1], this.delaytime); + + this.delaysend[0] = delayL * this.delayfeedback; + this.delaysend[1] = delayR * this.delayfeedback; + this.out[0] += delayL; + this.out[1] += delayR; + this.t++; + } +} diff --git a/packages/supradough/index.mjs b/packages/supradough/index.mjs new file mode 100644 index 000000000..cb132fb3f --- /dev/null +++ b/packages/supradough/index.mjs @@ -0,0 +1,5 @@ +// import _workletUrl from './dough-worklet.mjs?url'; // only for dev (breaks for production build) +import _workletUrl from './dough-worklet.mjs?audioworklet'; // only for prod (breaks in development?!) + +export * from './dough.mjs'; +export const workletUrl = _workletUrl; diff --git a/packages/supradough/package.json b/packages/supradough/package.json new file mode 100644 index 000000000..7e465c0a9 --- /dev/null +++ b/packages/supradough/package.json @@ -0,0 +1,37 @@ +{ + "name": "supradough", + "version": "1.2.3", + "description": "platform agnostic synth and sampler intended for live coding. a reimplementation of superdough.", + "main": "index.mjs", + "type": "module", + "publishConfig": { + "main": "dist/index.mjs" + }, + "scripts": { + "build": "vite build", + "prepublishOnly": "npm run build" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/tidalcycles/strudel.git" + }, + "keywords": [ + "tidalcycles", + "strudel", + "pattern", + "livecoding", + "algorave" + ], + "author": "Felix Roos ", + "license": "AGPL-3.0-or-later", + "bugs": { + "url": "https://github.com/tidalcycles/strudel/issues" + }, + "homepage": "https://github.com/tidalcycles/strudel#readme", + "devDependencies": { + "vite": "^6.0.11", + "vite-plugin-bundle-audioworklet": "workspace:*", + "wav-encoder": "^1.3.0" + }, + "dependencies": {} +} diff --git a/packages/tonal/package.json b/packages/tonal/package.json index 614e86f74..1461bdc8e 100644 --- a/packages/tonal/package.json +++ b/packages/tonal/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/tonal", - "version": "1.2.2", + "version": "1.2.4", "description": "Tonal functions for strudel", "main": "index.mjs", "publishConfig": { diff --git a/packages/tonal/test/tonal.test.mjs b/packages/tonal/test/tonal.test.mjs index cd8b3c88b..5f61b05a9 100644 --- a/packages/tonal/test/tonal.test.mjs +++ b/packages/tonal/test/tonal.test.mjs @@ -7,80 +7,130 @@ This program is free software: you can redistribute it and/or modify it under th // import { strict as assert } from 'assert'; import '../tonal.mjs'; // need to import this to add prototypes -import { pure, n, seq, note } from '@strudel/core'; +import { pure, n, seq, note, noteToMidi } from '@strudel/core'; import { describe, it, expect } from 'vitest'; import { mini } from '../../mini/mini.mjs'; describe('tonal', () => { - it('Should run tonal functions ', () => { - expect(pure('c3').scale('C major').scaleTranspose(1).firstCycleValues).toEqual(['D3']); + describe('scaleTranspose', () => { + it('transposes notes by scale degrees', () => { + expect(pure('c3').scale('C major').scaleTranspose(1).firstCycleValues).toEqual(['D3']); + }); }); - it('scale with plain values', () => { - expect( - seq(0, 1, 2) - .scale('C major') - .note() - .firstCycleValues.map((h) => h.note), - ).toEqual(['C3', 'D3', 'E3']); + describe('scale', () => { + it('converts plain values', () => { + expect( + seq(0, 1, 2) + .scale('C major') + .note() + .firstCycleValues.map((h) => h.note), + ).toEqual(['C3', 'D3', 'E3']); + }); + it('converts n values', () => { + expect( + n(seq(0, 1, 2)) + .scale('C major') + .firstCycleValues.map((h) => h.note), + ).toEqual(['C3', 'D3', 'E3']); + }); + it('converts n values (mini notation)', () => { + expect( + n(seq(0, 1, 2)) + .scale('C:major') + .firstCycleValues.map((h) => h.note), + ).toEqual(['C3', 'D3', 'E3']); + }); + it('converts n values (no tonic)', () => { + expect( + n(seq(0, 1, 2)) + .scale('major') + .firstCycleValues.map((h) => h.note), + ).toEqual(['C3', 'D3', 'E3']); + }); + it('converts n values (explicit mini notation)', () => { + expect( + n(seq(0, 1, 2)) + .scale(mini('C:major')) + .firstCycleValues.map((h) => h.note), + ).toEqual(['C3', 'D3', 'E3']); + }); + it('converts decorated n values', () => { + expect( + n(seq('0b', '1#', '-2', '3##', '4bb')) + .scale('C major') + .firstCycleValues.map((h) => h.note), + ).toEqual(['B2', 'Eb3', 'A2', 'G3', 'F3']); + }); + it('produces silence for mixed sharps and flats', () => { + expect( + n(seq('0b#', '1#b', '2#b#')) + .scale('C major') + .firstCycleValues.map((h) => h.note), + ).toEqual([]); + }); + it('snaps notes (upwards) to scale', () => { + const inputNotes = ['Cb', 'Eb', 'G', 'A#', 'Bb']; + const expectedNotes = ['B2', 'E3', 'G3', 'B3', 'B3']; + + expect( + note(seq(inputNotes)) + .scale('C major') + .firstCycleValues.map((h) => h.note), + ).toEqual(expectedNotes); + }); + it('snaps notes to the correct octave', () => { + const inputNotes = ['Cb0', 'Eb4', 'G1', 'A#19', 'Bb8']; + const expectedNotes = ['B#-1', 'D#4', 'G#1', 'A#19', 'A#8']; + + expect( + note(seq(inputNotes)) + .scale('A# minor') // A#, B#, C#, D#, E#, F#, G# + .firstCycleValues.map((h) => h.note), + ).toEqual(expectedNotes); + }); + it('handles scale names provided with colons', () => { + const inputNotes = ['Cb', 'E', 'G', 'A#', 'Bb']; + const expectedNotes = ['A#2', 'D#3', 'G#3', 'A#3', 'A#3']; + + expect( + note(seq(inputNotes)) + .scale('F#:pentatonic') // F#, G#, A#, C#, and D# + .firstCycleValues.map((h) => h.note), + ).toEqual(expectedNotes); + }); }); - it('scale with n values', () => { - expect( - n(seq(0, 1, 2)) - .scale('C major') - .firstCycleValues.map((h) => h.note), - ).toEqual(['C3', 'D3', 'E3']); - }); - it('scale with colon', () => { - expect( - n(seq(0, 1, 2)) - .scale('C:major') - .firstCycleValues.map((h) => h.note), - ).toEqual(['C3', 'D3', 'E3']); - }); - it('scale without tonic', () => { - expect( - n(seq(0, 1, 2)) - .scale('major') - .firstCycleValues.map((h) => h.note), - ).toEqual(['C3', 'D3', 'E3']); - }); - it('scale with mininotation colon', () => { - expect( - n(seq(0, 1, 2)) - .scale(mini('C:major')) - .firstCycleValues.map((h) => h.note), - ).toEqual(['C3', 'D3', 'E3']); - }); - it('transposes note numbers with interval numbers', () => { - expect( - note(seq(40, 40, 40)) - .transpose(0, 1, 2) - .firstCycleValues.map((h) => h.note), - ).toEqual([40, 41, 42]); - expect(seq(40, 40, 40).transpose(0, 1, 2).firstCycleValues).toEqual([40, 41, 42]); - }); - it('transposes note numbers with interval strings', () => { - expect( - note(seq(40, 40, 40)) - .transpose('1P', '2M', '3m') - .firstCycleValues.map((h) => h.note), - ).toEqual([40, 42, 43]); - expect(seq(40, 40, 40).transpose('1P', '2M', '3m').firstCycleValues).toEqual([40, 42, 43]); - }); - it('transposes note strings with interval numbers', () => { - expect( - note(seq('c', 'c', 'c')) - .transpose(0, 1, 2) - .firstCycleValues.map((h) => h.note), - ).toEqual(['C', 'Db', 'D']); - expect(seq('c', 'c', 'c').transpose(0, 1, 2).firstCycleValues).toEqual(['C', 'Db', 'D']); - }); - it('transposes note strings with interval strings', () => { - expect( - note(seq('c', 'c', 'c')) - .transpose('1P', '2M', '3m') - .firstCycleValues.map((h) => h.note), - ).toEqual(['C', 'D', 'Eb']); - expect(seq('c', 'c', 'c').transpose('1P', '2M', '3m').firstCycleValues).toEqual(['C', 'D', 'Eb']); + describe('transpose', () => { + it('transposes note numbers with interval numbers', () => { + expect( + note(seq(40, 40, 40)) + .transpose(0, 1, 2) + .firstCycleValues.map((h) => h.note), + ).toEqual([40, 41, 42]); + expect(seq(40, 40, 40).transpose(0, 1, 2).firstCycleValues).toEqual([40, 41, 42]); + }); + it('transposes note numbers with interval strings', () => { + expect( + note(seq(40, 40, 40)) + .transpose('1P', '2M', '3m') + .firstCycleValues.map((h) => h.note), + ).toEqual([40, 42, 43]); + expect(seq(40, 40, 40).transpose('1P', '2M', '3m').firstCycleValues).toEqual([40, 42, 43]); + }); + it('transposes note strings with interval numbers', () => { + expect( + note(seq('c', 'c', 'c')) + .transpose(0, 1, 2) + .firstCycleValues.map((h) => h.note), + ).toEqual(['C', 'Db', 'D']); + expect(seq('c', 'c', 'c').transpose(0, 1, 2).firstCycleValues).toEqual(['C', 'Db', 'D']); + }); + it('transposes note strings with interval strings', () => { + expect( + note(seq('c', 'c', 'c')) + .transpose('1P', '2M', '3m') + .firstCycleValues.map((h) => h.note), + ).toEqual(['C', 'D', 'Eb']); + expect(seq('c', 'c', 'c').transpose('1P', '2M', '3m').firstCycleValues).toEqual(['C', 'D', 'Eb']); + }); }); }); diff --git a/packages/tonal/tonal.mjs b/packages/tonal/tonal.mjs index a425782d2..ae75ab690 100644 --- a/packages/tonal/tonal.mjs +++ b/packages/tonal/tonal.mjs @@ -6,19 +6,28 @@ This program is free software: you can redistribute it and/or modify it under th import { Note, Interval, Scale } from '@tonaljs/tonal'; import { register, _mod, silence, logger, pure, isNote } from '@strudel/core'; -import { stepInNamedScale } from './tonleiter.mjs'; +import { stepInNamedScale, nearestNumberIndex } from './tonleiter.mjs'; +import { noteToMidi } from '../core/util.mjs'; const octavesInterval = (octaves) => (octaves <= 0 ? -1 : 1) + octaves * 7 + 'P'; -function scaleStep(step, scale) { - scale = scale.replaceAll(':', ' '); - step = Math.ceil(step); - let { intervals, tonic, empty } = Scale.get(scale); - if ((empty && isNote(scale)) || (empty && !tonic)) { - throw new Error(`incomplete scale. Make sure to use ":" instead of spaces, example: .scale("C:major")`); +function getScale(scaleName) { + scaleName = scaleName.replaceAll(':', ' '); + const scale = Scale.get(scaleName); + const { tonic, empty } = scale; + if ((empty && isNote(scaleName)) || (empty && !tonic)) { + throw new Error( + `Scale name ${scaleName} is incomplete. Make sure to use ":" instead of spaces, example: .scale("C:major")`, + ); } else if (empty) { - throw new Error(`invalid scale "${scale}"`); + throw new Error(`Invalid scale name "${scaleName}"`); } + return scale; +} + +function scaleStep(step, scale) { + step = Math.ceil(step); + let { intervals, tonic } = getScale(scale); tonic = tonic || 'C'; const { pc, oct = 3 } = Note.get(tonic); const octaveOffset = Math.floor(step / intervals.length); @@ -30,8 +39,7 @@ function scaleStep(step, scale) { // transpose note inside scale by offset steps // function scaleOffset(scale: string, offset: number, note: string) { function scaleOffset(scale, offset, note) { - let [tonic, scaleName] = Scale.tokenize(scale); - let { notes } = Scale.get(`${tonic} ${scaleName}`); + let { notes } = getScale(scale); notes = notes.map((note) => Note.get(note).pc); // use only pc! offset = Number(offset); if (isNaN(offset)) { @@ -88,13 +96,14 @@ function scaleOffset(scale, offset, note) { * @returns Pattern * @memberof Pattern * @name transpose + * @synonyms trans * @example * "c2 c3".fast(2).transpose("<0 -2 5 3>".slow(2)).note() * @example * "c2 c3".fast(2).transpose("<1P -2M 4P 3m>".slow(2)).note() */ -export const transpose = register('transpose', function (intervalOrSemitones, pat) { +export const { transpose, trans } = register(['transpose', 'trans'], function transposeFn(intervalOrSemitones, pat) { return pat.withHap((hap) => { const note = hap.value.note ?? hap.value; if (typeof note === 'number') { @@ -119,10 +128,7 @@ export const transpose = register('transpose', function (intervalOrSemitones, pa const interval = !isNaN(Number(intervalOrSemitones)) ? Interval.fromSemitones(intervalOrSemitones) : String(intervalOrSemitones); - // TODO: move simplify to player to preserve enharmonics - // tone.js doesn't understand multiple sharps flats e.g. F##3 has to be turned into G3 - // TODO: check if this is still relevant.. - const targetNote = Note.simplify(Note.transpose(note, interval)); + const targetNote = Note.transpose(note, interval); if (typeof hap.value === 'object') { return hap.withValue(() => ({ ...hap.value, note: targetNote })); } @@ -142,6 +148,7 @@ export const transpose = register('transpose', function (intervalOrSemitones, pa * @name scaleTranspose * @param {offset} offset number of steps inside the scale * @returns Pattern + * @synonyms scaleTrans, strans * @example * "-8 [2,4,6]" * .scale('C4 bebop major') @@ -149,25 +156,79 @@ export const transpose = register('transpose', function (intervalOrSemitones, pa * .note() */ -export const scaleTranspose = register('scaleTranspose', function (offset /* : number | string */, pat) { - return pat.withHap((hap) => { - if (!hap.context.scale) { - throw new Error('can only use scaleTranspose after .scale'); +export const { scaleTranspose, scaleTrans, strans } = register( + ['scaleTranspose', 'scaleTrans', 'strans'], + function (offset /* : number | string */, pat) { + return pat.withHap((hap) => { + if (!hap.context.scale) { + throw new Error('can only use scaleTranspose after .scale'); + } + if (typeof hap.value === 'object') + return hap.withValue(() => ({ + ...hap.value, + note: scaleOffset(hap.context.scale, Number(offset), hap.value.note), + })); + if (typeof hap.value !== 'string') { + throw new Error('can only use scaleTranspose with notes'); + } + return hap.withValue(() => scaleOffset(hap.context.scale, Number(offset), hap.value)); + }); + }, +); + +// Converts a step value, which is a number optionally decorated with sharps and flats, +// to a number and an `offset` number of semitones +function _convertStepToNumberAndOffset(step) { + let asNumber = Number(step); + let offset = 0; + if (isNaN(asNumber)) { + step = String(step); + // Check to see if the step matches the expected format: + // - A number (possibly negative) + // - Some number of sharps or flats (but not both) + const match = /^(-?\d+)(#+|b+)?$/.exec(step); + + if (!match) { + throw new Error(`invalid scale step "${step}", expected number or integer with optional # b suffixes`); } - if (typeof hap.value === 'object') - return hap.withValue(() => ({ - ...hap.value, - note: scaleOffset(hap.context.scale, Number(offset), hap.value.note), - })); - if (typeof hap.value !== 'string') { - throw new Error('can only use scaleTranspose with notes'); - } - return hap.withValue(() => scaleOffset(hap.context.scale, Number(offset), hap.value)); - }); -}); + asNumber = Number(match[1]); + // These decorations will determine the semitone offset based on the number of + // sharps or flats + const decorations = match[2] || ''; + offset = decorations[0] === '#' ? decorations.length : -decorations.length; + } + return [asNumber, offset]; +} + +let scaleToMidisAndNotes = {}; +// Finds the nearest scale note to `note` +function _getNearestScaleNote(scaleName, note, preferHigher = true) { + let noteMidi = typeof note === 'string' ? noteToMidi(note) : note; + if (scaleToMidisAndNotes[scaleName] === undefined) { + const { intervals, tonic } = getScale(scaleName); + const { pc } = Note.get(tonic); + const expandedIntervals = intervals.concat('8P'); // add the octave for wrapping + const sNotes = expandedIntervals.map((interval) => Note.transpose(pc + '0', interval)); + const sMidi = sNotes.map(noteToMidi); + // Cache + scaleToMidisAndNotes[scaleName] = [sMidi, sNotes]; + } + const [scaleMidis, scaleNotes] = scaleToMidisAndNotes[scaleName]; + const rootMidi = scaleMidis[0]; + const octaveDiff = Math.floor((noteMidi - rootMidi) / 12); + const alignedMidis = scaleMidis.map((m) => m + 12 * octaveDiff); + const noteIdx = nearestNumberIndex(noteMidi, alignedMidis, preferHigher); + const noteMatch = scaleNotes[noteIdx]; + return Note.transpose(noteMatch, Interval.fromSemitones(12 * octaveDiff)); +} /** - * Turns numbers into notes in the scale (zero indexed). Also sets scale for other scale operations, like {@link Pattern#scaleTranspose}. + * Turns numbers into notes in the scale (zero indexed) or quantizes notes to a scale. + * + * When describing notes via numbers, note that negative numbers can be used to wrap backwards + * in the scale as well as sharps or flats (but not both) to produce notes outside of the scale. + * + * Also sets scale for other scale operations, like {@link Pattern#scaleTranspose}. * * A scale consists of a root note (e.g. `c4`, `c`, `f#`, `bb4`) followed by semicolon (':') and then a [scale type](https://github.com/tonaljs/tonal/blob/main/packages/scale-type/data.ts). * @@ -186,6 +247,12 @@ export const scaleTranspose = register('scaleTranspose', function (offset /* : n * n(rand.range(0,12).segment(8)) * .scale("C:ritusen") * .s("piano") + * @example + * n("<[0,7b] [-4# -4] [-2,7##] 4 [0,7] [-4# -4b] [-2,7###] 4b>*4") + * .scale("C:/2") + * .s("piano") + * @example + * note("C1*16").transpose(irand(36)).scale('Cb2 major').scaleTranspose(3) */ export const scale = register( @@ -199,49 +266,35 @@ export const scale = register( pat .fmap((value) => { const isObject = typeof value === 'object'; - let step = isObject ? value.n : value; - if (isObject) { + // The case where the note has been defined via `n` or `pure` + if (!isObject || (isObject && ('n' in value || 'value' in value))) { + const step = isObject ? (value.n ?? value.value) : value; delete value.n; // remove n so it won't cause trouble - } - if (isNote(step)) { - // legacy.. - return pure(step); - } - let asNumber = Number(step); - let semitones = 0; - if (isNaN(asNumber)) { - step = String(step); - if (!/^[-+]?\d+(#*|b*){1}$/.test(step)) { - logger( - `[tonal] invalid scale step "${step}", expected number or integer with optional # b suffixes`, - 'error', - ); + if (isNote(step)) { + // legacy.. + return pure(step); + } + try { + const [number, offset] = _convertStepToNumberAndOffset(step); + let note; + if (isObject && value.anchor) { + note = stepInNamedScale(number, scale, value.anchor); + } else { + note = scaleStep(number, scale); + } + if (offset != 0) note = Note.transpose(note, Interval.fromSemitones(offset)); + value = pure(isObject ? { ...value, note } : note); + } catch (err) { + logger(`[tonal] ${err.message}`, 'error'); return silence; } - const isharp = step.indexOf('#'); - if (isharp >= 0) { - asNumber = Number(step.substring(0, isharp)); - semitones = step.length - isharp; - } else { - const iflat = step.indexOf('b'); - asNumber = Number(step.substring(0, iflat)); - semitones = iflat - step.length; - } + return value; } - try { - let note; - if (isObject && value.anchor) { - note = stepInNamedScale(asNumber, scale, value.anchor); - } else { - note = scaleStep(asNumber, scale); - } - if (semitones != 0) note = Note.transpose(note, Interval.fromSemitones(semitones)); - value = pure(isObject ? { ...value, note } : note); - } catch (err) { - logger(`[tonal] ${err.message}`, 'error'); - value = silence; + // The case where the note has been defined via `note` + else { + const note = _getNearestScaleNote(scale, value.note); + return pure(isObject ? { ...value, note } : note); } - return value; }) .outerJoin() // legacy: diff --git a/packages/tonal/tonleiter.mjs b/packages/tonal/tonleiter.mjs index 3814394f6..233129641 100644 --- a/packages/tonal/tonleiter.mjs +++ b/packages/tonal/tonleiter.mjs @@ -101,11 +101,11 @@ export function nearestNumberIndex(target, numbers, preferHigher) { let scaleSteps = {}; // [scaleName]: semitones[] export function stepInNamedScale(step, scale, anchor, preferHigher) { - let [root, scaleName] = Scale.tokenize(scale); + const [root, scaleName] = Scale.tokenize(scale); const rootMidi = x2midi(root); const rootChroma = midi2chroma(rootMidi); if (!scaleSteps[scaleName]) { - let { intervals } = Scale.get(`C ${scaleName}`); + const { intervals } = Scale.get(`C ${scaleName}`); // cache result scaleSteps[scaleName] = intervals.map(step2semitones); } @@ -222,6 +222,7 @@ export const Note = { }; // TODO: support octave numbers +// Example: Note("Bb3").transpose("c3") export function transpose(note, step) { // example: E, 3 const stepNumber = Step.tokenize(step)[1]; // 3 @@ -235,5 +236,3 @@ export function transpose(note, step) { const offsetAccidentals = accidentalString(Step.accidentals(step) + Note.accidentals(note) + stepIndex - indexOffset); // "we need to add a # to to the G to make it a major third from E" return [targetNote, offsetAccidentals].join(''); } - -//Note("Bb3").transpose("c3") diff --git a/packages/transpiler/package.json b/packages/transpiler/package.json index 2a5e39776..18722bdc2 100644 --- a/packages/transpiler/package.json +++ b/packages/transpiler/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/transpiler", - "version": "1.2.2", + "version": "1.2.4", "description": "Transpiler for strudel user code. Converts syntactically correct but semantically meaningless JS into evaluatable strudel code.", "main": "index.mjs", "type": "module", diff --git a/packages/web/package.json b/packages/web/package.json index 0feddc82d..df21f4055 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/web", - "version": "1.2.3", + "version": "1.2.5", "description": "Easy to setup, opiniated bundle of Strudel for the browser.", "module": "web.mjs", "publishConfig": { diff --git a/packages/webaudio/index.mjs b/packages/webaudio/index.mjs index 362e61c44..4933b7a01 100644 --- a/packages/webaudio/index.mjs +++ b/packages/webaudio/index.mjs @@ -7,4 +7,5 @@ This program is free software: you can redistribute it and/or modify it under th export * from './webaudio.mjs'; export * from './scope.mjs'; export * from './spectrum.mjs'; +export * from './supradough.mjs'; export * from 'superdough'; diff --git a/packages/webaudio/package.json b/packages/webaudio/package.json index 5cc0a5538..49da00f23 100644 --- a/packages/webaudio/package.json +++ b/packages/webaudio/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/webaudio", - "version": "1.2.3", + "version": "1.2.5", "description": "Web Audio helpers for Strudel", "main": "index.mjs", "type": "module", @@ -35,7 +35,8 @@ "dependencies": { "@strudel/core": "workspace:*", "@strudel/draw": "workspace:*", - "superdough": "workspace:*" + "superdough": "workspace:*", + "supradough": "workspace:*" }, "devDependencies": { "vite": "^6.0.11" diff --git a/packages/webaudio/supradough.mjs b/packages/webaudio/supradough.mjs new file mode 100644 index 000000000..f97251a07 --- /dev/null +++ b/packages/webaudio/supradough.mjs @@ -0,0 +1,130 @@ +import { Pattern } from '@strudel/core'; +import { connectToDestination, getAudioContext, getWorklet } from 'superdough'; + +let doughWorklet; + +function initDoughWorklet() { + const ac = getAudioContext(); + doughWorklet = getWorklet( + ac, + 'dough-processor', + {}, + { + outputChannelCount: [2], + }, + ); + connectToDestination(doughWorklet); // channels? +} + +const soundMap = new Map(); +const loadedSounds = new Map(); + +Pattern.prototype.supradough = function () { + return this.onTrigger((hap, __, cps, begin) => { + hap.value._begin = begin; + hap.value._duration = hap.duration / cps; + !doughWorklet && initDoughWorklet(); + const s = (hap.value.bank ? hap.value.bank + '_' : '') + hap.value.s; + const n = hap.value.n ?? 0; + const soundKey = `${s}:${n}`; + if (soundMap.has(s)) { + hap.value.s = soundKey; // dough.mjs is unaware of bank and n (only maps keys to buffers) + } + if (soundMap.has(s) && !loadedSounds.has(soundKey)) { + const urls = soundMap.get(s); + const url = urls[n % urls.length]; + console.log(`load ${soundKey} from ${url}`); + const loadSample = fetchSample(url); + loadedSounds.set(soundKey, loadSample); + loadSample.then(({ channels, sampleRate }) => + doughWorklet.port.postMessage({ + sample: soundKey, + channels, + sampleRate, + }), + ); + } + + doughWorklet.port.postMessage({ spawn: hap.value }); + }, 1); +}; + +function githubPath(base, subpath = '') { + if (!base.startsWith('github:')) { + throw new Error('expected "github:" at the start of pseudoUrl'); + } + let [_, path] = base.split('github:'); + path = path.endsWith('/') ? path.slice(0, -1) : path; + if (path.split('/').length === 2) { + // assume main as default branch if none set + path += '/main'; + } + return `https://raw.githubusercontent.com/${path}/${subpath}`; +} +export async function fetchSampleMap(url) { + if (url.startsWith('github:')) { + url = githubPath(url, 'strudel.json'); + } + if (url.startsWith('local:')) { + url = `http://localhost:5432`; + } + if (url.startsWith('shabda:')) { + let [_, path] = url.split('shabda:'); + url = `https://shabda.ndre.gr/${path}.json?strudel=1`; + } + if (url.startsWith('shabda/speech')) { + let [_, path] = url.split('shabda/speech'); + path = path.startsWith('/') ? path.substring(1) : path; + let [params, words] = path.split(':'); + let gender = 'f'; + let language = 'en-GB'; + if (params) { + [language, gender] = params.split('/'); + } + url = `https://shabda.ndre.gr/speech/${words}.json?gender=${gender}&language=${language}&strudel=1'`; + } + if (typeof fetch !== 'function') { + // not a browser + return; + } + const base = url.split('/').slice(0, -1).join('/'); + if (typeof fetch === 'undefined') { + // skip fetch when in node / testing + return; + } + const json = await fetch(url) + .then((res) => res.json()) + .catch((error) => { + console.error(error); + throw new Error(`error loading "${url}"`); + }); + return [json, json._base || base]; +} + +// for some reason, only piano and flute work.. is it because mp3?? + +async function fetchSample(url) { + const buffer = await fetch(url) + .then((res) => res.arrayBuffer()) + .then((buf) => getAudioContext().decodeAudioData(buf)); + let channels = []; + for (let i = 0; i < buffer.numberOfChannels; i++) { + channels.push(buffer.getChannelData(i)); + } + return { channels, sampleRate: buffer.sampleRate }; +} + +export async function doughsamples(sampleMap, baseUrl) { + if (typeof sampleMap === 'string') { + const [json, base] = await fetchSampleMap(sampleMap); + // console.log('json', json, 'base', base); + return doughsamples(json, base); + } + Object.entries(sampleMap).map(async ([key, urls]) => { + if (key !== '_base') { + urls = urls.map((url) => baseUrl + url); + // console.log('set', key, urls); + soundMap.set(key, urls); + } + }); +} diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index 91e28defd..383e87f87 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -5,7 +5,12 @@ This program is free software: you can redistribute it and/or modify it under th */ import * as strudel from '@strudel/core'; -import { superdough, getAudioContext, setLogger, doughTrigger } from 'superdough'; +import { superdough, getAudioContext, setLogger, doughTrigger, registerWorklet } from 'superdough'; +import './supradough.mjs'; +import { workletUrl } from 'supradough'; + +registerWorklet(workletUrl); + const { Pattern, logger, repl } = strudel; setLogger(logger); @@ -15,14 +20,10 @@ const hap2value = (hap) => { return hap.value; }; -export const webaudioOutputTrigger = (t, hap, ct, cps) => superdough(hap2value(hap), t - ct, hap.duration / cps, cps); // uses more precise, absolute t if available, see https://github.com/tidalcycles/strudel/pull/1004 -export const webaudioOutput = (hap, deadline, hapDuration, cps, t) => { - return superdough(hap2value(hap), t ? `=${t}` : deadline, hapDuration, cps); -}; - -Pattern.prototype.webaudio = function () { - return this.onTrigger(webaudioOutputTrigger); +// TODO: refactor output callbacks to eliminate deadline +export const webaudioOutput = (hap, _deadline, hapDuration, cps, t) => { + return superdough(hap2value(hap), t, hapDuration, cps, hap.whole?.begin.valueOf()); }; export function webaudioRepl(options = {}) { diff --git a/packages/xen/package.json b/packages/xen/package.json index 88c2bb082..0a6736d9c 100644 --- a/packages/xen/package.json +++ b/packages/xen/package.json @@ -1,6 +1,6 @@ { "name": "@strudel/xen", - "version": "1.2.2", + "version": "1.2.4", "description": "Xenharmonic API for strudel", "main": "index.mjs", "type": "module", diff --git a/packages/xen/tunejs.js b/packages/xen/tunejs.js index 6b5e7cb7c..5806cf596 100644 --- a/packages/xen/tunejs.js +++ b/packages/xen/tunejs.js @@ -139,10 +139,10 @@ Tune.prototype.MIDI = function(stepIn,octaveIn) { /* Load a new scale */ -Tune.prototype.loadScale = function(name){ +Tune.prototype.loadScale = function(scale){ /* load the scale */ - var freqs = TuningList[name].frequencies + var freqs = isArrayOfNumbers(scale) ? scale : TuningList[scale].frequencies this.scale = [] for (var i=0;i 0 && arg.every(item => typeof item === 'number' && !isNaN(item)); +} + +/* allow an array of values too */ +Tune.prototype.isValidScale = function(scale) { + return !!TuningList[scale] || isArrayOfNumbers(scale) ; } /* Return a collection of notes as an array */ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7aecd35bd..c225057f3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -201,8 +201,8 @@ importers: specifier: ^6.1.0 version: 6.1.0(@codemirror/autocomplete@6.18.4)(@codemirror/commands@6.8.0)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2) '@replit/codemirror-vim': - specifier: ^6.2.1 - version: 6.2.1(@codemirror/commands@6.8.0)(@codemirror/language@6.10.8)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2) + specifier: ^6.3.0 + version: 6.3.0(@codemirror/commands@6.8.0)(@codemirror/language@6.10.8)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2) '@replit/codemirror-vscode-keymap': specifier: ^6.0.2 version: 6.0.2(@codemirror/autocomplete@6.18.4)(@codemirror/commands@6.8.0)(@codemirror/language@6.10.8)(@codemirror/lint@6.8.4)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2) @@ -517,6 +517,18 @@ importers: specifier: workspace:* version: link:../vite-plugin-bundle-audioworklet + packages/supradough: + devDependencies: + vite: + specifier: ^6.0.11 + version: 6.0.11(@types/node@22.10.10)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0) + vite-plugin-bundle-audioworklet: + specifier: workspace:* + version: link:../vite-plugin-bundle-audioworklet + wav-encoder: + specifier: ^1.3.0 + version: 1.3.0 + packages/tidal: dependencies: '@strudel/core': @@ -625,6 +637,9 @@ importers: superdough: specifier: workspace:* version: link:../superdough + supradough: + specifier: workspace:* + version: link:../supradough devDependencies: vite: specifier: ^6.0.11 @@ -2232,14 +2247,14 @@ packages: '@codemirror/state': ^6.0.1 '@codemirror/view': ^6.3.0 - '@replit/codemirror-vim@6.2.1': - resolution: {integrity: sha512-qDAcGSHBYU5RrdO//qCmD8K9t6vbP327iCj/iqrkVnjbrpFhrjOt92weGXGHmTNRh16cUtkUZ7Xq7rZf+8HVow==} + '@replit/codemirror-vim@6.3.0': + resolution: {integrity: sha512-aTx931ULAMuJx6xLf7KQDOL7CxD+Sa05FktTDrtLaSy53uj01ll3Zf17JdKsriER248oS55GBzg0CfCTjEneAQ==} peerDependencies: - '@codemirror/commands': ^6.0.0 - '@codemirror/language': ^6.1.0 - '@codemirror/search': ^6.2.0 - '@codemirror/state': ^6.0.1 - '@codemirror/view': ^6.0.3 + '@codemirror/commands': 6.x.x + '@codemirror/language': 6.x.x + '@codemirror/search': 6.x.x + '@codemirror/state': 6.x.x + '@codemirror/view': 6.x.x '@replit/codemirror-vscode-keymap@6.0.2': resolution: {integrity: sha512-j45qTwGxzpsv82lMD/NreGDORFKSctMDVkGRopaP+OrzSzv+pXDQuU3LnFvKpasyjVT0lf+PKG1v2DSCn/vxxg==} @@ -5714,6 +5729,7 @@ packages: node-domexception@1.0.0: resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead node-fetch-native@1.6.6: resolution: {integrity: sha512-8Mc2HhqPdlIfedsuZoc3yioPuzp6b+L5jRCRY1QzuWZh2EGJVQrGppC6V6cF0bLdbW0+O2YpqCA25aF/1lvipQ==} @@ -6803,6 +6819,7 @@ packages: source-map@0.8.0-beta.0: resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} engines: {node: '>= 8'} + deprecated: The work that was done in this beta branch won't be included in future versions sourcemap-codec@1.4.8: resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} @@ -7535,6 +7552,9 @@ packages: walk-up-path@3.0.1: resolution: {integrity: sha512-9YlCL/ynK3CTlrSRrDxZvUauLzAswPCrsaCgilqFevUYpeEW0/3ScEjaa3kbW/T0ghhkEr7mv+fpjqn1Y1YuTA==} + wav-encoder@1.3.0: + resolution: {integrity: sha512-FXJdEu2qDOI+wbVYZpu21CS1vPEg5NaxNskBr4SaULpOJMrLE6xkH8dECa7PiS+ZoeyvP7GllWUAxPN3AvFSEw==} + wav@1.0.2: resolution: {integrity: sha512-viHtz3cDd/Tcr/HbNqzQCofKdF6kWUymH9LGDdskfWFoIy/HJ+RTihgjEcHfnsy1PO4e9B+y4HwgTwMrByquhg==} @@ -7657,6 +7677,7 @@ packages: workbox-google-analytics@7.0.0: resolution: {integrity: sha512-MEYM1JTn/qiC3DbpvP2BVhyIH+dV/5BjHk756u9VbwuAhu0QHyKscTnisQuz21lfRpOwiS9z4XdqeVAKol0bzg==} + deprecated: It is not compatible with newer versions of GA starting with v4, as long as you are using GAv3 it should be ok, but the package is not longer being maintained workbox-navigation-preload@7.0.0: resolution: {integrity: sha512-juWCSrxo/fiMz3RsvDspeSLGmbgC0U9tKqcUPZBCf35s64wlaLXyn2KdHHXVQrb2cqF7I0Hc9siQalainmnXJA==} @@ -9595,7 +9616,7 @@ snapshots: '@codemirror/state': 6.5.1 '@codemirror/view': 6.36.2 - '@replit/codemirror-vim@6.2.1(@codemirror/commands@6.8.0)(@codemirror/language@6.10.8)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)': + '@replit/codemirror-vim@6.3.0(@codemirror/commands@6.8.0)(@codemirror/language@6.10.8)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)': dependencies: '@codemirror/commands': 6.8.0 '@codemirror/language': 6.10.8 @@ -15955,6 +15976,8 @@ snapshots: walk-up-path@3.0.1: {} + wav-encoder@1.3.0: {} + wav@1.0.2: dependencies: buffer-alloc: 1.2.0 diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 1d9a4e539..053dff4db 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -1847,6 +1847,27 @@ exports[`runs examples > example "chop" example index 0 1`] = ` ] `; +exports[`runs examples > example "chorus" example index 0 1`] = ` +[ + "[ 0/1 → 1/4 | note:d s:sawtooth chorus:0.5 ]", + "[ 1/4 → 1/2 | note:d s:sawtooth chorus:0.5 ]", + "[ 1/2 → 3/4 | note:a# s:sawtooth chorus:0.5 ]", + "[ 3/4 → 1/1 | note:a s:sawtooth chorus:0.5 ]", + "[ 1/1 → 5/4 | note:d s:sawtooth chorus:0.5 ]", + "[ 5/4 → 3/2 | note:d s:sawtooth chorus:0.5 ]", + "[ 3/2 → 7/4 | note:a# s:sawtooth chorus:0.5 ]", + "[ 7/4 → 2/1 | note:a s:sawtooth chorus:0.5 ]", + "[ 2/1 → 9/4 | note:d s:sawtooth chorus:0.5 ]", + "[ 9/4 → 5/2 | note:d s:sawtooth chorus:0.5 ]", + "[ 5/2 → 11/4 | note:a# s:sawtooth chorus:0.5 ]", + "[ 11/4 → 3/1 | note:a s:sawtooth chorus:0.5 ]", + "[ 3/1 → 13/4 | note:d s:sawtooth chorus:0.5 ]", + "[ 13/4 → 7/2 | note:d s:sawtooth chorus:0.5 ]", + "[ 7/2 → 15/4 | note:a# s:sawtooth chorus:0.5 ]", + "[ 15/4 → 4/1 | note:a s:sawtooth chorus:0.5 ]", +] +`; + exports[`runs examples > example "chunk" example index 0 1`] = ` [ "[ 0/1 → 1/4 | note:A4 ]", @@ -2585,6 +2606,52 @@ exports[`runs examples > example "delayfeedback" example index 0 1`] = ` ] `; +exports[`runs examples > example "delayfeedback" example index 0 2`] = ` +[ + "[ 0/1 → 1/1 | s:bd delay:0.25 delayfeedback:0.25 ]", + "[ 1/1 → 2/1 | s:bd delay:0.25 delayfeedback:0.5 ]", + "[ 2/1 → 3/1 | s:bd delay:0.25 delayfeedback:0.75 ]", + "[ 3/1 → 4/1 | s:bd delay:0.25 delayfeedback:1 ]", +] +`; + +exports[`runs examples > example "delayspeed" example index 0 1`] = ` +[ + "[ 0/1 → 1/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]", + "[ 1/8 → 1/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]", + "[ 1/4 → 3/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]", + "[ 3/8 → 1/2 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]", + "[ 1/2 → 5/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]", + "[ 5/8 → 3/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]", + "[ 3/4 → 7/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]", + "[ 7/8 → 1/1 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]", + "[ 1/1 → 9/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]", + "[ 9/8 → 5/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]", + "[ 5/4 → 11/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]", + "[ 11/8 → 3/2 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]", + "[ 3/2 → 13/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]", + "[ 13/8 → 7/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]", + "[ 7/4 → 15/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]", + "[ 15/8 → 2/1 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]", + "[ 2/1 → 17/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]", + "[ 17/8 → 9/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]", + "[ 9/4 → 19/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]", + "[ 19/8 → 5/2 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]", + "[ 5/2 → 21/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]", + "[ 21/8 → 11/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]", + "[ 11/4 → 23/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]", + "[ 23/8 → 3/1 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]", + "[ 3/1 → 25/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]", + "[ 25/8 → 13/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]", + "[ 13/4 → 27/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]", + "[ 27/8 → 7/2 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]", + "[ 7/2 → 29/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]", + "[ 29/8 → 15/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]", + "[ 15/4 → 31/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]", + "[ 31/8 → 4/1 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]", +] +`; + exports[`runs examples > example "delaysync" example index 0 1`] = ` [ "[ 0/1 → 1/2 | s:bd delay:0.25 delaysync:0.125 ]", @@ -2598,19 +2665,6 @@ exports[`runs examples > example "delaysync" example index 0 1`] = ` ] `; -exports[`runs examples > example "delaytime" example index 0 1`] = ` -[ - "[ 0/1 → 1/2 | s:bd delay:0.25 delaytime:0.125 ]", - "[ 1/2 → 1/1 | s:bd delay:0.25 delaytime:0.125 ]", - "[ 1/1 → 3/2 | s:bd delay:0.25 delaytime:0.25 ]", - "[ 3/2 → 2/1 | s:bd delay:0.25 delaytime:0.25 ]", - "[ 2/1 → 5/2 | s:bd delay:0.25 delaytime:0.5 ]", - "[ 5/2 → 3/1 | s:bd delay:0.25 delaytime:0.5 ]", - "[ 3/1 → 7/2 | s:bd delay:0.25 delaytime:1 ]", - "[ 7/2 → 4/1 | s:bd delay:0.25 delaytime:1 ]", -] -`; - exports[`runs examples > example "density" example index 0 1`] = ` [ "[ 0/1 → 1/4 | s:crackle density:0.01 ]", @@ -2775,28 +2829,116 @@ exports[`runs examples > example "distort" example index 1 1`] = ` ] `; +exports[`runs examples > example "distort" example index 2 1`] = ` +[ + "[ 0/1 → 1/4 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]", + "[ 1/4 → 1/2 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]", + "[ 1/2 → 3/4 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]", + "[ 3/4 → 1/1 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]", + "[ 1/1 → 5/4 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]", + "[ 5/4 → 3/2 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]", + "[ 3/2 → 7/4 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]", + "[ 7/4 → 2/1 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]", + "[ 2/1 → 9/4 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]", + "[ 9/4 → 5/2 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]", + "[ 5/2 → 11/4 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]", + "[ 11/4 → 3/1 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]", + "[ 3/1 → 13/4 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]", + "[ 13/4 → 7/2 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]", + "[ 7/2 → 15/4 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]", + "[ 15/4 → 4/1 | s:bd n:4 bank:tr808 distort:3 distortvol:0.5 distorttype:diode ]", +] +`; + +exports[`runs examples > example "distorttype" example index 0 1`] = ` +[ + "[ 0/1 → 1/4 | s:bd bank:tr909 distort:2 distorttype:0 ]", + "[ 1/4 → 1/2 | s:bd bank:tr909 distort:2 distorttype:0 ]", + "[ 1/2 → 3/4 | s:bd bank:tr909 distort:2 distorttype:0 ]", + "[ 3/4 → 1/1 | s:bd bank:tr909 distort:2 distorttype:0 ]", + "[ 1/1 → 5/4 | s:bd bank:tr909 distort:2 distorttype:1 ]", + "[ 5/4 → 3/2 | s:bd bank:tr909 distort:2 distorttype:1 ]", + "[ 3/2 → 7/4 | s:bd bank:tr909 distort:2 distorttype:1 ]", + "[ 7/4 → 2/1 | s:bd bank:tr909 distort:2 distorttype:1 ]", + "[ 2/1 → 9/4 | s:bd bank:tr909 distort:2 distorttype:2 ]", + "[ 9/4 → 5/2 | s:bd bank:tr909 distort:2 distorttype:2 ]", + "[ 5/2 → 11/4 | s:bd bank:tr909 distort:2 distorttype:2 ]", + "[ 11/4 → 3/1 | s:bd bank:tr909 distort:2 distorttype:2 ]", + "[ 3/1 → 13/4 | s:bd bank:tr909 distort:2 distorttype:0 ]", + "[ 13/4 → 7/2 | s:bd bank:tr909 distort:2 distorttype:0 ]", + "[ 7/2 → 15/4 | s:bd bank:tr909 distort:2 distorttype:0 ]", + "[ 15/4 → 4/1 | s:bd bank:tr909 distort:2 distorttype:0 ]", +] +`; + +exports[`runs examples > example "distorttype" example index 1 1`] = ` +[ + "[ (0/1 → 1/2) ⇝ 1/1 | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:1 distorttype:fold ]", + "[ 0/1 ⇜ (1/2 → 1/1) | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:1 distorttype:fold ]", + "[ (1/1 → 3/2) ⇝ 2/1 | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:4.6367951557040215 distorttype:chebyshev ]", + "[ 1/1 ⇜ (3/2 → 2/1) | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:4.6367951557040215 distorttype:chebyshev ]", + "[ (2/1 → 5/2) ⇝ 3/1 | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:7.716689839959145 distorttype:scurve ]", + "[ 2/1 ⇜ (5/2 → 3/1) | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:7.716689839959145 distorttype:scurve ]", + "[ (3/1 → 7/2) ⇝ 4/1 | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:2.5210237745195627 distorttype:diode ]", + "[ 3/1 ⇜ (7/2 → 4/1) | s:sine note:F1 release:1 penv:24 pdecay:0.05 distort:2.5210237745195627 distorttype:diode ]", +] +`; + +exports[`runs examples > example "distortvol" example index 0 1`] = ` +[ + "[ 0/1 → 1/4 | s:bd bank:tr909 distort:2 distortvol:0.8 ]", + "[ 1/4 → 1/2 | s:bd bank:tr909 distort:2 distortvol:0.8 ]", + "[ 1/2 → 3/4 | s:bd bank:tr909 distort:2 distortvol:0.8 ]", + "[ 3/4 → 1/1 | s:bd bank:tr909 distort:2 distortvol:0.8 ]", + "[ 1/1 → 5/4 | s:bd bank:tr909 distort:2 distortvol:0.8 ]", + "[ 5/4 → 3/2 | s:bd bank:tr909 distort:2 distortvol:0.8 ]", + "[ 3/2 → 7/4 | s:bd bank:tr909 distort:2 distortvol:0.8 ]", + "[ 7/4 → 2/1 | s:bd bank:tr909 distort:2 distortvol:0.8 ]", + "[ 2/1 → 9/4 | s:bd bank:tr909 distort:2 distortvol:0.8 ]", + "[ 9/4 → 5/2 | s:bd bank:tr909 distort:2 distortvol:0.8 ]", + "[ 5/2 → 11/4 | s:bd bank:tr909 distort:2 distortvol:0.8 ]", + "[ 11/4 → 3/1 | s:bd bank:tr909 distort:2 distortvol:0.8 ]", + "[ 3/1 → 13/4 | s:bd bank:tr909 distort:2 distortvol:0.8 ]", + "[ 13/4 → 7/2 | s:bd bank:tr909 distort:2 distortvol:0.8 ]", + "[ 7/2 → 15/4 | s:bd bank:tr909 distort:2 distortvol:0.8 ]", + "[ 15/4 → 4/1 | s:bd bank:tr909 distort:2 distortvol:0.8 ]", +] +`; + exports[`runs examples > example "djf" example index 0 1`] = ` [ - "[ 0/1 → 1/4 | n:0 s:superzow octave:3 djf:0.5 ]", - "[ 1/4 → 1/2 | n:3 s:superzow octave:3 djf:0.5 ]", - "[ 1/2 → 3/4 | n:7 s:superzow octave:3 djf:0.5 ]", - "[ 3/4 → 1/1 | n:10 s:superzow octave:3 djf:0.5 ]", - "[ 3/4 → 1/1 | n:24 s:superzow octave:3 djf:0.5 ]", - "[ 1/1 → 5/4 | n:0 s:superzow octave:3 djf:0.25 ]", - "[ 5/4 → 3/2 | n:3 s:superzow octave:3 djf:0.25 ]", - "[ 3/2 → 7/4 | n:7 s:superzow octave:3 djf:0.25 ]", - "[ 7/4 → 2/1 | n:10 s:superzow octave:3 djf:0.25 ]", - "[ 7/4 → 2/1 | n:24 s:superzow octave:3 djf:0.25 ]", - "[ 2/1 → 9/4 | n:0 s:superzow octave:3 djf:0.5 ]", - "[ 9/4 → 5/2 | n:3 s:superzow octave:3 djf:0.5 ]", - "[ 5/2 → 11/4 | n:7 s:superzow octave:3 djf:0.5 ]", - "[ 11/4 → 3/1 | n:10 s:superzow octave:3 djf:0.5 ]", - "[ 11/4 → 3/1 | n:24 s:superzow octave:3 djf:0.5 ]", - "[ 3/1 → 13/4 | n:0 s:superzow octave:3 djf:0.75 ]", - "[ 13/4 → 7/2 | n:3 s:superzow octave:3 djf:0.75 ]", - "[ 7/2 → 15/4 | n:7 s:superzow octave:3 djf:0.75 ]", - "[ 15/4 → 4/1 | n:10 s:superzow octave:3 djf:0.75 ]", - "[ 15/4 → 4/1 | n:24 s:superzow octave:3 djf:0.75 ]", + "[ 0/1 → 1/8 | note:D3 s:supersaw djf:0.5 ]", + "[ 1/8 → 1/4 | note:G4 s:supersaw djf:0.5 ]", + "[ 1/4 → 3/8 | note:Bb3 s:supersaw djf:0.5 ]", + "[ 3/8 → 1/2 | note:C4 s:supersaw djf:0.5 ]", + "[ 1/2 → 5/8 | note:A3 s:supersaw djf:0.5 ]", + "[ 5/8 → 3/4 | note:F3 s:supersaw djf:0.5 ]", + "[ 3/4 → 7/8 | note:G3 s:supersaw djf:0.5 ]", + "[ 7/8 → 1/1 | note:C4 s:supersaw djf:0.5 ]", + "[ 1/1 → 9/8 | note:Eb4 s:supersaw djf:0.3 ]", + "[ 9/8 → 5/4 | note:G4 s:supersaw djf:0.3 ]", + "[ 5/4 → 11/8 | note:A4 s:supersaw djf:0.3 ]", + "[ 11/8 → 3/2 | note:F3 s:supersaw djf:0.3 ]", + "[ 3/2 → 13/8 | note:F4 s:supersaw djf:0.3 ]", + "[ 13/8 → 7/4 | note:D4 s:supersaw djf:0.3 ]", + "[ 7/4 → 15/8 | note:G3 s:supersaw djf:0.3 ]", + "[ 15/8 → 2/1 | note:F4 s:supersaw djf:0.3 ]", + "[ 2/1 → 17/8 | note:Eb5 s:supersaw djf:0.2 ]", + "[ 17/8 → 9/4 | note:D5 s:supersaw djf:0.2 ]", + "[ 9/4 → 19/8 | note:Bb3 s:supersaw djf:0.2 ]", + "[ 19/8 → 5/2 | note:C5 s:supersaw djf:0.2 ]", + "[ 5/2 → 21/8 | note:D4 s:supersaw djf:0.2 ]", + "[ 21/8 → 11/4 | note:F3 s:supersaw djf:0.2 ]", + "[ 11/4 → 23/8 | note:G4 s:supersaw djf:0.2 ]", + "[ 23/8 → 3/1 | note:D3 s:supersaw djf:0.2 ]", + "[ 3/1 → 25/8 | note:G3 s:supersaw djf:0.75 ]", + "[ 25/8 → 13/4 | note:Bb3 s:supersaw djf:0.75 ]", + "[ 13/4 → 27/8 | note:Eb5 s:supersaw djf:0.75 ]", + "[ 27/8 → 7/2 | note:C4 s:supersaw djf:0.75 ]", + "[ 7/2 → 29/8 | note:C4 s:supersaw djf:0.75 ]", + "[ 29/8 → 15/4 | note:Eb5 s:supersaw djf:0.75 ]", + "[ 15/4 → 31/8 | note:Bb4 s:supersaw djf:0.75 ]", + "[ 31/8 → 4/1 | note:A4 s:supersaw djf:0.75 ]", ] `; @@ -3036,6 +3178,251 @@ exports[`runs examples > example "dry" example index 0 1`] = ` ] `; +exports[`runs examples > example "duckattack" example index 0 1`] = ` +[ + "[ 0/1 → 1/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 1/4 → 5/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 1/2 → 9/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 11/16 → 3/4 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 7/8 → 15/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 1/1 → 17/16 | s:bd n:4 duckorbit:2 duckattack:0 duckdepth:1 ]", + "[ 5/4 → 21/16 | s:bd n:4 duckorbit:2 duckattack:0 duckdepth:1 ]", + "[ 3/2 → 25/16 | s:bd n:4 duckorbit:2 duckattack:0 duckdepth:1 ]", + "[ 27/16 → 7/4 | s:bd n:4 duckorbit:2 duckattack:0 duckdepth:1 ]", + "[ 15/8 → 31/16 | s:bd n:4 duckorbit:2 duckattack:0 duckdepth:1 ]", + "[ 2/1 → 33/16 | s:bd n:4 duckorbit:2 duckattack:0.4 duckdepth:1 ]", + "[ 9/4 → 37/16 | s:bd n:4 duckorbit:2 duckattack:0.4 duckdepth:1 ]", + "[ 5/2 → 41/16 | s:bd n:4 duckorbit:2 duckattack:0.4 duckdepth:1 ]", + "[ 43/16 → 11/4 | s:bd n:4 duckorbit:2 duckattack:0.4 duckdepth:1 ]", + "[ 23/8 → 47/16 | s:bd n:4 duckorbit:2 duckattack:0.4 duckdepth:1 ]", + "[ 3/1 → 49/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 13/4 → 53/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 7/2 → 57/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 59/16 → 15/4 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 31/8 → 63/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", +] +`; + +exports[`runs examples > example "duckattack" example index 1 1`] = ` +[ + "[ 0/1 → 1/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]", + "[ 1/4 → 5/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]", + "[ 1/2 → 9/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]", + "[ 11/16 → 3/4 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]", + "[ 7/8 → 15/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]", + "[ 1/1 → 17/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]", + "[ 5/4 → 21/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]", + "[ 3/2 → 25/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]", + "[ 27/16 → 7/4 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]", + "[ 15/8 → 31/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]", + "[ 2/1 → 33/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]", + "[ 9/4 → 37/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]", + "[ 5/2 → 41/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]", + "[ 43/16 → 11/4 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]", + "[ 23/8 → 47/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]", + "[ 3/1 → 49/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]", + "[ 13/4 → 53/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]", + "[ 7/2 → 57/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]", + "[ 59/16 → 15/4 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]", + "[ 31/8 → 63/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]", +] +`; + +exports[`runs examples > example "duckdepth" example index 0 1`] = ` +[ + "[ 0/1 → 1/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 0/1 → 1/8 | note:C3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 1/8 → 1/4 | note:D3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 1/4 → 5/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 1/4 → 3/8 | note:Eb3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 3/8 → 1/2 | note:F3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 1/2 → 9/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 1/2 → 5/8 | note:G3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 5/8 → 3/4 | note:Ab3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 11/16 → 3/4 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 3/4 → 7/8 | note:Bb3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 7/8 → 15/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 7/8 → 1/1 | note:C4 s:sawtooth delay:0.7 orbit:2 ]", + "[ 1/1 → 17/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:0.9 ]", + "[ 1/1 → 9/8 | note:C3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 9/8 → 5/4 | note:D3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 5/4 → 21/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:0.9 ]", + "[ 5/4 → 11/8 | note:Eb3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 11/8 → 3/2 | note:F3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 3/2 → 25/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:0.9 ]", + "[ 3/2 → 13/8 | note:G3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 13/8 → 7/4 | note:Ab3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 27/16 → 7/4 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:0.9 ]", + "[ 7/4 → 15/8 | note:Bb3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 15/8 → 31/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:0.9 ]", + "[ 15/8 → 2/1 | note:C4 s:sawtooth delay:0.7 orbit:2 ]", + "[ 2/1 → 33/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:0.6 ]", + "[ 2/1 → 17/8 | note:C3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 17/8 → 9/4 | note:D3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 9/4 → 37/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:0.6 ]", + "[ 9/4 → 19/8 | note:Eb3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 19/8 → 5/2 | note:F3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 5/2 → 41/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:0.6 ]", + "[ 5/2 → 21/8 | note:G3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 21/8 → 11/4 | note:Ab3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 43/16 → 11/4 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:0.6 ]", + "[ 11/4 → 23/8 | note:Bb3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 23/8 → 47/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:0.6 ]", + "[ 23/8 → 3/1 | note:C4 s:sawtooth delay:0.7 orbit:2 ]", + "[ 3/1 → 49/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:0 ]", + "[ 3/1 → 25/8 | note:C3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 25/8 → 13/4 | note:D3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 13/4 → 53/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:0 ]", + "[ 13/4 → 27/8 | note:Eb3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 27/8 → 7/2 | note:F3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 7/2 → 57/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:0 ]", + "[ 7/2 → 29/8 | note:G3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 29/8 → 15/4 | note:Ab3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 59/16 → 15/4 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:0 ]", + "[ 15/4 → 31/8 | note:Bb3 s:sawtooth delay:0.7 orbit:2 ]", + "[ 31/8 → 63/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:0 ]", + "[ 31/8 → 4/1 | note:C4 s:sawtooth delay:0.7 orbit:2 ]", +] +`; + +exports[`runs examples > example "duckdepth" example index 1 1`] = ` +[ + "[ 0/1 → 1/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]", + "[ 1/4 → 5/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]", + "[ 1/2 → 9/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]", + "[ 11/16 → 3/4 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]", + "[ 7/8 → 15/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]", + "[ 1/1 → 17/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]", + "[ 5/4 → 21/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]", + "[ 3/2 → 25/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]", + "[ 27/16 → 7/4 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]", + "[ 15/8 → 31/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]", + "[ 2/1 → 33/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]", + "[ 9/4 → 37/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]", + "[ 5/2 → 41/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]", + "[ 43/16 → 11/4 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]", + "[ 23/8 → 47/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]", + "[ 3/1 → 49/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]", + "[ 13/4 → 53/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]", + "[ 7/2 → 57/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]", + "[ 59/16 → 15/4 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]", + "[ 31/8 → 63/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]", +] +`; + +exports[`runs examples > example "duckonset" example index 0 1`] = ` +[ + "[ 0/1 → 1/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]", + "[ 1/4 → 1/2 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]", + "[ 1/2 → 3/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]", + "[ 3/4 → 1/1 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]", + "[ 1/1 → 5/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]", + "[ 5/4 → 3/2 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]", + "[ 3/2 → 7/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]", + "[ 7/4 → 2/1 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]", + "[ 2/1 → 9/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]", + "[ 9/4 → 5/2 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]", + "[ 5/2 → 11/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]", + "[ 11/4 → 3/1 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]", + "[ 3/1 → 13/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]", + "[ 13/4 → 7/2 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]", + "[ 7/2 → 15/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]", + "[ 15/4 → 4/1 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]", +] +`; + +exports[`runs examples > example "duckonset" example index 1 1`] = ` +[ + "[ 0/1 → 1/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]", + "[ 1/4 → 1/2 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]", + "[ 1/2 → 3/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]", + "[ 3/4 → 1/1 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]", + "[ 1/1 → 5/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]", + "[ 5/4 → 3/2 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]", + "[ 3/2 → 7/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]", + "[ 7/4 → 2/1 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]", + "[ 2/1 → 9/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]", + "[ 9/4 → 5/2 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]", + "[ 5/2 → 11/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]", + "[ 11/4 → 3/1 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]", + "[ 3/1 → 13/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]", + "[ 13/4 → 7/2 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]", + "[ 7/2 → 15/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]", + "[ 15/4 → 4/1 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]", +] +`; + +exports[`runs examples > example "duckonset" example index 2 1`] = ` +[ + "[ 0/1 → 1/4 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]", + "[ 1/4 → 1/2 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]", + "[ 1/2 → 3/4 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]", + "[ 3/4 → 1/1 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]", + "[ 1/1 → 5/4 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]", + "[ 5/4 → 3/2 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]", + "[ 3/2 → 7/4 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]", + "[ 7/4 → 2/1 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]", + "[ 2/1 → 9/4 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]", + "[ 9/4 → 5/2 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]", + "[ 5/2 → 11/4 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]", + "[ 11/4 → 3/1 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]", + "[ 3/1 → 13/4 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]", + "[ 13/4 → 7/2 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]", + "[ 7/2 → 15/4 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]", + "[ 15/4 → 4/1 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]", +] +`; + +exports[`runs examples > example "duckorbit" example index 0 1`] = ` +[ + "[ 0/1 → 1/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 1/4 → 5/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 1/2 → 9/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 11/16 → 3/4 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 7/8 → 15/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 1/1 → 17/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 5/4 → 21/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 3/2 → 25/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 27/16 → 7/4 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 15/8 → 31/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 2/1 → 33/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 9/4 → 37/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 5/2 → 41/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 43/16 → 11/4 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 23/8 → 47/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 3/1 → 49/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 13/4 → 53/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 7/2 → 57/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 59/16 → 15/4 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", + "[ 31/8 → 63/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]", +] +`; + +exports[`runs examples > example "duckorbit" example index 1 1`] = ` +[ + "[ 0/1 → 1/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]", + "[ 1/4 → 5/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]", + "[ 1/2 → 9/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]", + "[ 11/16 → 3/4 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]", + "[ 7/8 → 15/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]", + "[ 1/1 → 17/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]", + "[ 5/4 → 21/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]", + "[ 3/2 → 25/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]", + "[ 27/16 → 7/4 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]", + "[ 15/8 → 31/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]", + "[ 2/1 → 33/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]", + "[ 9/4 → 37/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]", + "[ 5/2 → 41/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]", + "[ 43/16 → 11/4 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]", + "[ 23/8 → 47/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]", + "[ 3/1 → 49/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]", + "[ 13/4 → 53/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]", + "[ 7/2 → 57/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]", + "[ 59/16 → 15/4 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]", + "[ 31/8 → 63/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]", +] +`; + exports[`runs examples > example "duration" example index 0 1`] = ` [ "[ 0/1 → 1/4 | note:c s:piano duration:0.5 ]", @@ -3271,6 +3658,39 @@ exports[`runs examples > example "euclidRot" example index 0 1`] = ` ] `; +exports[`runs examples > example "euclidish" example index 0 1`] = ` +[ + "[ 0/1 → 1/12 | s:hh pan:0.5 ]", + "[ 13/84 → 5/21 | s:hh pan:0.5606253170575308 ]", + "[ 15/56 → 59/168 | s:hh pan:0.604413082836085 ]", + "[ 71/168 → 85/168 | s:hh pan:0.6629314122869361 ]", + "[ 97/168 → 37/56 | s:hh pan:0.7190455010067492 ]", + "[ 29/42 → 65/84 | s:hh pan:0.7580531369037533 ]", + "[ 71/84 → 13/14 | s:hh pan:0.8080762739548087 ]", + "[ 1/1 → 13/12 | s:hh pan:0.8535533905932737 ]", + "[ 451099417/393511398 → 322594689/262340932 | s:hh pan:0.891768001805729 ]", + "[ 335923379/262340932 → 536677685/393511398 | s:hh pan:0.9222657853371297 ]", + "[ 1122946175/787022796 → 99044284/65585233 | s:hh pan:0.9501869591788796 ]", + "[ 1238122213/787022796 → 651853723/393511398 | s:hh pan:0.9721673436944069 ]", + "[ 335923379/196755699 → 469759583/262340932 | s:hh pan:0.9868472639237561 ]", + "[ 729434777/393511398 → 1524454787/787022796 | s:hh pan:0.9967009321321423 ]", + "[ 2/1 → 25/12 | s:hh pan:1 ]", + "[ 15/7 → 187/84 | s:hh pan:0.9968561049466214 ]", + "[ 16/7 → 199/84 | s:hh pan:0.9874639560909118 ]", + "[ 17/7 → 211/84 | s:hh pan:0.9719416651541839 ]", + "[ 18/7 → 223/84 | s:hh pan:0.9504844339512095 ]", + "[ 19/7 → 235/84 | s:hh pan:0.9233620996141421 ]", + "[ 20/7 → 247/84 | s:hh pan:0.890915741234015 ]", + "[ 3/1 → 37/12 | s:hh pan:0.8535533905932737 ]", + "[ 1238122213/393511398 → 847276553/262340932 | s:hh pan:0.8106731928589048 ]", + "[ 860605243/262340932 → 1323700481/393511398 | s:hh pan:0.7677528833339 ]", + "[ 2696991767/787022796 → 230214750/65585233 | s:hh pan:0.7175585019834292 ]", + "[ 2812167805/787022796 → 1438876519/393511398 | s:hh pan:0.6644931595798675 ]", + "[ 729434777/196755699 → 994441447/262340932 | s:hh pan:0.6139286689554151 ]", + "[ 1516457573/393511398 → 3098500379/787022796 | s:hh pan:0.557342689325327 ]", +] +`; + exports[`runs examples > example "every" example index 0 1`] = ` [ "[ 0/1 → 1/4 | note:g3 ]", @@ -3418,6 +3838,8 @@ exports[`runs examples > example "fast" example index 0 1`] = ` exports[`runs examples > example "fastChunk" example index 0 1`] = ` [ + "[ 0/1 → 1/4 | color:red note:0 ]", + "[ 1/4 → 1/2 | color:red note:1 ]", "[ 1/2 → 3/4 | note:E2 ]", "[ 3/4 → 1/1 | note:F2 ]", "[ 1/1 → 5/4 | note:G2 ]", @@ -3426,6 +3848,8 @@ exports[`runs examples > example "fastChunk" example index 0 1`] = ` "[ 7/4 → 2/1 | note:C3 ]", "[ 2/1 → 9/4 | note:D3 ]", "[ 9/4 → 5/2 | note:D2 ]", + "[ 5/2 → 11/4 | color:red note:2 ]", + "[ 11/4 → 3/1 | color:red note:3 ]", "[ 3/1 → 13/4 | note:G2 ]", "[ 13/4 → 7/2 | note:A2 ]", "[ 7/2 → 15/4 | note:B2 ]", @@ -3706,6 +4130,144 @@ exports[`runs examples > example "fmsustain" example index 0 1`] = ` ] `; +exports[`runs examples > example "fmwave" example index 0 1`] = ` +[ + "[ 0/1 → 1/16 | note:D3 s:sine fmwave:sine fmi:4 fmh:2.01 ]", + "[ 1/16 → 1/8 | note:E3 s:sine fmwave:sine fmi:4 fmh:2.01 ]", + "[ 1/8 → 3/16 | note:F3 s:sine fmwave:sine fmi:4 fmh:2.01 ]", + "[ 3/16 → 1/4 | note:G3 s:sine fmwave:sine fmi:4 fmh:2.01 ]", + "[ 1/4 → 5/16 | note:D3 s:sine fmwave:sine fmi:4 fmh:2.01 ]", + "[ 5/16 → 3/8 | note:E3 s:sine fmwave:sine fmi:4 fmh:2.01 ]", + "[ 3/8 → 7/16 | note:F3 s:sine fmwave:sine fmi:4 fmh:2.01 ]", + "[ 7/16 → 1/2 | note:G3 s:sine fmwave:sine fmi:4 fmh:2.01 ]", + "[ 1/2 → 9/16 | note:D3 s:sine fmwave:sine fmi:4 fmh:2.01 ]", + "[ 9/16 → 5/8 | note:E3 s:sine fmwave:sine fmi:4 fmh:2.01 ]", + "[ 5/8 → 11/16 | note:F3 s:sine fmwave:sine fmi:4 fmh:2.01 ]", + "[ 11/16 → 3/4 | note:G3 s:sine fmwave:sine fmi:4 fmh:2.01 ]", + "[ 3/4 → 13/16 | note:D3 s:sine fmwave:sine fmi:4 fmh:2.01 ]", + "[ 13/16 → 7/8 | note:E3 s:sine fmwave:sine fmi:4 fmh:2.01 ]", + "[ 7/8 → 15/16 | note:F3 s:sine fmwave:sine fmi:4 fmh:2.01 ]", + "[ 15/16 → 1/1 | note:G3 s:sine fmwave:sine fmi:4 fmh:2.01 ]", + "[ 1/1 → 17/16 | note:D3 s:sine fmwave:square fmi:4 fmh:2.01 ]", + "[ 17/16 → 9/8 | note:E3 s:sine fmwave:square fmi:4 fmh:2.01 ]", + "[ 9/8 → 19/16 | note:F3 s:sine fmwave:square fmi:4 fmh:2.01 ]", + "[ 19/16 → 5/4 | note:G3 s:sine fmwave:square fmi:4 fmh:2.01 ]", + "[ 5/4 → 21/16 | note:D3 s:sine fmwave:square fmi:4 fmh:2.01 ]", + "[ 21/16 → 11/8 | note:E3 s:sine fmwave:square fmi:4 fmh:2.01 ]", + "[ 11/8 → 23/16 | note:F3 s:sine fmwave:square fmi:4 fmh:2.01 ]", + "[ 23/16 → 3/2 | note:G3 s:sine fmwave:square fmi:4 fmh:2.01 ]", + "[ 3/2 → 25/16 | note:D3 s:sine fmwave:square fmi:4 fmh:2.01 ]", + "[ 25/16 → 13/8 | note:E3 s:sine fmwave:square fmi:4 fmh:2.01 ]", + "[ 13/8 → 27/16 | note:F3 s:sine fmwave:square fmi:4 fmh:2.01 ]", + "[ 27/16 → 7/4 | note:G3 s:sine fmwave:square fmi:4 fmh:2.01 ]", + "[ 7/4 → 29/16 | note:D3 s:sine fmwave:square fmi:4 fmh:2.01 ]", + "[ 29/16 → 15/8 | note:E3 s:sine fmwave:square fmi:4 fmh:2.01 ]", + "[ 15/8 → 31/16 | note:F3 s:sine fmwave:square fmi:4 fmh:2.01 ]", + "[ 31/16 → 2/1 | note:G3 s:sine fmwave:square fmi:4 fmh:2.01 ]", + "[ 2/1 → 33/16 | note:D3 s:sine fmwave:sawtooth fmi:4 fmh:2.01 ]", + "[ 33/16 → 17/8 | note:E3 s:sine fmwave:sawtooth fmi:4 fmh:2.01 ]", + "[ 17/8 → 35/16 | note:F3 s:sine fmwave:sawtooth fmi:4 fmh:2.01 ]", + "[ 35/16 → 9/4 | note:G3 s:sine fmwave:sawtooth fmi:4 fmh:2.01 ]", + "[ 9/4 → 37/16 | note:D3 s:sine fmwave:sawtooth fmi:4 fmh:2.01 ]", + "[ 37/16 → 19/8 | note:E3 s:sine fmwave:sawtooth fmi:4 fmh:2.01 ]", + "[ 19/8 → 39/16 | note:F3 s:sine fmwave:sawtooth fmi:4 fmh:2.01 ]", + "[ 39/16 → 5/2 | note:G3 s:sine fmwave:sawtooth fmi:4 fmh:2.01 ]", + "[ 5/2 → 41/16 | note:D3 s:sine fmwave:sawtooth fmi:4 fmh:2.01 ]", + "[ 41/16 → 21/8 | note:E3 s:sine fmwave:sawtooth fmi:4 fmh:2.01 ]", + "[ 21/8 → 43/16 | note:F3 s:sine fmwave:sawtooth fmi:4 fmh:2.01 ]", + "[ 43/16 → 11/4 | note:G3 s:sine fmwave:sawtooth fmi:4 fmh:2.01 ]", + "[ 11/4 → 45/16 | note:D3 s:sine fmwave:sawtooth fmi:4 fmh:2.01 ]", + "[ 45/16 → 23/8 | note:E3 s:sine fmwave:sawtooth fmi:4 fmh:2.01 ]", + "[ 23/8 → 47/16 | note:F3 s:sine fmwave:sawtooth fmi:4 fmh:2.01 ]", + "[ 47/16 → 3/1 | note:G3 s:sine fmwave:sawtooth fmi:4 fmh:2.01 ]", + "[ 3/1 → 49/16 | note:D3 s:sine fmwave:crackle fmi:4 fmh:2.01 ]", + "[ 49/16 → 25/8 | note:E3 s:sine fmwave:crackle fmi:4 fmh:2.01 ]", + "[ 25/8 → 51/16 | note:F3 s:sine fmwave:crackle fmi:4 fmh:2.01 ]", + "[ 51/16 → 13/4 | note:G3 s:sine fmwave:crackle fmi:4 fmh:2.01 ]", + "[ 13/4 → 53/16 | note:D3 s:sine fmwave:crackle fmi:4 fmh:2.01 ]", + "[ 53/16 → 27/8 | note:E3 s:sine fmwave:crackle fmi:4 fmh:2.01 ]", + "[ 27/8 → 55/16 | note:F3 s:sine fmwave:crackle fmi:4 fmh:2.01 ]", + "[ 55/16 → 7/2 | note:G3 s:sine fmwave:crackle fmi:4 fmh:2.01 ]", + "[ 7/2 → 57/16 | note:D3 s:sine fmwave:crackle fmi:4 fmh:2.01 ]", + "[ 57/16 → 29/8 | note:E3 s:sine fmwave:crackle fmi:4 fmh:2.01 ]", + "[ 29/8 → 59/16 | note:F3 s:sine fmwave:crackle fmi:4 fmh:2.01 ]", + "[ 59/16 → 15/4 | note:G3 s:sine fmwave:crackle fmi:4 fmh:2.01 ]", + "[ 15/4 → 61/16 | note:D3 s:sine fmwave:crackle fmi:4 fmh:2.01 ]", + "[ 61/16 → 31/8 | note:E3 s:sine fmwave:crackle fmi:4 fmh:2.01 ]", + "[ 31/8 → 63/16 | note:F3 s:sine fmwave:crackle fmi:4 fmh:2.01 ]", + "[ 63/16 → 4/1 | note:G3 s:sine fmwave:crackle fmi:4 fmh:2.01 ]", +] +`; + +exports[`runs examples > example "fmwave" example index 1 1`] = ` +[ + "[ 0/1 → 1/16 | note:50 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 1/16 → 1/8 | note:57 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 1/8 → 3/16 | note:62 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 3/16 → 1/4 | note:65 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 1/4 → 5/16 | note:50 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 5/16 → 3/8 | note:57 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 3/8 → 7/16 | note:62 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 7/16 → 1/2 | note:65 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 1/2 → 9/16 | note:50 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 9/16 → 5/8 | note:57 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 5/8 → 11/16 | note:62 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 11/16 → 3/4 | note:65 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 3/4 → 13/16 | note:50 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 13/16 → 7/8 | note:57 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 7/8 → 15/16 | note:62 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 15/16 → 1/1 | note:65 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 1/1 → 17/16 | note:57 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 17/16 → 9/8 | note:60 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 9/8 → 19/16 | note:64 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 19/16 → 5/4 | note:69 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 5/4 → 21/16 | note:57 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 21/16 → 11/8 | note:60 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 11/8 → 23/16 | note:64 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 23/16 → 3/2 | note:69 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 3/2 → 25/16 | note:57 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 25/16 → 13/8 | note:60 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 13/8 → 27/16 | note:64 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 27/16 → 7/4 | note:69 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 7/4 → 29/16 | note:57 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 29/16 → 15/8 | note:60 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 15/8 → 31/16 | note:64 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 31/16 → 2/1 | note:69 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 2/1 → 33/16 | note:53 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 33/16 → 17/8 | note:60 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 17/8 → 35/16 | note:65 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 35/16 → 9/4 | note:69 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 9/4 → 37/16 | note:53 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 37/16 → 19/8 | note:60 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 19/8 → 39/16 | note:65 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 39/16 → 5/2 | note:69 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 5/2 → 41/16 | note:53 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 41/16 → 21/8 | note:60 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 21/8 → 43/16 | note:65 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 43/16 → 11/4 | note:69 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 11/4 → 45/16 | note:53 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 45/16 → 23/8 | note:60 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 23/8 → 47/16 | note:65 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 47/16 → 3/1 | note:69 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 3/1 → 49/16 | note:55 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 49/16 → 25/8 | note:62 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 25/8 → 51/16 | note:67 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 51/16 → 13/4 | note:71 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 13/4 → 53/16 | note:55 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 53/16 → 27/8 | note:62 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 27/8 → 55/16 | note:67 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 55/16 → 7/2 | note:71 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 7/2 → 57/16 | note:55 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 57/16 → 29/8 | note:62 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 29/8 → 59/16 | note:67 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 59/16 → 15/4 | note:71 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 15/4 → 61/16 | note:55 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 61/16 → 31/8 | note:62 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 31/8 → 63/16 | note:67 s:sawtooth fmwave:brown fmi:0.6 ]", + "[ 63/16 → 4/1 | note:71 s:sawtooth fmwave:brown fmi:0.6 ]", +] +`; + exports[`runs examples > example "focus" example index 0 1`] = ` [ "[ 0/1 → 1/8 | s:sd ]", @@ -4634,6 +5196,43 @@ exports[`runs examples > example "irand" example index 0 1`] = ` ] `; +exports[`runs examples > example "irbegin" example index 0 1`] = ` +[ + "[ 0/1 → 1/8 | s:brk speed:0.5 unit:c begin:0 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", + "[ 1/8 → 1/4 | s:brk speed:0.5 unit:c begin:0.625 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", + "[ 1/4 → 3/8 | s:brk speed:0.5 unit:c begin:0.3125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", + "[ 3/8 → 1/2 | s:brk speed:0.5 unit:c begin:0.375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", + "[ 1/2 → 5/8 | s:brk speed:0.5 unit:c begin:0.25 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", + "[ 5/8 → 3/4 | s:brk speed:0.5 unit:c begin:0.125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", + "[ 3/4 → 7/8 | s:brk speed:0.5 unit:c begin:0.1875 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", + "[ 7/8 → 1/1 | s:brk speed:0.5 unit:c begin:0.375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", + "[ 1/1 → 9/8 | s:brk speed:0.5 unit:c begin:0.5 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", + "[ 9/8 → 5/4 | s:brk speed:0.5 unit:c begin:0.625 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", + "[ 5/4 → 11/8 | s:brk speed:0.5 unit:c begin:0.6875 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", + "[ 11/8 → 3/2 | s:brk speed:0.5 unit:c begin:0.125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", + "[ 3/2 → 13/8 | s:brk speed:0.5 unit:c begin:0.5625 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", + "[ 13/8 → 7/4 | s:brk speed:0.5 unit:c begin:0.4375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", + "[ 7/4 → 15/8 | s:brk speed:0.5 unit:c begin:0.1875 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", + "[ 15/8 → 2/1 | s:brk speed:0.5 unit:c begin:0.5625 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]", + "[ 2/1 → 17/8 | s:brk speed:0.5 unit:c begin:0.9375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", + "[ 17/8 → 9/4 | s:brk speed:0.5 unit:c begin:0.875 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", + "[ 9/4 → 19/8 | s:brk speed:0.5 unit:c begin:0.3125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", + "[ 19/8 → 5/2 | s:brk speed:0.5 unit:c begin:0.8125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", + "[ 5/2 → 21/8 | s:brk speed:0.5 unit:c begin:0.4375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", + "[ 21/8 → 11/4 | s:brk speed:0.5 unit:c begin:0.125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", + "[ 11/4 → 23/8 | s:brk speed:0.5 unit:c begin:0.625 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", + "[ 23/8 → 3/1 | s:brk speed:0.5 unit:c begin:0 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", + "[ 3/1 → 25/8 | s:brk speed:0.5 unit:c begin:0.1875 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", + "[ 25/8 → 13/4 | s:brk speed:0.5 unit:c begin:0.3125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", + "[ 13/4 → 27/8 | s:brk speed:0.5 unit:c begin:0.9375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", + "[ 27/8 → 7/2 | s:brk speed:0.5 unit:c begin:0.375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", + "[ 7/2 → 29/8 | s:brk speed:0.5 unit:c begin:0.375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", + "[ 29/8 → 15/4 | s:brk speed:0.5 unit:c begin:0.9375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", + "[ 15/4 → 31/8 | s:brk speed:0.5 unit:c begin:0.75 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", + "[ 31/8 → 4/1 | s:brk speed:0.5 unit:c begin:0.6875 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]", +] +`; + exports[`runs examples > example "iresponse" example index 0 1`] = ` [ "[ 0/1 → 1/4 | s:bd room:0.8 ir:shaker_large i:0 ]", @@ -4655,6 +5254,43 @@ exports[`runs examples > example "iresponse" example index 0 1`] = ` ] `; +exports[`runs examples > example "irspeed" example index 0 1`] = ` +[ + "[ 0/1 → 1/8 | s:brk speed:0.5 unit:c begin:0 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", + "[ 1/8 → 1/4 | s:brk speed:0.5 unit:c begin:0.625 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", + "[ 1/4 → 3/8 | s:brk speed:0.5 unit:c begin:0.3125 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", + "[ 3/8 → 1/2 | s:brk speed:0.5 unit:c begin:0.375 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", + "[ 1/2 → 5/8 | s:brk speed:0.5 unit:c begin:0.25 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", + "[ 5/8 → 3/4 | s:brk speed:0.5 unit:c begin:0.125 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", + "[ 3/4 → 7/8 | s:brk speed:0.5 unit:c begin:0.1875 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", + "[ 7/8 → 1/1 | s:brk speed:0.5 unit:c begin:0.375 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", + "[ 1/1 → 9/8 | s:brk speed:0.5 unit:c begin:0.5 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", + "[ 9/8 → 5/4 | s:brk speed:0.5 unit:c begin:0.625 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", + "[ 5/4 → 11/8 | s:brk speed:0.5 unit:c begin:0.6875 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", + "[ 11/8 → 3/2 | s:brk speed:0.5 unit:c begin:0.125 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", + "[ 3/2 → 13/8 | s:brk speed:0.5 unit:c begin:0.5625 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", + "[ 13/8 → 7/4 | s:brk speed:0.5 unit:c begin:0.4375 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", + "[ 7/4 → 15/8 | s:brk speed:0.5 unit:c begin:0.1875 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", + "[ 15/8 → 2/1 | s:brk speed:0.5 unit:c begin:0.5625 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]", + "[ 2/1 → 17/8 | s:brk speed:0.5 unit:c begin:0.9375 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", + "[ 17/8 → 9/4 | s:brk speed:0.5 unit:c begin:0.875 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", + "[ 9/4 → 19/8 | s:brk speed:0.5 unit:c begin:0.3125 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", + "[ 19/8 → 5/2 | s:brk speed:0.5 unit:c begin:0.8125 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", + "[ 5/2 → 21/8 | s:brk speed:0.5 unit:c begin:0.4375 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", + "[ 21/8 → 11/4 | s:brk speed:0.5 unit:c begin:0.125 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", + "[ 11/4 → 23/8 | s:brk speed:0.5 unit:c begin:0.625 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", + "[ 23/8 → 3/1 | s:brk speed:0.5 unit:c begin:0 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", + "[ 3/1 → 25/8 | s:brk speed:0.5 unit:c begin:0.1875 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", + "[ 25/8 → 13/4 | s:brk speed:0.5 unit:c begin:0.3125 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", + "[ 13/4 → 27/8 | s:brk speed:0.5 unit:c begin:0.9375 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", + "[ 27/8 → 7/2 | s:brk speed:0.5 unit:c begin:0.375 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", + "[ 7/2 → 29/8 | s:brk speed:0.5 unit:c begin:0.375 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", + "[ 29/8 → 15/4 | s:brk speed:0.5 unit:c begin:0.9375 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", + "[ 15/4 → 31/8 | s:brk speed:0.5 unit:c begin:0.75 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", + "[ 31/8 → 4/1 | s:brk speed:0.5 unit:c begin:0.6875 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]", +] +`; + exports[`runs examples > example "isaw" example index 0 1`] = ` [ "[ 0/1 → 1/8 | note:c3 clip:1 ]", @@ -5216,6 +5852,40 @@ exports[`runs examples > example "lock" example index 0 1`] = ` ] `; +exports[`runs examples > example "log" example index 0 1`] = ` +[ + "[ 0/1 → 1/2 | s:bd ]", + "[ 1/2 → 1/1 | s:sd ]", + "[ 1/1 → 3/2 | s:bd ]", + "[ 3/2 → 2/1 | s:sd ]", + "[ 2/1 → 5/2 | s:bd ]", + "[ 5/2 → 3/1 | s:sd ]", + "[ 3/1 → 7/2 | s:bd ]", + "[ 7/2 → 4/1 | s:sd ]", +] +`; + +exports[`runs examples > example "logValues" example index 0 1`] = ` +[ + "[ (0/1 → 1/3) ⇝ 1/2 | s:bd gain:0.25 n:2 ]", + "[ 0/1 ⇜ (1/3 → 1/2) | s:bd gain:0.5 n:1 ]", + "[ (1/2 → 2/3) ⇝ 1/1 | s:sd gain:0.5 n:1 ]", + "[ 1/2 ⇜ (2/3 → 1/1) | s:sd gain:1 n:0 ]", + "[ (1/1 → 4/3) ⇝ 3/2 | s:bd gain:0.25 n:2 ]", + "[ 1/1 ⇜ (4/3 → 3/2) | s:bd gain:0.5 n:1 ]", + "[ (3/2 → 5/3) ⇝ 2/1 | s:sd gain:0.5 n:1 ]", + "[ 3/2 ⇜ (5/3 → 2/1) | s:sd gain:1 n:0 ]", + "[ (2/1 → 7/3) ⇝ 5/2 | s:bd gain:0.25 n:2 ]", + "[ 2/1 ⇜ (7/3 → 5/2) | s:bd gain:0.5 n:1 ]", + "[ (5/2 → 8/3) ⇝ 3/1 | s:sd gain:0.5 n:1 ]", + "[ 5/2 ⇜ (8/3 → 3/1) | s:sd gain:1 n:0 ]", + "[ (3/1 → 10/3) ⇝ 7/2 | s:bd gain:0.25 n:2 ]", + "[ 3/1 ⇜ (10/3 → 7/2) | s:bd gain:0.5 n:1 ]", + "[ (7/2 → 11/3) ⇝ 4/1 | s:sd gain:0.5 n:1 ]", + "[ 7/2 ⇜ (11/3 → 4/1) | s:sd gain:1 n:0 ]", +] +`; + exports[`runs examples > example "loop" example index 0 1`] = ` [ "[ 0/1 → 1/1 | s:casio loop:1 ]", @@ -5895,6 +6565,48 @@ exports[`runs examples > example "miditouch" example index 0 1`] = ` ] `; +exports[`runs examples > example "morph" example index 0 1`] = ` +[ + "[ 0/1 → 1/8 | s:hh ]", + "[ 25/112 → 39/112 | s:hh ]", + "[ 27/56 → 17/28 | s:hh ]", + "[ 83/112 → 97/112 | s:hh ]", + "[ 1/1 → 9/8 | s:hh ]", + "[ 137/112 → 151/112 | s:hh ]", + "[ 83/56 → 45/28 | s:hh ]", + "[ 195/112 → 209/112 | s:hh ]", + "[ 2/1 → 17/8 | s:hh ]", + "[ 249/112 → 263/112 | s:hh ]", + "[ 139/56 → 73/28 | s:hh ]", + "[ 307/112 → 321/112 | s:hh ]", + "[ 3/1 → 25/8 | s:hh ]", + "[ 361/112 → 375/112 | s:hh ]", + "[ 195/56 → 101/28 | s:hh ]", + "[ 419/112 → 433/112 | s:hh ]", +] +`; + +exports[`runs examples > example "morph" example index 1 1`] = ` +[ + "[ 0/1 → 1/8 | s:hh ]", + "[ 11/56 → 9/28 | s:hh ]", + "[ 13/28 → 33/56 | s:hh ]", + "[ 41/56 → 6/7 | s:hh ]", + "[ 1/1 → 9/8 | s:hh ]", + "[ 303934523/262340932 → 673454279/524681864 | s:hh ]", + "[ 188758485/131170466 → 820619173/524681864 | s:hh ]", + "[ 451099417/262340932 → 967784067/524681864 | s:hh ]", + "[ 2/1 → 17/8 | s:hh ]", + "[ 15/7 → 127/56 | s:hh ]", + "[ 17/7 → 143/56 | s:hh ]", + "[ 19/7 → 159/56 | s:hh ]", + "[ 3/1 → 25/8 | s:hh ]", + "[ 828616387/262340932 → 1722818007/524681864 | s:hh ]", + "[ 451099417/131170466 → 1869982901/524681864 | s:hh ]", + "[ 975781281/262340932 → 2017147795/524681864 | s:hh ]", +] +`; + exports[`runs examples > example "mousex" example index 0 1`] = ` [ "[ 0/1 → 1/4 | note:C3 ]", @@ -6115,6 +6827,27 @@ exports[`runs examples > example "note" example index 2 1`] = ` ] `; +exports[`runs examples > example "note" example index 3 1`] = ` +[ + "[ 0/1 → 1/4 | note:fbb1 s:saw ]", + "[ 1/4 → 1/2 | note:a#0 s:saw ]", + "[ 1/2 → 3/4 | note:cbbb-1 s:saw ]", + "[ 3/4 → 1/1 | note:e##-2 s:saw ]", + "[ 1/1 → 5/4 | note:fbb1 s:saw ]", + "[ 5/4 → 3/2 | note:a#0 s:saw ]", + "[ 3/2 → 7/4 | note:cbbb-1 s:saw ]", + "[ 7/4 → 2/1 | note:e##-2 s:saw ]", + "[ 2/1 → 9/4 | note:fbb1 s:saw ]", + "[ 9/4 → 5/2 | note:a#0 s:saw ]", + "[ 5/2 → 11/4 | note:cbbb-1 s:saw ]", + "[ 11/4 → 3/1 | note:e##-2 s:saw ]", + "[ 3/1 → 13/4 | note:fbb1 s:saw ]", + "[ 13/4 → 7/2 | note:a#0 s:saw ]", + "[ 7/2 → 15/4 | note:cbbb-1 s:saw ]", + "[ 15/4 → 4/1 | note:e##-2 s:saw ]", +] +`; + exports[`runs examples > example "nrpnn" example index 0 1`] = ` [ "[ 0/1 → 1/1 | note:c4 nrpnn:[1 8] nrpv:123 midichan:1 ]", @@ -6920,6 +7653,64 @@ exports[`runs examples > example "ply" example index 0 1`] = ` ] `; +exports[`runs examples > example "plyForEach" example index 0 1`] = ` +[ + "[ 0/1 → 1/4 | note:C3 ]", + "[ 1/4 → 1/2 | note:Eb3 ]", + "[ 1/2 → 3/4 | note:G3 ]", + "[ 3/4 → 1/1 | note:Bb3 ]", + "[ 1/1 → 9/8 | note:Eb3 ]", + "[ 9/8 → 5/4 | note:G3 ]", + "[ 5/4 → 11/8 | note:Bb3 ]", + "[ 11/8 → 3/2 | note:D4 ]", + "[ 3/2 → 13/8 | note:G3 ]", + "[ 13/8 → 7/4 | note:Bb3 ]", + "[ 7/4 → 15/8 | note:D4 ]", + "[ 15/8 → 2/1 | note:F4 ]", + "[ 2/1 → 9/4 | note:C3 ]", + "[ 9/4 → 5/2 | note:Eb3 ]", + "[ 5/2 → 11/4 | note:G3 ]", + "[ 11/4 → 3/1 | note:Bb3 ]", + "[ 3/1 → 25/8 | note:Eb3 ]", + "[ 25/8 → 13/4 | note:G3 ]", + "[ 13/4 → 27/8 | note:Bb3 ]", + "[ 27/8 → 7/2 | note:D4 ]", + "[ 7/2 → 29/8 | note:G3 ]", + "[ 29/8 → 15/4 | note:Bb3 ]", + "[ 15/4 → 31/8 | note:D4 ]", + "[ 31/8 → 4/1 | note:F4 ]", +] +`; + +exports[`runs examples > example "plyWith" example index 0 1`] = ` +[ + "[ 0/1 → 1/4 | note:C3 ]", + "[ 1/4 → 1/2 | note:Eb3 ]", + "[ 1/2 → 3/4 | note:G3 ]", + "[ 3/4 → 1/1 | note:Bb3 ]", + "[ 1/1 → 9/8 | note:Eb3 ]", + "[ 9/8 → 5/4 | note:G3 ]", + "[ 5/4 → 11/8 | note:Bb3 ]", + "[ 11/8 → 3/2 | note:D4 ]", + "[ 3/2 → 13/8 | note:G3 ]", + "[ 13/8 → 7/4 | note:Bb3 ]", + "[ 7/4 → 15/8 | note:D4 ]", + "[ 15/8 → 2/1 | note:F4 ]", + "[ 2/1 → 9/4 | note:C3 ]", + "[ 9/4 → 5/2 | note:Eb3 ]", + "[ 5/2 → 11/4 | note:G3 ]", + "[ 11/4 → 3/1 | note:Bb3 ]", + "[ 3/1 → 25/8 | note:Eb3 ]", + "[ 25/8 → 13/4 | note:G3 ]", + "[ 13/4 → 27/8 | note:Bb3 ]", + "[ 27/8 → 7/2 | note:D4 ]", + "[ 7/2 → 29/8 | note:G3 ]", + "[ 29/8 → 15/4 | note:Bb3 ]", + "[ 15/4 → 31/8 | note:D4 ]", + "[ 31/8 → 4/1 | note:F4 ]", +] +`; + exports[`runs examples > example "polymeter" example index 0 1`] = ` [ "[ 0/1 → 1/6 | note:c ]", @@ -8345,6 +9136,108 @@ exports[`runs examples > example "scale" example index 2 1`] = ` ] `; +exports[`runs examples > example "scale" example index 3 1`] = ` +[ + "[ 0/1 → 1/4 | note:C3 s:piano ]", + "[ 0/1 → 1/4 | note:B3 s:piano ]", + "[ 1/4 → 3/8 | note:Gb2 s:piano ]", + "[ 3/8 → 1/2 | note:F2 s:piano ]", + "[ 1/2 → 3/4 | note:A2 s:piano ]", + "[ 1/2 → 3/4 | note:D4 s:piano ]", + "[ 3/4 → 1/1 | note:G3 s:piano ]", + "[ 1/1 → 5/4 | note:C3 s:piano ]", + "[ 1/1 → 5/4 | note:C4 s:piano ]", + "[ 5/4 → 11/8 | note:Gb2 s:piano ]", + "[ 11/8 → 3/2 | note:E2 s:piano ]", + "[ 3/2 → 7/4 | note:A2 s:piano ]", + "[ 3/2 → 7/4 | note:Eb4 s:piano ]", + "[ 7/4 → 2/1 | note:F#3 s:piano ]", + "[ 2/1 → 9/4 | note:C3 s:piano ]", + "[ 2/1 → 9/4 | note:B3 s:piano ]", + "[ 9/4 → 19/8 | note:Gb2 s:piano ]", + "[ 19/8 → 5/2 | note:F2 s:piano ]", + "[ 5/2 → 11/4 | note:Ab2 s:piano ]", + "[ 5/2 → 11/4 | note:D4 s:piano ]", + "[ 11/4 → 3/1 | note:G3 s:piano ]", + "[ 3/1 → 13/4 | note:C3 s:piano ]", + "[ 3/1 → 13/4 | note:C4 s:piano ]", + "[ 13/4 → 27/8 | note:Gb2 s:piano ]", + "[ 27/8 → 7/2 | note:E2 s:piano ]", + "[ 7/2 → 15/4 | note:Ab2 s:piano ]", + "[ 7/2 → 15/4 | note:Eb4 s:piano ]", + "[ 15/4 → 4/1 | note:F#3 s:piano ]", +] +`; + +exports[`runs examples > example "scale" example index 4 1`] = ` +[ + "[ 0/1 → 1/16 | note:Gb1 ]", + "[ 1/16 → 1/8 | note:Gb1 ]", + "[ 1/8 → 3/16 | note:Gb1 ]", + "[ 3/16 → 1/4 | note:Gb1 ]", + "[ 1/4 → 5/16 | note:Gb1 ]", + "[ 5/16 → 3/8 | note:Gb1 ]", + "[ 3/8 → 7/16 | note:Gb1 ]", + "[ 7/16 → 1/2 | note:Gb1 ]", + "[ 1/2 → 9/16 | note:Gb1 ]", + "[ 9/16 → 5/8 | note:Gb1 ]", + "[ 5/8 → 11/16 | note:Gb1 ]", + "[ 11/16 → 3/4 | note:Gb1 ]", + "[ 3/4 → 13/16 | note:Gb1 ]", + "[ 13/16 → 7/8 | note:Gb1 ]", + "[ 7/8 → 15/16 | note:Gb1 ]", + "[ 15/16 → 1/1 | note:Gb1 ]", + "[ 1/1 → 17/16 | note:Cb3 ]", + "[ 17/16 → 9/8 | note:Cb3 ]", + "[ 9/8 → 19/16 | note:Cb3 ]", + "[ 19/16 → 5/4 | note:Cb3 ]", + "[ 5/4 → 21/16 | note:Cb3 ]", + "[ 21/16 → 11/8 | note:Cb3 ]", + "[ 11/8 → 23/16 | note:Cb3 ]", + "[ 23/16 → 3/2 | note:Cb3 ]", + "[ 3/2 → 25/16 | note:Cb3 ]", + "[ 25/16 → 13/8 | note:Cb3 ]", + "[ 13/8 → 27/16 | note:Cb3 ]", + "[ 27/16 → 7/4 | note:Cb3 ]", + "[ 7/4 → 29/16 | note:Cb3 ]", + "[ 29/16 → 15/8 | note:Cb3 ]", + "[ 15/8 → 31/16 | note:Cb3 ]", + "[ 31/16 → 2/1 | note:Cb3 ]", + "[ 2/1 → 33/16 | note:Eb4 ]", + "[ 33/16 → 17/8 | note:Eb4 ]", + "[ 17/8 → 35/16 | note:Eb4 ]", + "[ 35/16 → 9/4 | note:Eb4 ]", + "[ 9/4 → 37/16 | note:Eb4 ]", + "[ 37/16 → 19/8 | note:Eb4 ]", + "[ 19/8 → 39/16 | note:Eb4 ]", + "[ 39/16 → 5/2 | note:Eb4 ]", + "[ 5/2 → 41/16 | note:Eb4 ]", + "[ 41/16 → 21/8 | note:Eb4 ]", + "[ 21/8 → 43/16 | note:Eb4 ]", + "[ 43/16 → 11/4 | note:Eb4 ]", + "[ 11/4 → 45/16 | note:Eb4 ]", + "[ 45/16 → 23/8 | note:Eb4 ]", + "[ 23/8 → 47/16 | note:Eb4 ]", + "[ 47/16 → 3/1 | note:Eb4 ]", + "[ 3/1 → 49/16 | note:Db2 ]", + "[ 49/16 → 25/8 | note:Db2 ]", + "[ 25/8 → 51/16 | note:Db2 ]", + "[ 51/16 → 13/4 | note:Db2 ]", + "[ 13/4 → 53/16 | note:Db2 ]", + "[ 53/16 → 27/8 | note:Db2 ]", + "[ 27/8 → 55/16 | note:Db2 ]", + "[ 55/16 → 7/2 | note:Db2 ]", + "[ 7/2 → 57/16 | note:Db2 ]", + "[ 57/16 → 29/8 | note:Db2 ]", + "[ 29/8 → 59/16 | note:Db2 ]", + "[ 59/16 → 15/4 | note:Db2 ]", + "[ 15/4 → 61/16 | note:Db2 ]", + "[ 61/16 → 31/8 | note:Db2 ]", + "[ 31/8 → 63/16 | note:Db2 ]", + "[ 63/16 → 4/1 | note:Db2 ]", +] +`; + exports[`runs examples > example "scaleTranspose" example index 0 1`] = ` [ "[ 0/1 → 1/2 | note:C3 ]", @@ -8882,46 +9775,46 @@ exports[`runs examples > example "shrink" example index 3 1`] = ` exports[`runs examples > example "shuffle" example index 0 1`] = ` [ - "[ 0/1 → 1/4 | note:c s:piano ]", + "[ 0/1 → 1/4 | note:e s:piano ]", "[ 1/4 → 1/2 | note:d s:piano ]", - "[ 1/2 → 3/4 | note:e s:piano ]", - "[ 3/4 → 1/1 | note:f s:piano ]", - "[ 1/1 → 5/4 | note:c s:piano ]", - "[ 5/4 → 3/2 | note:d s:piano ]", - "[ 3/2 → 7/4 | note:e s:piano ]", - "[ 7/4 → 2/1 | note:f s:piano ]", - "[ 2/1 → 9/4 | note:c s:piano ]", - "[ 9/4 → 5/2 | note:d s:piano ]", + "[ 1/2 → 3/4 | note:f s:piano ]", + "[ 3/4 → 1/1 | note:c s:piano ]", + "[ 1/1 → 5/4 | note:e s:piano ]", + "[ 5/4 → 3/2 | note:c s:piano ]", + "[ 3/2 → 7/4 | note:f s:piano ]", + "[ 7/4 → 2/1 | note:d s:piano ]", + "[ 2/1 → 9/4 | note:d s:piano ]", + "[ 9/4 → 5/2 | note:c s:piano ]", "[ 5/2 → 11/4 | note:e s:piano ]", "[ 11/4 → 3/1 | note:f s:piano ]", "[ 3/1 → 13/4 | note:c s:piano ]", - "[ 13/4 → 7/2 | note:d s:piano ]", - "[ 7/2 → 15/4 | note:e s:piano ]", - "[ 15/4 → 4/1 | note:f s:piano ]", + "[ 13/4 → 7/2 | note:e s:piano ]", + "[ 7/2 → 15/4 | note:f s:piano ]", + "[ 15/4 → 4/1 | note:d s:piano ]", ] `; exports[`runs examples > example "shuffle" example index 1 1`] = ` [ - "[ 0/1 → 1/8 | note:c s:piano ]", + "[ 0/1 → 1/8 | note:e s:piano ]", "[ 1/8 → 1/4 | note:d s:piano ]", - "[ 1/4 → 3/8 | note:e s:piano ]", - "[ 3/8 → 1/2 | note:f s:piano ]", + "[ 1/4 → 3/8 | note:f s:piano ]", + "[ 3/8 → 1/2 | note:c s:piano ]", "[ 1/2 → 1/1 | note:g s:piano ]", - "[ 1/1 → 9/8 | note:c s:piano ]", - "[ 9/8 → 5/4 | note:d s:piano ]", - "[ 5/4 → 11/8 | note:e s:piano ]", - "[ 11/8 → 3/2 | note:f s:piano ]", + "[ 1/1 → 9/8 | note:e s:piano ]", + "[ 9/8 → 5/4 | note:c s:piano ]", + "[ 5/4 → 11/8 | note:f s:piano ]", + "[ 11/8 → 3/2 | note:d s:piano ]", "[ 3/2 → 2/1 | note:g s:piano ]", - "[ 2/1 → 17/8 | note:c s:piano ]", - "[ 17/8 → 9/4 | note:d s:piano ]", + "[ 2/1 → 17/8 | note:d s:piano ]", + "[ 17/8 → 9/4 | note:c s:piano ]", "[ 9/4 → 19/8 | note:e s:piano ]", "[ 19/8 → 5/2 | note:f s:piano ]", "[ 5/2 → 3/1 | note:g s:piano ]", "[ 3/1 → 25/8 | note:c s:piano ]", - "[ 25/8 → 13/4 | note:d s:piano ]", - "[ 13/4 → 27/8 | note:e s:piano ]", - "[ 27/8 → 7/2 | note:f s:piano ]", + "[ 25/8 → 13/4 | note:e s:piano ]", + "[ 13/4 → 27/8 | note:f s:piano ]", + "[ 27/8 → 7/2 | note:d s:piano ]", "[ 7/2 → 4/1 | note:g s:piano ]", ] `; @@ -10179,6 +11072,420 @@ exports[`runs examples > example "transpose" example index 1 1`] = ` ] `; +exports[`runs examples > example "tremolo" example index 0 1`] = ` +[ + "[ 0/1 → 1/16 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 1/16 → 1/8 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 1/8 → 3/16 | note:d# s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 3/16 → 1/4 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 1/4 → 5/16 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 5/16 → 3/8 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 3/8 → 7/16 | note:d# s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 7/16 → 1/2 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 1/2 → 9/16 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 9/16 → 5/8 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 5/8 → 11/16 | note:d# s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 11/16 → 3/4 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 3/4 → 13/16 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 13/16 → 7/8 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 7/8 → 15/16 | note:d# s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 15/16 → 1/1 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 1/1 → 17/16 | note:d s:supersaw tremolo:2 tremoloskew:0.5 ]", + "[ 17/16 → 9/8 | note:d s:supersaw tremolo:2 tremoloskew:0.5 ]", + "[ 9/8 → 19/16 | note:d# s:supersaw tremolo:2 tremoloskew:0.5 ]", + "[ 19/16 → 5/4 | note:d s:supersaw tremolo:2 tremoloskew:0.5 ]", + "[ 5/4 → 21/16 | note:d s:supersaw tremolo:2 tremoloskew:0.5 ]", + "[ 21/16 → 11/8 | note:d s:supersaw tremolo:2 tremoloskew:0.5 ]", + "[ 11/8 → 23/16 | note:d# s:supersaw tremolo:2 tremoloskew:0.5 ]", + "[ 23/16 → 3/2 | note:d s:supersaw tremolo:2 tremoloskew:0.5 ]", + "[ 3/2 → 25/16 | note:d s:supersaw tremolo:2 tremoloskew:0.5 ]", + "[ 25/16 → 13/8 | note:d s:supersaw tremolo:2 tremoloskew:0.5 ]", + "[ 13/8 → 27/16 | note:d# s:supersaw tremolo:2 tremoloskew:0.5 ]", + "[ 27/16 → 7/4 | note:d s:supersaw tremolo:2 tremoloskew:0.5 ]", + "[ 7/4 → 29/16 | note:d s:supersaw tremolo:2 tremoloskew:0.5 ]", + "[ 29/16 → 15/8 | note:d s:supersaw tremolo:2 tremoloskew:0.5 ]", + "[ 15/8 → 31/16 | note:d# s:supersaw tremolo:2 tremoloskew:0.5 ]", + "[ 31/16 → 2/1 | note:d s:supersaw tremolo:2 tremoloskew:0.5 ]", + "[ 2/1 → 33/16 | note:d s:supersaw tremolo:100 tremoloskew:0.5 ]", + "[ 33/16 → 17/8 | note:d s:supersaw tremolo:100 tremoloskew:0.5 ]", + "[ 17/8 → 35/16 | note:d# s:supersaw tremolo:100 tremoloskew:0.5 ]", + "[ 35/16 → 9/4 | note:d s:supersaw tremolo:100 tremoloskew:0.5 ]", + "[ 9/4 → 37/16 | note:d s:supersaw tremolo:100 tremoloskew:0.5 ]", + "[ 37/16 → 19/8 | note:d s:supersaw tremolo:100 tremoloskew:0.5 ]", + "[ 19/8 → 39/16 | note:d# s:supersaw tremolo:100 tremoloskew:0.5 ]", + "[ 39/16 → 5/2 | note:d s:supersaw tremolo:100 tremoloskew:0.5 ]", + "[ 5/2 → 41/16 | note:d s:supersaw tremolo:100 tremoloskew:0.5 ]", + "[ 41/16 → 21/8 | note:d s:supersaw tremolo:100 tremoloskew:0.5 ]", + "[ 21/8 → 43/16 | note:d# s:supersaw tremolo:100 tremoloskew:0.5 ]", + "[ 43/16 → 11/4 | note:d s:supersaw tremolo:100 tremoloskew:0.5 ]", + "[ 11/4 → 45/16 | note:d s:supersaw tremolo:100 tremoloskew:0.5 ]", + "[ 45/16 → 23/8 | note:d s:supersaw tremolo:100 tremoloskew:0.5 ]", + "[ 23/8 → 47/16 | note:d# s:supersaw tremolo:100 tremoloskew:0.5 ]", + "[ 47/16 → 3/1 | note:d s:supersaw tremolo:100 tremoloskew:0.5 ]", + "[ 3/1 → 49/16 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 49/16 → 25/8 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 25/8 → 51/16 | note:d# s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 51/16 → 13/4 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 13/4 → 53/16 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 53/16 → 27/8 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 27/8 → 55/16 | note:d# s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 55/16 → 7/2 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 7/2 → 57/16 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 57/16 → 29/8 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 29/8 → 59/16 | note:d# s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 59/16 → 15/4 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 15/4 → 61/16 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 61/16 → 31/8 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 31/8 → 63/16 | note:d# s:supersaw tremolo:3 tremoloskew:0.5 ]", + "[ 63/16 → 4/1 | note:d s:supersaw tremolo:3 tremoloskew:0.5 ]", +] +`; + +exports[`runs examples > example "tremolodepth" example index 0 1`] = ` +[ + "[ 0/1 → 1/16 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 1/16 → 1/8 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 1/8 → 3/16 | note:a#1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 3/16 → 1/4 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 1/4 → 5/16 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 5/16 → 3/8 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 3/8 → 7/16 | note:a#1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 7/16 → 1/2 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 1/2 → 9/16 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 9/16 → 5/8 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 5/8 → 11/16 | note:a#1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 11/16 → 3/4 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 3/4 → 13/16 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 13/16 → 7/8 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 7/8 → 15/16 | note:a#1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 15/16 → 1/1 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 1/1 → 17/16 | note:a1 s:pulse tremolosync:4 tremolodepth:2 ]", + "[ 17/16 → 9/8 | note:a1 s:pulse tremolosync:4 tremolodepth:2 ]", + "[ 9/8 → 19/16 | note:a#1 s:pulse tremolosync:4 tremolodepth:2 ]", + "[ 19/16 → 5/4 | note:a1 s:pulse tremolosync:4 tremolodepth:2 ]", + "[ 5/4 → 21/16 | note:a1 s:pulse tremolosync:4 tremolodepth:2 ]", + "[ 21/16 → 11/8 | note:a1 s:pulse tremolosync:4 tremolodepth:2 ]", + "[ 11/8 → 23/16 | note:a#1 s:pulse tremolosync:4 tremolodepth:2 ]", + "[ 23/16 → 3/2 | note:a1 s:pulse tremolosync:4 tremolodepth:2 ]", + "[ 3/2 → 25/16 | note:a1 s:pulse tremolosync:4 tremolodepth:2 ]", + "[ 25/16 → 13/8 | note:a1 s:pulse tremolosync:4 tremolodepth:2 ]", + "[ 13/8 → 27/16 | note:a#1 s:pulse tremolosync:4 tremolodepth:2 ]", + "[ 27/16 → 7/4 | note:a1 s:pulse tremolosync:4 tremolodepth:2 ]", + "[ 7/4 → 29/16 | note:a1 s:pulse tremolosync:4 tremolodepth:2 ]", + "[ 29/16 → 15/8 | note:a1 s:pulse tremolosync:4 tremolodepth:2 ]", + "[ 15/8 → 31/16 | note:a#1 s:pulse tremolosync:4 tremolodepth:2 ]", + "[ 31/16 → 2/1 | note:a1 s:pulse tremolosync:4 tremolodepth:2 ]", + "[ 2/1 → 33/16 | note:a1 s:pulse tremolosync:4 tremolodepth:0.7 ]", + "[ 33/16 → 17/8 | note:a1 s:pulse tremolosync:4 tremolodepth:0.7 ]", + "[ 17/8 → 35/16 | note:a#1 s:pulse tremolosync:4 tremolodepth:0.7 ]", + "[ 35/16 → 9/4 | note:a1 s:pulse tremolosync:4 tremolodepth:0.7 ]", + "[ 9/4 → 37/16 | note:a1 s:pulse tremolosync:4 tremolodepth:0.7 ]", + "[ 37/16 → 19/8 | note:a1 s:pulse tremolosync:4 tremolodepth:0.7 ]", + "[ 19/8 → 39/16 | note:a#1 s:pulse tremolosync:4 tremolodepth:0.7 ]", + "[ 39/16 → 5/2 | note:a1 s:pulse tremolosync:4 tremolodepth:0.7 ]", + "[ 5/2 → 41/16 | note:a1 s:pulse tremolosync:4 tremolodepth:0.7 ]", + "[ 41/16 → 21/8 | note:a1 s:pulse tremolosync:4 tremolodepth:0.7 ]", + "[ 21/8 → 43/16 | note:a#1 s:pulse tremolosync:4 tremolodepth:0.7 ]", + "[ 43/16 → 11/4 | note:a1 s:pulse tremolosync:4 tremolodepth:0.7 ]", + "[ 11/4 → 45/16 | note:a1 s:pulse tremolosync:4 tremolodepth:0.7 ]", + "[ 45/16 → 23/8 | note:a1 s:pulse tremolosync:4 tremolodepth:0.7 ]", + "[ 23/8 → 47/16 | note:a#1 s:pulse tremolosync:4 tremolodepth:0.7 ]", + "[ 47/16 → 3/1 | note:a1 s:pulse tremolosync:4 tremolodepth:0.7 ]", + "[ 3/1 → 49/16 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 49/16 → 25/8 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 25/8 → 51/16 | note:a#1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 51/16 → 13/4 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 13/4 → 53/16 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 53/16 → 27/8 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 27/8 → 55/16 | note:a#1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 55/16 → 7/2 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 7/2 → 57/16 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 57/16 → 29/8 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 29/8 → 59/16 | note:a#1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 59/16 → 15/4 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 15/4 → 61/16 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 61/16 → 31/8 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 31/8 → 63/16 | note:a#1 s:pulse tremolosync:4 tremolodepth:1 ]", + "[ 63/16 → 4/1 | note:a1 s:pulse tremolosync:4 tremolodepth:1 ]", +] +`; + +exports[`runs examples > example "tremolophase" example index 0 1`] = ` +[ + "[ 0/1 → 1/16 | note:f s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 1/16 → 1/8 | note:a s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 1/8 → 3/16 | note:c s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 3/16 → 1/4 | note:e s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 1/4 → 5/16 | note:f s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 5/16 → 3/8 | note:a s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 3/8 → 7/16 | note:c s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 7/16 → 1/2 | note:e s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 1/2 → 9/16 | note:f s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 9/16 → 5/8 | note:a s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 5/8 → 11/16 | note:c s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 11/16 → 3/4 | note:e s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 3/4 → 13/16 | note:f s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 13/16 → 7/8 | note:a s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 7/8 → 15/16 | note:c s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 15/16 → 1/1 | note:e s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 1/1 → 17/16 | note:f s:sawtooth tremolosync:4 tremolophase:0.25 ]", + "[ 17/16 → 9/8 | note:a s:sawtooth tremolosync:4 tremolophase:0.25 ]", + "[ 9/8 → 19/16 | note:c s:sawtooth tremolosync:4 tremolophase:0.25 ]", + "[ 19/16 → 5/4 | note:e s:sawtooth tremolosync:4 tremolophase:0.25 ]", + "[ 5/4 → 21/16 | note:f s:sawtooth tremolosync:4 tremolophase:0.25 ]", + "[ 21/16 → 11/8 | note:a s:sawtooth tremolosync:4 tremolophase:0.25 ]", + "[ 11/8 → 23/16 | note:c s:sawtooth tremolosync:4 tremolophase:0.25 ]", + "[ 23/16 → 3/2 | note:e s:sawtooth tremolosync:4 tremolophase:0.25 ]", + "[ 3/2 → 25/16 | note:f s:sawtooth tremolosync:4 tremolophase:0.25 ]", + "[ 25/16 → 13/8 | note:a s:sawtooth tremolosync:4 tremolophase:0.25 ]", + "[ 13/8 → 27/16 | note:c s:sawtooth tremolosync:4 tremolophase:0.25 ]", + "[ 27/16 → 7/4 | note:e s:sawtooth tremolosync:4 tremolophase:0.25 ]", + "[ 7/4 → 29/16 | note:f s:sawtooth tremolosync:4 tremolophase:0.25 ]", + "[ 29/16 → 15/8 | note:a s:sawtooth tremolosync:4 tremolophase:0.25 ]", + "[ 15/8 → 31/16 | note:c s:sawtooth tremolosync:4 tremolophase:0.25 ]", + "[ 31/16 → 2/1 | note:e s:sawtooth tremolosync:4 tremolophase:0.25 ]", + "[ 2/1 → 33/16 | note:f s:sawtooth tremolosync:4 tremolophase:0.66 ]", + "[ 33/16 → 17/8 | note:a s:sawtooth tremolosync:4 tremolophase:0.66 ]", + "[ 17/8 → 35/16 | note:c s:sawtooth tremolosync:4 tremolophase:0.66 ]", + "[ 35/16 → 9/4 | note:e s:sawtooth tremolosync:4 tremolophase:0.66 ]", + "[ 9/4 → 37/16 | note:f s:sawtooth tremolosync:4 tremolophase:0.66 ]", + "[ 37/16 → 19/8 | note:a s:sawtooth tremolosync:4 tremolophase:0.66 ]", + "[ 19/8 → 39/16 | note:c s:sawtooth tremolosync:4 tremolophase:0.66 ]", + "[ 39/16 → 5/2 | note:e s:sawtooth tremolosync:4 tremolophase:0.66 ]", + "[ 5/2 → 41/16 | note:f s:sawtooth tremolosync:4 tremolophase:0.66 ]", + "[ 41/16 → 21/8 | note:a s:sawtooth tremolosync:4 tremolophase:0.66 ]", + "[ 21/8 → 43/16 | note:c s:sawtooth tremolosync:4 tremolophase:0.66 ]", + "[ 43/16 → 11/4 | note:e s:sawtooth tremolosync:4 tremolophase:0.66 ]", + "[ 11/4 → 45/16 | note:f s:sawtooth tremolosync:4 tremolophase:0.66 ]", + "[ 45/16 → 23/8 | note:a s:sawtooth tremolosync:4 tremolophase:0.66 ]", + "[ 23/8 → 47/16 | note:c s:sawtooth tremolosync:4 tremolophase:0.66 ]", + "[ 47/16 → 3/1 | note:e s:sawtooth tremolosync:4 tremolophase:0.66 ]", + "[ 3/1 → 49/16 | note:f s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 49/16 → 25/8 | note:a s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 25/8 → 51/16 | note:c s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 51/16 → 13/4 | note:e s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 13/4 → 53/16 | note:f s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 53/16 → 27/8 | note:a s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 27/8 → 55/16 | note:c s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 55/16 → 7/2 | note:e s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 7/2 → 57/16 | note:f s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 57/16 → 29/8 | note:a s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 29/8 → 59/16 | note:c s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 59/16 → 15/4 | note:e s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 15/4 → 61/16 | note:f s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 61/16 → 31/8 | note:a s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 31/8 → 63/16 | note:c s:sawtooth tremolosync:4 tremolophase:0 ]", + "[ 63/16 → 4/1 | note:e s:sawtooth tremolosync:4 tremolophase:0 ]", +] +`; + +exports[`runs examples > example "tremoloshape" example index 0 1`] = ` +[ + "[ 0/1 → 1/16 | note:f tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 1/16 → 1/8 | note:g tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 1/8 → 3/16 | note:c tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 3/16 → 1/4 | note:d tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 1/4 → 5/16 | note:f tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 5/16 → 3/8 | note:g tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 3/8 → 7/16 | note:c tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 7/16 → 1/2 | note:d tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 1/2 → 9/16 | note:f tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 9/16 → 5/8 | note:g tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 5/8 → 11/16 | note:c tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 11/16 → 3/4 | note:d tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 3/4 → 13/16 | note:f tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 13/16 → 7/8 | note:g tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 7/8 → 15/16 | note:c tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 15/16 → 1/1 | note:d tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 1/1 → 17/16 | note:f tremolosync:4 tremoloshape:tri s:sawtooth ]", + "[ 17/16 → 9/8 | note:g tremolosync:4 tremoloshape:tri s:sawtooth ]", + "[ 9/8 → 19/16 | note:c tremolosync:4 tremoloshape:tri s:sawtooth ]", + "[ 19/16 → 5/4 | note:d tremolosync:4 tremoloshape:tri s:sawtooth ]", + "[ 5/4 → 21/16 | note:f tremolosync:4 tremoloshape:tri s:sawtooth ]", + "[ 21/16 → 11/8 | note:g tremolosync:4 tremoloshape:tri s:sawtooth ]", + "[ 11/8 → 23/16 | note:c tremolosync:4 tremoloshape:tri s:sawtooth ]", + "[ 23/16 → 3/2 | note:d tremolosync:4 tremoloshape:tri s:sawtooth ]", + "[ 3/2 → 25/16 | note:f tremolosync:4 tremoloshape:tri s:sawtooth ]", + "[ 25/16 → 13/8 | note:g tremolosync:4 tremoloshape:tri s:sawtooth ]", + "[ 13/8 → 27/16 | note:c tremolosync:4 tremoloshape:tri s:sawtooth ]", + "[ 27/16 → 7/4 | note:d tremolosync:4 tremoloshape:tri s:sawtooth ]", + "[ 7/4 → 29/16 | note:f tremolosync:4 tremoloshape:tri s:sawtooth ]", + "[ 29/16 → 15/8 | note:g tremolosync:4 tremoloshape:tri s:sawtooth ]", + "[ 15/8 → 31/16 | note:c tremolosync:4 tremoloshape:tri s:sawtooth ]", + "[ 31/16 → 2/1 | note:d tremolosync:4 tremoloshape:tri s:sawtooth ]", + "[ 2/1 → 33/16 | note:f tremolosync:4 tremoloshape:square s:sawtooth ]", + "[ 33/16 → 17/8 | note:g tremolosync:4 tremoloshape:square s:sawtooth ]", + "[ 17/8 → 35/16 | note:c tremolosync:4 tremoloshape:square s:sawtooth ]", + "[ 35/16 → 9/4 | note:d tremolosync:4 tremoloshape:square s:sawtooth ]", + "[ 9/4 → 37/16 | note:f tremolosync:4 tremoloshape:square s:sawtooth ]", + "[ 37/16 → 19/8 | note:g tremolosync:4 tremoloshape:square s:sawtooth ]", + "[ 19/8 → 39/16 | note:c tremolosync:4 tremoloshape:square s:sawtooth ]", + "[ 39/16 → 5/2 | note:d tremolosync:4 tremoloshape:square s:sawtooth ]", + "[ 5/2 → 41/16 | note:f tremolosync:4 tremoloshape:square s:sawtooth ]", + "[ 41/16 → 21/8 | note:g tremolosync:4 tremoloshape:square s:sawtooth ]", + "[ 21/8 → 43/16 | note:c tremolosync:4 tremoloshape:square s:sawtooth ]", + "[ 43/16 → 11/4 | note:d tremolosync:4 tremoloshape:square s:sawtooth ]", + "[ 11/4 → 45/16 | note:f tremolosync:4 tremoloshape:square s:sawtooth ]", + "[ 45/16 → 23/8 | note:g tremolosync:4 tremoloshape:square s:sawtooth ]", + "[ 23/8 → 47/16 | note:c tremolosync:4 tremoloshape:square s:sawtooth ]", + "[ 47/16 → 3/1 | note:d tremolosync:4 tremoloshape:square s:sawtooth ]", + "[ 3/1 → 49/16 | note:f tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 49/16 → 25/8 | note:g tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 25/8 → 51/16 | note:c tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 51/16 → 13/4 | note:d tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 13/4 → 53/16 | note:f tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 53/16 → 27/8 | note:g tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 27/8 → 55/16 | note:c tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 55/16 → 7/2 | note:d tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 7/2 → 57/16 | note:f tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 57/16 → 29/8 | note:g tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 29/8 → 59/16 | note:c tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 59/16 → 15/4 | note:d tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 15/4 → 61/16 | note:f tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 61/16 → 31/8 | note:g tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 31/8 → 63/16 | note:c tremolosync:4 tremoloshape:sine s:sawtooth ]", + "[ 63/16 → 4/1 | note:d tremolosync:4 tremoloshape:sine s:sawtooth ]", +] +`; + +exports[`runs examples > example "tremoloskew" example index 0 1`] = ` +[ + "[ 0/1 → 1/16 | note:f s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 1/16 → 1/8 | note:a s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 1/8 → 3/16 | note:c s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 3/16 → 1/4 | note:e s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 1/4 → 5/16 | note:f s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 5/16 → 3/8 | note:a s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 3/8 → 7/16 | note:c s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 7/16 → 1/2 | note:e s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 1/2 → 9/16 | note:f s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 9/16 → 5/8 | note:a s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 5/8 → 11/16 | note:c s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 11/16 → 3/4 | note:e s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 3/4 → 13/16 | note:f s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 13/16 → 7/8 | note:a s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 7/8 → 15/16 | note:c s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 15/16 → 1/1 | note:e s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 1/1 → 17/16 | note:f s:sawtooth tremolosync:4 tremoloskew:0 ]", + "[ 17/16 → 9/8 | note:a s:sawtooth tremolosync:4 tremoloskew:0 ]", + "[ 9/8 → 19/16 | note:c s:sawtooth tremolosync:4 tremoloskew:0 ]", + "[ 19/16 → 5/4 | note:e s:sawtooth tremolosync:4 tremoloskew:0 ]", + "[ 5/4 → 21/16 | note:f s:sawtooth tremolosync:4 tremoloskew:0 ]", + "[ 21/16 → 11/8 | note:a s:sawtooth tremolosync:4 tremoloskew:0 ]", + "[ 11/8 → 23/16 | note:c s:sawtooth tremolosync:4 tremoloskew:0 ]", + "[ 23/16 → 3/2 | note:e s:sawtooth tremolosync:4 tremoloskew:0 ]", + "[ 3/2 → 25/16 | note:f s:sawtooth tremolosync:4 tremoloskew:0 ]", + "[ 25/16 → 13/8 | note:a s:sawtooth tremolosync:4 tremoloskew:0 ]", + "[ 13/8 → 27/16 | note:c s:sawtooth tremolosync:4 tremoloskew:0 ]", + "[ 27/16 → 7/4 | note:e s:sawtooth tremolosync:4 tremoloskew:0 ]", + "[ 7/4 → 29/16 | note:f s:sawtooth tremolosync:4 tremoloskew:0 ]", + "[ 29/16 → 15/8 | note:a s:sawtooth tremolosync:4 tremoloskew:0 ]", + "[ 15/8 → 31/16 | note:c s:sawtooth tremolosync:4 tremoloskew:0 ]", + "[ 31/16 → 2/1 | note:e s:sawtooth tremolosync:4 tremoloskew:0 ]", + "[ 2/1 → 33/16 | note:f s:sawtooth tremolosync:4 tremoloskew:1 ]", + "[ 33/16 → 17/8 | note:a s:sawtooth tremolosync:4 tremoloskew:1 ]", + "[ 17/8 → 35/16 | note:c s:sawtooth tremolosync:4 tremoloskew:1 ]", + "[ 35/16 → 9/4 | note:e s:sawtooth tremolosync:4 tremoloskew:1 ]", + "[ 9/4 → 37/16 | note:f s:sawtooth tremolosync:4 tremoloskew:1 ]", + "[ 37/16 → 19/8 | note:a s:sawtooth tremolosync:4 tremoloskew:1 ]", + "[ 19/8 → 39/16 | note:c s:sawtooth tremolosync:4 tremoloskew:1 ]", + "[ 39/16 → 5/2 | note:e s:sawtooth tremolosync:4 tremoloskew:1 ]", + "[ 5/2 → 41/16 | note:f s:sawtooth tremolosync:4 tremoloskew:1 ]", + "[ 41/16 → 21/8 | note:a s:sawtooth tremolosync:4 tremoloskew:1 ]", + "[ 21/8 → 43/16 | note:c s:sawtooth tremolosync:4 tremoloskew:1 ]", + "[ 43/16 → 11/4 | note:e s:sawtooth tremolosync:4 tremoloskew:1 ]", + "[ 11/4 → 45/16 | note:f s:sawtooth tremolosync:4 tremoloskew:1 ]", + "[ 45/16 → 23/8 | note:a s:sawtooth tremolosync:4 tremoloskew:1 ]", + "[ 23/8 → 47/16 | note:c s:sawtooth tremolosync:4 tremoloskew:1 ]", + "[ 47/16 → 3/1 | note:e s:sawtooth tremolosync:4 tremoloskew:1 ]", + "[ 3/1 → 49/16 | note:f s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 49/16 → 25/8 | note:a s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 25/8 → 51/16 | note:c s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 51/16 → 13/4 | note:e s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 13/4 → 53/16 | note:f s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 53/16 → 27/8 | note:a s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 27/8 → 55/16 | note:c s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 55/16 → 7/2 | note:e s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 7/2 → 57/16 | note:f s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 57/16 → 29/8 | note:a s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 29/8 → 59/16 | note:c s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 59/16 → 15/4 | note:e s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 15/4 → 61/16 | note:f s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 61/16 → 31/8 | note:a s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 31/8 → 63/16 | note:c s:sawtooth tremolosync:4 tremoloskew:0.5 ]", + "[ 63/16 → 4/1 | note:e s:sawtooth tremolosync:4 tremoloskew:0.5 ]", +] +`; + +exports[`runs examples > example "tremolosync" example index 0 1`] = ` +[ + "[ 0/1 → 1/16 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 1/16 → 1/8 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 1/8 → 3/16 | note:d# s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 3/16 → 1/4 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 1/4 → 5/16 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 5/16 → 3/8 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 3/8 → 7/16 | note:d# s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 7/16 → 1/2 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 1/2 → 9/16 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 9/16 → 5/8 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 5/8 → 11/16 | note:d# s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 11/16 → 3/4 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 3/4 → 13/16 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 13/16 → 7/8 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 7/8 → 15/16 | note:d# s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 15/16 → 1/1 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 1/1 → 17/16 | note:d s:supersaw tremolosync:4 tremoloskew:0.5 ]", + "[ 17/16 → 9/8 | note:d s:supersaw tremolosync:4 tremoloskew:0.5 ]", + "[ 9/8 → 19/16 | note:d# s:supersaw tremolosync:4 tremoloskew:0.5 ]", + "[ 19/16 → 5/4 | note:d s:supersaw tremolosync:4 tremoloskew:0.5 ]", + "[ 5/4 → 21/16 | note:d s:supersaw tremolosync:4 tremoloskew:0.5 ]", + "[ 21/16 → 11/8 | note:d s:supersaw tremolosync:4 tremoloskew:0.5 ]", + "[ 11/8 → 23/16 | note:d# s:supersaw tremolosync:4 tremoloskew:0.5 ]", + "[ 23/16 → 3/2 | note:d s:supersaw tremolosync:4 tremoloskew:0.5 ]", + "[ 3/2 → 25/16 | note:d s:supersaw tremolosync:4 tremoloskew:0.5 ]", + "[ 25/16 → 13/8 | note:d s:supersaw tremolosync:4 tremoloskew:0.5 ]", + "[ 13/8 → 27/16 | note:d# s:supersaw tremolosync:4 tremoloskew:0.5 ]", + "[ 27/16 → 7/4 | note:d s:supersaw tremolosync:4 tremoloskew:0.5 ]", + "[ 7/4 → 29/16 | note:d s:supersaw tremolosync:4 tremoloskew:0.5 ]", + "[ 29/16 → 15/8 | note:d s:supersaw tremolosync:4 tremoloskew:0.5 ]", + "[ 15/8 → 31/16 | note:d# s:supersaw tremolosync:4 tremoloskew:0.5 ]", + "[ 31/16 → 2/1 | note:d s:supersaw tremolosync:4 tremoloskew:0.5 ]", + "[ 2/1 → 33/16 | note:d s:supersaw tremolosync:4 tremoloskew:0 ]", + "[ 33/16 → 17/8 | note:d s:supersaw tremolosync:4 tremoloskew:0 ]", + "[ 17/8 → 35/16 | note:d# s:supersaw tremolosync:4 tremoloskew:0 ]", + "[ 35/16 → 9/4 | note:d s:supersaw tremolosync:4 tremoloskew:0 ]", + "[ 9/4 → 37/16 | note:d s:supersaw tremolosync:4 tremoloskew:0 ]", + "[ 37/16 → 19/8 | note:d s:supersaw tremolosync:4 tremoloskew:0 ]", + "[ 19/8 → 39/16 | note:d# s:supersaw tremolosync:4 tremoloskew:0 ]", + "[ 39/16 → 5/2 | note:d s:supersaw tremolosync:4 tremoloskew:0 ]", + "[ 5/2 → 41/16 | note:d s:supersaw tremolosync:4 tremoloskew:0 ]", + "[ 41/16 → 21/8 | note:d s:supersaw tremolosync:4 tremoloskew:0 ]", + "[ 21/8 → 43/16 | note:d# s:supersaw tremolosync:4 tremoloskew:0 ]", + "[ 43/16 → 11/4 | note:d s:supersaw tremolosync:4 tremoloskew:0 ]", + "[ 11/4 → 45/16 | note:d s:supersaw tremolosync:4 tremoloskew:0 ]", + "[ 45/16 → 23/8 | note:d s:supersaw tremolosync:4 tremoloskew:0 ]", + "[ 23/8 → 47/16 | note:d# s:supersaw tremolosync:4 tremoloskew:0 ]", + "[ 47/16 → 3/1 | note:d s:supersaw tremolosync:4 tremoloskew:0 ]", + "[ 3/1 → 49/16 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 49/16 → 25/8 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 25/8 → 51/16 | note:d# s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 51/16 → 13/4 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 13/4 → 53/16 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 53/16 → 27/8 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 27/8 → 55/16 | note:d# s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 55/16 → 7/2 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 7/2 → 57/16 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 57/16 → 29/8 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 29/8 → 59/16 | note:d# s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 59/16 → 15/4 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 15/4 → 61/16 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 61/16 → 31/8 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 31/8 → 63/16 | note:d# s:supersaw tremolosync:4 tremoloskew:1 ]", + "[ 63/16 → 4/1 | note:d s:supersaw tremolosync:4 tremoloskew:1 ]", +] +`; + exports[`runs examples > example "tri" example index 0 1`] = ` [ "[ 0/1 → 1/8 | note:C3 ]", @@ -10627,6 +11934,112 @@ exports[`runs examples > example "vowel" example index 1 1`] = ` ] `; +exports[`runs examples > example "warp" example index 0 1`] = ` +[ + "[ 0/1 → 1/8 | s:basique bank:wt_digital note:F1 warp:0 warpmode:spin ]", + "[ (1/8 → 1/5) ⇝ 1/4 | s:basique bank:wt_digital note:F1 warp:0 warpmode:spin ]", + "[ 1/8 ⇜ (1/5 → 1/4) | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]", + "[ 1/4 → 3/8 | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]", + "[ (3/8 → 2/5) ⇝ 1/2 | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]", + "[ 3/8 ⇜ (2/5 → 1/2) | s:basique bank:wt_digital note:F1 warp:0.5 warpmode:spin ]", + "[ (1/2 → 3/5) ⇝ 5/8 | s:basique bank:wt_digital note:F1 warp:0.5 warpmode:spin ]", + "[ 1/2 ⇜ (3/5 → 5/8) | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]", + "[ 5/8 → 3/4 | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]", + "[ (3/4 → 4/5) ⇝ 7/8 | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]", + "[ 3/4 ⇜ (4/5 → 7/8) | s:basique bank:wt_digital note:F1 warp:1 warpmode:spin ]", + "[ 7/8 → 1/1 | s:basique bank:wt_digital note:F1 warp:1 warpmode:spin ]", + "[ 1/1 → 9/8 | s:basique bank:wt_digital note:F1 warp:0 warpmode:spin ]", + "[ (9/8 → 6/5) ⇝ 5/4 | s:basique bank:wt_digital note:F1 warp:0 warpmode:spin ]", + "[ 9/8 ⇜ (6/5 → 5/4) | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]", + "[ 5/4 → 11/8 | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]", + "[ (11/8 → 7/5) ⇝ 3/2 | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]", + "[ 11/8 ⇜ (7/5 → 3/2) | s:basique bank:wt_digital note:F1 warp:0.5 warpmode:spin ]", + "[ (3/2 → 8/5) ⇝ 13/8 | s:basique bank:wt_digital note:F1 warp:0.5 warpmode:spin ]", + "[ 3/2 ⇜ (8/5 → 13/8) | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]", + "[ 13/8 → 7/4 | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]", + "[ (7/4 → 9/5) ⇝ 15/8 | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]", + "[ 7/4 ⇜ (9/5 → 15/8) | s:basique bank:wt_digital note:F1 warp:1 warpmode:spin ]", + "[ 15/8 → 2/1 | s:basique bank:wt_digital note:F1 warp:1 warpmode:spin ]", + "[ 2/1 → 17/8 | s:basique bank:wt_digital note:F1 warp:0 warpmode:spin ]", + "[ (17/8 → 11/5) ⇝ 9/4 | s:basique bank:wt_digital note:F1 warp:0 warpmode:spin ]", + "[ 17/8 ⇜ (11/5 → 9/4) | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]", + "[ 9/4 → 19/8 | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]", + "[ (19/8 → 12/5) ⇝ 5/2 | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]", + "[ 19/8 ⇜ (12/5 → 5/2) | s:basique bank:wt_digital note:F1 warp:0.5 warpmode:spin ]", + "[ (5/2 → 13/5) ⇝ 21/8 | s:basique bank:wt_digital note:F1 warp:0.5 warpmode:spin ]", + "[ 5/2 ⇜ (13/5 → 21/8) | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]", + "[ 21/8 → 11/4 | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]", + "[ (11/4 → 14/5) ⇝ 23/8 | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]", + "[ 11/4 ⇜ (14/5 → 23/8) | s:basique bank:wt_digital note:F1 warp:1 warpmode:spin ]", + "[ 23/8 → 3/1 | s:basique bank:wt_digital note:F1 warp:1 warpmode:spin ]", + "[ 3/1 → 25/8 | s:basique bank:wt_digital note:F1 warp:0 warpmode:spin ]", + "[ (25/8 → 16/5) ⇝ 13/4 | s:basique bank:wt_digital note:F1 warp:0 warpmode:spin ]", + "[ 25/8 ⇜ (16/5 → 13/4) | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]", + "[ 13/4 → 27/8 | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]", + "[ (27/8 → 17/5) ⇝ 7/2 | s:basique bank:wt_digital note:F1 warp:0.25 warpmode:spin ]", + "[ 27/8 ⇜ (17/5 → 7/2) | s:basique bank:wt_digital note:F1 warp:0.5 warpmode:spin ]", + "[ (7/2 → 18/5) ⇝ 29/8 | s:basique bank:wt_digital note:F1 warp:0.5 warpmode:spin ]", + "[ 7/2 ⇜ (18/5 → 29/8) | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]", + "[ 29/8 → 15/4 | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]", + "[ (15/4 → 19/5) ⇝ 31/8 | s:basique bank:wt_digital note:F1 warp:0.75 warpmode:spin ]", + "[ 15/4 ⇜ (19/5 → 31/8) | s:basique bank:wt_digital note:F1 warp:1 warpmode:spin ]", + "[ 31/8 → 4/1 | s:basique bank:wt_digital note:F1 warp:1 warpmode:spin ]", +] +`; + +exports[`runs examples > example "warpmode" example index 0 1`] = ` +[ + "[ 0/1 → 1/8 | s:morgana bank:wt_digital note:F1 warp:0 warpmode:asym ]", + "[ (1/8 → 1/5) ⇝ 1/4 | s:morgana bank:wt_digital note:F1 warp:0 warpmode:asym ]", + "[ 1/8 ⇜ (1/5 → 1/4) | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:asym ]", + "[ 1/4 → 3/8 | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:asym ]", + "[ (3/8 → 2/5) ⇝ 1/2 | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:asym ]", + "[ 3/8 ⇜ (2/5 → 1/2) | s:morgana bank:wt_digital note:F1 warp:0.5 warpmode:asym ]", + "[ (1/2 → 3/5) ⇝ 5/8 | s:morgana bank:wt_digital note:F1 warp:0.5 warpmode:bendp ]", + "[ 1/2 ⇜ (3/5 → 5/8) | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:bendp ]", + "[ 5/8 → 3/4 | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:bendp ]", + "[ (3/4 → 4/5) ⇝ 7/8 | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:bendp ]", + "[ 3/4 ⇜ (4/5 → 7/8) | s:morgana bank:wt_digital note:F1 warp:1 warpmode:bendp ]", + "[ 7/8 → 1/1 | s:morgana bank:wt_digital note:F1 warp:1 warpmode:bendp ]", + "[ 1/1 → 9/8 | s:morgana bank:wt_digital note:F1 warp:0 warpmode:spin ]", + "[ (9/8 → 6/5) ⇝ 5/4 | s:morgana bank:wt_digital note:F1 warp:0 warpmode:spin ]", + "[ 9/8 ⇜ (6/5 → 5/4) | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:spin ]", + "[ 5/4 → 11/8 | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:spin ]", + "[ (11/8 → 7/5) ⇝ 3/2 | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:spin ]", + "[ 11/8 ⇜ (7/5 → 3/2) | s:morgana bank:wt_digital note:F1 warp:0.5 warpmode:spin ]", + "[ (3/2 → 8/5) ⇝ 13/8 | s:morgana bank:wt_digital note:F1 warp:0.5 warpmode:logistic ]", + "[ 3/2 ⇜ (8/5 → 13/8) | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:logistic ]", + "[ 13/8 → 7/4 | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:logistic ]", + "[ (7/4 → 9/5) ⇝ 15/8 | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:logistic ]", + "[ 7/4 ⇜ (9/5 → 15/8) | s:morgana bank:wt_digital note:F1 warp:1 warpmode:logistic ]", + "[ 15/8 → 2/1 | s:morgana bank:wt_digital note:F1 warp:1 warpmode:logistic ]", + "[ 2/1 → 17/8 | s:morgana bank:wt_digital note:F1 warp:0 warpmode:sync ]", + "[ (17/8 → 11/5) ⇝ 9/4 | s:morgana bank:wt_digital note:F1 warp:0 warpmode:sync ]", + "[ 17/8 ⇜ (11/5 → 9/4) | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:sync ]", + "[ 9/4 → 19/8 | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:sync ]", + "[ (19/8 → 12/5) ⇝ 5/2 | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:sync ]", + "[ 19/8 ⇜ (12/5 → 5/2) | s:morgana bank:wt_digital note:F1 warp:0.5 warpmode:sync ]", + "[ (5/2 → 13/5) ⇝ 21/8 | s:morgana bank:wt_digital note:F1 warp:0.5 warpmode:wormhole ]", + "[ 5/2 ⇜ (13/5 → 21/8) | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:wormhole ]", + "[ 21/8 → 11/4 | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:wormhole ]", + "[ (11/4 → 14/5) ⇝ 23/8 | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:wormhole ]", + "[ 11/4 ⇜ (14/5 → 23/8) | s:morgana bank:wt_digital note:F1 warp:1 warpmode:wormhole ]", + "[ 23/8 → 3/1 | s:morgana bank:wt_digital note:F1 warp:1 warpmode:wormhole ]", + "[ 3/1 → 25/8 | s:morgana bank:wt_digital note:F1 warp:0 warpmode:brownian ]", + "[ (25/8 → 16/5) ⇝ 13/4 | s:morgana bank:wt_digital note:F1 warp:0 warpmode:brownian ]", + "[ 25/8 ⇜ (16/5 → 13/4) | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:brownian ]", + "[ 13/4 → 27/8 | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:brownian ]", + "[ (27/8 → 17/5) ⇝ 7/2 | s:morgana bank:wt_digital note:F1 warp:0.25 warpmode:brownian ]", + "[ 27/8 ⇜ (17/5 → 7/2) | s:morgana bank:wt_digital note:F1 warp:0.5 warpmode:brownian ]", + "[ (7/2 → 18/5) ⇝ 29/8 | s:morgana bank:wt_digital note:F1 warp:0.5 warpmode:asym ]", + "[ 7/2 ⇜ (18/5 → 29/8) | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:asym ]", + "[ 29/8 → 15/4 | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:asym ]", + "[ (15/4 → 19/5) ⇝ 31/8 | s:morgana bank:wt_digital note:F1 warp:0.75 warpmode:asym ]", + "[ 15/4 ⇜ (19/5 → 31/8) | s:morgana bank:wt_digital note:F1 warp:1 warpmode:asym ]", + "[ 31/8 → 4/1 | s:morgana bank:wt_digital note:F1 warp:1 warpmode:asym ]", +] +`; + exports[`runs examples > example "wchoose" example index 0 1`] = ` [ "[ 0/1 → 1/5 | note:c2 s:sine ]", @@ -10831,6 +12244,128 @@ exports[`runs examples > example "withValue" example index 0 1`] = ` ] `; +exports[`runs examples > example "wt" example index 0 1`] = ` +[ + "[ 0/1 → 1/8 | s:squelch bank:wt_digital note:F1 wt:0 ]", + "[ (1/8 → 1/5) ⇝ 1/4 | s:squelch bank:wt_digital note:F1 wt:0 ]", + "[ 1/8 ⇜ (1/5 → 1/4) | s:squelch bank:wt_digital note:F1 wt:0.25 ]", + "[ 1/4 → 3/8 | s:squelch bank:wt_digital note:F1 wt:0.25 ]", + "[ (3/8 → 2/5) ⇝ 1/2 | s:squelch bank:wt_digital note:F1 wt:0.25 ]", + "[ 3/8 ⇜ (2/5 → 1/2) | s:squelch bank:wt_digital note:F1 wt:0.5 ]", + "[ (1/2 → 3/5) ⇝ 5/8 | s:squelch bank:wt_digital note:F1 wt:0.5 ]", + "[ 1/2 ⇜ (3/5 → 5/8) | s:squelch bank:wt_digital note:F1 wt:0.75 ]", + "[ 5/8 → 3/4 | s:squelch bank:wt_digital note:F1 wt:0.75 ]", + "[ (3/4 → 4/5) ⇝ 7/8 | s:squelch bank:wt_digital note:F1 wt:0.75 ]", + "[ 3/4 ⇜ (4/5 → 7/8) | s:squelch bank:wt_digital note:F1 wt:1 ]", + "[ 7/8 → 1/1 | s:squelch bank:wt_digital note:F1 wt:1 ]", + "[ 1/1 → 9/8 | s:squelch bank:wt_digital note:F1 wt:0 ]", + "[ (9/8 → 6/5) ⇝ 5/4 | s:squelch bank:wt_digital note:F1 wt:0 ]", + "[ 9/8 ⇜ (6/5 → 5/4) | s:squelch bank:wt_digital note:F1 wt:0.25 ]", + "[ 5/4 → 11/8 | s:squelch bank:wt_digital note:F1 wt:0.25 ]", + "[ (11/8 → 7/5) ⇝ 3/2 | s:squelch bank:wt_digital note:F1 wt:0.25 ]", + "[ 11/8 ⇜ (7/5 → 3/2) | s:squelch bank:wt_digital note:F1 wt:0.5 ]", + "[ (3/2 → 8/5) ⇝ 13/8 | s:squelch bank:wt_digital note:F1 wt:0.5 ]", + "[ 3/2 ⇜ (8/5 → 13/8) | s:squelch bank:wt_digital note:F1 wt:0.75 ]", + "[ 13/8 → 7/4 | s:squelch bank:wt_digital note:F1 wt:0.75 ]", + "[ (7/4 → 9/5) ⇝ 15/8 | s:squelch bank:wt_digital note:F1 wt:0.75 ]", + "[ 7/4 ⇜ (9/5 → 15/8) | s:squelch bank:wt_digital note:F1 wt:1 ]", + "[ 15/8 → 2/1 | s:squelch bank:wt_digital note:F1 wt:1 ]", + "[ 2/1 → 17/8 | s:squelch bank:wt_digital note:F1 wt:0 ]", + "[ (17/8 → 11/5) ⇝ 9/4 | s:squelch bank:wt_digital note:F1 wt:0 ]", + "[ 17/8 ⇜ (11/5 → 9/4) | s:squelch bank:wt_digital note:F1 wt:0.25 ]", + "[ 9/4 → 19/8 | s:squelch bank:wt_digital note:F1 wt:0.25 ]", + "[ (19/8 → 12/5) ⇝ 5/2 | s:squelch bank:wt_digital note:F1 wt:0.25 ]", + "[ 19/8 ⇜ (12/5 → 5/2) | s:squelch bank:wt_digital note:F1 wt:0.5 ]", + "[ (5/2 → 13/5) ⇝ 21/8 | s:squelch bank:wt_digital note:F1 wt:0.5 ]", + "[ 5/2 ⇜ (13/5 → 21/8) | s:squelch bank:wt_digital note:F1 wt:0.75 ]", + "[ 21/8 → 11/4 | s:squelch bank:wt_digital note:F1 wt:0.75 ]", + "[ (11/4 → 14/5) ⇝ 23/8 | s:squelch bank:wt_digital note:F1 wt:0.75 ]", + "[ 11/4 ⇜ (14/5 → 23/8) | s:squelch bank:wt_digital note:F1 wt:1 ]", + "[ 23/8 → 3/1 | s:squelch bank:wt_digital note:F1 wt:1 ]", + "[ 3/1 → 25/8 | s:squelch bank:wt_digital note:F1 wt:0 ]", + "[ (25/8 → 16/5) ⇝ 13/4 | s:squelch bank:wt_digital note:F1 wt:0 ]", + "[ 25/8 ⇜ (16/5 → 13/4) | s:squelch bank:wt_digital note:F1 wt:0.25 ]", + "[ 13/4 → 27/8 | s:squelch bank:wt_digital note:F1 wt:0.25 ]", + "[ (27/8 → 17/5) ⇝ 7/2 | s:squelch bank:wt_digital note:F1 wt:0.25 ]", + "[ 27/8 ⇜ (17/5 → 7/2) | s:squelch bank:wt_digital note:F1 wt:0.5 ]", + "[ (7/2 → 18/5) ⇝ 29/8 | s:squelch bank:wt_digital note:F1 wt:0.5 ]", + "[ 7/2 ⇜ (18/5 → 29/8) | s:squelch bank:wt_digital note:F1 wt:0.75 ]", + "[ 29/8 → 15/4 | s:squelch bank:wt_digital note:F1 wt:0.75 ]", + "[ (15/4 → 19/5) ⇝ 31/8 | s:squelch bank:wt_digital note:F1 wt:0.75 ]", + "[ 15/4 ⇜ (19/5 → 31/8) | s:squelch bank:wt_digital note:F1 wt:1 ]", + "[ 31/8 → 4/1 | s:squelch bank:wt_digital note:F1 wt:1 ]", +] +`; + +exports[`runs examples > example "wtphaserand" example index 0 1`] = ` +[ + "[ 0/1 → 1/16 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 1/16 → 1/8 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 1/8 → 3/16 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 3/16 → 1/4 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 1/4 → 5/16 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 5/16 → 3/8 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 3/8 → 7/16 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 7/16 → 1/2 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 1/2 → 9/16 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 9/16 → 5/8 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 5/8 → 11/16 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 11/16 → 3/4 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 3/4 → 13/16 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 13/16 → 7/8 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 7/8 → 15/16 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 15/16 → 1/1 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 1/1 → 17/16 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 17/16 → 9/8 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 9/8 → 19/16 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 19/16 → 5/4 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 5/4 → 21/16 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 21/16 → 11/8 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 11/8 → 23/16 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 23/16 → 3/2 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 3/2 → 25/16 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 25/16 → 13/8 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 13/8 → 27/16 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 27/16 → 7/4 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 7/4 → 29/16 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 29/16 → 15/8 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 15/8 → 31/16 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 31/16 → 2/1 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 2/1 → 33/16 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 33/16 → 17/8 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 17/8 → 35/16 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 35/16 → 9/4 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 9/4 → 37/16 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 37/16 → 19/8 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 19/8 → 39/16 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 39/16 → 5/2 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 5/2 → 41/16 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 41/16 → 21/8 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 21/8 → 43/16 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 43/16 → 11/4 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 11/4 → 45/16 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 45/16 → 23/8 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 23/8 → 47/16 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 47/16 → 3/1 | s:basique bank:wt_digital wtphaserand:0 ]", + "[ 3/1 → 49/16 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 49/16 → 25/8 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 25/8 → 51/16 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 51/16 → 13/4 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 13/4 → 53/16 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 53/16 → 27/8 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 27/8 → 55/16 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 55/16 → 7/2 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 7/2 → 57/16 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 57/16 → 29/8 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 29/8 → 59/16 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 59/16 → 15/4 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 15/4 → 61/16 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 61/16 → 31/8 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 31/8 → 63/16 | s:basique bank:wt_digital wtphaserand:1 ]", + "[ 63/16 → 4/1 | s:basique bank:wt_digital wtphaserand:1 ]", +] +`; + exports[`runs examples > example "xfade" example index 0 1`] = ` [ "[ 0/1 → 1/8 | s:hh gain:0 ]", diff --git a/test/testtunes.mjs b/test/testtunes.mjs index fc887610e..3690d315b 100644 --- a/test/testtunes.mjs +++ b/test/testtunes.mjs @@ -359,28 +359,6 @@ stack( "[~ [0 ~]] 0 [~ [4 ~]] 4".sub(7).restart(scales).scale(scales).early(.25) ).note().piano().slow(2)`; -/* -export const customTrigger = `// licensed with CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/ -// by Felix Roos -stack( - freq("55 [110,165] 110 [220,275]".mul("<1 <3/4 2/3>>").struct("x(3,8)").layer(x=>x.mul("1.006,.995"))), - freq("440(5,8)".clip(.18).mul("<1 3/4 2 2/3>")).gain(perlin.range(.2,.8)) -).s("/2") - .onTrigger((t,hap,ct)=>{ - const ac = Tone.getContext().rawContext; - t = ac.currentTime + t - ct; - const { freq, s, gain = 1 } = hap.value; - const master = ac.createGain(); - master.gain.value = 0.1 * gain; - master.connect(ac.destination); - const o = ac.createOscillator(); - o.type = s || 'triangle'; - o.frequency.value = Number(freq); - o.connect(master); - o.start(t); - o.stop(t + hap.duration); -}).stack(s("bd(3,8),hh*4,~ sd").webdirt())`; */ - export const swimmingWithSoundfonts = `// Koji Kondo - Swimming (Super Mario World) stack( n( diff --git a/undocumented.json b/undocumented.json index 8a38f205f..3f5a3be30 100644 --- a/undocumented.json +++ b/undocumented.json @@ -647,7 +647,6 @@ "evaluate" ], "/packages/webaudio/webaudio.mjs": [ - "webaudioOutputTrigger", "webaudioOutput", "webaudioRepl" ], diff --git a/website/package.json b/website/package.json index 499758147..87f4025e4 100644 --- a/website/package.json +++ b/website/package.json @@ -8,8 +8,7 @@ "start": "astro dev", "build": "astro build", "preview": "astro preview --port 3009 --host 0.0.0.0", - "astro": "astro", - "postinstall": "cp node_modules/hs2js/dist/tree-sitter.wasm public && cp node_modules/hs2js/dist/tree-sitter-haskell.wasm public" + "astro": "astro" }, "dependencies": { "@algolia/client-search": "^5.20.0", diff --git a/website/public/EmuSP12.json b/website/public/EmuSP12.json deleted file mode 100644 index b129ee57a..000000000 --- a/website/public/EmuSP12.json +++ /dev/null @@ -1,17 +0,0 @@ -{ -"_base": "https://raw.githubusercontent.com/ritchse/tidal-drum-machines/main/machines/EmuSP12/", -"bd": ["emusp12-bd/Bassdrum-01.wav","emusp12-bd/Bassdrum-02.wav","emusp12-bd/Bassdrum-03.wav","emusp12-bd/Bassdrum-04.wav","emusp12-bd/Bassdrum-05.wav","emusp12-bd/Bassdrum-06.wav","emusp12-bd/Bassdrum-07.wav","emusp12-bd/Bassdrum-08.wav","emusp12-bd/Bassdrum-09.wav","emusp12-bd/Bassdrum-10.wav","emusp12-bd/Bassdrum-11.wav","emusp12-bd/Bassdrum-12.wav","emusp12-bd/Bassdrum-13.wav","emusp12-bd/Bassdrum-14.wav"], -"cb": ["emusp12-cb/Cowbell.wav"], -"cp": ["emusp12-cp/Clap.wav"], -"cr": ["emusp12-cr/Crash.wav"], -"hh": ["emusp12-hh/Hat Closed-01.wav","emusp12-hh/Hat Closed-02.wav"], -"ht": ["emusp12-ht/Tom H-01.wav","emusp12-ht/Tom H-02.wav","emusp12-ht/Tom H-03.wav","emusp12-ht/Tom H-04.wav","emusp12-ht/Tom H-05.wav","emusp12-ht/Tom H-06.wav"], -"lt": ["emusp12-lt/Tom L-01.wav","emusp12-lt/Tom L-02.wav","emusp12-lt/Tom L-03.wav","emusp12-lt/Tom L-04.wav","emusp12-lt/Tom L-05.wav","emusp12-lt/Tom L-06.wav"], -"misc": ["emusp12-misc/Metal-01.wav","emusp12-misc/Metal-02.wav","emusp12-misc/Metal-03.wav","emusp12-misc/Scratch.wav","emusp12-misc/Shot-01.wav","emusp12-misc/Shot-02.wav","emusp12-misc/Shot-03.wav"], -"mt": ["emusp12-mt/Tom M-01.wav","emusp12-mt/Tom M-02.wav","emusp12-mt/Tom M-03.wav","emusp12-mt/Tom M-05.wav"], -"oh": ["emusp12-oh/Hhopen1.wav"], -"perc": ["emusp12-perc/Blow1.wav"], -"rd": ["emusp12-rd/Ride.wav"], -"rim": ["emusp12-rim/zRim Shot-01.wav","emusp12-rim/zRim Shot-02.wav"], -"sd": ["emusp12-sd/Snaredrum-01.wav","emusp12-sd/Snaredrum-02.wav","emusp12-sd/Snaredrum-03.wav","emusp12-sd/Snaredrum-04.wav","emusp12-sd/Snaredrum-05.wav","emusp12-sd/Snaredrum-06.wav","emusp12-sd/Snaredrum-07.wav","emusp12-sd/Snaredrum-08.wav","emusp12-sd/Snaredrum-09.wav","emusp12-sd/Snaredrum-10.wav","emusp12-sd/Snaredrum-11.wav","emusp12-sd/Snaredrum-12.wav","emusp12-sd/Snaredrum-13.wav","emusp12-sd/Snaredrum-14.wav","emusp12-sd/Snaredrum-15.wav","emusp12-sd/Snaredrum-16.wav","emusp12-sd/Snaredrum-17.wav","emusp12-sd/Snaredrum-18.wav","emusp12-sd/Snaredrum-19.wav","emusp12-sd/Snaredrum-20.wav","emusp12-sd/Snaredrum-21.wav"] -} diff --git a/website/public/fonts/tic80/license.txt b/website/public/fonts/tic80/license.txt new file mode 100644 index 000000000..e42ba4eb9 --- /dev/null +++ b/website/public/fonts/tic80/license.txt @@ -0,0 +1,5 @@ +The FontStruction “TIC-80 wide font” +(https://fontstruct.com/fontstructions/show/1388526) by “nesbox” is licensed +under a Creative Commons CC0 Public Domain Dedication license +(http://creativecommons.org/publicdomain/zero/1.0/). +[ancestry] \ No newline at end of file diff --git a/website/public/fonts/tic80/readme.txt b/website/public/fonts/tic80/readme.txt new file mode 100644 index 000000000..b3aaff55c --- /dev/null +++ b/website/public/fonts/tic80/readme.txt @@ -0,0 +1,16 @@ +The font file in this archive was created using Fontstruct the free, online +font-building tool. +This font was created by “nesbox”. +This font has a homepage where this archive and other versions may be found: +https://fontstruct.com/fontstructions/show/1388526 +[ancestry] +Try Fontstruct at https://fontstruct.com +It’s easy and it’s fun. + +Fontstruct is copyright ©2017-2025 Rob Meek + +LEGAL NOTICE: +In using this font you must comply with the licensing terms described in the +file “license.txt” included with this archive. +If you redistribute the font file in this archive, it must be accompanied by all +the other files from this archive, including this one. diff --git a/website/public/fonts/tic80/tic-80-wide-font.otf b/website/public/fonts/tic80/tic-80-wide-font.otf new file mode 100644 index 000000000..ba1c1caf4 Binary files /dev/null and b/website/public/fonts/tic80/tic-80-wide-font.otf differ diff --git a/website/public/img/strudel-signal-flow.png b/website/public/img/strudel-signal-flow.png new file mode 100644 index 000000000..c5099c917 Binary files /dev/null and b/website/public/img/strudel-signal-flow.png differ diff --git a/website/public/uzu-drumkit.json b/website/public/uzu-drumkit.json new file mode 100644 index 000000000..b9c7250d5 --- /dev/null +++ b/website/public/uzu-drumkit.json @@ -0,0 +1,76 @@ +{ + "_base": "https://raw.githubusercontent.com/tidalcycles/uzu-drumkit/main/", + "bd": [ + "bd/10_bd_switchangel.wav", + "bd/11_bd_mot4i.wav", + "bd/12_bd_mot4i.wav", + "bd/13_bd_mot4i.wav", + "bd/14_bd_switchangel.wav", + "bd/15_bd_switchangel.wav", + "bd/16_bd_switchangel.wav", + "bd/17_bd_switchangel.wav" + ], + "brk": [ + "brk/10_break_amen_pprocessed.wav" + ], + "cb": [ + "cb/10_perc_switchangel.wav" + ], + "cp": [ + "cp/10_cp_switchangel.wav", + "cp/11_cp_mot4i.wav" + ], + "cr": [ + "cr/10_cr_switchangel.wav", + "cr/11_cr_mot4i.wav" + ], + "hh": [ + "hh/10_hh_switchangel.wav", + "hh/11_hh_mot4i.wav", + "hh/12_hh_switchangel.wav", + "hh/13_hh_switchangel.wav", + "hh/14_hh_mot4i.wav" + ], + "ht": [ + "ht/10_ht_mot4i.wav" + ], + "lt": [ + "lt/10_lt_mot4i.wav" + ], + "misc": [ + "misc/10_misc_switchangel_ludens.wav", + "misc/11_misc_switchangel_ludens.wav", + "misc/12_misc_switchangel_ludens.wav", + "misc/13_misc_switchangel_ludens.wav", + "misc/14_misc_switchangel_ludens.wav" + ], + "mt": [ + "mt/10_mt_mot4i.wav" + ], + "oh": [ + "oh/10_oh_switchangel.wav", + "oh/11_oh_switchangel.wav", + "oh/12_oh_switchangel.wav", + "oh/13_oh_switchangel.wav" + ], + "rd": [ + "rd/10_rd_switchangel.wav" + ], + "rim": [ + "rim/10_rim_switchangel.wav", + "rim/11_rim_switch_angel.wav" + ], + "sd": [ + "sd/10_sd_switchangel-bounce-2.wav", + "sd/11_sd_switchangel_3.wav", + "sd/12_sd_switchangel_2.wav", + "sd/13_sd_switchangel_2.wav", + "sd/14_sd.wav" + ], + "sh": [ + "sh/10_sh_switchangel.wav" + ], + "tb": [ + "tb/10_tb.wav" + ] + } \ No newline at end of file diff --git a/website/public/uzu-wavetables.json b/website/public/uzu-wavetables.json new file mode 100644 index 000000000..375b1fad3 --- /dev/null +++ b/website/public/uzu-wavetables.json @@ -0,0 +1,28 @@ +{ + "_base": "https://raw.githubusercontent.com/tidalcycles/uzu-wavetables/main/", + "wt_digital": [ + "wt_digital/wt_bad_day.wav", + "wt_digital/wt_basique.wav", + "wt_digital/wt_crickets.wav", + "wt_digital/wt_curses.wav", + "wt_digital/wt_echoes.wav" + ], + "wt_digital_bad_day": ["wt_digital/wt_bad_day.wav"], + "wt_digital_basique": ["wt_digital/wt_basique.wav"], + "wt_digital_crickets": ["wt_digital/wt_crickets.wav"], + "wt_digital_curses": ["wt_digital/wt_curses.wav"], + "wt_digital_echoes": ["wt_digital/wt_echoes.wav"], + "wt_vgame": [ + "wt_vgame/wt_vgame10.wav", + "wt_vgame/wt_vgame11.wav", + "wt_vgame/wt_vgame12.wav", + "wt_vgame/wt_vgame13.wav", + "wt_vgame/wt_vgame14.wav", + "wt_vgame/wt_vgame15.wav", + "wt_vgame/wt_vgame16.wav", + "wt_vgame/wt_vgame17.wav", + "wt_vgame/wt_vgame18.wav", + "wt_vgame/wt_vgame19.wav", + "wt_vgame/wt_vgame20.wav" + ] +} \ No newline at end of file diff --git a/website/src/config.ts b/website/src/config.ts index 2e2e264da..f335b272e 100644 --- a/website/src/config.ts +++ b/website/src/config.ts @@ -103,6 +103,7 @@ export const SIDEBAR: Sidebar = { Understand: [ { text: 'Coding syntax', link: 'learn/code' }, { text: 'Pitch', link: 'understand/pitch' }, + { text: 'Xen Harmonic Functions', link: 'learn/xen' }, { text: 'Cycles', link: 'understand/cycles' }, { text: 'Voicings', link: 'understand/voicings' }, { text: 'Pattern Alignment', link: 'technical-manual/alignment' }, diff --git a/website/src/pages/learn/code.mdx b/website/src/pages/learn/code.mdx index ca2564dec..c454dcceb 100644 --- a/website/src/pages/learn/code.mdx +++ b/website/src/pages/learn/code.mdx @@ -44,7 +44,7 @@ xxx("foo").yyy("bar") Generally, `xxx` and `yyy` are called [_functions_](), while `foo` and `bar` are called function [_arguments_ or _parameters_](). So far, we've used the functions to declare which aspect of the sound we want to control, and their arguments for the actual data. -The `yyy` function is called a [_chained_ function](https://en.wikipedia.org/wiki/Method_chaining), because it is appended with a dot (`.`). +The `yyy` function is called a [_chained_ function](https://en.wikipedia.org/wiki/Method_chaining), because it is preceded with a dot (`.`). Generally, the idea with chaining is that code such as `a("this").b("that").c("other")` allows `a`, `b` and `c` functions to happen in a specified order, without needing to write them as three separate lines of code. You can think of this as being similar to chaining audio effects together using guitar pedals or digital audio effects. @@ -60,6 +60,23 @@ Strudel makes heavy use of chained functions. Here is a more sophisticated examp .room(0.5)`} /> +## Write your own chained function + +You can write your own chained function using `register`. Here's the above chain but registered as a reusable, chained function. + + pat + .s("sawtooth") + .cutoff(500) + //.delay(0.5) + .room(0.5) + ) +note("a3 c#4 e4 a4").effectChain()`} +/> + +Try adding `.rev()` after `effectChain()` to hear further effects added. + # Comments The `//` in the example above is a line comment, resulting in the `delay` function being ignored. diff --git a/website/src/pages/learn/conditional-modifiers.mdx b/website/src/pages/learn/conditional-modifiers.mdx index c2e22595b..d31ab0cf0 100644 --- a/website/src/pages/learn/conditional-modifiers.mdx +++ b/website/src/pages/learn/conditional-modifiers.mdx @@ -34,11 +34,11 @@ import { JsDoc } from '../../docs/JsDoc'; ## arp - + ## arpWith 🧪 - + ## struct @@ -58,7 +58,7 @@ import { JsDoc } from '../../docs/JsDoc'; ## hush - + ## invert diff --git a/website/src/pages/learn/effects.mdx b/website/src/pages/learn/effects.mdx index 2f7eaa7f8..517eb658f 100644 --- a/website/src/pages/learn/effects.mdx +++ b/website/src/pages/learn/effects.mdx @@ -11,6 +11,129 @@ import { JsDoc } from '../../docs/JsDoc'; Whether you're using a synth or a sample, you can apply any of the following built-in audio effects. As you might suspect, the effects can be chained together, and they accept a pattern string as their argument. +# Signal chain + + + +The signal chain in Strudel is as follows: + +- An sound-generating event is triggered by a pattern + - This has a start time and a duration, which is usually + controlled by the note length and ADSR parameters + - If we exceed the max polyphony, old sounds begin to die off + - Muted sounds (one whose `s` value is `-`, `~`, or `_`) are skipped +- A sound is produced (through, say, a sample or an oscillator) + - This is where detune-based effects (like `detune`, `penv`, etc. occur) +- The following will occur _in order_ and only if they've been called in the pattern. Note that all of these are + single use effects, meaning that multiple occurrences of them in a pattern will simply override the values + (e.g. you can't do `s("bd").lpf(100).distort(2).lpf(800)` to lowpass, distort, and then lowpass + again) + - Phase vocoder (`stretch`) + - Gain is applied (`gain`) + - This is where the main (volume) ADSR happens + - A lowpass filter (`lpf`) + - A highpass filter (`hpf`) + - A bandpass filter (`bandpass`) + - A vowel filter (`vowel`) + - Sample rate reduction (`coarse`) + - Bit crushing (`crush`) + - Waveshape distortion (`shape`) + - Normal distortion (`distort`) + - Tremolo (`tremolo`) + - Compressor (`compressor`) + - Panning (`pan`) + - Phaser (`phaser`) + - Postgain (`post`) +- The sound is then split into multiple destinations + - Dry output (amount controlled by `dry` parameter) + - The sends + - Analyzers + - These are used for tooling like `scope` and `spectrum` and their setup usually happens behind the scenes + - Delay (amount controlled by `delay` parameter) + - Reverb (amount controlled by `room` parameter) +- The dry output, delay, and reverb are joined into what is called the "orbit" of the pattern (see more in the section below) + - The `duck` effect affects the volume of all signals in the orbit + - The orbit is then sent to the mixer + +## Orbits + +Orbits are the way in which outputs are handled in Strudel. They also prescribe which delay and reverb to associate with the dry signal. +By default, all orbits are mixed down to channels `1` and `2` in stereo, however with the "Multi Channel Orbits" setting +(under Settings at the right) you can use them as individual 2 channel stereo outs (orbit `i` will be mapped to +to channels `2i` and `2i + 1`). You can then use routers like Blackhole 16 to retrieve and record all of the channels in a DAW for later processing. + +The default orbit is `1` and it is set with `orbit`. You may send a sound to multiple orbits via mininotation + + + +but please be careful as this will create three copies of the sound behind the scenes, meaning that if they are mixed +down to a single output, they will triple the volume. We've reduced the gain here to save your ears. + +⚠️ There is only one delay and reverb per orbit, so please be aware that if you attempt to change the parameters on two +patterns pointing to the same orbit, it can lead to unpredictable results. Compare, for example, this pretty pluck +with a large reverb: + + + +versus the same pluck with a muted kick drum coming in and overwriting the `roomsize` value: + + + +This is due to them sharing the same orbit: the default of `1`. It can be corrected simply by updating the orbits to be +distinct: + + + +## Continuous changes + +As all of the above is triggered by a _sound occurring_, it is often the case that parameters may not be +modified continuously in time. For example, + + + +Will not produce a continually LFO'd low-pass filter due to the `tri` only being sampled every time the note hits +(in this case the default of once per cycle). You can fake it by introducing more sound-generating events, e.g.: + + + +Some parameters _do_ induce continuous variations in time, though: + +- The ADSR curve (governed by `attack`, `sustain`, `decay`, `release`) +- The pitch envelope curve (governed by `penv` and its associated ADSR) +- The FM curve (`fmenv`) +- The filter envelopes (`lpenv`, `hpenv`, `bpenv`) +- Tremolo (`tremolo`) +- Phaser (`phaser`) +- Vibrato (`vib`) +- Ducking (`duckorbit`) + # Filters Filters are an essential building block of [subtractive synthesis](https://en.wikipedia.org/wiki/Subtractive_synthesis). @@ -57,6 +180,34 @@ Each filter has 2 parameters: +# Amplitude Modulation + +Amplitude modulation changes the amplitude (gain) periodically over time. + +## am + + + +## tremolosync + + + +## tremolodepth + + + +## tremoloskew + + + +## tremolophase + + + +## tremoloshape + + + # Amplitude Envelope The amplitude [envelope]() controls the dynamic contour of a sound. @@ -311,4 +462,18 @@ global effects use the same chain for all events of the same orbit: +## Duck + +### duckorbit + + + +### duckattack + + + +### duckdepth + + + Next, we'll look at input / output via [MIDI, OSC and other methods](/learn/input-output). diff --git a/website/src/pages/learn/mini-notation.mdx b/website/src/pages/learn/mini-notation.mdx index 7dbf67ce2..02ead7434 100644 --- a/website/src/pages/learn/mini-notation.mdx +++ b/website/src/pages/learn/mini-notation.mdx @@ -168,6 +168,20 @@ Using "!" we can repeat without speeding up: *2")`} punchcard /> +## Randomness + +Events with a "?" placed after them will have a 50% chance of being removed from the pattern: + + + +Adding a number between 0 and 1 after the "?" will affect the likelihood of the event being removed. For example, events with "?0.1" placed after them will have a 10% chance of being removed: + + + +Events separated by a "|" will be chosen from at random: + + + ## Mini-notation review To recap what we've learned so far, compare the following patterns: @@ -179,6 +193,8 @@ To recap what we've learned so far, compare the following patterns: *2")`} /> *2")`} /> *2")`} /> +*2")`} /> +*2")`} /> ## Euclidian rhythms diff --git a/website/src/pages/learn/mondo-notation.mdx b/website/src/pages/learn/mondo-notation.mdx index 3b00b9902..e2b0c4027 100644 --- a/website/src/pages/learn/mondo-notation.mdx +++ b/website/src/pages/learn/mondo-notation.mdx @@ -176,3 +176,16 @@ $ chord # voicing /> The `$` sign is an alias for `,` so it will create a stack behind the scenes. + +## variables + +using the `def` keyword, you can define variables: + + diff --git a/website/src/pages/learn/samples.mdx b/website/src/pages/learn/samples.mdx index bbe650cf5..201d57dfa 100644 --- a/website/src/pages/learn/samples.mdx +++ b/website/src/pages/learn/samples.mdx @@ -59,6 +59,14 @@ Furthermore, strudel also loads instrument samples from [VCSL](https://github.co To see which sample names are available, open the `sounds` tab in the [REPL](https://strudel.cc/). +You can also create custom aliases for existing sounds using the `soundAlias` function: + + + Note that only the sample maps (mapping names to URLs) are loaded initially, while the audio samples themselves are not loaded until they are actually played. This behaviour of loading things only when they are needed is also called `lazy loading`. While it saves resources, it can also lead to sounds not being audible the first time they are triggered, because the sound is still loading. @@ -178,6 +186,16 @@ the version number). It is also possible, of course, to just remove it from cache (deleting cache in browser Privacy settings, or from the dev console if you're technically minded, or by using a cache deleting extension). +## Generating strudel.json + +You can use [@strudel/sampler](https://www.npmjs.com/package/@strudel/sampler) to generate a strudel.json file for you, by running: + +```sh +npx --yes @strudel/sampler --json > strudel.json +``` + +See other uses of strudel/sampler further below, under "From Disk via @strudel/sampler". + ## Github Shortcut Because loading samples from github is common, there is a shortcut: @@ -361,6 +379,10 @@ Sampler effects are functions that can be used to change the behaviour of sample +### scrub + + + ### speed diff --git a/website/src/pages/learn/xen.mdx b/website/src/pages/learn/xen.mdx new file mode 100644 index 000000000..b8b27ac7f --- /dev/null +++ b/website/src/pages/learn/xen.mdx @@ -0,0 +1,95 @@ +--- +title: Xen Harmonic Functions +layout: ../../layouts/MainLayout.astro +--- + +import { MiniRepl } from '../../docs/MiniRepl'; +import { JsDoc } from '../../docs/JsDoc'; + +# Xen Harmonic Functions + +These functions allow the use of scales other than your typical chromatic 12 based ones. + +### tune(scale) + + + +Here's an example of how to configure a basic hexany scale: + + + +Try other scales like `hexany1`, `iraq`, `gumbeng`, `gunkali`, or `tranh3` + +For a full list of available scales from tunejs, see http://abbernie.github.io/tune/scales.html + +You can set your root to be a particular note with `getFreq` + + + +Some tunings become more pronounced with a longer reverb decay: + + -".tune("gumbeng") + .mul(getFreq('c3')) + .freq().clip(.8).room("3:10").rdim(10000).rfade(5)`} +/> + +Additionally, you can combo this with `fmap` so that the base note changes: + +".fmap(getFreq)) + .freq().legato("2 .7").room("1:15").rdim(8500).rlp(14000).rfade(8)`} +/> + +Combining this with various polyrhythm tricks can become very evocative: + + ~ ~,<-4 -5>" + .transpose(4) + .tune("iraq") + .mul("".fmap(getFreq)) + .freq().clip(.5).room(1).rfade(9)`} +/> + +Another helpful trick when exploring new tunings is to strum them. +Many have a much more enchanting sound that was chosen over many generations of musicians for being strummed. + +Take the `sanza` tuning: + + + +Notes 7 and 9 will clash quite a bit if you arp them normally. Many tunings will have this sort of sound, and it can feel distracting on its own. +See how close they are on the pitch wheel? + + + +This quality is often due to how the tunings were formed with instruments that were played differently than a piano. +As such, some tunings are much better strummed, with the subtle clash of the detuned notes actually making the sound much more magical: + +") + .tune("sanza") + .mul(getFreq('c3')).freq() + .legato("3").room(1).rfade(5)`} +/> + +Note the legato and reverb effects make sure the sound of the strumming gets to wash together. Alternating the direction of the strum can make the +tones sound even more alive, too. + +The `tranh3` tuning has a similar set of notes, with two clashing. You might trying plugging that in above and see if you find a favorite strumming pattern. diff --git a/website/src/pages/technical-manual/project-start.mdx b/website/src/pages/technical-manual/project-start.mdx index 4e6e37636..4a269d1b9 100644 --- a/website/src/pages/technical-manual/project-start.mdx +++ b/website/src/pages/technical-manual/project-start.mdx @@ -7,6 +7,21 @@ layout: ../../layouts/MainLayout.astro This Guide shows you the different ways to get started with using Strudel in your own project. +## Respect the license + +First, please take a moment to understand Strudel's free/open source license, +[AGPL-3.0](https://www.gnu.org/licenses/agpl-3.0.en.html). + +Here is a lay summary, but check the license for legal definitions and responsibilities. + +- You can distribute modified versions if you keep track of the changes and the date you made them. +- You must license derivative work under the same license. +- Source code must be distributed along with web publication. + +Among other things, it means that when you share your work, the whole application must be shared under the same free/open source license, or one compatible with it. This is because we want Strudel to stay free/open source. In other words, you are not permitted to distribute integrations of Strudel with libraries or other code that does not have a compatible free/open source license. + +This also applies to clones informed by reading Strudel's source code, as legally speaking, that counts as a 'derivative work'. Again, please [read the licence](https://www.gnu.org/licenses/agpl-3.0.en.html) for details. + ## Embedding the Strudel REPL There are 3 quick ways to embed strudel in your website: diff --git a/website/src/pages/technical-manual/repl.mdx b/website/src/pages/technical-manual/repl.mdx index f53efac41..8c4287af5 100644 --- a/website/src/pages/technical-manual/repl.mdx +++ b/website/src/pages/technical-manual/repl.mdx @@ -9,7 +9,9 @@ import { MiniRepl } from '../../docs/MiniRepl'; {/* The [REPL](https://strudel.cc/) is the place where all packages come together to form a live coding system. It can also be seen as a reference implementation for users of the library. */} -While Strudel can be used as a library in any JavaScript codebase, its main, reference user interface is the Strudel REPL^[REPL stands for read, evaluate, print/play, loop. It is friendly jargon for an interactive programming interface from computing heritage, usually for a commandline interface but also applied to live coding editors.], which is a browser-based live coding environment. This live code editor is dedicated to manipulating Strudel patterns while they play. The REPL features built-in visual feedback, highlighting which elements in the patterned (mini-notation) sequences are influencing the event that is currently being played. This feedback is designed to support both learning and live use of Strudel. +While Strudel can be used as a library in any JavaScript codebase, its main, reference user interface is the Strudel REPL[^1], which is a browser-based live coding environment. This live code editor is dedicated to manipulating Strudel patterns while they play. The REPL features built-in visual feedback, highlighting which elements in the patterned (mini-notation) sequences are influencing the event that is currently being played. This feedback is designed to support both learning and live use of Strudel. + +[^1]: REPL stands for read, evaluate, print/play, loop. It is friendly jargon for an interactive programming interface from computing heritage, usually for a commandline interface but also applied to live coding editors. Besides a UI for playback control and meta information, the main part of the REPL interface is the code editor powered by CodeMirror. In it, the user can edit and evaluate pattern code live, using one of the available synthesis outputs to create music and/or sound art. The control flow of the REPL follows 3 basic steps: diff --git a/website/src/pages/understand/cycles.mdx b/website/src/pages/understand/cycles.mdx index 1c0a778a6..bcf987c4d 100644 --- a/website/src/pages/understand/cycles.mdx +++ b/website/src/pages/understand/cycles.mdx @@ -67,6 +67,8 @@ Or using 2 beats per cycle: s("bd sd, hh*4")`} /> +You can use the `setcps` method to set the global tempo in cycles per second. `setcpm(x)` is the same as `setcps(x / 60)`. + To set a specific bpm, use `setcpm(bpm/bpc)` diff --git a/website/src/pages/workshop/first-sounds.mdx b/website/src/pages/workshop/first-sounds.mdx index 74daf4bab..1d659972b 100644 --- a/website/src/pages/workshop/first-sounds.mdx +++ b/website/src/pages/workshop/first-sounds.mdx @@ -262,14 +262,14 @@ It is quite common that there are many ways to express the same idea. **selecting sample numbers separately** -Instead of using ":", we can also use the `n` function to select sample numbers: - - - -This is shorter and more readable than: +Instead of selecting sample numbers one by one: +We can also use the `n` function to make it shorter and more readable: + + + ## Recap Now we've learned the basics of the so called Mini-Notation, the rhythm language of Tidal. diff --git a/website/src/repl/Repl.css b/website/src/repl/Repl.css index 3e13ff5a2..b5cd34d81 100644 --- a/website/src/repl/Repl.css +++ b/website/src/repl/Repl.css @@ -69,3 +69,150 @@ text-decoration: underline 0.18rem; text-underline-offset: 0.22rem; } + +/* Override default styles from the codemirror inline css for autocomplete info tooltip*/ +.cm-tooltip.cm-completionInfo { + padding: 0 !important; + border: 1px solid var(--foreground) !important; + border-radius: 4px !important; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3) !important; + max-width: 500px !important; + min-width: 300px !important; + max-height: 400px !important; + background-color: var(--lineHighlight) !important; + overflow: auto; + background: var(--background) !important; +} + +/* Main tooltip container */ +.autocomplete-info-container { + padding: 12px !important; + border-radius: 4px !important; + color: var(--foreground); + font-family: var(--font-family, 'SF Mono', 'Monaco', monospace); + font-size: var(--font-size, 13px); + line-height: 1.4; + max-width: 600px; + height: 100%; + min-width: 400px; + white-space: normal !important; + overflow-y: auto !important; +} + +.autocomplete-info-tooltip { + overflow-y: auto !important; +} + +.autocomplete-info-function-description { + white-space: pre-wrap !important; +} + +.autocomplete-info-function-name { + font-size: 15px; + font-weight: 600; + color: var(--foreground); + margin: 0 0 8px 0; +} + +.autocomplete-info-function-synonyms { + margin: 0 0 12px 0; + color: var(--foreground); + line-height: 1.5; + opacity: 0.8; +} + +.autocomplete-info-function-description { + margin: 0 0 12px 0; + color: var(--foreground); + line-height: 1.5; + opacity: 0.8; +} + +.autocomplete-info-section-title { + font-size: 12px; + font-weight: 600; + color: var(--foreground); + margin: 16px 0 6px 0; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.autocomplete-info-section-title:first-child { + margin-top: 0; +} + +.autocomplete-info-params-section { + margin-top: 12px; +} + +.autocomplete-info-params-list { + list-style: none; + margin: 0; + padding: 0; +} + +.autocomplete-info-param-item { + margin-bottom: 8px; + padding: 8px; + background-color: var(--lineBackground); + border-radius: 3px; + border-left: 2px solid var(--foreground, #555); +} + +.autocomplete-info-param-item:last-child { + margin-bottom: 0; +} + +.autocomplete-info-param-name { + font-weight: 600; + color: var(--variable, var(--foreground)); + margin-right: 8px; +} + +.autocomplete-info-param-type { + color: var(--comment); + font-size: 12px; + background-color: var(--gutterForeground); + padding: 1px 4px; + border-radius: 2px; +} + +.autocomplete-info-param-desc { + color: var(--foreground); + font-size: 10px; + margin-top: 4px; + line-height: 1.4; + opacity: 0.7; +} + +.autocomplete-info-examples-section { + margin-top: 12px; +} + +.autocomplete-info-example-code { + background: var(--lineBackground); + color: var(--foreground); + padding: 8px; + border-radius: 3px; + font-family: var(--font-family, 'SF Mono', 'Monaco', monospace); + font-size: 12px; + line-height: 1.5; + margin: 4px 0; + overflow-x: auto; + white-space: pre; + border: 1px solid var(--foreground, #3a3a3a); +} + +.autocomplete-info-tooltip::-webkit-scrollbar { + width: 4px; +} + +.autocomplete-info-tooltip::-webkit-scrollbar-track { +} + +.autocomplete-info-tooltip::-webkit-scrollbar-thumb { + border-radius: 2px; +} + +.autocomplete-info-tooltip::-webkit-scrollbar-thumb:hover { +} diff --git a/website/src/repl/components/button/action-button.jsx b/website/src/repl/components/button/action-button.jsx new file mode 100644 index 000000000..d589b7bed --- /dev/null +++ b/website/src/repl/components/button/action-button.jsx @@ -0,0 +1,10 @@ +import cx from '@src/cx.mjs'; + +export function ActionButton({ children, label, labelIsHidden, className, ...buttonProps }) { + return ( + + ); +} diff --git a/website/src/repl/components/panel/PatternsTab.jsx b/website/src/repl/components/panel/PatternsTab.jsx index 8ced8a990..8e2b75b96 100644 --- a/website/src/repl/components/panel/PatternsTab.jsx +++ b/website/src/repl/components/panel/PatternsTab.jsx @@ -12,8 +12,8 @@ import { useMemo } from 'react'; import { getMetadata } from '../../../metadata_parser.js'; import { useExamplePatterns } from '../../useExamplePatterns.jsx'; import { parseJSON, isUdels } from '../../util.mjs'; -import { ButtonGroup } from './Forms.jsx'; -import { settingsMap, useSettings } from '../../../settings.mjs'; +import { useSettings } from '../../../settings.mjs'; +import { ActionButton } from '../button/action-button.jsx'; import { Pagination } from '../pagination/Pagination.jsx'; import { useState } from 'react'; import { useDebounce } from '../usedebounce.jsx'; @@ -75,15 +75,6 @@ function PatternButtons({ patterns, activePattern, onClick, started }) { ); } -function ActionButton({ children, onClick, label, labelIsHidden }) { - return ( - - ); -} - const updateCodeWindow = (context, patternData, reset = false) => { context.handleUpdate(patternData, reset); }; @@ -125,7 +116,7 @@ function UserPatterns({ context }) { style={{ display: 'none' }} type="file" multiple - accept="text/plain,application/json" + accept="text/plain,text/x-markdown,application/json" onChange={(e) => importPatterns(e.target.files)} /> import diff --git a/website/src/repl/components/panel/Reference.jsx b/website/src/repl/components/panel/Reference.jsx index 1b617341b..505cf50d2 100644 --- a/website/src/repl/components/panel/Reference.jsx +++ b/website/src/repl/components/panel/Reference.jsx @@ -2,9 +2,32 @@ import { useMemo, useState } from 'react'; import jsdocJson from '../../../../../doc.json'; import { Textbox } from '../textbox/Textbox'; -const availableFunctions = jsdocJson.docs - .filter(({ name, description }) => name && !name.startsWith('_') && !!description) - .sort((a, b) => /* a.meta.filename.localeCompare(b.meta.filename) + */ a.name.localeCompare(b.name)); + +const isValid = ({ name, description }) => name && !name.startsWith('_') && !!description; + +const availableFunctions = (() => { + const seen = new Set(); // avoid repetition + const functions = []; + for (const doc of jsdocJson.docs) { + if (!isValid(doc)) continue; + functions.push(doc); + const synonyms = doc.synonyms || []; + for (const s of synonyms) { + if (!s || seen.has(s)) continue; + seen.add(s); + // Swap `doc.name` in for `s` in the list of synonyms + const synonymsWithDoc = [doc.name, ...synonyms].filter((x) => x && x !== s); + functions.push({ + ...doc, + name: s, // update names for the synonym + longname: s, + synonyms: synonymsWithDoc, + synonyms_text: synonymsWithDoc.join(', '), + }); + } + } + return functions.sort((a, b) => /* a.meta.filename.localeCompare(b.meta.filename) + */ a.name.localeCompare(b.name)); +})(); const getInnerText = (html) => { var div = document.createElement('div'); @@ -21,10 +44,10 @@ export function Reference() { return true; } - const lowCaseSearch = search.toLowerCase(); + const lowerCaseSearch = search.toLowerCase(); return ( - entry.name.toLowerCase().includes(lowCaseSearch) || - (entry.synonyms?.some((s) => s.includes(lowCaseSearch)) ?? false) + entry.name.toLowerCase().includes(lowerCaseSearch) || + (entry.synonyms?.some((s) => s.toLowerCase().includes(lowerCaseSearch)) ?? false) ); }); }, [search]); diff --git a/website/src/repl/components/panel/SettingsTab.jsx b/website/src/repl/components/panel/SettingsTab.jsx index 5be9b602e..80daef018 100644 --- a/website/src/repl/components/panel/SettingsTab.jsx +++ b/website/src/repl/components/panel/SettingsTab.jsx @@ -74,6 +74,7 @@ const fontFamilyOptions = { FiraCode: 'FiraCode', 'FiraCode-SemiBold': 'FiraCode SemiBold', teletext: 'teletext', + tic80: 'tic80', mode7: 'mode7', BigBlueTerminal: 'BigBlueTerminal', x3270: 'x3270', @@ -109,6 +110,8 @@ export function SettingsTab({ started }) { togglePanelTrigger, maxPolyphony, multiChannelOrbits, + isTabIndentationEnabled, + isMultiCursorEnabled, } = useSettings(); const shouldAlwaysSync = isUdels(); const canChangeAudioDevice = AudioContext.prototype.setSinkId != null; @@ -262,6 +265,16 @@ export function SettingsTab({ started }) { onChange={(cbEvent) => settingsMap.setKey('isLineWrappingEnabled', cbEvent.target.checked)} value={isLineWrappingEnabled} /> + settingsMap.setKey('isTabIndentationEnabled', cbEvent.target.checked)} + value={isTabIndentationEnabled} + /> + settingsMap.setKey('isMultiCursorEnabled', cbEvent.target.checked)} + value={isMultiCursorEnabled} + /> settingsMap.setKey('isFlashEnabled', cbEvent.target.checked)} @@ -299,7 +312,8 @@ export function SettingsTab({ started }) { onClick={() => { confirmDialog('Sure?').then((r) => { if (r) { - settingsMap.set(defaultSettings); + const { userPatterns } = settingsMap.get(); // keep current patterns + settingsMap.set({ ...defaultSettings, userPatterns }); } }); }} diff --git a/website/src/repl/components/panel/SoundsTab.jsx b/website/src/repl/components/panel/SoundsTab.jsx index a976eb3d2..363f6b98a 100644 --- a/website/src/repl/components/panel/SoundsTab.jsx +++ b/website/src/repl/components/panel/SoundsTab.jsx @@ -2,16 +2,21 @@ import useEvent from '@src/useEvent.mjs'; import { useStore } from '@nanostores/react'; import { getAudioContext, soundMap, connectToDestination } from '@strudel/webaudio'; import { useMemo, useRef, useState } from 'react'; -import { settingsMap, useSettings } from '../../../settings.mjs'; +import { settingsMap, soundFilterType, useSettings } from '../../../settings.mjs'; import { ButtonGroup } from './Forms.jsx'; import ImportSoundsButton from './ImportSoundsButton.jsx'; import { Textbox } from '../textbox/Textbox.jsx'; +import { ActionButton } from '../button/action-button.jsx'; +import { confirmDialog } from '@src/repl/util.mjs'; +import { clearIDB, userSamplesDBConfig } from '@src/repl/idbutils.mjs'; +import { prebake } from '@src/repl/prebake.mjs'; const getSamples = (samples) => Array.isArray(samples) ? samples.length : typeof samples === 'object' ? Object.values(samples).length : 1; export function SoundsTab() { const sounds = useStore(soundMap); + const { soundsFilter } = useSettings(); const [search, setSearch] = useState(''); const { BASE_URL } = import.meta.env; @@ -27,18 +32,22 @@ export function SoundsTab() { .sort((a, b) => a[0].localeCompare(b[0])) .filter(([name]) => name.toLowerCase().includes(search.toLowerCase())); - if (soundsFilter === 'user') { + if (soundsFilter === soundFilterType.USER) { return filtered.filter(([_, { data }]) => !data.prebake); } - if (soundsFilter === 'drums') { + if (soundsFilter === soundFilterType.DRUMS) { return filtered.filter(([_, { data }]) => data.type === 'sample' && data.tag === 'drum-machines'); } - if (soundsFilter === 'samples') { + if (soundsFilter === soundFilterType.SAMPLES) { return filtered.filter(([_, { data }]) => data.type === 'sample' && data.tag !== 'drum-machines'); } - if (soundsFilter === 'synths') { + if (soundsFilter === soundFilterType.SYNTHS) { return filtered.filter(([_, { data }]) => ['synth', 'soundfont'].includes(data.type)); } + if (soundsFilter === soundFilterType.WAVETABLES) { + return filtered.filter(([_, { data }]) => data.type === 'wavetable'); + } + //TODO: tidy this up, it does not need to be saved in settings if (soundsFilter === 'importSounds') { return []; } @@ -48,6 +57,9 @@ export function SoundsTab() { // holds mutable ref to current triggered sound const trigRef = useRef(); + // Used to cycle through sound previews on banks with multiple sounds + let soundPreviewIdx = 0; + // stop current sound on mouseup useEvent('mouseup', () => { const t = trigRef.current; @@ -57,10 +69,10 @@ export function SoundsTab() { }); }); return ( -
+
setSearch(v)} /> -
+
settingsMap.setKey('soundsFilter', value)} @@ -68,13 +80,33 @@ export function SoundsTab() { samples: 'samples', drums: 'drum-machines', synths: 'Synths', + wavetables: 'Wavetables', user: 'User', importSounds: 'import-sounds', }} >
-
+ {soundsFilter === soundFilterType.USER && soundEntries.length > 0 && ( + { + try { + const confirmed = await confirmDialog('Delete all imported user samples?'); + if (confirmed) { + clearIDB(userSamplesDBConfig.dbName); + soundMap.set({}); + await prebake(); + } + } catch (e) { + console.error(e); + } + }} + /> + )} + +
{soundEntries.map(([name, { data, onTrigger }]) => { return ( trigRef.current?.node?.disconnect(); trigRef.current = Promise.resolve(onTrigger(time, params, onended)); @@ -101,6 +135,7 @@ export function SoundsTab() { {' '} {name} {data?.type === 'sample' ? `(${getSamples(data.samples)})` : ''} + {data?.type === 'wavetable' ? `(${getSamples(data.tables)})` : ''} {data?.type === 'soundfont' ? `(${data.fonts.length})` : ''} ); @@ -151,9 +186,7 @@ export function SoundsTab() { ) : ( '' )} - {!soundEntries.length && soundsFilter !== 'importSounds' - ? 'No custom sounds loaded in this pattern (yet).' - : ''} + {!soundEntries.length && soundsFilter !== 'importSounds' ? 'No sounds loaded' : ''}
); diff --git a/website/src/repl/idbutils.mjs b/website/src/repl/idbutils.mjs index 5fc62c576..d87d649c2 100644 --- a/website/src/repl/idbutils.mjs +++ b/website/src/repl/idbutils.mjs @@ -1,4 +1,4 @@ -import { registerSound, onTriggerSample } from '@strudel/webaudio'; +import { registerSampleSource } from '@strudel/webaudio'; import { isAudioFile } from './files.mjs'; import { logger } from '@strudel/core'; @@ -12,17 +12,21 @@ export const userSamplesDBConfig = { }; // deletes all of the databases, useful for debugging -function clearIDB() { +function clearAllIDB() { window.indexedDB .databases() .then((r) => { - for (var i = 0; i < r.length; i++) window.indexedDB.deleteDatabase(r[i].name); + for (var i = 0; i < r.length; i++) clearIDB(r[i].name); }) .then(() => { alert('All data cleared.'); }); } +export function clearIDB(dbName) { + return window.indexedDB.deleteDatabase(dbName); +} + // queries the DB, and registers the sounds so they can be played export function registerSamplesFromDB(config = userSamplesDBConfig, onComplete = () => {}) { openDB(config, (objectStore) => { @@ -72,13 +76,7 @@ export function registerSamplesFromDB(config = userSamplesDBConfig, onComplete = }) .map((title) => titlePathMap.get(title)); - registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, value), { - type: 'sample', - samples: value, - baseUrl: undefined, - prebake: false, - tag: undefined, - }); + registerSampleSource(key, value, { prebake: false }); }); logger('imported sounds registered!', 'success'); diff --git a/website/src/repl/prebake.mjs b/website/src/repl/prebake.mjs index 68befdd96..1fbc84021 100644 --- a/website/src/repl/prebake.mjs +++ b/website/src/repl/prebake.mjs @@ -28,7 +28,13 @@ export async function prebake() { prebake: true, tag: 'drum-machines', }), - samples(`${baseNoTrailing}/EmuSP12.json`, undefined, { prebake: true, tag: 'drum-machines' }), + samples(`${baseNoTrailing}/uzu-drumkit.json`, undefined, { + prebake: true, + tag: 'drum-machines', + }), + samples(`${baseNoTrailing}/uzu-wavetables.json`, undefined, { + prebake: true, + }), samples(`${baseNoTrailing}/mridangam.json`, undefined, { prebake: true, tag: 'drum-machines' }), samples( { diff --git a/website/src/settings.mjs b/website/src/settings.mjs index 84b433141..9365a6db5 100644 --- a/website/src/settings.mjs +++ b/website/src/settings.mjs @@ -8,6 +8,15 @@ export const audioEngineTargets = { osc: 'osc', }; +export const soundFilterType = { + USER: 'user', + DRUMS: 'drums', + SAMPLES: 'samples', + SYNTHS: 'synths', + WAVETABLES: 'wavetables', + ALL: 'all', +}; + export const defaultSettings = { activeFooter: 'intro', keybindings: 'codemirror', @@ -21,12 +30,14 @@ export const defaultSettings = { isSyncEnabled: false, isLineWrappingEnabled: false, isPatternHighlightingEnabled: true, + isTabIndentationEnabled: false, + isMultiCursorEnabled: false, theme: 'strudelTheme', fontFamily: 'monospace', fontSize: 18, latestCode: '', isZen: false, - soundsFilter: 'all', + soundsFilter: soundFilterType.ALL, patternFilter: 'community', // panelPosition: window.innerWidth > 1000 ? 'right' : 'bottom', //FIX: does not work on astro panelPosition: 'right', @@ -77,6 +88,8 @@ export function useSettings() { isLineWrappingEnabled: parseBoolean(state.isLineWrappingEnabled), isFlashEnabled: parseBoolean(state.isFlashEnabled), isSyncEnabled: isUdels() ? true : parseBoolean(state.isSyncEnabled), + isTabIndentationEnabled: parseBoolean(state.isTabIndentationEnabled), + isMultiCursorEnabled: parseBoolean(state.isMultiCursorEnabled), fontSize: Number(state.fontSize), panelPosition: state.activeFooter !== '' && !isUdels() ? state.panelPosition : 'bottom', // <-- keep this 'bottom' where it is! isPanelPinned: parseBoolean(state.isPanelPinned), diff --git a/website/src/styles/index.css b/website/src/styles/index.css index 7fa4b2df8..41b282ff4 100644 --- a/website/src/styles/index.css +++ b/website/src/styles/index.css @@ -50,6 +50,11 @@ src: url('/fonts/teletext/EuropeanTeletext.ttf'); size-adjust: 90%; } +@font-face { + font-family: 'tic80'; + src: url('/fonts/tic80/tic-80-wide-font.otf'); + size-adjust: 60%; +} @font-face { font-family: 'mode7'; src: url('/fonts/mode7/MODE7GX3.TTF'); diff --git a/website/src/user_pattern_utils.mjs b/website/src/user_pattern_utils.mjs index 791c6a8f9..f087c69e0 100644 --- a/website/src/user_pattern_utils.mjs +++ b/website/src/user_pattern_utils.mjs @@ -197,7 +197,7 @@ export async function importPatterns(fileList) { if (file.type === 'application/json') { const userPatterns = userPattern.getAll(); setUserPatterns({ ...userPatterns, ...parseJSON(content) }); - } else if (file.type === 'text/plain') { + } else if (['text/x-markdown', 'text/plain'].includes(file.type)) { const id = file.name.replace(/\.[^/.]+$/, ''); userPattern.update(id, { code: content }); }