mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-16 23:56:53 -04:00
Compare commits
208 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9a8f8a051c | |||
| 3eb40ee7d3 | |||
| 48a6a12648 | |||
| 6bbf7cbe32 | |||
| 9f36b73081 | |||
| a7e485992e | |||
| ab74ccfe7c | |||
| 92b2013cf6 | |||
| 2e97fb7be2 | |||
| df1934f87b | |||
| d3472feb5c | |||
| addb6db1c7 | |||
| 566d590453 | |||
| e57bd98da0 | |||
| f5c4373f99 | |||
| e762291bb7 | |||
| 9da027d377 | |||
| 96dccdc38b | |||
| d6254294b0 | |||
| 339c1bb573 | |||
| 9782795761 | |||
| 2d2b238da9 | |||
| f0839e02eb | |||
| a9f7d825c8 | |||
| 709b654ed3 | |||
| ed00686804 | |||
| e10c469749 | |||
| d81a214c74 | |||
| e57f240e70 | |||
| e18296e760 | |||
| c145355ccc | |||
| 352a3c39d7 | |||
| a46617f3f0 | |||
| d164b5e7f5 | |||
| c0bf86d11e | |||
| 6f06683d70 | |||
| 637d714ab2 | |||
| c42d00f048 | |||
| 702e558d1a | |||
| bcc2e8f759 | |||
| 27339715b7 | |||
| 5e2e6411e5 | |||
| 0fac16e769 | |||
| d0ce82e3cd | |||
| e82629de2e | |||
| 38ce593c60 | |||
| 8a91549a7b | |||
| cc27d482cd | |||
| 37e640470b | |||
| 5e5e7730c1 | |||
| 89e7d52308 | |||
| 99bb227cf4 | |||
| 70e776e799 | |||
| 66aad0004a | |||
| a968545458 | |||
| 5414bbe85d | |||
| e7e80bfd83 | |||
| c57f5bb429 | |||
| f2608e712b | |||
| 34b41fc26f | |||
| f6e171fed8 | |||
| 2511dcc09e | |||
| 798eb22d9a | |||
| 977420e74d | |||
| 9c108862ef | |||
| 2fbdc9fdd7 | |||
| 0a96fa5896 | |||
| 3e0e903d38 | |||
| 591c3fe08f | |||
| 56b1ea3fa5 | |||
| c199b51645 | |||
| 8d2a368da9 | |||
| 052d09e892 | |||
| 439a7dc5e6 | |||
| 06ebcbff8c | |||
| 4e2e790864 | |||
| 4f3b40bd7b | |||
| 142160d79a | |||
| ff2c59a2a6 | |||
| 9b9176325b | |||
| b8e1b8d7ac | |||
| e84c39837f | |||
| 6527c68cda | |||
| 6a09f54b25 | |||
| dc6b766ab7 | |||
| 9eb3c9410d | |||
| 189e650a73 | |||
| a5886bb9d4 | |||
| 877bc95a58 | |||
| 18b1739b89 | |||
| 022ac95bbd | |||
| b3e25cfc52 | |||
| 3d25aa1b91 | |||
| b54139376e | |||
| d64a0ef0eb | |||
| 659071a4ee | |||
| 64f7bc4442 | |||
| 8c97f2e2d0 | |||
| b8c98736c5 | |||
| b8c46d6b26 | |||
| 79453ac2c3 | |||
| b08890bd9e | |||
| 938c92fda2 | |||
| a0fc52b1ec | |||
| 9a9fe83f9c | |||
| 0693a32736 | |||
| 42a903ecc0 | |||
| da283eb55a | |||
| b12707316a | |||
| 66aa3ac1da | |||
| bf5d9917ab | |||
| 4f15d681b0 | |||
| d81b1ab65c | |||
| 8465517c76 | |||
| 215ab87809 | |||
| e5fe998327 | |||
| 6f75a0a62b | |||
| 0464845262 | |||
| e490774294 | |||
| 34e8a57472 | |||
| 59c8d70714 | |||
| ce7cff2c3b | |||
| 32fe73aba2 | |||
| d6ee10e05c | |||
| 36441e7b73 | |||
| 189daa3942 | |||
| d009b9592e | |||
| f7f1bd63e8 | |||
| 7d24e45692 | |||
| cf55d4c8d1 | |||
| e7e636886d | |||
| e9aef9e4d4 | |||
| c8c6a2ce08 | |||
| 612efad3eb | |||
| e7839a09a1 | |||
| 3292f88810 | |||
| a4c040e101 | |||
| 7046c1f546 | |||
| a4ab8e1b06 | |||
| 2eda047108 | |||
| c8cf1dc712 | |||
| a26559fd3d | |||
| 5448fc6077 | |||
| 06fafbd8eb | |||
| 10156f0067 | |||
| 5632d83afb | |||
| a32473d570 | |||
| e87bb4376f | |||
| 0722cf7ded | |||
| 37fde8f3e8 | |||
| 8655c5b73c | |||
| 31c1450edc | |||
| cbb4207bab | |||
| 5efcd14f40 | |||
| 8051e56d13 | |||
| 239987af0b | |||
| 8363ba5a41 | |||
| 30f7127a92 | |||
| 14193abe03 | |||
| 3caf08e199 | |||
| 5cbd38a3e8 | |||
| bbd7ed0f27 | |||
| 43504d16b6 | |||
| 7e8206dcbc | |||
| 3963b807eb | |||
| e99adffc56 | |||
| c9f494d865 | |||
| e8254735bb | |||
| 3d30007756 | |||
| aeaed50446 | |||
| 3ac0fdf619 | |||
| d1b6b6be85 | |||
| 1081fc69e9 | |||
| 73ef43dd2c | |||
| 181e51ef0e | |||
| 1167fdb910 | |||
| ec9109dd89 | |||
| 55aef18851 | |||
| 356d4360ec | |||
| 0f2aa9569b | |||
| ac7ee376e5 | |||
| ab052ce1ac | |||
| 5988e3d3b4 | |||
| 2f2bf5bf29 | |||
| b000f2297d | |||
| 77463a199a | |||
| e54449f5f7 | |||
| d743570546 | |||
| 1dac69635d | |||
| 49f38aeddb | |||
| 43c05cb504 | |||
| ccc90d547c | |||
| 998a010247 | |||
| e4cd5dc552 | |||
| 6cf0b6f651 | |||
| 794bf86904 | |||
| 9ed690ce45 | |||
| 623bf93368 | |||
| 0cc18a6468 | |||
| 87598e10d1 | |||
| ccec7e725a | |||
| f0e6c5483c | |||
| 2cbd35f6d2 | |||
| 930dabe4d6 | |||
| ff4b7ca171 | |||
| 5129fa6677 | |||
| 5336569c25 | |||
| 483843bd0a |
@@ -1,6 +1,6 @@
|
|||||||
name: Strudel tests
|
name: Strudel tests
|
||||||
|
|
||||||
on: [push]
|
on: [push, pull_request]
|
||||||
|
|
||||||
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
@@ -0,0 +1,21 @@
|
|||||||
|
FROM node:24
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN npm install pnpm --global
|
||||||
|
|
||||||
|
COPY pnpm-workspace.yaml ./
|
||||||
|
COPY package.json pnpm-lock.yaml ./
|
||||||
|
COPY packages/ ./packages/
|
||||||
|
COPY examples/ ./examples/
|
||||||
|
RUN mkdir -p website/public
|
||||||
|
COPY website/package.json ./website/
|
||||||
|
|
||||||
|
RUN pnpm install
|
||||||
|
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
EXPOSE 4321
|
||||||
|
|
||||||
|
CMD ["pnpm", "dev"]
|
||||||
@@ -3,8 +3,6 @@
|
|||||||
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>
|
||||||
@@ -47,3 +45,5 @@ 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,6 +42,7 @@ 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',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,68 +1,122 @@
|
|||||||
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';
|
||||||
|
|
||||||
function plaintext(str) {
|
const escapeHtml = (str) => {
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.innerText = str;
|
div.innerText = str;
|
||||||
return div.innerHTML;
|
return div.innerHTML;
|
||||||
}
|
};
|
||||||
|
|
||||||
const getDocLabel = (doc) => doc.name || doc.longname;
|
const stripHtml = (html) => {
|
||||||
const getInnerText = (html) => {
|
const div = document.createElement('div');
|
||||||
var div = document.createElement('div');
|
|
||||||
div.innerHTML = html;
|
div.innerHTML = html;
|
||||||
return div.textContent || div.innerText || '';
|
return div.textContent || div.innerText || '';
|
||||||
};
|
};
|
||||||
|
|
||||||
export function Autocomplete({ doc, label }) {
|
const getDocLabel = (doc) => doc.name || doc.longname;
|
||||||
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 jsdocCompletions = jsdoc.docs
|
const buildParamsList = (params) =>
|
||||||
.filter(
|
params?.length
|
||||||
(doc) =>
|
? `
|
||||||
getDocLabel(doc) &&
|
<div class="autocomplete-info-params-section">
|
||||||
!getDocLabel(doc).startsWith('_') &&
|
<h4 class="autocomplete-info-section-title">Parameters</h4>
|
||||||
!['package'].includes(doc.kind) &&
|
<ul class="autocomplete-info-params-list">
|
||||||
!['superdirtOnly', 'noAutocomplete'].some((tag) => doc.tags?.find((t) => t.originalTitle === tag)),
|
${params
|
||||||
|
.map(
|
||||||
|
({ name, type, description }) => `
|
||||||
|
<li class="autocomplete-info-param-item">
|
||||||
|
<span class="autocomplete-info-param-name">${name}</span>
|
||||||
|
<span class="autocomplete-info-param-type">${type.names?.join(' | ')}</span>
|
||||||
|
${description ? `<div class="autocomplete-info-param-desc">${stripHtml(description)}</div>` : ''}
|
||||||
|
</li>
|
||||||
|
`,
|
||||||
)
|
)
|
||||||
// https://codemirror.net/docs/ref/#autocomplete.Completion
|
.join('')}
|
||||||
.map((doc) /*: Completion */ => ({
|
</ul>
|
||||||
label: getDocLabel(doc),
|
</div>
|
||||||
// detail: 'xxx', // An optional short piece of information to show (with a different style) after the label.
|
`
|
||||||
info: () => Autocomplete({ doc }),
|
: '';
|
||||||
type: 'function', // https://codemirror.net/docs/ref/#autocomplete.Completion.type
|
|
||||||
}));
|
const 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,
|
||||||
@@ -74,11 +128,5 @@ export const strudelAutocomplete = (context /* : CompletionContext */) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function isAutoCompletionEnabled(on) {
|
export const isAutoCompletionEnabled = (enabled) =>
|
||||||
return on
|
enabled ? [autocompletion({ override: [strudelAutocomplete], closeOnBlur: false })] : [];
|
||||||
? [
|
|
||||||
autocompletion({ override: [strudelAutocomplete] }),
|
|
||||||
//javascriptLanguage.data.of({ autocomplete: strudelAutocomplete }),
|
|
||||||
]
|
|
||||||
: []; // autocompletion({ override: [] })
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import {
|
||||||
|
keymap,
|
||||||
|
highlightSpecialChars,
|
||||||
|
drawSelection,
|
||||||
|
highlightActiveLine,
|
||||||
|
dropCursor,
|
||||||
|
rectangularSelection,
|
||||||
|
crosshairCursor,
|
||||||
|
lineNumbers,
|
||||||
|
highlightActiveLineGutter,
|
||||||
|
} from '@codemirror/view';
|
||||||
|
import {
|
||||||
|
defaultHighlightStyle,
|
||||||
|
syntaxHighlighting,
|
||||||
|
bracketMatching,
|
||||||
|
foldGutter,
|
||||||
|
foldKeymap,
|
||||||
|
} from '@codemirror/language';
|
||||||
|
import { defaultKeymap, history, historyKeymap } from '@codemirror/commands';
|
||||||
|
import { searchKeymap, highlightSelectionMatches } from '@codemirror/search';
|
||||||
|
import { completionKeymap, closeBracketsKeymap } from '@codemirror/autocomplete';
|
||||||
|
|
||||||
|
// Taken + slightly modified from https://github.com/codemirror/basic-setup/blob/main/src/codemirror.ts
|
||||||
|
|
||||||
|
export const basicSetup = (() => [
|
||||||
|
// lineNumbers(),
|
||||||
|
// highlightActiveLineGutter(),
|
||||||
|
highlightSpecialChars(),
|
||||||
|
history(),
|
||||||
|
// foldGutter(),
|
||||||
|
// drawSelection(),
|
||||||
|
dropCursor(),
|
||||||
|
// EditorState.allowMultipleSelections.of(true),
|
||||||
|
// indentOnInput(),
|
||||||
|
// syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
|
||||||
|
// autocompletion(),
|
||||||
|
rectangularSelection(),
|
||||||
|
crosshairCursor(),
|
||||||
|
// highlightActiveLine(),
|
||||||
|
// highlightSelectionMatches(),
|
||||||
|
keymap.of([
|
||||||
|
...closeBracketsKeymap,
|
||||||
|
...defaultKeymap,
|
||||||
|
// ...searchKeymap,
|
||||||
|
...historyKeymap,
|
||||||
|
// ...foldKeymap,
|
||||||
|
// ...completionKeymap,
|
||||||
|
]),
|
||||||
|
])();
|
||||||
|
|
||||||
|
/// A minimal set of extensions to create a functional editor. Only
|
||||||
|
/// includes [the default keymap](#commands.defaultKeymap), [undo
|
||||||
|
/// history](#commands.history), [special character
|
||||||
|
/// highlighting](#view.highlightSpecialChars), [custom selection
|
||||||
|
/// drawing](#view.drawSelection), and [default highlight
|
||||||
|
/// style](#language.defaultHighlightStyle).
|
||||||
|
export const minimalSetup = (() => [
|
||||||
|
highlightSpecialChars(),
|
||||||
|
history(),
|
||||||
|
drawSelection(),
|
||||||
|
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
|
||||||
|
keymap.of([...defaultKeymap, ...historyKeymap]),
|
||||||
|
])();
|
||||||
@@ -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 { history, indentWithTab } from '@codemirror/commands';
|
import { indentWithTab } from '@codemirror/commands';
|
||||||
import { javascript } from '@codemirror/lang-javascript';
|
import { javascript, javascriptLanguage } 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 {
|
||||||
@@ -24,6 +24,7 @@ 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 : []),
|
||||||
@@ -85,13 +86,17 @@ 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,6 +1,7 @@
|
|||||||
const parser = typeof DOMParser !== 'undefined' ? new DOMParser() : null;
|
|
||||||
export let html = (string) => {
|
export let html = (string) => {
|
||||||
return parser?.parseFromString(string, 'text/html').querySelectorAll('*');
|
const template = document.createElement('template');
|
||||||
|
template.innerHTML = string.trim();
|
||||||
|
return template.content.childNodes;
|
||||||
};
|
};
|
||||||
let parseChunk = (chunk) => {
|
let parseChunk = (chunk) => {
|
||||||
if (Array.isArray(chunk)) return chunk.flat().join('');
|
if (Array.isArray(chunk)) return chunk.flat().join('');
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ 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, historyKeymap } from '@codemirror/commands';
|
import { defaultKeymap } from '@codemirror/commands';
|
||||||
|
|
||||||
const vscodePlugin = ViewPlugin.fromClass(
|
const vscodePlugin = ViewPlugin.fromClass(
|
||||||
class {
|
class {
|
||||||
@@ -27,5 +28,5 @@ const keymaps = {
|
|||||||
|
|
||||||
export function keybindings(name) {
|
export function keybindings(name) {
|
||||||
const active = keymaps[name];
|
const active = keymaps[name];
|
||||||
return [active ? active() : [], keymap.of(historyKeymap)];
|
return [active ? Prec.high(active()) : []];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/codemirror",
|
"name": "@strudel/codemirror",
|
||||||
"version": "1.2.3",
|
"version": "1.2.5",
|
||||||
"description": "Codemirror Extensions for Strudel",
|
"description": "Codemirror Extensions for Strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
"@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.2.1",
|
"@replit/codemirror-vim": "^6.3.0",
|
||||||
"@replit/codemirror-vscode-keymap": "^6.0.2",
|
"@replit/codemirror-vscode-keymap": "^6.0.2",
|
||||||
"@strudel/core": "workspace:*",
|
"@strudel/core": "workspace:*",
|
||||||
"@strudel/draw": "workspace:*",
|
"@strudel/draw": "workspace:*",
|
||||||
|
|||||||
@@ -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 } from './autocomplete.mjs';
|
import { Autocomplete, getSynonymDoc } from './autocomplete.mjs';
|
||||||
|
|
||||||
const getDocLabel = (doc) => doc.name || doc.longname;
|
const getDocLabel = (doc) => doc.name || doc.longname;
|
||||||
|
|
||||||
@@ -52,10 +52,11 @@ export const strudelTooltip = hoverTooltip(
|
|||||||
let entry = jsdoc.docs.filter((doc) => getDocLabel(doc) === word)[0];
|
let entry = jsdoc.docs.filter((doc) => getDocLabel(doc) === word)[0];
|
||||||
if (!entry) {
|
if (!entry) {
|
||||||
// Try for synonyms
|
// Try for synonyms
|
||||||
entry = jsdoc.docs.filter((doc) => doc.synonyms && doc.synonyms.includes(word))[0];
|
const doc = jsdoc.docs.filter((doc) => doc.synonyms && doc.synonyms.includes(word))[0];
|
||||||
if (!entry) {
|
if (!doc) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
entry = getSynonymDoc(doc, word);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -66,7 +67,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({ doc: entry, label: word });
|
const ac = Autocomplete(entry);
|
||||||
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 { calculateTactus, sequence, stack } from '../index.mjs';
|
import { calculateSteps, sequence, stack } from '../index.mjs';
|
||||||
|
|
||||||
const pat64 = sequence(...Array(64).keys());
|
const pat64 = sequence(...Array(64).keys());
|
||||||
|
|
||||||
describe('steps', () => {
|
describe('steps', () => {
|
||||||
calculateTactus(true);
|
calculateSteps(true);
|
||||||
bench(
|
bench(
|
||||||
'+tactus',
|
'+tactus',
|
||||||
() => {
|
() => {
|
||||||
@@ -14,7 +14,7 @@ describe('steps', () => {
|
|||||||
{ time: 1000 },
|
{ time: 1000 },
|
||||||
);
|
);
|
||||||
|
|
||||||
calculateTactus(false);
|
calculateSteps(false);
|
||||||
bench(
|
bench(
|
||||||
'-tactus',
|
'-tactus',
|
||||||
() => {
|
() => {
|
||||||
@@ -25,7 +25,7 @@ describe('steps', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('stack', () => {
|
describe('stack', () => {
|
||||||
calculateTactus(true);
|
calculateSteps(true);
|
||||||
bench(
|
bench(
|
||||||
'+tactus',
|
'+tactus',
|
||||||
() => {
|
() => {
|
||||||
@@ -34,7 +34,7 @@ describe('stack', () => {
|
|||||||
{ time: 1000 },
|
{ time: 1000 },
|
||||||
);
|
);
|
||||||
|
|
||||||
calculateTactus(false);
|
calculateSteps(false);
|
||||||
bench(
|
bench(
|
||||||
'-tactus',
|
'-tactus',
|
||||||
() => {
|
() => {
|
||||||
@@ -43,4 +43,4 @@ describe('stack', () => {
|
|||||||
{ time: 1000 },
|
{ time: 1000 },
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
calculateTactus(true);
|
calculateSteps(true);
|
||||||
|
|||||||
+131
-29
@@ -91,6 +91,7 @@ export const { s, sound } = registerControl(['s', 'n', 'gain'], 'sound');
|
|||||||
* 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
|
||||||
*
|
*
|
||||||
@@ -113,7 +114,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 octave number (0-9). Defaults to 3
|
* - optional (possibly negative) octave number (0-9). Defaults to 3
|
||||||
*
|
*
|
||||||
* Examples of valid note names: `c`, `bb`, `Bb`, `f#`, `c3`, `A4`, `Eb2`, `c#5`
|
* Examples of valid note names: `c`, `bb`, `Bb`, `f#`, `c3`, `A4`, `Eb2`, `c#5`
|
||||||
*
|
*
|
||||||
@@ -126,6 +127,8 @@ 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']);
|
||||||
|
|
||||||
@@ -141,8 +144,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")
|
||||||
@@ -254,7 +257,7 @@ export const { fmenv } = registerControl('fmenv');
|
|||||||
export const { fmattack } = registerControl('fmattack');
|
export const { fmattack } = registerControl('fmattack');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* waveform of the fm modulator
|
* Waveform of the fm modulator
|
||||||
*
|
*
|
||||||
* @name fmwave
|
* @name fmwave
|
||||||
* @param {number | Pattern} wave waveform
|
* @param {number | Pattern} wave waveform
|
||||||
@@ -309,6 +312,17 @@ 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 +391,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
|
||||||
@@ -438,7 +452,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).
|
||||||
@@ -449,7 +463,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.
|
||||||
@@ -460,7 +474,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
|
* Modulate the amplitude of a sound with a continuous waveform
|
||||||
*
|
*
|
||||||
* @name tremolo
|
* @name tremolo
|
||||||
* @synonyms trem
|
* @synonyms trem
|
||||||
@@ -472,7 +486,7 @@ export const { coarse } = registerControl('coarse');
|
|||||||
export const { tremolo } = registerControl(['tremolo', 'tremolodepth', 'tremoloskew', 'tremolophase'], 'trem');
|
export const { tremolo } = registerControl(['tremolo', 'tremolodepth', 'tremoloskew', 'tremolophase'], 'trem');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* modulate the amplitude of a sound with a continuous waveform
|
* Modulate the amplitude of a sound with a continuous waveform
|
||||||
*
|
*
|
||||||
* @name tremolosync
|
* @name tremolosync
|
||||||
* @synonyms tremsync
|
* @synonyms tremsync
|
||||||
@@ -487,7 +501,7 @@ export const { tremolosync } = registerControl(
|
|||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* depth of amplitude modulation
|
* Depth of amplitude modulation
|
||||||
*
|
*
|
||||||
* @name tremolodepth
|
* @name tremolodepth
|
||||||
* @synonyms tremdepth
|
* @synonyms tremdepth
|
||||||
@@ -498,7 +512,7 @@ export const { tremolosync } = registerControl(
|
|||||||
*/
|
*/
|
||||||
export const { tremolodepth } = registerControl('tremolodepth', 'tremdepth');
|
export const { tremolodepth } = registerControl('tremolodepth', 'tremdepth');
|
||||||
/**
|
/**
|
||||||
* alter the shape of the modulation waveform
|
* Alter the shape of the modulation waveform
|
||||||
*
|
*
|
||||||
* @name tremoloskew
|
* @name tremoloskew
|
||||||
* @synonyms tremskew
|
* @synonyms tremskew
|
||||||
@@ -510,7 +524,7 @@ export const { tremolodepth } = registerControl('tremolodepth', 'tremdepth');
|
|||||||
export const { tremoloskew } = registerControl('tremoloskew', 'tremskew');
|
export const { tremoloskew } = registerControl('tremoloskew', 'tremskew');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* alter the phase of the modulation waveform
|
* Alter the phase of the modulation waveform
|
||||||
*
|
*
|
||||||
* @name tremolophase
|
* @name tremolophase
|
||||||
* @synonyms tremphase
|
* @synonyms tremphase
|
||||||
@@ -522,9 +536,10 @@ export const { tremoloskew } = registerControl('tremoloskew', 'tremskew');
|
|||||||
export const { tremolophase } = registerControl('tremolophase', 'tremphase');
|
export const { tremolophase } = registerControl('tremolophase', 'tremphase');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* shape of amplitude modulation
|
* Shape of amplitude modulation
|
||||||
*
|
*
|
||||||
* @name tremoloshape
|
* @name tremoloshape
|
||||||
|
* @synonyms tremshape
|
||||||
* @param {number | Pattern} shape tri | square | sine | saw | ramp
|
* @param {number | Pattern} shape tri | square | sine | saw | ramp
|
||||||
* @example
|
* @example
|
||||||
* note("{f g c d}%16").tremsync(4).tremoloshape("<sine tri square>").s("sawtooth")
|
* note("{f g c d}%16").tremsync(4).tremoloshape("<sine tri square>").s("sawtooth")
|
||||||
@@ -532,7 +547,7 @@ export const { tremolophase } = registerControl('tremolophase', 'tremphase');
|
|||||||
*/
|
*/
|
||||||
export const { tremoloshape } = registerControl('tremoloshape', 'tremshape');
|
export const { tremoloshape } = registerControl('tremoloshape', 'tremshape');
|
||||||
/**
|
/**
|
||||||
* filter overdrive for supported filter types
|
* Filter overdrive for supported filter types
|
||||||
*
|
*
|
||||||
* @name drive
|
* @name drive
|
||||||
* @param {number | Pattern} amount
|
* @param {number | Pattern} amount
|
||||||
@@ -540,44 +555,94 @@ export const { tremoloshape } = registerControl('tremoloshape', 'tremshape');
|
|||||||
* note("{f g g c d a a#}%16".sub(17)).s("supersaw").lpenv(8).lpf(150).lpq(.8).ftype('ladder').drive("<.5 4>")
|
* note("{f g g c d a a#}%16".sub(17)).s("supersaw").lpenv(8).lpf(150).lpq(.8).ftype('ladder').drive("<.5 4>")
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
export const { drive } = registerControl('drive');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* modulate the amplitude of an orbit to create a "sidechain" like effect
|
* 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
|
* @name duckorbit
|
||||||
|
* @synonyms duck
|
||||||
* @param {number | Pattern} orbit target orbit
|
* @param {number | Pattern} orbit target orbit
|
||||||
* @example
|
* @example
|
||||||
* $: n(run(16)).scale("c:minor:pentatonic").s("sawtooth").delay(.7).orbit(2)
|
* $: 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)
|
* $: 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');
|
export const { duck } = registerControl('duckorbit', 'duck');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* the amount of ducking applied to target orbit
|
* 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
|
* @name duckdepth
|
||||||
* @param {number | Pattern} depth depth of modulation from 0 to 1
|
* @param {number | Pattern} depth depth of modulation from 0 to 1
|
||||||
* @example
|
* @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>"))
|
* 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');
|
export const { duckdepth } = registerControl('duckdepth');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* the attack time of the duck effect
|
* 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
|
* @name duckattack
|
||||||
* @param {number | Pattern} time
|
* @synonyms duckatt
|
||||||
|
*
|
||||||
|
* @param {number | Pattern} time The attack time in seconds
|
||||||
* @example
|
* @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 0 0.4>").duckdepth(1))
|
* 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');
|
export const { duckattack } = registerControl('duckattack', 'duckatt');
|
||||||
|
|
||||||
export const { drive } = registerControl('drive');
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create byte beats with custom expressions
|
* Create byte beats with custom expressions
|
||||||
*
|
*
|
||||||
@@ -618,7 +683,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
|
||||||
@@ -630,7 +695,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
|
||||||
@@ -642,7 +707,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
|
||||||
@@ -700,7 +765,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
|
* @synonyms phd, phasdp
|
||||||
* @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)
|
||||||
@@ -711,7 +776,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
|
||||||
@@ -1097,14 +1162,27 @@ 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 delaytime
|
* @name delayspeed
|
||||||
* @param {number | Pattern} seconds between 0 and Infinity
|
* @param {number | Pattern} delayspeed controls the pitch of the delay feedback
|
||||||
* @synonyms delayt, dt
|
* @synonyms delayt, dt
|
||||||
* @example
|
* @example
|
||||||
* s("bd bd").delay(.25).delaytime("<.125 .25 .5 1>")
|
* note("d d a# a".fast(2)).s("sawtooth").delay(.8).delaytime(1/2).delayspeed("<2 .5 -1 -2>")
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export const { delaytime, delayt, dt } = registerControl('delaytime', 'delayt', 'dt');
|
export const { delaytime, delayt, dt } = registerControl('delaytime', 'delayt', 'dt');
|
||||||
@@ -1182,6 +1260,7 @@ export const { dry } = registerControl('dry');
|
|||||||
* Used when using `begin`/`end` or `chop`/`striate` and friends, to change the fade out time of the 'grain' envelope.
|
* 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()
|
||||||
@@ -1516,6 +1595,29 @@ 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..
|
||||||
|
|||||||
@@ -5,8 +5,9 @@ let debounce = 1000,
|
|||||||
lastTime;
|
lastTime;
|
||||||
|
|
||||||
export function errorLogger(e, origin = 'cyclist') {
|
export function errorLogger(e, origin = 'cyclist') {
|
||||||
//TODO: add some kind of debug flag that enables this while in dev mode
|
if (process.env.NODE_ENV === 'development') {
|
||||||
// console.error(e);
|
console.error(e);
|
||||||
|
}
|
||||||
logger(`[${origin}] error: ${e.message}`);
|
logger(`[${origin}] error: ${e.message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/core",
|
"name": "@strudel/core",
|
||||||
"version": "1.2.3",
|
"version": "1.2.4",
|
||||||
"description": "Port of Tidal Cycles to JavaScript",
|
"description": "Port of Tidal Cycles to JavaScript",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1246,7 +1246,8 @@ 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
|
||||||
@@ -1299,7 +1300,8 @@ 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
|
||||||
@@ -1382,11 +1384,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"])
|
||||||
*
|
*
|
||||||
@@ -2397,6 +2399,57 @@ 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
|
||||||
@@ -3277,7 +3330,7 @@ export const slice = register(
|
|||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
* @example
|
* @example
|
||||||
* s("bd!8").onTriggerTime((hap) => {console.info(hap)})
|
* s("bd!8").onTriggerTime((hap) => {console.log(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) => {
|
||||||
|
|||||||
@@ -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 [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/csound",
|
"name": "@strudel/csound",
|
||||||
"version": "1.2.4",
|
"version": "1.2.5",
|
||||||
"description": "csound bindings for strudel",
|
"description": "csound bindings for strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/draw",
|
"name": "@strudel/draw",
|
||||||
"version": "1.2.3",
|
"version": "1.2.4",
|
||||||
"description": "Helpers for drawing with Strudel",
|
"description": "Helpers for drawing with Strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/gamepad",
|
"name": "@strudel/gamepad",
|
||||||
"version": "1.2.3",
|
"version": "1.2.4",
|
||||||
"description": "Gamepad Inputs for strudel",
|
"description": "Gamepad Inputs for strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/hydra",
|
"name": "@strudel/hydra",
|
||||||
"version": "1.2.3",
|
"version": "1.2.4",
|
||||||
"description": "Hydra integration for strudel",
|
"description": "Hydra integration for strudel",
|
||||||
"main": "hydra.mjs",
|
"main": "hydra.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -493,6 +493,9 @@ 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.4",
|
"version": "1.2.5",
|
||||||
"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 { calculateTactus } from '../../core/index.mjs';
|
import { calculateSteps } from '../../core/index.mjs';
|
||||||
import { mini } from '../index.mjs';
|
import { mini } from '../index.mjs';
|
||||||
|
|
||||||
describe('mini', () => {
|
describe('mini', () => {
|
||||||
calculateTactus(true);
|
calculateSteps(true);
|
||||||
bench(
|
bench(
|
||||||
'+tactus',
|
'+tactus',
|
||||||
() => {
|
() => {
|
||||||
@@ -13,7 +13,7 @@ describe('mini', () => {
|
|||||||
{ time: 1000 },
|
{ time: 1000 },
|
||||||
);
|
);
|
||||||
|
|
||||||
calculateTactus(false);
|
calculateSteps(false);
|
||||||
bench(
|
bench(
|
||||||
'-tactus',
|
'-tactus',
|
||||||
() => {
|
() => {
|
||||||
@@ -21,5 +21,5 @@ describe('mini', () => {
|
|||||||
},
|
},
|
||||||
{ time: 1000 },
|
{ time: 1000 },
|
||||||
);
|
);
|
||||||
calculateTactus(true);
|
calculateSteps(true);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/mini",
|
"name": "@strudel/mini",
|
||||||
"version": "1.2.3",
|
"version": "1.2.4",
|
||||||
"description": "Mini notation for strudel",
|
"description": "Mini notation for strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -108,7 +108,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');
|
||||||
}
|
}
|
||||||
|
|
||||||
let getLocations = (code, offset) => runner.parser.get_locations(code, offset);
|
export let getLocations = (code, offset) => runner.parser.get_locations(code, offset);
|
||||||
|
|
||||||
export const mondi = (str, offset) => {
|
export const mondi = (str, offset) => {
|
||||||
const code = `[${str}]`;
|
const code = `[${str}]`;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/mondo",
|
"name": "@strudel/mondo",
|
||||||
"version": "1.1.1",
|
"version": "1.1.4",
|
||||||
"description": "mondo notation for strudel",
|
"description": "mondo notation for strudel",
|
||||||
"main": "mondough.mjs",
|
"main": "mondough.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -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.3",
|
"version": "1.2.4",
|
||||||
"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.3",
|
"version": "1.2.4",
|
||||||
"description": "MQTT API for strudel",
|
"description": "MQTT API for strudel",
|
||||||
"main": "mqtt.mjs",
|
"main": "mqtt.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/osc",
|
"name": "@strudel/osc",
|
||||||
"version": "1.2.3",
|
"version": "1.2.4",
|
||||||
"description": "OSC messaging for strudel",
|
"description": "OSC messaging for strudel",
|
||||||
"main": "osc.mjs",
|
"main": "osc.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -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(null, hap, currentTime, cps, targetTime);
|
return trigger(hap, currentTime, cps, targetTime);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/repl",
|
"name": "@strudel/repl",
|
||||||
"version": "1.2.4",
|
"version": "1.2.6",
|
||||||
"description": "Strudel REPL as a Web Component",
|
"description": "Strudel REPL as a Web Component",
|
||||||
"module": "index.mjs",
|
"module": "index.mjs",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
|
|||||||
@@ -20,3 +20,13 @@ 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.0",
|
"version": "0.2.3",
|
||||||
"description": "",
|
"description": "",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"tidalcycles",
|
"tidalcycles",
|
||||||
|
|||||||
@@ -1,22 +1,20 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
import cowsay from 'cowsay';
|
import cowsay from 'cowsay';
|
||||||
import { createReadStream, existsSync } from 'fs';
|
import { createReadStream, existsSync, writeFileSync } from 'fs';
|
||||||
import { readdir } from 'fs/promises';
|
import { readdir } from 'fs/promises';
|
||||||
import http from 'http';
|
import http from 'http';
|
||||||
import { join, sep } from 'path';
|
import { join, resolve, 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'];
|
||||||
|
|
||||||
console.log(
|
const isAudioFile = (f) => {
|
||||||
cowsay.say({
|
const ext = f.split('.').slice(-1)[0].toLowerCase();
|
||||||
text: 'welcome to @strudel/sampler',
|
return VALID_AUDIO_EXTENSIONS.includes(ext);
|
||||||
e: 'oO',
|
};
|
||||||
T: 'U ',
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
async function getFilesInDirectory(directory) {
|
async function getFilesInDirectory(directory) {
|
||||||
let files = [];
|
let files = [];
|
||||||
@@ -29,42 +27,90 @@ async function getFilesInDirectory(directory) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const subFiles = (await getFilesInDirectory(fullPath)).filter((f) =>
|
const subFiles = (await getFilesInDirectory(fullPath)).filter(isAudioFile);
|
||||||
['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 {
|
||||||
files.push(fullPath);
|
isAudioFile(fullPath) && files.push(fullPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return files;
|
return files;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getBanks(directory) {
|
async function getBanks(directory, flat = false) {
|
||||||
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 [bank] = path.split('/').slice(-2);
|
const subDir = path.replace(directory, '');
|
||||||
|
const subDirFlat = subDir.replaceAll('/', '_').slice(1); // remove initial underscore
|
||||||
|
const subDirFlatStem = subDirFlat.replace(/\.[^.]+$/, ''); // remove extension
|
||||||
|
let bank = flat ? subDirFlatStem : path.split('/').slice(-2)[0];
|
||||||
banks[bank] = banks[bank] || [];
|
banks[bank] = banks[bank] || [];
|
||||||
const relativeUrl = path.replace(directory, '');
|
banks[bank].push(subDir);
|
||||||
banks[bank].push(relativeUrl);
|
return subDir;
|
||||||
return relativeUrl;
|
|
||||||
});
|
});
|
||||||
banks._base = `http://localhost:5432`;
|
banks._base = `http://localhost:5432`;
|
||||||
return { banks, files };
|
return { banks, files };
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line
|
const args = process.argv.slice(2);
|
||||||
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);
|
const { banks, files } = await getBanks(directory, getArgValue('--flat'));
|
||||||
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));
|
||||||
@@ -72,7 +118,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.3",
|
"version": "1.2.4",
|
||||||
"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.4",
|
"version": "1.2.5",
|
||||||
"description": "Soundsfont support for strudel",
|
"description": "Soundsfont support for strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
|
|||||||
@@ -174,7 +174,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) {
|
if (hasEnvelope === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const penv = nanFallback(value.penv, 1, true);
|
const penv = nanFallback(value.penv, 1, true);
|
||||||
@@ -211,11 +211,29 @@ export function getVibratoOscillator(param, value, t) {
|
|||||||
export function webAudioTimeout(audioContext, onComplete, startTime, stopTime) {
|
export function webAudioTimeout(audioContext, onComplete, startTime, stopTime) {
|
||||||
const constantNode = new ConstantSourceNode(audioContext);
|
const constantNode = new ConstantSourceNode(audioContext);
|
||||||
|
|
||||||
constantNode.start(startTime);
|
// Certain browsers requires audio nodes to be connected in order for their onended events
|
||||||
constantNode.stop(stopTime);
|
// 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 = () => {
|
constantNode.onended = () => {
|
||||||
|
// Ensure garbage collection
|
||||||
|
try {
|
||||||
|
zeroGain.disconnect();
|
||||||
|
} catch {
|
||||||
|
// pass
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
constantNode.disconnect();
|
||||||
|
} catch {
|
||||||
|
// pass
|
||||||
|
}
|
||||||
onComplete();
|
onComplete();
|
||||||
};
|
};
|
||||||
|
constantNode.start(startTime);
|
||||||
|
constantNode.stop(stopTime);
|
||||||
return constantNode;
|
return constantNode;
|
||||||
}
|
}
|
||||||
const mod = (freq, range = 1, type = 'sine') => {
|
const mod = (freq, range = 1, type = 'sine') => {
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
let log = (msg) => console.log(msg);
|
let log = (msg) => console.log(msg);
|
||||||
|
|
||||||
export function errorLogger(e, origin = 'cyclist') {
|
export function errorLogger(e, origin = 'superdough') {
|
||||||
//TODO: add some kind of debug flag that enables this while in dev mode
|
if (process.env.NODE_ENV === 'development') {
|
||||||
// console.error(e);
|
console.error(e);
|
||||||
|
}
|
||||||
logger(`[${origin}] error: ${e.message}`);
|
logger(`[${origin}] error: ${e.message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "superdough",
|
"name": "superdough",
|
||||||
"version": "1.2.4",
|
"version": "1.2.5",
|
||||||
"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,7 +1,9 @@
|
|||||||
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) {
|
AudioContext.prototype.adjustLength = function (duration, buffer, speed = 1, offsetAmount = 0) {
|
||||||
|
const sampleOffset = Math.floor(clamp(offsetAmount, 0, 1) * buffer.length);
|
||||||
const newLength = buffer.sampleRate * duration;
|
const 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++) {
|
||||||
@@ -9,22 +11,30 @@ 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++) {
|
||||||
newData[i] = oldData[i] || 0;
|
// loop the buffer around to prevent
|
||||||
|
let position = (sampleOffset + i * Math.abs(speed)) % oldData.length;
|
||||||
|
if (speed < 1) {
|
||||||
|
position = position * -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
newData[i] = oldData.at(position) || 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return newBuffer;
|
return newBuffer;
|
||||||
};
|
};
|
||||||
|
|
||||||
AudioContext.prototype.createReverb = function (duration, fade, lp, dim, ir) {
|
AudioContext.prototype.createReverb = function (duration, fade, lp, dim, ir, irspeed, irbegin) {
|
||||||
const convolver = this.createConvolver();
|
const convolver = this.createConvolver();
|
||||||
convolver.generate = (d = 2, fade = 0.1, lp = 15000, dim = 1000, ir) => {
|
convolver.generate = (d = 2, fade = 0.1, lp = 15000, dim = 1000, ir, irspeed, irbegin) => {
|
||||||
convolver.duration = d;
|
convolver.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);
|
convolver.buffer = this.adjustLength(d, ir, irspeed, irbegin);
|
||||||
} else {
|
} else {
|
||||||
reverbGen.generateReverb(
|
reverbGen.generateReverb(
|
||||||
{
|
{
|
||||||
@@ -41,7 +51,7 @@ if (typeof AudioContext !== 'undefined') {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
convolver.generate(duration, fade, lp, dim, ir);
|
convolver.generate(duration, fade, lp, dim, ir, irspeed, irbegin);
|
||||||
return convolver;
|
return convolver;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -196,6 +196,52 @@ 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
|
||||||
@@ -217,49 +263,8 @@ function getSamplesPrefixHandler(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') {
|
||||||
// check if custom prefix handler
|
const [json, base] = await fetchSampleMap(sampleMap);
|
||||||
const handler = getSamplesPrefixHandler(sampleMap);
|
return samples(json, baseUrl || base, options);
|
||||||
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(
|
||||||
|
|||||||
@@ -113,6 +113,19 @@ export async function aliasBank(...args) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register an alias for a sound.
|
||||||
|
* @param {string} original - The original sound name
|
||||||
|
* @param {string} alias - The alias to use for the sound
|
||||||
|
*/
|
||||||
|
export function soundAlias(original, alias) {
|
||||||
|
if (getSound(original) == null) {
|
||||||
|
logger('soundAlias: original sound not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
soundMap.setKey(alias, getSound(original));
|
||||||
|
}
|
||||||
|
|
||||||
export function getSound(s) {
|
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`);
|
||||||
@@ -196,7 +209,7 @@ export const resetLoadedSounds = () => soundMap.set({});
|
|||||||
let audioContext;
|
let audioContext;
|
||||||
|
|
||||||
export const setDefaultAudioContext = () => {
|
export const setDefaultAudioContext = () => {
|
||||||
audioContext = new AudioContext();
|
audioContext = new AudioContext({ latencyHint: 'playback' });
|
||||||
return audioContext;
|
return audioContext;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -212,11 +225,17 @@ 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();
|
||||||
workletsLoading = audioCtx.audioWorklet.addModule(workletsUrl);
|
const allWorkletURLs = externalWorklets.concat([workletsUrl]);
|
||||||
|
workletsLoading = Promise.all(allWorkletURLs.map((workletURL) => audioCtx.audioWorklet.addModule(workletURL)));
|
||||||
}
|
}
|
||||||
|
|
||||||
return workletsLoading;
|
return workletsLoading;
|
||||||
@@ -282,7 +301,6 @@ export async function initAudioOnFirstClick(options) {
|
|||||||
return audioReady;
|
return audioReady;
|
||||||
}
|
}
|
||||||
|
|
||||||
let delays = {};
|
|
||||||
const maxfeedback = 0.98;
|
const maxfeedback = 0.98;
|
||||||
|
|
||||||
let channelMerger, destinationGain;
|
let channelMerger, destinationGain;
|
||||||
@@ -326,23 +344,22 @@ export const panic = () => {
|
|||||||
channelMerger == null;
|
channelMerger == null;
|
||||||
};
|
};
|
||||||
|
|
||||||
function getDelay(orbit, delaytime, delayfeedback, t, channels) {
|
function getDelay(orbit, delaytime, delayfeedback, t) {
|
||||||
if (delayfeedback > maxfeedback) {
|
if (delayfeedback > maxfeedback) {
|
||||||
//logger(`delayfeedback was clamped to ${maxfeedback} to save your ears`);
|
//logger(`delayfeedback was clamped to ${maxfeedback} to save your ears`);
|
||||||
}
|
}
|
||||||
delayfeedback = clamp(delayfeedback, 0, 0.98);
|
delayfeedback = clamp(delayfeedback, 0, 0.98);
|
||||||
if (!orbits[orbit].delayNode) {
|
let delayNode = orbits[orbit].delayNode;
|
||||||
|
if (delayNode === undefined) {
|
||||||
const ac = getAudioContext();
|
const ac = getAudioContext();
|
||||||
const dly = ac.createFeedbackDelay(1, delaytime, delayfeedback);
|
delayNode = ac.createFeedbackDelay(1, delaytime, delayfeedback);
|
||||||
dly.start?.(t); // for some reason, this throws when audion extension is installed..
|
delayNode.start?.(t); // for some reason, this throws when audion extension is installed..
|
||||||
connectToOrbit(dly, orbit);
|
connectToOrbit(delayNode, orbit);
|
||||||
orbits[orbit].delayNode = dly;
|
orbits[orbit].delayNode = delayNode;
|
||||||
}
|
}
|
||||||
orbits[orbit].delayNode.delayTime.value !== delaytime &&
|
delayNode.delayTime.value !== delaytime && delayNode.delayTime.setValueAtTime(delaytime, t);
|
||||||
orbits[orbit].delayNode.delayTime.setValueAtTime(delaytime, t);
|
delayNode.feedback.value !== delayfeedback && delayNode.feedback.setValueAtTime(delayfeedback, t);
|
||||||
orbits[orbit].delayNode.feedback.value !== delayfeedback &&
|
return delayNode;
|
||||||
orbits[orbit].delayNode.feedback.setValueAtTime(delayfeedback, t);
|
|
||||||
return orbits[orbit].delayNode;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getLfo(audioContext, begin, end, properties = {}) {
|
export function getLfo(audioContext, begin, end, properties = {}) {
|
||||||
@@ -398,42 +415,60 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
//type orbit {
|
// type orbit {
|
||||||
// gain: number,
|
// output: GainNode,
|
||||||
// reverbNode: reverbNode
|
// reverbNode: ConvolverNode
|
||||||
// delayNode: delayNode
|
// delayNode: FeedbackDelayNode
|
||||||
//}
|
// }
|
||||||
let orbits = {};
|
let orbits = {};
|
||||||
function connectToOrbit(node, orbit) {
|
function connectToOrbit(node, orbit) {
|
||||||
if (orbits[orbit] == null) {
|
if (orbits[orbit] == null) {
|
||||||
errorLogger(new Error('target orbit does not exist'), 'superdough');
|
errorLogger(new Error('target orbit does not exist'), 'superdough');
|
||||||
}
|
}
|
||||||
node.connect(orbits[orbit].gain);
|
node.connect(orbits[orbit].output);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setOrbit(audioContext, orbit, channels) {
|
function setOrbit(audioContext, orbit, channels) {
|
||||||
if (orbits[orbit] == null) {
|
if (orbits[orbit] == null) {
|
||||||
orbits[orbit] = {
|
orbits[orbit] = {
|
||||||
gain: new GainNode(audioContext, { gain: 1 }),
|
// Setup output node through which all audio filters prior to hitting
|
||||||
|
// the destination (and thus allows for global volume automation)
|
||||||
|
output: new GainNode(audioContext, { gain: 1, channelCount: 2, channelCountMode: 'explicit' }),
|
||||||
};
|
};
|
||||||
connectToDestination(orbits[orbit].gain, channels);
|
connectToDestination(orbits[orbit].output, channels);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function duckOrbit(audioContext, targetOrbit, t, attacktime = 0.1, duckdepth = 1) {
|
|
||||||
const targetArr = [targetOrbit].flat();
|
|
||||||
|
|
||||||
targetArr.forEach((target) => {
|
function duckOrbit(audioContext, targetOrbit, t, onsettime = 0, attacktime = 0.1, duckdepth = 1) {
|
||||||
|
const targetArr = [targetOrbit].flat();
|
||||||
|
const onsetArr = [onsettime].flat();
|
||||||
|
const attackArr = [attacktime].flat();
|
||||||
|
const depthArr = [duckdepth].flat();
|
||||||
|
|
||||||
|
targetArr.forEach((target, idx) => {
|
||||||
if (orbits[target] == null) {
|
if (orbits[target] == null) {
|
||||||
errorLogger(new Error(`duck target orbit ${target} does not exist`), 'superdough');
|
errorLogger(new Error(`duck target orbit ${target} does not exist`), 'superdough');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const onset = onsetArr[idx] ?? onsetArr[0];
|
||||||
|
const attack = Math.max(attackArr[idx] ?? attackArr[0], 0.002);
|
||||||
|
const depth = depthArr[idx] ?? depthArr[0];
|
||||||
|
const gainParam = orbits[target].output.gain;
|
||||||
webAudioTimeout(
|
webAudioTimeout(
|
||||||
audioContext,
|
audioContext,
|
||||||
() => {
|
() => {
|
||||||
orbits[target].gain.gain.cancelScheduledValues(t);
|
const now = audioContext.currentTime;
|
||||||
const currVal = orbits[target].gain.gain.value;
|
|
||||||
orbits[target].gain.gain.linearRampToValueAtTime(clamp(1 - Math.pow(duckdepth, 0.5), 0.01, currVal), t);
|
// cancelScheduledValues and setValueAtTime together emulate cancelAndHoldAtTime
|
||||||
orbits[target].gain.gain.exponentialRampToValueAtTime(1, t + Math.max(0.002, attacktime));
|
// 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,
|
0,
|
||||||
t - 0.01,
|
t - 0.01,
|
||||||
@@ -442,29 +477,33 @@ function duckOrbit(audioContext, targetOrbit, t, attacktime = 0.1, duckdepth = 1
|
|||||||
}
|
}
|
||||||
|
|
||||||
let hasChanged = (now, before) => now !== undefined && now !== before;
|
let hasChanged = (now, before) => now !== undefined && now !== before;
|
||||||
function getReverb(orbit, duration, fade, lp, dim, ir) {
|
function getReverb(orbit, duration, fade, lp, dim, ir, irspeed, irbegin) {
|
||||||
// If no reverb has been created for a given orbit, create one
|
// If no reverb has been created for a given orbit, create one
|
||||||
if (!orbits[orbit].reverbNode) {
|
let reverbNode = orbits[orbit].reverbNode;
|
||||||
|
if (reverbNode === undefined) {
|
||||||
const ac = getAudioContext();
|
const ac = getAudioContext();
|
||||||
const reverb = ac.createReverb(duration, fade, lp, dim, ir);
|
reverbNode = ac.createReverb(duration, fade, lp, dim, ir, irspeed, irbegin);
|
||||||
connectToOrbit(reverb, orbit);
|
connectToOrbit(reverbNode, orbit);
|
||||||
orbits[orbit].reverbNode = reverb;
|
orbits[orbit].reverbNode = reverbNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
hasChanged(duration, orbits[orbit].reverbNode.duration) ||
|
hasChanged(duration, reverbNode.duration) ||
|
||||||
hasChanged(fade, orbits[orbit].reverbNode.fade) ||
|
hasChanged(fade, reverbNode.fade) ||
|
||||||
hasChanged(lp, orbits[orbit].reverbNode.lp) ||
|
hasChanged(lp, reverbNode.lp) ||
|
||||||
hasChanged(dim, orbits[orbit].reverbNode.dim) ||
|
hasChanged(dim, reverbNode.dim) ||
|
||||||
orbits[orbit].reverbNode.ir !== ir
|
hasChanged(irspeed, reverbNode.irspeed) ||
|
||||||
|
hasChanged(irbegin, reverbNode.irbegin) ||
|
||||||
|
reverbNode.ir !== ir
|
||||||
) {
|
) {
|
||||||
// only regenerate when something has changed
|
// only regenerate when something has changed
|
||||||
// avoids endless regeneration on things like
|
// avoids endless regeneration on things like
|
||||||
// stack(s("a"), s("b").rsize(8)).room(.5)
|
// stack(s("a"), s("b").rsize(8)).room(.5)
|
||||||
// this only works when args may stay undefined until here
|
// this only works when args may stay undefined until here
|
||||||
// setting default values breaks this
|
// setting default values breaks this
|
||||||
orbits[orbit].reverbNode.generate(duration, fade, lp, dim, ir);
|
reverbNode.generate(duration, fade, lp, dim, ir, irspeed, irbegin);
|
||||||
}
|
}
|
||||||
return orbits[orbit].reverbNode;
|
return reverbNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export let analysers = {},
|
export let analysers = {},
|
||||||
@@ -561,6 +600,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
|||||||
postgain = getDefaultValue('postgain'),
|
postgain = getDefaultValue('postgain'),
|
||||||
density = getDefaultValue('density'),
|
density = getDefaultValue('density'),
|
||||||
duckorbit,
|
duckorbit,
|
||||||
|
duckonset,
|
||||||
duckattack,
|
duckattack,
|
||||||
duckdepth,
|
duckdepth,
|
||||||
// filters
|
// filters
|
||||||
@@ -619,6 +659,8 @@ 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
|
||||||
@@ -640,7 +682,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
|||||||
setOrbit(ac, orbit, channels, t, cycle, cps);
|
setOrbit(ac, orbit, channels, t, cycle, cps);
|
||||||
|
|
||||||
if (duckorbit != null) {
|
if (duckorbit != null) {
|
||||||
duckOrbit(ac, duckorbit, t, duckattack, duckdepth);
|
duckOrbit(ac, duckorbit, t, duckonset, duckattack, duckdepth);
|
||||||
}
|
}
|
||||||
|
|
||||||
gain = applyGainCurve(nanFallback(gain, 1));
|
gain = applyGainCurve(nanFallback(gain, 1));
|
||||||
@@ -832,7 +874,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
|||||||
// delay
|
// delay
|
||||||
let delaySend;
|
let delaySend;
|
||||||
if (delay > 0 && delaytime > 0 && delayfeedback > 0) {
|
if (delay > 0 && delaytime > 0 && delayfeedback > 0) {
|
||||||
const delayNode = getDelay(orbit, delaytime, delayfeedback, t, orbitChannels);
|
const delayNode = getDelay(orbit, delaytime, delayfeedback, t);
|
||||||
delaySend = effectSend(post, delayNode, delay);
|
delaySend = effectSend(post, delayNode, delay);
|
||||||
audioNodes.push(delaySend);
|
audioNodes.push(delaySend);
|
||||||
}
|
}
|
||||||
@@ -850,7 +892,7 @@ 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);
|
||||||
}
|
}
|
||||||
const reverbNode = getReverb(orbit, roomsize, roomfade, roomlp, roomdim, roomIR, orbitChannels);
|
const reverbNode = getReverb(orbit, roomsize, roomfade, roomlp, roomdim, roomIR, irspeed, irbegin);
|
||||||
reverbSend = effectSend(post, reverbNode, room);
|
reverbSend = effectSend(post, reverbNode, room);
|
||||||
audioNodes.push(reverbSend);
|
audioNodes.push(reverbSend);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
pattern.wav
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# supradough
|
||||||
|
|
||||||
|
platform agnostic synth and sampler intended for live coding. a reimplementation of superdough.
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
// this is a poc of how a pattern can be rendered as a wav file using node
|
||||||
|
// run via: node dough-export.mjs
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import WavEncoder from 'wav-encoder';
|
||||||
|
import { evalScope } from '@strudel/core';
|
||||||
|
import { miniAllStrings } from '@strudel/mini';
|
||||||
|
import { Dough } from './dough.mjs';
|
||||||
|
|
||||||
|
await evalScope(
|
||||||
|
import('@strudel/core'),
|
||||||
|
import('@strudel/mini'),
|
||||||
|
import('@strudel/tonal'),
|
||||||
|
// import('@strudel/tonal'),
|
||||||
|
);
|
||||||
|
|
||||||
|
miniAllStrings(); // allows using single quotes for mini notation / skip transpilation
|
||||||
|
|
||||||
|
let sampleRate = 48000,
|
||||||
|
cps = 0.4;
|
||||||
|
|
||||||
|
/* await doughsamples('github:eddyflux/crate');
|
||||||
|
await doughsamples('github:eddyflux/wax'); */
|
||||||
|
|
||||||
|
let pat = note('c,eb,g,<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));
|
||||||
|
});
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { Dough } from './dough.mjs';
|
||||||
|
|
||||||
|
const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
|
||||||
|
|
||||||
|
class DoughProcessor extends AudioWorkletProcessor {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.dough = new Dough(sampleRate, currentTime);
|
||||||
|
this.port.onmessage = (event) => {
|
||||||
|
if (event.data.spawn) {
|
||||||
|
this.dough.scheduleSpawn(event.data.spawn);
|
||||||
|
} else if (event.data.sample) {
|
||||||
|
this.dough.loadSample(event.data.sample, event.data.channels, event.data.sampleRate);
|
||||||
|
} else if (event.data.samples) {
|
||||||
|
event.data.samples.forEach(([name, channels, sampleRate]) => {
|
||||||
|
this.dough.loadSample(name, channels, sampleRate);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log('unrecognized event type', event.data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
process(inputs, outputs, params) {
|
||||||
|
if (this.disconnected) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const output = outputs[0];
|
||||||
|
for (let i = 0; i < output[0].length; i++) {
|
||||||
|
this.dough.update();
|
||||||
|
for (let c = 0; c < output.length; c++) {
|
||||||
|
//prevent speaker blowout via clipping if threshold exceeds
|
||||||
|
output[c][i] = clamp(this.dough.out[c], -1, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true; // keep the audio processing going
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
registerProcessor('dough-processor', DoughProcessor);
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,5 @@
|
|||||||
|
// import _workletUrl from './dough-worklet.mjs?url'; // only for dev (breaks for production build)
|
||||||
|
import _workletUrl from './dough-worklet.mjs?audioworklet'; // only for prod (breaks in development?!)
|
||||||
|
|
||||||
|
export * from './dough.mjs';
|
||||||
|
export const workletUrl = _workletUrl;
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"name": "supradough",
|
||||||
|
"version": "1.2.3",
|
||||||
|
"description": "platform agnostic synth and sampler intended for live coding. a reimplementation of superdough.",
|
||||||
|
"main": "index.mjs",
|
||||||
|
"type": "module",
|
||||||
|
"publishConfig": {
|
||||||
|
"main": "dist/index.mjs"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "vite build",
|
||||||
|
"prepublishOnly": "npm run build"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/tidalcycles/strudel.git"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"tidalcycles",
|
||||||
|
"strudel",
|
||||||
|
"pattern",
|
||||||
|
"livecoding",
|
||||||
|
"algorave"
|
||||||
|
],
|
||||||
|
"author": "Felix Roos <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.3",
|
"version": "1.2.4",
|
||||||
"description": "Tonal functions for strudel",
|
"description": "Tonal functions for strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
|
|||||||
@@ -7,15 +7,18 @@ 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 } from '@strudel/core';
|
import { pure, n, seq, note, noteToMidi } 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', () => {
|
||||||
it('Should run tonal functions ', () => {
|
describe('scaleTranspose', () => {
|
||||||
|
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']);
|
||||||
});
|
});
|
||||||
it('scale with plain values', () => {
|
});
|
||||||
|
describe('scale', () => {
|
||||||
|
it('converts plain values', () => {
|
||||||
expect(
|
expect(
|
||||||
seq(0, 1, 2)
|
seq(0, 1, 2)
|
||||||
.scale('C major')
|
.scale('C major')
|
||||||
@@ -23,34 +26,80 @@ describe('tonal', () => {
|
|||||||
.firstCycleValues.map((h) => h.note),
|
.firstCycleValues.map((h) => h.note),
|
||||||
).toEqual(['C3', 'D3', 'E3']);
|
).toEqual(['C3', 'D3', 'E3']);
|
||||||
});
|
});
|
||||||
it('scale with n values', () => {
|
it('converts n values', () => {
|
||||||
expect(
|
expect(
|
||||||
n(seq(0, 1, 2))
|
n(seq(0, 1, 2))
|
||||||
.scale('C major')
|
.scale('C major')
|
||||||
.firstCycleValues.map((h) => h.note),
|
.firstCycleValues.map((h) => h.note),
|
||||||
).toEqual(['C3', 'D3', 'E3']);
|
).toEqual(['C3', 'D3', 'E3']);
|
||||||
});
|
});
|
||||||
it('scale with colon', () => {
|
it('converts n values (mini notation)', () => {
|
||||||
expect(
|
expect(
|
||||||
n(seq(0, 1, 2))
|
n(seq(0, 1, 2))
|
||||||
.scale('C:major')
|
.scale('C:major')
|
||||||
.firstCycleValues.map((h) => h.note),
|
.firstCycleValues.map((h) => h.note),
|
||||||
).toEqual(['C3', 'D3', 'E3']);
|
).toEqual(['C3', 'D3', 'E3']);
|
||||||
});
|
});
|
||||||
it('scale without tonic', () => {
|
it('converts n values (no tonic)', () => {
|
||||||
expect(
|
expect(
|
||||||
n(seq(0, 1, 2))
|
n(seq(0, 1, 2))
|
||||||
.scale('major')
|
.scale('major')
|
||||||
.firstCycleValues.map((h) => h.note),
|
.firstCycleValues.map((h) => h.note),
|
||||||
).toEqual(['C3', 'D3', 'E3']);
|
).toEqual(['C3', 'D3', 'E3']);
|
||||||
});
|
});
|
||||||
it('scale with mininotation colon', () => {
|
it('converts n values (explicit mini notation)', () => {
|
||||||
expect(
|
expect(
|
||||||
n(seq(0, 1, 2))
|
n(seq(0, 1, 2))
|
||||||
.scale(mini('C:major'))
|
.scale(mini('C:major'))
|
||||||
.firstCycleValues.map((h) => h.note),
|
.firstCycleValues.map((h) => h.note),
|
||||||
).toEqual(['C3', 'D3', 'E3']);
|
).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('transposes note numbers with interval numbers', () => {
|
it('transposes note numbers with interval numbers', () => {
|
||||||
expect(
|
expect(
|
||||||
note(seq(40, 40, 40))
|
note(seq(40, 40, 40))
|
||||||
@@ -83,4 +132,5 @@ describe('tonal', () => {
|
|||||||
).toEqual(['C', 'D', 'Eb']);
|
).toEqual(['C', 'D', 'Eb']);
|
||||||
expect(seq('c', 'c', 'c').transpose('1P', '2M', '3m').firstCycleValues).toEqual(['C', 'D', 'Eb']);
|
expect(seq('c', 'c', 'c').transpose('1P', '2M', '3m').firstCycleValues).toEqual(['C', 'D', 'Eb']);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+91
-43
@@ -6,19 +6,28 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
|
|
||||||
import { Note, Interval, Scale } from '@tonaljs/tonal';
|
import { 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 } from './tonleiter.mjs';
|
import { stepInNamedScale, nearestNumberIndex } from './tonleiter.mjs';
|
||||||
|
import { noteToMidi } from '../core/util.mjs';
|
||||||
|
|
||||||
const octavesInterval = (octaves) => (octaves <= 0 ? -1 : 1) + octaves * 7 + 'P';
|
const octavesInterval = (octaves) => (octaves <= 0 ? -1 : 1) + octaves * 7 + 'P';
|
||||||
|
|
||||||
function scaleStep(step, scale) {
|
function getScale(scaleName) {
|
||||||
scale = scale.replaceAll(':', ' ');
|
scaleName = scaleName.replaceAll(':', ' ');
|
||||||
step = Math.ceil(step);
|
const scale = Scale.get(scaleName);
|
||||||
let { intervals, tonic, empty } = Scale.get(scale);
|
const { tonic, empty } = scale;
|
||||||
if ((empty && isNote(scale)) || (empty && !tonic)) {
|
if ((empty && isNote(scaleName)) || (empty && !tonic)) {
|
||||||
throw new Error(`incomplete scale. Make sure to use ":" instead of spaces, example: .scale("C:major")`);
|
throw new Error(
|
||||||
|
`Scale name ${scaleName} is incomplete. Make sure to use ":" instead of spaces, example: .scale("C:major")`,
|
||||||
|
);
|
||||||
} else if (empty) {
|
} else if (empty) {
|
||||||
throw new Error(`invalid scale "${scale}"`);
|
throw new Error(`Invalid scale name "${scaleName}"`);
|
||||||
}
|
}
|
||||||
|
return scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
function scaleStep(step, scale) {
|
||||||
|
step = Math.ceil(step);
|
||||||
|
let { intervals, tonic } = getScale(scale);
|
||||||
tonic = tonic || 'C';
|
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);
|
||||||
@@ -30,8 +39,7 @@ 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 [tonic, scaleName] = Scale.tokenize(scale);
|
let { notes } = getScale(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)) {
|
||||||
@@ -120,10 +128,7 @@ 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);
|
||||||
// TODO: move simplify to player to preserve enharmonics
|
const targetNote = Note.transpose(note, interval);
|
||||||
// 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 }));
|
||||||
}
|
}
|
||||||
@@ -171,8 +176,59 @@ export const { scaleTranspose, scaleTrans, strans } = register(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 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]);
|
||||||
|
// These decorations will determine the semitone offset based on the number of
|
||||||
|
// sharps or flats
|
||||||
|
const decorations = match[2] || '';
|
||||||
|
offset = decorations[0] === '#' ? decorations.length : -decorations.length;
|
||||||
|
}
|
||||||
|
return [asNumber, offset];
|
||||||
|
}
|
||||||
|
|
||||||
|
let scaleToMidisAndNotes = {};
|
||||||
|
// Finds the nearest scale note to `note`
|
||||||
|
function _getNearestScaleNote(scaleName, note, preferHigher = true) {
|
||||||
|
let noteMidi = typeof note === 'string' ? noteToMidi(note) : note;
|
||||||
|
if (scaleToMidisAndNotes[scaleName] === undefined) {
|
||||||
|
const { intervals, tonic } = getScale(scaleName);
|
||||||
|
const { pc } = Note.get(tonic);
|
||||||
|
const expandedIntervals = intervals.concat('8P'); // add the octave for wrapping
|
||||||
|
const sNotes = expandedIntervals.map((interval) => Note.transpose(pc + '0', interval));
|
||||||
|
const sMidi = sNotes.map(noteToMidi);
|
||||||
|
// Cache
|
||||||
|
scaleToMidisAndNotes[scaleName] = [sMidi, sNotes];
|
||||||
|
}
|
||||||
|
const [scaleMidis, scaleNotes] = scaleToMidisAndNotes[scaleName];
|
||||||
|
const rootMidi = scaleMidis[0];
|
||||||
|
const octaveDiff = Math.floor((noteMidi - rootMidi) / 12);
|
||||||
|
const alignedMidis = scaleMidis.map((m) => m + 12 * octaveDiff);
|
||||||
|
const noteIdx = nearestNumberIndex(noteMidi, alignedMidis, preferHigher);
|
||||||
|
const noteMatch = scaleNotes[noteIdx];
|
||||||
|
return Note.transpose(noteMatch, Interval.fromSemitones(12 * octaveDiff));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Turns numbers into notes in the scale (zero indexed). Also sets scale for other scale operations, like {@link Pattern#scaleTranspose}.
|
* Turns numbers into notes in the scale (zero indexed) or quantizes notes to a scale.
|
||||||
|
*
|
||||||
|
* When describing notes via numbers, note that negative numbers can be used to wrap backwards
|
||||||
|
* in the scale as well as sharps or flats (but not both) to produce notes outside of the scale.
|
||||||
|
*
|
||||||
|
* Also sets scale for other scale operations, like {@link Pattern#scaleTranspose}.
|
||||||
*
|
*
|
||||||
* A scale consists of a root note (e.g. `c4`, `c`, `f#`, `bb4`) followed by semicolon (':') and then a [scale type](https://github.com/tonaljs/tonal/blob/main/packages/scale-type/data.ts).
|
* 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).
|
||||||
*
|
*
|
||||||
@@ -191,6 +247,12 @@ export const { scaleTranspose, scaleTrans, strans } = register(
|
|||||||
* 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(
|
||||||
@@ -204,49 +266,35 @@ export const scale = register(
|
|||||||
pat
|
pat
|
||||||
.fmap((value) => {
|
.fmap((value) => {
|
||||||
const isObject = typeof value === 'object';
|
const isObject = typeof value === 'object';
|
||||||
let step = isObject ? value.n : value;
|
// The case where the note has been defined via `n` or `pure`
|
||||||
if (isObject) {
|
if (!isObject || (isObject && ('n' in value || 'value' in value))) {
|
||||||
|
const step = isObject ? (value.n ?? value.value) : value;
|
||||||
delete value.n; // remove n so it won't cause trouble
|
delete value.n; // remove n so it won't cause trouble
|
||||||
}
|
|
||||||
if (isNote(step)) {
|
if (isNote(step)) {
|
||||||
// legacy..
|
// legacy..
|
||||||
return pure(step);
|
return pure(step);
|
||||||
}
|
}
|
||||||
let asNumber = Number(step);
|
|
||||||
let semitones = 0;
|
|
||||||
if (isNaN(asNumber)) {
|
|
||||||
step = String(step);
|
|
||||||
if (!/^[-+]?\d+(#*|b*){1}$/.test(step)) {
|
|
||||||
logger(
|
|
||||||
`[tonal] invalid scale step "${step}", expected number or integer with optional # b suffixes`,
|
|
||||||
'error',
|
|
||||||
);
|
|
||||||
return silence;
|
|
||||||
}
|
|
||||||
const isharp = step.indexOf('#');
|
|
||||||
if (isharp >= 0) {
|
|
||||||
asNumber = Number(step.substring(0, isharp));
|
|
||||||
semitones = step.length - isharp;
|
|
||||||
} else {
|
|
||||||
const iflat = step.indexOf('b');
|
|
||||||
asNumber = Number(step.substring(0, iflat));
|
|
||||||
semitones = iflat - step.length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
|
const [number, offset] = _convertStepToNumberAndOffset(step);
|
||||||
let note;
|
let note;
|
||||||
if (isObject && value.anchor) {
|
if (isObject && value.anchor) {
|
||||||
note = stepInNamedScale(asNumber, scale, value.anchor);
|
note = stepInNamedScale(number, scale, value.anchor);
|
||||||
} else {
|
} else {
|
||||||
note = scaleStep(asNumber, scale);
|
note = scaleStep(number, scale);
|
||||||
}
|
}
|
||||||
if (semitones != 0) note = Note.transpose(note, Interval.fromSemitones(semitones));
|
if (offset != 0) note = Note.transpose(note, Interval.fromSemitones(offset));
|
||||||
value = pure(isObject ? { ...value, note } : note);
|
value = pure(isObject ? { ...value, note } : note);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger(`[tonal] ${err.message}`, 'error');
|
logger(`[tonal] ${err.message}`, 'error');
|
||||||
value = silence;
|
return silence;
|
||||||
}
|
}
|
||||||
return value;
|
return value;
|
||||||
|
}
|
||||||
|
// The case where the note has been defined via `note`
|
||||||
|
else {
|
||||||
|
const note = _getNearestScaleNote(scale, value.note);
|
||||||
|
return pure(isObject ? { ...value, note } : note);
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.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) {
|
||||||
let [root, scaleName] = Scale.tokenize(scale);
|
const [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]) {
|
||||||
let { intervals } = Scale.get(`C ${scaleName}`);
|
const { intervals } = Scale.get(`C ${scaleName}`);
|
||||||
// cache result
|
// cache result
|
||||||
scaleSteps[scaleName] = intervals.map(step2semitones);
|
scaleSteps[scaleName] = intervals.map(step2semitones);
|
||||||
}
|
}
|
||||||
@@ -222,6 +222,7 @@ 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
|
||||||
@@ -235,5 +236,3 @@ export function transpose(note, step) {
|
|||||||
const offsetAccidentals = accidentalString(Step.accidentals(step) + Note.accidentals(note) + stepIndex - indexOffset); // "we need to add a # to to the G to make it a major third from E"
|
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,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/transpiler",
|
"name": "@strudel/transpiler",
|
||||||
"version": "1.2.3",
|
"version": "1.2.4",
|
||||||
"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",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/web",
|
"name": "@strudel/web",
|
||||||
"version": "1.2.4",
|
"version": "1.2.5",
|
||||||
"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,4 +7,5 @@ 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.4",
|
"version": "1.2.5",
|
||||||
"description": "Web Audio helpers for Strudel",
|
"description": "Web Audio helpers for Strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -35,7 +35,8 @@
|
|||||||
"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"
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
import { Pattern } from '@strudel/core';
|
||||||
|
import { connectToDestination, getAudioContext, getWorklet } from 'superdough';
|
||||||
|
|
||||||
|
let doughWorklet;
|
||||||
|
|
||||||
|
function initDoughWorklet() {
|
||||||
|
const ac = getAudioContext();
|
||||||
|
doughWorklet = getWorklet(
|
||||||
|
ac,
|
||||||
|
'dough-processor',
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
outputChannelCount: [2],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
connectToDestination(doughWorklet); // channels?
|
||||||
|
}
|
||||||
|
|
||||||
|
const soundMap = new Map();
|
||||||
|
const loadedSounds = new Map();
|
||||||
|
|
||||||
|
Pattern.prototype.supradough = function () {
|
||||||
|
return this.onTrigger((hap, __, cps, begin) => {
|
||||||
|
hap.value._begin = begin;
|
||||||
|
hap.value._duration = hap.duration / cps;
|
||||||
|
!doughWorklet && initDoughWorklet();
|
||||||
|
const s = (hap.value.bank ? hap.value.bank + '_' : '') + hap.value.s;
|
||||||
|
const n = hap.value.n ?? 0;
|
||||||
|
const soundKey = `${s}:${n}`;
|
||||||
|
if (soundMap.has(s)) {
|
||||||
|
hap.value.s = soundKey; // dough.mjs is unaware of bank and n (only maps keys to buffers)
|
||||||
|
}
|
||||||
|
if (soundMap.has(s) && !loadedSounds.has(soundKey)) {
|
||||||
|
const urls = soundMap.get(s);
|
||||||
|
const url = urls[n % urls.length];
|
||||||
|
console.log(`load ${soundKey} from ${url}`);
|
||||||
|
const loadSample = fetchSample(url);
|
||||||
|
loadedSounds.set(soundKey, loadSample);
|
||||||
|
loadSample.then(({ channels, sampleRate }) =>
|
||||||
|
doughWorklet.port.postMessage({
|
||||||
|
sample: soundKey,
|
||||||
|
channels,
|
||||||
|
sampleRate,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
doughWorklet.port.postMessage({ spawn: hap.value });
|
||||||
|
}, 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
function githubPath(base, subpath = '') {
|
||||||
|
if (!base.startsWith('github:')) {
|
||||||
|
throw new Error('expected "github:" at the start of pseudoUrl');
|
||||||
|
}
|
||||||
|
let [_, path] = base.split('github:');
|
||||||
|
path = path.endsWith('/') ? path.slice(0, -1) : path;
|
||||||
|
if (path.split('/').length === 2) {
|
||||||
|
// assume main as default branch if none set
|
||||||
|
path += '/main';
|
||||||
|
}
|
||||||
|
return `https://raw.githubusercontent.com/${path}/${subpath}`;
|
||||||
|
}
|
||||||
|
export async function fetchSampleMap(url) {
|
||||||
|
if (url.startsWith('github:')) {
|
||||||
|
url = githubPath(url, 'strudel.json');
|
||||||
|
}
|
||||||
|
if (url.startsWith('local:')) {
|
||||||
|
url = `http://localhost:5432`;
|
||||||
|
}
|
||||||
|
if (url.startsWith('shabda:')) {
|
||||||
|
let [_, path] = url.split('shabda:');
|
||||||
|
url = `https://shabda.ndre.gr/${path}.json?strudel=1`;
|
||||||
|
}
|
||||||
|
if (url.startsWith('shabda/speech')) {
|
||||||
|
let [_, path] = url.split('shabda/speech');
|
||||||
|
path = path.startsWith('/') ? path.substring(1) : path;
|
||||||
|
let [params, words] = path.split(':');
|
||||||
|
let gender = 'f';
|
||||||
|
let language = 'en-GB';
|
||||||
|
if (params) {
|
||||||
|
[language, gender] = params.split('/');
|
||||||
|
}
|
||||||
|
url = `https://shabda.ndre.gr/speech/${words}.json?gender=${gender}&language=${language}&strudel=1'`;
|
||||||
|
}
|
||||||
|
if (typeof fetch !== 'function') {
|
||||||
|
// not a browser
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const base = url.split('/').slice(0, -1).join('/');
|
||||||
|
if (typeof fetch === 'undefined') {
|
||||||
|
// skip fetch when in node / testing
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const json = await fetch(url)
|
||||||
|
.then((res) => res.json())
|
||||||
|
.catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
throw new Error(`error loading "${url}"`);
|
||||||
|
});
|
||||||
|
return [json, json._base || base];
|
||||||
|
}
|
||||||
|
|
||||||
|
// for some reason, only piano and flute work.. is it because mp3??
|
||||||
|
|
||||||
|
async function fetchSample(url) {
|
||||||
|
const buffer = await fetch(url)
|
||||||
|
.then((res) => res.arrayBuffer())
|
||||||
|
.then((buf) => getAudioContext().decodeAudioData(buf));
|
||||||
|
let channels = [];
|
||||||
|
for (let i = 0; i < buffer.numberOfChannels; i++) {
|
||||||
|
channels.push(buffer.getChannelData(i));
|
||||||
|
}
|
||||||
|
return { channels, sampleRate: buffer.sampleRate };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function doughsamples(sampleMap, baseUrl) {
|
||||||
|
if (typeof sampleMap === 'string') {
|
||||||
|
const [json, base] = await fetchSampleMap(sampleMap);
|
||||||
|
// console.log('json', json, 'base', base);
|
||||||
|
return doughsamples(json, base);
|
||||||
|
}
|
||||||
|
Object.entries(sampleMap).map(async ([key, urls]) => {
|
||||||
|
if (key !== '_base') {
|
||||||
|
urls = urls.map((url) => baseUrl + url);
|
||||||
|
// console.log('set', key, urls);
|
||||||
|
soundMap.set(key, urls);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -5,7 +5,12 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as strudel from '@strudel/core';
|
import * as strudel from '@strudel/core';
|
||||||
import { superdough, getAudioContext, setLogger, doughTrigger } from 'superdough';
|
import { superdough, getAudioContext, setLogger, doughTrigger, registerWorklet } from 'superdough';
|
||||||
|
import './supradough.mjs';
|
||||||
|
import { workletUrl } from 'supradough';
|
||||||
|
|
||||||
|
registerWorklet(workletUrl);
|
||||||
|
|
||||||
const { Pattern, logger, repl } = strudel;
|
const { Pattern, logger, repl } = strudel;
|
||||||
|
|
||||||
setLogger(logger);
|
setLogger(logger);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel/xen",
|
"name": "@strudel/xen",
|
||||||
"version": "1.2.3",
|
"version": "1.2.4",
|
||||||
"description": "Xenharmonic API for strudel",
|
"description": "Xenharmonic API for strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -139,10 +139,10 @@ Tune.prototype.MIDI = function(stepIn,octaveIn) {
|
|||||||
|
|
||||||
/* Load a new scale */
|
/* Load a new scale */
|
||||||
|
|
||||||
Tune.prototype.loadScale = function(name){
|
Tune.prototype.loadScale = function(scale){
|
||||||
|
|
||||||
/* load the scale */
|
/* load the scale */
|
||||||
var freqs = TuningList[name].frequencies
|
var freqs = isArrayOfNumbers(scale) ? scale : TuningList[scale].frequencies
|
||||||
this.scale = []
|
this.scale = []
|
||||||
for (var i=0;i<freqs.length-1;i++) {
|
for (var i=0;i<freqs.length-1;i++) {
|
||||||
this.scale.push(freqs[i]/freqs[0])
|
this.scale.push(freqs[i]/freqs[0])
|
||||||
@@ -207,8 +207,13 @@ Tune.prototype.search = function(letters) {
|
|||||||
return possible
|
return possible
|
||||||
}
|
}
|
||||||
|
|
||||||
Tune.prototype.isValidScale = function(name) {
|
function isArrayOfNumbers(arg) {
|
||||||
return !!TuningList[name];
|
return Array.isArray(arg) && arg.length > 0 && arg.every(item => typeof item === 'number' && !isNaN(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* allow an array of values too */
|
||||||
|
Tune.prototype.isValidScale = function(scale) {
|
||||||
|
return !!TuningList[scale] || isArrayOfNumbers(scale) ;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Return a collection of notes as an array */
|
/* Return a collection of notes as an array */
|
||||||
|
|||||||
Generated
+33
-10
@@ -201,8 +201,8 @@ importers:
|
|||||||
specifier: ^6.1.0
|
specifier: ^6.1.0
|
||||||
version: 6.1.0(@codemirror/autocomplete@6.18.4)(@codemirror/commands@6.8.0)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)
|
version: 6.1.0(@codemirror/autocomplete@6.18.4)(@codemirror/commands@6.8.0)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)
|
||||||
'@replit/codemirror-vim':
|
'@replit/codemirror-vim':
|
||||||
specifier: ^6.2.1
|
specifier: ^6.3.0
|
||||||
version: 6.2.1(@codemirror/commands@6.8.0)(@codemirror/language@6.10.8)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)
|
version: 6.3.0(@codemirror/commands@6.8.0)(@codemirror/language@6.10.8)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)
|
||||||
'@replit/codemirror-vscode-keymap':
|
'@replit/codemirror-vscode-keymap':
|
||||||
specifier: ^6.0.2
|
specifier: ^6.0.2
|
||||||
version: 6.0.2(@codemirror/autocomplete@6.18.4)(@codemirror/commands@6.8.0)(@codemirror/language@6.10.8)(@codemirror/lint@6.8.4)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)
|
version: 6.0.2(@codemirror/autocomplete@6.18.4)(@codemirror/commands@6.8.0)(@codemirror/language@6.10.8)(@codemirror/lint@6.8.4)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)
|
||||||
@@ -517,6 +517,18 @@ importers:
|
|||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../vite-plugin-bundle-audioworklet
|
version: link:../vite-plugin-bundle-audioworklet
|
||||||
|
|
||||||
|
packages/supradough:
|
||||||
|
devDependencies:
|
||||||
|
vite:
|
||||||
|
specifier: ^6.0.11
|
||||||
|
version: 6.0.11(@types/node@22.10.10)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0)
|
||||||
|
vite-plugin-bundle-audioworklet:
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../vite-plugin-bundle-audioworklet
|
||||||
|
wav-encoder:
|
||||||
|
specifier: ^1.3.0
|
||||||
|
version: 1.3.0
|
||||||
|
|
||||||
packages/tidal:
|
packages/tidal:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@strudel/core':
|
'@strudel/core':
|
||||||
@@ -625,6 +637,9 @@ importers:
|
|||||||
superdough:
|
superdough:
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../superdough
|
version: link:../superdough
|
||||||
|
supradough:
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../supradough
|
||||||
devDependencies:
|
devDependencies:
|
||||||
vite:
|
vite:
|
||||||
specifier: ^6.0.11
|
specifier: ^6.0.11
|
||||||
@@ -2232,14 +2247,14 @@ packages:
|
|||||||
'@codemirror/state': ^6.0.1
|
'@codemirror/state': ^6.0.1
|
||||||
'@codemirror/view': ^6.3.0
|
'@codemirror/view': ^6.3.0
|
||||||
|
|
||||||
'@replit/codemirror-vim@6.2.1':
|
'@replit/codemirror-vim@6.3.0':
|
||||||
resolution: {integrity: sha512-qDAcGSHBYU5RrdO//qCmD8K9t6vbP327iCj/iqrkVnjbrpFhrjOt92weGXGHmTNRh16cUtkUZ7Xq7rZf+8HVow==}
|
resolution: {integrity: sha512-aTx931ULAMuJx6xLf7KQDOL7CxD+Sa05FktTDrtLaSy53uj01ll3Zf17JdKsriER248oS55GBzg0CfCTjEneAQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@codemirror/commands': ^6.0.0
|
'@codemirror/commands': 6.x.x
|
||||||
'@codemirror/language': ^6.1.0
|
'@codemirror/language': 6.x.x
|
||||||
'@codemirror/search': ^6.2.0
|
'@codemirror/search': 6.x.x
|
||||||
'@codemirror/state': ^6.0.1
|
'@codemirror/state': 6.x.x
|
||||||
'@codemirror/view': ^6.0.3
|
'@codemirror/view': 6.x.x
|
||||||
|
|
||||||
'@replit/codemirror-vscode-keymap@6.0.2':
|
'@replit/codemirror-vscode-keymap@6.0.2':
|
||||||
resolution: {integrity: sha512-j45qTwGxzpsv82lMD/NreGDORFKSctMDVkGRopaP+OrzSzv+pXDQuU3LnFvKpasyjVT0lf+PKG1v2DSCn/vxxg==}
|
resolution: {integrity: sha512-j45qTwGxzpsv82lMD/NreGDORFKSctMDVkGRopaP+OrzSzv+pXDQuU3LnFvKpasyjVT0lf+PKG1v2DSCn/vxxg==}
|
||||||
@@ -5714,6 +5729,7 @@ packages:
|
|||||||
node-domexception@1.0.0:
|
node-domexception@1.0.0:
|
||||||
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
|
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
|
||||||
engines: {node: '>=10.5.0'}
|
engines: {node: '>=10.5.0'}
|
||||||
|
deprecated: Use your platform's native DOMException instead
|
||||||
|
|
||||||
node-fetch-native@1.6.6:
|
node-fetch-native@1.6.6:
|
||||||
resolution: {integrity: sha512-8Mc2HhqPdlIfedsuZoc3yioPuzp6b+L5jRCRY1QzuWZh2EGJVQrGppC6V6cF0bLdbW0+O2YpqCA25aF/1lvipQ==}
|
resolution: {integrity: sha512-8Mc2HhqPdlIfedsuZoc3yioPuzp6b+L5jRCRY1QzuWZh2EGJVQrGppC6V6cF0bLdbW0+O2YpqCA25aF/1lvipQ==}
|
||||||
@@ -6803,6 +6819,7 @@ packages:
|
|||||||
source-map@0.8.0-beta.0:
|
source-map@0.8.0-beta.0:
|
||||||
resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==}
|
resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
|
deprecated: The work that was done in this beta branch won't be included in future versions
|
||||||
|
|
||||||
sourcemap-codec@1.4.8:
|
sourcemap-codec@1.4.8:
|
||||||
resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==}
|
resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==}
|
||||||
@@ -7535,6 +7552,9 @@ packages:
|
|||||||
walk-up-path@3.0.1:
|
walk-up-path@3.0.1:
|
||||||
resolution: {integrity: sha512-9YlCL/ynK3CTlrSRrDxZvUauLzAswPCrsaCgilqFevUYpeEW0/3ScEjaa3kbW/T0ghhkEr7mv+fpjqn1Y1YuTA==}
|
resolution: {integrity: sha512-9YlCL/ynK3CTlrSRrDxZvUauLzAswPCrsaCgilqFevUYpeEW0/3ScEjaa3kbW/T0ghhkEr7mv+fpjqn1Y1YuTA==}
|
||||||
|
|
||||||
|
wav-encoder@1.3.0:
|
||||||
|
resolution: {integrity: sha512-FXJdEu2qDOI+wbVYZpu21CS1vPEg5NaxNskBr4SaULpOJMrLE6xkH8dECa7PiS+ZoeyvP7GllWUAxPN3AvFSEw==}
|
||||||
|
|
||||||
wav@1.0.2:
|
wav@1.0.2:
|
||||||
resolution: {integrity: sha512-viHtz3cDd/Tcr/HbNqzQCofKdF6kWUymH9LGDdskfWFoIy/HJ+RTihgjEcHfnsy1PO4e9B+y4HwgTwMrByquhg==}
|
resolution: {integrity: sha512-viHtz3cDd/Tcr/HbNqzQCofKdF6kWUymH9LGDdskfWFoIy/HJ+RTihgjEcHfnsy1PO4e9B+y4HwgTwMrByquhg==}
|
||||||
|
|
||||||
@@ -7657,6 +7677,7 @@ packages:
|
|||||||
|
|
||||||
workbox-google-analytics@7.0.0:
|
workbox-google-analytics@7.0.0:
|
||||||
resolution: {integrity: sha512-MEYM1JTn/qiC3DbpvP2BVhyIH+dV/5BjHk756u9VbwuAhu0QHyKscTnisQuz21lfRpOwiS9z4XdqeVAKol0bzg==}
|
resolution: {integrity: sha512-MEYM1JTn/qiC3DbpvP2BVhyIH+dV/5BjHk756u9VbwuAhu0QHyKscTnisQuz21lfRpOwiS9z4XdqeVAKol0bzg==}
|
||||||
|
deprecated: It is not compatible with newer versions of GA starting with v4, as long as you are using GAv3 it should be ok, but the package is not longer being maintained
|
||||||
|
|
||||||
workbox-navigation-preload@7.0.0:
|
workbox-navigation-preload@7.0.0:
|
||||||
resolution: {integrity: sha512-juWCSrxo/fiMz3RsvDspeSLGmbgC0U9tKqcUPZBCf35s64wlaLXyn2KdHHXVQrb2cqF7I0Hc9siQalainmnXJA==}
|
resolution: {integrity: sha512-juWCSrxo/fiMz3RsvDspeSLGmbgC0U9tKqcUPZBCf35s64wlaLXyn2KdHHXVQrb2cqF7I0Hc9siQalainmnXJA==}
|
||||||
@@ -9595,7 +9616,7 @@ snapshots:
|
|||||||
'@codemirror/state': 6.5.1
|
'@codemirror/state': 6.5.1
|
||||||
'@codemirror/view': 6.36.2
|
'@codemirror/view': 6.36.2
|
||||||
|
|
||||||
'@replit/codemirror-vim@6.2.1(@codemirror/commands@6.8.0)(@codemirror/language@6.10.8)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)':
|
'@replit/codemirror-vim@6.3.0(@codemirror/commands@6.8.0)(@codemirror/language@6.10.8)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@codemirror/commands': 6.8.0
|
'@codemirror/commands': 6.8.0
|
||||||
'@codemirror/language': 6.10.8
|
'@codemirror/language': 6.10.8
|
||||||
@@ -15955,6 +15976,8 @@ snapshots:
|
|||||||
|
|
||||||
walk-up-path@3.0.1: {}
|
walk-up-path@3.0.1: {}
|
||||||
|
|
||||||
|
wav-encoder@1.3.0: {}
|
||||||
|
|
||||||
wav@1.0.2:
|
wav@1.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
buffer-alloc: 1.2.0
|
buffer-alloc: 1.2.0
|
||||||
|
|||||||
@@ -1847,6 +1847,27 @@ exports[`runs examples > example "chop" example index 0 1`] = `
|
|||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "chorus" example index 0 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/4 | note:d s:sawtooth chorus:0.5 ]",
|
||||||
|
"[ 1/4 → 1/2 | note:d s:sawtooth chorus:0.5 ]",
|
||||||
|
"[ 1/2 → 3/4 | note:a# s:sawtooth chorus:0.5 ]",
|
||||||
|
"[ 3/4 → 1/1 | note:a s:sawtooth chorus:0.5 ]",
|
||||||
|
"[ 1/1 → 5/4 | note:d s:sawtooth chorus:0.5 ]",
|
||||||
|
"[ 5/4 → 3/2 | note:d s:sawtooth chorus:0.5 ]",
|
||||||
|
"[ 3/2 → 7/4 | note:a# s:sawtooth chorus:0.5 ]",
|
||||||
|
"[ 7/4 → 2/1 | note:a s:sawtooth chorus:0.5 ]",
|
||||||
|
"[ 2/1 → 9/4 | note:d s:sawtooth chorus:0.5 ]",
|
||||||
|
"[ 9/4 → 5/2 | note:d s:sawtooth chorus:0.5 ]",
|
||||||
|
"[ 5/2 → 11/4 | note:a# s:sawtooth chorus:0.5 ]",
|
||||||
|
"[ 11/4 → 3/1 | note:a s:sawtooth chorus:0.5 ]",
|
||||||
|
"[ 3/1 → 13/4 | note:d s:sawtooth chorus:0.5 ]",
|
||||||
|
"[ 13/4 → 7/2 | note:d s:sawtooth chorus:0.5 ]",
|
||||||
|
"[ 7/2 → 15/4 | note:a# s:sawtooth chorus:0.5 ]",
|
||||||
|
"[ 15/4 → 4/1 | note:a s:sawtooth chorus:0.5 ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`runs examples > example "chunk" example index 0 1`] = `
|
exports[`runs examples > example "chunk" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/4 | note:A4 ]",
|
"[ 0/1 → 1/4 | note:A4 ]",
|
||||||
@@ -2585,6 +2606,52 @@ exports[`runs examples > example "delayfeedback" example index 0 1`] = `
|
|||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "delayfeedback" example index 0 2`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/1 | s:bd delay:0.25 delayfeedback:0.25 ]",
|
||||||
|
"[ 1/1 → 2/1 | s:bd delay:0.25 delayfeedback:0.5 ]",
|
||||||
|
"[ 2/1 → 3/1 | s:bd delay:0.25 delayfeedback:0.75 ]",
|
||||||
|
"[ 3/1 → 4/1 | s:bd delay:0.25 delayfeedback:1 ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "delayspeed" example index 0 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]",
|
||||||
|
"[ 1/8 → 1/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]",
|
||||||
|
"[ 1/4 → 3/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]",
|
||||||
|
"[ 3/8 → 1/2 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]",
|
||||||
|
"[ 1/2 → 5/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]",
|
||||||
|
"[ 5/8 → 3/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]",
|
||||||
|
"[ 3/4 → 7/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]",
|
||||||
|
"[ 7/8 → 1/1 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:2 ]",
|
||||||
|
"[ 1/1 → 9/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]",
|
||||||
|
"[ 9/8 → 5/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]",
|
||||||
|
"[ 5/4 → 11/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]",
|
||||||
|
"[ 11/8 → 3/2 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]",
|
||||||
|
"[ 3/2 → 13/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]",
|
||||||
|
"[ 13/8 → 7/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]",
|
||||||
|
"[ 7/4 → 15/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]",
|
||||||
|
"[ 15/8 → 2/1 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:0.5 ]",
|
||||||
|
"[ 2/1 → 17/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]",
|
||||||
|
"[ 17/8 → 9/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]",
|
||||||
|
"[ 9/4 → 19/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]",
|
||||||
|
"[ 19/8 → 5/2 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]",
|
||||||
|
"[ 5/2 → 21/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]",
|
||||||
|
"[ 21/8 → 11/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]",
|
||||||
|
"[ 11/4 → 23/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]",
|
||||||
|
"[ 23/8 → 3/1 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-1 ]",
|
||||||
|
"[ 3/1 → 25/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]",
|
||||||
|
"[ 25/8 → 13/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]",
|
||||||
|
"[ 13/4 → 27/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]",
|
||||||
|
"[ 27/8 → 7/2 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]",
|
||||||
|
"[ 7/2 → 29/8 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]",
|
||||||
|
"[ 29/8 → 15/4 | note:d s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]",
|
||||||
|
"[ 15/4 → 31/8 | note:a# s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]",
|
||||||
|
"[ 31/8 → 4/1 | note:a s:sawtooth delay:0.8 delaytime:0.5 delayspeed:-2 ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`runs examples > example "delaysync" example index 0 1`] = `
|
exports[`runs examples > example "delaysync" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/2 | s:bd delay:0.25 delaysync:0.125 ]",
|
"[ 0/1 → 1/2 | s:bd delay:0.25 delaysync:0.125 ]",
|
||||||
@@ -2598,19 +2665,6 @@ exports[`runs examples > example "delaysync" example index 0 1`] = `
|
|||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`runs examples > example "delaytime" example index 0 1`] = `
|
|
||||||
[
|
|
||||||
"[ 0/1 → 1/2 | s:bd delay:0.25 delaytime:0.125 ]",
|
|
||||||
"[ 1/2 → 1/1 | s:bd delay:0.25 delaytime:0.125 ]",
|
|
||||||
"[ 1/1 → 3/2 | s:bd delay:0.25 delaytime:0.25 ]",
|
|
||||||
"[ 3/2 → 2/1 | s:bd delay:0.25 delaytime:0.25 ]",
|
|
||||||
"[ 2/1 → 5/2 | s:bd delay:0.25 delaytime:0.5 ]",
|
|
||||||
"[ 5/2 → 3/1 | s:bd delay:0.25 delaytime:0.5 ]",
|
|
||||||
"[ 3/1 → 7/2 | s:bd delay:0.25 delaytime:1 ]",
|
|
||||||
"[ 7/2 → 4/1 | s:bd delay:0.25 delaytime:1 ]",
|
|
||||||
]
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`runs examples > example "density" example index 0 1`] = `
|
exports[`runs examples > example "density" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/4 | s:crackle density:0.01 ]",
|
"[ 0/1 → 1/4 | s:crackle density:0.01 ]",
|
||||||
@@ -3039,57 +3093,50 @@ exports[`runs examples > example "dry" example index 0 1`] = `
|
|||||||
exports[`runs examples > example "duckattack" example index 0 1`] = `
|
exports[`runs examples > example "duckattack" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
|
"[ 0/1 → 1/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
|
||||||
"[ 0/1 → 1/8 | note:C3 s:sawtooth delay:0.7 orbit:2 ]",
|
|
||||||
"[ 1/8 → 1/4 | note:D3 s:sawtooth delay:0.7 orbit:2 ]",
|
|
||||||
"[ 1/4 → 5/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
|
"[ 1/4 → 5/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
|
||||||
"[ 1/4 → 3/8 | note:Eb3 s:sawtooth delay:0.7 orbit:2 ]",
|
|
||||||
"[ 3/8 → 1/2 | note:F3 s:sawtooth delay:0.7 orbit:2 ]",
|
|
||||||
"[ 1/2 → 9/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
|
"[ 1/2 → 9/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
|
||||||
"[ 1/2 → 5/8 | note:G3 s:sawtooth delay:0.7 orbit:2 ]",
|
|
||||||
"[ 5/8 → 3/4 | note:Ab3 s:sawtooth delay:0.7 orbit:2 ]",
|
|
||||||
"[ 11/16 → 3/4 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
|
"[ 11/16 → 3/4 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
|
||||||
"[ 3/4 → 7/8 | note:Bb3 s:sawtooth delay:0.7 orbit:2 ]",
|
|
||||||
"[ 7/8 → 15/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
|
"[ 7/8 → 15/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
|
||||||
"[ 7/8 → 1/1 | note:C4 s:sawtooth delay:0.7 orbit:2 ]",
|
|
||||||
"[ 1/1 → 17/16 | s:bd n:4 duckorbit:2 duckattack:0 duckdepth:1 ]",
|
"[ 1/1 → 17/16 | s:bd n:4 duckorbit:2 duckattack:0 duckdepth:1 ]",
|
||||||
"[ 1/1 → 9/8 | note:C3 s:sawtooth delay:0.7 orbit:2 ]",
|
|
||||||
"[ 9/8 → 5/4 | note:D3 s:sawtooth delay:0.7 orbit:2 ]",
|
|
||||||
"[ 5/4 → 21/16 | s:bd n:4 duckorbit:2 duckattack:0 duckdepth:1 ]",
|
"[ 5/4 → 21/16 | s:bd n:4 duckorbit:2 duckattack:0 duckdepth:1 ]",
|
||||||
"[ 5/4 → 11/8 | note:Eb3 s:sawtooth delay:0.7 orbit:2 ]",
|
|
||||||
"[ 11/8 → 3/2 | note:F3 s:sawtooth delay:0.7 orbit:2 ]",
|
|
||||||
"[ 3/2 → 25/16 | s:bd n:4 duckorbit:2 duckattack:0 duckdepth:1 ]",
|
"[ 3/2 → 25/16 | s:bd n:4 duckorbit:2 duckattack:0 duckdepth:1 ]",
|
||||||
"[ 3/2 → 13/8 | note:G3 s:sawtooth delay:0.7 orbit:2 ]",
|
|
||||||
"[ 13/8 → 7/4 | note:Ab3 s:sawtooth delay:0.7 orbit:2 ]",
|
|
||||||
"[ 27/16 → 7/4 | s:bd n:4 duckorbit:2 duckattack:0 duckdepth:1 ]",
|
"[ 27/16 → 7/4 | s:bd n:4 duckorbit:2 duckattack:0 duckdepth:1 ]",
|
||||||
"[ 7/4 → 15/8 | note:Bb3 s:sawtooth delay:0.7 orbit:2 ]",
|
|
||||||
"[ 15/8 → 31/16 | s:bd n:4 duckorbit:2 duckattack:0 duckdepth:1 ]",
|
"[ 15/8 → 31/16 | s:bd n:4 duckorbit:2 duckattack:0 duckdepth:1 ]",
|
||||||
"[ 15/8 → 2/1 | note:C4 s:sawtooth delay:0.7 orbit:2 ]",
|
|
||||||
"[ 2/1 → 33/16 | s:bd n:4 duckorbit:2 duckattack:0.4 duckdepth:1 ]",
|
"[ 2/1 → 33/16 | s:bd n:4 duckorbit:2 duckattack:0.4 duckdepth:1 ]",
|
||||||
"[ 2/1 → 17/8 | note:C3 s:sawtooth delay:0.7 orbit:2 ]",
|
|
||||||
"[ 17/8 → 9/4 | note:D3 s:sawtooth delay:0.7 orbit:2 ]",
|
|
||||||
"[ 9/4 → 37/16 | s:bd n:4 duckorbit:2 duckattack:0.4 duckdepth:1 ]",
|
"[ 9/4 → 37/16 | s:bd n:4 duckorbit:2 duckattack:0.4 duckdepth:1 ]",
|
||||||
"[ 9/4 → 19/8 | note:Eb3 s:sawtooth delay:0.7 orbit:2 ]",
|
|
||||||
"[ 19/8 → 5/2 | note:F3 s:sawtooth delay:0.7 orbit:2 ]",
|
|
||||||
"[ 5/2 → 41/16 | s:bd n:4 duckorbit:2 duckattack:0.4 duckdepth:1 ]",
|
"[ 5/2 → 41/16 | s:bd n:4 duckorbit:2 duckattack:0.4 duckdepth:1 ]",
|
||||||
"[ 5/2 → 21/8 | note:G3 s:sawtooth delay:0.7 orbit:2 ]",
|
|
||||||
"[ 21/8 → 11/4 | note:Ab3 s:sawtooth delay:0.7 orbit:2 ]",
|
|
||||||
"[ 43/16 → 11/4 | s:bd n:4 duckorbit:2 duckattack:0.4 duckdepth:1 ]",
|
"[ 43/16 → 11/4 | s:bd n:4 duckorbit:2 duckattack:0.4 duckdepth:1 ]",
|
||||||
"[ 11/4 → 23/8 | note:Bb3 s:sawtooth delay:0.7 orbit:2 ]",
|
|
||||||
"[ 23/8 → 47/16 | s:bd n:4 duckorbit:2 duckattack:0.4 duckdepth:1 ]",
|
"[ 23/8 → 47/16 | s:bd n:4 duckorbit:2 duckattack:0.4 duckdepth:1 ]",
|
||||||
"[ 23/8 → 3/1 | note:C4 s:sawtooth delay:0.7 orbit:2 ]",
|
|
||||||
"[ 3/1 → 49/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
|
"[ 3/1 → 49/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
|
||||||
"[ 3/1 → 25/8 | note:C3 s:sawtooth delay:0.7 orbit:2 ]",
|
|
||||||
"[ 25/8 → 13/4 | note:D3 s:sawtooth delay:0.7 orbit:2 ]",
|
|
||||||
"[ 13/4 → 53/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
|
"[ 13/4 → 53/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
|
||||||
"[ 13/4 → 27/8 | note:Eb3 s:sawtooth delay:0.7 orbit:2 ]",
|
|
||||||
"[ 27/8 → 7/2 | note:F3 s:sawtooth delay:0.7 orbit:2 ]",
|
|
||||||
"[ 7/2 → 57/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
|
"[ 7/2 → 57/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
|
||||||
"[ 7/2 → 29/8 | note:G3 s:sawtooth delay:0.7 orbit:2 ]",
|
|
||||||
"[ 29/8 → 15/4 | note:Ab3 s:sawtooth delay:0.7 orbit:2 ]",
|
|
||||||
"[ 59/16 → 15/4 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
|
"[ 59/16 → 15/4 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
|
||||||
"[ 15/4 → 31/8 | note:Bb3 s:sawtooth delay:0.7 orbit:2 ]",
|
|
||||||
"[ 31/8 → 63/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
|
"[ 31/8 → 63/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
|
||||||
"[ 31/8 → 4/1 | note:C4 s:sawtooth delay:0.7 orbit:2 ]",
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "duckattack" example index 1 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]",
|
||||||
|
"[ 1/4 → 5/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]",
|
||||||
|
"[ 1/2 → 9/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]",
|
||||||
|
"[ 11/16 → 3/4 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]",
|
||||||
|
"[ 7/8 → 15/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]",
|
||||||
|
"[ 1/1 → 17/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]",
|
||||||
|
"[ 5/4 → 21/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]",
|
||||||
|
"[ 3/2 → 25/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]",
|
||||||
|
"[ 27/16 → 7/4 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]",
|
||||||
|
"[ 15/8 → 31/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]",
|
||||||
|
"[ 2/1 → 33/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]",
|
||||||
|
"[ 9/4 → 37/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]",
|
||||||
|
"[ 5/2 → 41/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]",
|
||||||
|
"[ 43/16 → 11/4 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]",
|
||||||
|
"[ 23/8 → 47/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]",
|
||||||
|
"[ 3/1 → 49/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]",
|
||||||
|
"[ 13/4 → 53/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]",
|
||||||
|
"[ 7/2 → 57/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]",
|
||||||
|
"[ 59/16 → 15/4 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]",
|
||||||
|
"[ 31/8 → 63/16 | s:bd n:4 duckorbit:[2 5] duckattack:[0.4 0.1] ]",
|
||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -3150,6 +3197,94 @@ exports[`runs examples > example "duckdepth" example index 0 1`] = `
|
|||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "duckdepth" example index 1 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]",
|
||||||
|
"[ 1/4 → 5/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]",
|
||||||
|
"[ 1/2 → 9/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]",
|
||||||
|
"[ 11/16 → 3/4 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]",
|
||||||
|
"[ 7/8 → 15/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]",
|
||||||
|
"[ 1/1 → 17/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]",
|
||||||
|
"[ 5/4 → 21/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]",
|
||||||
|
"[ 3/2 → 25/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]",
|
||||||
|
"[ 27/16 → 7/4 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]",
|
||||||
|
"[ 15/8 → 31/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]",
|
||||||
|
"[ 2/1 → 33/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]",
|
||||||
|
"[ 9/4 → 37/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]",
|
||||||
|
"[ 5/2 → 41/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]",
|
||||||
|
"[ 43/16 → 11/4 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]",
|
||||||
|
"[ 23/8 → 47/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]",
|
||||||
|
"[ 3/1 → 49/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]",
|
||||||
|
"[ 13/4 → 53/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]",
|
||||||
|
"[ 7/2 → 57/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]",
|
||||||
|
"[ 59/16 → 15/4 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]",
|
||||||
|
"[ 31/8 → 63/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:[1 0.5] ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "duckonset" example index 0 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]",
|
||||||
|
"[ 1/4 → 1/2 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]",
|
||||||
|
"[ 1/2 → 3/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]",
|
||||||
|
"[ 3/4 → 1/1 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]",
|
||||||
|
"[ 1/1 → 5/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]",
|
||||||
|
"[ 5/4 → 3/2 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]",
|
||||||
|
"[ 3/2 → 7/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]",
|
||||||
|
"[ 7/4 → 2/1 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]",
|
||||||
|
"[ 2/1 → 9/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]",
|
||||||
|
"[ 9/4 → 5/2 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]",
|
||||||
|
"[ 5/2 → 11/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]",
|
||||||
|
"[ 11/4 → 3/1 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]",
|
||||||
|
"[ 3/1 → 13/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]",
|
||||||
|
"[ 13/4 → 7/2 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]",
|
||||||
|
"[ 7/2 → 15/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]",
|
||||||
|
"[ 15/4 → 4/1 | s:bd duckorbit:2 duckattack:0.3 duckonset:0 postgain:0 ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "duckonset" example index 1 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]",
|
||||||
|
"[ 1/4 → 1/2 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]",
|
||||||
|
"[ 1/2 → 3/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]",
|
||||||
|
"[ 3/4 → 1/1 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]",
|
||||||
|
"[ 1/1 → 5/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]",
|
||||||
|
"[ 5/4 → 3/2 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]",
|
||||||
|
"[ 3/2 → 7/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]",
|
||||||
|
"[ 7/4 → 2/1 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]",
|
||||||
|
"[ 2/1 → 9/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]",
|
||||||
|
"[ 9/4 → 5/2 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]",
|
||||||
|
"[ 5/2 → 11/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]",
|
||||||
|
"[ 11/4 → 3/1 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]",
|
||||||
|
"[ 3/1 → 13/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]",
|
||||||
|
"[ 13/4 → 7/2 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]",
|
||||||
|
"[ 7/2 → 15/4 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]",
|
||||||
|
"[ 15/4 → 4/1 | s:bd duckorbit:2 duckattack:0.3 duckonset:0.01 postgain:0 ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "duckonset" example index 2 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/4 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]",
|
||||||
|
"[ 1/4 → 1/2 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]",
|
||||||
|
"[ 1/2 → 3/4 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]",
|
||||||
|
"[ 3/4 → 1/1 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]",
|
||||||
|
"[ 1/1 → 5/4 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]",
|
||||||
|
"[ 5/4 → 3/2 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]",
|
||||||
|
"[ 3/2 → 7/4 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]",
|
||||||
|
"[ 7/4 → 2/1 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]",
|
||||||
|
"[ 2/1 → 9/4 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]",
|
||||||
|
"[ 9/4 → 5/2 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]",
|
||||||
|
"[ 5/2 → 11/4 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]",
|
||||||
|
"[ 11/4 → 3/1 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]",
|
||||||
|
"[ 3/1 → 13/4 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]",
|
||||||
|
"[ 13/4 → 7/2 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]",
|
||||||
|
"[ 7/2 → 15/4 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]",
|
||||||
|
"[ 15/4 → 4/1 | s:bd bank:tr909 duckorbit:[4 7] duckonset:[0.3 0.003] duckattack:0.25 ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`runs examples > example "duckorbit" example index 0 1`] = `
|
exports[`runs examples > example "duckorbit" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
|
"[ 0/1 → 1/16 | s:bd n:4 duckorbit:2 duckattack:0.2 duckdepth:1 ]",
|
||||||
@@ -3175,6 +3310,31 @@ exports[`runs examples > example "duckorbit" example index 0 1`] = `
|
|||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "duckorbit" example index 1 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]",
|
||||||
|
"[ 1/4 → 5/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]",
|
||||||
|
"[ 1/2 → 9/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]",
|
||||||
|
"[ 11/16 → 3/4 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]",
|
||||||
|
"[ 7/8 → 15/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]",
|
||||||
|
"[ 1/1 → 17/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]",
|
||||||
|
"[ 5/4 → 21/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]",
|
||||||
|
"[ 3/2 → 25/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]",
|
||||||
|
"[ 27/16 → 7/4 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]",
|
||||||
|
"[ 15/8 → 31/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]",
|
||||||
|
"[ 2/1 → 33/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]",
|
||||||
|
"[ 9/4 → 37/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]",
|
||||||
|
"[ 5/2 → 41/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]",
|
||||||
|
"[ 43/16 → 11/4 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]",
|
||||||
|
"[ 23/8 → 47/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]",
|
||||||
|
"[ 3/1 → 49/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]",
|
||||||
|
"[ 13/4 → 53/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]",
|
||||||
|
"[ 7/2 → 57/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]",
|
||||||
|
"[ 59/16 → 15/4 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]",
|
||||||
|
"[ 31/8 → 63/16 | s:bd n:4 duckorbit:[2 3] duckattack:0.2 duckdepth:1 ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`runs examples > example "duration" example index 0 1`] = `
|
exports[`runs examples > example "duration" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/4 | note:c s:piano duration:0.5 ]",
|
"[ 0/1 → 1/4 | note:c s:piano duration:0.5 ]",
|
||||||
@@ -3590,6 +3750,8 @@ exports[`runs examples > example "fast" example index 0 1`] = `
|
|||||||
|
|
||||||
exports[`runs examples > example "fastChunk" example index 0 1`] = `
|
exports[`runs examples > example "fastChunk" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
|
"[ 0/1 → 1/4 | color:red note:0 ]",
|
||||||
|
"[ 1/4 → 1/2 | color:red note:1 ]",
|
||||||
"[ 1/2 → 3/4 | note:E2 ]",
|
"[ 1/2 → 3/4 | note:E2 ]",
|
||||||
"[ 3/4 → 1/1 | note:F2 ]",
|
"[ 3/4 → 1/1 | note:F2 ]",
|
||||||
"[ 1/1 → 5/4 | note:G2 ]",
|
"[ 1/1 → 5/4 | note:G2 ]",
|
||||||
@@ -3598,6 +3760,8 @@ exports[`runs examples > example "fastChunk" example index 0 1`] = `
|
|||||||
"[ 7/4 → 2/1 | note:C3 ]",
|
"[ 7/4 → 2/1 | note:C3 ]",
|
||||||
"[ 2/1 → 9/4 | note:D3 ]",
|
"[ 2/1 → 9/4 | note:D3 ]",
|
||||||
"[ 9/4 → 5/2 | note:D2 ]",
|
"[ 9/4 → 5/2 | note:D2 ]",
|
||||||
|
"[ 5/2 → 11/4 | color:red note:2 ]",
|
||||||
|
"[ 11/4 → 3/1 | color:red note:3 ]",
|
||||||
"[ 3/1 → 13/4 | note:G2 ]",
|
"[ 3/1 → 13/4 | note:G2 ]",
|
||||||
"[ 13/4 → 7/2 | note:A2 ]",
|
"[ 13/4 → 7/2 | note:A2 ]",
|
||||||
"[ 7/2 → 15/4 | note:B2 ]",
|
"[ 7/2 → 15/4 | note:B2 ]",
|
||||||
@@ -4875,6 +5039,43 @@ exports[`runs examples > example "irand" example index 0 1`] = `
|
|||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "irbegin" example index 0 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/8 | s:brk speed:0.5 unit:c begin:0 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]",
|
||||||
|
"[ 1/8 → 1/4 | s:brk speed:0.5 unit:c begin:0.625 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]",
|
||||||
|
"[ 1/4 → 3/8 | s:brk speed:0.5 unit:c begin:0.3125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]",
|
||||||
|
"[ 3/8 → 1/2 | s:brk speed:0.5 unit:c begin:0.375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]",
|
||||||
|
"[ 1/2 → 5/8 | s:brk speed:0.5 unit:c begin:0.25 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]",
|
||||||
|
"[ 5/8 → 3/4 | s:brk speed:0.5 unit:c begin:0.125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]",
|
||||||
|
"[ 3/4 → 7/8 | s:brk speed:0.5 unit:c begin:0.1875 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]",
|
||||||
|
"[ 7/8 → 1/1 | s:brk speed:0.5 unit:c begin:0.375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]",
|
||||||
|
"[ 1/1 → 9/8 | s:brk speed:0.5 unit:c begin:0.5 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]",
|
||||||
|
"[ 9/8 → 5/4 | s:brk speed:0.5 unit:c begin:0.625 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]",
|
||||||
|
"[ 5/4 → 11/8 | s:brk speed:0.5 unit:c begin:0.6875 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]",
|
||||||
|
"[ 11/8 → 3/2 | s:brk speed:0.5 unit:c begin:0.125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]",
|
||||||
|
"[ 3/2 → 13/8 | s:brk speed:0.5 unit:c begin:0.5625 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]",
|
||||||
|
"[ 13/8 → 7/4 | s:brk speed:0.5 unit:c begin:0.4375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]",
|
||||||
|
"[ 7/4 → 15/8 | s:brk speed:0.5 unit:c begin:0.1875 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]",
|
||||||
|
"[ 15/8 → 2/1 | s:brk speed:0.5 unit:c begin:0.5625 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0 roomsize:0.6 ]",
|
||||||
|
"[ 2/1 → 17/8 | s:brk speed:0.5 unit:c begin:0.9375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]",
|
||||||
|
"[ 17/8 → 9/4 | s:brk speed:0.5 unit:c begin:0.875 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]",
|
||||||
|
"[ 9/4 → 19/8 | s:brk speed:0.5 unit:c begin:0.3125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]",
|
||||||
|
"[ 19/8 → 5/2 | s:brk speed:0.5 unit:c begin:0.8125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]",
|
||||||
|
"[ 5/2 → 21/8 | s:brk speed:0.5 unit:c begin:0.4375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]",
|
||||||
|
"[ 21/8 → 11/4 | s:brk speed:0.5 unit:c begin:0.125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]",
|
||||||
|
"[ 11/4 → 23/8 | s:brk speed:0.5 unit:c begin:0.625 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]",
|
||||||
|
"[ 23/8 → 3/1 | s:brk speed:0.5 unit:c begin:0 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]",
|
||||||
|
"[ 3/1 → 25/8 | s:brk speed:0.5 unit:c begin:0.1875 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]",
|
||||||
|
"[ 25/8 → 13/4 | s:brk speed:0.5 unit:c begin:0.3125 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]",
|
||||||
|
"[ 13/4 → 27/8 | s:brk speed:0.5 unit:c begin:0.9375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]",
|
||||||
|
"[ 27/8 → 7/2 | s:brk speed:0.5 unit:c begin:0.375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]",
|
||||||
|
"[ 7/2 → 29/8 | s:brk speed:0.5 unit:c begin:0.375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]",
|
||||||
|
"[ 29/8 → 15/4 | s:brk speed:0.5 unit:c begin:0.9375 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]",
|
||||||
|
"[ 15/4 → 31/8 | s:brk speed:0.5 unit:c begin:0.75 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]",
|
||||||
|
"[ 31/8 → 4/1 | s:brk speed:0.5 unit:c begin:0.6875 clip:1 ir:swpad i:4 room:0.65 irspeed:-2 irbegin:0.5 roomsize:0.6 ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`runs examples > example "iresponse" example index 0 1`] = `
|
exports[`runs examples > example "iresponse" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/4 | s:bd room:0.8 ir:shaker_large i:0 ]",
|
"[ 0/1 → 1/4 | s:bd room:0.8 ir:shaker_large i:0 ]",
|
||||||
@@ -4896,6 +5097,43 @@ exports[`runs examples > example "iresponse" example index 0 1`] = `
|
|||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "irspeed" example index 0 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/8 | s:brk speed:0.5 unit:c begin:0 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]",
|
||||||
|
"[ 1/8 → 1/4 | s:brk speed:0.5 unit:c begin:0.625 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]",
|
||||||
|
"[ 1/4 → 3/8 | s:brk speed:0.5 unit:c begin:0.3125 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]",
|
||||||
|
"[ 3/8 → 1/2 | s:brk speed:0.5 unit:c begin:0.375 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]",
|
||||||
|
"[ 1/2 → 5/8 | s:brk speed:0.5 unit:c begin:0.25 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]",
|
||||||
|
"[ 5/8 → 3/4 | s:brk speed:0.5 unit:c begin:0.125 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]",
|
||||||
|
"[ 3/4 → 7/8 | s:brk speed:0.5 unit:c begin:0.1875 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]",
|
||||||
|
"[ 7/8 → 1/1 | s:brk speed:0.5 unit:c begin:0.375 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]",
|
||||||
|
"[ 1/1 → 9/8 | s:brk speed:0.5 unit:c begin:0.5 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]",
|
||||||
|
"[ 9/8 → 5/4 | s:brk speed:0.5 unit:c begin:0.625 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]",
|
||||||
|
"[ 5/4 → 11/8 | s:brk speed:0.5 unit:c begin:0.6875 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]",
|
||||||
|
"[ 11/8 → 3/2 | s:brk speed:0.5 unit:c begin:0.125 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]",
|
||||||
|
"[ 3/2 → 13/8 | s:brk speed:0.5 unit:c begin:0.5625 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]",
|
||||||
|
"[ 13/8 → 7/4 | s:brk speed:0.5 unit:c begin:0.4375 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]",
|
||||||
|
"[ 7/4 → 15/8 | s:brk speed:0.5 unit:c begin:0.1875 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]",
|
||||||
|
"[ 15/8 → 2/1 | s:brk speed:0.5 unit:c begin:0.5625 clip:1 ir:swpad i:4 room:0.2 irspeed:2 irbegin:0.5 roomsize:0.5 ]",
|
||||||
|
"[ 2/1 → 17/8 | s:brk speed:0.5 unit:c begin:0.9375 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]",
|
||||||
|
"[ 17/8 → 9/4 | s:brk speed:0.5 unit:c begin:0.875 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]",
|
||||||
|
"[ 9/4 → 19/8 | s:brk speed:0.5 unit:c begin:0.3125 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]",
|
||||||
|
"[ 19/8 → 5/2 | s:brk speed:0.5 unit:c begin:0.8125 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]",
|
||||||
|
"[ 5/2 → 21/8 | s:brk speed:0.5 unit:c begin:0.4375 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]",
|
||||||
|
"[ 21/8 → 11/4 | s:brk speed:0.5 unit:c begin:0.125 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]",
|
||||||
|
"[ 11/4 → 23/8 | s:brk speed:0.5 unit:c begin:0.625 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]",
|
||||||
|
"[ 23/8 → 3/1 | s:brk speed:0.5 unit:c begin:0 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]",
|
||||||
|
"[ 3/1 → 25/8 | s:brk speed:0.5 unit:c begin:0.1875 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]",
|
||||||
|
"[ 25/8 → 13/4 | s:brk speed:0.5 unit:c begin:0.3125 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]",
|
||||||
|
"[ 13/4 → 27/8 | s:brk speed:0.5 unit:c begin:0.9375 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]",
|
||||||
|
"[ 27/8 → 7/2 | s:brk speed:0.5 unit:c begin:0.375 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]",
|
||||||
|
"[ 7/2 → 29/8 | s:brk speed:0.5 unit:c begin:0.375 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]",
|
||||||
|
"[ 29/8 → 15/4 | s:brk speed:0.5 unit:c begin:0.9375 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]",
|
||||||
|
"[ 15/4 → 31/8 | s:brk speed:0.5 unit:c begin:0.75 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]",
|
||||||
|
"[ 31/8 → 4/1 | s:brk speed:0.5 unit:c begin:0.6875 clip:1 ir:swpad i:4 room:0.2 irspeed:1 irbegin:0.5 roomsize:0.5 ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`runs examples > example "isaw" example index 0 1`] = `
|
exports[`runs examples > example "isaw" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/8 | note:c3 clip:1 ]",
|
"[ 0/1 → 1/8 | note:c3 clip:1 ]",
|
||||||
@@ -6432,6 +6670,27 @@ exports[`runs examples > example "note" example index 2 1`] = `
|
|||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "note" example index 3 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/4 | note:fbb1 s:saw ]",
|
||||||
|
"[ 1/4 → 1/2 | note:a#0 s:saw ]",
|
||||||
|
"[ 1/2 → 3/4 | note:cbbb-1 s:saw ]",
|
||||||
|
"[ 3/4 → 1/1 | note:e##-2 s:saw ]",
|
||||||
|
"[ 1/1 → 5/4 | note:fbb1 s:saw ]",
|
||||||
|
"[ 5/4 → 3/2 | note:a#0 s:saw ]",
|
||||||
|
"[ 3/2 → 7/4 | note:cbbb-1 s:saw ]",
|
||||||
|
"[ 7/4 → 2/1 | note:e##-2 s:saw ]",
|
||||||
|
"[ 2/1 → 9/4 | note:fbb1 s:saw ]",
|
||||||
|
"[ 9/4 → 5/2 | note:a#0 s:saw ]",
|
||||||
|
"[ 5/2 → 11/4 | note:cbbb-1 s:saw ]",
|
||||||
|
"[ 11/4 → 3/1 | note:e##-2 s:saw ]",
|
||||||
|
"[ 3/1 → 13/4 | note:fbb1 s:saw ]",
|
||||||
|
"[ 13/4 → 7/2 | note:a#0 s:saw ]",
|
||||||
|
"[ 7/2 → 15/4 | note:cbbb-1 s:saw ]",
|
||||||
|
"[ 15/4 → 4/1 | note:e##-2 s:saw ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`runs examples > example "nrpnn" example index 0 1`] = `
|
exports[`runs examples > example "nrpnn" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/1 | note:c4 nrpnn:[1 8] nrpv:123 midichan:1 ]",
|
"[ 0/1 → 1/1 | note:c4 nrpnn:[1 8] nrpv:123 midichan:1 ]",
|
||||||
@@ -7237,6 +7496,64 @@ exports[`runs examples > example "ply" example index 0 1`] = `
|
|||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "plyForEach" example index 0 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/4 | note:C3 ]",
|
||||||
|
"[ 1/4 → 1/2 | note:Eb3 ]",
|
||||||
|
"[ 1/2 → 3/4 | note:G3 ]",
|
||||||
|
"[ 3/4 → 1/1 | note:Bb3 ]",
|
||||||
|
"[ 1/1 → 9/8 | note:Eb3 ]",
|
||||||
|
"[ 9/8 → 5/4 | note:G3 ]",
|
||||||
|
"[ 5/4 → 11/8 | note:Bb3 ]",
|
||||||
|
"[ 11/8 → 3/2 | note:D4 ]",
|
||||||
|
"[ 3/2 → 13/8 | note:G3 ]",
|
||||||
|
"[ 13/8 → 7/4 | note:Bb3 ]",
|
||||||
|
"[ 7/4 → 15/8 | note:D4 ]",
|
||||||
|
"[ 15/8 → 2/1 | note:F4 ]",
|
||||||
|
"[ 2/1 → 9/4 | note:C3 ]",
|
||||||
|
"[ 9/4 → 5/2 | note:Eb3 ]",
|
||||||
|
"[ 5/2 → 11/4 | note:G3 ]",
|
||||||
|
"[ 11/4 → 3/1 | note:Bb3 ]",
|
||||||
|
"[ 3/1 → 25/8 | note:Eb3 ]",
|
||||||
|
"[ 25/8 → 13/4 | note:G3 ]",
|
||||||
|
"[ 13/4 → 27/8 | note:Bb3 ]",
|
||||||
|
"[ 27/8 → 7/2 | note:D4 ]",
|
||||||
|
"[ 7/2 → 29/8 | note:G3 ]",
|
||||||
|
"[ 29/8 → 15/4 | note:Bb3 ]",
|
||||||
|
"[ 15/4 → 31/8 | note:D4 ]",
|
||||||
|
"[ 31/8 → 4/1 | note:F4 ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "plyWith" example index 0 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/4 | note:C3 ]",
|
||||||
|
"[ 1/4 → 1/2 | note:Eb3 ]",
|
||||||
|
"[ 1/2 → 3/4 | note:G3 ]",
|
||||||
|
"[ 3/4 → 1/1 | note:Bb3 ]",
|
||||||
|
"[ 1/1 → 9/8 | note:Eb3 ]",
|
||||||
|
"[ 9/8 → 5/4 | note:G3 ]",
|
||||||
|
"[ 5/4 → 11/8 | note:Bb3 ]",
|
||||||
|
"[ 11/8 → 3/2 | note:D4 ]",
|
||||||
|
"[ 3/2 → 13/8 | note:G3 ]",
|
||||||
|
"[ 13/8 → 7/4 | note:Bb3 ]",
|
||||||
|
"[ 7/4 → 15/8 | note:D4 ]",
|
||||||
|
"[ 15/8 → 2/1 | note:F4 ]",
|
||||||
|
"[ 2/1 → 9/4 | note:C3 ]",
|
||||||
|
"[ 9/4 → 5/2 | note:Eb3 ]",
|
||||||
|
"[ 5/2 → 11/4 | note:G3 ]",
|
||||||
|
"[ 11/4 → 3/1 | note:Bb3 ]",
|
||||||
|
"[ 3/1 → 25/8 | note:Eb3 ]",
|
||||||
|
"[ 25/8 → 13/4 | note:G3 ]",
|
||||||
|
"[ 13/4 → 27/8 | note:Bb3 ]",
|
||||||
|
"[ 27/8 → 7/2 | note:D4 ]",
|
||||||
|
"[ 7/2 → 29/8 | note:G3 ]",
|
||||||
|
"[ 29/8 → 15/4 | note:Bb3 ]",
|
||||||
|
"[ 15/4 → 31/8 | note:D4 ]",
|
||||||
|
"[ 31/8 → 4/1 | note:F4 ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`runs examples > example "polymeter" example index 0 1`] = `
|
exports[`runs examples > example "polymeter" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/6 | note:c ]",
|
"[ 0/1 → 1/6 | note:c ]",
|
||||||
@@ -8662,6 +8979,108 @@ exports[`runs examples > example "scale" example index 2 1`] = `
|
|||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "scale" example index 3 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/4 | note:C3 s:piano ]",
|
||||||
|
"[ 0/1 → 1/4 | note:B3 s:piano ]",
|
||||||
|
"[ 1/4 → 3/8 | note:Gb2 s:piano ]",
|
||||||
|
"[ 3/8 → 1/2 | note:F2 s:piano ]",
|
||||||
|
"[ 1/2 → 3/4 | note:A2 s:piano ]",
|
||||||
|
"[ 1/2 → 3/4 | note:D4 s:piano ]",
|
||||||
|
"[ 3/4 → 1/1 | note:G3 s:piano ]",
|
||||||
|
"[ 1/1 → 5/4 | note:C3 s:piano ]",
|
||||||
|
"[ 1/1 → 5/4 | note:C4 s:piano ]",
|
||||||
|
"[ 5/4 → 11/8 | note:Gb2 s:piano ]",
|
||||||
|
"[ 11/8 → 3/2 | note:E2 s:piano ]",
|
||||||
|
"[ 3/2 → 7/4 | note:A2 s:piano ]",
|
||||||
|
"[ 3/2 → 7/4 | note:Eb4 s:piano ]",
|
||||||
|
"[ 7/4 → 2/1 | note:F#3 s:piano ]",
|
||||||
|
"[ 2/1 → 9/4 | note:C3 s:piano ]",
|
||||||
|
"[ 2/1 → 9/4 | note:B3 s:piano ]",
|
||||||
|
"[ 9/4 → 19/8 | note:Gb2 s:piano ]",
|
||||||
|
"[ 19/8 → 5/2 | note:F2 s:piano ]",
|
||||||
|
"[ 5/2 → 11/4 | note:Ab2 s:piano ]",
|
||||||
|
"[ 5/2 → 11/4 | note:D4 s:piano ]",
|
||||||
|
"[ 11/4 → 3/1 | note:G3 s:piano ]",
|
||||||
|
"[ 3/1 → 13/4 | note:C3 s:piano ]",
|
||||||
|
"[ 3/1 → 13/4 | note:C4 s:piano ]",
|
||||||
|
"[ 13/4 → 27/8 | note:Gb2 s:piano ]",
|
||||||
|
"[ 27/8 → 7/2 | note:E2 s:piano ]",
|
||||||
|
"[ 7/2 → 15/4 | note:Ab2 s:piano ]",
|
||||||
|
"[ 7/2 → 15/4 | note:Eb4 s:piano ]",
|
||||||
|
"[ 15/4 → 4/1 | note:F#3 s:piano ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "scale" example index 4 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/16 | note:Gb1 ]",
|
||||||
|
"[ 1/16 → 1/8 | note:Gb1 ]",
|
||||||
|
"[ 1/8 → 3/16 | note:Gb1 ]",
|
||||||
|
"[ 3/16 → 1/4 | note:Gb1 ]",
|
||||||
|
"[ 1/4 → 5/16 | note:Gb1 ]",
|
||||||
|
"[ 5/16 → 3/8 | note:Gb1 ]",
|
||||||
|
"[ 3/8 → 7/16 | note:Gb1 ]",
|
||||||
|
"[ 7/16 → 1/2 | note:Gb1 ]",
|
||||||
|
"[ 1/2 → 9/16 | note:Gb1 ]",
|
||||||
|
"[ 9/16 → 5/8 | note:Gb1 ]",
|
||||||
|
"[ 5/8 → 11/16 | note:Gb1 ]",
|
||||||
|
"[ 11/16 → 3/4 | note:Gb1 ]",
|
||||||
|
"[ 3/4 → 13/16 | note:Gb1 ]",
|
||||||
|
"[ 13/16 → 7/8 | note:Gb1 ]",
|
||||||
|
"[ 7/8 → 15/16 | note:Gb1 ]",
|
||||||
|
"[ 15/16 → 1/1 | note:Gb1 ]",
|
||||||
|
"[ 1/1 → 17/16 | note:Cb3 ]",
|
||||||
|
"[ 17/16 → 9/8 | note:Cb3 ]",
|
||||||
|
"[ 9/8 → 19/16 | note:Cb3 ]",
|
||||||
|
"[ 19/16 → 5/4 | note:Cb3 ]",
|
||||||
|
"[ 5/4 → 21/16 | note:Cb3 ]",
|
||||||
|
"[ 21/16 → 11/8 | note:Cb3 ]",
|
||||||
|
"[ 11/8 → 23/16 | note:Cb3 ]",
|
||||||
|
"[ 23/16 → 3/2 | note:Cb3 ]",
|
||||||
|
"[ 3/2 → 25/16 | note:Cb3 ]",
|
||||||
|
"[ 25/16 → 13/8 | note:Cb3 ]",
|
||||||
|
"[ 13/8 → 27/16 | note:Cb3 ]",
|
||||||
|
"[ 27/16 → 7/4 | note:Cb3 ]",
|
||||||
|
"[ 7/4 → 29/16 | note:Cb3 ]",
|
||||||
|
"[ 29/16 → 15/8 | note:Cb3 ]",
|
||||||
|
"[ 15/8 → 31/16 | note:Cb3 ]",
|
||||||
|
"[ 31/16 → 2/1 | note:Cb3 ]",
|
||||||
|
"[ 2/1 → 33/16 | note:Eb4 ]",
|
||||||
|
"[ 33/16 → 17/8 | note:Eb4 ]",
|
||||||
|
"[ 17/8 → 35/16 | note:Eb4 ]",
|
||||||
|
"[ 35/16 → 9/4 | note:Eb4 ]",
|
||||||
|
"[ 9/4 → 37/16 | note:Eb4 ]",
|
||||||
|
"[ 37/16 → 19/8 | note:Eb4 ]",
|
||||||
|
"[ 19/8 → 39/16 | note:Eb4 ]",
|
||||||
|
"[ 39/16 → 5/2 | note:Eb4 ]",
|
||||||
|
"[ 5/2 → 41/16 | note:Eb4 ]",
|
||||||
|
"[ 41/16 → 21/8 | note:Eb4 ]",
|
||||||
|
"[ 21/8 → 43/16 | note:Eb4 ]",
|
||||||
|
"[ 43/16 → 11/4 | note:Eb4 ]",
|
||||||
|
"[ 11/4 → 45/16 | note:Eb4 ]",
|
||||||
|
"[ 45/16 → 23/8 | note:Eb4 ]",
|
||||||
|
"[ 23/8 → 47/16 | note:Eb4 ]",
|
||||||
|
"[ 47/16 → 3/1 | note:Eb4 ]",
|
||||||
|
"[ 3/1 → 49/16 | note:Db2 ]",
|
||||||
|
"[ 49/16 → 25/8 | note:Db2 ]",
|
||||||
|
"[ 25/8 → 51/16 | note:Db2 ]",
|
||||||
|
"[ 51/16 → 13/4 | note:Db2 ]",
|
||||||
|
"[ 13/4 → 53/16 | note:Db2 ]",
|
||||||
|
"[ 53/16 → 27/8 | note:Db2 ]",
|
||||||
|
"[ 27/8 → 55/16 | note:Db2 ]",
|
||||||
|
"[ 55/16 → 7/2 | note:Db2 ]",
|
||||||
|
"[ 7/2 → 57/16 | note:Db2 ]",
|
||||||
|
"[ 57/16 → 29/8 | note:Db2 ]",
|
||||||
|
"[ 29/8 → 59/16 | note:Db2 ]",
|
||||||
|
"[ 59/16 → 15/4 | note:Db2 ]",
|
||||||
|
"[ 15/4 → 61/16 | note:Db2 ]",
|
||||||
|
"[ 61/16 → 31/8 | note:Db2 ]",
|
||||||
|
"[ 31/8 → 63/16 | note:Db2 ]",
|
||||||
|
"[ 63/16 → 4/1 | note:Db2 ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`runs examples > example "scaleTranspose" example index 0 1`] = `
|
exports[`runs examples > example "scaleTranspose" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/2 | note:C3 ]",
|
"[ 0/1 → 1/2 | note:C3 ]",
|
||||||
|
|||||||
@@ -8,8 +8,7 @@
|
|||||||
"start": "astro dev",
|
"start": "astro dev",
|
||||||
"build": "astro build",
|
"build": "astro build",
|
||||||
"preview": "astro preview --port 3009 --host 0.0.0.0",
|
"preview": "astro preview --port 3009 --host 0.0.0.0",
|
||||||
"astro": "astro",
|
"astro": "astro"
|
||||||
"postinstall": "cp node_modules/hs2js/dist/tree-sitter.wasm public && cp node_modules/hs2js/dist/tree-sitter-haskell.wasm public"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@algolia/client-search": "^5.20.0",
|
"@algolia/client-search": "^5.20.0",
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
The FontStruction “TIC-80 wide font”
|
||||||
|
(https://fontstruct.com/fontstructions/show/1388526) by “nesbox” is licensed
|
||||||
|
under a Creative Commons CC0 Public Domain Dedication license
|
||||||
|
(http://creativecommons.org/publicdomain/zero/1.0/).
|
||||||
|
[ancestry]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
The font file in this archive was created using Fontstruct the free, online
|
||||||
|
font-building tool.
|
||||||
|
This font was created by “nesbox”.
|
||||||
|
This font has a homepage where this archive and other versions may be found:
|
||||||
|
https://fontstruct.com/fontstructions/show/1388526
|
||||||
|
[ancestry]
|
||||||
|
Try Fontstruct at https://fontstruct.com
|
||||||
|
It’s easy and it’s fun.
|
||||||
|
|
||||||
|
Fontstruct is copyright ©2017-2025 Rob Meek
|
||||||
|
|
||||||
|
LEGAL NOTICE:
|
||||||
|
In using this font you must comply with the licensing terms described in the
|
||||||
|
file “license.txt” included with this archive.
|
||||||
|
If you redistribute the font file in this archive, it must be accompanied by all
|
||||||
|
the other files from this archive, including this one.
|
||||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
@@ -103,6 +103,7 @@ export const SIDEBAR: Sidebar = {
|
|||||||
Understand: [
|
Understand: [
|
||||||
{ text: 'Coding syntax', link: 'learn/code' },
|
{ text: 'Coding syntax', link: 'learn/code' },
|
||||||
{ text: 'Pitch', link: 'understand/pitch' },
|
{ text: 'Pitch', link: 'understand/pitch' },
|
||||||
|
{ text: 'Xen Harmonic Functions', link: 'learn/xen' },
|
||||||
{ text: 'Cycles', link: 'understand/cycles' },
|
{ text: 'Cycles', link: 'understand/cycles' },
|
||||||
{ text: 'Voicings', link: 'understand/voicings' },
|
{ text: 'Voicings', link: 'understand/voicings' },
|
||||||
{ text: 'Pattern Alignment', link: 'technical-manual/alignment' },
|
{ text: 'Pattern Alignment', link: 'technical-manual/alignment' },
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ xxx("foo").yyy("bar")
|
|||||||
|
|
||||||
Generally, `xxx` and `yyy` are called [_functions_](<https://en.wikipedia.org/wiki/Function_(computer_programming)>), while `foo` and `bar` are called function [_arguments_ or _parameters_](<https://en.wikipedia.org/wiki/Parameter_(computer_programming)>).
|
Generally, `xxx` and `yyy` are called [_functions_](<https://en.wikipedia.org/wiki/Function_(computer_programming)>), while `foo` and `bar` are called function [_arguments_ or _parameters_](<https://en.wikipedia.org/wiki/Parameter_(computer_programming)>).
|
||||||
So far, we've used the functions to declare which aspect of the sound we want to control, and their arguments for the actual data.
|
So far, we've used the functions to declare which aspect of the sound we want to control, and their arguments for the actual data.
|
||||||
The `yyy` function is called a [_chained_ function](https://en.wikipedia.org/wiki/Method_chaining), because it is appended with a dot (`.`).
|
The `yyy` function is called a [_chained_ function](https://en.wikipedia.org/wiki/Method_chaining), because it is preceded with a dot (`.`).
|
||||||
|
|
||||||
Generally, the idea with chaining is that code such as `a("this").b("that").c("other")` allows `a`, `b` and `c` functions to happen in a specified order, without needing to write them as three separate lines of code.
|
Generally, the idea with chaining is that code such as `a("this").b("that").c("other")` allows `a`, `b` and `c` functions to happen in a specified order, without needing to write them as three separate lines of code.
|
||||||
You can think of this as being similar to chaining audio effects together using guitar pedals or digital audio effects.
|
You can think of this as being similar to chaining audio effects together using guitar pedals or digital audio effects.
|
||||||
|
|||||||
@@ -34,11 +34,11 @@ import { JsDoc } from '../../docs/JsDoc';
|
|||||||
|
|
||||||
## arp
|
## arp
|
||||||
|
|
||||||
<JsDoc client:idle name="Pattern#arp" h={0} />
|
<JsDoc client:idle name="arp" h={0} />
|
||||||
|
|
||||||
## arpWith 🧪
|
## arpWith 🧪
|
||||||
|
|
||||||
<JsDoc client:idle name="Pattern#arpWith" h={0} />
|
<JsDoc client:idle name="arpWith" h={0} />
|
||||||
|
|
||||||
## struct
|
## struct
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@ import { JsDoc } from '../../docs/JsDoc';
|
|||||||
|
|
||||||
## hush
|
## hush
|
||||||
|
|
||||||
<JsDoc client:idle name="hush" h={0} />
|
<JsDoc client:idle name="Pattern#hush" h={0} />
|
||||||
|
|
||||||
## invert
|
## invert
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,129 @@ import { JsDoc } from '../../docs/JsDoc';
|
|||||||
Whether you're using a synth or a sample, you can apply any of the following built-in audio effects.
|
Whether you're using a synth or a sample, you can apply any of the following built-in audio effects.
|
||||||
As you might suspect, the effects can be chained together, and they accept a pattern string as their argument.
|
As you might suspect, the effects can be chained together, and they accept a pattern string as their argument.
|
||||||
|
|
||||||
|
# Signal chain
|
||||||
|
|
||||||
|
<img src="/img/strudel-signal-flow.png"></img>
|
||||||
|
|
||||||
|
The signal chain in Strudel is as follows:
|
||||||
|
|
||||||
|
- An sound-generating event is triggered by a pattern
|
||||||
|
- This has a start time and a duration, which is usually
|
||||||
|
controlled by the note length and ADSR parameters
|
||||||
|
- If we exceed the max polyphony, old sounds begin to die off
|
||||||
|
- Muted sounds (one whose `s` value is `-`, `~`, or `_`) are skipped
|
||||||
|
- A sound is produced (through, say, a sample or an oscillator)
|
||||||
|
- This is where detune-based effects (like `detune`, `penv`, etc. occur)
|
||||||
|
- The following will occur _in order_ and only if they've been called in the pattern. Note that all of these are
|
||||||
|
single use effects, meaning that multiple occurrences of them in a pattern will simply override the values
|
||||||
|
(e.g. you can't do `s("bd").lpf(100).distort(2).lpf(800)` to lowpass, distort, and then lowpass
|
||||||
|
again)
|
||||||
|
- Phase vocoder (`stretch`)
|
||||||
|
- Gain is applied (`gain`)
|
||||||
|
- This is where the main (volume) ADSR happens
|
||||||
|
- A lowpass filter (`lpf`)
|
||||||
|
- A highpass filter (`hpf`)
|
||||||
|
- A bandpass filter (`bandpass`)
|
||||||
|
- A vowel filter (`vowel`)
|
||||||
|
- Sample rate reduction (`coarse`)
|
||||||
|
- Bit crushing (`crush`)
|
||||||
|
- Waveshape distortion (`shape`)
|
||||||
|
- Normal distortion (`distort`)
|
||||||
|
- Tremolo (`tremolo`)
|
||||||
|
- Compressor (`compressor`)
|
||||||
|
- Panning (`pan`)
|
||||||
|
- Phaser (`phaser`)
|
||||||
|
- Postgain (`post`)
|
||||||
|
- The sound is then split into multiple destinations
|
||||||
|
- Dry output (amount controlled by `dry` parameter)
|
||||||
|
- The sends
|
||||||
|
- Analyzers
|
||||||
|
- These are used for tooling like `scope` and `spectrum` and their setup usually happens behind the scenes
|
||||||
|
- Delay (amount controlled by `delay` parameter)
|
||||||
|
- Reverb (amount controlled by `room` parameter)
|
||||||
|
- The dry output, delay, and reverb are joined into what is called the "orbit" of the pattern (see more in the section below)
|
||||||
|
- The `duck` effect affects the volume of all signals in the orbit
|
||||||
|
- The orbit is then sent to the mixer
|
||||||
|
|
||||||
|
## Orbits
|
||||||
|
|
||||||
|
Orbits are the way in which outputs are handled in Strudel. They also prescribe which delay and reverb to associate with the dry signal.
|
||||||
|
By default, all orbits are mixed down to channels `1` and `2` in stereo, however with the "Multi Channel Orbits" setting
|
||||||
|
(under Settings at the right) you can use them as individual 2 channel stereo outs (orbit `i` will be mapped to
|
||||||
|
to channels `2i` and `2i + 1`). You can then use routers like Blackhole 16 to retrieve and record all of the channels in a DAW for later processing.
|
||||||
|
|
||||||
|
The default orbit is `1` and it is set with `orbit`. You may send a sound to multiple orbits via mininotation
|
||||||
|
|
||||||
|
<MiniRepl client:visible tune={`s("white").orbit("2,3,4").gain(0.2)`} />
|
||||||
|
|
||||||
|
but please be careful as this will create three copies of the sound behind the scenes, meaning that if they are mixed
|
||||||
|
down to a single output, they will triple the volume. We've reduced the gain here to save your ears.
|
||||||
|
|
||||||
|
⚠️ There is only one delay and reverb per orbit, so please be aware that if you attempt to change the parameters on two
|
||||||
|
patterns pointing to the same orbit, it can lead to unpredictable results. Compare, for example, this pretty pluck
|
||||||
|
with a large reverb:
|
||||||
|
|
||||||
|
<MiniRepl
|
||||||
|
client:visible
|
||||||
|
tune={`
|
||||||
|
$: s("triangle*4").decay(0.5).n(irand(12)).scale('C minor')
|
||||||
|
.room(1).roomsize(10)`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
versus the same pluck with a muted kick drum coming in and overwriting the `roomsize` value:
|
||||||
|
|
||||||
|
<MiniRepl
|
||||||
|
client:visible
|
||||||
|
tune={`
|
||||||
|
$: s("triangle*4").decay(0.5).n(irand(12)).scale('C minor')
|
||||||
|
.room(1).roomsize(10)
|
||||||
|
|
||||||
|
$: s("bd\*4").room(0.01).roomsize(0.01).postgain(0)`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
This is due to them sharing the same orbit: the default of `1`. It can be corrected simply by updating the orbits to be
|
||||||
|
distinct:
|
||||||
|
|
||||||
|
<MiniRepl
|
||||||
|
client:visible
|
||||||
|
tune={`
|
||||||
|
$: s("triangle*4").decay(0.5).n(irand(12)).scale('C minor')
|
||||||
|
.room(1).roomsize(10).orbit(2)
|
||||||
|
|
||||||
|
$: s("bd\*4").room(0.01).roomsize(0.01).postgain(0)`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
## Continuous changes
|
||||||
|
|
||||||
|
As all of the above is triggered by a _sound occurring_, it is often the case that parameters may not be
|
||||||
|
modified continuously in time. For example,
|
||||||
|
|
||||||
|
<MiniRepl
|
||||||
|
client:visible
|
||||||
|
tune={`
|
||||||
|
s("supersaw").lpf(tri.range(100, 5000).slow(2))`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
Will not produce a continually LFO'd low-pass filter due to the `tri` only being sampled every time the note hits
|
||||||
|
(in this case the default of once per cycle). You can fake it by introducing more sound-generating events, e.g.:
|
||||||
|
|
||||||
|
<MiniRepl
|
||||||
|
client:visible
|
||||||
|
tune={`
|
||||||
|
s("supersaw").seg(16).lpf(tri.range(100, 5000).slow(2))`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
Some parameters _do_ induce continuous variations in time, though:
|
||||||
|
|
||||||
|
- The ADSR curve (governed by `attack`, `sustain`, `decay`, `release`)
|
||||||
|
- The pitch envelope curve (governed by `penv` and its associated ADSR)
|
||||||
|
- The FM curve (`fmenv`)
|
||||||
|
- The filter envelopes (`lpenv`, `hpenv`, `bpenv`)
|
||||||
|
- Tremolo (`tremolo`)
|
||||||
|
- Phaser (`phaser`)
|
||||||
|
- Vibrato (`vib`)
|
||||||
|
- Ducking (`duckorbit`)
|
||||||
|
|
||||||
# Filters
|
# Filters
|
||||||
|
|
||||||
Filters are an essential building block of [subtractive synthesis](https://en.wikipedia.org/wiki/Subtractive_synthesis).
|
Filters are an essential building block of [subtractive synthesis](https://en.wikipedia.org/wiki/Subtractive_synthesis).
|
||||||
@@ -339,4 +462,18 @@ global effects use the same chain for all events of the same orbit:
|
|||||||
|
|
||||||
<JsDoc client:idle name="phasersweep" h={0} />
|
<JsDoc client:idle name="phasersweep" h={0} />
|
||||||
|
|
||||||
|
## Duck
|
||||||
|
|
||||||
|
### duckorbit
|
||||||
|
|
||||||
|
<JsDoc client:idle name="duckorbit" h={0} />
|
||||||
|
|
||||||
|
### duckattack
|
||||||
|
|
||||||
|
<JsDoc client:idle name="duckattack" h={0} />
|
||||||
|
|
||||||
|
### duckdepth
|
||||||
|
|
||||||
|
<JsDoc client:idle name="duckdepth" h={0} />
|
||||||
|
|
||||||
Next, we'll look at input / output via [MIDI, OSC and other methods](/learn/input-output).
|
Next, we'll look at input / output via [MIDI, OSC and other methods](/learn/input-output).
|
||||||
|
|||||||
@@ -168,6 +168,20 @@ Using "!" we can repeat without speeding up:
|
|||||||
|
|
||||||
<MiniRepl client:idle tune={`note("<[g3,b3,e4]!2 [a3,c3,e4] [b3,d3,f#4]>*2")`} punchcard />
|
<MiniRepl client:idle tune={`note("<[g3,b3,e4]!2 [a3,c3,e4] [b3,d3,f#4]>*2")`} punchcard />
|
||||||
|
|
||||||
|
## Randomness
|
||||||
|
|
||||||
|
Events with a "?" placed after them will have a 50% chance of being removed from the pattern:
|
||||||
|
|
||||||
|
<MiniRepl client:idle tune={`note("[g3,b3,e4]*8?")`} punchcard />
|
||||||
|
|
||||||
|
Adding a number between 0 and 1 after the "?" will affect the likelihood of the event being removed. For example, events with "?0.1" placed after them will have a 10% chance of being removed:
|
||||||
|
|
||||||
|
<MiniRepl client:idle tune={`note("[g3,b3,e4]*8?0.1")`} punchcard />
|
||||||
|
|
||||||
|
Events separated by a "|" will be chosen from at random:
|
||||||
|
|
||||||
|
<MiniRepl client:idle tune={`note("[g3,b3,e4] | [a3,c3,e4] | [b3,d3,f#4]")`} punchcard />
|
||||||
|
|
||||||
## Mini-notation review
|
## Mini-notation review
|
||||||
|
|
||||||
To recap what we've learned so far, compare the following patterns:
|
To recap what we've learned so far, compare the following patterns:
|
||||||
@@ -179,6 +193,8 @@ To recap what we've learned so far, compare the following patterns:
|
|||||||
<MiniRepl client:idle tune={`note("<[g3,b3,e4] _ [a3,c3,e4] [b3,d3,f#4]>*2")`} />
|
<MiniRepl client:idle tune={`note("<[g3,b3,e4] _ [a3,c3,e4] [b3,d3,f#4]>*2")`} />
|
||||||
<MiniRepl client:idle tune={`note("<[g3,b3,e4]@2 [a3,c3,e4] [b3,d3,f#4]>*2")`} />
|
<MiniRepl client:idle tune={`note("<[g3,b3,e4]@2 [a3,c3,e4] [b3,d3,f#4]>*2")`} />
|
||||||
<MiniRepl client:idle tune={`note("<[g3,b3,e4]!2 [a3,c3,e4] [b3,d3,f#4]>*2")`} />
|
<MiniRepl client:idle tune={`note("<[g3,b3,e4]!2 [a3,c3,e4] [b3,d3,f#4]>*2")`} />
|
||||||
|
<MiniRepl client:idle tune={`note("<[g3,b3,e4]? [a3,c3,e4] [b3,d3,f#4]>*2")`} />
|
||||||
|
<MiniRepl client:idle tune={`note("<[g3|b3|e4] [a3,c3,e4] [b3,d3,f#4]>*2")`} />
|
||||||
|
|
||||||
## Euclidian rhythms
|
## Euclidian rhythms
|
||||||
|
|
||||||
|
|||||||
@@ -59,6 +59,14 @@ Furthermore, strudel also loads instrument samples from [VCSL](https://github.co
|
|||||||
|
|
||||||
To see which sample names are available, open the `sounds` tab in the [REPL](https://strudel.cc/).
|
To see which sample names are available, open the `sounds` tab in the [REPL](https://strudel.cc/).
|
||||||
|
|
||||||
|
You can also create custom aliases for existing sounds using the `soundAlias` function:
|
||||||
|
|
||||||
|
<MiniRepl
|
||||||
|
client:idle
|
||||||
|
tune={`soundAlias("RolandTR808_bd", "kick")
|
||||||
|
s("kick")`}
|
||||||
|
/>
|
||||||
|
|
||||||
Note that only the sample maps (mapping names to URLs) are loaded initially, while the audio samples themselves are not loaded until they are actually played.
|
Note that only the sample maps (mapping names to URLs) are loaded initially, while the audio samples themselves are not loaded until they are actually played.
|
||||||
This behaviour of loading things only when they are needed is also called `lazy loading`.
|
This behaviour of loading things only when they are needed is also called `lazy loading`.
|
||||||
While it saves resources, it can also lead to sounds not being audible the first time they are triggered, because the sound is still loading.
|
While it saves resources, it can also lead to sounds not being audible the first time they are triggered, because the sound is still loading.
|
||||||
@@ -178,6 +186,16 @@ the version number).
|
|||||||
It is also possible, of course, to just remove it from cache (deleting cache in browser Privacy settings,
|
It is also possible, of course, to just remove it from cache (deleting cache in browser Privacy settings,
|
||||||
or from the dev console if you're technically minded, or by using a cache deleting extension).
|
or from the dev console if you're technically minded, or by using a cache deleting extension).
|
||||||
|
|
||||||
|
## Generating strudel.json
|
||||||
|
|
||||||
|
You can use [@strudel/sampler](https://www.npmjs.com/package/@strudel/sampler) to generate a strudel.json file for you, by running:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npx --yes @strudel/sampler --json > strudel.json
|
||||||
|
```
|
||||||
|
|
||||||
|
See other uses of strudel/sampler further below, under "From Disk via @strudel/sampler".
|
||||||
|
|
||||||
## Github Shortcut
|
## Github Shortcut
|
||||||
|
|
||||||
Because loading samples from github is common, there is a shortcut:
|
Because loading samples from github is common, there is a shortcut:
|
||||||
@@ -361,6 +379,10 @@ Sampler effects are functions that can be used to change the behaviour of sample
|
|||||||
|
|
||||||
<JsDoc client:idle name="splice" h={0} />
|
<JsDoc client:idle name="splice" h={0} />
|
||||||
|
|
||||||
|
### scrub
|
||||||
|
|
||||||
|
<JsDoc client:idle name="Pattern.scrub" h={0} />{' '}
|
||||||
|
|
||||||
### speed
|
### speed
|
||||||
|
|
||||||
<JsDoc client:idle name="speed" h={0} />
|
<JsDoc client:idle name="speed" h={0} />
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
---
|
||||||
|
title: Xen Harmonic Functions
|
||||||
|
layout: ../../layouts/MainLayout.astro
|
||||||
|
---
|
||||||
|
|
||||||
|
import { MiniRepl } from '../../docs/MiniRepl';
|
||||||
|
import { JsDoc } from '../../docs/JsDoc';
|
||||||
|
|
||||||
|
# Xen Harmonic Functions
|
||||||
|
|
||||||
|
These functions allow the use of scales other than your typical chromatic 12 based ones.
|
||||||
|
|
||||||
|
### tune(scale)
|
||||||
|
|
||||||
|
<JsDoc client:idle name="tune" h={0} />
|
||||||
|
|
||||||
|
Here's an example of how to configure a basic hexany scale:
|
||||||
|
|
||||||
|
<MiniRepl client:idle tune={`"0 1 2 3 4 5".tune("hexany15").mul("220").freq()`} />
|
||||||
|
|
||||||
|
Try other scales like `hexany1`, `iraq`, `gumbeng`, `gunkali`, or `tranh3`
|
||||||
|
|
||||||
|
For a full list of available scales from tunejs, see http://abbernie.github.io/tune/scales.html
|
||||||
|
|
||||||
|
You can set your root to be a particular note with `getFreq`
|
||||||
|
|
||||||
|
<MiniRepl
|
||||||
|
client:idle
|
||||||
|
tune={`"4 8 9 10 - - 5 7 9 11 - -".tune("tranh3")
|
||||||
|
.mul(getFreq('c3'))
|
||||||
|
.freq().clip(.5).room(1)`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
Some tunings become more pronounced with a longer reverb decay:
|
||||||
|
|
||||||
|
<MiniRepl
|
||||||
|
client:idle
|
||||||
|
tune={`"<[5 6 8 10] - [5 7 9 12] -> -".tune("gumbeng")
|
||||||
|
.mul(getFreq('c3'))
|
||||||
|
.freq().clip(.8).room("3:10").rdim(10000).rfade(5)`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
Additionally, you can combo this with `fmap` so that the base note changes:
|
||||||
|
|
||||||
|
<MiniRepl
|
||||||
|
client:idle
|
||||||
|
tune={`"9 11 12 10 - - -".tune("gunkali")
|
||||||
|
.mul("<c3 c3 a3 d#3>".fmap(getFreq))
|
||||||
|
.freq().legato("2 .7").room("1:15").rdim(8500).rlp(14000).rfade(8)`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
Combining this with various polyrhythm tricks can become very evocative:
|
||||||
|
|
||||||
|
<MiniRepl
|
||||||
|
client:idle
|
||||||
|
tune={`"<[0 3 1 -] [-1 4 2 8]> ~ ~,<-4 -5>"
|
||||||
|
.transpose(4)
|
||||||
|
.tune("iraq")
|
||||||
|
.mul("<c3 d3 c#3>".fmap(getFreq))
|
||||||
|
.freq().clip(.5).room(1).rfade(9)`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
Another helpful trick when exploring new tunings is to strum them.
|
||||||
|
Many have a much more enchanting sound that was chosen over many generations of musicians for being strummed.
|
||||||
|
|
||||||
|
Take the `sanza` tuning:
|
||||||
|
|
||||||
|
<MiniRepl
|
||||||
|
client:idle
|
||||||
|
tune={`"4 5 6 7 8 9".tune("sanza")
|
||||||
|
.mul(getFreq('c3'))
|
||||||
|
.freq()`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
Notes 7 and 9 will clash quite a bit if you arp them normally. Many tunings will have this sort of sound, and it can feel distracting on its own.
|
||||||
|
See how close they are on the pitch wheel?
|
||||||
|
|
||||||
|
<MiniRepl client:idle tune={`"[7 9]!3".tune("sanza").mul(getFreq('c3')).freq()._pitchwheel()`} />
|
||||||
|
|
||||||
|
This quality is often due to how the tunings were formed with instruments that were played differently than a piano.
|
||||||
|
As such, some tunings are much better strummed, with the subtle clash of the detuned notes actually making the sound much more magical:
|
||||||
|
|
||||||
|
<MiniRepl
|
||||||
|
client:idle
|
||||||
|
tune={`"[0 1 2 3 4 5 6]@0.3 -"
|
||||||
|
.transpose("<2 5 8 1>")
|
||||||
|
.tune("sanza")
|
||||||
|
.mul(getFreq('c3')).freq()
|
||||||
|
.legato("3").room(1).rfade(5)`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
Note the legato and reverb effects make sure the sound of the strumming gets to wash together. Alternating the direction of the strum can make the
|
||||||
|
tones sound even more alive, too.
|
||||||
|
|
||||||
|
The `tranh3` tuning has a similar set of notes, with two clashing. You might trying plugging that in above and see if you find a favorite strumming pattern.
|
||||||
@@ -9,7 +9,9 @@ import { MiniRepl } from '../../docs/MiniRepl';
|
|||||||
|
|
||||||
{/* The [REPL](https://strudel.cc/) is the place where all packages come together to form a live coding system. It can also be seen as a reference implementation for users of the library. */}
|
{/* The [REPL](https://strudel.cc/) is the place where all packages come together to form a live coding system. It can also be seen as a reference implementation for users of the library. */}
|
||||||
|
|
||||||
While Strudel can be used as a library in any JavaScript codebase, its main, reference user interface is the Strudel REPL^[REPL stands for read, evaluate, print/play, loop. It is friendly jargon for an interactive programming interface from computing heritage, usually for a commandline interface but also applied to live coding editors.], which is a browser-based live coding environment. This live code editor is dedicated to manipulating Strudel patterns while they play. The REPL features built-in visual feedback, highlighting which elements in the patterned (mini-notation) sequences are influencing the event that is currently being played. This feedback is designed to support both learning and live use of Strudel.
|
While Strudel can be used as a library in any JavaScript codebase, its main, reference user interface is the Strudel REPL[^1], which is a browser-based live coding environment. This live code editor is dedicated to manipulating Strudel patterns while they play. The REPL features built-in visual feedback, highlighting which elements in the patterned (mini-notation) sequences are influencing the event that is currently being played. This feedback is designed to support both learning and live use of Strudel.
|
||||||
|
|
||||||
|
[^1]: REPL stands for read, evaluate, print/play, loop. It is friendly jargon for an interactive programming interface from computing heritage, usually for a commandline interface but also applied to live coding editors.
|
||||||
|
|
||||||
Besides a UI for playback control and meta information, the main part of the REPL interface is the code editor powered by CodeMirror. In it, the user can edit and evaluate pattern code live, using one of the available synthesis outputs to create music and/or sound art. The control flow of the REPL follows 3 basic steps:
|
Besides a UI for playback control and meta information, the main part of the REPL interface is the code editor powered by CodeMirror. In it, the user can edit and evaluate pattern code live, using one of the available synthesis outputs to create music and/or sound art. The control flow of the REPL follows 3 basic steps:
|
||||||
|
|
||||||
|
|||||||
@@ -262,14 +262,14 @@ It is quite common that there are many ways to express the same idea.
|
|||||||
|
|
||||||
**selecting sample numbers separately**
|
**selecting sample numbers separately**
|
||||||
|
|
||||||
Instead of using ":", we can also use the `n` function to select sample numbers:
|
Instead of selecting sample numbers one by one:
|
||||||
|
|
||||||
<MiniRepl client:visible tune={`n("0 1 [4 2] 3*2").sound("jazz")`} punchcard />
|
|
||||||
|
|
||||||
This is shorter and more readable than:
|
|
||||||
|
|
||||||
<MiniRepl client:visible tune={`sound("jazz:0 jazz:1 [jazz:4 jazz:2] jazz:3*2")`} punchcard />
|
<MiniRepl client:visible tune={`sound("jazz:0 jazz:1 [jazz:4 jazz:2] jazz:3*2")`} punchcard />
|
||||||
|
|
||||||
|
We can also use the `n` function to make it shorter and more readable:
|
||||||
|
|
||||||
|
<MiniRepl client:visible tune={`n("0 1 [4 2] 3*2").sound("jazz")`} punchcard />
|
||||||
|
|
||||||
## Recap
|
## Recap
|
||||||
|
|
||||||
Now we've learned the basics of the so called Mini-Notation, the rhythm language of Tidal.
|
Now we've learned the basics of the so called Mini-Notation, the rhythm language of Tidal.
|
||||||
|
|||||||
@@ -69,3 +69,150 @@
|
|||||||
text-decoration: underline 0.18rem;
|
text-decoration: underline 0.18rem;
|
||||||
text-underline-offset: 0.22rem;
|
text-underline-offset: 0.22rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Override default styles from the codemirror inline css for autocomplete info tooltip*/
|
||||||
|
.cm-tooltip.cm-completionInfo {
|
||||||
|
padding: 0 !important;
|
||||||
|
border: 1px solid var(--foreground) !important;
|
||||||
|
border-radius: 4px !important;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3) !important;
|
||||||
|
max-width: 500px !important;
|
||||||
|
min-width: 300px !important;
|
||||||
|
max-height: 400px !important;
|
||||||
|
background-color: var(--lineHighlight) !important;
|
||||||
|
overflow: auto;
|
||||||
|
background: var(--background) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main tooltip container */
|
||||||
|
.autocomplete-info-container {
|
||||||
|
padding: 12px !important;
|
||||||
|
border-radius: 4px !important;
|
||||||
|
color: var(--foreground);
|
||||||
|
font-family: var(--font-family, 'SF Mono', 'Monaco', monospace);
|
||||||
|
font-size: var(--font-size, 13px);
|
||||||
|
line-height: 1.4;
|
||||||
|
max-width: 600px;
|
||||||
|
height: 100%;
|
||||||
|
min-width: 400px;
|
||||||
|
white-space: normal !important;
|
||||||
|
overflow-y: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.autocomplete-info-tooltip {
|
||||||
|
overflow-y: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.autocomplete-info-function-description {
|
||||||
|
white-space: pre-wrap !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.autocomplete-info-function-name {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--foreground);
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.autocomplete-info-function-synonyms {
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
color: var(--foreground);
|
||||||
|
line-height: 1.5;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.autocomplete-info-function-description {
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
color: var(--foreground);
|
||||||
|
line-height: 1.5;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.autocomplete-info-section-title {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--foreground);
|
||||||
|
margin: 16px 0 6px 0;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.autocomplete-info-section-title:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.autocomplete-info-params-section {
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.autocomplete-info-params-list {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.autocomplete-info-param-item {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
background-color: var(--lineBackground);
|
||||||
|
border-radius: 3px;
|
||||||
|
border-left: 2px solid var(--foreground, #555);
|
||||||
|
}
|
||||||
|
|
||||||
|
.autocomplete-info-param-item:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.autocomplete-info-param-name {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--variable, var(--foreground));
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.autocomplete-info-param-type {
|
||||||
|
color: var(--comment);
|
||||||
|
font-size: 12px;
|
||||||
|
background-color: var(--gutterForeground);
|
||||||
|
padding: 1px 4px;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.autocomplete-info-param-desc {
|
||||||
|
color: var(--foreground);
|
||||||
|
font-size: 10px;
|
||||||
|
margin-top: 4px;
|
||||||
|
line-height: 1.4;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.autocomplete-info-examples-section {
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.autocomplete-info-example-code {
|
||||||
|
background: var(--lineBackground);
|
||||||
|
color: var(--foreground);
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-family: var(--font-family, 'SF Mono', 'Monaco', monospace);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
margin: 4px 0;
|
||||||
|
overflow-x: auto;
|
||||||
|
white-space: pre;
|
||||||
|
border: 1px solid var(--foreground, #3a3a3a);
|
||||||
|
}
|
||||||
|
|
||||||
|
.autocomplete-info-tooltip::-webkit-scrollbar {
|
||||||
|
width: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.autocomplete-info-tooltip::-webkit-scrollbar-track {
|
||||||
|
}
|
||||||
|
|
||||||
|
.autocomplete-info-tooltip::-webkit-scrollbar-thumb {
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.autocomplete-info-tooltip::-webkit-scrollbar-thumb:hover {
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import cx from '@src/cx.mjs';
|
||||||
|
|
||||||
|
export function ActionButton({ children, label, labelIsHidden, className, ...buttonProps }) {
|
||||||
|
return (
|
||||||
|
<button className={cx('hover:opacity-50 text-nowrap w-fit', className)} title={label} {...buttonProps}>
|
||||||
|
{labelIsHidden !== true && label}
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -12,8 +12,8 @@ import { useMemo } from 'react';
|
|||||||
import { getMetadata } from '../../../metadata_parser.js';
|
import { getMetadata } from '../../../metadata_parser.js';
|
||||||
import { useExamplePatterns } from '../../useExamplePatterns.jsx';
|
import { useExamplePatterns } from '../../useExamplePatterns.jsx';
|
||||||
import { parseJSON, isUdels } from '../../util.mjs';
|
import { parseJSON, isUdels } from '../../util.mjs';
|
||||||
import { ButtonGroup } from './Forms.jsx';
|
import { useSettings } from '../../../settings.mjs';
|
||||||
import { settingsMap, useSettings } from '../../../settings.mjs';
|
import { ActionButton } from '../button/action-button.jsx';
|
||||||
import { Pagination } from '../pagination/Pagination.jsx';
|
import { Pagination } from '../pagination/Pagination.jsx';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useDebounce } from '../usedebounce.jsx';
|
import { useDebounce } from '../usedebounce.jsx';
|
||||||
@@ -75,15 +75,6 @@ function PatternButtons({ patterns, activePattern, onClick, started }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ActionButton({ children, onClick, label, labelIsHidden }) {
|
|
||||||
return (
|
|
||||||
<button className="hover:opacity-50 text-nowrap" onClick={onClick} title={label}>
|
|
||||||
{labelIsHidden !== true && label}
|
|
||||||
{children}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateCodeWindow = (context, patternData, reset = false) => {
|
const updateCodeWindow = (context, patternData, reset = false) => {
|
||||||
context.handleUpdate(patternData, reset);
|
context.handleUpdate(patternData, reset);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,9 +2,32 @@ import { useMemo, useState } from 'react';
|
|||||||
|
|
||||||
import jsdocJson from '../../../../../doc.json';
|
import jsdocJson from '../../../../../doc.json';
|
||||||
import { Textbox } from '../textbox/Textbox';
|
import { Textbox } from '../textbox/Textbox';
|
||||||
const availableFunctions = jsdocJson.docs
|
|
||||||
.filter(({ name, description }) => name && !name.startsWith('_') && !!description)
|
const isValid = ({ name, description }) => name && !name.startsWith('_') && !!description;
|
||||||
.sort((a, b) => /* a.meta.filename.localeCompare(b.meta.filename) + */ a.name.localeCompare(b.name));
|
|
||||||
|
const availableFunctions = (() => {
|
||||||
|
const seen = new Set(); // avoid repetition
|
||||||
|
const functions = [];
|
||||||
|
for (const doc of jsdocJson.docs) {
|
||||||
|
if (!isValid(doc)) continue;
|
||||||
|
functions.push(doc);
|
||||||
|
const synonyms = doc.synonyms || [];
|
||||||
|
for (const s of synonyms) {
|
||||||
|
if (!s || seen.has(s)) continue;
|
||||||
|
seen.add(s);
|
||||||
|
// Swap `doc.name` in for `s` in the list of synonyms
|
||||||
|
const synonymsWithDoc = [doc.name, ...synonyms].filter((x) => x && x !== s);
|
||||||
|
functions.push({
|
||||||
|
...doc,
|
||||||
|
name: s, // update names for the synonym
|
||||||
|
longname: s,
|
||||||
|
synonyms: synonymsWithDoc,
|
||||||
|
synonyms_text: synonymsWithDoc.join(', '),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return functions.sort((a, b) => /* a.meta.filename.localeCompare(b.meta.filename) + */ a.name.localeCompare(b.name));
|
||||||
|
})();
|
||||||
|
|
||||||
const getInnerText = (html) => {
|
const getInnerText = (html) => {
|
||||||
var div = document.createElement('div');
|
var div = document.createElement('div');
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ const fontFamilyOptions = {
|
|||||||
FiraCode: 'FiraCode',
|
FiraCode: 'FiraCode',
|
||||||
'FiraCode-SemiBold': 'FiraCode SemiBold',
|
'FiraCode-SemiBold': 'FiraCode SemiBold',
|
||||||
teletext: 'teletext',
|
teletext: 'teletext',
|
||||||
|
tic80: 'tic80',
|
||||||
mode7: 'mode7',
|
mode7: 'mode7',
|
||||||
BigBlueTerminal: 'BigBlueTerminal',
|
BigBlueTerminal: 'BigBlueTerminal',
|
||||||
x3270: 'x3270',
|
x3270: 'x3270',
|
||||||
@@ -311,7 +312,8 @@ export function SettingsTab({ started }) {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
confirmDialog('Sure?').then((r) => {
|
confirmDialog('Sure?').then((r) => {
|
||||||
if (r) {
|
if (r) {
|
||||||
settingsMap.set(defaultSettings);
|
const { userPatterns } = settingsMap.get(); // keep current patterns
|
||||||
|
settingsMap.set({ ...defaultSettings, userPatterns });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -2,16 +2,21 @@ import useEvent from '@src/useEvent.mjs';
|
|||||||
import { useStore } from '@nanostores/react';
|
import { useStore } from '@nanostores/react';
|
||||||
import { getAudioContext, soundMap, connectToDestination } from '@strudel/webaudio';
|
import { getAudioContext, soundMap, connectToDestination } from '@strudel/webaudio';
|
||||||
import { useMemo, useRef, useState } from 'react';
|
import { useMemo, useRef, useState } from 'react';
|
||||||
import { settingsMap, useSettings } from '../../../settings.mjs';
|
import { settingsMap, soundFilterType, useSettings } from '../../../settings.mjs';
|
||||||
import { ButtonGroup } from './Forms.jsx';
|
import { ButtonGroup } from './Forms.jsx';
|
||||||
import ImportSoundsButton from './ImportSoundsButton.jsx';
|
import ImportSoundsButton from './ImportSoundsButton.jsx';
|
||||||
import { Textbox } from '../textbox/Textbox.jsx';
|
import { Textbox } from '../textbox/Textbox.jsx';
|
||||||
|
import { ActionButton } from '../button/action-button.jsx';
|
||||||
|
import { confirmDialog } from '@src/repl/util.mjs';
|
||||||
|
import { clearIDB, userSamplesDBConfig } from '@src/repl/idbutils.mjs';
|
||||||
|
import { prebake } from '@src/repl/prebake.mjs';
|
||||||
|
|
||||||
const getSamples = (samples) =>
|
const getSamples = (samples) =>
|
||||||
Array.isArray(samples) ? samples.length : typeof samples === 'object' ? Object.values(samples).length : 1;
|
Array.isArray(samples) ? samples.length : typeof samples === 'object' ? Object.values(samples).length : 1;
|
||||||
|
|
||||||
export function SoundsTab() {
|
export function SoundsTab() {
|
||||||
const sounds = useStore(soundMap);
|
const sounds = useStore(soundMap);
|
||||||
|
|
||||||
const { soundsFilter } = useSettings();
|
const { soundsFilter } = useSettings();
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
const { BASE_URL } = import.meta.env;
|
const { BASE_URL } = import.meta.env;
|
||||||
@@ -27,18 +32,19 @@ export function SoundsTab() {
|
|||||||
.sort((a, b) => a[0].localeCompare(b[0]))
|
.sort((a, b) => a[0].localeCompare(b[0]))
|
||||||
.filter(([name]) => name.toLowerCase().includes(search.toLowerCase()));
|
.filter(([name]) => name.toLowerCase().includes(search.toLowerCase()));
|
||||||
|
|
||||||
if (soundsFilter === 'user') {
|
if (soundsFilter === soundFilterType.USER) {
|
||||||
return filtered.filter(([_, { data }]) => !data.prebake);
|
return filtered.filter(([_, { data }]) => !data.prebake);
|
||||||
}
|
}
|
||||||
if (soundsFilter === 'drums') {
|
if (soundsFilter === soundFilterType.DRUMS) {
|
||||||
return filtered.filter(([_, { data }]) => data.type === 'sample' && data.tag === 'drum-machines');
|
return filtered.filter(([_, { data }]) => data.type === 'sample' && data.tag === 'drum-machines');
|
||||||
}
|
}
|
||||||
if (soundsFilter === 'samples') {
|
if (soundsFilter === soundFilterType.SAMPLES) {
|
||||||
return filtered.filter(([_, { data }]) => data.type === 'sample' && data.tag !== 'drum-machines');
|
return filtered.filter(([_, { data }]) => data.type === 'sample' && data.tag !== 'drum-machines');
|
||||||
}
|
}
|
||||||
if (soundsFilter === 'synths') {
|
if (soundsFilter === soundFilterType.SYNTHS) {
|
||||||
return filtered.filter(([_, { data }]) => ['synth', 'soundfont'].includes(data.type));
|
return filtered.filter(([_, { data }]) => ['synth', 'soundfont'].includes(data.type));
|
||||||
}
|
}
|
||||||
|
//TODO: tidy this up, it does not need to be saved in settings
|
||||||
if (soundsFilter === 'importSounds') {
|
if (soundsFilter === 'importSounds') {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -57,10 +63,10 @@ export function SoundsTab() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
<div id="sounds-tab" className="px-4 flex flex-col w-full h-full text-foreground">
|
<div id="sounds-tab" className="px-4 flex gap-2 flex-col w-full h-full text-foreground">
|
||||||
<Textbox placeholder="Search" value={search} onChange={(v) => setSearch(v)} />
|
<Textbox placeholder="Search" value={search} onChange={(v) => setSearch(v)} />
|
||||||
|
|
||||||
<div className="pb-2 flex shrink-0 flex-wrap">
|
<div className=" flex shrink-0 flex-wrap">
|
||||||
<ButtonGroup
|
<ButtonGroup
|
||||||
value={soundsFilter}
|
value={soundsFilter}
|
||||||
onChange={(value) => settingsMap.setKey('soundsFilter', value)}
|
onChange={(value) => settingsMap.setKey('soundsFilter', value)}
|
||||||
@@ -74,7 +80,26 @@ export function SoundsTab() {
|
|||||||
></ButtonGroup>
|
></ButtonGroup>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="min-h-0 max-h-full grow overflow-auto text-sm break-normal pb-2">
|
{soundsFilter === soundFilterType.USER && soundEntries.length > 0 && (
|
||||||
|
<ActionButton
|
||||||
|
className="pl-2"
|
||||||
|
label="delete-all"
|
||||||
|
onClick={async () => {
|
||||||
|
try {
|
||||||
|
const confirmed = await confirmDialog('Delete all imported user samples?');
|
||||||
|
if (confirmed) {
|
||||||
|
clearIDB(userSamplesDBConfig.dbName);
|
||||||
|
soundMap.set({});
|
||||||
|
await prebake();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="min-h-0 max-h-full grow overflow-auto text-sm break-normal bg-background p-2 rounded-md">
|
||||||
{soundEntries.map(([name, { data, onTrigger }]) => {
|
{soundEntries.map(([name, { data, onTrigger }]) => {
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
@@ -151,9 +176,7 @@ export function SoundsTab() {
|
|||||||
) : (
|
) : (
|
||||||
''
|
''
|
||||||
)}
|
)}
|
||||||
{!soundEntries.length && soundsFilter !== 'importSounds'
|
{!soundEntries.length && soundsFilter !== 'importSounds' ? 'No sounds loaded' : ''}
|
||||||
? 'No custom sounds loaded in this pattern (yet).'
|
|
||||||
: ''}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -12,17 +12,21 @@ export const userSamplesDBConfig = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// deletes all of the databases, useful for debugging
|
// deletes all of the databases, useful for debugging
|
||||||
function clearIDB() {
|
function clearAllIDB() {
|
||||||
window.indexedDB
|
window.indexedDB
|
||||||
.databases()
|
.databases()
|
||||||
.then((r) => {
|
.then((r) => {
|
||||||
for (var i = 0; i < r.length; i++) window.indexedDB.deleteDatabase(r[i].name);
|
for (var i = 0; i < r.length; i++) clearIDB(r[i].name);
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
alert('All data cleared.');
|
alert('All data cleared.');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function clearIDB(dbName) {
|
||||||
|
return window.indexedDB.deleteDatabase(dbName);
|
||||||
|
}
|
||||||
|
|
||||||
// queries the DB, and registers the sounds so they can be played
|
// queries the DB, and registers the sounds so they can be played
|
||||||
export function registerSamplesFromDB(config = userSamplesDBConfig, onComplete = () => {}) {
|
export function registerSamplesFromDB(config = userSamplesDBConfig, onComplete = () => {}) {
|
||||||
openDB(config, (objectStore) => {
|
openDB(config, (objectStore) => {
|
||||||
|
|||||||
@@ -8,6 +8,14 @@ export const audioEngineTargets = {
|
|||||||
osc: 'osc',
|
osc: 'osc',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const soundFilterType = {
|
||||||
|
USER: 'user',
|
||||||
|
DRUMS: 'drums',
|
||||||
|
SAMPLES: 'samples',
|
||||||
|
SYNTHS: 'synths',
|
||||||
|
ALL: 'all',
|
||||||
|
};
|
||||||
|
|
||||||
export const defaultSettings = {
|
export const defaultSettings = {
|
||||||
activeFooter: 'intro',
|
activeFooter: 'intro',
|
||||||
keybindings: 'codemirror',
|
keybindings: 'codemirror',
|
||||||
@@ -28,7 +36,7 @@ export const defaultSettings = {
|
|||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
latestCode: '',
|
latestCode: '',
|
||||||
isZen: false,
|
isZen: false,
|
||||||
soundsFilter: 'all',
|
soundsFilter: soundFilterType.ALL,
|
||||||
patternFilter: 'community',
|
patternFilter: 'community',
|
||||||
// panelPosition: window.innerWidth > 1000 ? 'right' : 'bottom', //FIX: does not work on astro
|
// panelPosition: window.innerWidth > 1000 ? 'right' : 'bottom', //FIX: does not work on astro
|
||||||
panelPosition: 'right',
|
panelPosition: 'right',
|
||||||
|
|||||||
@@ -50,6 +50,11 @@
|
|||||||
src: url('/fonts/teletext/EuropeanTeletext.ttf');
|
src: url('/fonts/teletext/EuropeanTeletext.ttf');
|
||||||
size-adjust: 90%;
|
size-adjust: 90%;
|
||||||
}
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'tic80';
|
||||||
|
src: url('/fonts/tic80/tic-80-wide-font.otf');
|
||||||
|
size-adjust: 60%;
|
||||||
|
}
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'mode7';
|
font-family: 'mode7';
|
||||||
src: url('/fonts/mode7/MODE7GX3.TTF');
|
src: url('/fonts/mode7/MODE7GX3.TTF');
|
||||||
|
|||||||
Reference in New Issue
Block a user