mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-24 22:55:38 -04:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4603a34f7c | |||
| 30977019cc |
@@ -1,6 +1,6 @@
|
|||||||
name: Strudel tests
|
name: Strudel tests
|
||||||
|
|
||||||
on: [push, pull_request]
|
on: [push]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -19,7 +19,7 @@ jobs:
|
|||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
cache: "pnpm"
|
cache: 'pnpm'
|
||||||
- run: pnpm install
|
- run: pnpm install
|
||||||
- run: pnpm run format-check
|
- run: pnpm run format-check
|
||||||
- run: pnpm run lint
|
- run: pnpm run lint
|
||||||
|
|||||||
-21
@@ -1,21 +0,0 @@
|
|||||||
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"]
|
|
||||||
@@ -3,6 +3,8 @@
|
|||||||
Live coding patterns on the web
|
Live coding patterns on the web
|
||||||
https://strudel.cc/
|
https://strudel.cc/
|
||||||
|
|
||||||
|
Development is moving to https://codeberg.org/uzu/strudel
|
||||||
|
|
||||||
- Try it here: <https://strudel.cc>
|
- Try it here: <https://strudel.cc>
|
||||||
- Docs: <https://strudel.cc/learn>
|
- Docs: <https://strudel.cc/learn>
|
||||||
- Technical Blog Post: <https://loophole-letters.vercel.app/strudel>
|
- Technical Blog Post: <https://loophole-letters.vercel.app/strudel>
|
||||||
@@ -45,5 +47,3 @@ There is a #strudel channel on the TidalCycles discord: <https://discord.com/inv
|
|||||||
You can also ask questions and find related discussions on the tidal club forum: <https://club.tidalcycles.org/>
|
You can also ask questions and find related discussions on the tidal club forum: <https://club.tidalcycles.org/>
|
||||||
|
|
||||||
The discord and forum is shared with the haskell (tidal) and python (vortex) siblings of this project.
|
The discord and forum is shared with the haskell (tidal) and python (vortex) siblings of this project.
|
||||||
|
|
||||||
We also have a mastodon account: <a rel="me" href="https://social.toplap.org/@strudel">social.toplap.org/@strudel</a>
|
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ export default [
|
|||||||
'**/hydra.mjs',
|
'**/hydra.mjs',
|
||||||
'**/jsdoc-synonyms.js',
|
'**/jsdoc-synonyms.js',
|
||||||
'packages/hs2js/src/hs2js.mjs',
|
'packages/hs2js/src/hs2js.mjs',
|
||||||
'packages/supradough/dough-export.mjs',
|
|
||||||
'**/samples',
|
'**/samples',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -84,14 +83,4 @@ export default [
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
// Properties provided by AudioWorkletGlobalScope
|
|
||||||
files: ['packages/superdough/worklets.mjs'],
|
|
||||||
languageOptions: {
|
|
||||||
globals: {
|
|
||||||
currentTime: 'readonly',
|
|
||||||
sampleRate: 'readonly',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
jsdoc-synonyms.js - Add support for @synonyms tag
|
jsdoc-synonyms.js - Add support for @synonym tag
|
||||||
Copyright (C) 2023 Strudel contributors - see <https://codeberg.org/uzu/strudel/activity/contributors>
|
Copyright (C) 2023 Strudel contributors - see <https://codeberg.org/uzu/strudel/activity/contributors>
|
||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|||||||
+2
-1
@@ -48,6 +48,7 @@
|
|||||||
"homepage": "https://strudel.cc",
|
"homepage": "https://strudel.cc",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@strudel/core": "workspace:*",
|
"@strudel/core": "workspace:*",
|
||||||
|
"@strudel/cyclist": "workspace:*",
|
||||||
"@strudel/mini": "workspace:*",
|
"@strudel/mini": "workspace:*",
|
||||||
"@strudel/tonal": "workspace:*",
|
"@strudel/tonal": "workspace:*",
|
||||||
"@strudel/transpiler": "workspace:*",
|
"@strudel/transpiler": "workspace:*",
|
||||||
@@ -74,4 +75,4 @@
|
|||||||
"vitest": "^3.0.4",
|
"vitest": "^3.0.4",
|
||||||
"vite-plugin-bundle-audioworklet": "workspace:*"
|
"vite-plugin-bundle-audioworklet": "workspace:*"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,122 +1,68 @@
|
|||||||
import jsdoc from '../../doc.json';
|
import jsdoc from '../../doc.json';
|
||||||
|
// import { javascriptLanguage } from '@codemirror/lang-javascript';
|
||||||
import { autocompletion } from '@codemirror/autocomplete';
|
import { autocompletion } from '@codemirror/autocomplete';
|
||||||
import { h } from './html';
|
import { h } from './html';
|
||||||
|
|
||||||
const escapeHtml = (str) => {
|
function plaintext(str) {
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.innerText = str;
|
div.innerText = str;
|
||||||
return div.innerHTML;
|
return div.innerHTML;
|
||||||
};
|
}
|
||||||
|
|
||||||
const stripHtml = (html) => {
|
const getDocLabel = (doc) => doc.name || doc.longname;
|
||||||
const div = document.createElement('div');
|
const getInnerText = (html) => {
|
||||||
|
var div = document.createElement('div');
|
||||||
div.innerHTML = html;
|
div.innerHTML = html;
|
||||||
return div.textContent || div.innerText || '';
|
return div.textContent || div.innerText || '';
|
||||||
};
|
};
|
||||||
|
|
||||||
const getDocLabel = (doc) => doc.name || doc.longname;
|
export function Autocomplete({ doc, label }) {
|
||||||
|
return h`<div class="prose dark:prose-invert max-h-[400px] overflow-auto p-2">
|
||||||
|
<h1 class="pt-0 mt-0">${label || getDocLabel(doc)}</h1>
|
||||||
|
${doc.description}
|
||||||
|
<ul>
|
||||||
|
${doc.params?.map(
|
||||||
|
({ name, type, description }) =>
|
||||||
|
`<li>${name} : ${type.names?.join(' | ')} ${description ? ` - ${getInnerText(description)}` : ''}</li>`,
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
<div>
|
||||||
|
${doc.examples?.map((example) => `<div><pre>${plaintext(example)}</pre></div>`)}
|
||||||
|
</div>
|
||||||
|
</div>`[0];
|
||||||
|
/*
|
||||||
|
<pre
|
||||||
|
className="cursor-pointer"
|
||||||
|
onMouseDown={(e) => {
|
||||||
|
console.log('ola!');
|
||||||
|
navigator.clipboard.writeText(example);
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{example}
|
||||||
|
</pre>
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
const buildParamsList = (params) =>
|
const jsdocCompletions = jsdoc.docs
|
||||||
params?.length
|
.filter(
|
||||||
? `
|
(doc) =>
|
||||||
<div class="autocomplete-info-params-section">
|
getDocLabel(doc) &&
|
||||||
<h4 class="autocomplete-info-section-title">Parameters</h4>
|
!getDocLabel(doc).startsWith('_') &&
|
||||||
<ul class="autocomplete-info-params-list">
|
!['package'].includes(doc.kind) &&
|
||||||
${params
|
!['superdirtOnly', 'noAutocomplete'].some((tag) => doc.tags?.find((t) => t.originalTitle === tag)),
|
||||||
.map(
|
)
|
||||||
({ name, type, description }) => `
|
// https://codemirror.net/docs/ref/#autocomplete.Completion
|
||||||
<li class="autocomplete-info-param-item">
|
.map((doc) /*: Completion */ => ({
|
||||||
<span class="autocomplete-info-param-name">${name}</span>
|
label: getDocLabel(doc),
|
||||||
<span class="autocomplete-info-param-type">${type.names?.join(' | ')}</span>
|
// detail: 'xxx', // An optional short piece of information to show (with a different style) after the label.
|
||||||
${description ? `<div class="autocomplete-info-param-desc">${stripHtml(description)}</div>` : ''}
|
info: () => Autocomplete({ doc }),
|
||||||
</li>
|
type: 'function', // https://codemirror.net/docs/ref/#autocomplete.Completion.type
|
||||||
`,
|
}));
|
||||||
)
|
|
||||||
.join('')}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: '';
|
|
||||||
|
|
||||||
const buildExamples = (examples) =>
|
|
||||||
examples?.length
|
|
||||||
? `
|
|
||||||
<div class="autocomplete-info-examples-section">
|
|
||||||
<h4 class="autocomplete-info-section-title">Examples</h4>
|
|
||||||
${examples
|
|
||||||
.map(
|
|
||||||
(example) => `
|
|
||||||
<pre class="autocomplete-info-example-code">${escapeHtml(example)}</pre>
|
|
||||||
`,
|
|
||||||
)
|
|
||||||
.join('')}
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: '';
|
|
||||||
|
|
||||||
export const Autocomplete = (doc) =>
|
|
||||||
h`
|
|
||||||
<div class="autocomplete-info-container">
|
|
||||||
<div class="autocomplete-info-tooltip">
|
|
||||||
<h3 class="autocomplete-info-function-name">${getDocLabel(doc)}</h3>
|
|
||||||
${doc.synonyms_text ? `<div class="autocomplete-info-function-synonyms">Synonyms: ${doc.synonyms_text}</div>` : ''}
|
|
||||||
${doc.description ? `<div class="autocomplete-info-function-description">${doc.description}</div>` : ''}
|
|
||||||
${buildParamsList(doc.params)}
|
|
||||||
${buildExamples(doc.examples)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`[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 {
|
return {
|
||||||
from: word.from,
|
from: word.from,
|
||||||
options: jsdocCompletions,
|
options: jsdocCompletions,
|
||||||
@@ -128,5 +74,11 @@ export const strudelAutocomplete = (context) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isAutoCompletionEnabled = (enabled) =>
|
export function isAutoCompletionEnabled(on) {
|
||||||
enabled ? [autocompletion({ override: [strudelAutocomplete], closeOnBlur: false })] : [];
|
return on
|
||||||
|
? [
|
||||||
|
autocompletion({ override: [strudelAutocomplete] }),
|
||||||
|
//javascriptLanguage.data.of({ autocomplete: strudelAutocomplete }),
|
||||||
|
]
|
||||||
|
: []; // autocompletion({ override: [] })
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,63 +0,0 @@
|
|||||||
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]),
|
|
||||||
])();
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { closeBrackets } from '@codemirror/autocomplete';
|
import { closeBrackets } from '@codemirror/autocomplete';
|
||||||
export { toggleComment, toggleBlockComment, toggleLineComment, toggleBlockCommentByLine } from '@codemirror/commands';
|
export { toggleComment, toggleBlockComment, toggleLineComment, toggleBlockCommentByLine } from '@codemirror/commands';
|
||||||
// import { search, highlightSelectionMatches } from '@codemirror/search';
|
// import { search, highlightSelectionMatches } from '@codemirror/search';
|
||||||
import { indentWithTab } from '@codemirror/commands';
|
import { history, indentWithTab } from '@codemirror/commands';
|
||||||
import { javascript, javascriptLanguage } from '@codemirror/lang-javascript';
|
import { javascript } from '@codemirror/lang-javascript';
|
||||||
import { defaultHighlightStyle, syntaxHighlighting, bracketMatching } from '@codemirror/language';
|
import { defaultHighlightStyle, syntaxHighlighting, bracketMatching } from '@codemirror/language';
|
||||||
import { Compartment, EditorState, Prec } from '@codemirror/state';
|
import { Compartment, EditorState, Prec } from '@codemirror/state';
|
||||||
import {
|
import {
|
||||||
@@ -13,7 +13,8 @@ import {
|
|||||||
lineNumbers,
|
lineNumbers,
|
||||||
drawSelection,
|
drawSelection,
|
||||||
} from '@codemirror/view';
|
} from '@codemirror/view';
|
||||||
import { repl, registerControl } from '@strudel/core';
|
import { registerControl } from '@strudel/core';
|
||||||
|
import { repl } from '@strudel/cyclist';
|
||||||
import { Drawer, cleanupDraw } from '@strudel/draw';
|
import { Drawer, cleanupDraw } from '@strudel/draw';
|
||||||
import { isAutoCompletionEnabled } from './autocomplete.mjs';
|
import { isAutoCompletionEnabled } from './autocomplete.mjs';
|
||||||
import { isTooltipEnabled } from './tooltip.mjs';
|
import { isTooltipEnabled } from './tooltip.mjs';
|
||||||
@@ -24,7 +25,6 @@ import { initTheme, activateTheme, theme } from './themes.mjs';
|
|||||||
import { sliderPlugin, updateSliderWidgets } from './slider.mjs';
|
import { sliderPlugin, updateSliderWidgets } from './slider.mjs';
|
||||||
import { widgetPlugin, updateWidgets } from './widget.mjs';
|
import { widgetPlugin, updateWidgets } from './widget.mjs';
|
||||||
import { persistentAtom } from '@nanostores/persistent';
|
import { persistentAtom } from '@nanostores/persistent';
|
||||||
import { basicSetup } from './basicSetup.mjs';
|
|
||||||
|
|
||||||
const extensions = {
|
const extensions = {
|
||||||
isLineWrappingEnabled: (on) => (on ? EditorView.lineWrapping : []),
|
isLineWrappingEnabled: (on) => (on ? EditorView.lineWrapping : []),
|
||||||
@@ -86,17 +86,13 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo
|
|||||||
/* search(),
|
/* search(),
|
||||||
highlightSelectionMatches(), */
|
highlightSelectionMatches(), */
|
||||||
...initialSettings,
|
...initialSettings,
|
||||||
basicSetup,
|
|
||||||
mondo ? [] : javascript(),
|
mondo ? [] : javascript(),
|
||||||
javascriptLanguage.data.of({
|
|
||||||
closeBrackets: { brackets: ['(', '[', '{', "'", '"', '<'] },
|
|
||||||
bracketMatching: { brackets: ['(', '[', '{', "'", '"', '<'] },
|
|
||||||
}),
|
|
||||||
sliderPlugin,
|
sliderPlugin,
|
||||||
widgetPlugin,
|
widgetPlugin,
|
||||||
// indentOnInput(), // works without. already brought with javascript extension?
|
// indentOnInput(), // works without. already brought with javascript extension?
|
||||||
// bracketMatching(), // does not do anything
|
// bracketMatching(), // does not do anything
|
||||||
syntaxHighlighting(defaultHighlightStyle),
|
syntaxHighlighting(defaultHighlightStyle),
|
||||||
|
history(),
|
||||||
EditorView.updateListener.of((v) => onChange(v)),
|
EditorView.updateListener.of((v) => onChange(v)),
|
||||||
drawSelection({ cursorBlinkRate: 0 }),
|
drawSelection({ cursorBlinkRate: 0 }),
|
||||||
Prec.highest(
|
Prec.highest(
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
|
const parser = typeof DOMParser !== 'undefined' ? new DOMParser() : null;
|
||||||
export let html = (string) => {
|
export let html = (string) => {
|
||||||
const template = document.createElement('template');
|
return parser?.parseFromString(string, 'text/html').querySelectorAll('*');
|
||||||
template.innerHTML = string.trim();
|
|
||||||
return template.content.childNodes;
|
|
||||||
};
|
};
|
||||||
let parseChunk = (chunk) => {
|
let parseChunk = (chunk) => {
|
||||||
if (Array.isArray(chunk)) return chunk.flat().join('');
|
if (Array.isArray(chunk)) return chunk.flat().join('');
|
||||||
|
|||||||
@@ -3,9 +3,8 @@ import { keymap, ViewPlugin } from '@codemirror/view';
|
|||||||
// import { searchKeymap } from '@codemirror/search';
|
// import { searchKeymap } from '@codemirror/search';
|
||||||
import { emacs } from '@replit/codemirror-emacs';
|
import { emacs } from '@replit/codemirror-emacs';
|
||||||
import { vim } from '@replit/codemirror-vim';
|
import { vim } from '@replit/codemirror-vim';
|
||||||
// import { vim } from './vim_test.mjs';
|
|
||||||
import { vscodeKeymap } from '@replit/codemirror-vscode-keymap';
|
import { vscodeKeymap } from '@replit/codemirror-vscode-keymap';
|
||||||
import { defaultKeymap } from '@codemirror/commands';
|
import { defaultKeymap, historyKeymap } from '@codemirror/commands';
|
||||||
|
|
||||||
const vscodePlugin = ViewPlugin.fromClass(
|
const vscodePlugin = ViewPlugin.fromClass(
|
||||||
class {
|
class {
|
||||||
@@ -28,5 +27,5 @@ const keymaps = {
|
|||||||
|
|
||||||
export function keybindings(name) {
|
export function keybindings(name) {
|
||||||
const active = keymaps[name];
|
const active = keymaps[name];
|
||||||
return [active ? Prec.high(active()) : []];
|
return [active ? active() : [], keymap.of(historyKeymap)];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/codemirror",
|
"name": "@strudel/codemirror",
|
||||||
"version": "1.2.5",
|
"version": "1.2.2",
|
||||||
"description": "Codemirror Extensions for Strudel",
|
"description": "Codemirror Extensions for Strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
@@ -42,9 +42,10 @@
|
|||||||
"@lezer/highlight": "^1.2.1",
|
"@lezer/highlight": "^1.2.1",
|
||||||
"@nanostores/persistent": "^0.10.2",
|
"@nanostores/persistent": "^0.10.2",
|
||||||
"@replit/codemirror-emacs": "^6.1.0",
|
"@replit/codemirror-emacs": "^6.1.0",
|
||||||
"@replit/codemirror-vim": "^6.3.0",
|
"@replit/codemirror-vim": "^6.2.1",
|
||||||
"@replit/codemirror-vscode-keymap": "^6.0.2",
|
"@replit/codemirror-vscode-keymap": "^6.0.2",
|
||||||
"@strudel/core": "workspace:*",
|
"@strudel/core": "workspace:*",
|
||||||
|
"@strudel/cyclist": "workspace:*",
|
||||||
"@strudel/draw": "workspace:*",
|
"@strudel/draw": "workspace:*",
|
||||||
"@strudel/transpiler": "workspace:*",
|
"@strudel/transpiler": "workspace:*",
|
||||||
"nanostores": "^0.11.3"
|
"nanostores": "^0.11.3"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { hoverTooltip } from '@codemirror/view';
|
import { hoverTooltip } from '@codemirror/view';
|
||||||
import jsdoc from '../../doc.json';
|
import jsdoc from '../../doc.json';
|
||||||
import { Autocomplete, getSynonymDoc } from './autocomplete.mjs';
|
import { Autocomplete } from './autocomplete.mjs';
|
||||||
|
|
||||||
const getDocLabel = (doc) => doc.name || doc.longname;
|
const getDocLabel = (doc) => doc.name || doc.longname;
|
||||||
|
|
||||||
@@ -52,11 +52,10 @@ export const strudelTooltip = hoverTooltip(
|
|||||||
let entry = jsdoc.docs.filter((doc) => getDocLabel(doc) === word)[0];
|
let entry = jsdoc.docs.filter((doc) => getDocLabel(doc) === word)[0];
|
||||||
if (!entry) {
|
if (!entry) {
|
||||||
// Try for synonyms
|
// Try for synonyms
|
||||||
const doc = jsdoc.docs.filter((doc) => doc.synonyms && doc.synonyms.includes(word))[0];
|
entry = jsdoc.docs.filter((doc) => doc.synonyms && doc.synonyms.includes(word))[0];
|
||||||
if (!doc) {
|
if (!entry) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
entry = getSynonymDoc(doc, word);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -67,7 +66,7 @@ export const strudelTooltip = hoverTooltip(
|
|||||||
create(view) {
|
create(view) {
|
||||||
let dom = document.createElement('div');
|
let dom = document.createElement('div');
|
||||||
dom.className = 'strudel-tooltip';
|
dom.className = 'strudel-tooltip';
|
||||||
const ac = Autocomplete(entry);
|
const ac = Autocomplete({ doc: entry, label: word });
|
||||||
dom.appendChild(ac);
|
dom.appendChild(ac);
|
||||||
return { dom };
|
return { dom };
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { describe, bench } from 'vitest';
|
import { describe, bench } from 'vitest';
|
||||||
|
|
||||||
import { calculateSteps, sequence, stack } from '../index.mjs';
|
import { calculateTactus, sequence, stack } from '../index.mjs';
|
||||||
|
|
||||||
const pat64 = sequence(...Array(64).keys());
|
const pat64 = sequence(...Array(64).keys());
|
||||||
|
|
||||||
describe('steps', () => {
|
describe('steps', () => {
|
||||||
calculateSteps(true);
|
calculateTactus(true);
|
||||||
bench(
|
bench(
|
||||||
'+tactus',
|
'+tactus',
|
||||||
() => {
|
() => {
|
||||||
@@ -14,7 +14,7 @@ describe('steps', () => {
|
|||||||
{ time: 1000 },
|
{ time: 1000 },
|
||||||
);
|
);
|
||||||
|
|
||||||
calculateSteps(false);
|
calculateTactus(false);
|
||||||
bench(
|
bench(
|
||||||
'-tactus',
|
'-tactus',
|
||||||
() => {
|
() => {
|
||||||
@@ -25,7 +25,7 @@ describe('steps', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('stack', () => {
|
describe('stack', () => {
|
||||||
calculateSteps(true);
|
calculateTactus(true);
|
||||||
bench(
|
bench(
|
||||||
'+tactus',
|
'+tactus',
|
||||||
() => {
|
() => {
|
||||||
@@ -34,7 +34,7 @@ describe('stack', () => {
|
|||||||
{ time: 1000 },
|
{ time: 1000 },
|
||||||
);
|
);
|
||||||
|
|
||||||
calculateSteps(false);
|
calculateTactus(false);
|
||||||
bench(
|
bench(
|
||||||
'-tactus',
|
'-tactus',
|
||||||
() => {
|
() => {
|
||||||
@@ -43,4 +43,4 @@ describe('stack', () => {
|
|||||||
{ time: 1000 },
|
{ time: 1000 },
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
calculateSteps(true);
|
calculateTactus(true);
|
||||||
|
|||||||
+29
-284
@@ -87,54 +87,10 @@ export function registerControl(names, ...aliases) {
|
|||||||
*/
|
*/
|
||||||
export const { s, sound } = registerControl(['s', 'n', 'gain'], 'sound');
|
export const { s, sound } = registerControl(['s', 'n', 'gain'], 'sound');
|
||||||
|
|
||||||
/**
|
|
||||||
* Position in the wavetable of the wavetable oscillator
|
|
||||||
*
|
|
||||||
* @name wtPos
|
|
||||||
* @param {number | Pattern} position Position in the wavetable from 0 to 1
|
|
||||||
* @synonyms wavetablePosition
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export const { wtPos, wavetablePosition } = registerControl('wtPos', 'wavetablePosition');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Amount of warp (alteration of the waveform) to apply to the wavetable oscillator
|
|
||||||
*
|
|
||||||
* @name wtWarp
|
|
||||||
* @param {number | Pattern} amount Warp of the wavetable from 0 to 1
|
|
||||||
* @synonyms wavetableWarp
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export const { wtWarp, wavetableWarp } = registerControl('wtWarp', 'wavetableWarp');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Amount 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 wtWarpMode
|
|
||||||
* @param {number | string | Pattern} mode Warp mode
|
|
||||||
* @synonyms wavetableWarpMode
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export const { wtWarpMode, wavetableWarpMode } = registerControl('wtWarpMode', '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
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export const { wtPhaseRand, wavetablePhaseRand } = registerControl('wtPhaseRand', 'wavetablePhaseRand');
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Define a custom webaudio node to use as a sound source.
|
* Define a custom webaudio node to use as a sound source.
|
||||||
*
|
*
|
||||||
* @name source
|
* @name source
|
||||||
* @synonyms src
|
|
||||||
* @param {function} getSource
|
* @param {function} getSource
|
||||||
* @synonyms src
|
* @synonyms src
|
||||||
*
|
*
|
||||||
@@ -157,7 +113,7 @@ export const { n } = registerControl('n');
|
|||||||
*
|
*
|
||||||
* - a letter (a-g or A-G)
|
* - a letter (a-g or A-G)
|
||||||
* - optional accidentals (b or #)
|
* - optional accidentals (b or #)
|
||||||
* - optional (possibly negative) octave number (0-9). Defaults to 3
|
* - optional octave number (0-9). Defaults to 3
|
||||||
*
|
*
|
||||||
* Examples of valid note names: `c`, `bb`, `Bb`, `f#`, `c3`, `A4`, `Eb2`, `c#5`
|
* Examples of valid note names: `c`, `bb`, `Bb`, `f#`, `c3`, `A4`, `Eb2`, `c#5`
|
||||||
*
|
*
|
||||||
@@ -170,8 +126,6 @@ export const { n } = registerControl('n');
|
|||||||
* note("c4 a4 f4 e4")
|
* note("c4 a4 f4 e4")
|
||||||
* @example
|
* @example
|
||||||
* note("60 69 65 64")
|
* note("60 69 65 64")
|
||||||
* @example
|
|
||||||
* note("fbb1 a#0 cbbb-1 e##-2").sound("saw")
|
|
||||||
*/
|
*/
|
||||||
export const { note } = registerControl(['note', 'n']);
|
export const { note } = registerControl(['note', 'n']);
|
||||||
|
|
||||||
@@ -187,8 +141,8 @@ export const { note } = registerControl(['note', 'n']);
|
|||||||
*/
|
*/
|
||||||
export const { accelerate } = registerControl('accelerate');
|
export const { accelerate } = registerControl('accelerate');
|
||||||
/**
|
/**
|
||||||
* Sets the velocity from 0 to 1. Is multiplied together with gain.
|
|
||||||
*
|
*
|
||||||
|
* Sets the velocity from 0 to 1. Is multiplied together with gain.
|
||||||
* @name velocity
|
* @name velocity
|
||||||
* @example
|
* @example
|
||||||
* s("hh*8")
|
* s("hh*8")
|
||||||
@@ -298,20 +252,6 @@ export const { fmenv } = registerControl('fmenv');
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export const { fmattack } = registerControl('fmattack');
|
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("<sine square sawtooth crackle>").fm(4).fmh(2.01)
|
|
||||||
* @example
|
|
||||||
* n("0 1 2 3".fast(4)).chord("<Dm Am F G>").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.
|
* Decay time for the FM envelope: seconds until the sustain level is reached after the attack phase.
|
||||||
*
|
*
|
||||||
@@ -355,17 +295,6 @@ export const { fmvelocity } = registerControl('fmvelocity');
|
|||||||
*/
|
*/
|
||||||
export const { bank } = registerControl('bank');
|
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)
|
// analyser node send amount 0 - 1 (used by scope)
|
||||||
export const { analyze } = registerControl('analyze');
|
export const { analyze } = registerControl('analyze');
|
||||||
// fftSize of analyser
|
// fftSize of analyser
|
||||||
@@ -377,7 +306,6 @@ export const { fft } = registerControl('fft');
|
|||||||
*
|
*
|
||||||
* @name decay
|
* @name decay
|
||||||
* @param {number | Pattern} time decay time in seconds
|
* @param {number | Pattern} time decay time in seconds
|
||||||
* @synonyms dec
|
|
||||||
* @example
|
* @example
|
||||||
* note("c3 e3 f3 g3").decay("<.1 .2 .3 .4>").sustain(0)
|
* note("c3 e3 f3 g3").decay("<.1 .2 .3 .4>").sustain(0)
|
||||||
*
|
*
|
||||||
@@ -434,7 +362,7 @@ export const { bandf, bpf, bp } = registerControl(['bandf', 'bandq', 'bpenv'], '
|
|||||||
// ['bpq'],
|
// ['bpq'],
|
||||||
export const { bandq, bpq } = registerControl('bandq', '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
|
* @memberof Pattern
|
||||||
* @name begin
|
* @name begin
|
||||||
@@ -495,7 +423,7 @@ export const { loopBegin, loopb } = registerControl('loopBegin', 'loopb');
|
|||||||
*/
|
*/
|
||||||
export const { loopEnd, loope } = registerControl('loopEnd', 'loope');
|
export const { loopEnd, loope } = registerControl('loopEnd', 'loope');
|
||||||
/**
|
/**
|
||||||
* Bit crusher effect.
|
* bit crusher effect.
|
||||||
*
|
*
|
||||||
* @name crush
|
* @name crush
|
||||||
* @param {number | Pattern} depth between 1 (for drastic reduction in bit-depth) to 16 (for barely no reduction).
|
* @param {number | Pattern} depth between 1 (for drastic reduction in bit-depth) to 16 (for barely no reduction).
|
||||||
@@ -506,7 +434,7 @@ export const { loopEnd, loope } = registerControl('loopEnd', 'loope');
|
|||||||
// ['clhatdecay'],
|
// ['clhatdecay'],
|
||||||
export const { crush } = registerControl('crush');
|
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
|
* @name coarse
|
||||||
* @param {number | Pattern} factor 1 for original 2 for half, 3 for a third and so on.
|
* @param {number | Pattern} factor 1 for original 2 for half, 3 for a third and so on.
|
||||||
@@ -517,80 +445,7 @@ export const { crush } = registerControl('crush');
|
|||||||
export const { coarse } = registerControl('coarse');
|
export const { coarse } = registerControl('coarse');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Modulate the amplitude of a sound with a continuous waveform
|
* filter overdrive for supported filter types
|
||||||
*
|
|
||||||
* @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("<sine tri square>").s("sawtooth")
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export const { tremoloshape } = registerControl('tremoloshape', 'tremshape');
|
|
||||||
/**
|
|
||||||
* Filter overdrive for supported filter types
|
|
||||||
*
|
*
|
||||||
* @name drive
|
* @name drive
|
||||||
* @param {number | Pattern} amount
|
* @param {number | Pattern} amount
|
||||||
@@ -600,92 +455,6 @@ export const { tremoloshape } = registerControl('tremoloshape', 'tremshape');
|
|||||||
*/
|
*/
|
||||||
export const { drive } = registerControl('drive');
|
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
|
* Create byte beats with custom expressions
|
||||||
*
|
*
|
||||||
@@ -726,7 +495,7 @@ export const { byteBeatStartTime, bbst } = registerControl('byteBeatStartTime',
|
|||||||
export const { channels, ch } = registerControl('channels', 'ch');
|
export const { channels, ch } = registerControl('channels', 'ch');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Controls the pulsewidth of the pulse oscillator
|
* controls the pulsewidth of the pulse oscillator
|
||||||
*
|
*
|
||||||
* @name pw
|
* @name pw
|
||||||
* @param {number | Pattern} pulsewidth
|
* @param {number | Pattern} pulsewidth
|
||||||
@@ -738,7 +507,7 @@ export const { channels, ch } = registerControl('channels', 'ch');
|
|||||||
export const { pw } = registerControl(['pw', 'pwrate', 'pwsweep']);
|
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
|
* @name pwrate
|
||||||
* @param {number | Pattern} rate
|
* @param {number | Pattern} rate
|
||||||
@@ -750,7 +519,7 @@ export const { pw } = registerControl(['pw', 'pwrate', 'pwsweep']);
|
|||||||
export const { pwrate } = registerControl('pwrate');
|
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
|
* @name pwsweep
|
||||||
* @param {number | Pattern} sweep
|
* @param {number | Pattern} sweep
|
||||||
@@ -791,7 +560,7 @@ export const { phaserrate, ph, phaser } = registerControl(
|
|||||||
export const { phasersweep, phs } = registerControl('phasersweep', 'phs');
|
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
|
* @name phasercenter
|
||||||
* @synonyms phc
|
* @synonyms phc
|
||||||
@@ -808,7 +577,7 @@ export const { phasercenter, phc } = registerControl('phasercenter', 'phc');
|
|||||||
* The amount the signal is affected by the phaser effect. Defaults to 0.75
|
* The amount the signal is affected by the phaser effect. Defaults to 0.75
|
||||||
*
|
*
|
||||||
* @name phaserdepth
|
* @name phaserdepth
|
||||||
* @synonyms phd, phasdp
|
* @synonyms phd
|
||||||
* @param {number | Pattern} depth number between 0 and 1
|
* @param {number | Pattern} depth number between 0 and 1
|
||||||
* @example
|
* @example
|
||||||
* n(run(8)).scale("D:pentatonic").s("sawtooth").release(0.5)
|
* n(run(8)).scale("D:pentatonic").s("sawtooth").release(0.5)
|
||||||
@@ -819,7 +588,7 @@ export const { phasercenter, phc } = registerControl('phasercenter', 'phc');
|
|||||||
export const { phaserdepth, phd, phasdp } = registerControl('phaserdepth', 'phd', 'phasdp');
|
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
|
* @name channel
|
||||||
* @param {number | Pattern} channel channel number
|
* @param {number | Pattern} channel channel number
|
||||||
@@ -1171,7 +940,7 @@ export const { resonance, lpq } = registerControl('resonance', 'lpq');
|
|||||||
* @name djf
|
* @name djf
|
||||||
* @param {number | Pattern} cutoff below 0.5 is low pass filter, above is high pass filter
|
* @param {number | Pattern} cutoff below 0.5 is low pass filter, above is high pass filter
|
||||||
* @example
|
* @example
|
||||||
* n(irand(16).seg(8)).scale("d:phrygian").s("supersaw").djf("<.5 .3 .2 .75>")
|
* n("0 3 7 [10,24]").s('superzow').octave(3).djf("<.5 .25 .5 .75>").osc()
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export const { djf } = registerControl('djf');
|
export const { djf } = registerControl('djf');
|
||||||
@@ -1205,27 +974,14 @@ export const { delay } = registerControl(['delay', 'delaytime', 'delayfeedback']
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export const { delayfeedback, delayfb, dfb } = registerControl('delayfeedback', 'delayfb', 'dfb');
|
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.
|
* Sets the time of the delay effect.
|
||||||
*
|
*
|
||||||
* @name delayspeed
|
* @name delaytime
|
||||||
* @param {number | Pattern} delayspeed controls the pitch of the delay feedback
|
* @param {number | Pattern} seconds between 0 and Infinity
|
||||||
* @synonyms delayt, dt
|
* @synonyms delayt, dt
|
||||||
* @example
|
* @example
|
||||||
* note("d d a# a".fast(2)).s("sawtooth").delay(.8).delaytime(1/2).delayspeed("<2 .5 -1 -2>")
|
* s("bd bd").delay(.25).delaytime("<.125 .25 .5 1>")
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export const { delaytime, delayt, dt } = registerControl('delaytime', 'delayt', 'dt');
|
export const { delaytime, delayt, dt } = registerControl('delaytime', 'delayt', 'dt');
|
||||||
@@ -1303,7 +1059,6 @@ 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.
|
* Used when using `begin`/`end` or `chop`/`striate` and friends, to change the fade out time of the 'grain' envelope.
|
||||||
*
|
*
|
||||||
* @name fadeTime
|
* @name fadeTime
|
||||||
* @synonyms fadeOutTime
|
|
||||||
* @param {number | Pattern} time between 0 and 1
|
* @param {number | Pattern} time between 0 and 1
|
||||||
* @example
|
* @example
|
||||||
* s("oh*4").end(.1).fadeTime("<0 .2 .4 .8>").osc()
|
* s("oh*4").end(.1).fadeTime("<0 .2 .4 .8>").osc()
|
||||||
@@ -1638,29 +1393,6 @@ export const { roomfade, rfade } = registerControl('roomfade', 'rfade');
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export const { ir, iresponse } = registerControl(['ir', 'i'], 'iresponse');
|
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`.
|
* Sets the room size of the reverb, see `room`.
|
||||||
* When this property is changed, the reverb will be recaculated, so only change this sparsely..
|
* When this property is changed, the reverb will be recaculated, so only change this sparsely..
|
||||||
@@ -1829,6 +1561,18 @@ export const { density } = registerControl('density');
|
|||||||
// ['modwheel'],
|
// ['modwheel'],
|
||||||
export const { expression } = registerControl('expression');
|
export const { expression } = registerControl('expression');
|
||||||
export const { sustainpedal } = registerControl('sustainpedal');
|
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 { fshift } = registerControl('fshift');
|
||||||
export const { fshiftnote } = registerControl('fshiftnote');
|
export const { fshiftnote } = registerControl('fshiftnote');
|
||||||
@@ -1905,6 +1649,7 @@ export const { zmod } = registerControl('zmod');
|
|||||||
// like crush but scaled differently
|
// like crush but scaled differently
|
||||||
export const { zcrush } = registerControl('zcrush');
|
export const { zcrush } = registerControl('zcrush');
|
||||||
export const { zdelay } = registerControl('zdelay');
|
export const { zdelay } = registerControl('zdelay');
|
||||||
|
export const { tremolo } = registerControl('tremolo');
|
||||||
export const { zzfx } = registerControl('zzfx');
|
export const { zzfx } = registerControl('zzfx');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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 <https://www.gnu.org/licenses/>.
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { timeCat, register, silence, stack, pure, _morph } from './pattern.mjs';
|
import { timeCat, register, silence } from './pattern.mjs';
|
||||||
import { rotate, flatten, splitAt, zipWith } from './util.mjs';
|
import { rotate, flatten, splitAt, zipWith } from './util.mjs';
|
||||||
import Fraction, { lcm } from './fraction.mjs';
|
import Fraction, { lcm } from './fraction.mjs';
|
||||||
|
|
||||||
@@ -196,26 +196,3 @@ export const euclidLegato = register(['euclidLegato'], function (pulses, steps,
|
|||||||
export const euclidLegatoRot = register(['euclidLegatoRot'], function (pulses, steps, rotation, pat) {
|
export const euclidLegatoRot = register(['euclidLegatoRot'], function (pulses, steps, rotation, pat) {
|
||||||
return _euclidLegato(pulses, steps, rotation, pat);
|
return _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);
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -126,8 +126,6 @@ export const lcm = (...fractions) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isFraction = (x) => x instanceof Fraction;
|
|
||||||
|
|
||||||
fraction._original = Fraction;
|
fraction._original = Fraction;
|
||||||
|
|
||||||
export default fraction;
|
export default fraction;
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/
|
|||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
import Fraction from './fraction.mjs';
|
import Fraction from './fraction.mjs';
|
||||||
import { stringifyValues } from './util.mjs';
|
|
||||||
|
|
||||||
export class Hap {
|
export class Hap {
|
||||||
/*
|
/*
|
||||||
@@ -149,7 +148,13 @@ export class Hap {
|
|||||||
}
|
}
|
||||||
|
|
||||||
showWhole(compact = false) {
|
showWhole(compact = false) {
|
||||||
return `${this.whole == undefined ? '~' : this.whole.show()}: ${stringifyValues(this.value, compact)}`;
|
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
|
||||||
|
}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
combineContext(b) {
|
combineContext(b) {
|
||||||
|
|||||||
@@ -7,9 +7,8 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
import * as controls from './controls.mjs'; // legacy
|
import * as controls from './controls.mjs'; // legacy
|
||||||
export * from './euclid.mjs';
|
export * from './euclid.mjs';
|
||||||
import Fraction from './fraction.mjs';
|
import Fraction from './fraction.mjs';
|
||||||
import createClock from './zyklus.mjs';
|
|
||||||
import { logger } from './logger.mjs';
|
import { logger } from './logger.mjs';
|
||||||
export { Fraction, controls, createClock };
|
export { Fraction, controls };
|
||||||
export * from './controls.mjs';
|
export * from './controls.mjs';
|
||||||
export * from './hap.mjs';
|
export * from './hap.mjs';
|
||||||
export * from './pattern.mjs';
|
export * from './pattern.mjs';
|
||||||
@@ -18,13 +17,7 @@ export * from './pick.mjs';
|
|||||||
export * from './state.mjs';
|
export * from './state.mjs';
|
||||||
export * from './timespan.mjs';
|
export * from './timespan.mjs';
|
||||||
export * from './util.mjs';
|
export * from './util.mjs';
|
||||||
export * from './speak.mjs';
|
|
||||||
export * from './evaluate.mjs';
|
|
||||||
export * from './repl.mjs';
|
|
||||||
export * from './cyclist.mjs';
|
|
||||||
export * from './logger.mjs';
|
export * from './logger.mjs';
|
||||||
export * from './time.mjs';
|
|
||||||
export * from './ui.mjs';
|
|
||||||
export { default as drawLine } from './drawLine.mjs';
|
export { default as drawLine } from './drawLine.mjs';
|
||||||
// below won't work with runtime.mjs (json import fails)
|
// below won't work with runtime.mjs (json import fails)
|
||||||
/* import * as p from './package.json';
|
/* import * as p from './package.json';
|
||||||
|
|||||||
@@ -4,13 +4,6 @@ let debounce = 1000,
|
|||||||
lastMessage,
|
lastMessage,
|
||||||
lastTime;
|
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 = {}) {
|
export function logger(message, type, data = {}) {
|
||||||
let t = performance.now();
|
let t = performance.now();
|
||||||
if (lastMessage === message && t - lastTime < debounce) {
|
if (lastMessage === message && t - lastTime < debounce) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/core",
|
"name": "@strudel/core",
|
||||||
"version": "1.2.4",
|
"version": "1.2.2",
|
||||||
"description": "Port of Tidal Cycles to JavaScript",
|
"description": "Port of Tidal Cycles to JavaScript",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
+16
-152
@@ -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 TimeSpan from './timespan.mjs';
|
||||||
import Fraction, { isFraction, lcm } from './fraction.mjs';
|
import Fraction, { lcm } from './fraction.mjs';
|
||||||
import Hap from './hap.mjs';
|
import Hap from './hap.mjs';
|
||||||
import State from './state.mjs';
|
import State from './state.mjs';
|
||||||
import { unionWithObj } from './value.mjs';
|
import { unionWithObj } from './value.mjs';
|
||||||
@@ -21,8 +21,6 @@ import {
|
|||||||
numeralArgs,
|
numeralArgs,
|
||||||
parseNumeral,
|
parseNumeral,
|
||||||
pairs,
|
pairs,
|
||||||
zipWith,
|
|
||||||
stringifyValues,
|
|
||||||
} from './util.mjs';
|
} from './util.mjs';
|
||||||
import drawLine from './drawLine.mjs';
|
import drawLine from './drawLine.mjs';
|
||||||
import { logger } from './logger.mjs';
|
import { logger } from './logger.mjs';
|
||||||
@@ -854,29 +852,14 @@ 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) => {
|
return this.onTrigger((...args) => {
|
||||||
logger(func(...args), undefined, getData(...args));
|
logger(func(...args), undefined, getData(...args));
|
||||||
}, false);
|
}, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
logValues(func = id) {
|
||||||
* A simplified version of `log` which writes all "values" (various configurable parameters)
|
return this.log((_, hap) => func(hap.value));
|
||||||
* 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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////
|
||||||
@@ -999,7 +982,7 @@ addToPrototype('weaveWith', function (t, ...funcs) {
|
|||||||
// compose matrix functions
|
// compose matrix functions
|
||||||
|
|
||||||
function _nonArrayObject(x) {
|
function _nonArrayObject(x) {
|
||||||
return !Array.isArray(x) && typeof x === 'object' && !isFraction(x);
|
return !Array.isArray(x) && typeof x === 'object';
|
||||||
}
|
}
|
||||||
function _composeOp(a, b, func) {
|
function _composeOp(a, b, func) {
|
||||||
if (_nonArrayObject(a) || _nonArrayObject(b)) {
|
if (_nonArrayObject(a) || _nonArrayObject(b)) {
|
||||||
@@ -1246,8 +1229,7 @@ export const silence = gap(1);
|
|||||||
/* Like silence, but with a 'steps' (relative duration) of 0 */
|
/* Like silence, but with a 'steps' (relative duration) of 0 */
|
||||||
export const nothing = gap(0);
|
export const nothing = gap(0);
|
||||||
|
|
||||||
/**
|
/** A discrete value that repeats once per cycle.
|
||||||
* A discrete value that repeats once per cycle.
|
|
||||||
*
|
*
|
||||||
* @returns {Pattern}
|
* @returns {Pattern}
|
||||||
* @example
|
* @example
|
||||||
@@ -1300,14 +1282,13 @@ export function sequenceP(pats) {
|
|||||||
return result;
|
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}
|
* @return {Pattern}
|
||||||
* @synonyms polyrhythm, pr
|
* @synonyms polyrhythm, pr
|
||||||
* @example
|
* @example
|
||||||
* stack("g3", "b3", ["e4", "d4"]).note()
|
* stack("g3", "b3", ["e4", "d4"]).note()
|
||||||
* // "g3,b3,[e4 d4]".note()
|
* // "g3,b3,[e4,d4]".note()
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* // As a chained function:
|
* // As a chained function:
|
||||||
@@ -1384,11 +1365,11 @@ export function stackBy(by, ...pats) {
|
|||||||
.setSteps(steps);
|
.setSteps(steps);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Concatenation: combines a list of patterns, switching between them successively, one per cycle:
|
||||||
* Concatenation: combines a list of patterns, switching between them successively, one per cycle.
|
*
|
||||||
|
* synonyms: `cat`
|
||||||
*
|
*
|
||||||
* @return {Pattern}
|
* @return {Pattern}
|
||||||
* @synonyms cat
|
|
||||||
* @example
|
* @example
|
||||||
* slowcat("e5", "b4", ["d5", "c5"])
|
* slowcat("e5", "b4", ["d5", "c5"])
|
||||||
*
|
*
|
||||||
@@ -1588,7 +1569,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.
|
* Registers a new pattern method. The method is added to the Pattern class + the standalone function is returned from register.
|
||||||
*
|
*
|
||||||
* @param {string | string[]} name name of the function, or an array of names to be used as synonyms
|
* @param {string} name name of the function
|
||||||
* @param {function} func function with 1 or more params, where last is the current pattern
|
* @param {function} func function with 1 or more params, where last is the current pattern
|
||||||
* @noAutocomplete
|
* @noAutocomplete
|
||||||
*
|
*
|
||||||
@@ -2399,57 +2380,6 @@ export const stut = register('stut', function (times, feedback, time, pat) {
|
|||||||
return pat._echoWith(times, time, (pat, i) => pat.gain(Math.pow(feedback, i)));
|
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.
|
* 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
|
* @name iter
|
||||||
@@ -2592,7 +2522,7 @@ export const { fastchunk, fastChunk } = register(
|
|||||||
/**
|
/**
|
||||||
* Like `chunk`, but the function is applied to a looped subcycle of the source pattern.
|
* Like `chunk`, but the function is applied to a looped subcycle of the source pattern.
|
||||||
* @name chunkInto
|
* @name chunkInto
|
||||||
* @synonyms chunkinto
|
* @synonym chunkinto
|
||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @example
|
* @example
|
||||||
* sound("bd sd ht lt bd - cp lt").chunkInto(4, hurry(2))
|
* sound("bd sd ht lt bd - cp lt").chunkInto(4, hurry(2))
|
||||||
@@ -2605,7 +2535,7 @@ export const { chunkinto, chunkInto } = register(['chunkinto', 'chunkInto'], fun
|
|||||||
/**
|
/**
|
||||||
* Like `chunkInto`, but moves backwards through the chunks.
|
* Like `chunkInto`, but moves backwards through the chunks.
|
||||||
* @name chunkBackInto
|
* @name chunkBackInto
|
||||||
* @synonyms chunkbackinto
|
* @synonym chunkbackinto
|
||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @example
|
* @example
|
||||||
* sound("bd sd ht lt bd - cp lt").chunkInto(4, hurry(2))
|
* sound("bd sd ht lt bd - cp lt").chunkInto(4, hurry(2))
|
||||||
@@ -2635,7 +2565,7 @@ export const bypass = register(
|
|||||||
* Loops the pattern inside an `offset` for `cycles`.
|
* 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.
|
* 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
|
* @name ribbon
|
||||||
* @synonyms rib
|
* @synonym rib
|
||||||
* @param {number} offset start point of loop in cycles
|
* @param {number} offset start point of loop in cycles
|
||||||
* @param {number} cycles loop length in cycles
|
* @param {number} cycles loop length in cycles
|
||||||
* @example
|
* @example
|
||||||
@@ -3330,7 +3260,7 @@ export const slice = register(
|
|||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
* @example
|
* @example
|
||||||
* s("bd!8").onTriggerTime((hap) => {console.log(hap)})
|
* s("bd!8").onTriggerTime((hap) => {console.info(hap)})
|
||||||
*/
|
*/
|
||||||
Pattern.prototype.onTriggerTime = function (func) {
|
Pattern.prototype.onTriggerTime = function (func) {
|
||||||
return this.onTrigger((hap, currentTime, _cps, targetTime) => {
|
return this.onTrigger((hap, currentTime, _cps, targetTime) => {
|
||||||
@@ -3470,69 +3400,3 @@ export const { beat } = register(
|
|||||||
['beat'],
|
['beat'],
|
||||||
__beat((x) => x.innerJoin()),
|
__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))));
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -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 Fraction from 'fraction.js';
|
||||||
|
|
||||||
import { describe, it, expect, vi } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
TimeSpan,
|
TimeSpan,
|
||||||
@@ -55,8 +55,6 @@ import {
|
|||||||
expand,
|
expand,
|
||||||
} from '../index.mjs';
|
} from '../index.mjs';
|
||||||
|
|
||||||
import { log, logValues } from '../pattern.mjs';
|
|
||||||
|
|
||||||
import { steady } from '../signal.mjs';
|
import { steady } from '../signal.mjs';
|
||||||
|
|
||||||
import { n, s } from '../controls.mjs';
|
import { n, s } from '../controls.mjs';
|
||||||
@@ -1308,40 +1306,4 @@ 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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ export class TimeSpan {
|
|||||||
}
|
}
|
||||||
|
|
||||||
intersection(other) {
|
intersection(other) {
|
||||||
// Intersection of two timespans, returns undefined if they don't intersect.
|
// Intersection of two timespans, returns None if they don't intersect.
|
||||||
const intersect_begin = this.begin.max(other.begin);
|
const intersect_begin = this.begin.max(other.begin);
|
||||||
const intersect_end = this.end.min(other.end);
|
const intersect_end = this.end.min(other.end);
|
||||||
|
|
||||||
|
|||||||
+2
-12
@@ -8,12 +8,12 @@ import { logger } from './logger.mjs';
|
|||||||
|
|
||||||
// returns true if the given string is a note
|
// returns true if the given string is a note
|
||||||
export const isNoteWithOctave = (name) => /^[a-gA-G][#bs]*[0-9]$/.test(name);
|
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) => {
|
export const tokenizeNote = (note) => {
|
||||||
if (typeof note !== 'string') {
|
if (typeof note !== 'string') {
|
||||||
return [];
|
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) {
|
if (!pc) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -487,13 +487,3 @@ export function getCurrentKeyboardState() {
|
|||||||
// }
|
// }
|
||||||
// return lcm((x * y) / gcd(x, y), ...z);
|
// 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;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/csound",
|
"name": "@strudel/csound",
|
||||||
"version": "1.2.5",
|
"version": "1.2.3",
|
||||||
"description": "csound bindings for strudel",
|
"description": "csound bindings for strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -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 createClock from './zyklus.mjs';
|
||||||
import { errorLogger, logger } from './logger.mjs';
|
import { logger } from '@strudel/core';
|
||||||
|
|
||||||
export class Cyclist {
|
export class Cyclist {
|
||||||
constructor({
|
constructor({
|
||||||
@@ -76,7 +76,7 @@ export class Cyclist {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
errorLogger(e);
|
logger(`[cyclist] error: ${e.message}`);
|
||||||
onError?.(e);
|
onError?.(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
export * from './speak.mjs';
|
||||||
|
export * from './evaluate.mjs';
|
||||||
|
export * from './repl.mjs';
|
||||||
|
export * from './cyclist.mjs';
|
||||||
|
export * from './time.mjs';
|
||||||
|
export * from './ui.mjs';
|
||||||
|
import createClock from './zyklus.mjs';
|
||||||
|
export { createClock };
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
export const logKey = 'strudel.log';
|
||||||
|
|
||||||
|
let debounce = 1000,
|
||||||
|
lastMessage,
|
||||||
|
lastTime;
|
||||||
|
|
||||||
|
export function logger(message, type, data = {}) {
|
||||||
|
let t = performance.now();
|
||||||
|
if (lastMessage === message && t - lastTime < debounce) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lastMessage = message;
|
||||||
|
lastTime = t;
|
||||||
|
console.log(`%c${message}`, 'background-color: black;color:white;border-radius:15px');
|
||||||
|
if (typeof document !== 'undefined' && typeof CustomEvent !== 'undefined') {
|
||||||
|
document.dispatchEvent(
|
||||||
|
new CustomEvent(logKey, {
|
||||||
|
detail: {
|
||||||
|
message,
|
||||||
|
type,
|
||||||
|
data,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.key = logKey;
|
||||||
@@ -5,12 +5,13 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { logger } from './logger.mjs';
|
import { logger } from './logger.mjs';
|
||||||
import { ClockCollator, cycleToSeconds } from './util.mjs';
|
import { ClockCollator, cycleToSeconds } from '@strudel/core';
|
||||||
|
|
||||||
export class NeoCyclist {
|
export class NeoCyclist {
|
||||||
constructor({ onTrigger, onToggle, getTime }) {
|
constructor({ onTrigger, onToggle, getTime }) {
|
||||||
this.started = false;
|
this.started = false;
|
||||||
this.cps = 0.5;
|
this.cps = 0.5;
|
||||||
|
this.lastTick = 0; // absolute time when last tick (clock callback) happened
|
||||||
this.getTime = getTime; // get absolute time
|
this.getTime = getTime; // get absolute time
|
||||||
this.time_at_last_tick_message = 0;
|
this.time_at_last_tick_message = 0;
|
||||||
// the clock of the worker and the audio context clock can drift apart over time
|
// the clock of the worker and the audio context clock can drift apart over time
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"name": "@strudel/cyclist",
|
||||||
|
"version": "1.2.2",
|
||||||
|
"description": "Event Scheduler for Strudel",
|
||||||
|
"main": "index.mjs",
|
||||||
|
"type": "module",
|
||||||
|
"publishConfig": {
|
||||||
|
"main": "dist/index.mjs"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"test": "vitest run",
|
||||||
|
"bench": "vitest bench",
|
||||||
|
"build": "vite build",
|
||||||
|
"prepublishOnly": "pnpm build"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://codeberg.org/uzu/strudel.git"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"tidalcycles",
|
||||||
|
"strudel",
|
||||||
|
"pattern",
|
||||||
|
"livecoding",
|
||||||
|
"algorave"
|
||||||
|
],
|
||||||
|
"author": "Alex McLean <alex@slab.org> (https://slab.org)",
|
||||||
|
"license": "AGPL-3.0-or-later",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://codeberg.org/uzu/strudel/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://strudel.cc",
|
||||||
|
"dependencies": {
|
||||||
|
"@strudel/core": "*"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"vite": "^6.0.11",
|
||||||
|
"vitest": "^3.0.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import { NeoCyclist } from './neocyclist.mjs';
|
import { NeoCyclist } from './neocyclist.mjs';
|
||||||
import { Cyclist } from './cyclist.mjs';
|
import { Cyclist } from './cyclist.mjs';
|
||||||
import { evaluate as _evaluate } from './evaluate.mjs';
|
import { evaluate as _evaluate } from './evaluate.mjs';
|
||||||
import { errorLogger, logger } from './logger.mjs';
|
import { logger } from './logger.mjs';
|
||||||
import { setTime } from './time.mjs';
|
import { setTime } from './time.mjs';
|
||||||
import { evalScope } from './evaluate.mjs';
|
import { evalScope } from './evaluate.mjs';
|
||||||
import { register, Pattern, isPattern, silence, stack } from './pattern.mjs';
|
import { register, Pattern, isPattern, silence, stack } from '@strudel/core';
|
||||||
|
|
||||||
export function repl({
|
export function repl({
|
||||||
defaultOutput,
|
defaultOutput,
|
||||||
@@ -74,14 +74,6 @@ export function repl({
|
|||||||
return silence;
|
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) => {
|
const setPattern = async (pattern, autostart = true) => {
|
||||||
pattern = editPattern?.(pattern) || pattern;
|
pattern = editPattern?.(pattern) || pattern;
|
||||||
await scheduler.setPattern(pattern, autostart);
|
await scheduler.setPattern(pattern, autostart);
|
||||||
@@ -93,10 +85,7 @@ export function repl({
|
|||||||
const start = () => scheduler.start();
|
const start = () => scheduler.start();
|
||||||
const pause = () => scheduler.pause();
|
const pause = () => scheduler.pause();
|
||||||
const toggle = () => scheduler.toggle();
|
const toggle = () => scheduler.toggle();
|
||||||
const setCps = (cps) => {
|
const setCps = (cps) => scheduler.setCps(cps);
|
||||||
scheduler.setCps(unpure(cps));
|
|
||||||
return silence;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Changes the global tempo to the given cycles per minute
|
* Changes the global tempo to the given cycles per minute
|
||||||
@@ -108,10 +97,7 @@ export function repl({
|
|||||||
* setcpm(140/4) // =140 bpm in 4/4
|
* setcpm(140/4) // =140 bpm in 4/4
|
||||||
* $: s("bd*4,[- sd]*2").bank('tr707')
|
* $: s("bd*4,[- sd]*2").bank('tr707')
|
||||||
*/
|
*/
|
||||||
const setCpm = (cpm) => {
|
const setCpm = (cpm) => scheduler.setCps(cpm / 60);
|
||||||
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`..
|
// TODO - not documented as jsdoc examples as the test framework doesn't simulate enough context for `each` and `all`..
|
||||||
|
|
||||||
@@ -270,6 +256,6 @@ export const getTrigger =
|
|||||||
await hap.context.onTrigger(hap, getTime(), cps, t);
|
await hap.context.onTrigger(hap, getTime(), cps, t);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
errorLogger(err, 'getTrigger');
|
logger(`[cyclist] error: ${err.message}`, 'error');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -4,7 +4,7 @@ Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/
|
|||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { register } from './index.mjs';
|
import { register } from '@strudel/core';
|
||||||
|
|
||||||
let synth;
|
let synth;
|
||||||
try {
|
try {
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import { dependencies } from './package.json';
|
||||||
|
import { resolve } from 'path';
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
base: './',
|
||||||
|
plugins: [],
|
||||||
|
build: {
|
||||||
|
lib: {
|
||||||
|
entry: resolve(__dirname, 'index.mjs'),
|
||||||
|
formats: ['es'],
|
||||||
|
fileName: (ext) => ({ es: 'index.mjs' })[ext],
|
||||||
|
},
|
||||||
|
rollupOptions: {
|
||||||
|
external: [...Object.keys(dependencies)],
|
||||||
|
},
|
||||||
|
target: 'esnext',
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -4,7 +4,8 @@ Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/
|
|||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Pattern, getTime, State, TimeSpan } from '@strudel/core';
|
import { Pattern, State, TimeSpan } from '@strudel/core';
|
||||||
|
import { getTime } from '@strudel/cyclist';
|
||||||
|
|
||||||
export const getDrawContext = (id = 'test-canvas', options) => {
|
export const getDrawContext = (id = 'test-canvas', options) => {
|
||||||
let { contextType = '2d', pixelated = false, pixelRatio = window.devicePixelRatio } = options || {};
|
let { contextType = '2d', pixelated = false, pixelRatio = window.devicePixelRatio } = options || {};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/draw",
|
"name": "@strudel/draw",
|
||||||
"version": "1.2.4",
|
"version": "1.2.2",
|
||||||
"description": "Helpers for drawing with Strudel",
|
"description": "Helpers for drawing with Strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -29,7 +29,8 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://codeberg.org/uzu/strudel#readme",
|
"homepage": "https://codeberg.org/uzu/strudel#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@strudel/core": "workspace:*"
|
"@strudel/core": "workspace:*",
|
||||||
|
"@strudel/cyclist": "workspace:*"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vite": "^6.0.11"
|
"vite": "^6.0.11"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/embed",
|
"name": "@strudel/embed",
|
||||||
"version": "1.1.1",
|
"version": "1.1.0",
|
||||||
"description": "Embeddable Web Component to load a Strudel REPL into an iframe",
|
"description": "Embeddable Web Component to load a Strudel REPL into an iframe",
|
||||||
"main": "embed.js",
|
"main": "embed.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/gamepad",
|
"name": "@strudel/gamepad",
|
||||||
"version": "1.2.4",
|
"version": "1.2.2",
|
||||||
"description": "Gamepad Inputs for strudel",
|
"description": "Gamepad Inputs for strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { getDrawContext } from '@strudel/draw';
|
import { getDrawContext } from '@strudel/draw';
|
||||||
import { controls, getTime, reify } from '@strudel/core';
|
import { controls, reify } from '@strudel/core';
|
||||||
|
import { getTime } from '@strudel/cyclist';
|
||||||
|
|
||||||
let latestOptions;
|
let latestOptions;
|
||||||
let hydra;
|
let hydra;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/hydra",
|
"name": "@strudel/hydra",
|
||||||
"version": "1.2.4",
|
"version": "1.2.2",
|
||||||
"description": "Hydra integration for strudel",
|
"description": "Hydra integration for strudel",
|
||||||
"main": "hydra.mjs",
|
"main": "hydra.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -34,6 +34,7 @@
|
|||||||
"homepage": "https://codeberg.org/uzu/strudel#readme",
|
"homepage": "https://codeberg.org/uzu/strudel#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@strudel/core": "workspace:*",
|
"@strudel/core": "workspace:*",
|
||||||
|
"@strudel/cyclist": "workspace:*",
|
||||||
"@strudel/draw": "workspace:*",
|
"@strudel/draw": "workspace:*",
|
||||||
"hydra-synth": "^1.3.29"
|
"hydra-synth": "^1.3.29"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -493,9 +493,6 @@ export async function midin(input) {
|
|||||||
otherInputs?.length ? `Also available: ${getMidiDeviceNamesString(otherInputs)}` : ''
|
otherInputs?.length ? `Also available: ${getMidiDeviceNamesString(otherInputs)}` : ''
|
||||||
}`,
|
}`,
|
||||||
);
|
);
|
||||||
}
|
|
||||||
// ensure refs for this input are initialized
|
|
||||||
if (!refs[input]) {
|
|
||||||
refs[input] = {};
|
refs[input] = {};
|
||||||
}
|
}
|
||||||
const cc = (cc) => ref(() => refs[input][cc] || 0);
|
const cc = (cc) => ref(() => refs[input][cc] || 0);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/midi",
|
"name": "@strudel/midi",
|
||||||
"version": "1.2.5",
|
"version": "1.2.3",
|
||||||
"description": "Midi API for strudel",
|
"description": "Midi API for strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { describe, bench } from 'vitest';
|
import { describe, bench } from 'vitest';
|
||||||
|
|
||||||
import { calculateSteps } from '../../core/index.mjs';
|
import { calculateTactus } from '../../core/index.mjs';
|
||||||
import { mini } from '../index.mjs';
|
import { mini } from '../index.mjs';
|
||||||
|
|
||||||
describe('mini', () => {
|
describe('mini', () => {
|
||||||
calculateSteps(true);
|
calculateTactus(true);
|
||||||
bench(
|
bench(
|
||||||
'+tactus',
|
'+tactus',
|
||||||
() => {
|
() => {
|
||||||
@@ -13,7 +13,7 @@ describe('mini', () => {
|
|||||||
{ time: 1000 },
|
{ time: 1000 },
|
||||||
);
|
);
|
||||||
|
|
||||||
calculateSteps(false);
|
calculateTactus(false);
|
||||||
bench(
|
bench(
|
||||||
'-tactus',
|
'-tactus',
|
||||||
() => {
|
() => {
|
||||||
@@ -21,5 +21,5 @@ describe('mini', () => {
|
|||||||
},
|
},
|
||||||
{ time: 1000 },
|
{ time: 1000 },
|
||||||
);
|
);
|
||||||
calculateSteps(true);
|
calculateTactus(true);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/mini",
|
"name": "@strudel/mini",
|
||||||
"version": "1.2.4",
|
"version": "1.2.2",
|
||||||
"description": "Mini notation for strudel",
|
"description": "Mini notation for strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "mondolang",
|
"name": "mondolang",
|
||||||
"version": "1.1.1",
|
"version": "1.1.0",
|
||||||
"description": "a language for functional composition that translates to js",
|
"description": "a language for functional composition that translates to js",
|
||||||
"main": "mondo.mjs",
|
"main": "mondo.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,17 +1,5 @@
|
|||||||
import {
|
import { reify, fast, slow, seq, stepcat, extend, expand, pace, chooseIn, degradeBy, silence } from '@strudel/core';
|
||||||
strudelScope,
|
import { strudelScope } from '@strudel/cyclist';
|
||||||
reify,
|
|
||||||
fast,
|
|
||||||
slow,
|
|
||||||
seq,
|
|
||||||
stepcat,
|
|
||||||
extend,
|
|
||||||
expand,
|
|
||||||
pace,
|
|
||||||
chooseIn,
|
|
||||||
degradeBy,
|
|
||||||
silence,
|
|
||||||
} from '@strudel/core';
|
|
||||||
import { registerLanguage } from '@strudel/transpiler';
|
import { registerLanguage } from '@strudel/transpiler';
|
||||||
import { MondoRunner } from 'mondolang';
|
import { MondoRunner } from 'mondolang';
|
||||||
|
|
||||||
@@ -85,7 +73,7 @@ function evaluator(node, scope) {
|
|||||||
let pat;
|
let pat;
|
||||||
if (type === 'plain' && typeof variable !== 'undefined') {
|
if (type === 'plain' && typeof variable !== 'undefined') {
|
||||||
// some function names are not patternable, so we skip reification here
|
// some function names are not patternable, so we skip reification here
|
||||||
if (['!', 'extend', '@', 'expand', 'square', 'angle', 'all', 'setcpm', 'setcps'].includes(value)) {
|
if (['!', 'extend', '@', 'expand', 'square', 'angle'].includes(value)) {
|
||||||
return variable;
|
return variable;
|
||||||
}
|
}
|
||||||
pat = reify(variable);
|
pat = reify(variable);
|
||||||
@@ -108,7 +96,7 @@ export function mondo(code, offset = 0) {
|
|||||||
return pat.markcss('color: var(--caret,--foreground);text-decoration:underline');
|
return pat.markcss('color: var(--caret,--foreground);text-decoration:underline');
|
||||||
}
|
}
|
||||||
|
|
||||||
export let getLocations = (code, offset) => runner.parser.get_locations(code, offset);
|
let getLocations = (code, offset) => runner.parser.get_locations(code, offset);
|
||||||
|
|
||||||
export const mondi = (str, offset) => {
|
export const mondi = (str, offset) => {
|
||||||
const code = `[${str}]`;
|
const code = `[${str}]`;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/mondo",
|
"name": "@strudel/mondo",
|
||||||
"version": "1.1.4",
|
"version": "1.1.0",
|
||||||
"description": "mondo notation for strudel",
|
"description": "mondo notation for strudel",
|
||||||
"main": "mondough.mjs",
|
"main": "mondough.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -33,6 +33,7 @@
|
|||||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@strudel/core": "workspace:*",
|
"@strudel/core": "workspace:*",
|
||||||
|
"@strudel/cyclist": "workspace:*",
|
||||||
"@strudel/transpiler": "workspace:*",
|
"@strudel/transpiler": "workspace:*",
|
||||||
"mondolang": "workspace:*"
|
"mondolang": "workspace:*"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from 'vite';
|
||||||
import { dependencies } from './package.json';
|
//import { dependencies } from './package.json';
|
||||||
import { resolve } from 'path';
|
import { resolve } from 'path';
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
@@ -12,7 +12,7 @@ export default defineConfig({
|
|||||||
fileName: (ext) => ({ es: 'mondough.mjs' })[ext],
|
fileName: (ext) => ({ es: 'mondough.mjs' })[ext],
|
||||||
},
|
},
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
external: [...Object.keys(dependencies)],
|
// external: [...Object.keys(dependencies)],
|
||||||
},
|
},
|
||||||
target: 'esnext',
|
target: 'esnext',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/motion",
|
"name": "@strudel/motion",
|
||||||
"version": "1.2.4",
|
"version": "1.2.2",
|
||||||
"description": "DeviceMotion API for strudel",
|
"description": "DeviceMotion API for strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/mqtt",
|
"name": "@strudel/mqtt",
|
||||||
"version": "1.2.4",
|
"version": "1.2.2",
|
||||||
"description": "MQTT API for strudel",
|
"description": "MQTT API for strudel",
|
||||||
"main": "mqtt.mjs",
|
"main": "mqtt.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
+16
-26
@@ -4,13 +4,21 @@ OSC output for strudel patterns! Currently only tested with super collider / sup
|
|||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
Assuming you have [node.js](https://nodejs.org/) installed, you can run the osc bridge server via:
|
OSC will only work if you run the REPL locally + the OSC server besides it:
|
||||||
|
|
||||||
```sh
|
From the project root:
|
||||||
npx @strudel/osc
|
|
||||||
|
```js
|
||||||
|
npm run repl
|
||||||
```
|
```
|
||||||
|
|
||||||
You should see something like:
|
and in a seperate shell:
|
||||||
|
|
||||||
|
```js
|
||||||
|
npm run osc
|
||||||
|
```
|
||||||
|
|
||||||
|
This should give you
|
||||||
|
|
||||||
```log
|
```log
|
||||||
osc client running on port 57120
|
osc client running on port 57120
|
||||||
@@ -18,32 +26,14 @@ osc server running on port 57121
|
|||||||
websocket server running on port 8080
|
websocket server running on port 8080
|
||||||
```
|
```
|
||||||
|
|
||||||
### --port
|
Now open Supercollider (with the super dirt startup file)
|
||||||
|
|
||||||
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:
|
Now open the REPL and type:
|
||||||
|
|
||||||
```sh
|
|
||||||
npx @strudel/osc --port 7771 # classic dirt
|
|
||||||
```
|
|
||||||
|
|
||||||
### --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
|
```js
|
||||||
$: s("bd*4")
|
s("<bd sd> hh").osc()
|
||||||
|
|
||||||
all(osc)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
[open in repl](https://strudel.cc/#JDogcygiYmQqNCIpCgphbGwob3NjKQ%3D%3D)
|
or just [click here](https://strudel.cc/#cygiPGJkIHNkPiBoaCIpLm9zYygp)...
|
||||||
|
|
||||||
You can read more about [how to use Superdirt with Strudel the Tutorial](https://strudel.cc/learn/input-output/#superdirt-api)
|
You can read more about [how to use Superdirt with Strudel the Tutorial](https://strudel.cc/learn/input-output/#superdirt-api)
|
||||||
|
|||||||
@@ -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 OSC from 'osc-js';
|
||||||
|
|
||||||
import { logger, parseNumeral, register, isNote, noteToMidi, ClockCollator } from '@strudel/core';
|
import { logger, parseNumeral, Pattern, isNote, noteToMidi, ClockCollator } from '@strudel/core';
|
||||||
|
|
||||||
let connection; // Promise<OSC>
|
let connection; // Promise<OSC>
|
||||||
function connect() {
|
function connect() {
|
||||||
@@ -81,4 +81,6 @@ export async function oscTrigger(hap, currentTime, cps = 1, targetTime) {
|
|||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
*/
|
*/
|
||||||
export const osc = register('osc', (pat) => pat.onTrigger(oscTrigger));
|
Pattern.prototype.osc = function () {
|
||||||
|
return this.onTrigger(oscTrigger);
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/osc",
|
"name": "@strudel/osc",
|
||||||
"version": "1.2.10",
|
"version": "1.2.2",
|
||||||
"description": "OSC messaging for strudel",
|
"description": "OSC messaging for strudel",
|
||||||
"main": "osc.mjs",
|
"main": "osc.mjs",
|
||||||
"bin": "./server.js",
|
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"main": "dist/index.mjs"
|
"main": "dist/index.mjs"
|
||||||
|
|||||||
+2
-43
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
server.js - <short description TODO>
|
server.js - <short description TODO>
|
||||||
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/osc/server.js>
|
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/osc/server.js>
|
||||||
@@ -8,19 +6,6 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
|
|
||||||
import OSC from 'osc-js';
|
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 = {
|
const config = {
|
||||||
receiver: 'ws', // @param {string} Where messages sent via 'send' method will be delivered to, 'ws' for Websocket clients, 'udp' for udp client
|
receiver: 'ws', // @param {string} Where messages sent via 'send' method will be delivered to, 'ws' for Websocket clients, 'udp' for udp client
|
||||||
udpServer: {
|
udpServer: {
|
||||||
@@ -32,7 +17,7 @@ const config = {
|
|||||||
},
|
},
|
||||||
udpClient: {
|
udpClient: {
|
||||||
host: 'localhost', // @param {string} Hostname of udp client for messaging
|
host: 'localhost', // @param {string} Hostname of udp client for messaging
|
||||||
port: udpClientPort, // @param {number} Port of udp client for messaging
|
port: 7771, // @param {number} Port of udp client for messaging
|
||||||
},
|
},
|
||||||
wsServer: {
|
wsServer: {
|
||||||
host: 'localhost', // @param {string} Hostname of WebSocket server
|
host: 'localhost', // @param {string} Hostname of WebSocket server
|
||||||
@@ -42,34 +27,8 @@ const config = {
|
|||||||
|
|
||||||
const osc = new OSC({ plugin: new OSC.BridgePlugin(config) });
|
const osc = new OSC({ plugin: new OSC.BridgePlugin(config) });
|
||||||
|
|
||||||
if (debug) {
|
osc.open(); // start a WebSocket server on port 8080
|
||||||
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 client running on port', config.udpClient.port);
|
||||||
console.log('osc server running on port', config.udpServer.port);
|
console.log('osc server running on port', config.udpServer.port);
|
||||||
console.log('websocket server running on port', config.wsServer.port);
|
console.log('websocket server running on port', config.wsServer.port);
|
||||||
if (debug) {
|
|
||||||
console.log('debug logs enabled. incoming messages will appear below');
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
/* import { oscTriggerTauri } from '../desktopbridge/oscbridge.mjs';
|
import { oscTriggerTauri } from '../desktopbridge/oscbridge.mjs';
|
||||||
import { isTauri } from '../desktopbridge/utils.mjs'; */
|
import { isTauri } from '../desktopbridge/utils.mjs';
|
||||||
import { oscTrigger } from './osc.mjs';
|
import { oscTrigger } from './osc.mjs';
|
||||||
|
|
||||||
const trigger = /* isTauri() ? oscTriggerTauri : */ oscTrigger;
|
const trigger = isTauri() ? oscTriggerTauri : oscTrigger;
|
||||||
|
|
||||||
export const superdirtOutput = (hap, deadline, hapDuration, cps, targetTime) => {
|
export const superdirtOutput = (hap, deadline, hapDuration, cps, targetTime) => {
|
||||||
const currentTime = performance.now() / 1000;
|
const currentTime = performance.now() / 1000;
|
||||||
return trigger(hap, currentTime, cps, targetTime);
|
return trigger(null, hap, currentTime, cps, targetTime);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/reference",
|
"name": "@strudel/reference",
|
||||||
"version": "1.2.1",
|
"version": "1.2.0",
|
||||||
"description": "Headless reference of all strudel functions",
|
"description": "Headless reference of all strudel functions",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/repl",
|
"name": "@strudel/repl",
|
||||||
"version": "1.2.6",
|
"version": "1.2.3",
|
||||||
"description": "Strudel REPL as a Web Component",
|
"description": "Strudel REPL as a Web Component",
|
||||||
"module": "index.mjs",
|
"module": "index.mjs",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export async function prebake() {
|
|||||||
samples(`${ds}/tidal-drum-machines.json`),
|
samples(`${ds}/tidal-drum-machines.json`),
|
||||||
samples(`${ds}/piano.json`),
|
samples(`${ds}/piano.json`),
|
||||||
samples(`${ds}/Dirt-Samples.json`),
|
samples(`${ds}/Dirt-Samples.json`),
|
||||||
samples(`${ds}/uzu-drumkit.json`),
|
samples(`${ds}/EmuSP12.json`),
|
||||||
samples(`${ds}/vcsl.json`),
|
samples(`${ds}/vcsl.json`),
|
||||||
samples(`${ds}/mridangam.json`),
|
samples(`${ds}/mridangam.json`),
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -20,13 +20,3 @@ samples('http://localhost:5432')
|
|||||||
LOG=1 npx @strudel/sampler # adds logging
|
LOG=1 npx @strudel/sampler # adds logging
|
||||||
PORT=5555 npx @strudel/sampler # changes port
|
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.
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/sampler",
|
"name": "@strudel/sampler",
|
||||||
"version": "0.2.3",
|
"version": "0.2.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"tidalcycles",
|
"tidalcycles",
|
||||||
|
|||||||
@@ -1,20 +1,22 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
import cowsay from 'cowsay';
|
import cowsay from 'cowsay';
|
||||||
import { createReadStream, existsSync, writeFileSync } from 'fs';
|
import { createReadStream, existsSync } from 'fs';
|
||||||
import { readdir } from 'fs/promises';
|
import { readdir } from 'fs/promises';
|
||||||
import http from 'http';
|
import http from 'http';
|
||||||
import { join, resolve, sep } from 'path';
|
import { join, sep } from 'path';
|
||||||
import readline from 'readline';
|
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
|
|
||||||
|
// eslint-disable-next-line
|
||||||
const LOG = !!process.env.LOG || false;
|
const LOG = !!process.env.LOG || false;
|
||||||
const VALID_AUDIO_EXTENSIONS = ['wav', 'mp3', 'ogg'];
|
|
||||||
|
|
||||||
const isAudioFile = (f) => {
|
console.log(
|
||||||
const ext = f.split('.').slice(-1)[0].toLowerCase();
|
cowsay.say({
|
||||||
return VALID_AUDIO_EXTENSIONS.includes(ext);
|
text: 'welcome to @strudel/sampler',
|
||||||
};
|
e: 'oO',
|
||||||
|
T: 'U ',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
async function getFilesInDirectory(directory) {
|
async function getFilesInDirectory(directory) {
|
||||||
let files = [];
|
let files = [];
|
||||||
@@ -27,90 +29,42 @@ async function getFilesInDirectory(directory) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const subFiles = (await getFilesInDirectory(fullPath)).filter(isAudioFile);
|
const subFiles = (await getFilesInDirectory(fullPath)).filter((f) =>
|
||||||
|
['wav', 'mp3', 'ogg'].includes(f.split('.').slice(-1)[0].toLowerCase()),
|
||||||
|
);
|
||||||
files = files.concat(subFiles);
|
files = files.concat(subFiles);
|
||||||
LOG && console.log(`${dirent.name} (${subFiles.length})`);
|
LOG && console.log(`${dirent.name} (${subFiles.length})`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
LOG && console.warn(`skipped due to error: ${fullPath}`);
|
LOG && console.warn(`skipped due to error: ${fullPath}`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
isAudioFile(fullPath) && files.push(fullPath);
|
files.push(fullPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return files;
|
return files;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getBanks(directory, flat = false) {
|
async function getBanks(directory) {
|
||||||
let files = await getFilesInDirectory(directory);
|
let files = await getFilesInDirectory(directory);
|
||||||
let banks = {};
|
let banks = {};
|
||||||
directory = directory.split(sep).join('/');
|
directory = directory.split(sep).join('/');
|
||||||
files = files.map((path) => {
|
files = files.map((path) => {
|
||||||
path = path.split(sep).join('/');
|
path = path.split(sep).join('/');
|
||||||
const subDir = path.replace(directory, '');
|
const [bank] = path.split('/').slice(-2);
|
||||||
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] || [];
|
banks[bank] = banks[bank] || [];
|
||||||
banks[bank].push(subDir);
|
const relativeUrl = path.replace(directory, '');
|
||||||
return subDir;
|
banks[bank].push(relativeUrl);
|
||||||
|
return relativeUrl;
|
||||||
});
|
});
|
||||||
banks._base = `http://localhost:5432`;
|
banks._base = `http://localhost:5432`;
|
||||||
return { banks, files };
|
return { banks, files };
|
||||||
}
|
}
|
||||||
|
|
||||||
const args = process.argv.slice(2);
|
// eslint-disable-next-line
|
||||||
|
const directory = process.cwd();
|
||||||
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) => {
|
const server = http.createServer(async (req, res) => {
|
||||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||||
const { banks, files } = await getBanks(directory, getArgValue('--flat'));
|
const { banks, files } = await getBanks(directory);
|
||||||
if (req.url === '/') {
|
if (req.url === '/') {
|
||||||
res.setHeader('Content-Type', 'application/json');
|
res.setHeader('Content-Type', 'application/json');
|
||||||
return res.end(JSON.stringify(banks));
|
return res.end(JSON.stringify(banks));
|
||||||
@@ -118,7 +72,7 @@ const server = http.createServer(async (req, res) => {
|
|||||||
let subpath = decodeURIComponent(req.url);
|
let subpath = decodeURIComponent(req.url);
|
||||||
const filePath = join(directory, subpath.split('/').join(sep));
|
const filePath = join(directory, subpath.split('/').join(sep));
|
||||||
|
|
||||||
// console.log('GET:', filePath);
|
//console.log('GET:', filePath);
|
||||||
const isFound = existsSync(filePath);
|
const isFound = existsSync(filePath);
|
||||||
if (!isFound) {
|
if (!isFound) {
|
||||||
res.statusCode = 404;
|
res.statusCode = 404;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/serial",
|
"name": "@strudel/serial",
|
||||||
"version": "1.2.4",
|
"version": "1.2.2",
|
||||||
"description": "Webserial API for strudel",
|
"description": "Webserial API for strudel",
|
||||||
"main": "serial.mjs",
|
"main": "serial.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/soundfonts",
|
"name": "@strudel/soundfonts",
|
||||||
"version": "1.2.5",
|
"version": "1.2.3",
|
||||||
"description": "Soundsfont support for strudel",
|
"description": "Soundsfont support for strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
import { getAudioContext } from './superdough.mjs';
|
import { getAudioContext } from './superdough.mjs';
|
||||||
import { clamp, nanFallback, midiToFreq, noteToMidi } from './util.mjs';
|
import { clamp, nanFallback } from './util.mjs';
|
||||||
import { getNoiseBuffer } from './noise.mjs';
|
|
||||||
|
|
||||||
export const noises = ['pink', 'white', 'brown', 'crackle'];
|
|
||||||
|
|
||||||
export function gainNode(value) {
|
export function gainNode(value) {
|
||||||
const node = getAudioContext().createGain();
|
const node = getAudioContext().createGain();
|
||||||
@@ -10,13 +7,6 @@ export function gainNode(value) {
|
|||||||
return node;
|
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 getSlope = (y1, y2, x1, x2) => {
|
||||||
const denom = x2 - x1;
|
const denom = x2 - x1;
|
||||||
if (denom === 0) {
|
if (denom === 0) {
|
||||||
@@ -28,9 +18,7 @@ const getSlope = (y1, y2, x1, x2) => {
|
|||||||
export function getWorklet(ac, processor, params, config) {
|
export function getWorklet(ac, processor, params, config) {
|
||||||
const node = new AudioWorkletNode(ac, processor, config);
|
const node = new AudioWorkletNode(ac, processor, config);
|
||||||
Object.entries(params).forEach(([key, value]) => {
|
Object.entries(params).forEach(([key, value]) => {
|
||||||
if (value !== undefined) {
|
node.parameters.get(key).value = value;
|
||||||
node.parameters.get(key).value = value;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
@@ -183,7 +171,7 @@ let curves = ['linear', 'exponential'];
|
|||||||
export function getPitchEnvelope(param, value, t, holdEnd) {
|
export function getPitchEnvelope(param, value, t, holdEnd) {
|
||||||
// envelope is active when any of these values is set
|
// envelope is active when any of these values is set
|
||||||
const hasEnvelope = value.pattack ?? value.pdecay ?? value.psustain ?? value.prelease ?? value.penv;
|
const hasEnvelope = value.pattack ?? value.pdecay ?? value.psustain ?? value.prelease ?? value.penv;
|
||||||
if (hasEnvelope === undefined) {
|
if (!hasEnvelope) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const penv = nanFallback(value.penv, 1, true);
|
const penv = nanFallback(value.penv, 1, true);
|
||||||
@@ -218,46 +206,19 @@ export function getVibratoOscillator(param, value, t) {
|
|||||||
// ConstantSource inherits AudioScheduledSourceNode, which has scheduling abilities
|
// ConstantSource inherits AudioScheduledSourceNode, which has scheduling abilities
|
||||||
// a bit of a hack, but it works very well :)
|
// a bit of a hack, but it works very well :)
|
||||||
export function webAudioTimeout(audioContext, onComplete, startTime, stopTime) {
|
export function webAudioTimeout(audioContext, onComplete, startTime, stopTime) {
|
||||||
const constantNode = new ConstantSourceNode(audioContext);
|
const constantNode = audioContext.createConstantSource();
|
||||||
|
|
||||||
// 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.start(startTime);
|
||||||
constantNode.stop(stopTime);
|
constantNode.stop(stopTime);
|
||||||
|
constantNode.onended = () => {
|
||||||
|
onComplete();
|
||||||
|
};
|
||||||
return constantNode;
|
return constantNode;
|
||||||
}
|
}
|
||||||
const mod = (freq, range = 1, type = 'sine') => {
|
const mod = (freq, range = 1, type = 'sine') => {
|
||||||
const ctx = getAudioContext();
|
const ctx = getAudioContext();
|
||||||
let osc;
|
const osc = ctx.createOscillator();
|
||||||
if (noises.includes(type)) {
|
osc.type = type;
|
||||||
osc = ctx.createBufferSource();
|
osc.frequency.value = freq;
|
||||||
osc.buffer = getNoiseBuffer(type, 2);
|
|
||||||
osc.loop = true;
|
|
||||||
} else {
|
|
||||||
osc = ctx.createOscillator();
|
|
||||||
osc.type = type;
|
|
||||||
osc.frequency.value = freq;
|
|
||||||
}
|
|
||||||
|
|
||||||
osc.start();
|
osc.start();
|
||||||
const g = new GainNode(ctx, { gain: range });
|
const g = new GainNode(ctx, { gain: range });
|
||||||
osc.connect(g); // -range, range
|
osc.connect(g); // -range, range
|
||||||
@@ -292,7 +253,7 @@ export function applyFM(param, value, begin) {
|
|||||||
|
|
||||||
modulator = fmmod.node;
|
modulator = fmmod.node;
|
||||||
stop = fmmod.stop;
|
stop = fmmod.stop;
|
||||||
if (![fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity].some((v) => v !== undefined)) {
|
if (![fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity].find((v) => v !== undefined)) {
|
||||||
// no envelope by default
|
// no envelope by default
|
||||||
modulator.connect(param);
|
modulator.connect(param);
|
||||||
} else {
|
} else {
|
||||||
@@ -316,25 +277,3 @@ export function applyFM(param, value, begin) {
|
|||||||
}
|
}
|
||||||
return { stop };
|
return { stop };
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -11,4 +11,3 @@ export * from './synth.mjs';
|
|||||||
export * from './zzfx.mjs';
|
export * from './zzfx.mjs';
|
||||||
export * from './logger.mjs';
|
export * from './logger.mjs';
|
||||||
export * from './dspworklet.mjs';
|
export * from './dspworklet.mjs';
|
||||||
export * from './wavetable.mjs';
|
|
||||||
|
|||||||
@@ -1,12 +1,5 @@
|
|||||||
let log = (msg) => console.log(msg);
|
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 logger = (...args) => log(...args);
|
||||||
|
|
||||||
export const setLogger = (fn) => {
|
export const setLogger = (fn) => {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { getAudioContext } from './superdough.mjs';
|
|||||||
let noiseCache = {};
|
let noiseCache = {};
|
||||||
|
|
||||||
// lazy generates noise buffers and keeps them forever
|
// lazy generates noise buffers and keeps them forever
|
||||||
export function getNoiseBuffer(type, density) {
|
function getNoiseBuffer(type, density) {
|
||||||
const ac = getAudioContext();
|
const ac = getAudioContext();
|
||||||
if (noiseCache[type]) {
|
if (noiseCache[type]) {
|
||||||
return noiseCache[type];
|
return noiseCache[type];
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "superdough",
|
"name": "superdough",
|
||||||
"version": "1.2.5",
|
"version": "1.2.3",
|
||||||
"description": "simple web audio synth and sampler intended for live coding. inspired by superdirt and webdirt.",
|
"description": "simple web audio synth and sampler intended for live coding. inspired by superdirt and webdirt.",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import reverbGen from './reverbGen.mjs';
|
import reverbGen from './reverbGen.mjs';
|
||||||
import { clamp } from './util.mjs';
|
|
||||||
|
|
||||||
if (typeof AudioContext !== 'undefined') {
|
if (typeof AudioContext !== 'undefined') {
|
||||||
AudioContext.prototype.adjustLength = function (duration, buffer, speed = 1, offsetAmount = 0) {
|
AudioContext.prototype.adjustLength = function (duration, buffer) {
|
||||||
const sampleOffset = Math.floor(clamp(offsetAmount, 0, 1) * buffer.length);
|
|
||||||
const newLength = buffer.sampleRate * duration;
|
const newLength = buffer.sampleRate * duration;
|
||||||
const newBuffer = this.createBuffer(buffer.numberOfChannels, buffer.length, buffer.sampleRate);
|
const newBuffer = this.createBuffer(buffer.numberOfChannels, buffer.length, buffer.sampleRate);
|
||||||
for (let channel = 0; channel < buffer.numberOfChannels; channel++) {
|
for (let channel = 0; channel < buffer.numberOfChannels; channel++) {
|
||||||
@@ -11,30 +9,22 @@ if (typeof AudioContext !== 'undefined') {
|
|||||||
let newData = newBuffer.getChannelData(channel);
|
let newData = newBuffer.getChannelData(channel);
|
||||||
|
|
||||||
for (let i = 0; i < newLength; i++) {
|
for (let i = 0; i < newLength; i++) {
|
||||||
// loop the buffer around to prevent
|
newData[i] = oldData[i] || 0;
|
||||||
let position = (sampleOffset + i * Math.abs(speed)) % oldData.length;
|
|
||||||
if (speed < 1) {
|
|
||||||
position = position * -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
newData[i] = oldData.at(position) || 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return newBuffer;
|
return newBuffer;
|
||||||
};
|
};
|
||||||
|
|
||||||
AudioContext.prototype.createReverb = function (duration, fade, lp, dim, ir, irspeed, irbegin) {
|
AudioContext.prototype.createReverb = function (duration, fade, lp, dim, ir) {
|
||||||
const convolver = this.createConvolver();
|
const convolver = this.createConvolver();
|
||||||
convolver.generate = (d = 2, fade = 0.1, lp = 15000, dim = 1000, ir, irspeed, irbegin) => {
|
convolver.generate = (d = 2, fade = 0.1, lp = 15000, dim = 1000, ir) => {
|
||||||
convolver.duration = d;
|
convolver.duration = d;
|
||||||
convolver.fade = fade;
|
convolver.fade = fade;
|
||||||
convolver.lp = lp;
|
convolver.lp = lp;
|
||||||
convolver.dim = dim;
|
convolver.dim = dim;
|
||||||
convolver.ir = ir;
|
convolver.ir = ir;
|
||||||
convolver.irspeed = irspeed;
|
|
||||||
convolver.irbegin = irbegin;
|
|
||||||
if (ir) {
|
if (ir) {
|
||||||
convolver.buffer = this.adjustLength(d, ir, irspeed, irbegin);
|
convolver.buffer = this.adjustLength(d, ir);
|
||||||
} else {
|
} else {
|
||||||
reverbGen.generateReverb(
|
reverbGen.generateReverb(
|
||||||
{
|
{
|
||||||
@@ -51,7 +41,7 @@ if (typeof AudioContext !== 'undefined') {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
convolver.generate(duration, fade, lp, dim, ir, irspeed, irbegin);
|
convolver.generate(duration, fade, lp, dim, ir);
|
||||||
return convolver;
|
return convolver;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { noteToMidi, valueToMidi, getSoundIndex, getCommonSampleInfo } from './util.mjs';
|
import { noteToMidi, valueToMidi, getSoundIndex } from './util.mjs';
|
||||||
import { getAudioContext, registerSound, registerWaveTable } from './index.mjs';
|
import { getAudioContext, registerSound } from './index.mjs';
|
||||||
import { getADSRValues, getParamADSR, getPitchEnvelope, getVibratoOscillator } from './helpers.mjs';
|
import { getADSRValues, getParamADSR, getPitchEnvelope, getVibratoOscillator } from './helpers.mjs';
|
||||||
import { logger } from './logger.mjs';
|
import { logger } from './logger.mjs';
|
||||||
|
|
||||||
@@ -22,16 +22,39 @@ function humanFileSize(bytes, si) {
|
|||||||
return bytes.toFixed(1) + ' ' + units[u];
|
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) {
|
export function getSampleInfo(hapValue, bank) {
|
||||||
const { speed = 1.0 } = hapValue;
|
const { s, n = 0, speed = 1.0 } = hapValue;
|
||||||
const { transpose, url, index, midi, label } = getCommonSampleInfo(hapValue, bank);
|
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}`;
|
||||||
let playbackRate = Math.abs(speed) * Math.pow(2, transpose / 12);
|
let playbackRate = Math.abs(speed) * Math.pow(2, transpose / 12);
|
||||||
return { transpose, url, index, midi, label, playbackRate };
|
return { transpose, sampleUrl, index, midi, label, playbackRate };
|
||||||
}
|
}
|
||||||
|
|
||||||
// takes hapValue and returns buffer + playbackRate.
|
// takes hapValue and returns buffer + playbackRate.
|
||||||
export const getSampleBuffer = async (hapValue, bank, resolveUrl) => {
|
export const getSampleBuffer = async (hapValue, bank, resolveUrl) => {
|
||||||
let { url: sampleUrl, label, playbackRate } = getSampleInfo(hapValue, bank);
|
let { sampleUrl, label, playbackRate } = getSampleInfo(hapValue, bank);
|
||||||
if (resolveUrl) {
|
if (resolveUrl) {
|
||||||
sampleUrl = await resolveUrl(sampleUrl);
|
sampleUrl = await resolveUrl(sampleUrl);
|
||||||
}
|
}
|
||||||
@@ -56,14 +79,14 @@ export const getSampleBufferSource = async (hapValue, bank, resolveUrl) => {
|
|||||||
bufferSource.buffer = buffer;
|
bufferSource.buffer = buffer;
|
||||||
bufferSource.playbackRate.value = playbackRate;
|
bufferSource.playbackRate.value = playbackRate;
|
||||||
|
|
||||||
const { loopBegin = 0, loopEnd = 1, begin = 0, end = 1 } = hapValue;
|
const { s, 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,
|
// "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,
|
// 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."
|
// the midway point through a 10-second audio buffer is still 5."
|
||||||
const offset = begin * bufferSource.buffer.duration;
|
const offset = begin * bufferSource.buffer.duration;
|
||||||
|
|
||||||
const loop = hapValue.loop;
|
const loop = s.startsWith('wt_') ? 1 : hapValue.loop;
|
||||||
if (loop) {
|
if (loop) {
|
||||||
bufferSource.loop = true;
|
bufferSource.loop = true;
|
||||||
bufferSource.loopStart = loopBegin * bufferSource.buffer.duration - offset;
|
bufferSource.loopStart = loopBegin * bufferSource.buffer.duration - offset;
|
||||||
@@ -173,52 +196,6 @@ function getSamplesPrefixHandler(url) {
|
|||||||
return;
|
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`
|
* Loads a collection of samples to use with `s`
|
||||||
* @example
|
* @example
|
||||||
@@ -240,16 +217,61 @@ export async function fetchSampleMap(url) {
|
|||||||
|
|
||||||
export const samples = async (sampleMap, baseUrl = sampleMap._base || '', options = {}) => {
|
export const samples = async (sampleMap, baseUrl = sampleMap._base || '', options = {}) => {
|
||||||
if (typeof sampleMap === 'string') {
|
if (typeof sampleMap === 'string') {
|
||||||
const [json, base] = await fetchSampleMap(sampleMap);
|
// check if custom prefix handler
|
||||||
return samples(json, baseUrl || base, options);
|
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 { prebake, tag } = options;
|
const { prebake, tag } = options;
|
||||||
|
|
||||||
processSampleMap(
|
processSampleMap(
|
||||||
sampleMap,
|
sampleMap,
|
||||||
(key, bank) => {
|
(key, bank) =>
|
||||||
registerSampleSource(key, bank, { baseUrl, prebake, tag });
|
registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, bank), {
|
||||||
},
|
type: 'sample',
|
||||||
|
samples: bank,
|
||||||
|
baseUrl,
|
||||||
|
prebake,
|
||||||
|
tag,
|
||||||
|
}),
|
||||||
baseUrl,
|
baseUrl,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -339,20 +361,3 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
|
|||||||
|
|
||||||
return handle;
|
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
+136
-133
@@ -7,13 +7,12 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
import './feedbackdelay.mjs';
|
import './feedbackdelay.mjs';
|
||||||
import './reverb.mjs';
|
import './reverb.mjs';
|
||||||
import './vowel.mjs';
|
import './vowel.mjs';
|
||||||
import { nanFallback, _mod, cycleToSeconds } from './util.mjs';
|
import { clamp, nanFallback, _mod, cycleToSeconds } from './util.mjs';
|
||||||
import workletsUrl from './worklets.mjs?audioworklet';
|
import workletsUrl from './worklets.mjs?audioworklet';
|
||||||
import { createFilter, gainNode, getCompressor, getWorklet, effectSend } from './helpers.mjs';
|
import { createFilter, gainNode, getCompressor, getWorklet } from './helpers.mjs';
|
||||||
import { map } from 'nanostores';
|
import { map } from 'nanostores';
|
||||||
import { logger } from './logger.mjs';
|
import { logger } from './logger.mjs';
|
||||||
import { loadBuffer } from './sampler.mjs';
|
import { loadBuffer } from './sampler.mjs';
|
||||||
import { SuperdoughAudioController } from './superdoughoutput.mjs';
|
|
||||||
|
|
||||||
export const DEFAULT_MAX_POLYPHONY = 128;
|
export const DEFAULT_MAX_POLYPHONY = 128;
|
||||||
const DEFAULT_AUDIO_DEVICE_NAME = 'System Standard';
|
const DEFAULT_AUDIO_DEVICE_NAME = 'System Standard';
|
||||||
@@ -29,13 +28,6 @@ export function setMultiChannelOrbits(bool) {
|
|||||||
multiChannelOrbits = bool == true;
|
multiChannelOrbits = bool == true;
|
||||||
}
|
}
|
||||||
|
|
||||||
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 const soundMap = map();
|
export const soundMap = map();
|
||||||
|
|
||||||
export function registerSound(key, onTrigger, data = {}) {
|
export function registerSound(key, onTrigger, data = {}) {
|
||||||
@@ -114,19 +106,6 @@ 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) {
|
export function getSound(s) {
|
||||||
if (typeof s !== 'string') {
|
if (typeof s !== 'string') {
|
||||||
console.warn(`getSound: expected string got "${s}". fall back to triangle`);
|
console.warn(`getSound: expected string got "${s}". fall back to triangle`);
|
||||||
@@ -210,7 +189,7 @@ export const resetLoadedSounds = () => soundMap.set({});
|
|||||||
let audioContext;
|
let audioContext;
|
||||||
|
|
||||||
export const setDefaultAudioContext = () => {
|
export const setDefaultAudioContext = () => {
|
||||||
audioContext = new AudioContext({ latencyHint: 'playback' });
|
audioContext = new AudioContext();
|
||||||
return audioContext;
|
return audioContext;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -226,17 +205,11 @@ export function getAudioContextCurrentTime() {
|
|||||||
return getAudioContext().currentTime;
|
return getAudioContext().currentTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
let externalWorklets = [];
|
|
||||||
export function registerWorklet(url) {
|
|
||||||
externalWorklets.push(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
let workletsLoading;
|
let workletsLoading;
|
||||||
function loadWorklets() {
|
function loadWorklets() {
|
||||||
if (!workletsLoading) {
|
if (!workletsLoading) {
|
||||||
const audioCtx = getAudioContext();
|
const audioCtx = getAudioContext();
|
||||||
const allWorkletURLs = externalWorklets.concat([workletsUrl]);
|
workletsLoading = audioCtx.audioWorklet.addModule(workletsUrl);
|
||||||
workletsLoading = Promise.all(allWorkletURLs.map((workletURL) => audioCtx.audioWorklet.addModule(workletURL)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return workletsLoading;
|
return workletsLoading;
|
||||||
@@ -302,38 +275,79 @@ export async function initAudioOnFirstClick(options) {
|
|||||||
return audioReady;
|
return audioReady;
|
||||||
}
|
}
|
||||||
|
|
||||||
let controller;
|
let delays = {};
|
||||||
function getSuperdoughAudioController() {
|
const maxfeedback = 0.98;
|
||||||
if (controller == null) {
|
|
||||||
controller = new SuperdoughAudioController(getAudioContext());
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// input: AudioNode, channels: ?Array<int>
|
||||||
|
export const connectToDestination = (input, channels = [0, 1]) => {
|
||||||
|
const ctx = getAudioContext();
|
||||||
|
if (channelMerger == null) {
|
||||||
|
initializeAudioOutput();
|
||||||
}
|
}
|
||||||
return controller;
|
//This upmix can be removed if correct channel counts are set throughout the app,
|
||||||
}
|
// and then strudel could theoretically support surround sound audio files
|
||||||
export function connectToDestination(input, channels) {
|
const stereoMix = new StereoPannerNode(ctx);
|
||||||
const controller = getSuperdoughAudioController();
|
input.connect(stereoMix);
|
||||||
controller.output.connectToDestination(input, channels);
|
|
||||||
|
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, begin, end, properties = {}) {
|
export function getLfo(audioContext, time, end, properties = {}) {
|
||||||
const { shape = 0, ...props } = properties;
|
return getWorklet(audioContext, 'lfo-processor', {
|
||||||
const { dcoffset = -0.5, depth = 1 } = properties;
|
|
||||||
const lfoprops = {
|
|
||||||
frequency: 1,
|
frequency: 1,
|
||||||
depth,
|
depth: 1,
|
||||||
skew: 0.5,
|
skew: 0,
|
||||||
phaseoffset: 0,
|
phaseoffset: 0,
|
||||||
time: begin,
|
time,
|
||||||
begin,
|
|
||||||
end,
|
end,
|
||||||
shape: getModulationShapeInput(shape),
|
shape: 1,
|
||||||
dcoffset,
|
dcoffset: -0.5,
|
||||||
min: dcoffset * depth,
|
...properties,
|
||||||
max: dcoffset * depth + depth,
|
});
|
||||||
curve: 1,
|
|
||||||
...props,
|
|
||||||
};
|
|
||||||
|
|
||||||
return getWorklet(audioContext, 'lfo-processor', lfoprops);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPhaser(time, end, frequency = 1, depth = 0.5, centerFrequency = 1000, sweep = 2000) {
|
function getPhaser(time, end, frequency = 1, depth = 0.5, centerFrequency = 1000, sweep = 2000) {
|
||||||
@@ -367,6 +381,33 @@ function getFilterType(ftype) {
|
|||||||
return typeof ftype === 'number' ? filterTypes[Math.floor(_mod(ftype, filterTypes.length))] : 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 = {},
|
export let analysers = {},
|
||||||
analysersData = {};
|
analysersData = {};
|
||||||
|
|
||||||
@@ -399,8 +440,16 @@ export function getAnalyzerData(type = 'time', id = 1) {
|
|||||||
return analysersData[id];
|
return analysersData[id];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function effectSend(input, effect, wet) {
|
||||||
|
const send = gainNode(wet);
|
||||||
|
input.connect(send);
|
||||||
|
send.connect(effect);
|
||||||
|
return send;
|
||||||
|
}
|
||||||
|
|
||||||
export function resetGlobalEffects() {
|
export function resetGlobalEffects() {
|
||||||
controller?.reset();
|
delays = {};
|
||||||
|
reverbs = {};
|
||||||
analysers = {};
|
analysers = {};
|
||||||
analysersData = {};
|
analysersData = {};
|
||||||
}
|
}
|
||||||
@@ -412,11 +461,9 @@ function mapChannelNumbers(channels) {
|
|||||||
return (Array.isArray(channels) ? channels : [channels]).map((ch) => ch - 1);
|
return (Array.isArray(channels) ? channels : [channels]).map((ch) => ch - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) => {
|
export const superdough = async (value, t, hapDuration, cps = 0.5) => {
|
||||||
// new: t is always expected to be the absolute target onset time
|
// new: t is always expected to be the absolute target onset time
|
||||||
const ac = getAudioContext();
|
const ac = getAudioContext();
|
||||||
const audioController = getSuperdoughAudioController();
|
|
||||||
|
|
||||||
let { stretch } = value;
|
let { stretch } = value;
|
||||||
if (stretch != null) {
|
if (stretch != null) {
|
||||||
//account for phase vocoder latency
|
//account for phase vocoder latency
|
||||||
@@ -442,27 +489,15 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
|||||||
}
|
}
|
||||||
// destructure
|
// destructure
|
||||||
let {
|
let {
|
||||||
tremolo,
|
|
||||||
tremolosync,
|
|
||||||
tremolodepth = 1,
|
|
||||||
tremoloskew,
|
|
||||||
tremolophase = 0,
|
|
||||||
tremoloshape,
|
|
||||||
s = getDefaultValue('s'),
|
s = getDefaultValue('s'),
|
||||||
bank,
|
bank,
|
||||||
source,
|
source,
|
||||||
gain = getDefaultValue('gain'),
|
gain = getDefaultValue('gain'),
|
||||||
postgain = getDefaultValue('postgain'),
|
postgain = getDefaultValue('postgain'),
|
||||||
density = getDefaultValue('density'),
|
density = getDefaultValue('density'),
|
||||||
duckorbit,
|
|
||||||
duckonset,
|
|
||||||
duckattack,
|
|
||||||
duckdepth,
|
|
||||||
djf,
|
|
||||||
// filters
|
// filters
|
||||||
fanchor = getDefaultValue('fanchor'),
|
fanchor = getDefaultValue('fanchor'),
|
||||||
drive = 0.69,
|
drive = 0.69,
|
||||||
release = 0,
|
|
||||||
// low pass
|
// low pass
|
||||||
cutoff,
|
cutoff,
|
||||||
lpenv,
|
lpenv,
|
||||||
@@ -495,9 +530,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
|||||||
phasercenter,
|
phasercenter,
|
||||||
//
|
//
|
||||||
coarse,
|
coarse,
|
||||||
|
|
||||||
crush,
|
crush,
|
||||||
dry,
|
|
||||||
shape,
|
shape,
|
||||||
shapevol = getDefaultValue('shapevol'),
|
shapevol = getDefaultValue('shapevol'),
|
||||||
distort,
|
distort,
|
||||||
@@ -515,8 +548,6 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
|||||||
roomdim,
|
roomdim,
|
||||||
roomsize,
|
roomsize,
|
||||||
ir,
|
ir,
|
||||||
irspeed,
|
|
||||||
irbegin,
|
|
||||||
i = getDefaultValue('i'),
|
i = getDefaultValue('i'),
|
||||||
velocity = getDefaultValue('velocity'),
|
velocity = getDefaultValue('velocity'),
|
||||||
analyze, // analyser wet
|
analyze, // analyser wet
|
||||||
@@ -533,12 +564,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
|||||||
const orbitChannels = mapChannelNumbers(
|
const orbitChannels = mapChannelNumbers(
|
||||||
multiChannelOrbits && orbit > 0 ? [orbit * 2 - 1, orbit * 2] : getDefaultValue('channels'),
|
multiChannelOrbits && orbit > 0 ? [orbit * 2 - 1, orbit * 2] : getDefaultValue('channels'),
|
||||||
);
|
);
|
||||||
|
|
||||||
const channels = value.channels != null ? mapChannelNumbers(value.channels) : orbitChannels;
|
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));
|
gain = applyGainCurve(nanFallback(gain, 1));
|
||||||
postgain = applyGainCurve(postgain);
|
postgain = applyGainCurve(postgain);
|
||||||
@@ -546,11 +572,8 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
|||||||
distortvol = applyGainCurve(distortvol);
|
distortvol = applyGainCurve(distortvol);
|
||||||
delay = applyGainCurve(delay);
|
delay = applyGainCurve(delay);
|
||||||
velocity = applyGainCurve(velocity);
|
velocity = applyGainCurve(velocity);
|
||||||
tremolodepth = applyGainCurve(tremolodepth);
|
|
||||||
gain *= velocity; // velocity currently only multiplies with gain. it might do other things in the future
|
gain *= velocity; // velocity currently only multiplies with gain. it might do other things in the future
|
||||||
|
|
||||||
const end = t + hapDuration;
|
|
||||||
const endWithRelease = end + release;
|
|
||||||
const chainID = Math.round(Math.random() * 1000000);
|
const chainID = Math.round(Math.random() * 1000000);
|
||||||
|
|
||||||
// oldest audio nodes will be destroyed if maximum polyphony is exceeded
|
// oldest audio nodes will be destroyed if maximum polyphony is exceeded
|
||||||
@@ -625,7 +648,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
|||||||
lprelease,
|
lprelease,
|
||||||
lpenv,
|
lpenv,
|
||||||
t,
|
t,
|
||||||
end,
|
t + hapDuration,
|
||||||
fanchor,
|
fanchor,
|
||||||
ftype,
|
ftype,
|
||||||
drive,
|
drive,
|
||||||
@@ -649,7 +672,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
|||||||
hprelease,
|
hprelease,
|
||||||
hpenv,
|
hpenv,
|
||||||
t,
|
t,
|
||||||
end,
|
t + hapDuration,
|
||||||
fanchor,
|
fanchor,
|
||||||
);
|
);
|
||||||
chain.push(hp());
|
chain.push(hp());
|
||||||
@@ -660,7 +683,20 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
|||||||
|
|
||||||
if (bandf !== undefined) {
|
if (bandf !== undefined) {
|
||||||
let bp = () =>
|
let bp = () =>
|
||||||
createFilter(ac, 'bandpass', bandf, bandq, bpattack, bpdecay, bpsustain, bprelease, bpenv, t, end, fanchor);
|
createFilter(
|
||||||
|
ac,
|
||||||
|
'bandpass',
|
||||||
|
bandf,
|
||||||
|
bandq,
|
||||||
|
bpattack,
|
||||||
|
bpdecay,
|
||||||
|
bpsustain,
|
||||||
|
bprelease,
|
||||||
|
bpenv,
|
||||||
|
t,
|
||||||
|
t + hapDuration,
|
||||||
|
fanchor,
|
||||||
|
);
|
||||||
chain.push(bp());
|
chain.push(bp());
|
||||||
if (ftype === '24db') {
|
if (ftype === '24db') {
|
||||||
chain.push(bp());
|
chain.push(bp());
|
||||||
@@ -678,33 +714,6 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
|||||||
shape !== undefined && chain.push(getWorklet(ac, 'shape-processor', { shape, postgain: shapevol }));
|
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(getWorklet(ac, 'distort-processor', { distort, postgain: distortvol }));
|
||||||
|
|
||||||
if (tremolosync != null) {
|
|
||||||
tremolo = cps * tremolosync;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 &&
|
compressorThreshold !== undefined &&
|
||||||
chain.push(
|
chain.push(
|
||||||
getCompressor(ac, compressorThreshold, compressorRatio, compressorKnee, compressorAttack, compressorRelease),
|
getCompressor(ac, compressorThreshold, compressorRatio, compressorKnee, compressorAttack, compressorRelease),
|
||||||
@@ -718,20 +727,24 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
|||||||
}
|
}
|
||||||
// phaser
|
// phaser
|
||||||
if (phaser !== undefined && phaserdepth > 0) {
|
if (phaser !== undefined && phaserdepth > 0) {
|
||||||
const phaserFX = getPhaser(t, endWithRelease, phaser, phaserdepth, phasercenter, phasersweep);
|
const phaserFX = getPhaser(t, t + hapDuration, phaser, phaserdepth, phasercenter, phasersweep);
|
||||||
chain.push(phaserFX);
|
chain.push(phaserFX);
|
||||||
}
|
}
|
||||||
|
|
||||||
// last gain
|
// last gain
|
||||||
const post = new GainNode(ac, { gain: postgain });
|
const post = new GainNode(ac, { gain: postgain });
|
||||||
chain.push(post);
|
chain.push(post);
|
||||||
|
connectToDestination(post, channels);
|
||||||
|
|
||||||
// delay
|
// delay
|
||||||
|
let delaySend;
|
||||||
if (delay > 0 && delaytime > 0 && delayfeedback > 0) {
|
if (delay > 0 && delaytime > 0 && delayfeedback > 0) {
|
||||||
orbitBus.getDelay(delaytime, delayfeedback, t);
|
const delayNode = getDelay(orbit, delaytime, delayfeedback, t, orbitChannels);
|
||||||
orbitBus.sendDelay(post, delay);
|
delaySend = effectSend(post, delayNode, delay);
|
||||||
|
audioNodes.push(delaySend);
|
||||||
}
|
}
|
||||||
// reverb
|
// reverb
|
||||||
|
let reverbSend;
|
||||||
if (room > 0) {
|
if (room > 0) {
|
||||||
let roomIR;
|
let roomIR;
|
||||||
if (ir !== undefined) {
|
if (ir !== undefined) {
|
||||||
@@ -744,28 +757,18 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
|||||||
}
|
}
|
||||||
roomIR = await loadBuffer(url, ac, ir, 0);
|
roomIR = await loadBuffer(url, ac, ir, 0);
|
||||||
}
|
}
|
||||||
orbitBus.getReverb(roomsize, roomfade, roomlp, roomdim, roomIR, irspeed, irbegin);
|
const reverbNode = getReverb(orbit, roomsize, roomfade, roomlp, roomdim, roomIR, orbitChannels);
|
||||||
orbitBus.sendReverb(post, room);
|
reverbSend = effectSend(post, reverbNode, room);
|
||||||
}
|
audioNodes.push(reverbSend);
|
||||||
|
|
||||||
if (djf != null) {
|
|
||||||
orbitBus.getDjf(djf, t);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// analyser
|
// analyser
|
||||||
|
let analyserSend;
|
||||||
if (analyze) {
|
if (analyze) {
|
||||||
const analyserNode = getAnalyserById(analyze, 2 ** (fft + 5));
|
const analyserNode = getAnalyserById(analyze, 2 ** (fft + 5));
|
||||||
const analyserSend = effectSend(post, analyserNode, 1);
|
analyserSend = effectSend(post, analyserNode, 1);
|
||||||
audioNodes.push(analyserSend);
|
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
|
// connect chain elements together
|
||||||
chain.slice(1).reduce((last, current) => last.connect(current), chain[0]);
|
chain.slice(1).reduce((last, current) => last.connect(current), chain[0]);
|
||||||
|
|||||||
@@ -1,206 +0,0 @@
|
|||||||
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.channelMerger.disconnect();
|
|
||||||
this.destinationGain.disconnect();
|
|
||||||
this.destinationGain = null;
|
|
||||||
this.channelMerger = null;
|
|
||||||
this.nodes = {};
|
|
||||||
this.initializeAudio();
|
|
||||||
}
|
|
||||||
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.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];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +1,38 @@
|
|||||||
import { clamp } from './util.mjs';
|
import { clamp, midiToFreq, noteToMidi } from './util.mjs';
|
||||||
import { registerSound, getAudioContext, soundMap, getLfo } from './superdough.mjs';
|
import { registerSound, getAudioContext, soundMap, getLfo } from './superdough.mjs';
|
||||||
import {
|
import {
|
||||||
applyFM,
|
applyFM,
|
||||||
destroyAudioWorkletNode,
|
|
||||||
gainNode,
|
gainNode,
|
||||||
getADSRValues,
|
getADSRValues,
|
||||||
getFrequencyFromValue,
|
|
||||||
getParamADSR,
|
getParamADSR,
|
||||||
getPitchEnvelope,
|
getPitchEnvelope,
|
||||||
getVibratoOscillator,
|
getVibratoOscillator,
|
||||||
getWorklet,
|
|
||||||
noises,
|
|
||||||
webAudioTimeout,
|
webAudioTimeout,
|
||||||
|
getWorklet,
|
||||||
} from './helpers.mjs';
|
} from './helpers.mjs';
|
||||||
import { getNoiseMix, getNoiseOscillator } from './noise.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 waveforms = ['triangle', 'square', 'sawtooth', 'sine'];
|
||||||
const waveformAliases = [
|
const waveformAliases = [
|
||||||
['tri', 'triangle'],
|
['tri', 'triangle'],
|
||||||
@@ -22,17 +40,7 @@ const waveformAliases = [
|
|||||||
['saw', 'sawtooth'],
|
['saw', 'sawtooth'],
|
||||||
['sin', 'sine'],
|
['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() {
|
export function registerSynthSounds() {
|
||||||
[...waveforms].forEach((s) => {
|
[...waveforms].forEach((s) => {
|
||||||
@@ -76,75 +84,6 @@ export function registerSynthSounds() {
|
|||||||
{ type: 'synth', prebake: true },
|
{ 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(
|
registerSound(
|
||||||
'supersaw',
|
'supersaw',
|
||||||
(begin, value, onended) => {
|
(begin, value, onended) => {
|
||||||
@@ -182,7 +121,10 @@ export function registerSynthSounds() {
|
|||||||
const gainAdjustment = 1 / Math.sqrt(voices);
|
const gainAdjustment = 1 / Math.sqrt(voices);
|
||||||
getPitchEnvelope(o.parameters.get('detune'), value, begin, holdend);
|
getPitchEnvelope(o.parameters.get('detune'), value, begin, holdend);
|
||||||
const vibratoOscillator = getVibratoOscillator(o.parameters.get('detune'), value, begin);
|
const vibratoOscillator = getVibratoOscillator(o.parameters.get('detune'), value, begin);
|
||||||
const fm = applyFM(o.parameters.get('frequency'), 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
|
||||||
let envGain = gainNode(1);
|
let envGain = gainNode(1);
|
||||||
envGain = o.connect(envGain);
|
envGain = o.connect(envGain);
|
||||||
|
|
||||||
@@ -194,7 +136,7 @@ export function registerSynthSounds() {
|
|||||||
destroyAudioWorkletNode(o);
|
destroyAudioWorkletNode(o);
|
||||||
envGain.disconnect();
|
envGain.disconnect();
|
||||||
onended();
|
onended();
|
||||||
fm?.stop();
|
// fm?.stop();
|
||||||
vibratoOscillator?.stop();
|
vibratoOscillator?.stop();
|
||||||
},
|
},
|
||||||
begin,
|
begin,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export const tokenizeNote = (note) => {
|
|||||||
if (typeof note !== 'string') {
|
if (typeof note !== 'string') {
|
||||||
return [];
|
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) {
|
if (!pc) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -72,35 +72,3 @@ export const getSoundIndex = (n, numSounds) => {
|
|||||||
export function cycleToSeconds(cycle, cps) {
|
export function cycleToSeconds(cycle, cps) {
|
||||||
return 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 };
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,236 +0,0 @@
|
|||||||
import { getAudioContext, registerSound } from './index.mjs';
|
|
||||||
import { getCommonSampleInfo, getSoundIndex, valueToMidi } from './util.mjs';
|
|
||||||
import {
|
|
||||||
destroyAudioWorkletNode,
|
|
||||||
getADSRValues,
|
|
||||||
getFrequencyFromValue,
|
|
||||||
getParamADSR,
|
|
||||||
getPitchEnvelope,
|
|
||||||
getVibratoOscillator,
|
|
||||||
getWorklet,
|
|
||||||
webAudioTimeout,
|
|
||||||
} from './helpers.mjs';
|
|
||||||
import { logger } from './logger.mjs';
|
|
||||||
|
|
||||||
const WT_MAX_MIP_LEVELS = 6;
|
|
||||||
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,
|
|
||||||
});
|
|
||||||
|
|
||||||
async function loadWavetableFrames(url, label, frameLen = 2048) {
|
|
||||||
const ac = getAudioContext();
|
|
||||||
const buf = await loadBuffer(url, ac, 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// build mipmaps
|
|
||||||
const mipmaps = [frames];
|
|
||||||
let levelFrames = frames;
|
|
||||||
for (let level = 1; level < WT_MAX_MIP_LEVELS; level++) {
|
|
||||||
const prevLen = levelFrames[0].length;
|
|
||||||
if (prevLen <= 32) break;
|
|
||||||
const nextLen = prevLen >> 1;
|
|
||||||
const next = levelFrames.map((src) => {
|
|
||||||
const out = new Float32Array(nextLen);
|
|
||||||
for (let j = 0; j < nextLen; j++) {
|
|
||||||
out[j] = (src[2 * j] + src[2 * j + 1]) / 2;
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
});
|
|
||||||
mipmaps.push(next);
|
|
||||||
levelFrames = next;
|
|
||||||
}
|
|
||||||
return { frames, mipmaps, frameLen, numFrames };
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadCache = {};
|
|
||||||
|
|
||||||
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];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getTableInfo(hapValue, urls) {
|
|
||||||
return getCommonSampleInfo(hapValue, urls);
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadBuffer = (url, ac, 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 ac.decodeAudioData(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) => {
|
|
||||||
return Object.entries(json).forEach(([key, value]) => {
|
|
||||||
if (typeof value === 'string') {
|
|
||||||
value = [value];
|
|
||||||
}
|
|
||||||
if (typeof value !== 'object') {
|
|
||||||
throw new Error('wrong json format for ' + key);
|
|
||||||
}
|
|
||||||
baseUrl = value._base || baseUrl;
|
|
||||||
if (baseUrl.startsWith('github:')) {
|
|
||||||
baseUrl = githubPath(baseUrl, '');
|
|
||||||
}
|
|
||||||
value = value.map((v) => baseUrl + v);
|
|
||||||
registerWaveTable(key, value, { baseUrl, frameLen });
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export function registerWaveTable(key, bank, params) {
|
|
||||||
registerSound(key, (t, hapValue, onended) => onTriggerSynth(t, hapValue, onended, bank, params?.frameLen ?? 2048), {
|
|
||||||
type: 'wavetable',
|
|
||||||
tables: bank,
|
|
||||||
...params,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads a collection of wavetables to use with `s`
|
|
||||||
*
|
|
||||||
* @name tables
|
|
||||||
*/
|
|
||||||
export const tables = async (url, frameLen, json) => {
|
|
||||||
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))
|
|
||||||
.catch((error) => {
|
|
||||||
console.error(error);
|
|
||||||
throw new Error(`error loading "${url}"`);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function onTriggerSynth(t, value, onended, bank, frameLen) {
|
|
||||||
const { s, n = 0, duration } = value;
|
|
||||||
const ac = getAudioContext();
|
|
||||||
const [attack, decay, sustain, release] = getADSRValues([value.attack, value.decay, value.sustain, value.release]);
|
|
||||||
let { wtWarpMode } = value;
|
|
||||||
if (typeof wtWarpMode === 'string') {
|
|
||||||
wtWarpMode = WarpMode[wtWarpMode.toUpperCase()] ?? WarpMode.NONE;
|
|
||||||
}
|
|
||||||
const frequency = getFrequencyFromValue(value);
|
|
||||||
const { url, label } = getTableInfo(value, bank);
|
|
||||||
const payload = await loadWavetableFrames(url, label, frameLen);
|
|
||||||
const holdEnd = t + duration;
|
|
||||||
const envEnd = holdEnd + release + 0.01;
|
|
||||||
const source = getWorklet(
|
|
||||||
ac,
|
|
||||||
'wavetable-oscillator-processor',
|
|
||||||
{
|
|
||||||
begin: t,
|
|
||||||
end: envEnd,
|
|
||||||
frequency,
|
|
||||||
detune: value.detune,
|
|
||||||
position: value.wtPos,
|
|
||||||
warp: value.wtWarp,
|
|
||||||
warpMode: wtWarpMode,
|
|
||||||
voices: value.unison,
|
|
||||||
spread: value.spread,
|
|
||||||
phaserand: value.wtPhaseRand,
|
|
||||||
},
|
|
||||||
{ outputChannelCount: [2] },
|
|
||||||
);
|
|
||||||
source.port.postMessage({ type: 'tables', payload });
|
|
||||||
if (ac.currentTime > t) {
|
|
||||||
logger(`[wavetable] still loading sound "${s}:${n}"`, 'highlight');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const vibratoOscillator = getVibratoOscillator(source.detune, value, t);
|
|
||||||
const envGain = ac.createGain();
|
|
||||||
const node = source.connect(envGain);
|
|
||||||
getParamADSR(node.gain, attack, decay, sustain, release, 0, 1, t, holdEnd, 'linear');
|
|
||||||
getPitchEnvelope(source.detune, value, t, holdEnd);
|
|
||||||
const handle = { node, source };
|
|
||||||
const timeoutNode = webAudioTimeout(
|
|
||||||
ac,
|
|
||||||
() => {
|
|
||||||
source.disconnect();
|
|
||||||
destroyAudioWorkletNode(source);
|
|
||||||
vibratoOscillator?.stop();
|
|
||||||
node.disconnect();
|
|
||||||
onended();
|
|
||||||
},
|
|
||||||
t,
|
|
||||||
envEnd,
|
|
||||||
);
|
|
||||||
handle.stop = (time) => {
|
|
||||||
timeoutNode.stop(time);
|
|
||||||
};
|
|
||||||
return handle;
|
|
||||||
}
|
|
||||||
@@ -6,44 +6,20 @@ import OLAProcessor from './ola-processor';
|
|||||||
import FFT from './fft.js';
|
import FFT from './fft.js';
|
||||||
|
|
||||||
const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
|
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;
|
const blockSize = 128;
|
||||||
// Smooth waveshape near discontinuities to remove frequencies above Nyquist and prevent aliasing
|
// adjust waveshape to remove frequencies above nyquist to prevent aliasing
|
||||||
// referenced from https://www.kvraudio.com/forum/viewtopic.php?t=375517
|
// referenced from https://www.kvraudio.com/forum/viewtopic.php?t=375517
|
||||||
function polyBlep(phase, dt) {
|
function polyBlep(phase, dt) {
|
||||||
dt = Math.min(dt, 1 - dt);
|
// 0 <= phase < 1
|
||||||
// Start of cycle
|
|
||||||
if (phase < dt) {
|
if (phase < dt) {
|
||||||
phase /= dt;
|
phase /= dt;
|
||||||
// 2 * (phase - phase^2/2 - 0.5)
|
// 2 * (phase - phase^2/2 - 0.5)
|
||||||
return phase + phase - phase * phase - 1;
|
return phase + phase - phase * phase - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// End of cycle
|
// -1 < phase < 0
|
||||||
else if (phase > 1 - dt) {
|
else if (phase > 1 - dt) {
|
||||||
phase = (phase - 1) / dt;
|
phase = (phase - 1) / dt;
|
||||||
// 2 * (phase^2/2 + phase + 0.5)
|
// 2 * (phase^2/2 + phase + 0.5)
|
||||||
@@ -55,7 +31,7 @@ function polyBlep(phase, dt) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// The order is important for dough integration
|
|
||||||
const waveshapes = {
|
const waveshapes = {
|
||||||
tri(phase, skew = 0.5) {
|
tri(phase, skew = 0.5) {
|
||||||
const x = 1 - skew;
|
const x = 1 - skew;
|
||||||
@@ -105,12 +81,10 @@ function getParamValue(block, param) {
|
|||||||
}
|
}
|
||||||
return param[0];
|
return param[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
const waveShapeNames = Object.keys(waveshapes);
|
const waveShapeNames = Object.keys(waveshapes);
|
||||||
class LFOProcessor extends AudioWorkletProcessor {
|
class LFOProcessor extends AudioWorkletProcessor {
|
||||||
static get parameterDescriptors() {
|
static get parameterDescriptors() {
|
||||||
return [
|
return [
|
||||||
{ name: 'begin', defaultValue: 0 },
|
|
||||||
{ name: 'time', defaultValue: 0 },
|
{ name: 'time', defaultValue: 0 },
|
||||||
{ name: 'end', defaultValue: 0 },
|
{ name: 'end', defaultValue: 0 },
|
||||||
{ name: 'frequency', defaultValue: 0.5 },
|
{ name: 'frequency', defaultValue: 0.5 },
|
||||||
@@ -118,10 +92,7 @@ class LFOProcessor extends AudioWorkletProcessor {
|
|||||||
{ name: 'depth', defaultValue: 1 },
|
{ name: 'depth', defaultValue: 1 },
|
||||||
{ name: 'phaseoffset', defaultValue: 0 },
|
{ name: 'phaseoffset', defaultValue: 0 },
|
||||||
{ name: 'shape', defaultValue: 0 },
|
{ name: 'shape', defaultValue: 0 },
|
||||||
{ name: 'curve', defaultValue: 1 },
|
|
||||||
{ name: 'dcoffset', defaultValue: 0 },
|
{ name: 'dcoffset', defaultValue: 0 },
|
||||||
{ name: 'min', defaultValue: 0 },
|
|
||||||
{ name: 'max', defaultValue: 1 },
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,13 +109,10 @@ class LFOProcessor extends AudioWorkletProcessor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
process(inputs, outputs, parameters) {
|
process(inputs, outputs, parameters) {
|
||||||
const begin = parameters['begin'][0];
|
// eslint-disable-next-line no-undef
|
||||||
if (currentTime >= parameters.end[0]) {
|
if (currentTime >= parameters.end[0]) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (currentTime <= begin) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const output = outputs[0];
|
const output = outputs[0];
|
||||||
const frequency = parameters['frequency'][0];
|
const frequency = parameters['frequency'][0];
|
||||||
@@ -154,24 +122,20 @@ class LFOProcessor extends AudioWorkletProcessor {
|
|||||||
const skew = parameters['skew'][0];
|
const skew = parameters['skew'][0];
|
||||||
const phaseoffset = parameters['phaseoffset'][0];
|
const phaseoffset = parameters['phaseoffset'][0];
|
||||||
|
|
||||||
const curve = parameters['curve'][0];
|
|
||||||
|
|
||||||
const dcoffset = parameters['dcoffset'][0];
|
const dcoffset = parameters['dcoffset'][0];
|
||||||
const min = parameters['min'][0];
|
|
||||||
const max = parameters['max'][0];
|
|
||||||
const shape = waveShapeNames[parameters['shape'][0]];
|
const shape = waveShapeNames[parameters['shape'][0]];
|
||||||
|
|
||||||
const blockSize = output[0].length ?? 0;
|
const blockSize = output[0].length ?? 0;
|
||||||
|
|
||||||
if (this.phase == null) {
|
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;
|
const dt = frequency / sampleRate;
|
||||||
for (let n = 0; n < blockSize; n++) {
|
for (let n = 0; n < blockSize; n++) {
|
||||||
for (let i = 0; i < output.length; i++) {
|
for (let i = 0; i < output.length; i++) {
|
||||||
let modval = (waveshapes[shape](this.phase, skew) + dcoffset) * depth;
|
const modval = (waveshapes[shape](this.phase, skew) + dcoffset) * depth;
|
||||||
modval = Math.pow(modval, curve);
|
output[i][n] = modval;
|
||||||
output[i][n] = clamp(modval, min, max);
|
|
||||||
}
|
}
|
||||||
this.incrementPhase(dt);
|
this.incrementPhase(dt);
|
||||||
}
|
}
|
||||||
@@ -285,73 +249,6 @@ class ShapeProcessor extends AudioWorkletProcessor {
|
|||||||
}
|
}
|
||||||
registerProcessor('shape-processor', ShapeProcessor);
|
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) {
|
function fast_tanh(x) {
|
||||||
const x2 = x * x;
|
const x2 = x * x;
|
||||||
return (x * (27.0 + x2)) / (27.0 + 9.0 * x2);
|
return (x * (27.0 + x2)) / (27.0 + 9.0 * x2);
|
||||||
@@ -394,6 +291,7 @@ class LadderProcessor extends AudioWorkletProcessor {
|
|||||||
const drive = clamp(Math.exp(parameters.drive[0]), 0.1, 2000);
|
const drive = clamp(Math.exp(parameters.drive[0]), 0.1, 2000);
|
||||||
|
|
||||||
let cutoff = parameters.frequency[0];
|
let cutoff = parameters.frequency[0];
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
cutoff = (cutoff * 2 * _PI) / sampleRate;
|
cutoff = (cutoff * 2 * _PI) / sampleRate;
|
||||||
cutoff = cutoff > 1 ? 1 : cutoff;
|
cutoff = cutoff > 1 ? 1 : cutoff;
|
||||||
|
|
||||||
@@ -459,6 +357,21 @@ class DistortProcessor extends AudioWorkletProcessor {
|
|||||||
registerProcessor('distort-processor', DistortProcessor);
|
registerProcessor('distort-processor', DistortProcessor);
|
||||||
|
|
||||||
// SUPERSAW
|
// 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 {
|
class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
@@ -511,47 +424,53 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
process(input, outputs, params) {
|
process(input, outputs, params) {
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
if (currentTime <= params.begin[0]) {
|
if (currentTime <= params.begin[0]) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
if (currentTime >= params.end[0]) {
|
if (currentTime >= params.end[0]) {
|
||||||
// this.port.postMessage({ type: 'onended' });
|
// this.port.postMessage({ type: 'onended' });
|
||||||
return false;
|
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 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 i = 0; i < output[0].length; i++) {
|
for (let n = 0; n < voices; n++) {
|
||||||
const detune = pv(params.detune, i);
|
const isOdd = (n & 1) == 1;
|
||||||
const voices = pv(params.voices, i);
|
|
||||||
const freqspread = pv(params.freqspread, i);
|
//applies unison "spread" detune in semitones
|
||||||
const panspread = pv(params.panspread, i) * 0.5 + 0.5;
|
const freq = applySemitoneDetuneToFrequency(frequency, getUnisonDetune(voices, freqspread, n));
|
||||||
const gain1 = Math.sqrt(1 - panspread);
|
let gainL = gain1;
|
||||||
const gain2 = Math.sqrt(panspread);
|
let gainR = gain2;
|
||||||
let freq = pv(params.frequency, i);
|
// invert right and left gain
|
||||||
// Main detuning
|
if (isOdd) {
|
||||||
freq = applySemitoneDetuneToFrequency(freq, detune / 100);
|
gainL = gain2;
|
||||||
for (let n = 0; n < voices; n++) {
|
gainR = gain1;
|
||||||
const isOdd = (n & 1) == 1;
|
}
|
||||||
let gainL = gain1;
|
// eslint-disable-next-line no-undef
|
||||||
let gainR = gain2;
|
const dt = freq / sampleRate;
|
||||||
// invert right and left gain
|
|
||||||
if (isOdd) {
|
for (let i = 0; i < output[0].length; i++) {
|
||||||
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();
|
this.phase[n] = this.phase[n] ?? Math.random();
|
||||||
const v = waveshapes.sawblep(this.phase[n], dt);
|
const v = waveshapes.sawblep(this.phase[n], dt);
|
||||||
|
|
||||||
output[0][i] = output[0][i] + v * gainL;
|
output[0][i] = output[0][i] + v * gainL;
|
||||||
output[1][i] = output[1][i] + v * gainR;
|
output[1][i] = output[1][i] + v * gainR;
|
||||||
|
|
||||||
this.phase[n] = wrapPhase(this.phase[n] + dt);
|
this.phase[n] += dt;
|
||||||
|
|
||||||
|
if (this.phase[n] > 1.0) {
|
||||||
|
this.phase[n] = this.phase[n] - 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -560,7 +479,7 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
|
|||||||
|
|
||||||
registerProcessor('supersaw-oscillator', SuperSawOscillatorProcessor);
|
registerProcessor('supersaw-oscillator', SuperSawOscillatorProcessor);
|
||||||
|
|
||||||
// Phase Vocoder sourced from https://github.com/olvb/phaze/tree/master?tab=readme-ov-file
|
// Phase Vocoder sourced from // sourced from https://github.com/olvb/phaze/tree/master?tab=readme-ov-file
|
||||||
const BUFFERED_BLOCK_SIZE = 2048;
|
const BUFFERED_BLOCK_SIZE = 2048;
|
||||||
|
|
||||||
function genHannWindow(length) {
|
function genHannWindow(length) {
|
||||||
@@ -975,323 +894,3 @@ class ByteBeatProcessor extends AudioWorkletProcessor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
registerProcessor('byte-beat-processor', ByteBeatProcessor);
|
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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: 220, minValue: 0.01, maxValue: 20000 },
|
|
||||||
{ name: 'detune', defaultValue: 0.18 },
|
|
||||||
{ name: 'position', defaultValue: 0, minValue: 0, maxValue: 1 },
|
|
||||||
{ name: 'warp', defaultValue: 0, minValue: 0, maxValue: 1 },
|
|
||||||
{ name: 'warpMode', defaultValue: 0 },
|
|
||||||
{ name: 'voices', defaultValue: 1, minValue: 1, maxValue: 32 },
|
|
||||||
{ name: 'spread', defaultValue: 0, minValue: 0, maxValue: 1 },
|
|
||||||
{ name: 'phaserand', defaultValue: 1, minValue: 0, maxValue: 1 },
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(options) {
|
|
||||||
super(options);
|
|
||||||
this.tables = null;
|
|
||||||
this.frameLen = 0;
|
|
||||||
this.numFrames = 0;
|
|
||||||
this.phase = [];
|
|
||||||
this.syncRatio = 1;
|
|
||||||
|
|
||||||
this.port.onmessage = (e) => {
|
|
||||||
const { type, payload } = e.data || {};
|
|
||||||
if (type === 'tables') {
|
|
||||||
this.tables = payload.mipmaps;
|
|
||||||
this.frameLen = payload.frameLen;
|
|
||||||
this.numFrames = this.tables[0].length;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
_chooseMip(dphi) {
|
|
||||||
const approxHarm = Math.min(64, 1 / Math.max(1e-6, dphi));
|
|
||||||
let level = 0;
|
|
||||||
while (level + 1 < (this.tables?.length || 1) && approxHarm < this.tables[level][0].length / 8) {
|
|
||||||
level++;
|
|
||||||
}
|
|
||||||
return level;
|
|
||||||
}
|
|
||||||
|
|
||||||
_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 pos = phase * (frame.length - 1);
|
|
||||||
const i = pos | 0;
|
|
||||||
const frac = pos - i;
|
|
||||||
const a = frame[i];
|
|
||||||
const b = frame[(i + 1) % frame.length];
|
|
||||||
return a + (b - a) * frac;
|
|
||||||
}
|
|
||||||
|
|
||||||
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];
|
|
||||||
const gainAdjustment = 0.3;
|
|
||||||
|
|
||||||
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 spread = pv(parameters.spread, i) * 0.5 + 0.5;
|
|
||||||
const tablePos = pv(parameters.position, i);
|
|
||||||
const idx = tablePos * (this.numFrames - 1);
|
|
||||||
const fIdx = idx | 0;
|
|
||||||
const frac = idx - fIdx;
|
|
||||||
const warpAmount = pv(parameters.warp, i);
|
|
||||||
const warpMode = pv(parameters.warpMode, i);
|
|
||||||
const voices = pv(parameters.voices, i);
|
|
||||||
const phaseRand = pv(parameters.phaserand, i);
|
|
||||||
const gain1 = Math.sqrt(1 - spread);
|
|
||||||
const gain2 = Math.sqrt(spread);
|
|
||||||
let f = pv(parameters.frequency, i);
|
|
||||||
f = applySemitoneDetuneToFrequency(f, detune / 100); // overall detune
|
|
||||||
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, detune, n)); // voice detune
|
|
||||||
const dPhase = fVoice / sampleRate;
|
|
||||||
const level = this._chooseMip(dPhase);
|
|
||||||
const bank = 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(bank[fIdx], ph);
|
|
||||||
const s1 = this._sampleFrame(bank[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) / Math.sqrt(voices)) * gainAdjustment;
|
|
||||||
outR[i] += ((s * gainR) / Math.sqrt(voices)) * gainAdjustment;
|
|
||||||
this.phase[n] = wrapPhase(this.phase[n] + dPhase);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
registerProcessor('wavetable-oscillator-processor', WavetableOscillatorProcessor);
|
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
pattern.wav
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
# supradough
|
|
||||||
|
|
||||||
platform agnostic synth and sampler intended for live coding. a reimplementation of superdough.
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
// 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,<bb c4 d4 eb4>')
|
|
||||||
.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('<pink white>*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('<c2 - [- f1] ->*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('<Cm Cm7 Cm9 Cm11 Fm Fm7 Fm9 Fm11>')
|
|
||||||
.voicing()
|
|
||||||
.s('<sine>')
|
|
||||||
.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));
|
|
||||||
});
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
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);
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +0,0 @@
|
|||||||
// 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;
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
{
|
|
||||||
"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 <flix91@gmail.com>",
|
|
||||||
"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": {}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/tonal",
|
"name": "@strudel/tonal",
|
||||||
"version": "1.2.4",
|
"version": "1.2.2",
|
||||||
"description": "Tonal functions for strudel",
|
"description": "Tonal functions for strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
|
|||||||
@@ -7,130 +7,80 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
// import { strict as assert } from 'assert';
|
// import { strict as assert } from 'assert';
|
||||||
|
|
||||||
import '../tonal.mjs'; // need to import this to add prototypes
|
import '../tonal.mjs'; // need to import this to add prototypes
|
||||||
import { pure, n, seq, note, noteToMidi } from '@strudel/core';
|
import { pure, n, seq, note } from '@strudel/core';
|
||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { mini } from '../../mini/mini.mjs';
|
import { mini } from '../../mini/mini.mjs';
|
||||||
|
|
||||||
describe('tonal', () => {
|
describe('tonal', () => {
|
||||||
describe('scaleTranspose', () => {
|
it('Should run tonal functions ', () => {
|
||||||
it('transposes notes by scale degrees', () => {
|
expect(pure('c3').scale('C major').scaleTranspose(1).firstCycleValues).toEqual(['D3']);
|
||||||
expect(pure('c3').scale('C major').scaleTranspose(1).firstCycleValues).toEqual(['D3']);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
describe('scale', () => {
|
it('scale with plain values', () => {
|
||||||
it('converts plain values', () => {
|
expect(
|
||||||
expect(
|
seq(0, 1, 2)
|
||||||
seq(0, 1, 2)
|
.scale('C major')
|
||||||
.scale('C major')
|
.note()
|
||||||
.note()
|
.firstCycleValues.map((h) => h.note),
|
||||||
.firstCycleValues.map((h) => h.note),
|
).toEqual(['C3', 'D3', 'E3']);
|
||||||
).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);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
describe('transpose', () => {
|
it('scale with n values', () => {
|
||||||
it('transposes note numbers with interval numbers', () => {
|
expect(
|
||||||
expect(
|
n(seq(0, 1, 2))
|
||||||
note(seq(40, 40, 40))
|
.scale('C major')
|
||||||
.transpose(0, 1, 2)
|
.firstCycleValues.map((h) => h.note),
|
||||||
.firstCycleValues.map((h) => h.note),
|
).toEqual(['C3', 'D3', 'E3']);
|
||||||
).toEqual([40, 41, 42]);
|
});
|
||||||
expect(seq(40, 40, 40).transpose(0, 1, 2).firstCycleValues).toEqual([40, 41, 42]);
|
it('scale with colon', () => {
|
||||||
});
|
expect(
|
||||||
it('transposes note numbers with interval strings', () => {
|
n(seq(0, 1, 2))
|
||||||
expect(
|
.scale('C:major')
|
||||||
note(seq(40, 40, 40))
|
.firstCycleValues.map((h) => h.note),
|
||||||
.transpose('1P', '2M', '3m')
|
).toEqual(['C3', 'D3', 'E3']);
|
||||||
.firstCycleValues.map((h) => h.note),
|
});
|
||||||
).toEqual([40, 42, 43]);
|
it('scale without tonic', () => {
|
||||||
expect(seq(40, 40, 40).transpose('1P', '2M', '3m').firstCycleValues).toEqual([40, 42, 43]);
|
expect(
|
||||||
});
|
n(seq(0, 1, 2))
|
||||||
it('transposes note strings with interval numbers', () => {
|
.scale('major')
|
||||||
expect(
|
.firstCycleValues.map((h) => h.note),
|
||||||
note(seq('c', 'c', 'c'))
|
).toEqual(['C3', 'D3', 'E3']);
|
||||||
.transpose(0, 1, 2)
|
});
|
||||||
.firstCycleValues.map((h) => h.note),
|
it('scale with mininotation colon', () => {
|
||||||
).toEqual(['C', 'Db', 'D']);
|
expect(
|
||||||
expect(seq('c', 'c', 'c').transpose(0, 1, 2).firstCycleValues).toEqual(['C', 'Db', 'D']);
|
n(seq(0, 1, 2))
|
||||||
});
|
.scale(mini('C:major'))
|
||||||
it('transposes note strings with interval strings', () => {
|
.firstCycleValues.map((h) => h.note),
|
||||||
expect(
|
).toEqual(['C3', 'D3', 'E3']);
|
||||||
note(seq('c', 'c', 'c'))
|
});
|
||||||
.transpose('1P', '2M', '3m')
|
it('transposes note numbers with interval numbers', () => {
|
||||||
.firstCycleValues.map((h) => h.note),
|
expect(
|
||||||
).toEqual(['C', 'D', 'Eb']);
|
note(seq(40, 40, 40))
|
||||||
expect(seq('c', 'c', 'c').transpose('1P', '2M', '3m').firstCycleValues).toEqual(['C', 'D', 'Eb']);
|
.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']);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+69
-122
@@ -6,28 +6,19 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
|
|
||||||
import { Note, Interval, Scale } from '@tonaljs/tonal';
|
import { Note, Interval, Scale } from '@tonaljs/tonal';
|
||||||
import { register, _mod, silence, logger, pure, isNote } from '@strudel/core';
|
import { register, _mod, silence, logger, pure, isNote } from '@strudel/core';
|
||||||
import { stepInNamedScale, nearestNumberIndex } from './tonleiter.mjs';
|
import { stepInNamedScale } from './tonleiter.mjs';
|
||||||
import { noteToMidi } from '../core/util.mjs';
|
|
||||||
|
|
||||||
const octavesInterval = (octaves) => (octaves <= 0 ? -1 : 1) + octaves * 7 + 'P';
|
const octavesInterval = (octaves) => (octaves <= 0 ? -1 : 1) + octaves * 7 + 'P';
|
||||||
|
|
||||||
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 name "${scaleName}"`);
|
|
||||||
}
|
|
||||||
return scale;
|
|
||||||
}
|
|
||||||
|
|
||||||
function scaleStep(step, scale) {
|
function scaleStep(step, scale) {
|
||||||
|
scale = scale.replaceAll(':', ' ');
|
||||||
step = Math.ceil(step);
|
step = Math.ceil(step);
|
||||||
let { intervals, tonic } = getScale(scale);
|
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")`);
|
||||||
|
} else if (empty) {
|
||||||
|
throw new Error(`invalid scale "${scale}"`);
|
||||||
|
}
|
||||||
tonic = tonic || 'C';
|
tonic = tonic || 'C';
|
||||||
const { pc, oct = 3 } = Note.get(tonic);
|
const { pc, oct = 3 } = Note.get(tonic);
|
||||||
const octaveOffset = Math.floor(step / intervals.length);
|
const octaveOffset = Math.floor(step / intervals.length);
|
||||||
@@ -39,7 +30,8 @@ function scaleStep(step, scale) {
|
|||||||
// transpose note inside scale by offset steps
|
// transpose note inside scale by offset steps
|
||||||
// function scaleOffset(scale: string, offset: number, note: string) {
|
// function scaleOffset(scale: string, offset: number, note: string) {
|
||||||
function scaleOffset(scale, offset, note) {
|
function scaleOffset(scale, offset, note) {
|
||||||
let { notes } = getScale(scale);
|
let [tonic, scaleName] = Scale.tokenize(scale);
|
||||||
|
let { notes } = Scale.get(`${tonic} ${scaleName}`);
|
||||||
notes = notes.map((note) => Note.get(note).pc); // use only pc!
|
notes = notes.map((note) => Note.get(note).pc); // use only pc!
|
||||||
offset = Number(offset);
|
offset = Number(offset);
|
||||||
if (isNaN(offset)) {
|
if (isNaN(offset)) {
|
||||||
@@ -96,14 +88,13 @@ function scaleOffset(scale, offset, note) {
|
|||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @name transpose
|
* @name transpose
|
||||||
* @synonyms trans
|
|
||||||
* @example
|
* @example
|
||||||
* "c2 c3".fast(2).transpose("<0 -2 5 3>".slow(2)).note()
|
* "c2 c3".fast(2).transpose("<0 -2 5 3>".slow(2)).note()
|
||||||
* @example
|
* @example
|
||||||
* "c2 c3".fast(2).transpose("<1P -2M 4P 3m>".slow(2)).note()
|
* "c2 c3".fast(2).transpose("<1P -2M 4P 3m>".slow(2)).note()
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const { transpose, trans } = register(['transpose', 'trans'], function transposeFn(intervalOrSemitones, pat) {
|
export const transpose = register('transpose', function (intervalOrSemitones, pat) {
|
||||||
return pat.withHap((hap) => {
|
return pat.withHap((hap) => {
|
||||||
const note = hap.value.note ?? hap.value;
|
const note = hap.value.note ?? hap.value;
|
||||||
if (typeof note === 'number') {
|
if (typeof note === 'number') {
|
||||||
@@ -128,7 +119,10 @@ export const { transpose, trans } = register(['transpose', 'trans'], function tr
|
|||||||
const interval = !isNaN(Number(intervalOrSemitones))
|
const interval = !isNaN(Number(intervalOrSemitones))
|
||||||
? Interval.fromSemitones(intervalOrSemitones)
|
? Interval.fromSemitones(intervalOrSemitones)
|
||||||
: String(intervalOrSemitones);
|
: String(intervalOrSemitones);
|
||||||
const targetNote = Note.transpose(note, interval);
|
// 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));
|
||||||
if (typeof hap.value === 'object') {
|
if (typeof hap.value === 'object') {
|
||||||
return hap.withValue(() => ({ ...hap.value, note: targetNote }));
|
return hap.withValue(() => ({ ...hap.value, note: targetNote }));
|
||||||
}
|
}
|
||||||
@@ -148,7 +142,6 @@ export const { transpose, trans } = register(['transpose', 'trans'], function tr
|
|||||||
* @name scaleTranspose
|
* @name scaleTranspose
|
||||||
* @param {offset} offset number of steps inside the scale
|
* @param {offset} offset number of steps inside the scale
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
* @synonyms scaleTrans, strans
|
|
||||||
* @example
|
* @example
|
||||||
* "-8 [2,4,6]"
|
* "-8 [2,4,6]"
|
||||||
* .scale('C4 bebop major')
|
* .scale('C4 bebop major')
|
||||||
@@ -156,79 +149,25 @@ export const { transpose, trans } = register(['transpose', 'trans'], function tr
|
|||||||
* .note()
|
* .note()
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const { scaleTranspose, scaleTrans, strans } = register(
|
export const scaleTranspose = register('scaleTranspose', function (offset /* : number | string */, pat) {
|
||||||
['scaleTranspose', 'scaleTrans', 'strans'],
|
return pat.withHap((hap) => {
|
||||||
function (offset /* : number | string */, pat) {
|
if (!hap.context.scale) {
|
||||||
return pat.withHap((hap) => {
|
throw new Error('can only use scaleTranspose after .scale');
|
||||||
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`);
|
|
||||||
}
|
}
|
||||||
asNumber = Number(match[1]);
|
if (typeof hap.value === 'object')
|
||||||
// These decorations will determine the semitone offset based on the number of
|
return hap.withValue(() => ({
|
||||||
// sharps or flats
|
...hap.value,
|
||||||
const decorations = match[2] || '';
|
note: scaleOffset(hap.context.scale, Number(offset), hap.value.note),
|
||||||
offset = decorations[0] === '#' ? decorations.length : -decorations.length;
|
}));
|
||||||
}
|
if (typeof hap.value !== 'string') {
|
||||||
return [asNumber, offset];
|
throw new Error('can only use scaleTranspose with notes');
|
||||||
}
|
}
|
||||||
|
return hap.withValue(() => scaleOffset(hap.context.scale, Number(offset), hap.value));
|
||||||
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) or quantizes notes to a scale.
|
* Turns numbers into notes in the scale (zero indexed). Also sets scale for other scale operations, like {@link Pattern#scaleTranspose}.
|
||||||
*
|
|
||||||
* 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).
|
* 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).
|
||||||
*
|
*
|
||||||
@@ -247,12 +186,6 @@ function _getNearestScaleNote(scaleName, note, preferHigher = true) {
|
|||||||
* n(rand.range(0,12).segment(8))
|
* n(rand.range(0,12).segment(8))
|
||||||
* .scale("C:ritusen")
|
* .scale("C:ritusen")
|
||||||
* .s("piano")
|
* .s("piano")
|
||||||
* @example
|
|
||||||
* n("<[0,7b] [-4# -4] [-2,7##] 4 [0,7] [-4# -4b] [-2,7###] 4b>*4")
|
|
||||||
* .scale("C:<major minor>/2")
|
|
||||||
* .s("piano")
|
|
||||||
* @example
|
|
||||||
* note("C1*16").transpose(irand(36)).scale('Cb2 major').scaleTranspose(3)
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const scale = register(
|
export const scale = register(
|
||||||
@@ -266,35 +199,49 @@ export const scale = register(
|
|||||||
pat
|
pat
|
||||||
.fmap((value) => {
|
.fmap((value) => {
|
||||||
const isObject = typeof value === 'object';
|
const isObject = typeof value === 'object';
|
||||||
// The case where the note has been defined via `n` or `pure`
|
let step = isObject ? value.n : value;
|
||||||
if (!isObject || (isObject && ('n' in value || 'value' in value))) {
|
if (isObject) {
|
||||||
const step = isObject ? (value.n ?? value.value) : value;
|
|
||||||
delete value.n; // remove n so it won't cause trouble
|
delete value.n; // remove n so it won't cause trouble
|
||||||
if (isNote(step)) {
|
}
|
||||||
// legacy..
|
if (isNote(step)) {
|
||||||
return pure(step);
|
// legacy..
|
||||||
}
|
return pure(step);
|
||||||
try {
|
}
|
||||||
const [number, offset] = _convertStepToNumberAndOffset(step);
|
let asNumber = Number(step);
|
||||||
let note;
|
let semitones = 0;
|
||||||
if (isObject && value.anchor) {
|
if (isNaN(asNumber)) {
|
||||||
note = stepInNamedScale(number, scale, value.anchor);
|
step = String(step);
|
||||||
} else {
|
if (!/^[-+]?\d+(#*|b*){1}$/.test(step)) {
|
||||||
note = scaleStep(number, scale);
|
logger(
|
||||||
}
|
`[tonal] invalid scale step "${step}", expected number or integer with optional # b suffixes`,
|
||||||
if (offset != 0) note = Note.transpose(note, Interval.fromSemitones(offset));
|
'error',
|
||||||
value = pure(isObject ? { ...value, note } : note);
|
);
|
||||||
} catch (err) {
|
|
||||||
logger(`[tonal] ${err.message}`, 'error');
|
|
||||||
return silence;
|
return silence;
|
||||||
}
|
}
|
||||||
return value;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// The case where the note has been defined via `note`
|
try {
|
||||||
else {
|
let note;
|
||||||
const note = _getNearestScaleNote(scale, value.note);
|
if (isObject && value.anchor) {
|
||||||
return pure(isObject ? { ...value, note } : note);
|
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;
|
||||||
}
|
}
|
||||||
|
return value;
|
||||||
})
|
})
|
||||||
.outerJoin()
|
.outerJoin()
|
||||||
// legacy:
|
// legacy:
|
||||||
|
|||||||
@@ -101,11 +101,11 @@ export function nearestNumberIndex(target, numbers, preferHigher) {
|
|||||||
let scaleSteps = {}; // [scaleName]: semitones[]
|
let scaleSteps = {}; // [scaleName]: semitones[]
|
||||||
|
|
||||||
export function stepInNamedScale(step, scale, anchor, preferHigher) {
|
export function stepInNamedScale(step, scale, anchor, preferHigher) {
|
||||||
const [root, scaleName] = Scale.tokenize(scale);
|
let [root, scaleName] = Scale.tokenize(scale);
|
||||||
const rootMidi = x2midi(root);
|
const rootMidi = x2midi(root);
|
||||||
const rootChroma = midi2chroma(rootMidi);
|
const rootChroma = midi2chroma(rootMidi);
|
||||||
if (!scaleSteps[scaleName]) {
|
if (!scaleSteps[scaleName]) {
|
||||||
const { intervals } = Scale.get(`C ${scaleName}`);
|
let { intervals } = Scale.get(`C ${scaleName}`);
|
||||||
// cache result
|
// cache result
|
||||||
scaleSteps[scaleName] = intervals.map(step2semitones);
|
scaleSteps[scaleName] = intervals.map(step2semitones);
|
||||||
}
|
}
|
||||||
@@ -222,7 +222,6 @@ export const Note = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// TODO: support octave numbers
|
// TODO: support octave numbers
|
||||||
// Example: Note("Bb3").transpose("c3")
|
|
||||||
export function transpose(note, step) {
|
export function transpose(note, step) {
|
||||||
// example: E, 3
|
// example: E, 3
|
||||||
const stepNumber = Step.tokenize(step)[1]; // 3
|
const stepNumber = Step.tokenize(step)[1]; // 3
|
||||||
@@ -236,3 +235,5 @@ 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"
|
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('');
|
return [targetNote, offsetAccidentals].join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Note("Bb3").transpose("c3")
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { evaluate as _evaluate } from '@strudel/core';
|
import { evaluate as _evaluate } from '@strudel/cyclist';
|
||||||
import { transpiler } from './transpiler.mjs';
|
import { transpiler } from './transpiler.mjs';
|
||||||
export * from './transpiler.mjs';
|
export * from './transpiler.mjs';
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/transpiler",
|
"name": "@strudel/transpiler",
|
||||||
"version": "1.2.4",
|
"version": "1.2.2",
|
||||||
"description": "Transpiler for strudel user code. Converts syntactically correct but semantically meaningless JS into evaluatable strudel code.",
|
"description": "Transpiler for strudel user code. Converts syntactically correct but semantically meaningless JS into evaluatable strudel code.",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -31,6 +31,7 @@
|
|||||||
"homepage": "https://codeberg.org/uzu/strudel#readme",
|
"homepage": "https://codeberg.org/uzu/strudel#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@strudel/core": "workspace:*",
|
"@strudel/core": "workspace:*",
|
||||||
|
"@strudel/cyclist": "workspace:*",
|
||||||
"@strudel/mini": "workspace:*",
|
"@strudel/mini": "workspace:*",
|
||||||
"acorn": "^8.14.0",
|
"acorn": "^8.14.0",
|
||||||
"escodegen": "^2.1.0",
|
"escodegen": "^2.1.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/web",
|
"name": "@strudel/web",
|
||||||
"version": "1.2.5",
|
"version": "1.2.3",
|
||||||
"description": "Easy to setup, opiniated bundle of Strudel for the browser.",
|
"description": "Easy to setup, opiniated bundle of Strudel for the browser.",
|
||||||
"module": "web.mjs",
|
"module": "web.mjs",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
|
|||||||
@@ -7,5 +7,4 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
export * from './webaudio.mjs';
|
export * from './webaudio.mjs';
|
||||||
export * from './scope.mjs';
|
export * from './scope.mjs';
|
||||||
export * from './spectrum.mjs';
|
export * from './spectrum.mjs';
|
||||||
export * from './supradough.mjs';
|
|
||||||
export * from 'superdough';
|
export * from 'superdough';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/webaudio",
|
"name": "@strudel/webaudio",
|
||||||
"version": "1.2.5",
|
"version": "1.2.3",
|
||||||
"description": "Web Audio helpers for Strudel",
|
"description": "Web Audio helpers for Strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -35,8 +35,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@strudel/core": "workspace:*",
|
"@strudel/core": "workspace:*",
|
||||||
"@strudel/draw": "workspace:*",
|
"@strudel/draw": "workspace:*",
|
||||||
"superdough": "workspace:*",
|
"superdough": "workspace:*"
|
||||||
"supradough": "workspace:*"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vite": "^6.0.11"
|
"vite": "^6.0.11"
|
||||||
|
|||||||
@@ -1,130 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -5,12 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as strudel from '@strudel/core';
|
import * as strudel from '@strudel/core';
|
||||||
import { superdough, getAudioContext, setLogger, doughTrigger, registerWorklet } from 'superdough';
|
import { superdough, getAudioContext, setLogger, doughTrigger } from 'superdough';
|
||||||
import './supradough.mjs';
|
|
||||||
import { workletUrl } from 'supradough';
|
|
||||||
|
|
||||||
registerWorklet(workletUrl);
|
|
||||||
|
|
||||||
const { Pattern, logger, repl } = strudel;
|
const { Pattern, logger, repl } = strudel;
|
||||||
|
|
||||||
setLogger(logger);
|
setLogger(logger);
|
||||||
@@ -22,8 +17,8 @@ const hap2value = (hap) => {
|
|||||||
|
|
||||||
// uses more precise, absolute t if available, see https://github.com/tidalcycles/strudel/pull/1004
|
// uses more precise, absolute t if available, see https://github.com/tidalcycles/strudel/pull/1004
|
||||||
// TODO: refactor output callbacks to eliminate deadline
|
// TODO: refactor output callbacks to eliminate deadline
|
||||||
export const webaudioOutput = (hap, _deadline, hapDuration, cps, t) => {
|
export const webaudioOutput = (hap, deadline, hapDuration, cps, t) => {
|
||||||
return superdough(hap2value(hap), t, hapDuration, cps, hap.whole?.begin.valueOf());
|
return superdough(hap2value(hap), t, hapDuration, cps);
|
||||||
};
|
};
|
||||||
|
|
||||||
export function webaudioRepl(options = {}) {
|
export function webaudioRepl(options = {}) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/xen",
|
"name": "@strudel/xen",
|
||||||
"version": "1.2.4",
|
"version": "1.2.2",
|
||||||
"description": "Xenharmonic API for strudel",
|
"description": "Xenharmonic API for strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user