Compare commits

..

46 Commits

Author SHA1 Message Date
Vasilii Milovidov fbcd28f09a path fix 2023-10-16 14:26:58 +04:00
Vasilii Milovidov bfb32d89a9 Various improvements + reverb tests 2023-10-16 14:24:47 +04:00
Vasilii Milovidov 6e18bd8d57 tests: reverb 2023-10-16 11:59:23 +04:00
Vasilii Milovidov 9d22a9f9ba tests: reverb 2023-10-07 19:11:35 +04:00
Vasilii Milovidov c0411c389f tests: reverb 2023-10-06 03:14:56 +04:00
Vasilii Milovidov 163f8b53fd tests: reverb 2023-10-06 01:53:22 +04:00
Vasilii Milovidov d447b9cf95 tests: reverb 2023-10-05 21:41:15 +04:00
Vasilii Milovidov c0c6963112 tests: reverb 2023-10-05 16:16:53 +04:00
Vasilii Milovidov e793cb671a tests: reverb 2023-10-05 02:58:58 +04:00
Vasilii Milovidov f6fc55b43e tests: reverb 2023-10-04 01:30:20 +04:00
Vasilii Milovidov 56d992def9 tests: reverb 2023-10-03 14:20:35 +04:00
Vasilii Milovidov eef51f9228 tests: reverb 2023-10-03 04:52:31 +04:00
Vasilii Milovidov 56dd5bb604 tests: reverb 2023-10-03 00:04:07 +04:00
Vasilii Milovidov c2fc2272c5 tests: reverb 2023-10-02 22:11:25 +04:00
Vasilii Milovidov 18a198df9b tests: reverb 2023-10-02 21:53:08 +04:00
Vasilii Milovidov 870e7e4f32 begin implementing orbits 2023-10-02 15:20:32 +04:00
Vasilii Milovidov 6b219ee11a Structural changes, cpu usage improvements, some other fixes 2023-09-30 01:08:36 +04:00
Vasilii Milovidov 38af4af4ff Adding structs and traits 2023-09-29 13:09:27 +04:00
Vasilii Milovidov 69a3e27eec various fixes 2023-09-26 21:30:58 +04:00
Vasilii Milovidov 9606ce0d51 first go at handling samples from the web 2023-09-26 18:42:18 +04:00
Vasilii Milovidov 229dfe487a first go at handling samples from the web 2023-09-26 18:42:15 +04:00
Vasilii Milovidov 592b982990 wip 2023-09-26 15:54:39 +04:00
Vasilii Milovidov 6633b373bc wip 2023-09-26 14:04:20 +04:00
Vasilii Milovidov 5a8d369fa6 wip 2023-09-26 14:00:58 +04:00
Vasilii Milovidov 747ea8e6a3 wip 2023-09-26 13:56:11 +04:00
Vasilii Milovidov a693f89796 remove orbits for now, more adsr tweaking 2023-09-22 15:28:08 +04:00
Vasilii Milovidov b93ac61a7a add compressor 2023-09-22 11:20:46 +04:00
Vasilii Milovidov 6a066a5304 adsr tests 2023-09-22 05:17:12 +04:00
Vasilii Milovidov 4ca57b308f adsr tests 2023-09-21 14:54:19 +04:00
Vasilii Milovidov 97e2d89e61 implementing delay 2023-09-21 04:32:31 +04:00
Vasilii Milovidov f5ca1ff49d prettier 2023-09-20 16:39:00 +04:00
Vasilii Milovidov c456186f0f make it closer to the js implementation 2023-09-20 16:36:03 +04:00
Vasilii Milovidov ebbbcfb071 looper and reverse tests 2023-09-20 05:35:58 +04:00
Vasilii Milovidov 7492c9383a looper and reverse tests 2023-09-20 05:21:43 +04:00
Vasilii Milovidov 51a16b45a6 more adsr tests 2023-09-20 04:48:59 +04:00
Vasilii Milovidov 89d65ad09d sample reverse 2023-09-20 02:40:43 +04:00
Vasilii Milovidov a92834de42 sample reverse 2023-09-20 02:31:04 +04:00
Vasilii Milovidov c8270a98e9 make filter nodes conditional 2023-09-19 12:26:35 +04:00
Vasilii Milovidov 12d8e22b46 adsr tests 2023-09-19 10:37:34 +04:00
Vasilii Milovidov 78a8ca2b4f organize 2023-09-19 10:12:48 +04:00
Vasilii Milovidov c90bd23a53 Gain and filter ADSR envelopes 2023-09-18 19:11:20 +04:00
Vasilii Milovidov 725ddc805d webaudio for tauri WIP 2023-09-18 14:52:10 +04:00
Vasilii Milovidov 00c0df4bfb webaudio for tauri WIP 2023-09-18 14:41:56 +04:00
Vasilii Milovidov 7a17ec6863 Merge branch 'web_audio_tests' into tauri_webaudio
# Conflicts:
#	packages/desktopbridge/index.mjs
#	packages/desktopbridge/package.json
#	packages/desktopbridge/webaudiobridge.mjs
#	src-tauri/Cargo.lock
#	src-tauri/Cargo.toml
#	src-tauri/src/main.rs
#	src-tauri/src/webaudiobridge.rs
2023-09-18 14:18:55 +04:00
Vasilii Milovidov 081f93cb79 webaudio for tauri 2023-09-18 14:18:00 +04:00
Vasilii Milovidov 6d5fb0687c rust webaudio for tauri tests 2023-09-05 01:59:25 +04:00
144 changed files with 6638 additions and 9276 deletions
+1 -3
View File
@@ -18,6 +18,4 @@ vite.config.js
**/*.json
**/dev-dist
**/dist
/src-tauri/target/**/*
reverbGen.mjs
hydra.mjs
/src-tauri/target/**/*
+3 -3
View File
@@ -13,7 +13,7 @@ To get in touch with the contributors, either
## Ask a Question
If you have any questions about strudel, make sure you've glanced through the
[docs](https://strudel.cc/learn/) to find out if it answers your question.
[docs](https://strudel.tidalcycles.org/learn/) to find out if it answers your question.
If not, use one of the Communication Channels above!
Don't be afraid to ask! Your question might be of great value for other people too.
@@ -31,7 +31,7 @@ Use one of the Communication Channels listed above.
## Improve the Docs
If you find some weak spots in the [docs](https://strudel.cc/workshop/getting-started/),
If you find some weak spots in the [docs](https://strudel.tidalcycles.org/workshop/getting-started/),
you can edit each file directly on github via the "Edit this page" link located in the right sidebar.
## Propose a Feature
@@ -83,7 +83,7 @@ Please report any problems you've had with the setup instructions!
To make sure the code changes only where it should, we are using prettier to unify the code style.
- You can format all files at once by running `pnpm codeformat` from the project root
- You can format all files at once by running `pnpm prettier` from the project root
- Run `pnpm format-check` from the project root to check if all files are well formatted
If you use VSCode, you can
+2 -2
View File
@@ -4,8 +4,8 @@
An experiment in making a [Tidal](https://github.com/tidalcycles/tidal/) using web technologies. This software is slowly stabilising, but please continue to tread carefully.
- Try it here: <https://strudel.cc>
- Docs: <https://strudel.cc/learn>
- Try it here: <https://strudel.tidalcycles.org/>
- Docs: <https://strudel.tidalcycles.org/learn/>
- Technical Blog Post: <https://loophole-letters.vercel.app/strudel>
- 1 Year of Strudel Blog Post: <https://loophole-letters.vercel.app/strudel1year>
+1 -1
View File
@@ -47,7 +47,7 @@ If you want to automatically deploy your site on push, go to `deploy.yml` and ch
## running locally
- install dependencies with `npm run setup`
- run dev server with `npm run repl` and open `http://localhost:4321/strudel/swatch/`
- run dev server with `npm run repl` and open `http://localhost:3000/strudel/swatch/`
## tests fail?
+1 -1
View File
@@ -43,7 +43,7 @@
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
},
"homepage": "https://strudel.cc",
"homepage": "https://strudel.tidalcycles.org",
"dependencies": {
"@strudel.cycles/core": "workspace:*",
"@strudel.cycles/mini": "workspace:*",
-88
View File
@@ -1,88 +0,0 @@
import { createRoot } from 'react-dom/client';
import jsdoc from '../../doc.json';
// import { javascriptLanguage } from '@codemirror/lang-javascript';
import { autocompletion } from '@codemirror/autocomplete';
const getDocLabel = (doc) => doc.name || doc.longname;
const getInnerText = (html) => {
var div = document.createElement('div');
div.innerHTML = html;
return div.textContent || div.innerText || '';
};
export function Autocomplete({ doc }) {
return (
<div className="prose dark:prose-invert max-h-[400px] overflow-auto">
<h3 className="pt-0 mt-0">{getDocLabel(doc)}</h3>
<div dangerouslySetInnerHTML={{ __html: doc.description }} />
<ul>
{doc.params?.map(({ name, type, description }, i) => (
<li key={i}>
{name} : {type.names?.join(' | ')} {description ? <> - {getInnerText(description)}</> : ''}
</li>
))}
</ul>
<div>
{doc.examples?.map((example, i) => (
<div key={i}>
<pre
className="cursor-pointer"
onMouseDown={(e) => {
console.log('ola!');
navigator.clipboard.writeText(example);
e.stopPropagation();
}}
>
{example}
</pre>
</div>
))}
</div>
</div>
);
}
const jsdocCompletions = jsdoc.docs
.filter(
(doc) =>
getDocLabel(doc) &&
!getDocLabel(doc).startsWith('_') &&
!['package'].includes(doc.kind) &&
!['superdirtOnly', 'noAutocomplete'].some((tag) => doc.tags?.find((t) => t.originalTitle === tag)),
)
// https://codemirror.net/docs/ref/#autocomplete.Completion
.map((doc) /*: Completion */ => ({
label: getDocLabel(doc),
// detail: 'xxx', // An optional short piece of information to show (with a different style) after the label.
info: () => {
const node = document.createElement('div');
// if Autocomplete is non-interactive, it could also be rendered at build time..
// .. using renderToStaticMarkup
createRoot(node).render(<Autocomplete doc={doc} />);
return node;
},
type: 'function', // https://codemirror.net/docs/ref/#autocomplete.Completion.type
}));
export const strudelAutocomplete = (context /* : CompletionContext */) => {
let word = context.matchBefore(/\w*/);
if (word.from == word.to && !context.explicit) return null;
return {
from: word.from,
options: jsdocCompletions,
/* options: [
{ label: 'match', type: 'keyword' },
{ label: 'hello', type: 'variable', info: '(World)' },
{ label: 'magic', type: 'text', apply: '⠁⭒*.✩.*⭒⠁', detail: 'macro' },
], */
};
};
export function isAutoCompletionEnabled(on) {
return on
? [
autocompletion({ override: [strudelAutocomplete] }),
//javascriptLanguage.data.of({ autocomplete: strudelAutocomplete }),
]
: []; // autocompletion({ override: [] })
}
+40 -169
View File
@@ -1,78 +1,36 @@
import { closeBrackets } from '@codemirror/autocomplete';
// import { search, highlightSelectionMatches } from '@codemirror/search';
import { history } from '@codemirror/commands';
import { defaultKeymap } from '@codemirror/commands';
import { javascript } from '@codemirror/lang-javascript';
import { defaultHighlightStyle, syntaxHighlighting } from '@codemirror/language';
import { Compartment, EditorState } from '@codemirror/state';
import { EditorView, highlightActiveLineGutter, highlightActiveLine, keymap, lineNumbers } from '@codemirror/view';
import { Pattern, Drawer, repl, cleanupDraw } from '@strudel.cycles/core';
import { isAutoCompletionEnabled } from './Autocomplete';
import { flash, isFlashEnabled } from './flash.mjs';
import { highlightMiniLocations, isPatternHighlightingEnabled, updateMiniLocations } from './highlight.mjs';
import { keybindings } from './keybindings.mjs';
import { theme } from './themes.mjs';
import { updateWidgets, sliderPlugin } from './slider.mjs';
const extensions = {
isLineWrappingEnabled: (on) => (on ? EditorView.lineWrapping : []),
isLineNumbersDisplayed: (on) => (on ? lineNumbers() : []),
theme,
isAutoCompletionEnabled,
isPatternHighlightingEnabled,
isFlashEnabled,
keybindings,
};
const compartments = Object.fromEntries(Object.keys(extensions).map((key) => [key, new Compartment()]));
import { EditorState } from '@codemirror/state';
import { EditorView, highlightActiveLineGutter, keymap, lineNumbers } from '@codemirror/view';
import { Drawer, repl } from '@strudel.cycles/core';
import { flashField, flash } from './flash.mjs';
import { highlightExtension, highlightMiniLocations } from './highlight.mjs';
import { oneDark } from './themes/one-dark';
// https://codemirror.net/docs/guide/
export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, settings, root }) {
const initialSettings = Object.keys(compartments).map((key) =>
compartments[key].of(extensions[key](parseBooleans(settings[key]))),
);
export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, theme = oneDark, root }) {
let state = EditorState.create({
doc: initialCode,
extensions: [
/* search(),
highlightSelectionMatches(), */
...initialSettings,
theme,
javascript(),
sliderPlugin,
// indentOnInput(), // works without. already brought with javascript extension?
// bracketMatching(), // does not do anything
closeBrackets(),
lineNumbers(),
highlightExtension,
highlightActiveLineGutter(),
highlightActiveLine(),
syntaxHighlighting(defaultHighlightStyle),
history(),
keymap.of(defaultKeymap),
flashField,
EditorView.updateListener.of((v) => onChange(v)),
keymap.of([
{
key: 'Ctrl-Enter',
run: () => onEvaluate?.(),
},
{
key: 'Alt-Enter',
run: () => onEvaluate?.(),
run: () => onEvaluate(),
},
{
key: 'Ctrl-.',
run: () => onStop?.(),
run: () => onStop(),
},
{
key: 'Alt-.',
run: (_, e) => {
e.preventDefault();
onStop?.();
},
},
/* {
key: 'Ctrl-Shift-.',
run: () => (onPanic ? onPanic() : onStop?.()),
},
{
key: 'Ctrl-Shift-Enter',
run: () => (onReEvaluate ? onReEvaluate() : onEvaluate?.()),
}, */
]),
],
});
@@ -85,158 +43,71 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, set
export class StrudelMirror {
constructor(options) {
const { root, initialCode = '', onDraw, drawTime = [-2, 2], prebake, settings, ...replOptions } = options;
const { root, initialCode = '', onDraw, drawTime = [-2, 2], prebake, ...replOptions } = options;
this.code = initialCode;
this.root = root;
this.miniLocations = [];
this.widgets = [];
this.painters = [];
this.onDraw = onDraw;
const self = this;
this.drawer = new Drawer((haps, time) => {
const currentFrame = haps.filter((hap) => time >= hap.whole.begin && time <= hap.endClipped);
this.highlight(currentFrame, time);
this.onDraw?.(haps, time, currentFrame, this.painters);
onDraw?.(haps, time, currentFrame);
}, drawTime);
// this approach might not work with multiple repls on screen..
Pattern.prototype.onPaint = function (onPaint) {
self.painters.push(onPaint);
return this;
};
this.prebaked = prebake();
// this.drawFirstFrame();
const prebaked = prebake();
prebaked.then(async () => {
if (!onDraw) {
return;
}
const { scheduler, evaluate } = await this.repl;
// draw first frame instantly
prebaked.then(async () => {
await evaluate(this.code, false);
this.drawer.invalidate(scheduler);
onDraw?.(this.drawer.visibleHaps, 0, []);
});
});
this.repl = repl({
...replOptions,
onToggle: (started) => {
onToggle: async (started) => {
replOptions?.onToggle?.(started);
const { scheduler } = await this.repl;
if (started) {
this.drawer.start(this.repl.scheduler);
this.drawer.start(scheduler);
} else {
this.drawer.stop();
updateMiniLocations(this.editor, []);
cleanupDraw(false);
}
},
beforeEval: async () => {
cleanupDraw();
this.painters = [];
await this.prebaked;
await replOptions?.beforeEval?.();
await prebaked;
},
afterEval: (options) => {
// remember for when highlighting is toggled on
this.miniLocations = options.meta?.miniLocations;
this.widgets = options.meta?.widgets;
updateWidgets(this.editor, this.widgets);
updateMiniLocations(this.editor, this.miniLocations);
replOptions?.afterEval?.(options);
this.drawer.invalidate();
},
});
this.editor = initEditor({
root,
settings,
initialCode,
onChange: (v) => {
if (v.docChanged) {
this.code = v.state.doc.toString();
this.repl.setCode(this.code);
}
this.code = v.state.doc.toString();
},
onEvaluate: () => this.evaluate(),
onStop: () => this.stop(),
});
}
async drawFirstFrame() {
if (!this.onDraw) {
return;
}
// draw first frame instantly
await this.prebaked;
try {
await this.repl.evaluate(this.code, false);
this.drawer.invalidate(this.repl.scheduler);
this.onDraw?.(this.drawer.visibleHaps, 0, []);
} catch (err) {
console.warn('first frame could not be painted');
}
}
async evaluate() {
const { evaluate } = await this.repl;
this.flash();
await this.repl.evaluate(this.code);
await evaluate(this.code);
}
async stop() {
this.repl.scheduler.stop();
}
async toggle() {
if (this.repl.scheduler.started) {
this.repl.scheduler.stop();
} else {
this.evaluate();
}
const { scheduler } = await this.repl;
scheduler.stop();
}
flash(ms) {
flash(this.editor, ms);
}
highlight(haps, time) {
highlightMiniLocations(this.editor, time, haps);
}
setFontSize(size) {
this.root.style.fontSize = size + 'px';
}
setFontFamily(family) {
this.root.style.fontFamily = family;
}
reconfigureExtension(key, value) {
if (!extensions[key]) {
console.warn(`extension ${key} is not known`);
return;
}
value = parseBooleans(value);
const newValue = extensions[key](value, this);
this.editor.dispatch({
effects: compartments[key].reconfigure(newValue),
});
}
setLineWrappingEnabled(enabled) {
this.reconfigureExtension('isLineWrappingEnabled', enabled);
}
setLineNumbersDisplayed(enabled) {
this.reconfigureExtension('isLineNumbersDisplayed', enabled);
}
setTheme(theme) {
this.reconfigureExtension('theme', theme);
}
setAutocompletionEnabled(enabled) {
this.reconfigureExtension('isAutoCompletionEnabled', enabled);
}
updateSettings(settings) {
this.setFontSize(settings.fontSize);
this.setFontFamily(settings.fontFamily);
for (let key in extensions) {
this.reconfigureExtension(key, settings[key]);
}
}
changeSetting(key, value) {
if (extensions[key]) {
this.reconfigureExtension(key, value);
return;
} else if (key === 'fontFamily') {
this.setFontFamily(value);
} else if (key === 'fontSize') {
this.setFontSize(value);
}
}
setCode(code) {
const changes = { from: 0, to: this.editor.state.doc.length, insert: code };
this.editor.dispatch({ changes });
highlightMiniLocations(this.editor.view, time, haps);
}
}
function parseBooleans(value) {
return { true: true, false: false }[value] ?? value;
}
-2
View File
@@ -33,5 +33,3 @@ export const flash = (view, ms = 200) => {
view.dispatch({ effects: setFlash.of(false) });
}, ms);
};
export const isFlashEnabled = (on) => (on ? flashField : []);
-9
View File
@@ -124,12 +124,3 @@ const miniLocationHighlights = EditorView.decorations.compute([miniLocations, vi
});
export const highlightExtension = [miniLocations, visibleMiniLocations, miniLocationHighlights];
export const isPatternHighlightingEnabled = (on, config) => {
on &&
config &&
setTimeout(() => {
updateMiniLocations(config.editor, config.miniLocations);
}, 100);
return on ? highlightExtension : [];
};
-1
View File
@@ -1,4 +1,3 @@
export * from './codemirror.mjs';
export * from './highlight.mjs';
export * from './flash.mjs';
export * from './slider.mjs';
-31
View File
@@ -1,31 +0,0 @@
import { Prec } from '@codemirror/state';
import { keymap, ViewPlugin } from '@codemirror/view';
// import { searchKeymap } from '@codemirror/search';
import { emacs } from '@replit/codemirror-emacs';
import { vim } from '@replit/codemirror-vim';
import { vscodeKeymap } from '@replit/codemirror-vscode-keymap';
import { defaultKeymap, historyKeymap } from '@codemirror/commands';
const vscodePlugin = ViewPlugin.fromClass(
class {
constructor() {}
},
{
provide: () => {
return Prec.highest(keymap.of([...vscodeKeymap]));
},
},
);
const vscodeExtension = (options) => [vscodePlugin].concat(options ?? []);
const keymaps = {
vim,
emacs,
vscode: vscodeExtension,
};
export function keybindings(name) {
const active = keymaps[name];
return [keymap.of(defaultKeymap), keymap.of(historyKeymap), active ? active() : []];
// keymap.of(searchKeymap),
}
+1 -9
View File
@@ -33,21 +33,13 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@codemirror/autocomplete": "^6.6.0",
"@codemirror/commands": "^6.2.4",
"@codemirror/lang-javascript": "^6.1.7",
"@codemirror/language": "^6.6.0",
"@codemirror/search": "^6.0.0",
"@codemirror/state": "^6.2.0",
"@codemirror/view": "^6.10.0",
"@lezer/highlight": "^1.1.4",
"@replit/codemirror-emacs": "^6.0.1",
"@replit/codemirror-vim": "^6.0.14",
"@replit/codemirror-vscode-keymap": "^6.0.2",
"@strudel.cycles/core": "workspace:*",
"@uiw/codemirror-themes": "^4.19.16",
"@uiw/codemirror-themes-all": "^4.19.16",
"react-dom": "^18.2.0"
"@strudel.cycles/core": "workspace:*"
},
"devDependencies": {
"vite": "^4.3.3"
-135
View File
@@ -1,135 +0,0 @@
import { ref, pure } from '@strudel.cycles/core';
import { WidgetType, ViewPlugin, Decoration } from '@codemirror/view';
import { StateEffect, StateField } from '@codemirror/state';
export let sliderValues = {};
const getSliderID = (from) => `slider_${from}`;
export class SliderWidget extends WidgetType {
constructor(value, min, max, from, to, step, view) {
super();
this.value = value;
this.min = min;
this.max = max;
this.from = from;
this.originalFrom = from;
this.to = to;
this.step = step;
this.view = view;
}
eq() {
return false;
}
toDOM() {
let wrap = document.createElement('span');
wrap.setAttribute('aria-hidden', 'true');
wrap.className = 'cm-slider'; // inline-flex items-center
let slider = wrap.appendChild(document.createElement('input'));
slider.type = 'range';
slider.min = this.min;
slider.max = this.max;
slider.step = this.step ?? (this.max - this.min) / 1000;
slider.originalValue = this.value;
// to make sure the code stays in sync, let's save the original value
// becuase .value automatically clamps values so it'll desync with the code
slider.value = slider.originalValue;
slider.from = this.from;
slider.originalFrom = this.originalFrom;
slider.to = this.to;
slider.style = 'width:64px;margin-right:4px;transform:translateY(4px)';
this.slider = slider;
slider.addEventListener('input', (e) => {
const next = e.target.value;
let insert = next;
//let insert = next.toFixed(2);
const to = slider.from + slider.originalValue.length;
let change = { from: slider.from, to, insert };
slider.originalValue = insert;
slider.value = insert;
this.view.dispatch({ changes: change });
const id = getSliderID(slider.originalFrom); // matches id generated in transpiler
window.postMessage({ type: 'cm-slider', value: Number(next), id });
});
return wrap;
}
ignoreEvent(e) {
return true;
}
}
export const setWidgets = StateEffect.define();
export const updateWidgets = (view, widgets) => {
view.dispatch({ effects: setWidgets.of(widgets) });
};
function getWidgets(widgetConfigs, view) {
return widgetConfigs.map(({ from, to, value, min, max, step }) => {
return Decoration.widget({
widget: new SliderWidget(value, min, max, from, to, step, view),
side: 0,
}).range(from /* , to */);
});
}
export const sliderPlugin = ViewPlugin.fromClass(
class {
decorations; //: DecorationSet
constructor(view /* : EditorView */) {
this.decorations = Decoration.set([]);
}
update(update /* : ViewUpdate */) {
update.transactions.forEach((tr) => {
if (tr.docChanged) {
this.decorations = this.decorations.map(tr.changes);
const iterator = this.decorations.iter();
while (iterator.value) {
// when the widgets are moved, we need to tell the dom node the current position
// this is important because the updateSliderValue function has to work with the dom node
if (iterator.value?.widget?.slider) {
iterator.value.widget.slider.from = iterator.from;
iterator.value.widget.slider.to = iterator.to;
}
iterator.next();
}
}
for (let e of tr.effects) {
if (e.is(setWidgets)) {
this.decorations = Decoration.set(getWidgets(e.value, update.view));
}
}
});
}
},
{
decorations: (v) => v.decorations,
},
);
export let slider = (value) => {
console.warn('slider will only work when the transpiler is used... passing value as is');
return pure(value);
};
// function transpiled from slider = (value, min, max)
export let sliderWithID = (id, value, min, max) => {
sliderValues[id] = value; // sync state at eval time (code -> state)
return ref(() => sliderValues[id]); // use state at query time
};
// update state when sliders are moved
if (typeof window !== 'undefined') {
window.addEventListener('message', (e) => {
if (e.data.type === 'cm-slider') {
if (sliderValues[e.data.id] !== undefined) {
// update state when slider is moved
sliderValues[e.data.id] = e.data.value;
} else {
console.warn(`slider with id "${e.data.id}" is not registered. Only ${Object.keys(sliderValues)}`);
}
}
});
}
-484
View File
@@ -1,484 +0,0 @@
import {
abcdef,
androidstudio,
atomone,
aura,
bespin,
darcula,
dracula,
duotoneDark,
eclipse,
githubDark,
gruvboxDark,
materialDark,
nord,
okaidia,
solarizedDark,
sublime,
tokyoNight,
tokyoNightStorm,
vscodeDark,
xcodeDark,
bbedit,
duotoneLight,
githubLight,
gruvboxLight,
materialLight,
noctisLilac,
solarizedLight,
tokyoNightDay,
xcodeLight,
} from '@uiw/codemirror-themes-all';
import strudelTheme from './themes/strudel-theme';
import bluescreen, { settings as bluescreenSettings } from './themes/bluescreen';
import blackscreen, { settings as blackscreenSettings } from './themes/blackscreen';
import whitescreen, { settings as whitescreenSettings } from './themes/whitescreen';
import teletext, { settings as teletextSettings } from './themes/teletext';
import algoboy, { settings as algoboySettings } from './themes/algoboy';
import terminal, { settings as terminalSettings } from './themes/terminal';
export const themes = {
strudelTheme,
bluescreen,
blackscreen,
whitescreen,
teletext,
algoboy,
terminal,
abcdef,
androidstudio,
atomone,
aura,
bespin,
darcula,
dracula,
duotoneDark,
eclipse,
githubDark,
gruvboxDark,
materialDark,
nord,
okaidia,
solarizedDark,
sublime,
tokyoNight,
tokyoNightStorm,
vscodeDark,
xcodeDark,
bbedit,
duotoneLight,
githubLight,
gruvboxLight,
materialLight,
noctisLilac,
solarizedLight,
tokyoNightDay,
xcodeLight,
};
// lineBackground is background with 50% opacity, to make sure the selection below is visible
export const settings = {
strudelTheme: {
background: '#222',
lineBackground: '#22222299',
foreground: '#fff',
// foreground: '#75baff',
caret: '#ffcc00',
selection: 'rgba(128, 203, 196, 0.5)',
selectionMatch: '#036dd626',
// lineHighlight: '#8a91991a', // original
lineHighlight: '#00000050',
gutterBackground: 'transparent',
// gutterForeground: '#8a919966',
gutterForeground: '#8a919966',
},
bluescreen: bluescreenSettings,
blackscreen: blackscreenSettings,
whitescreen: whitescreenSettings,
teletext: teletextSettings,
algoboy: algoboySettings,
terminal: terminalSettings,
abcdef: {
background: '#0f0f0f',
lineBackground: '#0f0f0f99',
foreground: '#defdef',
caret: '#00FF00',
selection: '#515151',
selectionMatch: '#515151',
gutterBackground: '#555',
gutterForeground: '#FFFFFF',
lineHighlight: '#314151',
},
androidstudio: {
background: '#282b2e',
lineBackground: '#282b2e99',
foreground: '#a9b7c6',
caret: '#00FF00',
selection: '#343739',
selectionMatch: '#343739',
lineHighlight: '#343739',
},
atomone: {
background: '#272C35',
lineBackground: '#272C3599',
foreground: '#9d9b97',
caret: '#797977',
selection: '#ffffff30',
selectionMatch: '#2B323D',
gutterBackground: '#272C35',
gutterForeground: '#465063',
gutterBorder: 'transparent',
lineHighlight: '#2B323D',
},
aura: {
background: '#21202e',
lineBackground: '#21202e99',
foreground: '#edecee',
caret: '#a277ff',
selection: '#3d375e7f',
selectionMatch: '#3d375e7f',
gutterBackground: '#21202e',
gutterForeground: '#edecee',
gutterBorder: 'transparent',
lineHighlight: '#a394f033',
},
bbedit: {
light: true,
background: '#FFFFFF',
lineBackground: '#FFFFFF99',
foreground: '#000000',
caret: '#FBAC52',
selection: '#FFD420',
selectionMatch: '#FFD420',
gutterBackground: '#f5f5f5',
gutterForeground: '#4D4D4C',
gutterBorder: 'transparent',
lineHighlight: '#00000012',
},
bespin: {
background: '#28211c',
lineBackground: '#28211c99',
foreground: '#9d9b97',
caret: '#797977',
selection: '#36312e',
selectionMatch: '#4f382b',
gutterBackground: '#28211c',
gutterForeground: '#666666',
lineHighlight: 'rgba(255, 255, 255, 0.1)',
},
darcula: {
background: '#2B2B2B',
lineBackground: '#2B2B2B99',
foreground: '#f8f8f2',
caret: '#FFFFFF',
selection: 'rgba(255, 255, 255, 0.1)',
selectionMatch: 'rgba(255, 255, 255, 0.2)',
gutterBackground: 'rgba(255, 255, 255, 0.1)',
gutterForeground: '#999',
gutterBorder: 'transparent',
lineHighlight: 'rgba(255, 255, 255, 0.1)',
},
dracula: {
background: '#282a36',
lineBackground: '#282a3699',
foreground: '#f8f8f2',
caret: '#f8f8f0',
selection: 'rgba(255, 255, 255, 0.1)',
selectionMatch: 'rgba(255, 255, 255, 0.2)',
gutterBackground: '#282a36',
gutterForeground: '#6D8A88',
gutterBorder: 'transparent',
lineHighlight: 'rgba(255, 255, 255, 0.1)',
},
duotoneLight: {
light: true,
background: '#faf8f5',
lineBackground: '#faf8f599',
foreground: '#b29762',
caret: '#93abdc',
selection: '#e3dcce',
selectionMatch: '#e3dcce',
gutterBackground: '#faf8f5',
gutterForeground: '#cdc4b1',
gutterBorder: 'transparent',
lineHighlight: '#EFEFEF',
},
duotoneDark: {
background: '#2a2734',
lineBackground: '#2a273499',
foreground: '#6c6783',
caret: '#ffad5c',
selection: 'rgba(255, 255, 255, 0.1)',
gutterBackground: '#2a2734',
gutterForeground: '#545167',
lineHighlight: '#36334280',
},
eclipse: {
light: true,
background: '#fff',
lineBackground: '#ffffff99',
foreground: '#000',
caret: '#FFFFFF',
selection: '#d7d4f0',
selectionMatch: '#d7d4f0',
gutterBackground: '#f7f7f7',
gutterForeground: '#999',
lineHighlight: '#e8f2ff',
gutterBorder: 'transparent',
},
githubLight: {
light: true,
background: '#fff',
lineBackground: '#ffffff99',
foreground: '#24292e',
selection: '#BBDFFF',
selectionMatch: '#BBDFFF',
gutterBackground: '#fff',
gutterForeground: '#6e7781',
},
githubDark: {
background: '#0d1117',
lineBackground: '#0d111799',
foreground: '#c9d1d9',
caret: '#c9d1d9',
selection: '#003d73',
selectionMatch: '#003d73',
lineHighlight: '#36334280',
},
gruvboxDark: {
background: '#282828',
lineBackground: '#28282899',
foreground: '#ebdbb2',
caret: '#ebdbb2',
selection: '#bdae93',
selectionMatch: '#bdae93',
lineHighlight: '#3c3836',
gutterBackground: '#282828',
gutterForeground: '#7c6f64',
},
gruvboxLight: {
light: true,
background: '#fbf1c7',
lineBackground: '#fbf1c799',
foreground: '#3c3836',
caret: '#af3a03',
selection: '#ebdbb2',
selectionMatch: '#bdae93',
lineHighlight: '#ebdbb2',
gutterBackground: '#ebdbb2',
gutterForeground: '#665c54',
gutterBorder: 'transparent',
},
materialDark: {
background: '#2e3235',
lineBackground: '#2e323599',
foreground: '#bdbdbd',
caret: '#a0a4ae',
selection: '#d7d4f0',
selectionMatch: '#d7d4f0',
gutterBackground: '#2e3235',
gutterForeground: '#999',
gutterActiveForeground: '#4f5b66',
lineHighlight: '#545b61',
},
materialLight: {
light: true,
background: '#FAFAFA',
lineBackground: '#FAFAFA99',
foreground: '#90A4AE',
caret: '#272727',
selection: '#80CBC440',
selectionMatch: '#FAFAFA',
gutterBackground: '#FAFAFA',
gutterForeground: '#90A4AE',
gutterBorder: 'transparent',
lineHighlight: '#CCD7DA50',
},
noctisLilac: {
light: true,
background: '#f2f1f8',
lineBackground: '#f2f1f899',
foreground: '#0c006b',
caret: '#5c49e9',
selection: '#d5d1f2',
selectionMatch: '#d5d1f2',
gutterBackground: '#f2f1f8',
gutterForeground: '#0c006b70',
lineHighlight: '#e1def3',
},
nord: {
background: '#2e3440',
lineBackground: '#2e344099',
foreground: '#FFFFFF',
caret: '#FFFFFF',
selection: '#3b4252',
selectionMatch: '#e5e9f0',
gutterBackground: '#2e3440',
gutterForeground: '#4c566a',
gutterActiveForeground: '#d8dee9',
lineHighlight: '#4c566a',
},
okaidia: {
background: '#272822',
lineBackground: '#27282299',
foreground: '#FFFFFF',
caret: '#FFFFFF',
selection: '#49483E',
selectionMatch: '#49483E',
gutterBackground: '#272822',
gutterForeground: '#FFFFFF70',
lineHighlight: '#00000059',
},
solarizedLight: {
light: true,
background: '#fdf6e3',
lineBackground: '#fdf6e399',
foreground: '#657b83',
caret: '#586e75',
selection: '#dfd9c8',
selectionMatch: '#dfd9c8',
gutterBackground: '#00000010',
gutterForeground: '#657b83',
lineHighlight: '#dfd9c8',
},
solarizedDark: {
background: '#002b36',
lineBackground: '#002b3699',
foreground: '#93a1a1',
caret: '#839496',
selection: '#173541',
selectionMatch: '#aafe661a',
gutterBackground: '#00252f',
gutterForeground: '#839496',
lineHighlight: '#173541',
},
sublime: {
background: '#303841',
lineBackground: '#30384199',
foreground: '#FFFFFF',
caret: '#FBAC52',
selection: '#4C5964',
selectionMatch: '#3A546E',
gutterBackground: '#303841',
gutterForeground: '#FFFFFF70',
lineHighlight: '#00000059',
},
tokyoNightDay: {
light: true,
background: '#e1e2e7',
lineBackground: '#e1e2e799',
foreground: '#3760bf',
caret: '#3760bf',
selection: '#99a7df',
selectionMatch: '#99a7df',
gutterBackground: '#e1e2e7',
gutterForeground: '#3760bf',
gutterBorder: 'transparent',
lineHighlight: '#5f5faf11',
},
tokyoNightStorm: {
background: '#24283b',
lineBackground: '#24283b99',
foreground: '#7982a9',
caret: '#c0caf5',
selection: '#6f7bb630',
selectionMatch: '#1f2335',
gutterBackground: '#24283b',
gutterForeground: '#7982a9',
gutterBorder: 'transparent',
lineHighlight: '#292e42',
},
tokyoNight: {
background: '#1a1b26',
lineBackground: '#1a1b2699',
foreground: '#787c99',
caret: '#c0caf5',
selection: '#515c7e40',
selectionMatch: '#16161e',
gutterBackground: '#1a1b26',
gutterForeground: '#787c99',
gutterBorder: 'transparent',
lineHighlight: '#1e202e',
},
vscodeDark: {
background: '#1e1e1e',
lineBackground: '#1e1e1e99',
foreground: '#9cdcfe',
caret: '#c6c6c6',
selection: '#6199ff2f',
selectionMatch: '#72a1ff59',
lineHighlight: '#ffffff0f',
gutterBackground: '#1e1e1e',
gutterForeground: '#838383',
gutterActiveForeground: '#fff',
},
xcodeLight: {
light: true,
background: '#fff',
lineBackground: '#ffffff99',
foreground: '#3D3D3D',
selection: '#BBDFFF',
selectionMatch: '#BBDFFF',
gutterBackground: '#fff',
gutterForeground: '#AFAFAF',
lineHighlight: '#EDF4FF',
},
xcodeDark: {
background: '#292A30',
lineBackground: '#292A3099',
foreground: '#CECFD0',
caret: '#fff',
selection: '#727377',
selectionMatch: '#727377',
lineHighlight: '#2F3239',
},
};
function getColors(str) {
const colorRegex = /#([0-9A-Fa-f]{6}|[0-9A-Fa-f]{3})/g;
const colors = [];
let match;
while ((match = colorRegex.exec(str)) !== null) {
const color = match[0];
if (!colors.includes(color)) {
colors.push(color);
}
}
return colors;
}
// TODO: remove
export function themeColors(theme) {
return getColors(stringifySafe(theme));
}
function getCircularReplacer() {
const seen = new WeakSet();
return (key, value) => {
if (typeof value === 'object' && value !== null) {
if (seen.has(value)) {
return;
}
seen.add(value);
}
return value;
};
}
function stringifySafe(json) {
return JSON.stringify(json, getCircularReplacer());
}
export function injectStyle(rule) {
const newStyle = document.createElement('style');
document.head.appendChild(newStyle);
const styleSheet = newStyle.sheet;
const ruleIndex = styleSheet.insertRule(rule, 0);
return () => styleSheet.deleteRule(ruleIndex);
}
export const theme = (theme) => themes[theme] || themes.strudelTheme;
-41
View File
@@ -1,41 +0,0 @@
import { tags as t } from '@lezer/highlight';
import { createTheme } from '@uiw/codemirror-themes';
export const settings = {
background: '#9bbc0f',
foreground: '#0f380f', // whats that?
caret: '#0f380f',
selection: '#306230',
selectionMatch: '#ffffff26',
lineHighlight: '#8bac0f',
lineBackground: '#9bbc0f50',
//lineBackground: 'transparent',
gutterBackground: 'transparent',
gutterForeground: '#0f380f',
light: true,
customStyle: '.cm-line { line-height: 1 }',
};
export default createTheme({
theme: 'light',
settings,
styles: [
{ tag: t.keyword, color: '#0f380f' },
{ tag: t.operator, color: '#0f380f' },
{ tag: t.special(t.variableName), color: '#0f380f' },
{ tag: t.typeName, color: '#0f380f' },
{ tag: t.atom, color: '#0f380f' },
{ tag: t.number, color: '#0f380f' },
{ tag: t.definition(t.variableName), color: '#0f380f' },
{ tag: t.string, color: '#0f380f' },
{ tag: t.special(t.string), color: '#0f380f' },
{ tag: t.comment, color: '#0f380f' },
{ tag: t.variableName, color: '#0f380f' },
{ tag: t.tagName, color: '#0f380f' },
{ tag: t.bracket, color: '#0f380f' },
{ tag: t.meta, color: '#0f380f' },
{ tag: t.attributeName, color: '#0f380f' },
{ tag: t.propertyName, color: '#0f380f' },
{ tag: t.className, color: '#0f380f' },
{ tag: t.invalid, color: '#0f380f' },
{ tag: [t.unit, t.punctuation], color: '#0f380f' },
],
});
-38
View File
@@ -1,38 +0,0 @@
import { tags as t } from '@lezer/highlight';
import { createTheme } from '@uiw/codemirror-themes';
export const settings = {
background: 'black',
foreground: 'white', // whats that?
caret: 'white',
selection: '#ffffff20',
selectionMatch: '#036dd626',
lineHighlight: '#ffffff10',
lineBackground: '#00000050',
gutterBackground: 'transparent',
gutterForeground: '#8a919966',
};
export default createTheme({
theme: 'dark',
settings,
styles: [
{ tag: t.keyword, color: 'white' },
{ tag: t.operator, color: 'white' },
{ tag: t.special(t.variableName), color: 'white' },
{ tag: t.typeName, color: 'white' },
{ tag: t.atom, color: 'white' },
{ tag: t.number, color: 'white' },
{ tag: t.definition(t.variableName), color: 'white' },
{ tag: t.string, color: 'white' },
{ tag: t.special(t.string), color: 'white' },
{ tag: t.comment, color: 'white' },
{ tag: t.variableName, color: 'white' },
{ tag: t.tagName, color: 'white' },
{ tag: t.bracket, color: 'white' },
{ tag: t.meta, color: 'white' },
{ tag: t.attributeName, color: 'white' },
{ tag: t.propertyName, color: 'white' },
{ tag: t.className, color: 'white' },
{ tag: t.invalid, color: 'white' },
{ tag: [t.unit, t.punctuation], color: 'white' },
],
});
-41
View File
@@ -1,41 +0,0 @@
import { tags as t } from '@lezer/highlight';
import { createTheme } from '@uiw/codemirror-themes';
export const settings = {
background: '#051DB5',
lineBackground: '#051DB550',
foreground: 'white', // whats that?
caret: 'white',
selection: 'rgba(128, 203, 196, 0.5)',
selectionMatch: '#036dd626',
// lineHighlight: '#8a91991a', // original
lineHighlight: '#00000050',
gutterBackground: 'transparent',
// gutterForeground: '#8a919966',
gutterForeground: '#8a919966',
};
export default createTheme({
theme: 'dark',
settings,
styles: [
{ tag: t.keyword, color: 'white' },
{ tag: t.operator, color: 'white' },
{ tag: t.special(t.variableName), color: 'white' },
{ tag: t.typeName, color: 'white' },
{ tag: t.atom, color: 'white' },
{ tag: t.number, color: 'white' },
{ tag: t.definition(t.variableName), color: 'white' },
{ tag: t.string, color: 'white' },
{ tag: t.special(t.string), color: 'white' },
{ tag: t.comment, color: 'white' },
{ tag: t.variableName, color: 'white' },
{ tag: t.tagName, color: 'white' },
{ tag: t.bracket, color: 'white' },
{ tag: t.meta, color: 'white' },
{ tag: t.attributeName, color: 'white' },
{ tag: t.propertyName, color: 'white' },
{ tag: t.className, color: 'white' },
{ tag: t.invalid, color: 'white' },
{ tag: [t.unit, t.punctuation], color: 'white' },
],
});
+139
View File
@@ -0,0 +1,139 @@
import { EditorView } from '@codemirror/view';
import { HighlightStyle, syntaxHighlighting } from '@codemirror/language';
import { tags as t } from '@lezer/highlight';
// Using https://github.com/one-dark/vscode-one-dark-theme/ as reference for the colors
const chalky = '#e5c07b',
coral = '#e06c75',
cyan = '#56b6c2',
invalid = '#ffffff',
ivory = '#abb2bf',
stone = '#7d8799', // Brightened compared to original to increase contrast
malibu = '#61afef',
sage = '#98c379',
whiskey = '#d19a66',
violet = '#c678dd',
darkBackground = '#21252b',
highlightBackground = '#2c313a',
background = '#282c34',
tooltipBackground = '#353a42',
selection = '#3E4451',
cursor = '#528bff';
/// The colors used in the theme, as CSS color strings.
export const color = {
chalky,
coral,
cyan,
invalid,
ivory,
stone,
malibu,
sage,
whiskey,
violet,
darkBackground,
highlightBackground,
background,
tooltipBackground,
selection,
cursor,
};
/// The editor theme styles for One Dark.
export const oneDarkTheme = EditorView.theme(
{
'&': {
color: ivory,
backgroundColor: background,
},
'.cm-content': {
caretColor: cursor,
},
'.cm-cursor, .cm-dropCursor': { borderLeftColor: cursor },
'&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection':
{ backgroundColor: selection },
'.cm-panels': { backgroundColor: darkBackground, color: ivory },
'.cm-panels.cm-panels-top': { borderBottom: '2px solid black' },
'.cm-panels.cm-panels-bottom': { borderTop: '2px solid black' },
'.cm-searchMatch': {
backgroundColor: '#72a1ff59',
outline: '1px solid #457dff',
},
'.cm-searchMatch.cm-searchMatch-selected': {
backgroundColor: '#6199ff2f',
},
'.cm-activeLine': { backgroundColor: '#6699ff0b' },
'.cm-selectionMatch': { backgroundColor: '#aafe661a' },
'&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': {
backgroundColor: '#bad0f847',
},
'.cm-gutters': {
backgroundColor: background,
color: stone,
border: 'none',
},
'.cm-activeLineGutter': {
backgroundColor: highlightBackground,
},
'.cm-foldPlaceholder': {
backgroundColor: 'transparent',
border: 'none',
color: '#ddd',
},
'.cm-tooltip': {
border: 'none',
backgroundColor: tooltipBackground,
},
'.cm-tooltip .cm-tooltip-arrow:before': {
borderTopColor: 'transparent',
borderBottomColor: 'transparent',
},
'.cm-tooltip .cm-tooltip-arrow:after': {
borderTopColor: tooltipBackground,
borderBottomColor: tooltipBackground,
},
'.cm-tooltip-autocomplete': {
'& > ul > li[aria-selected]': {
backgroundColor: highlightBackground,
color: ivory,
},
},
},
{ dark: true },
);
/// The highlighting style for code in the One Dark theme.
export const oneDarkHighlightStyle = HighlightStyle.define([
{ tag: t.keyword, color: violet },
{ tag: [t.name, t.deleted, t.character, t.propertyName, t.macroName], color: coral },
{ tag: [t.function(t.variableName), t.labelName], color: malibu },
{ tag: [t.color, t.constant(t.name), t.standard(t.name)], color: whiskey },
{ tag: [t.definition(t.name), t.separator], color: ivory },
{ tag: [t.typeName, t.className, t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: chalky },
{ tag: [t.operator, t.operatorKeyword, t.url, t.escape, t.regexp, t.link, t.special(t.string)], color: cyan },
{ tag: [t.meta, t.comment], color: stone },
{ tag: t.strong, fontWeight: 'bold' },
{ tag: t.emphasis, fontStyle: 'italic' },
{ tag: t.strikethrough, textDecoration: 'line-through' },
{ tag: t.link, color: stone, textDecoration: 'underline' },
{ tag: t.heading, fontWeight: 'bold', color: coral },
{ tag: [t.atom, t.bool, t.special(t.variableName)], color: whiskey },
{ tag: [t.processingInstruction, t.string, t.inserted], color: sage },
{ tag: t.invalid, color: invalid },
]);
/// Extension to enable the One Dark theme (both the editor theme and
/// the highlight style).
export const oneDark = [oneDarkTheme, syntaxHighlighting(oneDarkHighlightStyle)];
-45
View File
@@ -1,45 +0,0 @@
import { tags as t } from '@lezer/highlight';
import { createTheme } from '@uiw/codemirror-themes';
export default createTheme({
theme: 'dark',
settings: {
background: '#222',
foreground: '#75baff', // whats that?
caret: '#ffcc00',
selection: 'rgba(128, 203, 196, 0.5)',
selectionMatch: '#036dd626',
// lineHighlight: '#8a91991a', // original
lineHighlight: '#00000050',
gutterBackground: 'transparent',
// gutterForeground: '#8a919966',
gutterForeground: '#8a919966',
},
styles: [
{ tag: t.keyword, color: '#c792ea' },
{ tag: t.operator, color: '#89ddff' },
{ tag: t.special(t.variableName), color: '#eeffff' },
// { tag: t.typeName, color: '#f07178' }, // original
{ tag: t.typeName, color: '#c3e88d' },
{ tag: t.atom, color: '#f78c6c' },
// { tag: t.number, color: '#ff5370' }, // original
{ tag: t.number, color: '#c3e88d' },
{ tag: t.definition(t.variableName), color: '#82aaff' },
{ tag: t.string, color: '#c3e88d' },
// { tag: t.special(t.string), color: '#f07178' }, // original
{ tag: t.special(t.string), color: '#c3e88d' },
{ tag: t.comment, color: '#7d8799' },
// { tag: t.variableName, color: '#f07178' }, // original
{ tag: t.variableName, color: '#c792ea' },
// { tag: t.tagName, color: '#ff5370' }, // original
{ tag: t.tagName, color: '#c3e88d' },
{ tag: t.bracket, color: '#525154' },
// { tag: t.bracket, color: '#a2a1a4' }, // original
{ tag: t.meta, color: '#ffcb6b' },
{ tag: t.attributeName, color: '#c792ea' },
{ tag: t.propertyName, color: '#c792ea' },
{ tag: t.className, color: '#decb6b' },
{ tag: t.invalid, color: '#ffffff' },
{ tag: [t.unit, t.punctuation], color: '#82aaff' },
],
});
-50
View File
@@ -1,50 +0,0 @@
import { tags as t } from '@lezer/highlight';
import { createTheme } from '@uiw/codemirror-themes';
let colorA = '#6edee4';
//let colorB = 'magenta';
let colorB = 'white';
let colorC = 'red';
let colorD = '#f8fc55';
export const settings = {
background: '#000000',
foreground: colorA, // whats that?
caret: colorC,
selection: colorD,
selectionMatch: colorA,
lineHighlight: '#6edee440', // panel bg
lineBackground: '#00000040',
gutterBackground: 'transparent',
gutterForeground: '#8a919966',
customStyle: '.cm-line { line-height: 1 }',
};
let punctuation = colorD;
let mini = colorB;
export default createTheme({
theme: 'dark',
settings,
styles: [
{ tag: t.keyword, color: colorA },
{ tag: t.operator, color: mini },
{ tag: t.special(t.variableName), color: colorA },
{ tag: t.typeName, color: colorA },
{ tag: t.atom, color: colorA },
{ tag: t.number, color: mini },
{ tag: t.definition(t.variableName), color: colorA },
{ tag: t.string, color: mini },
{ tag: t.special(t.string), color: mini },
{ tag: t.comment, color: punctuation },
{ tag: t.variableName, color: colorA },
{ tag: t.tagName, color: colorA },
{ tag: t.bracket, color: punctuation },
{ tag: t.meta, color: colorA },
{ tag: t.attributeName, color: colorA },
{ tag: t.propertyName, color: colorA }, // methods
{ tag: t.className, color: colorA },
{ tag: t.invalid, color: colorC },
{ tag: [t.unit, t.punctuation], color: punctuation },
],
});
-36
View File
@@ -1,36 +0,0 @@
import { tags as t } from '@lezer/highlight';
import { createTheme } from '@uiw/codemirror-themes';
export const settings = {
background: 'black',
foreground: '#41FF00', // whats that?
caret: '#41FF00',
selection: '#ffffff20',
selectionMatch: '#036dd626',
lineHighlight: '#ffffff10',
gutterBackground: 'transparent',
gutterForeground: '#8a919966',
};
export default createTheme({
theme: 'dark',
settings,
styles: [
{ tag: t.keyword, color: '#41FF00' },
{ tag: t.operator, color: '#41FF00' },
{ tag: t.special(t.variableName), color: '#41FF00' },
{ tag: t.typeName, color: '#41FF00' },
{ tag: t.atom, color: '#41FF00' },
{ tag: t.number, color: '#41FF00' },
{ tag: t.definition(t.variableName), color: '#41FF00' },
{ tag: t.string, color: '#41FF00' },
{ tag: t.special(t.string), color: '#41FF00' },
{ tag: t.comment, color: '#41FF00' },
{ tag: t.variableName, color: '#41FF00' },
{ tag: t.tagName, color: '#41FF00' },
{ tag: t.bracket, color: '#41FF00' },
{ tag: t.meta, color: '#41FF00' },
{ tag: t.attributeName, color: '#41FF00' },
{ tag: t.propertyName, color: '#41FF00' },
{ tag: t.className, color: '#41FF00' },
{ tag: t.invalid, color: '#41FF00' },
],
});
-38
View File
@@ -1,38 +0,0 @@
import { tags as t } from '@lezer/highlight';
import { createTheme } from '@uiw/codemirror-themes';
export const settings = {
background: 'white',
foreground: 'black', // whats that?
caret: 'black',
selection: 'rgba(128, 203, 196, 0.5)',
selectionMatch: '#ffffff26',
lineHighlight: '#cccccc50',
lineBackground: '#ffffff50',
gutterBackground: 'transparent',
gutterForeground: 'black',
light: true,
};
export default createTheme({
theme: 'light',
settings,
styles: [
{ tag: t.keyword, color: 'black' },
{ tag: t.operator, color: 'black' },
{ tag: t.special(t.variableName), color: 'black' },
{ tag: t.typeName, color: 'black' },
{ tag: t.atom, color: 'black' },
{ tag: t.number, color: 'black' },
{ tag: t.definition(t.variableName), color: 'black' },
{ tag: t.string, color: 'black' },
{ tag: t.special(t.string), color: 'black' },
{ tag: t.comment, color: 'black' },
{ tag: t.variableName, color: 'black' },
{ tag: t.tagName, color: 'black' },
{ tag: t.bracket, color: 'black' },
{ tag: t.meta, color: 'black' },
{ tag: t.attributeName, color: 'black' },
{ tag: t.propertyName, color: 'black' },
{ tag: t.className, color: 'black' },
{ tag: t.invalid, color: 'black' },
],
});
+10 -116
View File
@@ -86,16 +86,6 @@ const generic_params = [
*
*/
['gain'],
/**
* Gain applied after all effects have been processed.
*
* @name postgain
* @example
* s("bd sd,hh*4")
* .compressor("-20:20:10:.002:.02").postgain(1.5)
*
*/
['postgain'],
/**
* Like {@link gain}, but linear.
*
@@ -380,62 +370,6 @@ const generic_params = [
*
*/
['coarse'],
['phaserrate', 'phasr'], // superdirt only
/**
* Phaser audio effect that approximates popular guitar pedals.
*
* @name phaser
* @synonyms ph
* @param {number | Pattern} speed speed of modulation
* @example
* n(run(8)).scale("D:pentatonic").s("sawtooth").release(0.5)
* .phaser("<1 2 4 8>")
*
*/
[['phaser', 'phaserdepth', 'phasercenter', 'phasersweep'], 'ph'],
/**
* The frequency sweep range of the lfo for the phaser effect. Defaults to 2000
*
* @name phasersweep
* @synonyms phs
* @param {number | Pattern} phasersweep most useful values are between 0 and 4000
* @example
* n(run(8)).scale("D:pentatonic").s("sawtooth").release(0.5)
* .phaser(2).phasersweep("<800 2000 4000>")
*
*/
['phasersweep', 'phs'],
/**
* The center frequency of the phaser in HZ. Defaults to 1000
*
* @name phasercenter
* @synonyms phc
* @param {number | Pattern} centerfrequency in HZ
* @example
* n(run(8)).scale("D:pentatonic").s("sawtooth").release(0.5)
* .phaser(2).phasercenter("<800 2000 4000>")
*
*/
['phasercenter', 'phc'],
/**
* The amount the signal is affected by the phaser effect. Defaults to 0.75
*
* @name phaserdepth
* @synonyms phd
* @param {number | Pattern} depth number between 0 and 1
* @example
* n(run(8)).scale("D:pentatonic").s("sawtooth").release(0.5)
* .phaser(2).phaserdepth("<0 .5 .75 1>")
*
*/
['phaserdepth', 'phd', 'phasdp'], // also a superdirt control
/**
* choose the channel the pattern is sent to in superdirt
*
@@ -721,15 +655,6 @@ const generic_params = [
* .vib("<.5 1 2 4 8 16>:12")
*/
[['vib', 'vibmod'], 'vibrato', 'v'],
/**
* Adds pink noise to the mix
*
* @name noise
* @param {number | Pattern} wet wet amount
* @example
* sound("<white pink brown>/2")
*/
['noise'],
/**
* Sets the vibrato depth in semitones. Only has an effect if `vibrato` | `vib` | `v` is is also set
*
@@ -923,12 +848,7 @@ const generic_params = [
*
*/
['lsize'],
/**
* Sets the displayed text for an event on the pianoroll
*
* @name label
* @param {string} label text to display
*/
// label for pianoroll
['activeLabel'],
[['label', 'activeLabel']],
// ['lfo'],
@@ -1093,9 +1013,9 @@ const generic_params = [
['roomfade', 'rfade'],
/**
* Sets the sample to use as an impulse response for the reverb.
*
* @name iresponse
* @param {string | Pattern} sample to use as an impulse response
* @synonyms ir
* @param {string | Pattern} sample sample to set as the impulse response
* @example
* s("bd sd").room(.8).ir("<shaker_large:0 shaker_large:2>")
*
@@ -1103,21 +1023,18 @@ const generic_params = [
[['ir', 'i'], 'iresponse'],
/**
* Sets the room size of the reverb, see {@link room}.
* When this property is changed, the reverb will be recaculated, so only change this sparsely..
*
* @name roomsize
* @param {number | Pattern} size between 0 and 10
* @synonyms rsize, sz, size
* @synonyms size, sz
* @example
* s("bd sd").room(.8).rsize(1)
* @example
* s("bd sd").room(.8).rsize(4)
* s("bd sd").room(.8).roomsize("<0 1 2 4 8>")
*
*/
// TODO: find out why :
// s("bd sd").room(.8).roomsize("<0 .2 .4 .6 .8 [1,0]>").osc()
// .. does not work. Is it because room is only one effect?
['roomsize', 'size', 'sz', 'rsize'],
['size', 'sz', 'roomsize'],
// ['sagogo'],
// ['sclap'],
// ['sclaves'],
@@ -1132,21 +1049,6 @@ const generic_params = [
*
*/
['shape'],
/**
* Dynamics Compressor. The params are `compressor("threshold:ratio:knee:attack:release")`
* More info [here](https://developer.mozilla.org/en-US/docs/Web/API/DynamicsCompressorNode?retiredLocale=de#instance_properties)
*
* @name compressor
* @example
* s("bd sd,hh*4")
* .compressor("-20:20:10:.002:.02")
*
*/
[['compressor', 'compressorRatio', 'compressorKnee', 'compressorAttack', 'compressorRelease']],
['compressorKnee'],
['compressorRatio'],
['compressorAttack'],
['compressorRelease'],
/**
* Changes the speed of sample playback, i.e. a cheap way of changing pitch.
*
@@ -1231,6 +1133,9 @@ const generic_params = [
*/
['tremolodepth', 'tremdp'],
['tremolorate', 'tremr'],
// TODO: doesn't seem to do anything
['phaserdepth', 'phasdp'],
['phaserrate', 'phasr'],
['fshift'],
['fshiftnote'],
@@ -1299,7 +1204,7 @@ const generic_params = [
['pitchJump'],
['pitchJumpTime'],
['lfo', 'repeatTime'],
['znoise'], // noise on the frequency or as bubo calls it "frequency fog" :)
['noise'],
['zmod'],
['zcrush'], // like crush but scaled differently
['zdelay'],
@@ -1355,17 +1260,6 @@ generic_params.forEach(([names, ...aliases]) => {
controls.createParams = (...names) =>
names.reduce((acc, name) => Object.assign(acc, { [name]: controls.createParam(name) }), {});
/**
* ADSR envelope: Combination of Attack, Decay, Sustain, and Release.
*
* @name adsr
* @param {number | Pattern} time attack time in seconds
* @param {number | Pattern} time decay time in seconds
* @param {number | Pattern} gain sustain level (0 to 1)
* @param {number | Pattern} time release time in seconds
* @example
* note("<c3 bb2 f3 eb3>").sound("sawtooth").lpf(600).adsr(".1:.1:.5:.2")
*/
controls.adsr = register('adsr', (adsr, pat) => {
adsr = !Array.isArray(adsr) ? [adsr] : adsr;
const [attack, decay, sustain, release] = adsr;
+1 -1
View File
@@ -146,7 +146,7 @@ export class Drawer {
);
}
invalidate(scheduler = this.scheduler) {
if (!scheduler || !scheduler.pattern) {
if (!scheduler) {
return;
}
this.scheduler = scheduler;
@@ -3,6 +3,6 @@
This folder demonstrates how to set up a strudel repl using vite and vanilla JS + codemirror. Run it using:
```sh
pnpm i
pnpm dev
npm i
npm run dev
```
@@ -1,6 +1,6 @@
export const bumpStreet = `// froos - "22 bump street", licensed with CC BY-NC-SA 4.0
await samples('github:felixroos/samples/main')
await samples('https://strudel.cc/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
await samples('https://strudel.tidalcycles.org/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
"<[0,<6 7 9>,13,<17 20 22 26>]!2>/2"
// make it 22 edo
@@ -34,7 +34,7 @@ await samples('https://strudel.cc/tidal-drum-machines.json', 'github:ritchse/tid
export const trafficFlam = `// froos - "traffic flam", licensed with CC BY-NC-SA 4.0
await samples('github:felixroos/samples/main')
await samples('https://strudel.cc/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
await samples('https://strudel.tidalcycles.org/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
addVoicings('hip', {
m11: ['2M 3m 4P 7m'],
@@ -70,7 +70,7 @@ export const funk42 = `// froos - how to funk in 42 lines of code
// thanks to peach for the transcription: https://www.youtube.com/watch?v=8eiPXvIgda4
await samples('github:felixroos/samples/main')
await samples('https://strudel.cc/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
await samples('https://strudel.tidalcycles.org/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
setcps(.5)
+2 -3
View File
@@ -29,10 +29,9 @@
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
},
"homepage": "https://strudel.cc",
"homepage": "https://strudel.tidalcycles.org",
"dependencies": {
"fraction.js": "^4.2.0",
"nanostores": "^0.8.1"
"fraction.js": "^4.2.0"
},
"gitHead": "0e26d4e741500f5bae35b023608f062a794905c2",
"devDependencies": {
+4 -82
View File
@@ -2100,43 +2100,23 @@ export const { iterBack, iterback } = register(['iterBack', 'iterback'], functio
return _iter(times, pat, true);
});
/**
* Repeats each cycle the given number of times.
* @name repeatCycles
* @memberof Pattern
* @returns Pattern
* @example
* note(irand(12).add(34)).segment(4).repeatCycles(2).s("gm_acoustic_guitar_nylon")
*/
const _repeatCycles = function (n, pat) {
return slowcat(...Array(n).fill(pat));
};
const { repeatCycles } = register('repeatCycles', _repeatCycles);
/**
* Divides a pattern into a given number of parts, then cycles through those parts in turn, applying the given function to each part in turn (one part per cycle).
* @name chunk
* @synonyms slowChunk, slowchunk
* @memberof Pattern
* @returns Pattern
* @example
* "0 1 2 3".chunk(4, x=>x.add(7)).scale('A minor').note()
*/
const _chunk = function (n, func, pat, back = false, fast = false) {
const _chunk = function (n, func, pat, back = false) {
const binary = Array(n - 1).fill(false);
binary.unshift(true);
// Invert the 'back' because we want to shift the pattern forwards,
// and so time backwards
const binary_pat = _iter(n, sequence(...binary), !back);
if (!fast) {
pat = pat.repeatCycles(n);
}
const binary_pat = _iter(n, sequence(...binary), back);
return pat.when(binary_pat, func);
};
const { chunk, slowchunk, slowChunk } = register(['chunk', 'slowchunk', 'slowChunk'], function (n, func, pat) {
return _chunk(n, func, pat, false, false);
export const chunk = register('chunk', function (n, func, pat) {
return _chunk(n, func, pat, false);
});
/**
@@ -2152,21 +2132,6 @@ export const { chunkBack, chunkback } = register(['chunkBack', 'chunkback'], fun
return _chunk(n, func, pat, true);
});
/**
* Like `chunk`, but the cycles of the source pattern aren't repeated
* for each set of chunks.
* @name fastChunk
* @synonyms fastchunk
* @memberof Pattern
* @returns Pattern
* @example
* "<0 8> 1 2 3 4 5 6 7".fastChunk(4, x => x.color('red')).slow(4).scale("C2:major").note()
.s("folkharp")
*/
const { fastchunk, fastChunk } = register(['fastchunk', 'fastChunk'], function (n, func, pat) {
return _chunk(n, func, pat, false, true);
});
// TODO - redefine elsewhere in terms of mask
export const bypass = register('bypass', function (on, pat) {
on = Boolean(parseInt(on));
@@ -2191,9 +2156,6 @@ export const duration = register('duration', function (value, pat) {
/**
* Sets the color of the hap in visualizations like pianoroll or highlighting.
* @name color
* @synonyms colour
* @param {string} color Hexadecimal or CSS color name
*/
// TODO: move this to controls https://github.com/tidalcycles/strudel/issues/288
export const { color, colour } = register(['color', 'colour'], function (color, pat) {
@@ -2255,14 +2217,6 @@ export const chop = register('chop', function (n, pat) {
return pat.squeezeBind(func);
});
/**
* Cuts each sample into the given number of parts, triggering progressive portions of each sample at each loop.
* @name striate
* @memberof Pattern
* @returns Pattern
* @example
* s("numbers:0 numbers:1 numbers:2").striate(6).slow(6)
*/
export const striate = register('striate', function (n, pat) {
const slices = Array.from({ length: n }, (x, i) => i);
const slice_objects = slices.map((i) => ({ begin: i / n, end: (i + 1) / n }));
@@ -2389,35 +2343,3 @@ export const fit = register('fit', (pat) =>
export const { loopAtCps, loopatcps } = register(['loopAtCps', 'loopatcps'], function (factor, cps, pat) {
return _loopAt(factor, pat, cps);
});
/** exposes a custom value at query time. basically allows mutating state without evaluation */
export const ref = (accessor) =>
pure(1)
.withValue(() => reify(accessor()))
.innerJoin();
let fadeGain = (p) => (p < 0.5 ? 1 : 1 - (p - 0.5) / 0.5);
/**
* Cross-fades between left and right from 0 to 1:
* - 0 = (full left, no right)
* - .5 = (both equal)
* - 1 = (no left, full right)
*
* @name xfade
* @example
* xfade(s("bd*2"), "<0 .25 .5 .75 1>", s("hh*8"))
*/
export let xfade = (a, pos, b) => {
pos = reify(pos);
a = reify(a);
b = reify(b);
let gaina = pos.fmap((v) => ({ gain: fadeGain(v) }));
let gainb = pos.fmap((v) => ({ gain: fadeGain(1 - v) }));
return stack(a.mul(gaina), b.mul(gainb));
};
// the prototype version is actually flipped so left/right makes sense
Pattern.prototype.xfade = function (pos, b) {
return xfade(this, pos, b);
};
+1 -36
View File
@@ -56,40 +56,6 @@ Pattern.prototype.pianoroll = function (options = {}) {
// this function allows drawing a pianoroll without ties to Pattern.prototype
// it will probably replace the above in the future
/**
* Displays a midi-style piano roll
*
* @name pianoroll
* @param {Object} options Object containing all the optional following parameters as key value pairs:
* @param {integer} cycles number of cycles to be displayed at the same time - defaults to 4
* @param {number} playhead location of the active notes on the time axis - 0 to 1, defaults to 0.5
* @param {boolean} vertical displays the roll vertically - 0 by default
* @param {boolean} labels displays labels on individual notes (see the label function) - 0 by default
* @param {boolean} flipTime reverse the direction of the roll - 0 by default
* @param {boolean} flipValues reverse the relative location of notes on the value axis - 0 by default
* @param {number} overscan lookup X cycles outside of the cycles window to display notes in advance - 1 by default
* @param {boolean} hideNegative hide notes with negative time (before starting playing the pattern) - 0 by default
* @param {boolean} smear notes leave a solid trace - 0 by default
* @param {boolean} fold notes takes the full value axis width - 0 by default
* @param {string} active hexadecimal or CSS color of the active notes - defaults to #FFCA28
* @param {string} inactive hexadecimal or CSS color of the inactive notes - defaults to #7491D2
* @param {string} background hexadecimal or CSS color of the background - defaults to transparent
* @param {string} playheadColor hexadecimal or CSS color of the line representing the play head - defaults to white
* @param {boolean} fill notes are filled with color (otherwise only the label is displayed) - 0 by default
* @param {boolean} fillActive active notes are filled with color - 0 by default
* @param {boolean} stroke notes are shown with colored borders - 0 by default
* @param {boolean} strokeActive active notes are shown with colored borders - 0 by default
* @param {boolean} hideInactive only active notes are shown - 0 by default
* @param {boolean} colorizeInactive use note color for inactive notes - 1 by default
* @param {string} fontFamily define the font used by notes labels - defaults to 'monospace'
* @param {integer} minMidi minimum note value to display on the value axis - defaults to 10
* @param {integer} maxMidi maximum note value to display on the value axis - defaults to 90
* @param {boolean} autorange automatically calculate the minMidi and maxMidi parameters - 0 by default
*
* @example
* note("C2 A2 G2").euclid(5,8).s('piano').clip(1).color('salmon').pianoroll({vertical:1, labels:1})
*/
export function pianoroll({
time,
haps,
@@ -120,7 +86,6 @@ export function pianoroll({
hideInactive = 0,
colorizeInactive = 1,
fontFamily,
clear = true,
ctx,
} = {}) {
const w = ctx.canvas.width;
@@ -166,7 +131,7 @@ export function pianoroll({
barThickness = fold ? valueAxis / foldValues.length : valueAxis / valueExtent;
ctx.fillStyle = background;
ctx.globalAlpha = 1; // reset!
if (clear && !smear) {
if (!smear) {
ctx.clearRect(0, 0, w, h);
ctx.fillRect(0, 0, w, h);
}
+2 -45
View File
@@ -4,22 +4,6 @@ import { logger } from './logger.mjs';
import { setTime } from './time.mjs';
import { evalScope } from './evaluate.mjs';
import { register } from './pattern.mjs';
import { atom, computed } from 'nanostores';
export const $replstate = atom({
schedulerError: undefined,
evalError: undefined,
code: '// LOADING',
activeCode: '// LOADING',
pattern: undefined,
miniLocations: [],
widgets: [],
pending: true,
started: false,
});
export const $repldirty = computed($replstate, (s) => s.code !== s.activeCode);
export const setReplState = (key, value) => $replstate.set({ ...$replstate.get(), [key]: value });
export function repl({
interval,
@@ -38,12 +22,8 @@ export function repl({
onTrigger: getTrigger({ defaultOutput, getTime }),
onError: onSchedulerError,
getTime,
onToggle: (started) => {
setReplState('started', started);
onToggle?.(started);
},
onToggle,
});
let playPatterns = [];
const setPattern = (pattern, autostart = true) => {
pattern = editPattern?.(pattern) || pattern;
scheduler.setPattern(pattern, autostart);
@@ -54,37 +34,21 @@ export function repl({
throw new Error('no code to evaluate');
}
try {
setReplState('code', code);
setReplState('pending', true);
await beforeEval?.({ code });
playPatterns = [];
let { pattern, meta } = await _evaluate(code, transpiler);
if (playPatterns.length) {
pattern = pattern.stack(...playPatterns);
}
logger(`[eval] code updated`);
setPattern(pattern, autostart);
setReplState('miniLocations', meta?.miniLocations || []);
setReplState('widgets', meta?.widgets || []);
setReplState('activeCode', code);
setReplState('pattern', pattern);
setReplState('evalError', undefined);
setReplState('schedulerError', undefined);
setReplState('pending', false);
afterEval?.({ code, pattern, meta });
return pattern;
} catch (err) {
// console.warn(`[repl] eval error: ${err.message}`);
logger(`[eval] error: ${err.message}`, 'error');
setReplState('evalError', err);
setReplState('pending', false);
onEvalError?.(err);
}
};
const stop = () => scheduler.stop();
const start = () => scheduler.start();
const pause = () => scheduler.pause();
const toggle = () => scheduler.toggle();
const setCps = (cps) => scheduler.setCps(cps);
const setCpm = (cpm) => scheduler.setCps(cpm / 60);
@@ -93,11 +57,6 @@ export function repl({
return pat.loopAtCps(cycles, scheduler.cps);
});
const play = register('play', (pat) => {
playPatterns.push(pat);
return pat;
});
const fit = register('fit', (pat) =>
pat.withHap((hap) =>
hap.withValue((v) => ({
@@ -111,15 +70,13 @@ export function repl({
evalScope({
loopAt,
fit,
play,
setCps,
setcps: setCps,
setCpm,
setcpm: setCpm,
});
const setCode = (c) => setReplState('code', c);
return { scheduler, evaluate, start, stop, pause, setCps, setPattern, setCode, toggle };
return { scheduler, evaluate, start, stop, pause, setCps, setPattern };
}
export const getTrigger =
+2 -54
View File
@@ -7,7 +7,7 @@ This program is free software: you can redistribute it and/or modify it under th
import { Hap } from './hap.mjs';
import { Pattern, fastcat, reify, silence, stack, register } from './pattern.mjs';
import Fraction from './fraction.mjs';
import { id, _mod, clamp } from './util.mjs';
import { id } from './util.mjs';
export function steady(value) {
// A continuous value
@@ -155,62 +155,12 @@ export const _irand = (i) => rand.fmap((x) => Math.trunc(x * i));
*/
export const irand = (ipat) => reify(ipat).fmap(_irand).innerJoin();
/**
* pick from the list of values (or patterns of values) via the index using the given
* pattern of integers
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
* @example
* note(pick("<0 1 [2!2] 3>", ["g a", "e f", "f g f g" , "g a c d"]))
*/
export const pick = (pat, xs) => {
xs = xs.map(reify);
if (xs.length == 0) {
return silence;
}
return pat
.fmap((i) => {
const key = clamp(Math.round(i), 0, xs.length - 1);
return xs[key];
})
.innerJoin();
};
/**
* pick from the list of values (or patterns of values) via the index using the given
* pattern of integers. The selected pattern will be compressed to fit the duration of the selecting event
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
* @example
* note(squeeze("<0@2 [1!2] 2>", ["g a", "f g f g" , "g a c d"]))
*/
export const squeeze = (pat, xs) => {
xs = xs.map(reify);
if (xs.length == 0) {
return silence;
}
return pat
.fmap((i) => {
const key = _mod(Math.round(i), xs.length);
return xs[key];
})
.squeezeJoin();
};
export const __chooseWith = (pat, xs) => {
xs = xs.map(reify);
if (xs.length == 0) {
return silence;
}
return pat.range(0, xs.length).fmap((i) => {
const key = Math.min(Math.max(Math.floor(i), 0), xs.length - 1);
return xs[key];
});
return pat.range(0, xs.length).fmap((i) => xs[Math.floor(i)]);
};
/**
* Choose from the list of values (or patterns of values) using the given
@@ -218,8 +168,6 @@ export const __chooseWith = (pat, xs) => {
* @param {Pattern} pat
* @param {*} xs
* @returns {Pattern}
* @example
* note("c2 g2!2 d2 f1").s(chooseWith(sine.fast(2), ["sawtooth", "triangle", "bd:6"]))
*/
export const chooseWith = (pat, xs) => {
return __chooseWith(pat, xs).outerJoin();
+3 -3
View File
@@ -67,9 +67,9 @@ Pattern.prototype.spiral = function (options = {}) {
// logSpiral = true,
} = options;
function spiral({ ctx, time, haps, drawTime, clear }) {
function spiral({ ctx, time, haps, drawTime }) {
const [w, h] = [ctx.canvas.width, ctx.canvas.height];
clear && ctx.clearRect(0, 0, w * 2, h * 2);
ctx.clearRect(0, 0, w * 2, h * 2);
const [cx, cy] = [w / 2, h / 2];
const settings = {
margin: size / stretch,
@@ -114,5 +114,5 @@ Pattern.prototype.spiral = function (options = {}) {
});
}
return this.onPaint((ctx, time, haps, drawTime, options) => spiral({ ctx, time, haps, drawTime, ...options }));
return this.onPaint((ctx, time, haps, drawTime) => spiral({ ctx, time, haps, drawTime }));
};
-19
View File
@@ -1003,23 +1003,4 @@ describe('Pattern', () => {
);
});
});
describe('chunk', () => {
it('Processes each cycle of the source pattern multiple times, once for each chunk', () => {
expect(sequence(0, 1, 2, 3).slow(2).chunk(2, add(10)).fast(4).firstCycleValues).toStrictEqual([
10, 1, 0, 11, 12, 3, 2, 13,
]);
});
});
describe('fastChunk', () => {
it('Unlike chunk, cycles of the source pattern proceed cycle-by-cycle', () => {
expect(sequence(0, 1, 2, 3).slow(2).fastChunk(2, add(10)).fast(4).firstCycleValues).toStrictEqual([
10, 1, 2, 13, 10, 1, 2, 13,
]);
});
});
describe('repeatCycles', () => {
it('Repeats each cycle of the source pattern the given number of times', () => {
expect(slowcat(0, 1).repeatCycles(2).fast(6).firstCycleValues).toStrictEqual([0, 0, 1, 1, 0, 0]);
});
});
});
+2
View File
@@ -46,5 +46,7 @@ export const cleanupUi = () => {
const container = document.getElementById('code');
if (container) {
container.style = '';
// TODO: find a way to remove that duplication..
container.className = 'grow flex text-gray-100 relative overflow-auto cursor-text pb-0'; // has to match App.tsx
}
};
+1 -1
View File
@@ -137,7 +137,7 @@ export async function loadOrc(url) {
export const csoundm = register('csoundm', (instrument, pat) => {
let p1 = instrument;
if (typeof instrument === 'string') {
p1 = `"${instrument}"`;
p1 = `"{instrument}"`;
}
init(); // not async to support csound inside other patterns + to be able to call pattern methods after it
return pat.onTrigger((tidal_time, hap) => {
+1
View File
@@ -7,3 +7,4 @@ This program is free software: you can redistribute it and/or modify it under th
export * from './midibridge.mjs';
export * from './utils.mjs';
export * from './oscbridge.mjs';
export * from './webaudiobridge.mjs';
+1
View File
@@ -22,6 +22,7 @@
"url": "https://github.com/tidalcycles/strudel/issues"
},
"dependencies": {
"@strudel.cycles/webaudio": "^0.9.0",
"@strudel.cycles/core": "workspace:*",
"@tauri-apps/api": "^1.4.0"
},
+244
View File
@@ -0,0 +1,244 @@
import { Invoke } from './utils.mjs';
import { duration, isNote, midiToFreq, noteToMidi, Pattern, valueToMidi } from '@strudel.cycles/core';
import { getAudioContext, getEnvelope, getSound, logger } from '@strudel.cycles/webaudio';
export const desktopAudio = async (value, deadline, hapDuration) => {
const ac = getAudioContext();
if (typeof value !== 'object') {
throw new Error(
`expected hap.value to be an object, but got "${value}". Hint: append .note() or .s() to the end`,
'error',
);
}
let t = ac.currentTime + deadline;
let {
note = 'C2',
s = 'triangle',
bank,
source,
gain = 0.8,
// low pass
cutoff = 8000,
resonance = 1,
// high pass
hcutoff = 0,
hresonance = 1,
// band pass
bandf = 0,
bandq = 1,
//
coarse,
crush,
shape,
pan,
vowel,
delay,
delayfeedback,
delaytime,
orbit = 1,
room,
roomfade,
roomlp,
roomdim,
size,
ir,
i = 0,
velocity = 1,
analyze, // analyser wet
fft = 8, // fftSize 0 - 10
speed = 1, // sample playback speed
begin = 0,
end = 1,
loop = 0,
loopBegin = 0,
loopEnd = 1,
attack,
decay,
sustain,
release,
lpattack,
lpdecay,
lpsustain,
lprelease,
lpenv,
hpattack,
hpdecay,
hpsustain,
hprelease,
hpenv,
bpattack,
bpdecay,
bpsustain,
bprelease,
bpenv,
n = 0,
freq,
unit,
} = value;
value.duration = hapDuration;
if (bank && s) {
s = `${bank}_${s}`;
}
if (freq !== undefined && note !== undefined) {
logger('[sampler] hap has note and freq. ignoring note', 'warning');
}
let midi = valueToMidi({ freq, note }, 36);
let transpose = midi - 36;
let sampleUrl;
let baseUrl;
let path;
const waveForms = new Set(['sine', 'square', 'saw', 'sawtooth', 'triangle']);
if (waveForms.has(s)) {
sampleUrl = 'none';
} else {
if (getSound(s).data.baseUrl !== undefined) {
baseUrl = getSound(s).data.baseUrl;
if (baseUrl === './piano/') {
path = 'https://strudel.tidalcycles.org/';
} else if (baseUrl === './EmuSP12/') {
path = 'https://strudel.tidalcycles.org/';
} else {
path = '';
}
}
let map = getSound(s).data.samples;
if (Array.isArray(map)) {
sampleUrl =
path !== undefined ? path + map[n % map.length].replace('./', '') : map[n % map.length].replace('./', '');
} else {
const midiDiff = (noteA) => noteToMidi(noteA) - midi;
// object format will expect keys as notes
const closest = Object.keys(map)
.filter((k) => !k.startsWith('_'))
.reduce(
(closest, key, j) => (!closest || Math.abs(midiDiff(key)) < Math.abs(midiDiff(closest)) ? key : closest),
null,
);
transpose = -midiDiff(closest); // semitones to repitch
sampleUrl =
path !== undefined
? path + map[closest][n % map[closest].length].replace('./', '')
: map[closest][n % map[closest].length].replace('./', '');
}
}
if (isNote(note)) {
note = noteToMidi(note);
}
const playbackRate = 1.0 * Math.pow(2, transpose / 12);
if (delay !== 0) {
delay = Math.abs(delay);
delayfeedback = Math.abs(delayfeedback);
delaytime = Math.abs(delaytime);
}
let ir_path;
let url;
if (ir !== undefined) {
if (getSound(ir).data.baseUrl !== undefined) {
baseUrl = getSound(ir).data.baseUrl;
if (baseUrl === './piano/') {
ir_path = 'https://strudel.tidalcycles.org/';
} else if (baseUrl === './EmuSP12/') {
ir_path = 'https://strudel.tidalcycles.org/';
} else {
ir_path = '';
}
}
let ir_samples = getSound(ir);
if (Array.isArray(ir_samples)) {
url =
ir_path !== undefined
? ir_path + ir_samples[n % ir_samples.length].replace('./', '')
: ir_samples[n % ir_samples.length].replace('./', '');
} else if (typeof ir_samples === 'object') {
console.log(
'obk',
Object.values(ir_samples.data.samples)[i & Object.values(ir_samples.data.samples).length].replace('./', ''),
);
url =
ir_path !== undefined
? ir_path +
Object.values(ir_samples.data.samples)[i & Object.values(ir_samples.data.samples).length].replace('./', '')
: Object.values(ir_samples.data.samples)[i & Object.values(ir_samples.data.samples).length].replace('./', '');
}
}
console.log('url', url);
let folder;
if (baseUrl !== undefined) {
folder = baseUrl.replace('./', '') + '/' + s + '/';
} else {
folder = 'misc';
}
const dirname = folder + '/' + s + '/';
const offset = (t - getAudioContext().currentTime) * 1000;
const roundedOffset = Math.round(offset);
const messagesfromjs = [];
const packages = {
loop: [loop, loopBegin, loopEnd],
delay: [delay, delaytime, delayfeedback],
reverb: [room, size, roomfade, roomlp, roomdim,ir, url],
lpf: [cutoff, resonance],
hpf: [hcutoff, hresonance],
bpf: [bandf, bandq],
adsr: [attack, decay, sustain, release],
lpenv: [lpattack, lpdecay, lpsustain, lprelease, lpenv],
hpenv: [hpattack, hpdecay, hpsustain, hprelease, hpenv],
bpenv: [bpattack, bpdecay, bpsustain, bprelease, bpenv],
};
messagesfromjs.push({
note: midiToFreq(note),
offset: roundedOffset,
waveform: s,
lpf: packages.lpf,
hpf: packages.hpf,
bpf: packages.bpf,
duration: hapDuration,
velocity: velocity,
delay: packages.delay,
reverb: packages.reverb,
orbit: orbit,
speed: speed,
begin: begin,
end: end,
looper: packages.loop,
adsr: packages.adsr,
lpenv: packages.lpenv,
hpenv: packages.hpenv,
bpenv: packages.bpenv,
n: n,
sampleurl: sampleUrl,
dirname: dirname,
unit: unit,
playbackrate: playbackRate,
});
if (messagesfromjs.length) {
setTimeout(() => {
Invoke('sendwebaudio', { messagesfromjs });
});
}
};
const hap2value = (hap) => {
hap.ensureObjectValue();
return { ...hap.value, velocity: hap.context.velocity };
};
export const webaudioDesktopOutputTrigger = (t, hap, ct, cps) =>
desktopAudio(hap2value(hap), t - ct, hap.duration / cps, cps);
export const webaudioDesktopOutput = (hap, deadline, hapDuration) =>
desktopAudio(hap2value(hap), deadline, hapDuration);
Pattern.prototype.webaudio = function () {
return this.onTrigger(webaudioDesktopOutputTrigger);
};
+1 -1
View File
@@ -6,7 +6,7 @@ class Strudel extends HTMLElement {
setTimeout(() => {
const code = (this.innerHTML + '').replace('<!--', '').replace('-->', '').trim();
const iframe = document.createElement('iframe');
const src = `https://strudel.cc/#${encodeURIComponent(btoa(code))}`;
const src = `https://strudel.tidalcycles.org/#${encodeURIComponent(btoa(code))}`;
// const src = `http://localhost:3000/#${encodeURIComponent(btoa(code))}`;
iframe.setAttribute('src', src);
iframe.setAttribute('width', '600');
-29
View File
@@ -1,29 +0,0 @@
# @strudel/hydra
This package integrates [hydra-synth](https://www.npmjs.com/package/hydra-synth) into strudel.
## Usage in Strudel
This package is imported into strudel by default. To activate Hydra, place this code at the top of your code:
```js
await initHydra();
```
Then you can use hydra below!
## Usage via npm
```sh
npm i @strudel/hydra
```
Then add the import to your evalScope:
```js
import { evalScope } from '@strudel.cycles/core';
evalScope(
import('@strudel/hydra')
)
```
-15
View File
@@ -1,15 +0,0 @@
import { getDrawContext } from '@strudel.cycles/core';
export async function initHydra() {
if (!document.getElementById('hydra-canvas')) {
const { canvas: testCanvas } = getDrawContext();
await import('https://unpkg.com/hydra-synth');
const hydraCanvas = testCanvas.cloneNode(true);
hydraCanvas.id = 'hydra-canvas';
testCanvas.after(hydraCanvas);
new Hydra({ canvas: hydraCanvas, detectAudio: false });
s0.init({ src: testCanvas });
}
}
export const H = (p) => () => p.queryArc(getTime(), getTime())[0].value;
-43
View File
@@ -1,43 +0,0 @@
{
"name": "@strudel/hydra",
"version": "0.9.0",
"description": "Hydra integration for strudel",
"main": "hydra.mjs",
"publishConfig": {
"main": "dist/index.js",
"module": "dist/index.mjs"
},
"scripts": {
"server": "node server.js",
"tidal-sniffer": "node tidal-sniffer.js",
"client": "npx serve -p 4321",
"build-bin": "npx pkg server.js --targets node16-macos-x64,node16-win-x64,node16-linux-x64 --out-path bin",
"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",
"dependencies": {
"@strudel.cycles/core": "workspace:*",
"hydra-synth": "^1.3.29"
},
"devDependencies": {
"pkg": "^5.8.1",
"vite": "^4.3.3"
}
}
-19
View File
@@ -1,19 +0,0 @@
import { defineConfig } from 'vite';
import { dependencies } from './package.json';
import { resolve } from 'path';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [],
build: {
lib: {
entry: resolve(__dirname, 'hydra.mjs'),
formats: ['es', 'cjs'],
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' }[ext]),
},
rollupOptions: {
external: [...Object.keys(dependencies)],
},
target: 'esnext',
},
});
+18 -71
View File
@@ -5,7 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th
*/
import * as _WebMidi from 'webmidi';
import { Pattern, isPattern, logger, ref } from '@strudel.cycles/core';
import { Pattern, isPattern, logger } from '@strudel.cycles/core';
import { noteToMidi } from '@strudel.cycles/core';
import { Note } from 'webmidi';
// if you use WebMidi from outside of this package, make sure to import that instance:
@@ -15,8 +15,8 @@ function supportsMidi() {
return typeof navigator.requestMIDIAccess === 'function';
}
function getMidiDeviceNamesString(devices) {
return devices.map((o) => `'${o.name}'`).join(' | ');
function getMidiDeviceNamesString(outputs) {
return outputs.map((o) => `'${o.name}'`).join(' | ');
}
export function enableWebMidi(options = {}) {
@@ -52,41 +52,30 @@ export function enableWebMidi(options = {}) {
});
});
}
// const outputByName = (name: string) => WebMidi.getOutputByName(name);
const outputByName = (name) => WebMidi.getOutputByName(name);
function getDevice(indexOrName, devices) {
if (!devices.length) {
// output?: string | number, outputs: typeof WebMidi.outputs
function getDevice(output, outputs) {
if (!outputs.length) {
throw new Error(`🔌 No MIDI devices found. Connect a device or enable IAC Driver.`);
}
if (typeof indexOrName === 'number') {
return devices[indexOrName];
if (typeof output === 'number') {
return outputs[output];
}
const byName = (name) => devices.find((output) => output.name.includes(name));
if (typeof indexOrName === 'string') {
return byName(indexOrName);
if (typeof output === 'string') {
return outputByName(output);
}
// attempt to default to first IAC device if none is specified
const IACOutput = byName('IAC');
const device = IACOutput ?? devices[0];
const IACOutput = outputs.find((output) => output.name.includes('IAC'));
const device = IACOutput ?? outputs[0];
if (!device) {
throw new Error(
`🔌 MIDI device '${device ? device : ''}' not found. Use one of ${getMidiDeviceNamesString(devices)}`,
`🔌 MIDI device '${output ? output : ''}' not found. Use one of ${getMidiDeviceNamesString(WebMidi.outputs)}`,
);
}
return IACOutput ?? devices[0];
}
// send start/stop messages to outputs when repl starts/stops
if (typeof window !== 'undefined') {
window.addEventListener('message', (e) => {
if (!WebMidi?.enabled) {
return;
}
if (e.data === 'strudel-stop') {
WebMidi.outputs.forEach((output) => output.sendStop());
}
// cannot start here, since we have no timing info, see sendStart below
});
return IACOutput ?? outputs[0];
}
Pattern.prototype.midi = function (output) {
@@ -114,7 +103,6 @@ Pattern.prototype.midi = function (output) {
return this.onTrigger((time, hap, currentTime, cps) => {
if (!WebMidi.enabled) {
console.log('not enabled');
return;
}
const device = getDevice(output, WebMidi.outputs);
@@ -125,7 +113,7 @@ Pattern.prototype.midi = function (output) {
const timeOffsetString = `+${offset}`;
// destructure value
const { note, nrpnn, nrpv, ccn, ccv, midichan = 1, midicmd } = hap.value;
const { note, nrpnn, nrpv, ccn, ccv, midichan = 1 } = hap.value;
const velocity = hap.context?.velocity ?? 0.9; // TODO: refactor velocity
// note off messages will often a few ms arrive late, try to prevent glitching by subtracting from the duration length
@@ -137,7 +125,7 @@ Pattern.prototype.midi = function (output) {
time: timeOffsetString,
});
}
if (ccv !== undefined && ccn !== undefined) {
if (ccv && ccn) {
if (typeof ccv !== 'number' || ccv < 0 || ccv > 1) {
throw new Error('expected ccv to be a number between 0 and 1');
}
@@ -147,46 +135,5 @@ Pattern.prototype.midi = function (output) {
const scaled = Math.round(ccv * 127);
device.sendControlChange(ccn, scaled, midichan, { time: timeOffsetString });
}
if (hap.whole.begin + 0 === 0) {
// we need to start here because we have the timing info
device.sendStart({ time: timeOffsetString });
}
if (['clock', 'midiClock'].includes(midicmd)) {
device.sendClock({ time: timeOffsetString });
} else if (['start'].includes(midicmd)) {
device.sendStart({ time: timeOffsetString });
} else if (['stop'].includes(midicmd)) {
device.sendStop({ time: timeOffsetString });
} else if (['continue'].includes(midicmd)) {
device.sendContinue({ time: timeOffsetString });
}
});
};
let listeners = {};
const refs = {};
export async function midin(input) {
const initial = await enableWebMidi(); // only returns on first init
const device = getDevice(input, WebMidi.inputs);
if (initial) {
const otherInputs = WebMidi.inputs.filter((o) => o.name !== device.name);
logger(
`Midi enabled! Using "${device.name}". ${
otherInputs?.length ? `Also available: ${getMidiDeviceNamesString(otherInputs)}` : ''
}`,
);
refs[input] = {};
}
const cc = (cc) => ref(() => refs[input][cc] || 0);
listeners[input] && device.removeListener('midimessage', listeners[input]);
listeners[input] = (e) => {
const cc = e.dataBytes[0];
const v = e.dataBytes[1];
refs[input] && (refs[input][cc] = v / 127);
};
device.addListener('midimessage', listeners[input]);
return cc;
}
+2 -2
View File
@@ -32,7 +32,7 @@ yields:
## Mini Notation API
See "Mini Notation" in the [Strudel Tutorial](https://strudel.cc/learn/mini-notation)
See "Mini Notation" in the [Strudel Tutorial](https://strudel.tidalcycles.org/learn/mini-notation)
## Building the Parser
@@ -40,5 +40,5 @@ The parser [krill-parser.js] is generated from [krill.pegjs](./krill.pegjs) usin
To generate the parser, run
```js
npm run build:parser
npm build:parser
```
File diff suppressed because one or more lines are too long
+2 -31
View File
@@ -79,9 +79,6 @@ frac
int
= zero / (digit1_9 DIGIT*)
intneg
= minus? int { return parseInt(text()); }
minus
= "-"
@@ -103,8 +100,7 @@ quote = '"' / "'"
// ------------------ steps and cycles ---------------------------
// single step definition (e.g bd)
step_char "a letter, a number, \"-\", \"#\", \".\", \"^\", \"_\"" =
unicode_letter / [0-9~] / "-" / "#" / "." / "^" / "_"
step_char = [0-9a-zA-Z~] / "-" / "#" / "." / "^" / "_"
step = ws chars:step_char+ ws { return new AtomStub(chars.join("")) }
// define a sub cycle e.g. [1 2, 3 [4]]
@@ -127,7 +123,7 @@ slice = step / sub_cycle / polymeter / slow_sequence
// slice modifier affects the timing/size of a slice (e.g. [a b c]@3)
// at this point, we assume we can represent them as regular sequence operators
slice_op = op_weight / op_bjorklund / op_slow / op_fast / op_replicate / op_degrade / op_tail / op_range
slice_op = op_weight / op_bjorklund / op_slow / op_fast / op_replicate / op_degrade / op_tail
op_weight = "@" a:number
{ return x => x.options_['weight'] = a }
@@ -150,9 +146,6 @@ op_degrade = "?"a:number?
op_tail = ":" s:slice
{ return x => x.options_['ops'].push({ type_: "tail", arguments_ :{ element:s } }) }
op_range = ".." s:slice
{ return x => x.options_['ops'].push({ type_: "range", arguments_ :{ element:s } }) }
// a slice with an modifier applied i.e [bd@4 sd@3]@2 hh]
slice_with_ops = s:slice ops:slice_op*
{ const result = new ElementStub(s, {ops: [], weight: 1, reps: 1});
@@ -266,25 +259,3 @@ hush = "hush"
// ---------------------- statements ----------------------------
statement = mini_definition / command
// ---------------------- unicode ----------------------------
unicode_letter = Lu / Ll / Lt / Lm / Lo / Nl
// Letter, Lowercase
Ll = [\u0061-\u007A\u00B5\u00DF-\u00F6\u00F8-\u00FF\u0101\u0103\u0105\u0107\u0109\u010B\u010D\u010F\u0111\u0113\u0115\u0117\u0119\u011B\u011D\u011F\u0121\u0123\u0125\u0127\u0129\u012B\u012D\u012F\u0131\u0133\u0135\u0137-\u0138\u013A\u013C\u013E\u0140\u0142\u0144\u0146\u0148-\u0149\u014B\u014D\u014F\u0151\u0153\u0155\u0157\u0159\u015B\u015D\u015F\u0161\u0163\u0165\u0167\u0169\u016B\u016D\u016F\u0171\u0173\u0175\u0177\u017A\u017C\u017E-\u0180\u0183\u0185\u0188\u018C-\u018D\u0192\u0195\u0199-\u019B\u019E\u01A1\u01A3\u01A5\u01A8\u01AA-\u01AB\u01AD\u01B0\u01B4\u01B6\u01B9-\u01BA\u01BD-\u01BF\u01C6\u01C9\u01CC\u01CE\u01D0\u01D2\u01D4\u01D6\u01D8\u01DA\u01DC-\u01DD\u01DF\u01E1\u01E3\u01E5\u01E7\u01E9\u01EB\u01ED\u01EF-\u01F0\u01F3\u01F5\u01F9\u01FB\u01FD\u01FF\u0201\u0203\u0205\u0207\u0209\u020B\u020D\u020F\u0211\u0213\u0215\u0217\u0219\u021B\u021D\u021F\u0221\u0223\u0225\u0227\u0229\u022B\u022D\u022F\u0231\u0233-\u0239\u023C\u023F-\u0240\u0242\u0247\u0249\u024B\u024D\u024F-\u0293\u0295-\u02AF\u0371\u0373\u0377\u037B-\u037D\u0390\u03AC-\u03CE\u03D0-\u03D1\u03D5-\u03D7\u03D9\u03DB\u03DD\u03DF\u03E1\u03E3\u03E5\u03E7\u03E9\u03EB\u03ED\u03EF-\u03F3\u03F5\u03F8\u03FB-\u03FC\u0430-\u045F\u0461\u0463\u0465\u0467\u0469\u046B\u046D\u046F\u0471\u0473\u0475\u0477\u0479\u047B\u047D\u047F\u0481\u048B\u048D\u048F\u0491\u0493\u0495\u0497\u0499\u049B\u049D\u049F\u04A1\u04A3\u04A5\u04A7\u04A9\u04AB\u04AD\u04AF\u04B1\u04B3\u04B5\u04B7\u04B9\u04BB\u04BD\u04BF\u04C2\u04C4\u04C6\u04C8\u04CA\u04CC\u04CE-\u04CF\u04D1\u04D3\u04D5\u04D7\u04D9\u04DB\u04DD\u04DF\u04E1\u04E3\u04E5\u04E7\u04E9\u04EB\u04ED\u04EF\u04F1\u04F3\u04F5\u04F7\u04F9\u04FB\u04FD\u04FF\u0501\u0503\u0505\u0507\u0509\u050B\u050D\u050F\u0511\u0513\u0515\u0517\u0519\u051B\u051D\u051F\u0521\u0523\u0525\u0527\u0529\u052B\u052D\u052F\u0560-\u0588\u10D0-\u10FA\u10FD-\u10FF\u13F8-\u13FD\u1C80-\u1C88\u1D00-\u1D2B\u1D6B-\u1D77\u1D79-\u1D9A\u1E01\u1E03\u1E05\u1E07\u1E09\u1E0B\u1E0D\u1E0F\u1E11\u1E13\u1E15\u1E17\u1E19\u1E1B\u1E1D\u1E1F\u1E21\u1E23\u1E25\u1E27\u1E29\u1E2B\u1E2D\u1E2F\u1E31\u1E33\u1E35\u1E37\u1E39\u1E3B\u1E3D\u1E3F\u1E41\u1E43\u1E45\u1E47\u1E49\u1E4B\u1E4D\u1E4F\u1E51\u1E53\u1E55\u1E57\u1E59\u1E5B\u1E5D\u1E5F\u1E61\u1E63\u1E65\u1E67\u1E69\u1E6B\u1E6D\u1E6F\u1E71\u1E73\u1E75\u1E77\u1E79\u1E7B\u1E7D\u1E7F\u1E81\u1E83\u1E85\u1E87\u1E89\u1E8B\u1E8D\u1E8F\u1E91\u1E93\u1E95-\u1E9D\u1E9F\u1EA1\u1EA3\u1EA5\u1EA7\u1EA9\u1EAB\u1EAD\u1EAF\u1EB1\u1EB3\u1EB5\u1EB7\u1EB9\u1EBB\u1EBD\u1EBF\u1EC1\u1EC3\u1EC5\u1EC7\u1EC9\u1ECB\u1ECD\u1ECF\u1ED1\u1ED3\u1ED5\u1ED7\u1ED9\u1EDB\u1EDD\u1EDF\u1EE1\u1EE3\u1EE5\u1EE7\u1EE9\u1EEB\u1EED\u1EEF\u1EF1\u1EF3\u1EF5\u1EF7\u1EF9\u1EFB\u1EFD\u1EFF-\u1F07\u1F10-\u1F15\u1F20-\u1F27\u1F30-\u1F37\u1F40-\u1F45\u1F50-\u1F57\u1F60-\u1F67\u1F70-\u1F7D\u1F80-\u1F87\u1F90-\u1F97\u1FA0-\u1FA7\u1FB0-\u1FB4\u1FB6-\u1FB7\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FC7\u1FD0-\u1FD3\u1FD6-\u1FD7\u1FE0-\u1FE7\u1FF2-\u1FF4\u1FF6-\u1FF7\u210A\u210E-\u210F\u2113\u212F\u2134\u2139\u213C-\u213D\u2146-\u2149\u214E\u2184\u2C30-\u2C5E\u2C61\u2C65-\u2C66\u2C68\u2C6A\u2C6C\u2C71\u2C73-\u2C74\u2C76-\u2C7B\u2C81\u2C83\u2C85\u2C87\u2C89\u2C8B\u2C8D\u2C8F\u2C91\u2C93\u2C95\u2C97\u2C99\u2C9B\u2C9D\u2C9F\u2CA1\u2CA3\u2CA5\u2CA7\u2CA9\u2CAB\u2CAD\u2CAF\u2CB1\u2CB3\u2CB5\u2CB7\u2CB9\u2CBB\u2CBD\u2CBF\u2CC1\u2CC3\u2CC5\u2CC7\u2CC9\u2CCB\u2CCD\u2CCF\u2CD1\u2CD3\u2CD5\u2CD7\u2CD9\u2CDB\u2CDD\u2CDF\u2CE1\u2CE3-\u2CE4\u2CEC\u2CEE\u2CF3\u2D00-\u2D25\u2D27\u2D2D\uA641\uA643\uA645\uA647\uA649\uA64B\uA64D\uA64F\uA651\uA653\uA655\uA657\uA659\uA65B\uA65D\uA65F\uA661\uA663\uA665\uA667\uA669\uA66B\uA66D\uA681\uA683\uA685\uA687\uA689\uA68B\uA68D\uA68F\uA691\uA693\uA695\uA697\uA699\uA69B\uA723\uA725\uA727\uA729\uA72B\uA72D\uA72F-\uA731\uA733\uA735\uA737\uA739\uA73B\uA73D\uA73F\uA741\uA743\uA745\uA747\uA749\uA74B\uA74D\uA74F\uA751\uA753\uA755\uA757\uA759\uA75B\uA75D\uA75F\uA761\uA763\uA765\uA767\uA769\uA76B\uA76D\uA76F\uA771-\uA778\uA77A\uA77C\uA77F\uA781\uA783\uA785\uA787\uA78C\uA78E\uA791\uA793-\uA795\uA797\uA799\uA79B\uA79D\uA79F\uA7A1\uA7A3\uA7A5\uA7A7\uA7A9\uA7AF\uA7B5\uA7B7\uA7B9\uA7FA\uAB30-\uAB5A\uAB60-\uAB65\uAB70-\uABBF\uFB00-\uFB06\uFB13-\uFB17\uFF41-\uFF5A]
// Letter, Modifier
Lm = [\u02B0-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0374\u037A\u0559\u0640\u06E5-\u06E6\u07F4-\u07F5\u07FA\u081A\u0824\u0828\u0971\u0E46\u0EC6\u10FC\u17D7\u1843\u1AA7\u1C78-\u1C7D\u1D2C-\u1D6A\u1D78\u1D9B-\u1DBF\u2071\u207F\u2090-\u209C\u2C7C-\u2C7D\u2D6F\u2E2F\u3005\u3031-\u3035\u303B\u309D-\u309E\u30FC-\u30FE\uA015\uA4F8-\uA4FD\uA60C\uA67F\uA69C-\uA69D\uA717-\uA71F\uA770\uA788\uA7F8-\uA7F9\uA9CF\uA9E6\uAA70\uAADD\uAAF3-\uAAF4\uAB5C-\uAB5F\uFF70\uFF9E-\uFF9F]
// Letter, Other
Lo = [\u00AA\u00BA\u01BB\u01C0-\u01C3\u0294\u05D0-\u05EA\u05EF-\u05F2\u0620-\u063F\u0641-\u064A\u066E-\u066F\u0671-\u06D3\u06D5\u06EE-\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u0800-\u0815\u0840-\u0858\u0860-\u086A\u08A0-\u08B4\u08B6-\u08BD\u0904-\u0939\u093D\u0950\u0958-\u0961\u0972-\u0980\u0985-\u098C\u098F-\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC-\u09DD\u09DF-\u09E1\u09F0-\u09F1\u09FC\u0A05-\u0A0A\u0A0F-\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32-\u0A33\u0A35-\u0A36\u0A38-\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2-\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0-\u0AE1\u0AF9\u0B05-\u0B0C\u0B0F-\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32-\u0B33\u0B35-\u0B39\u0B3D\u0B5C-\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99-\u0B9A\u0B9C\u0B9E-\u0B9F\u0BA3-\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58-\u0C5A\u0C60-\u0C61\u0C80\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0-\u0CE1\u0CF1-\u0CF2\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D54-\u0D56\u0D5F-\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32-\u0E33\u0E40-\u0E45\u0E81-\u0E82\u0E84\u0E87-\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA-\u0EAB\u0EAD-\u0EB0\u0EB2-\u0EB3\u0EBD\u0EC0-\u0EC4\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065-\u1066\u106E-\u1070\u1075-\u1081\u108E\u1100-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16F1-\u16F8\u1700-\u170C\u170E-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17DC\u1820-\u1842\u1844-\u1878\u1880-\u1884\u1887-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u1A00-\u1A16\u1A20-\u1A54\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE-\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C77\u1CE9-\u1CEC\u1CEE-\u1CF1\u1CF5-\u1CF6\u2135-\u2138\u2D30-\u2D67\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u3006\u303C\u3041-\u3096\u309F\u30A1-\u30FA\u30FF\u3105-\u312F\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FEF\uA000-\uA014\uA016-\uA48C\uA4D0-\uA4F7\uA500-\uA60B\uA610-\uA61F\uA62A-\uA62B\uA66E\uA6A0-\uA6E5\uA78F\uA7F7\uA7FB-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA8FD-\uA8FE\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9E0-\uA9E4\uA9E7-\uA9EF\uA9FA-\uA9FE\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA6F\uAA71-\uAA76\uAA7A\uAA7E-\uAAAF\uAAB1\uAAB5-\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADC\uAAE0-\uAAEA\uAAF2\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uABC0-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40-\uFB41\uFB43-\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF66-\uFF6F\uFF71-\uFF9D\uFFA0-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]
// Letter, Titlecase
Lt = [\u01C5\u01C8\u01CB\u01F2\u1F88-\u1F8F\u1F98-\u1F9F\u1FA8-\u1FAF\u1FBC\u1FCC\u1FFC]
// Letter, Uppercase
Lu = [\u0041-\u005A\u00C0-\u00D6\u00D8-\u00DE\u0100\u0102\u0104\u0106\u0108\u010A\u010C\u010E\u0110\u0112\u0114\u0116\u0118\u011A\u011C\u011E\u0120\u0122\u0124\u0126\u0128\u012A\u012C\u012E\u0130\u0132\u0134\u0136\u0139\u013B\u013D\u013F\u0141\u0143\u0145\u0147\u014A\u014C\u014E\u0150\u0152\u0154\u0156\u0158\u015A\u015C\u015E\u0160\u0162\u0164\u0166\u0168\u016A\u016C\u016E\u0170\u0172\u0174\u0176\u0178-\u0179\u017B\u017D\u0181-\u0182\u0184\u0186-\u0187\u0189-\u018B\u018E-\u0191\u0193-\u0194\u0196-\u0198\u019C-\u019D\u019F-\u01A0\u01A2\u01A4\u01A6-\u01A7\u01A9\u01AC\u01AE-\u01AF\u01B1-\u01B3\u01B5\u01B7-\u01B8\u01BC\u01C4\u01C7\u01CA\u01CD\u01CF\u01D1\u01D3\u01D5\u01D7\u01D9\u01DB\u01DE\u01E0\u01E2\u01E4\u01E6\u01E8\u01EA\u01EC\u01EE\u01F1\u01F4\u01F6-\u01F8\u01FA\u01FC\u01FE\u0200\u0202\u0204\u0206\u0208\u020A\u020C\u020E\u0210\u0212\u0214\u0216\u0218\u021A\u021C\u021E\u0220\u0222\u0224\u0226\u0228\u022A\u022C\u022E\u0230\u0232\u023A-\u023B\u023D-\u023E\u0241\u0243-\u0246\u0248\u024A\u024C\u024E\u0370\u0372\u0376\u037F\u0386\u0388-\u038A\u038C\u038E-\u038F\u0391-\u03A1\u03A3-\u03AB\u03CF\u03D2-\u03D4\u03D8\u03DA\u03DC\u03DE\u03E0\u03E2\u03E4\u03E6\u03E8\u03EA\u03EC\u03EE\u03F4\u03F7\u03F9-\u03FA\u03FD-\u042F\u0460\u0462\u0464\u0466\u0468\u046A\u046C\u046E\u0470\u0472\u0474\u0476\u0478\u047A\u047C\u047E\u0480\u048A\u048C\u048E\u0490\u0492\u0494\u0496\u0498\u049A\u049C\u049E\u04A0\u04A2\u04A4\u04A6\u04A8\u04AA\u04AC\u04AE\u04B0\u04B2\u04B4\u04B6\u04B8\u04BA\u04BC\u04BE\u04C0-\u04C1\u04C3\u04C5\u04C7\u04C9\u04CB\u04CD\u04D0\u04D2\u04D4\u04D6\u04D8\u04DA\u04DC\u04DE\u04E0\u04E2\u04E4\u04E6\u04E8\u04EA\u04EC\u04EE\u04F0\u04F2\u04F4\u04F6\u04F8\u04FA\u04FC\u04FE\u0500\u0502\u0504\u0506\u0508\u050A\u050C\u050E\u0510\u0512\u0514\u0516\u0518\u051A\u051C\u051E\u0520\u0522\u0524\u0526\u0528\u052A\u052C\u052E\u0531-\u0556\u10A0-\u10C5\u10C7\u10CD\u13A0-\u13F5\u1C90-\u1CBA\u1CBD-\u1CBF\u1E00\u1E02\u1E04\u1E06\u1E08\u1E0A\u1E0C\u1E0E\u1E10\u1E12\u1E14\u1E16\u1E18\u1E1A\u1E1C\u1E1E\u1E20\u1E22\u1E24\u1E26\u1E28\u1E2A\u1E2C\u1E2E\u1E30\u1E32\u1E34\u1E36\u1E38\u1E3A\u1E3C\u1E3E\u1E40\u1E42\u1E44\u1E46\u1E48\u1E4A\u1E4C\u1E4E\u1E50\u1E52\u1E54\u1E56\u1E58\u1E5A\u1E5C\u1E5E\u1E60\u1E62\u1E64\u1E66\u1E68\u1E6A\u1E6C\u1E6E\u1E70\u1E72\u1E74\u1E76\u1E78\u1E7A\u1E7C\u1E7E\u1E80\u1E82\u1E84\u1E86\u1E88\u1E8A\u1E8C\u1E8E\u1E90\u1E92\u1E94\u1E9E\u1EA0\u1EA2\u1EA4\u1EA6\u1EA8\u1EAA\u1EAC\u1EAE\u1EB0\u1EB2\u1EB4\u1EB6\u1EB8\u1EBA\u1EBC\u1EBE\u1EC0\u1EC2\u1EC4\u1EC6\u1EC8\u1ECA\u1ECC\u1ECE\u1ED0\u1ED2\u1ED4\u1ED6\u1ED8\u1EDA\u1EDC\u1EDE\u1EE0\u1EE2\u1EE4\u1EE6\u1EE8\u1EEA\u1EEC\u1EEE\u1EF0\u1EF2\u1EF4\u1EF6\u1EF8\u1EFA\u1EFC\u1EFE\u1F08-\u1F0F\u1F18-\u1F1D\u1F28-\u1F2F\u1F38-\u1F3F\u1F48-\u1F4D\u1F59\u1F5B\u1F5D\u1F5F\u1F68-\u1F6F\u1FB8-\u1FBB\u1FC8-\u1FCB\u1FD8-\u1FDB\u1FE8-\u1FEC\u1FF8-\u1FFB\u2102\u2107\u210B-\u210D\u2110-\u2112\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u2130-\u2133\u213E-\u213F\u2145\u2183\u2C00-\u2C2E\u2C60\u2C62-\u2C64\u2C67\u2C69\u2C6B\u2C6D-\u2C70\u2C72\u2C75\u2C7E-\u2C80\u2C82\u2C84\u2C86\u2C88\u2C8A\u2C8C\u2C8E\u2C90\u2C92\u2C94\u2C96\u2C98\u2C9A\u2C9C\u2C9E\u2CA0\u2CA2\u2CA4\u2CA6\u2CA8\u2CAA\u2CAC\u2CAE\u2CB0\u2CB2\u2CB4\u2CB6\u2CB8\u2CBA\u2CBC\u2CBE\u2CC0\u2CC2\u2CC4\u2CC6\u2CC8\u2CCA\u2CCC\u2CCE\u2CD0\u2CD2\u2CD4\u2CD6\u2CD8\u2CDA\u2CDC\u2CDE\u2CE0\u2CE2\u2CEB\u2CED\u2CF2\uA640\uA642\uA644\uA646\uA648\uA64A\uA64C\uA64E\uA650\uA652\uA654\uA656\uA658\uA65A\uA65C\uA65E\uA660\uA662\uA664\uA666\uA668\uA66A\uA66C\uA680\uA682\uA684\uA686\uA688\uA68A\uA68C\uA68E\uA690\uA692\uA694\uA696\uA698\uA69A\uA722\uA724\uA726\uA728\uA72A\uA72C\uA72E\uA732\uA734\uA736\uA738\uA73A\uA73C\uA73E\uA740\uA742\uA744\uA746\uA748\uA74A\uA74C\uA74E\uA750\uA752\uA754\uA756\uA758\uA75A\uA75C\uA75E\uA760\uA762\uA764\uA766\uA768\uA76A\uA76C\uA76E\uA779\uA77B\uA77D-\uA77E\uA780\uA782\uA784\uA786\uA78B\uA78D\uA790\uA792\uA796\uA798\uA79A\uA79C\uA79E\uA7A0\uA7A2\uA7A4\uA7A6\uA7A8\uA7AA-\uA7AE\uA7B0-\uA7B4\uA7B6\uA7B8\uFF21-\uFF3A]
// Number, Letter
Nl = [\u16EE-\u16F0\u2160-\u2182\u2185-\u2188\u3007\u3021-\u3029\u3038-\u303A\uA6E6-\uA6EF]
-11
View File
@@ -45,17 +45,6 @@ const applyOptions = (parent, enter) => (pat, i) => {
pat = pat.fmap((a) => (b) => Array.isArray(a) ? [...a, b] : [a, b]).appLeft(friend);
break;
}
case 'range': {
const friend = enter(op.arguments_.element);
pat = strudel.reify(pat);
const arrayRange = (start, stop, step = 1) =>
Array.from({ length: Math.abs(stop - start) / step + 1 }, (value, index) =>
start < stop ? start + index * step : start - index * step,
);
let range = (apat, bpat) => apat.squeezeBind((a) => bpat.bind((b) => strudel.fastcat(...arrayRange(a, b))));
pat = range(pat, friend);
break;
}
default: {
console.warn(`operator "${op.type_}" not implemented`);
}
-6
View File
@@ -184,12 +184,6 @@ describe('mini', () => {
it('supports lists', () => {
expect(minV('a:b c:d:[e:f] g')).toEqual([['a', 'b'], ['c', 'd', ['e', 'f']], 'g']);
});
it('supports ranges', () => {
expect(minV('0 .. 4')).toEqual([0, 1, 2, 3, 4]);
});
it('supports patterned ranges', () => {
expect(minS('[<0 1> .. <2 4>]*2')).toEqual(minS('[0 1 2] [1 2 3 4]'));
});
});
describe('getLeafLocation', () => {
+2 -2
View File
@@ -34,6 +34,6 @@ Now open the REPL and type:
s("<bd sd> hh").osc()
```
or just [click here](https://strudel.cc/#cygiPGJkIHNkPiBoaCIpLm9zYygp)...
or just [click here](https://strudel.tidalcycles.org/#cygiPGJkIHNkPiBoaCIpLm9zYygp)...
You can read more about [how to use Superdirt with Strudel the Tutorial](https://strudel.cc/learn/input-output/#superdirt-api)
You can read more about [how to use Superdirt with Strudel the Tutorial](https://strudel.tidalcycles.org/learn/input-output/#superdirt-api)
+1 -1
View File
@@ -37,7 +37,7 @@ function connect() {
/**
*
* Sends each hap as an OSC message, which can be picked up by SuperCollider or any other OSC-enabled software.
* For more info, read [MIDI & OSC in the docs](https://strudel.cc/learn/input-output)
* For more info, read [MIDI & OSC in the docs](https://strudel.tidalcycles.org/learn/input-output)
*
* @name osc
* @memberof Pattern
+3 -3
View File
@@ -21,12 +21,12 @@ import { samples, initAudioOnFirstClick } from '@strudel.cycles/webaudio';
async function prebake() {
await samples(
'https://strudel.cc/tidal-drum-machines.json',
'https://strudel.tidalcycles.org/tidal-drum-machines.json',
'github:ritchse/tidal-drum-machines/main/machines/'
);
await samples(
'https://strudel.cc/EmuSP12.json',
'https://strudel.cc/EmuSP12/'
'https://strudel.tidalcycles.org/EmuSP12.json',
'https://strudel.tidalcycles.org/EmuSP12/'
);
}
+28 -23
View File
@@ -1,5 +1,5 @@
import { controls, evalScope } from '@strudel.cycles/core';
import { CodeMirror, useHighlighting, useStrudel, flash } from '@strudel.cycles/react';
import { CodeMirror, useHighlighting, useKeydown, useStrudel, flash } from '@strudel.cycles/react';
import {
getAudioContext,
initAudioOnFirstClick,
@@ -93,6 +93,32 @@ function App() {
});
const error = evalError || schedulerError;
useKeydown(
useCallback(
async (e) => {
if (e.ctrlKey || e.altKey) {
if (e.code === 'Enter') {
e.preventDefault();
flash(view);
await evaluate(code);
if (e.shiftKey) {
panic();
scheduler.stop();
scheduler.start();
}
if (!scheduler.started) {
scheduler.start();
}
} else if (e.code === 'Period') {
scheduler.stop();
panic();
e.preventDefault();
}
}
},
[scheduler, evaluate, view, code],
),
);
return (
<div>
<nav className="z-[12] w-full flex justify-center fixed bottom-0">
@@ -110,28 +136,7 @@ function App() {
</div>
{error && <p>error {error.message}</p>}
</nav>
<CodeMirror
value={code}
onChange={setCode}
onViewChanged={setView}
onEvaluate={async () => {
flash(view);
await evaluate(code);
if (!scheduler.started) {
scheduler.start();
}
}}
onReEvaluate={async () => {
await evaluate(code);
panic();
scheduler.stop();
scheduler.start();
}}
onStop={() => {
scheduler.stop();
panic();
}}
/>
<CodeMirror value={code} onChange={setCode} onViewChanged={setView} />
</div>
);
}
-5
View File
@@ -33,17 +33,12 @@
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@codemirror/autocomplete": "^6.6.0",
"@codemirror/commands": "^6.0.0",
"@codemirror/lang-javascript": "^6.1.7",
"@codemirror/language": "^6.0.0",
"@codemirror/lint": "^6.0.0",
"@codemirror/search": "^6.0.0",
"@codemirror/state": "^6.2.0",
"@codemirror/view": "^6.10.0",
"@lezer/highlight": "^1.1.4",
"@replit/codemirror-emacs": "^6.0.1",
"@replit/codemirror-vim": "^6.0.14",
"@replit/codemirror-vscode-keymap": "^6.0.2",
"@strudel.cycles/core": "workspace:*",
"@strudel.cycles/transpiler": "workspace:*",
"@strudel.cycles/webaudio": "workspace:*",
@@ -26,6 +26,7 @@ export function Autocomplete({ doc }) {
<pre
className="cursor-pointer"
onMouseDown={(e) => {
console.log('ola!');
navigator.clipboard.writeText(example);
e.stopPropagation();
}}
+3 -71
View File
@@ -1,15 +1,12 @@
import { autocompletion } from '@codemirror/autocomplete';
import { Prec } from '@codemirror/state';
import { javascript, javascriptLanguage } from '@codemirror/lang-javascript';
import { ViewPlugin, EditorView, keymap } from '@codemirror/view';
import { EditorView } from '@codemirror/view';
import { emacs } from '@replit/codemirror-emacs';
import { vim } from '@replit/codemirror-vim';
import { vscodeKeymap } from '@replit/codemirror-vscode-keymap';
import _CodeMirror from '@uiw/react-codemirror';
import React, { useCallback, useMemo } from 'react';
import strudelTheme from '../themes/strudel-theme';
import { strudelAutocomplete } from './Autocomplete';
import { strudelTooltip } from './Tooltip';
import {
highlightExtension,
flashField,
@@ -18,11 +15,10 @@ import {
updateMiniLocations,
} from '@strudel/codemirror';
import './style.css';
import { sliderPlugin } from '@strudel/codemirror/slider.mjs';
export { flash, highlightMiniLocations, updateMiniLocations };
const staticExtensions = [javascript(), flashField, highlightExtension, sliderPlugin];
const staticExtensions = [javascript(), flashField, highlightExtension];
export default function CodeMirror({
value,
@@ -30,15 +26,10 @@ export default function CodeMirror({
onViewChanged,
onSelectionChange,
onDocChange,
onEvaluate,
onReEvaluate,
onPanic,
onStop,
theme,
keybindings,
isLineNumbersDisplayed,
isAutoCompletionEnabled,
isTooltipEnabled,
isLineWrappingEnabled,
fontSize = 18,
fontFamily = 'monospace',
@@ -69,25 +60,11 @@ export default function CodeMirror({
[onSelectionChange],
);
const vscodePlugin = ViewPlugin.fromClass(
class {
constructor(view) {}
},
{
provide: (plugin) => {
return Prec.highest(keymap.of([...vscodeKeymap]));
},
},
);
const vscodeExtension = (options) => [vscodePlugin].concat(options ?? []);
const extensions = useMemo(() => {
let _extensions = [...staticExtensions];
let bindings = {
vim,
emacs,
vscode: vscodeExtension,
};
if (bindings[keybindings]) {
@@ -99,58 +76,13 @@ export default function CodeMirror({
} else {
_extensions.push(autocompletion({ override: [] }));
}
if (isTooltipEnabled) {
_extensions.push(strudelTooltip);
}
//_extensions.push([keymap.of({})]);
if (isLineWrappingEnabled) {
_extensions.push(EditorView.lineWrapping);
}
_extensions.push(
keymap.of([
{
key: 'Ctrl-Enter',
run: () => onEvaluate?.(),
},
{
key: 'Alt-Enter',
run: () => onEvaluate?.(),
},
{
key: 'Ctrl-.',
run: () => onStop?.(),
},
{
key: 'Alt-.',
run: (_, e) => {
e.preventDefault();
onStop?.();
},
},
{
key: 'Ctrl-Shift-.',
run: () => (onPanic ? onPanic() : onStop?.()),
},
{
key: 'Ctrl-Shift-Enter',
run: () => (onReEvaluate ? onReEvaluate() : onEvaluate?.()),
},
]),
);
return _extensions;
}, [
keybindings,
isAutoCompletionEnabled,
isTooltipEnabled,
isLineWrappingEnabled,
onEvaluate,
onReEvaluate,
onStop,
onPanic,
]);
}, [keybindings, isAutoCompletionEnabled, isLineWrappingEnabled]);
const basicSetup = useMemo(() => ({ lineNumbers: isLineNumbersDisplayed }), [isLineNumbersDisplayed]);
+22 -5
View File
@@ -10,6 +10,7 @@ import { Icon } from './Icon';
import './style.css';
import { logger } from '@strudel.cycles/core';
import useEvent from '../hooks/useEvent.mjs';
import useKeydown from '../hooks/useKeydown.mjs';
const getTime = () => getAudioContext().currentTime;
@@ -91,6 +92,27 @@ export function MiniRepl({
getTime: () => scheduler.now(),
});
// keyboard shortcuts
useKeydown(
useCallback(
async (e) => {
if (view?.hasFocus) {
if (e.ctrlKey || e.altKey) {
if (e.code === 'Enter') {
e.preventDefault();
flash(view);
await activateCode();
} else if (e.key === '.' || e.code === 'Period') {
stop();
e.preventDefault();
}
}
}
},
[activateCode, stop, view],
),
);
const [log, setLog] = useState([]);
useLogger(
useCallback((e) => {
@@ -142,11 +164,6 @@ export function MiniRepl({
fontSize={fontSize}
keybindings={keybindings}
isLineNumbersDisplayed={isLineNumbersDisplayed}
onEvaluate={() => {
flash(view);
activateCode();
}}
onStop={() => stop()}
/>
)}
{error && <div className="text-right p-1 text-md text-red-200">{error.message}</div>}
-69
View File
@@ -1,69 +0,0 @@
import { createRoot } from 'react-dom/client';
import { hoverTooltip } from '@codemirror/view';
import jsdoc from '../../../../doc.json';
import { Autocomplete } from './Autocomplete';
const getDocLabel = (doc) => doc.name || doc.longname;
let ctrlDown = false;
// Record Control key event to trigger or block the tooltip depending on the state
window.addEventListener(
'keyup',
function (e) {
if (e.key == 'Control') {
ctrlDown = false;
}
},
true,
);
window.addEventListener(
'keydown',
function (e) {
if (e.key == 'Control') {
ctrlDown = true;
}
},
true,
);
export const strudelTooltip = hoverTooltip(
(view, pos, side) => {
// Word selection from CodeMirror Hover Tooltip example https://codemirror.net/examples/tooltip/#hover-tooltips
let { from, to, text } = view.state.doc.lineAt(pos);
let start = pos,
end = pos;
while (start > from && /\w/.test(text[start - from - 1])) {
start--;
}
while (end < to && /\w/.test(text[end - from])) {
end++;
}
if ((start == pos && side < 0) || (end == pos && side > 0)) {
return null;
}
let word = text.slice(start - from, end - from);
// Get entry from Strudel documentation
let entry = jsdoc.docs.filter((doc) => getDocLabel(doc) === word)[0];
if (!entry) {
return null;
}
if (!ctrlDown) {
return null;
}
return {
pos: start,
end,
above: false,
arrow: true,
create(view) {
let dom = document.createElement('div');
dom.className = 'strudel-tooltip';
createRoot(dom).render(<Autocomplete doc={entry} />);
return { dom };
},
};
},
{ hoverTime: 10 },
);
-4
View File
@@ -28,7 +28,3 @@
footer {
z-index: 0 !important;
}
.strudel-tooltip {
padding: 5px;
}
-13
View File
@@ -1,13 +0,0 @@
import { useEffect, useState } from 'react';
import { updateWidgets } from '@strudel/codemirror';
// i know this is ugly.. in the future, repl needs to run without react
export function useWidgets(view) {
const [widgets, setWidgets] = useState([]);
useEffect(() => {
if (view) {
updateWidgets(view, widgets);
}
}, [view, widgets]);
return { widgets, setWidgets };
}
+9 -9
View File
@@ -6,23 +6,23 @@ This program is free software: you can redistribute it and/or modify it under th
import { Pattern, isPattern } from '@strudel.cycles/core';
var writeMessagers = {};
var writeMessage;
var choosing = false;
export async function getWriter(name, br) {
export async function getWriter(br = 38400) {
if (choosing) {
return;
}
choosing = true;
if (name in writeMessagers) {
return writeMessagers[name];
if (writeMessage) {
return writeMessage;
}
if ('serial' in navigator) {
const port = await navigator.serial.requestPort();
await port.open({ baudRate: br });
const encoder = new TextEncoder();
const writer = port.writable.getWriter();
writeMessagers[name] = function (message, chk) {
writeMessage = function (message, chk) {
const encoded = encoder.encode(message);
if (!chk) {
writer.write(encoded);
@@ -63,10 +63,10 @@ function crc16(data) {
return crc & 0xffff;
}
Pattern.prototype.serial = function (br = 115200, sendcrc = false, singlecharids = false, name = 'default') {
Pattern.prototype.serial = function (br = 38400, sendcrc = false, singlecharids = false) {
return this.withHap((hap) => {
if (!(name in writeMessagers)) {
getWriter(name, br);
if (!writeMessage) {
getWriter(br);
}
const onTrigger = (time, hap, currentTime) => {
var message = '';
@@ -108,7 +108,7 @@ Pattern.prototype.serial = function (br = 115200, sendcrc = false, singlecharids
const offset = (time - currentTime + latency) * 1000;
window.setTimeout(function () {
writeMessagers[name](message, chk);
writeMessage(message, chk);
}, offset);
};
return hap.setContext({ ...hap.context, onTrigger, dominantTrigger: true });
+1 -5
View File
@@ -1,7 +1,7 @@
# superdough
superdough is a simple web audio sampler and synth, intended for live coding.
It is the default output of [strudel](https://strudel.cc/).
It is the default output of [strudel](https://strudel.tidalcycles.org/).
This package has no ties to strudel and can be used to quickly bake your own music system on the web.
## Install
@@ -67,10 +67,6 @@ superdough({ s: 'bd', delay: 0.5 }, 0, 1);
- `crush`: amplitude bit crusher using given number of bits
- `shape`: distortion effect from 0 (none) to 1 (full). might get loud!
- `pan`: stereo panning from 0 (left) to 1 (right)
- `phaser`: sets the speed of the modulation
- `phaserdepth`: the amount the signal is affected by the phaser effect.
- `phasersweep`: the frequency sweep range of the lfo for the phaser effect.
- `phasercenter`: the amount the signal is affected by the phaser effect.
- `vowel`: vowel filter. possible values: "a", "e", "i", "o", "u"
- `delay`: delay mix
- `delayfeedback`: delay feedback
-79
View File
@@ -1,79 +0,0 @@
import { getAudioContext } from './superdough.mjs';
let worklet;
export async function dspWorklet(ac, code) {
const name = `dsp-worklet-${Date.now()}`;
const workletCode = `${code}
let __q = []; // trigger queue
class MyProcessor extends AudioWorkletProcessor {
constructor() {
super();
this.t = 0;
this.stopped = false;
this.port.onmessage = (e) => {
if(e.data==='stop') {
this.stopped = true;
} else if(e.data?.dough) {
__q.push(e.data)
} else {
msg?.(e.data)
}
};
}
process(inputs, outputs, parameters) {
const output = outputs[0];
if(__q.length) {
for(let i=0;i<__q.length;++i) {
const deadline = __q[i].time-currentTime;
if(deadline<=0) {
trigger(__q[i].dough)
__q.splice(i,1)
}
}
}
for (let i = 0; i < output[0].length; i++) {
const out = dsp(this.t / sampleRate);
output.forEach((channel) => {
channel[i] = out;
});
this.t++;
}
return !this.stopped;
}
}
registerProcessor('${name}', MyProcessor);
`;
const base64String = btoa(workletCode);
const dataURL = `data:text/javascript;base64,${base64String}`;
await ac.audioWorklet.addModule(dataURL);
const node = new AudioWorkletNode(ac, name);
const stop = () => node.port.postMessage('stop');
return { node, stop };
}
const stop = () => {
if (worklet) {
worklet?.stop();
worklet?.node?.disconnect();
}
};
if (typeof window !== 'undefined') {
window.addEventListener('message', (e) => {
if (e.data === 'strudel-stop') {
stop();
} else if (e.data?.dough) {
worklet?.node.port.postMessage(e.data);
}
});
}
export const dough = async (code) => {
const ac = getAudioContext();
stop();
worklet = await dspWorklet(ac, code);
worklet.node.connect(ac.destination);
};
export function doughTrigger(t, hap, currentTime, duration, cps) {
window.postMessage({ time: t, dough: hap.value, currentTime, duration, cps });
}
-33
View File
@@ -78,17 +78,6 @@ export const getParamADSR = (param, attack, decay, sustain, release, min, max, b
param.linearRampToValueAtTime(min, end + Math.max(release, 0.1));
};
export function getCompressor(ac, threshold, ratio, knee, attack, release) {
const options = {
threshold: threshold ?? -3,
ratio: ratio ?? 10,
knee: knee ?? 10,
attack: attack ?? 0.005,
release: release ?? 0.05,
};
return new DynamicsCompressorNode(ac, options);
}
export function createFilter(
context,
type,
@@ -123,25 +112,3 @@ export function createFilter(
return filter;
}
// stays 1 until .5, then fades out
let wetfade = (d) => (d < 0.5 ? 1 : 1 - (d - 0.5) / 0.5);
// mix together dry and wet nodes. 0 = only dry 1 = only wet
// still not too sure about how this could be used more generally...
export function drywet(dry, wet, wetAmount = 0) {
const ac = getAudioContext();
if (!wetAmount) {
return dry;
}
let dry_gain = ac.createGain();
let wet_gain = ac.createGain();
dry.connect(dry_gain);
wet.connect(wet_gain);
dry_gain.gain.value = wetfade(wetAmount);
wet_gain.gain.value = wetfade(1 - wetAmount);
let mix = ac.createGain();
dry_gain.connect(mix);
wet_gain.connect(mix);
return mix;
}
-1
View File
@@ -10,4 +10,3 @@ export * from './helpers.mjs';
export * from './synth.mjs';
export * from './zzfx.mjs';
export * from './logger.mjs';
export * from './dspworklet.mjs';
-63
View File
@@ -1,63 +0,0 @@
import { drywet } from './helpers.mjs';
import { getAudioContext } from './superdough.mjs';
let noiseCache = {};
// lazy generates noise buffers and keeps them forever
function getNoiseBuffer(type) {
const ac = getAudioContext();
if (noiseCache[type]) {
return noiseCache[type];
}
const bufferSize = 2 * ac.sampleRate;
const noiseBuffer = ac.createBuffer(1, bufferSize, ac.sampleRate);
const output = noiseBuffer.getChannelData(0);
let lastOut = 0;
let b0, b1, b2, b3, b4, b5, b6;
b0 = b1 = b2 = b3 = b4 = b5 = b6 = 0.0;
for (let i = 0; i < bufferSize; i++) {
if (type === 'white') {
output[i] = Math.random() * 2 - 1;
} else if (type === 'brown') {
let white = Math.random() * 2 - 1;
output[i] = (lastOut + 0.02 * white) / 1.02;
lastOut = output[i];
} else if (type === 'pink') {
let white = Math.random() * 2 - 1;
b0 = 0.99886 * b0 + white * 0.0555179;
b1 = 0.99332 * b1 + white * 0.0750759;
b2 = 0.969 * b2 + white * 0.153852;
b3 = 0.8665 * b3 + white * 0.3104856;
b4 = 0.55 * b4 + white * 0.5329522;
b5 = -0.7616 * b5 - white * 0.016898;
output[i] = b0 + b1 + b2 + b3 + b4 + b5 + b6 + white * 0.5362;
output[i] *= 0.11;
b6 = white * 0.115926;
}
}
noiseCache[type] = noiseBuffer;
return noiseBuffer;
}
// expects one of noises as type
export function getNoiseOscillator(type = 'white', t) {
const ac = getAudioContext();
const o = ac.createBufferSource();
o.buffer = getNoiseBuffer(type);
o.loop = true;
o.start(t);
return {
node: o,
stop: (time) => o.stop(time),
};
}
export function getNoiseMix(inputNode, wet, t) {
const noiseOscillator = getNoiseOscillator('pink', t);
const noiseMix = drywet(inputNode, noiseOscillator.node, wet);
return {
node: noiseMix,
stop: (time) => noiseOscillator?.stop(time),
};
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "superdough",
"version": "0.9.10",
"version": "0.9.8",
"description": "simple web audio synth and sampler intended for live coding. inspired by superdirt and webdirt.",
"main": "index.mjs",
"type": "module",
+15 -39
View File
@@ -1,47 +1,23 @@
import reverbGen from './reverbGen.mjs';
if (typeof AudioContext !== 'undefined') {
AudioContext.prototype.adjustLength = function (duration, buffer) {
const newLength = buffer.sampleRate * duration;
const newBuffer = this.createBuffer(buffer.numberOfChannels, buffer.length, buffer.sampleRate);
for (let channel = 0; channel < buffer.numberOfChannels; channel++) {
let oldData = buffer.getChannelData(channel);
let newData = newBuffer.getChannelData(channel);
for (let i = 0; i < newLength; i++) {
newData[i] = oldData[i] || 0;
}
}
return newBuffer;
AudioContext.prototype.impulseResponse = function (duration, channels = 1) {
const length = this.sampleRate * duration;
const impulse = this.createBuffer(channels, length, this.sampleRate);
const IR = impulse.getChannelData(0);
for (let i = 0; i < length; i++) IR[i] = (2 * Math.random() - 1) * Math.pow(1 - i / length, duration);
return impulse;
};
AudioContext.prototype.createReverb = function (duration, fade, lp, dim, ir) {
AudioContext.prototype.createReverb = function (duration) {
const convolver = this.createConvolver();
convolver.generate = (d = 2, fade = 0.1, lp = 15000, dim = 1000, ir) => {
convolver.duration = d;
convolver.fade = fade;
convolver.lp = lp;
convolver.dim = dim;
convolver.ir = ir;
if (ir) {
convolver.buffer = this.adjustLength(d, ir);
} else {
reverbGen.generateReverb(
{
audioContext: this,
numChannels: 2,
decayTime: d,
fadeInTime: fade,
lpFreqStart: lp,
lpFreqEnd: dim,
},
(buffer) => {
convolver.buffer = buffer;
},
);
}
convolver.setDuration = (d) => {
convolver.buffer = this.impulseResponse(d);
convolver.duration = duration;
return convolver;
};
convolver.generate(duration, fade, lp, dim, ir);
convolver.setDuration(duration);
return convolver;
};
}
// TODO: make the reverb more exciting
// check out https://blog.gskinner.com/archives/2019/02/reverb-web-audio-api.html
-130
View File
@@ -1,130 +0,0 @@
// Copyright 2014 Alan deLespinasse
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
var reverbGen = {};
/** Generates a reverb impulse response.
@param {!Object} params TODO: Document the properties.
@param {!function(!AudioBuffer)} callback Function to call when
the impulse response has been generated. The impulse response
is passed to this function as its parameter. May be called
immediately within the current execution context, or later. */
reverbGen.generateReverb = function (params, callback) {
var audioContext = params.audioContext || new AudioContext();
var sampleRate = audioContext.sampleRate;
var numChannels = params.numChannels || 2;
// params.decayTime is the -60dB fade time. We let it go 50% longer to get to -90dB.
var totalTime = params.decayTime * 1.5;
var decaySampleFrames = Math.round(params.decayTime * sampleRate);
var numSampleFrames = Math.round(totalTime * sampleRate);
var fadeInSampleFrames = Math.round((params.fadeInTime || 0) * sampleRate);
// 60dB is a factor of 1 million in power, or 1000 in amplitude.
var decayBase = Math.pow(1 / 1000, 1 / decaySampleFrames);
var reverbIR = audioContext.createBuffer(numChannels, numSampleFrames, sampleRate);
for (var i = 0; i < numChannels; i++) {
var chan = reverbIR.getChannelData(i);
for (var j = 0; j < numSampleFrames; j++) {
chan[j] = randomSample() * Math.pow(decayBase, j);
}
for (var j = 0; j < fadeInSampleFrames; j++) {
chan[j] *= j / fadeInSampleFrames;
}
}
applyGradualLowpass(reverbIR, params.lpFreqStart || 0, params.lpFreqEnd || 0, params.decayTime, callback);
};
/** Creates a canvas element showing a graph of the given data.
@param {!Float32Array} data An array of numbers, or a Float32Array.
@param {number} width Width in pixels of the canvas.
@param {number} height Height in pixels of the canvas.
@param {number} min Minimum value of data for the graph (lower edge).
@param {number} max Maximum value of data in the graph (upper edge).
@return {!CanvasElement} The generated canvas element. */
reverbGen.generateGraph = function (data, width, height, min, max) {
var canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
var gc = canvas.getContext('2d');
gc.fillStyle = '#000';
gc.fillRect(0, 0, canvas.width, canvas.height);
gc.fillStyle = '#fff';
var xscale = width / data.length;
var yscale = height / (max - min);
for (var i = 0; i < data.length; i++) {
gc.fillRect(i * xscale, height - (data[i] - min) * yscale, 1, 1);
}
return canvas;
};
/** Applies a constantly changing lowpass filter to the given sound.
@private
@param {!AudioBuffer} input
@param {number} lpFreqStart
@param {number} lpFreqEnd
@param {number} lpFreqEndAt
@param {!function(!AudioBuffer)} callback May be called
immediately within the current execution context, or later.*/
var applyGradualLowpass = function (input, lpFreqStart, lpFreqEnd, lpFreqEndAt, callback) {
if (lpFreqStart == 0) {
callback(input);
return;
}
var channelData = getAllChannelData(input);
var context = new OfflineAudioContext(input.numberOfChannels, channelData[0].length, input.sampleRate);
var player = context.createBufferSource();
player.buffer = input;
var filter = context.createBiquadFilter();
lpFreqStart = Math.min(lpFreqStart, input.sampleRate / 2);
lpFreqEnd = Math.min(lpFreqEnd, input.sampleRate / 2);
filter.type = 'lowpass';
filter.Q.value = 0.0001;
filter.frequency.setValueAtTime(lpFreqStart, 0);
filter.frequency.linearRampToValueAtTime(lpFreqEnd, lpFreqEndAt);
player.connect(filter);
filter.connect(context.destination);
player.start();
context.oncomplete = function (event) {
callback(event.renderedBuffer);
};
context.startRendering();
window.filterNode = filter;
};
/** @private
@param {!AudioBuffer} buffer
@return {!Array.<!Float32Array>} An array containing the Float32Array of each channel's samples. */
var getAllChannelData = function (buffer) {
var channels = [];
for (var i = 0; i < buffer.numberOfChannels; i++) {
channels[i] = buffer.getChannelData(i);
}
return channels;
};
/** @private
@return {number} A random number from -1 to 1. */
var randomSample = function () {
return Math.random() * 2 - 1;
};
export default reverbGen;
+2 -46
View File
@@ -57,13 +57,13 @@ export const getSampleBufferSource = async (s, n, note, speed, freq, bank, resol
const bufferSource = ac.createBufferSource();
bufferSource.buffer = buffer;
const playbackRate = 1.0 * Math.pow(2, transpose / 12);
// bufferSource.playbackRate.value = Math.pow(2, transpose / 12);
bufferSource.playbackRate.value = playbackRate;
return bufferSource;
};
export const loadBuffer = (url, ac, s, n = 0) => {
const label = s ? `sound "${s}:${n}"` : 'sample';
url = url.replace('#', '%23');
if (!loadCache[url]) {
logger(`[sampler] load ${label}..`, 'load-sample', { url });
const timestamp = Date.now();
@@ -146,12 +146,7 @@ function getSamplesPrefixHandler(url) {
* sd: '808sd/SD0010.WAV'
* }, 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/');
* s("[bd ~]*2, [~ hh]*2, ~ sd")
* @example
* samples('shabda:noise,chimp:2')
* s("noise <chimp:0*2 chimp:1>")
* @example
* samples('shabda/speech/fr-FR/f:chocolat')
* s("chocolat*4")
*
*/
export const samples = async (sampleMap, baseUrl = sampleMap._base || '', options = {}) => {
@@ -161,34 +156,11 @@ export const samples = async (sampleMap, baseUrl = sampleMap._base || '', option
if (handler) {
return handler(sampleMap);
}
if (sampleMap.startsWith('bubo:')) {
const [_, repo] = sampleMap.split(':');
sampleMap = `github:Bubobubobubobubo/dough-${repo}`;
}
if (sampleMap.startsWith('github:')) {
let [_, path] = sampleMap.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';
}
sampleMap = `https://raw.githubusercontent.com/${path}/strudel.json`;
}
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;
@@ -239,8 +211,6 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
begin = 0,
loopEnd = 1,
end = 1,
vib,
vibmod = 0.5,
} = value;
// load sample
if (speed === 0) {
@@ -256,19 +226,6 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
const bufferSource = await getSampleBufferSource(s, n, note, speed, freq, bank, resolveUrl);
// vibrato
let vibratoOscillator;
if (vib > 0) {
vibratoOscillator = getAudioContext().createOscillator();
vibratoOscillator.frequency.value = vib;
const gain = getAudioContext().createGain();
// Vibmod is the amount of vibrato, in semitones
gain.gain.value = vibmod * 100;
vibratoOscillator.connect(gain);
gain.connect(bufferSource.detune);
vibratoOscillator.start(0);
}
// asny stuff above took too long?
if (ac.currentTime > t) {
logger(`[sampler] still loading sound "${s}:${n}"`, 'highlight');
@@ -300,7 +257,6 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
envelope.connect(out);
bufferSource.onended = function () {
bufferSource.disconnect();
vibratoOscillator?.stop();
envelope.disconnect();
out.disconnect();
onended();
+10 -113
View File
@@ -9,21 +9,17 @@ import './reverb.mjs';
import './vowel.mjs';
import { clamp } from './util.mjs';
import workletsUrl from './worklets.mjs?url';
import { createFilter, gainNode, getCompressor } from './helpers.mjs';
import { createFilter, gainNode } from './helpers.mjs';
import { map } from 'nanostores';
import { logger } from './logger.mjs';
import { loadBuffer } from './sampler.mjs';
export const soundMap = map();
export function registerSound(key, onTrigger, data = {}) {
soundMap.setKey(key, { onTrigger, data });
}
export function getSound(s) {
return soundMap.get()[s];
}
export const resetLoadedSounds = () => soundMap.set({});
let audioContext;
@@ -50,7 +46,6 @@ export const panic = () => {
};
let workletsLoading;
function loadWorklets() {
if (workletsLoading) {
return workletsLoading;
@@ -94,7 +89,6 @@ export async function initAudioOnFirstClick(options) {
let delays = {};
const maxfeedback = 0.98;
function getDelay(orbit, delaytime, delayfeedback, t) {
if (delayfeedback > maxfeedback) {
//logger(`delayfeedback was clamped to ${maxfeedback} to save your ears`);
@@ -112,79 +106,22 @@ function getDelay(orbit, delaytime, delayfeedback, t) {
return delays[orbit];
}
// each orbit will have its own lfo
const phaserLFOs = {};
function getPhaser(orbit, t, speed = 1, depth = 0.5, centerFrequency = 1000, sweep = 2000) {
//gain
const ac = getAudioContext();
const lfoGain = ac.createGain();
lfoGain.gain.value = sweep;
//LFO
if (phaserLFOs[orbit] == null) {
phaserLFOs[orbit] = ac.createOscillator();
phaserLFOs[orbit].frequency.value = speed;
phaserLFOs[orbit].type = 'sine';
phaserLFOs[orbit].start();
}
phaserLFOs[orbit].connect(lfoGain);
if (phaserLFOs[orbit].frequency.value != speed) {
phaserLFOs[orbit].frequency.setValueAtTime(speed, t);
}
//filters
const numStages = 2; //num of filters in series
let fOffset = 0;
const filterChain = [];
for (let i = 0; i < numStages; i++) {
const filter = ac.createBiquadFilter();
filter.type = 'notch';
filter.gain.value = 1;
filter.frequency.value = centerFrequency + fOffset;
filter.Q.value = 2 - Math.min(Math.max(depth * 2, 0), 1.9);
lfoGain.connect(filter.detune);
fOffset += 282;
if (i > 0) {
filterChain[i - 1].connect(filter);
}
filterChain.push(filter);
}
return filterChain[filterChain.length - 1];
}
let reverbs = {};
let hasChanged = (now, before) => now !== undefined && now !== before;
function getReverb(orbit, duration, fade, lp, dim, ir) {
// If no reverb has been created for a given orbit, create one
function getReverb(orbit, duration = 2) {
if (!reverbs[orbit]) {
const ac = getAudioContext();
const reverb = ac.createReverb(duration, fade, lp, dim, ir);
const reverb = ac.createReverb(duration);
reverb.connect(getDestination());
reverbs[orbit] = reverb;
}
if (
hasChanged(duration, reverbs[orbit].duration) ||
hasChanged(fade, reverbs[orbit].fade) ||
hasChanged(lp, reverbs[orbit].lp) ||
hasChanged(dim, reverbs[orbit].dim) ||
reverbs[orbit].ir !== ir
) {
// only regenerate when something has changed
// avoids endless regeneration on things like
// stack(s("a"), s("b").rsize(8)).room(.5)
// this only works when args may stay undefined until here
// setting default values breaks this
reverbs[orbit].generate(duration, fade, lp, dim, ir);
if (reverbs[orbit].duration !== duration) {
reverbs[orbit] = reverbs[orbit].setDuration(duration);
reverbs[orbit].duration = duration;
}
return reverbs[orbit];
}
export let analyser, analyserData /* s = {} */;
export function getAnalyser(/* orbit, */ fftSize = 2048) {
if (!analyser /*s [orbit] */) {
const analyserNode = getAudioContext().createAnalyser();
@@ -240,7 +177,6 @@ export const superdough = async (value, deadline, hapDuration) => {
bank,
source,
gain = 0.8,
postgain = 1,
// filters
ftype = '12db',
fanchor = 0.5,
@@ -268,12 +204,6 @@ export const superdough = async (value, deadline, hapDuration) => {
bpsustain = 1,
bprelease = 0.01,
bandq = 1,
//phaser
phaser,
phaserdepth = 0.75,
phasersweep,
phasercenter,
//
coarse,
crush,
@@ -285,20 +215,10 @@ export const superdough = async (value, deadline, hapDuration) => {
delaytime = 0.25,
orbit = 1,
room,
roomfade,
roomlp,
roomdim,
roomsize,
ir,
i = 0,
size = 2,
velocity = 1,
analyze, // analyser wet
fft = 8, // fftSize 0 - 10
compressor: compressorThreshold,
compressorRatio,
compressorKnee,
compressorAttack,
compressorRelease,
} = value;
gain *= velocity; // legacy fix for velocity
let toDisconnect = []; // audio nodes that will be disconnected when the source has ended
@@ -308,7 +228,6 @@ export const superdough = async (value, deadline, hapDuration) => {
if (bank && s) {
s = `${bank}_${s}`;
}
// get source AudioNode
let sourceNode;
if (source) {
@@ -328,7 +247,6 @@ export const superdough = async (value, deadline, hapDuration) => {
// this can be used for things like speed(0) in the sampler
return;
}
if (ac.currentTime > t) {
logger('[webaudio] skip hap: still loading', ac.currentTime - t);
return;
@@ -415,25 +333,15 @@ export const superdough = async (value, deadline, hapDuration) => {
crush !== undefined && chain.push(getWorklet(ac, 'crush-processor', { crush }));
shape !== undefined && chain.push(getWorklet(ac, 'shape-processor', { shape }));
compressorThreshold !== undefined &&
chain.push(
getCompressor(ac, compressorThreshold, compressorRatio, compressorKnee, compressorAttack, compressorRelease),
);
// panning
if (pan !== undefined) {
const panner = ac.createStereoPanner();
panner.pan.value = 2 * pan - 1;
chain.push(panner);
}
// phaser
if (phaser !== undefined && phaserdepth > 0) {
const phaserFX = getPhaser(orbit, t, phaser, phaserdepth, phasercenter, phasersweep);
chain.push(phaserFX);
}
// last gain
const post = gainNode(postgain);
const post = gainNode(1);
chain.push(post);
post.connect(getDestination());
@@ -445,19 +353,8 @@ export const superdough = async (value, deadline, hapDuration) => {
}
// reverb
let reverbSend;
if (room > 0) {
let roomIR;
if (ir !== undefined) {
let url;
let sample = getSound(ir);
if (Array.isArray(sample)) {
url = sample.data.samples[i % sample.data.samples.length];
} else if (typeof sample === 'object') {
url = Object.values(sample.data.samples).flat()[i % Object.values(sample.data.samples).length];
}
roomIR = await loadBuffer(url, ac, ir, 0);
}
const reverbNode = getReverb(orbit, roomsize, roomfade, roomlp, roomdim, roomIR);
if (room > 0 && size > 0) {
const reverbNode = getReverb(orbit, size);
reverbSend = effectSend(post, reverbNode, room);
}
+81 -105
View File
@@ -1,7 +1,6 @@
import { midiToFreq, noteToMidi } from './util.mjs';
import { registerSound, getAudioContext } from './superdough.mjs';
import { gainNode, getEnvelope, getExpEnvelope } from './helpers.mjs';
import { getNoiseMix, getNoiseOscillator } from './noise.mjs';
const mod = (freq, range = 1, type = 'sine') => {
const ctx = getAudioContext();
@@ -21,25 +20,74 @@ const fm = (osc, harmonicityRatio, modulationIndex, wave = 'sine') => {
return mod(modfreq, modgain, wave);
};
const waveforms = ['sine', 'square', 'triangle', 'sawtooth'];
const noises = ['pink', 'white', 'brown'];
export function registerSynthSounds() {
[...waveforms, ...noises].forEach((s) => {
['sine', 'square', 'triangle', 'sawtooth'].forEach((wave) => {
registerSound(
s,
wave,
(t, value, onended) => {
// destructure adsr here, because the default should be different for synths and samples
let { attack = 0.001, decay = 0.05, sustain = 0.6, release = 0.01 } = value;
let sound;
if (waveforms.includes(s)) {
sound = getOscillator(s, t, value);
} else {
sound = getNoiseOscillator(s, t);
let {
attack = 0.001,
decay = 0.05,
sustain = 0.6,
release = 0.01,
fmh: fmHarmonicity = 1,
fmi: fmModulationIndex,
fmenv: fmEnvelopeType = 'lin',
fmattack: fmAttack,
fmdecay: fmDecay,
fmsustain: fmSustain,
fmrelease: fmRelease,
fmvelocity: fmVelocity,
fmwave: fmWaveform = 'sine',
vib = 0,
vibmod = 0.5,
} = value;
let { n, note, freq } = value;
// with synths, n and note are the same thing
note = note || 36;
if (typeof note === 'string') {
note = noteToMidi(note); // e.g. c3 => 48
}
// get frequency
if (!freq && typeof note === 'number') {
freq = midiToFreq(note); // + 48);
}
// maybe pull out the above frequency resolution?? (there is also getFrequency but it has no default)
// make oscillator
const { node: o, stop } = getOscillator({
t,
s: wave,
freq,
vib,
vibmod,
partials: n,
});
let { node: o, stop, triggerRelease } = sound;
// FM + FM envelope
let stopFm, fmEnvelope;
if (fmModulationIndex) {
const { node: modulator, stop } = fm(o, fmHarmonicity, fmModulationIndex, fmWaveform);
if (![fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity].find((v) => v !== undefined)) {
// no envelope by default
modulator.connect(o.frequency);
} else {
fmAttack = fmAttack ?? 0.001;
fmDecay = fmDecay ?? 0.001;
fmSustain = fmSustain ?? 1;
fmRelease = fmRelease ?? 0.001;
fmVelocity = fmVelocity ?? 1;
fmEnvelope = getEnvelope(fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity, t);
if (fmEnvelopeType === 'exp') {
fmEnvelope = getExpEnvelope(fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity, t);
fmEnvelope.node.maxValue = fmModulationIndex * 2;
fmEnvelope.node.minValue = 0.00001;
}
modulator.connect(fmEnvelope.node);
fmEnvelope.node.connect(o.frequency);
}
stopFm = stop;
}
// turn down
const g = gainNode(0.3);
@@ -56,9 +104,10 @@ export function registerSynthSounds() {
node: o.connect(g).connect(envelope),
stop: (releaseTime) => {
releaseEnvelope(releaseTime);
triggerRelease?.(releaseTime);
fmEnvelope?.stop(releaseTime);
let end = releaseTime + release;
stop(end);
stopFm?.(end);
},
};
},
@@ -73,13 +122,13 @@ export function waveformN(partials, type) {
const ac = getAudioContext();
const osc = ac.createOscillator();
const terms = {
sawtooth: (n) => [0, -1 / n],
square: (n) => [0, n % 2 === 0 ? 0 : 1 / n],
triangle: (n) => [n % 2 === 0 ? 0 : 1 / (n * n), 0],
const amplitudes = {
sawtooth: (n) => 1 / n,
square: (n) => (n % 2 === 0 ? 0 : 1 / n),
triangle: (n) => (n % 2 === 0 ? 0 : 1 / (n * n)),
};
if (!terms[type]) {
if (!amplitudes[type]) {
throw new Error(`unknown wave type ${type}`);
}
@@ -87,9 +136,8 @@ export function waveformN(partials, type) {
imag[0] = 0;
let n = 1;
while (n <= partials) {
const [r, i] = terms[type](n);
real[n] = r;
imag[n] = i;
real[n] = amplitudes[type](n);
imag[n] = 0;
n++;
}
@@ -98,108 +146,36 @@ export function waveformN(partials, type) {
return osc;
}
// expects one of waveforms as s
export function getOscillator(
s,
t,
{
n: partials,
note,
freq,
vib = 0,
vibmod = 0.5,
noise = 0,
// fm
fmh: fmHarmonicity = 1,
fmi: fmModulationIndex,
fmenv: fmEnvelopeType = 'lin',
fmattack: fmAttack,
fmdecay: fmDecay,
fmsustain: fmSustain,
fmrelease: fmRelease,
fmvelocity: fmVelocity,
fmwave: fmWaveform = 'sine',
},
) {
let ac = getAudioContext();
export function getOscillator({ s, freq, t, vib, vibmod, partials }) {
// Make oscillator with partial count
let o;
// If no partials are given, use stock waveforms
if (!partials || s === 'sine') {
o = getAudioContext().createOscillator();
o.type = s || 'triangle';
}
// generate custom waveform if partials are given
else {
} else {
o = waveformN(partials, s);
}
// get frequency from note...
note = note || 36;
if (typeof note === 'string') {
note = noteToMidi(note); // e.g. c3 => 48
}
// get frequency
if (!freq && typeof note === 'number') {
freq = midiToFreq(note); // + 48);
}
// set frequency
o.frequency.value = Number(freq);
o.start(t);
// FM
let stopFm, fmEnvelope;
if (fmModulationIndex) {
const { node: modulator, stop } = fm(o, fmHarmonicity, fmModulationIndex, fmWaveform);
if (![fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity].find((v) => v !== undefined)) {
// no envelope by default
modulator.connect(o.frequency);
} else {
fmAttack = fmAttack ?? 0.001;
fmDecay = fmDecay ?? 0.001;
fmSustain = fmSustain ?? 1;
fmRelease = fmRelease ?? 0.001;
fmVelocity = fmVelocity ?? 1;
fmEnvelope = getEnvelope(fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity, t);
if (fmEnvelopeType === 'exp') {
fmEnvelope = getExpEnvelope(fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity, t);
fmEnvelope.node.maxValue = fmModulationIndex * 2;
fmEnvelope.node.minValue = 0.00001;
}
modulator.connect(fmEnvelope.node);
fmEnvelope.node.connect(o.frequency);
}
stopFm = stop;
}
// Additional oscillator for vibrato effect
let vibratoOscillator;
let vibrato_oscillator;
if (vib > 0) {
vibratoOscillator = getAudioContext().createOscillator();
vibratoOscillator.frequency.value = vib;
vibrato_oscillator = getAudioContext().createOscillator();
vibrato_oscillator.frequency.value = vib;
const gain = getAudioContext().createGain();
// Vibmod is the amount of vibrato, in semitones
gain.gain.value = vibmod * 100;
vibratoOscillator.connect(gain);
vibrato_oscillator.connect(gain);
gain.connect(o.detune);
vibratoOscillator.start(t);
}
let noiseMix;
if (noise) {
noiseMix = getNoiseMix(o, noise, t);
vibrato_oscillator.start(t);
}
return {
node: noiseMix?.node || o,
node: o,
stop: (time) => {
vibratoOscillator?.stop(time);
noiseMix?.stop(time);
stopFm?.(time);
vibrato_oscillator?.stop(time);
o.stop(time);
},
triggerRelease: (time) => {
fmEnvelope?.stop(time);
},
};
}
+2 -2
View File
@@ -20,7 +20,7 @@ export const getZZFX = (value, t) => {
pitchJump = 0,
pitchJumpTime = 0,
lfo = 0,
znoise = 0,
noise = 0,
zmod = 0,
zcrush = 0,
zdelay = 0,
@@ -54,7 +54,7 @@ export const getZZFX = (value, t) => {
pitchJump,
pitchJumpTime,
lfo,
znoise,
noise,
zmod,
zcrush,
zdelay,
+1 -1
View File
@@ -31,4 +31,4 @@ yields:
## Tonal API
See "Tonal API" in the [Strudel Tutorial](https://strudel.cc/learn/tonal)
See "Tonal API" in the [Strudel Tutorial](https://strudel.tidalcycles.org/learn/tonal)
+5 -17
View File
@@ -7,19 +7,6 @@ This program is free software: you can redistribute it and/or modify it under th
import { Note, Interval, Scale } from '@tonaljs/tonal';
import { register, _mod } from '@strudel.cycles/core';
const octavesInterval = (octaves) => (octaves <= 0 ? -1 : 1) + octaves * 7 + 'P';
function scaleStep(step, scale) {
scale = scale.replaceAll(':', ' ');
step = Math.ceil(step);
const { intervals, tonic } = Scale.get(scale);
const { pc, oct = 3 } = Note.get(tonic);
const octaveOffset = Math.floor(step / intervals.length);
const scaleStep = _mod(step, intervals.length);
const interval = Interval.add(intervals[scaleStep], octavesInterval(octaveOffset));
return Note.transpose(pc + oct, interval);
}
// transpose note inside scale by offset steps
// function scaleOffset(scale: string, offset: number, note: string) {
function scaleOffset(scale, offset, note) {
@@ -163,12 +150,13 @@ export const scale = register('scale', function (scale, pat) {
return pat.withHap((hap) => {
const isObject = typeof hap.value === 'object';
let note = isObject ? hap.value.n : hap.value;
if (isObject) {
delete hap.value.n; // remove n so it won't cause trouble
}
const asNumber = Number(note);
if (!isNaN(asNumber)) {
note = scaleStep(asNumber, scale);
// TODO: worth keeping for supporting ':' in (non-mininotation) strings?
scale = scale.replaceAll(':', ' ');
let [tonic, scaleName] = Scale.tokenize(scale);
const { pc, oct = 3 } = Note.get(tonic);
note = scaleOffset(pc + ' ' + scaleName, asNumber, pc + oct);
}
return hap.withValue(() => (isObject ? { ...hap.value, note } : note)).setContext({ ...hap.context, scale });
});
+6 -36
View File
@@ -5,7 +5,7 @@ import { isNoteWithOctave } from '@strudel.cycles/core';
import { getLeafLocations } from '@strudel.cycles/mini';
export function transpiler(input, options = {}) {
const { wrapAsync = false, addReturn = true, emitMiniLocations = true, emitWidgets = true } = options;
const { wrapAsync = false, addReturn = true, emitMiniLocations = true } = options;
let ast = parse(input, {
ecmaVersion: 2022,
@@ -16,9 +16,9 @@ export function transpiler(input, options = {}) {
let miniLocations = [];
const collectMiniLocations = (value, node) => {
const leafLocs = getLeafLocations(`"${value}"`, node.start); // stimmt!
//const withOffset = leafLocs.map((offsets) => offsets.map((o) => o + node.start));
miniLocations = miniLocations.concat(leafLocs);
};
let widgets = [];
walk(ast, {
enter(node, parent /* , prop, index */) {
@@ -35,18 +35,6 @@ export function transpiler(input, options = {}) {
emitMiniLocations && collectMiniLocations(value, node);
return this.replace(miniWithLocation(value, node));
}
if (isWidgetFunction(node)) {
emitWidgets &&
widgets.push({
from: node.arguments[0].start,
to: node.arguments[0].end,
value: node.arguments[0].raw, // don't use value!
min: node.arguments[1]?.value ?? 0,
max: node.arguments[2]?.value ?? 1,
step: node.arguments[3]?.value,
});
return this.replace(widgetWithLocation(node));
}
// TODO: remove pseudo note variables?
if (node.type === 'Identifier' && isNoteWithOctave(node.name)) {
this.skip();
@@ -76,14 +64,15 @@ export function transpiler(input, options = {}) {
if (!emitMiniLocations) {
return { output };
}
return { output, miniLocations, widgets };
return { output, miniLocations };
}
function isStringWithDoubleQuotes(node, locations, code) {
if (node.type !== 'Literal') {
const { raw, type } = node;
if (type !== 'Literal') {
return false;
}
return node.raw[0] === '"';
return raw[0] === '"';
}
function isBackTickString(node, parent) {
@@ -105,22 +94,3 @@ function miniWithLocation(value, node) {
optional: false,
};
}
// these functions are connected to @strudel/codemirror -> slider.mjs
// maybe someday there will be pluggable transpiler functions, then move this there
function isWidgetFunction(node) {
return node.type === 'CallExpression' && node.callee.name === 'slider';
}
function widgetWithLocation(node) {
const id = 'slider_' + node.arguments[0].start; // use loc of first arg for id
// add loc as identifier to first argument
// the sliderWithID function is assumed to be sliderWithID(id, value, min?, max?)
node.arguments.unshift({
type: 'Literal',
value: id,
raw: id,
});
node.callee.name = 'sliderWithID';
return node;
}
+2 -2
View File
@@ -51,7 +51,7 @@ document.getElementById('play').addEventListener('click',
)
```
You can learn [more about the `samples` function here](https://strudel.cc/learn/samples#loading-custom-samples).
You can learn [more about the `samples` function here](https://strudel.tidalcycles.org/learn/samples#loading-custom-samples).
### Evaluating Code
@@ -72,7 +72,7 @@ document.getElementById('play').addEventListener('stop',
### Double vs Single Quotes
There is a tiny difference between the [Strudel REPL](https://strudel.cc/) and `@strudel/web`.
There is a tiny difference between the [Strudel REPL](https://strudel.tidalcycles.org/) and `@strudel/web`.
In the REPL you can use 'single quotes' for regular JS strings and "double quotes" for mini notation patterns.
In `@strudel/web`, it does not matter which types of quotes you're using.
There will probably be an escapte hatch for that in the future.
@@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="https://strudel.cc/favicon.ico" />
<link rel="icon" type="image/svg+xml" href="https://strudel.tidalcycles.org/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@strudel/web REPL Example</title>
</head>
+1 -1
View File
@@ -33,4 +33,4 @@ document.getElementById("stop").addEventListener("click", () => scheduler.stop()
[Play with the example codesandbox](https://codesandbox.io/s/amazing-dawn-gclfwg?file=/src/index.js).
Read more in the docs about [samples](https://strudel.cc/learn/samples/), [synths](https://strudel.cc/learn/synths/) and [effects](https://strudel.cc/learn/effects/).
Read more in the docs about [samples](https://strudel.tidalcycles.org/learn/samples/), [synths](https://strudel.tidalcycles.org/learn/synths/) and [effects](https://strudel.tidalcycles.org/learn/effects/).
+3 -28
View File
@@ -3,7 +3,7 @@ import { analyser, getAnalyzerData } from 'superdough';
export function drawTimeScope(
analyser,
{ align = true, color = 'white', thickness = 3, scale = 0.25, pos = 0.75, trigger = 0 } = {},
{ align = true, color = 'white', thickness = 3, scale = 0.25, pos = 0.75, next = 1, trigger = 0 } = {},
) {
const ctx = getDrawContext();
const dataArray = getAnalyzerData('time');
@@ -22,9 +22,10 @@ export function drawTimeScope(
const sliceWidth = (canvas.width * 1.0) / bufferSize;
let x = 0;
for (let i = triggerIndex; i < bufferSize; i++) {
const v = dataArray[i] + 1;
const y = (pos - scale * (v - 1)) * canvas.height;
const y = (scale * (v - 1) + pos) * canvas.height;
if (i === 0) {
ctx.moveTo(x, y);
@@ -70,18 +71,6 @@ function clearScreen(smear = 0, smearRGB = `0,0,0`) {
}
}
/**
* Renders an oscilloscope for the frequency domain of the audio signal.
* @name fscope
* @param {string} color line color as hex or color name. defaults to white.
* @param {number} scale scales the y-axis. Defaults to 0.25
* @param {number} pos y-position relative to screen height. 0 = top, 1 = bottom of screen
* @param {number} lean y-axis alignment where 0 = top and 1 = bottom
* @param {number} min min value
* @param {number} max max value
* @example
* s("sawtooth").fscope()
*/
Pattern.prototype.fscope = function (config = {}) {
return this.analyze(1).draw(() => {
clearScreen(config.smear);
@@ -89,20 +78,6 @@ Pattern.prototype.fscope = function (config = {}) {
});
};
/**
* Renders an oscilloscope for the time domain of the audio signal.
* @name scope
* @synonyms tscope
* @param {object} config optional config with options:
* @param {boolean} align if 1, the scope will be aligned to the first zero crossing. defaults to 1
* @param {string} color line color as hex or color name. defaults to white.
* @param {number} thickness line thickness. defaults to 3
* @param {number} scale scales the y-axis. Defaults to 0.25
* @param {number} pos y-position relative to screen height. 0 = top, 1 = bottom of screen
* @param {number} trigger amplitude value that is used to align the scope. defaults to 0.
* @example
* s("sawtooth").scope()
*/
Pattern.prototype.tscope = function (config = {}) {
return this.analyze(1).draw(() => {
clearScreen(config.smear);
+1 -5
View File
@@ -5,7 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th
*/
import * as strudel from '@strudel.cycles/core';
import { superdough, getAudioContext, setLogger, doughTrigger } from 'superdough';
import { superdough, getAudioContext, setLogger } from 'superdough';
const { Pattern, logger } = strudel;
setLogger(logger);
@@ -35,7 +35,3 @@ export function webaudioScheduler(options = {}) {
onTrigger: strudel.getTrigger({ defaultOutput, getTime }),
});
}
Pattern.prototype.dough = function () {
return this.onTrigger(doughTrigger, 1);
};
+1 -1
View File
@@ -199,7 +199,7 @@ interfaces.
# Links
The Strudel REPL is available at <https://strudel.cc>,
The Strudel REPL is available at <https://strudel.tidalcycles.org>,
including an interactive tutorial. The repository is at
<https://github.com/tidalcycles/strudel>, all the code is open source
under the GPL-3.0 License.
+1 -1
View File
@@ -127,7 +127,7 @@ For the future, it is planned to integrate alternative sound engines such as Gli
# Links
The Strudel REPL is available at <https://strudel.cc>, including an interactive tutorial.
The Strudel REPL is available at <https://strudel.tidalcycles.org>, including an interactive tutorial.
The repository is at <https://github.com/tidalcycles/strudel>, all the code is open source under the GPL-3.0 License.
# Acknowledgments
+2 -2
View File
@@ -717,8 +717,8 @@ fun ahead.</p>
<h1 data-number="11" id="links"><span
class="header-section-number">11</span> Links</h1>
<p>The Strudel REPL is available at <a
href="https://strudel.cc"
class="uri">https://strudel.cc</a>, including an
href="https://strudel.tidalcycles.org"
class="uri">https://strudel.tidalcycles.org</a>, including an
interactive tutorial. The repository is at <a
href="https://github.com/tidalcycles/strudel"
class="uri">https://github.com/tidalcycles/strudel</a>, all the code is
+1 -1
View File
@@ -450,7 +450,7 @@ While Haskell's type system makes it a great language for the ongoing developmen
# Links
The Strudel REPL is available at <https://strudel.cc>, including an interactive tutorial.
The Strudel REPL is available at <https://strudel.tidalcycles.org>, including an interactive tutorial.
The repository is at <https://github.com/tidalcycles/strudel>, all the code is open source under the AGPL-3.0 License.
# Acknowledgments
+1235 -1966
View File
File diff suppressed because it is too large Load Diff
+1196 -112
View File
File diff suppressed because it is too large Load Diff
+9 -2
View File
@@ -17,10 +17,17 @@ tauri-build = { version = "1.4.0", features = [] }
[dependencies]
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
tauri = { version = "1.4.0", features = [ "dialog-all", "clipboard-write-text", "fs-all"] }
tauri = { version = "1.4.0", features = ["fs-all"] }
midir = "0.9.1"
tokio = { version = "1.29.0", features = ["full"] }
rosc = "0.10.1"
web-audio-api = { git = "https://github.com/orottier/web-audio-api-rs.git", branch = "main" }
reqwest = "0.11.20"
quick_cache = "0.4.0"
mini-moka = "0.10.2"
hound = "3.4.0"
rand = "0.8.5"
rand_distr = "0.4.3"
[features]
# this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled.
@@ -31,5 +38,5 @@ custom-protocol = ["tauri/custom-protocol"]
[profile.release]
panic = "abort"
codegen-units = 1
lto = true
lto = "fat"
opt-level = "s"
+38 -24
View File
@@ -4,12 +4,20 @@
mod midibridge;
mod oscbridge;
mod loggerbridge;
mod webaudiobridge;
// mod superdough;
// mod reverbgen;
mod webaudio;
use std::sync::Arc;
use loggerbridge::Logger;
use tauri::Manager;
use tokio::sync::mpsc;
use tokio::sync::Mutex;
use tokio::{
sync::mpsc,
sync::Mutex
};
// the payload type must implement `Serialize` and `Clone`.
#[derive(Clone, serde::Serialize)]
struct Payload {
@@ -21,27 +29,33 @@ fn main() {
let (async_output_transmitter_midi, async_output_receiver_midi) = mpsc::channel(1);
let (async_input_transmitter_osc, async_input_receiver_osc) = mpsc::channel(1);
let (async_output_transmitter_osc, async_output_receiver_osc) = mpsc::channel(1);
let (async_input_transmitter_webaudio, async_input_receiver_webaudio) = mpsc::channel(1);
let (async_output_transmitter_webaudio, async_output_receiver_webaudio) = mpsc::channel(1);
tauri::Builder
::default()
.manage(midibridge::AsyncInputTransmit {
inner: Mutex::new(async_input_transmitter_midi),
})
.manage(oscbridge::AsyncInputTransmit {
inner: Mutex::new(async_input_transmitter_osc),
})
.invoke_handler(tauri::generate_handler![midibridge::sendmidi, oscbridge::sendosc])
.setup(|app| {
let window = Arc::new(app.get_window("main").unwrap());
let logger = Logger { window };
midibridge::init(
logger.clone(),
async_input_receiver_midi,
async_output_receiver_midi,
async_output_transmitter_midi
);
oscbridge::init(logger, async_input_receiver_osc, async_output_receiver_osc, async_output_transmitter_osc);
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
::default()
.manage(midibridge::AsyncInputTransmit {
inner: Mutex::new(async_input_transmitter_midi),
})
.manage(oscbridge::AsyncInputTransmit {
inner: Mutex::new(async_input_transmitter_osc),
})
.manage(webaudiobridge::AsyncInputTransmitWebAudio {
inner: Mutex::new(async_input_transmitter_webaudio),
})
.invoke_handler(tauri::generate_handler![midibridge::sendmidi, oscbridge::sendosc, webaudiobridge::sendwebaudio])
.setup(|app| {
let window = Arc::new(app.get_window("main").unwrap());
let logger = Logger { window };
midibridge::init(
logger.clone(),
async_input_receiver_midi,
async_output_receiver_midi,
async_output_transmitter_midi
);
oscbridge::init(logger, async_input_receiver_osc, async_output_receiver_osc, async_output_transmitter_osc);
webaudiobridge::init(async_input_receiver_webaudio, async_output_receiver_webaudio, async_output_transmitter_webaudio);
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
+5
View File
@@ -0,0 +1,5 @@
mod superdough;
mod reverbgen;
pub use self::superdough::*;
pub use self::reverbgen::*;
+241
View File
@@ -0,0 +1,241 @@
use std::collections::HashMap;
use std::fs::File;
use mini_moka::sync::Cache;
use rand_distr::{Distribution, Normal};
use web_audio_api::AudioBuffer;
use web_audio_api::context::{AudioContext, BaseAudioContext, OfflineAudioContext};
use web_audio_api::node::{AudioNode, DynamicsCompressorNode};
use crate::webaudio::{create_filepath_generic, download_file, Reverb, Sampler, WebAudioInstrument};
use crate::webaudiobridge::WebAudioMessage;
fn apply_decay(input: &mut [f32], decay_factor: f32) {
let len = input.len();
for i in 0..len {
// Multiply each sample by an exponentially decreasing factor
let decay_amt = (-(i as f32) / (len as f32) * decay_factor).exp();
input[i] *= decay_amt;
}
}
fn apply_fir_filter(input: &mut [f32], coefficients: &[f32]) {
let num_coeffs = coefficients.len();
let len = input.len();
let input_copy = input.to_vec();
for i in num_coeffs..len {
let mut acc = 0.0;
for j in 0..num_coeffs {
acc += input_copy[i - j] * coefficients[j];
}
input[i] = acc;
}
}
fn sinc(x: f32, fc: f32) -> f32 {
let pi = std::f32::consts::PI;
if x == 0.0 {
2.0 * fc
} else {
let arg = 2.0 * pi * fc * x;
arg.sin() / arg
}
}
fn hamming(n: usize, size: usize) -> f32 {
0.54 - 0.46 * ((2. * std::f32::consts::PI * n as f32) / ((size - 1) as f32)).cos()
}
pub fn generate_coefficients_lp(size: usize, fc: f32) -> Vec<f32> {
let mut h = vec![0.0; size];
let m = (size - 1) / 2;
for n in 0..size {
h[n] = sinc((n as f32 - m as f32), fc);
h[n] *= hamming(n, size);
}
let sum: f32 = h.iter().sum();
for n in 0..size {
h[n] /= sum;
}
h
}
pub fn create_impulse_response(
len: usize,
decay_factor: f32,
fir_coeffs: &[f32],
sample_rate: f32,
fade: f32,
) -> Vec<f32> {
let mut rng = rand::thread_rng();
let normal = Normal::new(0.0, 1.0).unwrap();
let fade_in_samples = (fade * sample_rate) as usize; // Compute the number of samples for the fade-in duration
let mut impulse_response: Vec<f32> = (0..len)
.map(|_| normal.sample(&mut rng) as f32)
.map(|v| v.max(-1.0).min(1.0)) // Clamp values to the -1.0 to 1.0 range
.collect();
// Apply fade-in to the starting samples
for i in 0..fade_in_samples.min(len) {
impulse_response[i] *= i as f32 / fade_in_samples as f32;
}
apply_decay(&mut impulse_response, decay_factor);
apply_fir_filter(&mut impulse_response, fir_coeffs);
impulse_response
}
pub fn write_to_buffer(buffer: &mut AudioBuffer, data: &[f32]) {
let num_chans = buffer.number_of_channels();
for i in 0..num_chans {
let chan = buffer.get_channel_data_mut(i);
for j in 0..chan.len() {
chan[j] = data[j];
}
}
}
pub fn apply_reverb<T: WebAudioInstrument>(mut audio_context: &AudioContext, compressor: &DynamicsCompressorNode, cache: &Cache<String, AudioBuffer>, message: &WebAudioMessage, instrument: &mut T, reverbs: &mut HashMap<usize, Reverb>) {
if message.reverb.ir.is_none() {
let key = format!("reverb: {}{}{}{}{}", message.reverb.room.unwrap_or(0.0), message.reverb.size.unwrap_or(1.0), message.reverb.roomlp.unwrap_or(0.0), message.reverb.roomfade.unwrap_or(0.0), message.reverb.roomdim.unwrap_or(0.0));
if let Some(ir) = cache.get(&key) {
let reverb = reverbs.entry(message.orbit).or_insert({
let mut reverb = Reverb::new(&audio_context, message.reverb.room, message.reverb.size, message.reverb.roomlp, message.reverb.roomfade, message.reverb.roomdim, &compressor);
reverb.convolver.set_buffer(ir.clone());
instrument.connect_reverb(&reverb.convolver);
reverb
});
if reverb.room.unwrap_or(0.0) != message.reverb.room.unwrap_or(0.0)
|| reverb.roomlp.unwrap_or(0.0) != message.reverb.roomlp.unwrap_or(0.0)
|| reverb.roomfade.unwrap_or(0.0) != message.reverb.roomfade.unwrap_or(0.0)
|| reverb.roomdim.unwrap_or(0.3) != message.reverb.roomdim.unwrap_or(0.0)
|| reverb.roomsize.unwrap_or(0.0) != message.reverb.size.unwrap_or(0.0)
|| reverb.roomfade.unwrap_or(0.0) != message.reverb.roomfade.unwrap_or(0.0) {
reverb.room = message.reverb.room;
reverb.roomlp = message.reverb.roomlp;
reverb.roomfade = message.reverb.roomfade;
reverb.roomdim = message.reverb.roomdim;
reverb.roomsize = message.reverb.size;
let len = (audio_context.sample_rate() * message.reverb.size.unwrap_or(0.6)) as usize;
let decay = message.reverb.roomdim.unwrap_or(2.0);
let num_coeffs = 101;
let fade = message.reverb.roomfade.unwrap_or(0.01);
let cutoff = message.reverb.roomlp.unwrap_or(1000.0) / audio_context.sample_rate();
let cache_cache = cache.clone();
let mut buffer = audio_context.create_buffer(2, len, 44100.0);
tokio::spawn(async move {
// GENERATE IR
let fir = generate_coefficients_lp(num_coeffs, cutoff);
let ir = create_impulse_response(len, decay, &fir, 44100.0, fade);
write_to_buffer(&mut buffer, &ir);
cache_cache.insert(key, buffer);
});
}
reverb.convolver.set_buffer(ir);
instrument.connect_reverb(&reverb.convolver);
// Generate new IR buffer if it doesn't exist
} else {
let len = (audio_context.sample_rate() * message.reverb.size.unwrap_or(1.0)) as usize;
let decay = 4.0;
let num_coeffs = 101;
let fade = 2.4;
let cutoff = 500.0 / audio_context.sample_rate();
let cache_cache = cache.clone();
let mut buffer = audio_context.create_buffer(2, len, 44100.0);
tokio::spawn(async move {
// Generate IR and write to buffer
let fir = generate_coefficients_lp(num_coeffs, cutoff);
let ir = create_impulse_response(len, decay, &fir, 44100.0, fade);
write_to_buffer(&mut buffer, &ir);
cache_cache.insert(key, buffer);
});
}
} else if message.reverb.ir.is_some() {
let message_url = &match message.reverb.url.clone() {
Some(url) => url,
None => "todo".to_string(),
};
let (ir_url, ir_file_path, ir_file_path_clone) = create_filepath_generic(message_url, &message.dirname);
let ir_cache_clone = cache.clone();
let requested_size = message.reverb.size.unwrap_or(2.0).clone();
if let Some(ir_buffer) = cache.get(&ir_file_path_clone) {
let reverb = reverbs.entry(message.orbit).or_insert({
let mut reverb = Reverb::new(&audio_context, message.reverb.room, message.reverb.size, message.reverb.roomlp, message.reverb.roomfade, message.reverb.roomdim, &compressor);
reverb.convolver.set_buffer(ir_buffer.clone());
instrument.connect_reverb(&reverb.convolver);
reverb
});
if reverb.roomsize.unwrap_or(0.0) != message.reverb.size.unwrap_or(0.0) {
adjust_length(&mut audio_context, cache, message, ir_file_path_clone, requested_size, reverb);
}
reverb.convolver.set_buffer(ir_buffer);
instrument.connect_reverb(&reverb.convolver);
} else {
tokio::spawn(async move {
if let Ok(file1) = File::open(&ir_file_path) {
create_new_ir_buffer_from_file(&ir_file_path, ir_cache_clone, requested_size, file1);
} else if tokio::fs::metadata(&ir_file_path.clone()).await.is_err() {
download_file(ir_url, &ir_file_path).await;
}
});
}
}
}
fn create_new_ir_buffer_from_file(ir_file_path: &String, ir_cache_clone: Cache<String, AudioBuffer>, requested_size: f32, file1: File) {
let context = OfflineAudioContext::new(2, 400, 44100.0);
let ir_buffer = context.decode_audio_data_sync(file1).expect("Failed to decode file.");
let new_length = ir_buffer.sample_rate() * requested_size;
let mut new_buffer = context.create_buffer(ir_buffer.number_of_channels(), new_length as usize, ir_buffer.sample_rate());
for ch in 0..ir_buffer.number_of_channels() {
let old_data = ir_buffer.get_channel_data(ch);
let new_data = new_buffer.get_channel_data_mut(ch);
for i in 0..new_length as usize {
if i < old_data.len() {
new_data[i] = old_data[i];
} else {
new_data[i] = 0.0;
}
}
}
ir_cache_clone.insert(ir_file_path.clone(), new_buffer);
}
fn adjust_length(audio_context: &mut &AudioContext, cache: &Cache<String, AudioBuffer>, message: &WebAudioMessage, ir_file_path_clone: String, requested_size: f32, reverb: &mut Reverb) {
let buf = reverb.convolver.buffer().unwrap();
reverb.roomsize = message.reverb.size;
let new_length = buf.sample_rate() * requested_size;
let mut new_buffer = audio_context.create_buffer(buf.number_of_channels(), new_length as usize, buf.sample_rate());
for ch in 0..buf.number_of_channels() {
let old_data = buf.get_channel_data(ch);
let new_data = new_buffer.get_channel_data_mut(ch);
for i in 0..new_length as usize {
if i < old_data.len() {
new_data[i] = old_data[i];
} else {
new_data[i] = 0.0;
}
}
}
reverb.convolver.set_buffer(new_buffer.clone());
cache.insert(ir_file_path_clone.clone(), new_buffer);
}
+393
View File
@@ -0,0 +1,393 @@
use std::collections::HashMap;
use std::path::Path;
use reqwest::Url;
use tokio::fs;
use tokio::io::AsyncWriteExt;
use web_audio_api::{
context::{AudioContext, BaseAudioContext},
AudioBuffer,
node::{AudioBufferSourceNode, AudioNode, AudioScheduledSourceNode, BiquadFilterNode, BiquadFilterType, GainNode, OscillatorNode, OscillatorType},
node::BiquadFilterType::{Bandpass, Highpass, Lowpass},
};
use web_audio_api::node::{ConvolverNode, DelayNode, DynamicsCompressorNode};
use crate::webaudiobridge::WebAudioMessage;
#[derive(Clone, Copy, Debug)]
pub struct DelayMessage {
pub wet: Option<f32>,
pub delay_time: Option<f32>,
pub feedback: Option<f32>,
}
#[derive(Clone, Debug)]
pub struct ReverbMessage {
pub room: Option<f32>,
pub size: Option<f32>,
pub roomfade: Option<f32>,
pub roomlp: Option<f32>,
pub roomdim: Option<f32>,
pub ir: Option<String>,
pub url: Option<String>,
}
#[derive(Clone, Copy, Debug)]
pub struct LoopMessage {
pub is_loop: u8,
pub loop_start: f64,
pub loop_end: f64,
}
#[derive(Clone, Copy, Debug)]
pub struct LPFMessage {
pub frequency: f32,
pub resonance: f32,
}
#[derive(Clone, Copy, Debug)]
pub struct HPFMessage {
pub frequency: f32,
pub resonance: f32,
}
#[derive(Clone, Copy, Debug)]
pub struct BPFMessage {
pub frequency: f32,
pub resonance: f32,
}
#[derive(Debug, Copy, Clone)]
pub struct ADSRMessage {
pub attack: Option<f64>,
pub decay: Option<f64>,
pub sustain: Option<f32>,
pub release: Option<f64>,
}
#[derive(Debug, Copy, Clone)]
pub struct FilterADSRMessage {
pub attack: Option<f64>,
pub decay: Option<f64>,
pub sustain: Option<f64>,
pub release: Option<f64>,
pub env: Option<f64>,
}
pub trait WebAudioInstrument {
fn set_adsr(&mut self, t: f64, adsr: &ADSRMessage, velocity: f32, duration: f64);
fn play(&mut self, t: f64, message: &WebAudioMessage, duration: f64);
fn set_filters(&mut self, context: &mut AudioContext, message: &WebAudioMessage) -> Vec<BiquadFilterNode>;
fn connect_delay(&mut self, delay: &DelayNode);
fn connect_reverb(&mut self, reverb: &ConvolverNode);
}
pub struct Delay {
pub delay: DelayNode,
pub input: GainNode,
pub output: GainNode,
pub feedback: GainNode,
pub pre_gain: GainNode,
}
impl Delay {
pub fn new(context: &AudioContext, compressor: &DynamicsCompressorNode) -> Self {
let delay = context.create_delay(1.);
let input = context.create_gain();
let output = context.create_gain();
let feedback = context.create_gain();
let pre_gain = context.create_gain();
output.connect(compressor);
delay.connect(&output);
feedback.connect(&delay);
pre_gain.connect(&feedback);
input.connect(&pre_gain);
Self { delay, input, output, feedback, pre_gain }
}
}
pub struct Reverb {
pub convolver: ConvolverNode,
pub room: Option<f32>,
pub roomsize: Option<f32>,
pub roomfade: Option<f32>,
pub roomlp: Option<f32>,
pub roomdim: Option<f32>,
}
impl Reverb {
pub(crate) fn new(context: &AudioContext, room: Option<f32>, roomsize: Option<f32>, roomfade: Option<f32>, roomlp: Option<f32>, roomdim: Option<f32>, compressor: &DynamicsCompressorNode) -> Self {
let mut convolver = context.create_convolver();
convolver.connect(compressor);
Self { convolver, room, roomsize, roomfade, roomlp, roomdim }
}
}
pub struct Synth {
pub oscillator: OscillatorNode,
pub envelope: GainNode,
}
impl Synth {
pub fn new(context: &mut AudioContext) -> Self {
let oscillator = context.create_oscillator();
let envelope = context.create_gain();
Self { oscillator, envelope }
}
pub fn set_frequency(&mut self, frequency: &f32) {
self.oscillator.frequency().set_value(*frequency);
}
pub fn set_waveform(&mut self, waveform: &str) {
match waveform {
"sine" => self.oscillator.set_type(OscillatorType::Sine),
"square" => self.oscillator.set_type(OscillatorType::Square),
"triangle" => self.oscillator.set_type(OscillatorType::Triangle),
"saw" | "sawtooth" => self.oscillator.set_type(OscillatorType::Sawtooth),
_ => {}
}
}
}
impl WebAudioInstrument for Synth {
fn set_adsr(&mut self, t: f64, adsr: &ADSRMessage, velocity: f32, duration: f64) {
let attack = adsr.attack.unwrap_or(0.001);
let decay = adsr.decay.unwrap_or(0.05);
let sustain = adsr.sustain.unwrap_or(0.6);
let release = adsr.release.unwrap_or(0.01);
self.envelope.gain()
.set_value_at_time(0.0, t)
.linear_ramp_to_value_at_time(velocity, t + attack)
.exponential_ramp_to_value_at_time((sustain + 0.0001) * velocity, t + attack + decay)
// .set_value_at_time((sustain + 0.00001) * velocity, t + duration)
.exponential_ramp_to_value_at_time(0.000001, t + duration + release);
}
fn play(&mut self, t: f64, message: &WebAudioMessage, release: f64) {
self.oscillator.start();
self.oscillator.stop_at(t + message.duration + release);
}
fn set_filters(&mut self, context: &mut AudioContext, message: &WebAudioMessage) -> Vec<BiquadFilterNode> {
let mut filters = Vec::new();
if message.bpf.frequency > 0.0 {
let mut bpf = context.create_biquad_filter();
bpf.set_type(Bandpass);
bpf.frequency().set_value(message.bpf.frequency);
bpf.q().set_value(message.bpf.resonance);
filters.push(bpf);
} else if message.lpf.frequency > 0.0 {
let mut lpf = context.create_biquad_filter();
lpf.set_type(Lowpass);
lpf.frequency().set_value(message.lpf.frequency);
lpf.q().set_value(message.lpf.resonance);
filters.push(lpf);
} else if message.hpf.frequency > 0.0 {
let mut hpf = context.create_biquad_filter();
hpf.set_type(Highpass);
hpf.frequency().set_value(message.hpf.frequency);
hpf.q().set_value(message.hpf.resonance);
filters.push(hpf);
}
if !filters.is_empty() {
self.oscillator.connect(filters.first().unwrap());
filters.last().unwrap().connect(&self.envelope);
} else {
self.oscillator.connect(&self.envelope);
};
filters
}
fn connect_delay(&mut self, delay: &DelayNode) {
self.envelope.connect(delay);
}
fn connect_reverb(&mut self, reverb: &ConvolverNode) {
self.envelope.connect(reverb);
}
}
pub struct Sampler {
pub sample: AudioBufferSourceNode,
pub envelope: GainNode,
}
impl Sampler {
pub fn new(context: &mut AudioContext, audio_buffer: AudioBuffer) -> Self {
let mut sample = context.create_buffer_source();
sample.set_buffer(audio_buffer);
let envelope = context.create_gain();
Self { sample, envelope }
}
}
impl WebAudioInstrument for Sampler {
fn set_adsr(&mut self, t: f64, adsr: &ADSRMessage, velocity: f32, duration: f64) {
let attack = adsr.attack.unwrap_or(0.001);
let decay = adsr.decay.unwrap_or(0.001);
let sustain = adsr.sustain.unwrap_or(1.0);
let release = adsr.release.unwrap_or(0.01);
self.envelope.gain()
.set_value_at_time(0.0, t)
.linear_ramp_to_value_at_time(velocity, t + attack)
.linear_ramp_to_value_at_time((sustain + 0.00001) * velocity, t + attack + decay)
.set_value_at_time((sustain + 0.00001) * velocity, t + duration)
.linear_ramp_to_value_at_time(0.0, t + duration + release);
}
fn play(&mut self, t: f64, message: &WebAudioMessage, release: f64) {
let buffer_duration = release;
let (start_at, stop_at) = if message.speed < 0.0 {
(buffer_duration, t + message.duration + 0.2)
} else {
(message.begin * buffer_duration, t + message.duration + message.adsr.release.unwrap_or(0.01))
};
if message.looper.is_loop > 0 {
self.sample.set_loop(true);
self.sample.set_loop_start(message.looper.loop_start);
self.sample.set_loop_end(message.looper.loop_end);
self.sample.start_at_with_offset(
t,
self.sample.loop_start(),
);
self.sample.stop_at(t + message.duration + message.adsr.release.unwrap_or(0.01));
} else {
self.sample.start_at_with_offset(
t,
start_at,
);
self.sample.stop_at(stop_at);
}
}
fn set_filters(&mut self, context: &mut AudioContext, message: &WebAudioMessage) -> Vec<BiquadFilterNode> {
let mut filters = Vec::new();
if message.bpf.frequency > 0.0 {
let mut bpf = context.create_biquad_filter();
bpf.set_type(Bandpass);
bpf.frequency().set_value(message.bpf.frequency);
bpf.q().set_value(message.bpf.resonance);
filters.push(bpf);
} else if message.lpf.frequency > 0.0 {
let mut lpf = context.create_biquad_filter();
lpf.set_type(Lowpass);
lpf.frequency().set_value(message.lpf.frequency);
lpf.q().set_value(message.lpf.resonance);
filters.push(lpf);
} else if message.hpf.frequency > 0.0 {
let mut hpf = context.create_biquad_filter();
hpf.set_type(Highpass);
hpf.frequency().set_value(message.hpf.frequency);
hpf.q().set_value(message.hpf.resonance);
filters.push(hpf);
}
if !filters.is_empty() {
self.sample.connect(filters.first().unwrap());
filters.last().unwrap().connect(&self.envelope);
} else {
self.sample.connect(&self.envelope);
};
filters
}
fn connect_delay(&mut self, delay: &DelayNode) {
self.envelope.connect(delay);
}
fn connect_reverb(&mut self, reverb: &ConvolverNode) {
self.envelope.connect(reverb);
}
}
pub fn is_filter_envelope_on(adsr: &FilterADSRMessage) -> bool {
adsr.env.is_some() || adsr.attack.is_some() || adsr.decay.is_some() || adsr.sustain.is_some() || adsr.release.is_some()
}
pub fn apply_filter_adsr(filter_node: &BiquadFilterNode, message: &WebAudioMessage, filter: &BiquadFilterType, now: f64) {
let env = match filter {
Lowpass => message.lpenv,
Highpass => message.hpenv,
Bandpass => message.bpenv,
_ => message.lpenv,
};
let freq = match filter {
Lowpass => message.lpf.frequency,
Highpass => message.hpf.frequency,
Bandpass => message.bpf.frequency,
_ => 8000.0,
};
let offset = env.env.unwrap_or(1.0) * 0.5;
let min = (2f32.powf(-offset as f32) * freq).clamp(0.0, 20000.0);
let max = (2f32.powf((env.env.unwrap_or(1.0) - offset) as f32) * freq).clamp(0.0, 20000.0);
let range = max - min;
let peak = min + range;
let sustain_level = min + env.sustain.unwrap_or(1.0) as f32 * range;
filter_node.frequency().set_value_at_time(min, now)
.linear_ramp_to_value_at_time(peak, now + env.attack.unwrap_or(0.01))
.linear_ramp_to_value_at_time(sustain_level, now + env.attack.unwrap_or(0.01) + env.decay.unwrap_or(0.01))
// .set_value_at_time(sustain_level, now + message.duration)
.linear_ramp_to_value_at_time(min, now + message.duration + env.release.unwrap_or(0.01));
}
pub fn apply_delay<T: WebAudioInstrument>(audio_context: &mut AudioContext, delays: &mut HashMap<usize, Delay>, compressor: &DynamicsCompressorNode, message: &WebAudioMessage, t: f64, instrument: &mut T) {
delays.entry(message.orbit).or_insert({
Delay::new(&audio_context, &compressor)
});
let delay = delays.get(&message.orbit).unwrap();
instrument.connect_delay(&delay.delay);
delay.delay.delay_time().set_value(message.delay.delay_time.unwrap_or(0.25));
delay.feedback.gain().set_value(message.delay.feedback.unwrap_or(0.5));
delay.pre_gain.gain().set_value_at_time(message.delay.wet.unwrap_or(0.25), t + message.delay.delay_time.unwrap_or(0.5) as f64);
}
pub fn apply_delay_synth(audio_context: &mut AudioContext, delays: &mut HashMap<usize, Delay>, compressor: &DynamicsCompressorNode, message: &WebAudioMessage, t: f64, synth: &mut Synth) {
delays.entry(message.orbit).or_insert({
Delay::new(&audio_context, &compressor)
});
let delay = delays.get(&message.orbit).unwrap();
synth.envelope.connect(&delay.input);
delay.delay.delay_time().set_value(message.delay.delay_time.unwrap_or(0.25));
delay.feedback.gain().set_value(message.delay.feedback.unwrap_or(0.5));
delay.pre_gain.gain().set_value_at_time(message.delay.wet.unwrap_or(0.25), t + message.delay.delay_time.unwrap_or(0.5) as f64);
}
pub async fn download_file(ir_url: Url, ir_file_path: &String) {
let response = reqwest::get(ir_url)
.await
.unwrap_or_else(|_| panic!("Failed to send GET request"));
let bytes = response.bytes().await.unwrap();
let path = Path::new(&ir_file_path);
let mut file = create_file_and_dirs(path).await;
file.write_all(&bytes)
.await
.unwrap_or_else(|_| panic!("Failed to write to file"));
}
pub fn create_filepath_generic(url_string: &str, dirname: &str) -> (Url, String, String) {
let url = Url::parse(url_string).expect("failed to parse url");
let filename = url.path_segments()
.and_then(Iterator::last)
.and_then(|name| if name.is_empty() { None } else { Some(name) })
.unwrap_or("tmp.ben");
let file_path = format!("samples/{}{}", dirname, filename);
let file_path_clone = file_path.clone();
(url, file_path, file_path_clone)
}
pub async fn create_file_and_dirs(path: &Path) -> fs::File {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).await.unwrap();
}
let file = fs::File::create(path).await.unwrap();
file
}
+368
View File
@@ -0,0 +1,368 @@
use std::{
sync::Arc,
time::Duration,
fs::File,
path::Path,
time::Instant,
};
use std::collections::HashMap;
use mini_moka::sync::Cache;
use tokio::{
sync::{mpsc, Mutex},
io::AsyncWriteExt,
};
use serde::Deserialize;
use web_audio_api::{
context::BaseAudioContext,
AudioBuffer,
context::{AudioContext, AudioContextLatencyCategory, AudioContextOptions},
node::AudioNode,
};
use web_audio_api::context::OfflineAudioContext;
use crate::webaudio::*;
#[derive(Debug, Clone)]
pub struct WebAudioMessage {
pub note: f32,
pub instant: Instant,
pub offset: f64,
pub waveform: String,
pub lpf: LPFMessage,
pub hpf: HPFMessage,
pub bpf: BPFMessage,
pub duration: f64,
pub velocity: f32,
pub delay: DelayMessage,
pub reverb: ReverbMessage,
pub orbit: usize,
pub speed: f32,
pub begin: f64,
pub end: f64,
pub looper: LoopMessage,
pub adsr: ADSRMessage,
pub lpenv: FilterADSRMessage,
pub hpenv: FilterADSRMessage,
pub bpenv: FilterADSRMessage,
pub n: usize,
pub sampleurl: String,
pub dirname: String,
pub unit: Option<String>,
pub playbackrate: f32,
}
pub struct AsyncInputTransmitWebAudio {
pub inner: Mutex<mpsc::Sender<Vec<WebAudioMessage>>>,
}
pub fn init(
async_input_receiver: mpsc::Receiver<Vec<WebAudioMessage>>,
mut async_output_receiver: mpsc::Receiver<Vec<WebAudioMessage>>,
async_output_transmitter: mpsc::Sender<Vec<WebAudioMessage>>,
) {
tauri::async_runtime::spawn(async move { async_process_model(async_input_receiver, async_output_transmitter).await });
let (sender, receiver) = std::sync::mpsc::channel();
let message_queue: Arc<Mutex<Vec<WebAudioMessage>>> = Arc::new(Mutex::new(Vec::new()));
/* ...........................................................
Listen For incoming messages and add to queue
............................................................*/
let message_queue_clone = Arc::clone(&message_queue);
tauri::async_runtime::spawn(async move {
loop {
if let Some(package) = async_output_receiver.recv().await {
let mut message_queue = message_queue_clone.lock().await;
let messages = package;
for message in messages {
(*message_queue).push(message);
}
}
}
});
let message_queue_clone = Arc::clone(&message_queue);
tauri::async_runtime::spawn(async move {
/* ...........................................................
Process queued messages
............................................................*/
loop {
let mut message_queue = message_queue_clone.lock().await;
// Iterate over each message, play and remove messages when they are ready
message_queue.retain(|message| {
if message.instant.elapsed().as_millis() < message.offset as u128 {
return true;
};
match sender.send(message.clone()) {
Ok(_) => {}
Err(_) => {}
};
return false;
});
tokio::time::sleep(Duration::from_millis(1)).await;
}
});
/* ...........................................................
Create and setup WebAudio
............................................................*/
tauri::async_runtime::spawn(async move {
let latency_hint = match std::env::var("WEB_AUDIO_LATENCY").as_deref() {
Ok("playback") => AudioContextLatencyCategory::Playback,
_ => AudioContextLatencyCategory::default(),
};
let mut audio_context = AudioContext::new(AudioContextOptions {
latency_hint,
..AudioContextOptions::default()
});
let _ = audio_context.create_buffer_source();
let mut delays: HashMap<usize, Delay> = HashMap::new();
let mut reverbs: HashMap<usize, Reverb> = HashMap::new();
let compressor = audio_context.create_dynamics_compressor();
compressor.threshold().set_value(-50.0);
compressor.connect(&audio_context.destination());
let cache: Cache<String, AudioBuffer> = Cache::builder()
.max_capacity(31 * 1024 * 1024)
.time_to_idle(Duration::from_secs(40))
.build();
let ir_cache: Cache<String, AudioBuffer> = Cache::builder().max_capacity(31 * 1024 * 1024).build();
loop {
match receiver.recv() {
Ok(message) => {
match message.waveform.as_str() {
"sine" | "square" | "triangle" | "saw" | "sawtooth" => {
let t = audio_context.current_time();
let mut synth = Synth::new(&mut audio_context);
synth.set_frequency(&message.note);
synth.set_waveform(&message.waveform);
let filters = synth.set_filters(&mut audio_context, &message);
if message.delay.wet.is_some() {
apply_delay(&mut audio_context, &mut delays, &compressor, &message, t, &mut synth);
}
if message.reverb.room.is_some() && message.reverb.room.unwrap() > 0.0 {
apply_reverb(&audio_context, &compressor, &ir_cache, &message, &mut synth, &mut reverbs);
}
synth.envelope.connect(&compressor);
synth.play(t, &message, message.adsr.release.unwrap_or(0.001));
synth.set_adsr(t, &message.adsr, message.velocity, message.duration);
if is_filter_envelope_on(&message.lpenv) || is_filter_envelope_on(&message.hpenv) || is_filter_envelope_on(&message.bpenv) {
for f in &filters {
apply_filter_adsr(f, &message, &f.type_(), t);
}
}
}
_ => {
let t = audio_context.current_time();
// Create filepaths
let message_url = &message.sampleurl.clone();
let (url, file_path, file_path_clone) = create_filepath_generic(message_url, &message.dirname);
// Download file if it doesn't exist
tokio::spawn(async move {
if tokio::fs::metadata(&file_path.clone()).await.is_err() {
let response = reqwest::get(url)
.await
.unwrap_or_else(|_| panic!("Failed to send GET request"));
let bytes = response.bytes().await.unwrap();
let path = Path::new(&file_path);
let mut file = create_file_and_dirs(path).await;
file.write_all(&bytes)
.await
.unwrap_or_else(|_| panic!("Failed to write to file"));
}
});
// Play the buffer, if exists
if let Some(audio_buffer) = cache.get(&file_path_clone) {
let audio_buffer_duration = audio_buffer.duration();
let mut sampler = Sampler::new(&mut audio_context, audio_buffer);
// Playback speed
match message.unit {
Some(_) => {
sampler.sample.playback_rate().set_value(message.speed * audio_buffer_duration as f32 * 1.0);
}
_ => {
sampler.sample.playback_rate().set_value(message.speed * message.playbackrate);
}
}
// ADSR
sampler.set_adsr(t, &message.adsr, message.velocity, message.duration);
// Filters
let filters = sampler.set_filters(&mut audio_context, &message);
if is_filter_envelope_on(&message.lpenv) || is_filter_envelope_on(&message.hpenv) || is_filter_envelope_on(&message.bpenv) {
for f in &filters {
apply_filter_adsr(f, &message, &f.type_(), t);
}
}
// Delay
if message.delay.wet.is_some() {
apply_delay(&mut audio_context, &mut delays, &compressor, &message, t, &mut sampler);
};
// Reverb
if message.reverb.room.is_some() && message.reverb.room.unwrap() > 0.0 {
apply_reverb(&audio_context, &compressor, &ir_cache, &message, &mut sampler, &mut reverbs);
}
// Connect to output
sampler.envelope.connect(&compressor);
sampler.play(t, &message, message.adsr.release.unwrap_or(0.001));
// Decode file, if it exists
} else if let Ok(file) = File::open(&file_path_clone) {
let cache_clone = cache.clone();
tokio::spawn(async move {
let context = OfflineAudioContext::new(2, 88200, 44100.0);
let audio_buffer = context.decode_audio_data_sync(file).expect("Failed to decode file");
cache_clone.insert(file_path_clone.clone(), audio_buffer);
});
}
}
}
}
Err(_) => {}
}
}
});
}
pub async fn async_process_model(
mut input_receiver: mpsc::Receiver<Vec<WebAudioMessage>>,
output_transmitter: mpsc::Sender<Vec<WebAudioMessage>>,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
while let Some(input) = input_receiver.recv().await {
let output = input;
output_transmitter.send(output).await?;
}
Ok(())
}
#[derive(Deserialize)]
pub struct MessageFromJS {
note: f32,
offset: f64,
waveform: String,
lpf: (f32, f32),
hpf: (f32, f32),
bpf: (f32, f32),
duration: f64,
velocity: f32,
delay: (Option<f32>, Option<f32>, Option<f32>),
reverb: (Option<f32>, Option<f32>, Option<f32>, Option<f32>, Option<f32>, Option<String>, Option<String>),
orbit: usize,
speed: f32,
begin: f64,
end: f64,
looper: (u8, f64, f64),
adsr: (Option<f64>, Option<f64>, Option<f32>, Option<f64>),
lpenv: (Option<f64>, Option<f64>, Option<f64>, Option<f64>, Option<f64>),
hpenv: (Option<f64>, Option<f64>, Option<f64>, Option<f64>, Option<f64>),
bpenv: (Option<f64>, Option<f64>, Option<f64>, Option<f64>, Option<f64>),
n: usize,
sampleurl: String,
dirname: String,
unit: Option<String>,
playbackrate: f32,
}
// Called from JS
#[tauri::command]
pub async fn sendwebaudio(
messagesfromjs: Vec<MessageFromJS>,
state: tauri::State<'_, AsyncInputTransmitWebAudio>,
) -> Result<(), String> {
let async_proc_input_tx = state.inner.lock().await;
let mut messages_to_process: Vec<WebAudioMessage> = Vec::new();
for m in messagesfromjs {
let message_to_process = WebAudioMessage {
note: m.note,
instant: Instant::now(),
offset: m.offset,
waveform: m.waveform,
lpf: LPFMessage {
frequency: m.lpf.0,
resonance: m.lpf.1,
},
hpf: HPFMessage {
frequency: m.hpf.0,
resonance: m.hpf.1,
},
bpf: BPFMessage {
frequency: m.bpf.0,
resonance: m.bpf.1,
},
duration: m.duration,
velocity: m.velocity,
delay: DelayMessage {
wet: m.delay.0,
delay_time: m.delay.1,
feedback: m.delay.2,
},
reverb: ReverbMessage {
room: m.reverb.0,
size: m.reverb.1,
roomfade: m.reverb.2,
roomlp: m.reverb.3,
roomdim: m.reverb.4,
ir: m.reverb.5,
url: m.reverb.6,
},
orbit: m.orbit,
speed: m.speed,
begin: m.begin,
end: m.end,
looper: LoopMessage {
is_loop: m.looper.0,
loop_start: m.looper.1,
loop_end: m.looper.2,
},
adsr: ADSRMessage {
attack: m.adsr.0,
decay: m.adsr.1,
sustain: m.adsr.2,
release: m.adsr.3,
},
lpenv: FilterADSRMessage {
attack: m.lpenv.0,
decay: m.lpenv.1,
sustain: m.lpenv.2,
release: m.lpenv.3,
env: m.lpenv.4,
},
hpenv: FilterADSRMessage {
attack: m.hpenv.0,
decay: m.hpenv.1,
sustain: m.hpenv.2,
release: m.hpenv.3,
env: m.hpenv.4,
},
bpenv: FilterADSRMessage {
attack: m.bpenv.0,
decay: m.bpenv.1,
sustain: m.bpenv.2,
release: m.bpenv.3,
env: m.bpenv.4,
},
n: m.n,
sampleurl: m.sampleurl,
dirname: m.dirname,
unit: m.unit,
playbackrate: m.playbackrate,
};
messages_to_process.push(message_to_process);
}
async_proc_input_tx.send(messages_to_process).await.map_err(|e| e.to_string())
}
+1 -7
View File
@@ -3,7 +3,7 @@
"build": {
"beforeBuildCommand": "npm run build",
"beforeDevCommand": "npm run dev",
"devPath": "http://localhost:4321",
"devPath": "http://localhost:3000",
"distDir": "../website/dist",
"withGlobalTauri": true
},
@@ -13,12 +13,6 @@
},
"tauri": {
"allowlist": {
"dialog": {
"all": true
},
"clipboard": {
"writeText": true
},
"all": false,
"fs": {
"all": true,
+109 -681
View File
@@ -633,15 +633,6 @@ exports[`runs examples > example "addVoicings" example index 0 1`] = `
]
`;
exports[`runs examples > example "adsr" example index 0 1`] = `
[
"[ 0/1 → 1/1 | note:c3 s:sawtooth cutoff:600 ]",
"[ 1/1 → 2/1 | note:bb2 s:sawtooth cutoff:600 ]",
"[ 2/1 → 3/1 | note:f3 s:sawtooth cutoff:600 ]",
"[ 3/1 → 4/1 | note:eb3 s:sawtooth cutoff:600 ]",
]
`;
exports[`runs examples > example "almostAlways" example index 0 1`] = `
[
"[ 0/1 → 1/8 | s:hh speed:0.5 ]",
@@ -1080,31 +1071,6 @@ exports[`runs examples > example "chooseCycles" example index 1 1`] = `
]
`;
exports[`runs examples > example "chooseWith" example index 0 1`] = `
[
"[ 0/1 → 1/5 | note:c2 s:bd n:6 ]",
"[ 1/5 → 2/5 | note:g2 s:sawtooth ]",
"[ 2/5 → 3/5 | note:g2 s:triangle ]",
"[ 3/5 → 4/5 | note:d2 s:bd n:6 ]",
"[ 4/5 → 1/1 | note:f1 s:sawtooth ]",
"[ 1/1 → 6/5 | note:c2 s:bd n:6 ]",
"[ 6/5 → 7/5 | note:g2 s:sawtooth ]",
"[ 7/5 → 8/5 | note:g2 s:triangle ]",
"[ 8/5 → 9/5 | note:d2 s:bd n:6 ]",
"[ 9/5 → 2/1 | note:f1 s:sawtooth ]",
"[ 2/1 → 11/5 | note:c2 s:bd n:6 ]",
"[ 11/5 → 12/5 | note:g2 s:sawtooth ]",
"[ 12/5 → 13/5 | note:g2 s:triangle ]",
"[ 13/5 → 14/5 | note:d2 s:bd n:6 ]",
"[ 14/5 → 3/1 | note:f1 s:sawtooth ]",
"[ 3/1 → 16/5 | note:c2 s:bd n:6 ]",
"[ 16/5 → 17/5 | note:g2 s:sawtooth ]",
"[ 17/5 → 18/5 | note:g2 s:triangle ]",
"[ 18/5 → 19/5 | note:d2 s:bd n:6 ]",
"[ 19/5 → 4/1 | note:f1 s:sawtooth ]",
]
`;
exports[`runs examples > example "chop" example index 0 1`] = `
[
"[ 0/1 → 1/1 | s:rhodes begin:0.75 end:1 speed:0.25 unit:c ]",
@@ -1115,27 +1081,6 @@ exports[`runs examples > example "chop" example index 0 1`] = `
`;
exports[`runs examples > example "chunk" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:A4 ]",
"[ 1/4 → 1/2 | note:B3 ]",
"[ 1/2 → 3/4 | note:C4 ]",
"[ 3/4 → 1/1 | note:D4 ]",
"[ 1/1 → 5/4 | note:A3 ]",
"[ 5/4 → 3/2 | note:B4 ]",
"[ 3/2 → 7/4 | note:C4 ]",
"[ 7/4 → 2/1 | note:D4 ]",
"[ 2/1 → 9/4 | note:A3 ]",
"[ 9/4 → 5/2 | note:B3 ]",
"[ 5/2 → 11/4 | note:C5 ]",
"[ 11/4 → 3/1 | note:D4 ]",
"[ 3/1 → 13/4 | note:A3 ]",
"[ 13/4 → 7/2 | note:B3 ]",
"[ 7/2 → 15/4 | note:C4 ]",
"[ 15/4 → 4/1 | note:D5 ]",
]
`;
exports[`runs examples > example "chunkBack" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:A4 ]",
"[ 1/4 → 1/2 | note:B3 ]",
@@ -1156,6 +1101,27 @@ exports[`runs examples > example "chunkBack" example index 0 1`] = `
]
`;
exports[`runs examples > example "chunkBack" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:A4 ]",
"[ 1/4 → 1/2 | note:B3 ]",
"[ 1/2 → 3/4 | note:C4 ]",
"[ 3/4 → 1/1 | note:D4 ]",
"[ 1/1 → 5/4 | note:A3 ]",
"[ 5/4 → 3/2 | note:B4 ]",
"[ 3/2 → 7/4 | note:C4 ]",
"[ 7/4 → 2/1 | note:D4 ]",
"[ 2/1 → 9/4 | note:A3 ]",
"[ 9/4 → 5/2 | note:B3 ]",
"[ 5/2 → 11/4 | note:C5 ]",
"[ 11/4 → 3/1 | note:D4 ]",
"[ 3/1 → 13/4 | note:A3 ]",
"[ 13/4 → 7/2 | note:B3 ]",
"[ 7/2 → 15/4 | note:C4 ]",
"[ 15/4 → 4/1 | note:D5 ]",
]
`;
exports[`runs examples > example "clip" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:c s:piano clip:0.5 ]",
@@ -1219,35 +1185,6 @@ exports[`runs examples > example "compress" example index 0 1`] = `
]
`;
exports[`runs examples > example "compressor" example index 0 1`] = `
[
"[ 0/1 → 1/4 | s:hh compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 ]",
"[ 0/1 → 1/2 | s:bd compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 ]",
"[ 1/4 → 1/2 | s:hh compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 ]",
"[ 1/2 → 3/4 | s:hh compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 ]",
"[ 1/2 → 1/1 | s:sd compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 ]",
"[ 3/4 → 1/1 | s:hh compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 ]",
"[ 1/1 → 5/4 | s:hh compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 ]",
"[ 1/1 → 3/2 | s:bd compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 ]",
"[ 5/4 → 3/2 | s:hh compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 ]",
"[ 3/2 → 7/4 | s:hh compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 ]",
"[ 3/2 → 2/1 | s:sd compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 ]",
"[ 7/4 → 2/1 | s:hh compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 ]",
"[ 2/1 → 9/4 | s:hh compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 ]",
"[ 2/1 → 5/2 | s:bd compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 ]",
"[ 9/4 → 5/2 | s:hh compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 ]",
"[ 5/2 → 11/4 | s:hh compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 ]",
"[ 5/2 → 3/1 | s:sd compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 ]",
"[ 11/4 → 3/1 | s:hh compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 ]",
"[ 3/1 → 13/4 | s:hh compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 ]",
"[ 3/1 → 7/2 | s:bd compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 ]",
"[ 13/4 → 7/2 | s:hh compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 ]",
"[ 7/2 → 15/4 | s:hh compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 ]",
"[ 7/2 → 4/1 | s:sd compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 ]",
"[ 15/4 → 4/1 | s:hh compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 ]",
]
`;
exports[`runs examples > example "cosine" example index 0 1`] = `
[
"[ 0/1 → 1/8 | note:Eb4 ]",
@@ -1857,19 +1794,6 @@ exports[`runs examples > example "fast" example index 0 1`] = `
]
`;
exports[`runs examples > example "fastChunk" example index 0 1`] = `
[
"[ 0/1 → 1/2 | note:C2 s:folkharp ]",
"[ 1/2 → 1/1 | note:D2 s:folkharp ]",
"[ 1/1 → 3/2 | note:E2 s:folkharp ]",
"[ 3/2 → 2/1 | note:F2 s:folkharp ]",
"[ 2/1 → 5/2 | note:G2 s:folkharp ]",
"[ 5/2 → 3/1 | note:A2 s:folkharp ]",
"[ 3/1 → 7/2 | note:B2 s:folkharp ]",
"[ 7/2 → 4/1 | note:C3 s:folkharp ]",
]
`;
exports[`runs examples > example "fastGap" example index 0 1`] = `
[
"[ 0/1 → 1/4 | s:bd ]",
@@ -2143,15 +2067,6 @@ exports[`runs examples > example "freq" example index 1 1`] = `
]
`;
exports[`runs examples > example "fscope" example index 0 1`] = `
[
"[ 0/1 → 1/1 | s:sawtooth analyze:1 ]",
"[ 1/1 → 2/1 | s:sawtooth analyze:1 ]",
"[ 2/1 → 3/1 | s:sawtooth analyze:1 ]",
"[ 3/1 → 4/1 | s:sawtooth analyze:1 ]",
]
`;
exports[`runs examples > example "ftype" example index 0 1`] = `
[
"[ 0/1 → 1/1 | note:c2 s:sawtooth cutoff:500 bpenv:4 ftype:12db ]",
@@ -2439,19 +2354,6 @@ exports[`runs examples > example "irand" example index 0 1`] = `
]
`;
exports[`runs examples > example "iresponse" example index 0 1`] = `
[
"[ 0/1 → 1/2 | s:bd room:0.8 ir:shaker_large i:0 ]",
"[ 1/2 → 1/1 | s:sd room:0.8 ir:shaker_large i:0 ]",
"[ 1/1 → 3/2 | s:bd room:0.8 ir:shaker_large i:2 ]",
"[ 3/2 → 2/1 | s:sd room:0.8 ir:shaker_large i:2 ]",
"[ 2/1 → 5/2 | s:bd room:0.8 ir:shaker_large i:0 ]",
"[ 5/2 → 3/1 | s:sd room:0.8 ir:shaker_large i:0 ]",
"[ 3/1 → 7/2 | s:bd room:0.8 ir:shaker_large i:2 ]",
"[ 7/2 → 4/1 | s:sd room:0.8 ir:shaker_large i:2 ]",
]
`;
exports[`runs examples > example "iter" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:A3 ]",
@@ -3057,15 +2959,6 @@ exports[`runs examples > example "never" example index 0 1`] = `
]
`;
exports[`runs examples > example "noise" example index 0 1`] = `
[
"[ (0/1 → 1/1) ⇝ 2/1 | s:white ]",
"[ 0/1 ⇜ (1/1 → 2/1) | s:white ]",
"[ (2/1 → 3/1) ⇝ 4/1 | s:pink ]",
"[ 2/1 ⇜ (3/1 → 4/1) | s:pink ]",
]
`;
exports[`runs examples > example "note" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:c ]",
@@ -3319,204 +3212,6 @@ exports[`runs examples > example "perlin" example index 0 1`] = `
]
`;
exports[`runs examples > example "phaser" example index 0 1`] = `
[
"[ 0/1 → 1/8 | note:D3 s:sawtooth release:0.5 phaser:1 ]",
"[ 1/8 → 1/4 | note:E3 s:sawtooth release:0.5 phaser:1 ]",
"[ 1/4 → 3/8 | note:F#3 s:sawtooth release:0.5 phaser:1 ]",
"[ 3/8 → 1/2 | note:A3 s:sawtooth release:0.5 phaser:1 ]",
"[ 1/2 → 5/8 | note:B3 s:sawtooth release:0.5 phaser:1 ]",
"[ 5/8 → 3/4 | note:D4 s:sawtooth release:0.5 phaser:1 ]",
"[ 3/4 → 7/8 | note:E4 s:sawtooth release:0.5 phaser:1 ]",
"[ 7/8 → 1/1 | note:F#4 s:sawtooth release:0.5 phaser:1 ]",
"[ 1/1 → 9/8 | note:D3 s:sawtooth release:0.5 phaser:2 ]",
"[ 9/8 → 5/4 | note:E3 s:sawtooth release:0.5 phaser:2 ]",
"[ 5/4 → 11/8 | note:F#3 s:sawtooth release:0.5 phaser:2 ]",
"[ 11/8 → 3/2 | note:A3 s:sawtooth release:0.5 phaser:2 ]",
"[ 3/2 → 13/8 | note:B3 s:sawtooth release:0.5 phaser:2 ]",
"[ 13/8 → 7/4 | note:D4 s:sawtooth release:0.5 phaser:2 ]",
"[ 7/4 → 15/8 | note:E4 s:sawtooth release:0.5 phaser:2 ]",
"[ 15/8 → 2/1 | note:F#4 s:sawtooth release:0.5 phaser:2 ]",
"[ 2/1 → 17/8 | note:D3 s:sawtooth release:0.5 phaser:4 ]",
"[ 17/8 → 9/4 | note:E3 s:sawtooth release:0.5 phaser:4 ]",
"[ 9/4 → 19/8 | note:F#3 s:sawtooth release:0.5 phaser:4 ]",
"[ 19/8 → 5/2 | note:A3 s:sawtooth release:0.5 phaser:4 ]",
"[ 5/2 → 21/8 | note:B3 s:sawtooth release:0.5 phaser:4 ]",
"[ 21/8 → 11/4 | note:D4 s:sawtooth release:0.5 phaser:4 ]",
"[ 11/4 → 23/8 | note:E4 s:sawtooth release:0.5 phaser:4 ]",
"[ 23/8 → 3/1 | note:F#4 s:sawtooth release:0.5 phaser:4 ]",
"[ 3/1 → 25/8 | note:D3 s:sawtooth release:0.5 phaser:8 ]",
"[ 25/8 → 13/4 | note:E3 s:sawtooth release:0.5 phaser:8 ]",
"[ 13/4 → 27/8 | note:F#3 s:sawtooth release:0.5 phaser:8 ]",
"[ 27/8 → 7/2 | note:A3 s:sawtooth release:0.5 phaser:8 ]",
"[ 7/2 → 29/8 | note:B3 s:sawtooth release:0.5 phaser:8 ]",
"[ 29/8 → 15/4 | note:D4 s:sawtooth release:0.5 phaser:8 ]",
"[ 15/4 → 31/8 | note:E4 s:sawtooth release:0.5 phaser:8 ]",
"[ 31/8 → 4/1 | note:F#4 s:sawtooth release:0.5 phaser:8 ]",
]
`;
exports[`runs examples > example "phasercenter" example index 0 1`] = `
[
"[ 0/1 → 1/8 | note:D3 s:sawtooth release:0.5 phaser:2 phasercenter:800 ]",
"[ 1/8 → 1/4 | note:E3 s:sawtooth release:0.5 phaser:2 phasercenter:800 ]",
"[ 1/4 → 3/8 | note:F#3 s:sawtooth release:0.5 phaser:2 phasercenter:800 ]",
"[ 3/8 → 1/2 | note:A3 s:sawtooth release:0.5 phaser:2 phasercenter:800 ]",
"[ 1/2 → 5/8 | note:B3 s:sawtooth release:0.5 phaser:2 phasercenter:800 ]",
"[ 5/8 → 3/4 | note:D4 s:sawtooth release:0.5 phaser:2 phasercenter:800 ]",
"[ 3/4 → 7/8 | note:E4 s:sawtooth release:0.5 phaser:2 phasercenter:800 ]",
"[ 7/8 → 1/1 | note:F#4 s:sawtooth release:0.5 phaser:2 phasercenter:800 ]",
"[ 1/1 → 9/8 | note:D3 s:sawtooth release:0.5 phaser:2 phasercenter:2000 ]",
"[ 9/8 → 5/4 | note:E3 s:sawtooth release:0.5 phaser:2 phasercenter:2000 ]",
"[ 5/4 → 11/8 | note:F#3 s:sawtooth release:0.5 phaser:2 phasercenter:2000 ]",
"[ 11/8 → 3/2 | note:A3 s:sawtooth release:0.5 phaser:2 phasercenter:2000 ]",
"[ 3/2 → 13/8 | note:B3 s:sawtooth release:0.5 phaser:2 phasercenter:2000 ]",
"[ 13/8 → 7/4 | note:D4 s:sawtooth release:0.5 phaser:2 phasercenter:2000 ]",
"[ 7/4 → 15/8 | note:E4 s:sawtooth release:0.5 phaser:2 phasercenter:2000 ]",
"[ 15/8 → 2/1 | note:F#4 s:sawtooth release:0.5 phaser:2 phasercenter:2000 ]",
"[ 2/1 → 17/8 | note:D3 s:sawtooth release:0.5 phaser:2 phasercenter:4000 ]",
"[ 17/8 → 9/4 | note:E3 s:sawtooth release:0.5 phaser:2 phasercenter:4000 ]",
"[ 9/4 → 19/8 | note:F#3 s:sawtooth release:0.5 phaser:2 phasercenter:4000 ]",
"[ 19/8 → 5/2 | note:A3 s:sawtooth release:0.5 phaser:2 phasercenter:4000 ]",
"[ 5/2 → 21/8 | note:B3 s:sawtooth release:0.5 phaser:2 phasercenter:4000 ]",
"[ 21/8 → 11/4 | note:D4 s:sawtooth release:0.5 phaser:2 phasercenter:4000 ]",
"[ 11/4 → 23/8 | note:E4 s:sawtooth release:0.5 phaser:2 phasercenter:4000 ]",
"[ 23/8 → 3/1 | note:F#4 s:sawtooth release:0.5 phaser:2 phasercenter:4000 ]",
"[ 3/1 → 25/8 | note:D3 s:sawtooth release:0.5 phaser:2 phasercenter:800 ]",
"[ 25/8 → 13/4 | note:E3 s:sawtooth release:0.5 phaser:2 phasercenter:800 ]",
"[ 13/4 → 27/8 | note:F#3 s:sawtooth release:0.5 phaser:2 phasercenter:800 ]",
"[ 27/8 → 7/2 | note:A3 s:sawtooth release:0.5 phaser:2 phasercenter:800 ]",
"[ 7/2 → 29/8 | note:B3 s:sawtooth release:0.5 phaser:2 phasercenter:800 ]",
"[ 29/8 → 15/4 | note:D4 s:sawtooth release:0.5 phaser:2 phasercenter:800 ]",
"[ 15/4 → 31/8 | note:E4 s:sawtooth release:0.5 phaser:2 phasercenter:800 ]",
"[ 31/8 → 4/1 | note:F#4 s:sawtooth release:0.5 phaser:2 phasercenter:800 ]",
]
`;
exports[`runs examples > example "phaserdepth" example index 0 1`] = `
[
"[ 0/1 → 1/8 | note:D3 s:sawtooth release:0.5 phaser:2 phaserdepth:0 ]",
"[ 1/8 → 1/4 | note:E3 s:sawtooth release:0.5 phaser:2 phaserdepth:0 ]",
"[ 1/4 → 3/8 | note:F#3 s:sawtooth release:0.5 phaser:2 phaserdepth:0 ]",
"[ 3/8 → 1/2 | note:A3 s:sawtooth release:0.5 phaser:2 phaserdepth:0 ]",
"[ 1/2 → 5/8 | note:B3 s:sawtooth release:0.5 phaser:2 phaserdepth:0 ]",
"[ 5/8 → 3/4 | note:D4 s:sawtooth release:0.5 phaser:2 phaserdepth:0 ]",
"[ 3/4 → 7/8 | note:E4 s:sawtooth release:0.5 phaser:2 phaserdepth:0 ]",
"[ 7/8 → 1/1 | note:F#4 s:sawtooth release:0.5 phaser:2 phaserdepth:0 ]",
"[ 1/1 → 9/8 | note:D3 s:sawtooth release:0.5 phaser:2 phaserdepth:0.5 ]",
"[ 9/8 → 5/4 | note:E3 s:sawtooth release:0.5 phaser:2 phaserdepth:0.5 ]",
"[ 5/4 → 11/8 | note:F#3 s:sawtooth release:0.5 phaser:2 phaserdepth:0.5 ]",
"[ 11/8 → 3/2 | note:A3 s:sawtooth release:0.5 phaser:2 phaserdepth:0.5 ]",
"[ 3/2 → 13/8 | note:B3 s:sawtooth release:0.5 phaser:2 phaserdepth:0.5 ]",
"[ 13/8 → 7/4 | note:D4 s:sawtooth release:0.5 phaser:2 phaserdepth:0.5 ]",
"[ 7/4 → 15/8 | note:E4 s:sawtooth release:0.5 phaser:2 phaserdepth:0.5 ]",
"[ 15/8 → 2/1 | note:F#4 s:sawtooth release:0.5 phaser:2 phaserdepth:0.5 ]",
"[ 2/1 → 17/8 | note:D3 s:sawtooth release:0.5 phaser:2 phaserdepth:0.75 ]",
"[ 17/8 → 9/4 | note:E3 s:sawtooth release:0.5 phaser:2 phaserdepth:0.75 ]",
"[ 9/4 → 19/8 | note:F#3 s:sawtooth release:0.5 phaser:2 phaserdepth:0.75 ]",
"[ 19/8 → 5/2 | note:A3 s:sawtooth release:0.5 phaser:2 phaserdepth:0.75 ]",
"[ 5/2 → 21/8 | note:B3 s:sawtooth release:0.5 phaser:2 phaserdepth:0.75 ]",
"[ 21/8 → 11/4 | note:D4 s:sawtooth release:0.5 phaser:2 phaserdepth:0.75 ]",
"[ 11/4 → 23/8 | note:E4 s:sawtooth release:0.5 phaser:2 phaserdepth:0.75 ]",
"[ 23/8 → 3/1 | note:F#4 s:sawtooth release:0.5 phaser:2 phaserdepth:0.75 ]",
"[ 3/1 → 25/8 | note:D3 s:sawtooth release:0.5 phaser:2 phaserdepth:1 ]",
"[ 25/8 → 13/4 | note:E3 s:sawtooth release:0.5 phaser:2 phaserdepth:1 ]",
"[ 13/4 → 27/8 | note:F#3 s:sawtooth release:0.5 phaser:2 phaserdepth:1 ]",
"[ 27/8 → 7/2 | note:A3 s:sawtooth release:0.5 phaser:2 phaserdepth:1 ]",
"[ 7/2 → 29/8 | note:B3 s:sawtooth release:0.5 phaser:2 phaserdepth:1 ]",
"[ 29/8 → 15/4 | note:D4 s:sawtooth release:0.5 phaser:2 phaserdepth:1 ]",
"[ 15/4 → 31/8 | note:E4 s:sawtooth release:0.5 phaser:2 phaserdepth:1 ]",
"[ 31/8 → 4/1 | note:F#4 s:sawtooth release:0.5 phaser:2 phaserdepth:1 ]",
]
`;
exports[`runs examples > example "phasersweep" example index 0 1`] = `
[
"[ 0/1 → 1/8 | note:D3 s:sawtooth release:0.5 phaser:2 phasersweep:800 ]",
"[ 1/8 → 1/4 | note:E3 s:sawtooth release:0.5 phaser:2 phasersweep:800 ]",
"[ 1/4 → 3/8 | note:F#3 s:sawtooth release:0.5 phaser:2 phasersweep:800 ]",
"[ 3/8 → 1/2 | note:A3 s:sawtooth release:0.5 phaser:2 phasersweep:800 ]",
"[ 1/2 → 5/8 | note:B3 s:sawtooth release:0.5 phaser:2 phasersweep:800 ]",
"[ 5/8 → 3/4 | note:D4 s:sawtooth release:0.5 phaser:2 phasersweep:800 ]",
"[ 3/4 → 7/8 | note:E4 s:sawtooth release:0.5 phaser:2 phasersweep:800 ]",
"[ 7/8 → 1/1 | note:F#4 s:sawtooth release:0.5 phaser:2 phasersweep:800 ]",
"[ 1/1 → 9/8 | note:D3 s:sawtooth release:0.5 phaser:2 phasersweep:2000 ]",
"[ 9/8 → 5/4 | note:E3 s:sawtooth release:0.5 phaser:2 phasersweep:2000 ]",
"[ 5/4 → 11/8 | note:F#3 s:sawtooth release:0.5 phaser:2 phasersweep:2000 ]",
"[ 11/8 → 3/2 | note:A3 s:sawtooth release:0.5 phaser:2 phasersweep:2000 ]",
"[ 3/2 → 13/8 | note:B3 s:sawtooth release:0.5 phaser:2 phasersweep:2000 ]",
"[ 13/8 → 7/4 | note:D4 s:sawtooth release:0.5 phaser:2 phasersweep:2000 ]",
"[ 7/4 → 15/8 | note:E4 s:sawtooth release:0.5 phaser:2 phasersweep:2000 ]",
"[ 15/8 → 2/1 | note:F#4 s:sawtooth release:0.5 phaser:2 phasersweep:2000 ]",
"[ 2/1 → 17/8 | note:D3 s:sawtooth release:0.5 phaser:2 phasersweep:4000 ]",
"[ 17/8 → 9/4 | note:E3 s:sawtooth release:0.5 phaser:2 phasersweep:4000 ]",
"[ 9/4 → 19/8 | note:F#3 s:sawtooth release:0.5 phaser:2 phasersweep:4000 ]",
"[ 19/8 → 5/2 | note:A3 s:sawtooth release:0.5 phaser:2 phasersweep:4000 ]",
"[ 5/2 → 21/8 | note:B3 s:sawtooth release:0.5 phaser:2 phasersweep:4000 ]",
"[ 21/8 → 11/4 | note:D4 s:sawtooth release:0.5 phaser:2 phasersweep:4000 ]",
"[ 11/4 → 23/8 | note:E4 s:sawtooth release:0.5 phaser:2 phasersweep:4000 ]",
"[ 23/8 → 3/1 | note:F#4 s:sawtooth release:0.5 phaser:2 phasersweep:4000 ]",
"[ 3/1 → 25/8 | note:D3 s:sawtooth release:0.5 phaser:2 phasersweep:800 ]",
"[ 25/8 → 13/4 | note:E3 s:sawtooth release:0.5 phaser:2 phasersweep:800 ]",
"[ 13/4 → 27/8 | note:F#3 s:sawtooth release:0.5 phaser:2 phasersweep:800 ]",
"[ 27/8 → 7/2 | note:A3 s:sawtooth release:0.5 phaser:2 phasersweep:800 ]",
"[ 7/2 → 29/8 | note:B3 s:sawtooth release:0.5 phaser:2 phasersweep:800 ]",
"[ 29/8 → 15/4 | note:D4 s:sawtooth release:0.5 phaser:2 phasersweep:800 ]",
"[ 15/4 → 31/8 | note:E4 s:sawtooth release:0.5 phaser:2 phasersweep:800 ]",
"[ 31/8 → 4/1 | note:F#4 s:sawtooth release:0.5 phaser:2 phasersweep:800 ]",
]
`;
exports[`runs examples > example "pianoroll" example index 0 1`] = `
[
"[ 0/1 → 1/8 | note:C2 s:piano clip:1 ]",
"[ (1/4 → 1/3) ⇝ 3/8 | note:C2 s:piano clip:1 ]",
"[ 1/4 ⇜ (1/3 → 3/8) | note:A2 s:piano clip:1 ]",
"[ 3/8 → 1/2 | note:A2 s:piano clip:1 ]",
"[ (5/8 → 2/3) ⇝ 3/4 | note:A2 s:piano clip:1 ]",
"[ 5/8 ⇜ (2/3 → 3/4) | note:G2 s:piano clip:1 ]",
"[ 3/4 → 7/8 | note:G2 s:piano clip:1 ]",
"[ 1/1 → 9/8 | note:C2 s:piano clip:1 ]",
"[ (5/4 → 4/3) ⇝ 11/8 | note:C2 s:piano clip:1 ]",
"[ 5/4 ⇜ (4/3 → 11/8) | note:A2 s:piano clip:1 ]",
"[ 11/8 → 3/2 | note:A2 s:piano clip:1 ]",
"[ (13/8 → 5/3) ⇝ 7/4 | note:A2 s:piano clip:1 ]",
"[ 13/8 ⇜ (5/3 → 7/4) | note:G2 s:piano clip:1 ]",
"[ 7/4 → 15/8 | note:G2 s:piano clip:1 ]",
"[ 2/1 → 17/8 | note:C2 s:piano clip:1 ]",
"[ (9/4 → 7/3) ⇝ 19/8 | note:C2 s:piano clip:1 ]",
"[ 9/4 ⇜ (7/3 → 19/8) | note:A2 s:piano clip:1 ]",
"[ 19/8 → 5/2 | note:A2 s:piano clip:1 ]",
"[ (21/8 → 8/3) ⇝ 11/4 | note:A2 s:piano clip:1 ]",
"[ 21/8 ⇜ (8/3 → 11/4) | note:G2 s:piano clip:1 ]",
"[ 11/4 → 23/8 | note:G2 s:piano clip:1 ]",
"[ 3/1 → 25/8 | note:C2 s:piano clip:1 ]",
"[ (13/4 → 10/3) ⇝ 27/8 | note:C2 s:piano clip:1 ]",
"[ 13/4 ⇜ (10/3 → 27/8) | note:A2 s:piano clip:1 ]",
"[ 27/8 → 7/2 | note:A2 s:piano clip:1 ]",
"[ (29/8 → 11/3) ⇝ 15/4 | note:A2 s:piano clip:1 ]",
"[ 29/8 ⇜ (11/3 → 15/4) | note:G2 s:piano clip:1 ]",
"[ 15/4 → 31/8 | note:G2 s:piano clip:1 ]",
]
`;
exports[`runs examples > example "pick" example index 0 1`] = `
[
"[ 0/1 → 1/2 | note:g ]",
"[ 1/2 → 1/1 | note:a ]",
"[ 1/1 → 3/2 | note:e ]",
"[ 3/2 → 2/1 | note:f ]",
"[ 2/1 → 9/4 | note:f ]",
"[ 9/4 → 5/2 | note:g ]",
"[ 5/2 → 11/4 | note:f ]",
"[ 11/4 → 3/1 | note:g ]",
"[ 3/1 → 13/4 | note:g ]",
"[ 13/4 → 7/2 | note:a ]",
"[ 7/2 → 15/4 | note:c ]",
"[ 15/4 → 4/1 | note:d ]",
]
`;
exports[`runs examples > example "ply" example index 0 1`] = `
[
"[ 0/1 → 1/4 | s:bd ]",
@@ -3589,35 +3284,6 @@ exports[`runs examples > example "polymeterSteps" example index 0 1`] = `
]
`;
exports[`runs examples > example "postgain" example index 0 1`] = `
[
"[ 0/1 → 1/4 | s:hh compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 postgain:1.5 ]",
"[ 0/1 → 1/2 | s:bd compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 postgain:1.5 ]",
"[ 1/4 → 1/2 | s:hh compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 postgain:1.5 ]",
"[ 1/2 → 3/4 | s:hh compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 postgain:1.5 ]",
"[ 1/2 → 1/1 | s:sd compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 postgain:1.5 ]",
"[ 3/4 → 1/1 | s:hh compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 postgain:1.5 ]",
"[ 1/1 → 5/4 | s:hh compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 postgain:1.5 ]",
"[ 1/1 → 3/2 | s:bd compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 postgain:1.5 ]",
"[ 5/4 → 3/2 | s:hh compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 postgain:1.5 ]",
"[ 3/2 → 7/4 | s:hh compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 postgain:1.5 ]",
"[ 3/2 → 2/1 | s:sd compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 postgain:1.5 ]",
"[ 7/4 → 2/1 | s:hh compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 postgain:1.5 ]",
"[ 2/1 → 9/4 | s:hh compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 postgain:1.5 ]",
"[ 2/1 → 5/2 | s:bd compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 postgain:1.5 ]",
"[ 9/4 → 5/2 | s:hh compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 postgain:1.5 ]",
"[ 5/2 → 11/4 | s:hh compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 postgain:1.5 ]",
"[ 5/2 → 3/1 | s:sd compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 postgain:1.5 ]",
"[ 11/4 → 3/1 | s:hh compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 postgain:1.5 ]",
"[ 3/1 → 13/4 | s:hh compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 postgain:1.5 ]",
"[ 3/1 → 7/2 | s:bd compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 postgain:1.5 ]",
"[ 13/4 → 7/2 | s:hh compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 postgain:1.5 ]",
"[ 7/2 → 15/4 | s:hh compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 postgain:1.5 ]",
"[ 7/2 → 4/1 | s:sd compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 postgain:1.5 ]",
"[ 15/4 → 4/1 | s:hh compressor:-20 compressorRatio:20 compressorKnee:10 compressorAttack:0.002 compressorRelease:0.02 postgain:1.5 ]",
]
`;
exports[`runs examples > example "press" example index 0 1`] = `
[
"[ 0/1 → 1/2 | s:hh ]",
@@ -3862,27 +3528,6 @@ exports[`runs examples > example "release" example index 0 1`] = `
]
`;
exports[`runs examples > example "repeatCycles" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:42 s:gm_acoustic_guitar_nylon ]",
"[ 1/4 → 1/2 | note:38 s:gm_acoustic_guitar_nylon ]",
"[ 1/2 → 3/4 | note:35 s:gm_acoustic_guitar_nylon ]",
"[ 3/4 → 1/1 | note:38 s:gm_acoustic_guitar_nylon ]",
"[ 1/1 → 5/4 | note:42 s:gm_acoustic_guitar_nylon ]",
"[ 5/4 → 3/2 | note:38 s:gm_acoustic_guitar_nylon ]",
"[ 3/2 → 7/4 | note:35 s:gm_acoustic_guitar_nylon ]",
"[ 7/4 → 2/1 | note:38 s:gm_acoustic_guitar_nylon ]",
"[ 2/1 → 9/4 | note:42 s:gm_acoustic_guitar_nylon ]",
"[ 9/4 → 5/2 | note:36 s:gm_acoustic_guitar_nylon ]",
"[ 5/2 → 11/4 | note:39 s:gm_acoustic_guitar_nylon ]",
"[ 11/4 → 3/1 | note:41 s:gm_acoustic_guitar_nylon ]",
"[ 3/1 → 13/4 | note:42 s:gm_acoustic_guitar_nylon ]",
"[ 13/4 → 7/2 | note:36 s:gm_acoustic_guitar_nylon ]",
"[ 7/2 → 15/4 | note:39 s:gm_acoustic_guitar_nylon ]",
"[ 15/4 → 4/1 | note:41 s:gm_acoustic_guitar_nylon ]",
]
`;
exports[`runs examples > example "reset" example index 0 1`] = `
[
"[ 0/1 → 1/4 | s:hh ]",
@@ -4009,107 +3654,16 @@ exports[`runs examples > example "room" example index 1 1`] = `
]
`;
exports[`runs examples > example "roomdim" example index 0 1`] = `
[
"[ 0/1 → 1/2 | s:bd room:0.5 roomlp:10000 roomdim:8000 ]",
"[ 1/2 → 1/1 | s:sd room:0.5 roomlp:10000 roomdim:8000 ]",
"[ 1/1 → 3/2 | s:bd room:0.5 roomlp:10000 roomdim:8000 ]",
"[ 3/2 → 2/1 | s:sd room:0.5 roomlp:10000 roomdim:8000 ]",
"[ 2/1 → 5/2 | s:bd room:0.5 roomlp:10000 roomdim:8000 ]",
"[ 5/2 → 3/1 | s:sd room:0.5 roomlp:10000 roomdim:8000 ]",
"[ 3/1 → 7/2 | s:bd room:0.5 roomlp:10000 roomdim:8000 ]",
"[ 7/2 → 4/1 | s:sd room:0.5 roomlp:10000 roomdim:8000 ]",
]
`;
exports[`runs examples > example "roomdim" example index 1 1`] = `
[
"[ 0/1 → 1/2 | s:bd room:0.5 roomlp:5000 roomdim:400 ]",
"[ 1/2 → 1/1 | s:sd room:0.5 roomlp:5000 roomdim:400 ]",
"[ 1/1 → 3/2 | s:bd room:0.5 roomlp:5000 roomdim:400 ]",
"[ 3/2 → 2/1 | s:sd room:0.5 roomlp:5000 roomdim:400 ]",
"[ 2/1 → 5/2 | s:bd room:0.5 roomlp:5000 roomdim:400 ]",
"[ 5/2 → 3/1 | s:sd room:0.5 roomlp:5000 roomdim:400 ]",
"[ 3/1 → 7/2 | s:bd room:0.5 roomlp:5000 roomdim:400 ]",
"[ 7/2 → 4/1 | s:sd room:0.5 roomlp:5000 roomdim:400 ]",
]
`;
exports[`runs examples > example "roomfade" example index 0 1`] = `
[
"[ 0/1 → 1/2 | s:bd room:0.5 roomlp:10000 roomfade:0.5 ]",
"[ 1/2 → 1/1 | s:sd room:0.5 roomlp:10000 roomfade:0.5 ]",
"[ 1/1 → 3/2 | s:bd room:0.5 roomlp:10000 roomfade:0.5 ]",
"[ 3/2 → 2/1 | s:sd room:0.5 roomlp:10000 roomfade:0.5 ]",
"[ 2/1 → 5/2 | s:bd room:0.5 roomlp:10000 roomfade:0.5 ]",
"[ 5/2 → 3/1 | s:sd room:0.5 roomlp:10000 roomfade:0.5 ]",
"[ 3/1 → 7/2 | s:bd room:0.5 roomlp:10000 roomfade:0.5 ]",
"[ 7/2 → 4/1 | s:sd room:0.5 roomlp:10000 roomfade:0.5 ]",
]
`;
exports[`runs examples > example "roomfade" example index 1 1`] = `
[
"[ 0/1 → 1/2 | s:bd room:0.5 roomlp:5000 roomfade:4 ]",
"[ 1/2 → 1/1 | s:sd room:0.5 roomlp:5000 roomfade:4 ]",
"[ 1/1 → 3/2 | s:bd room:0.5 roomlp:5000 roomfade:4 ]",
"[ 3/2 → 2/1 | s:sd room:0.5 roomlp:5000 roomfade:4 ]",
"[ 2/1 → 5/2 | s:bd room:0.5 roomlp:5000 roomfade:4 ]",
"[ 5/2 → 3/1 | s:sd room:0.5 roomlp:5000 roomfade:4 ]",
"[ 3/1 → 7/2 | s:bd room:0.5 roomlp:5000 roomfade:4 ]",
"[ 7/2 → 4/1 | s:sd room:0.5 roomlp:5000 roomfade:4 ]",
]
`;
exports[`runs examples > example "roomlp" example index 0 1`] = `
[
"[ 0/1 → 1/2 | s:bd room:0.5 roomlp:10000 ]",
"[ 1/2 → 1/1 | s:sd room:0.5 roomlp:10000 ]",
"[ 1/1 → 3/2 | s:bd room:0.5 roomlp:10000 ]",
"[ 3/2 → 2/1 | s:sd room:0.5 roomlp:10000 ]",
"[ 2/1 → 5/2 | s:bd room:0.5 roomlp:10000 ]",
"[ 5/2 → 3/1 | s:sd room:0.5 roomlp:10000 ]",
"[ 3/1 → 7/2 | s:bd room:0.5 roomlp:10000 ]",
"[ 7/2 → 4/1 | s:sd room:0.5 roomlp:10000 ]",
]
`;
exports[`runs examples > example "roomlp" example index 1 1`] = `
[
"[ 0/1 → 1/2 | s:bd room:0.5 roomlp:5000 ]",
"[ 1/2 → 1/1 | s:sd room:0.5 roomlp:5000 ]",
"[ 1/1 → 3/2 | s:bd room:0.5 roomlp:5000 ]",
"[ 3/2 → 2/1 | s:sd room:0.5 roomlp:5000 ]",
"[ 2/1 → 5/2 | s:bd room:0.5 roomlp:5000 ]",
"[ 5/2 → 3/1 | s:sd room:0.5 roomlp:5000 ]",
"[ 3/1 → 7/2 | s:bd room:0.5 roomlp:5000 ]",
"[ 7/2 → 4/1 | s:sd room:0.5 roomlp:5000 ]",
]
`;
exports[`runs examples > example "roomsize" example index 0 1`] = `
[
"[ 0/1 → 1/2 | s:bd room:0.8 roomsize:1 ]",
"[ 1/2 → 1/1 | s:sd room:0.8 roomsize:1 ]",
"[ 1/1 → 3/2 | s:bd room:0.8 roomsize:1 ]",
"[ 3/2 → 2/1 | s:sd room:0.8 roomsize:1 ]",
"[ 2/1 → 5/2 | s:bd room:0.8 roomsize:1 ]",
"[ 5/2 → 3/1 | s:sd room:0.8 roomsize:1 ]",
"[ 3/1 → 7/2 | s:bd room:0.8 roomsize:1 ]",
"[ 7/2 → 4/1 | s:sd room:0.8 roomsize:1 ]",
]
`;
exports[`runs examples > example "roomsize" example index 1 1`] = `
[
"[ 0/1 → 1/2 | s:bd room:0.8 roomsize:4 ]",
"[ 1/2 → 1/1 | s:sd room:0.8 roomsize:4 ]",
"[ 1/1 → 3/2 | s:bd room:0.8 roomsize:4 ]",
"[ 3/2 → 2/1 | s:sd room:0.8 roomsize:4 ]",
"[ 2/1 → 5/2 | s:bd room:0.8 roomsize:4 ]",
"[ 5/2 → 3/1 | s:sd room:0.8 roomsize:4 ]",
"[ 3/1 → 7/2 | s:bd room:0.8 roomsize:4 ]",
"[ 7/2 → 4/1 | s:sd room:0.8 roomsize:4 ]",
"[ 0/1 → 1/2 | s:bd room:0.8 size:0 ]",
"[ 1/2 → 1/1 | s:sd room:0.8 size:0 ]",
"[ 1/1 → 3/2 | s:bd room:0.8 size:1 ]",
"[ 3/2 → 2/1 | s:sd room:0.8 size:1 ]",
"[ 2/1 → 5/2 | s:bd room:0.8 size:2 ]",
"[ 5/2 → 3/1 | s:sd room:0.8 size:2 ]",
"[ 3/1 → 7/2 | s:bd room:0.8 size:4 ]",
"[ 7/2 → 4/1 | s:sd room:0.8 size:4 ]",
]
`;
@@ -4244,42 +3798,6 @@ exports[`runs examples > example "samples" example index 1 1`] = `
]
`;
exports[`runs examples > example "samples" example index 2 1`] = `
[
"[ 0/1 → 1/2 | s:noise ]",
"[ 1/2 → 3/4 | s:chimp n:0 ]",
"[ 3/4 → 1/1 | s:chimp n:0 ]",
"[ 1/1 → 3/2 | s:noise ]",
"[ 3/2 → 2/1 | s:chimp n:1 ]",
"[ 2/1 → 5/2 | s:noise ]",
"[ 5/2 → 11/4 | s:chimp n:0 ]",
"[ 11/4 → 3/1 | s:chimp n:0 ]",
"[ 3/1 → 7/2 | s:noise ]",
"[ 7/2 → 4/1 | s:chimp n:1 ]",
]
`;
exports[`runs examples > example "samples" example index 3 1`] = `
[
"[ 0/1 → 1/4 | s:chocolat ]",
"[ 1/4 → 1/2 | s:chocolat ]",
"[ 1/2 → 3/4 | s:chocolat ]",
"[ 3/4 → 1/1 | s:chocolat ]",
"[ 1/1 → 5/4 | s:chocolat ]",
"[ 5/4 → 3/2 | s:chocolat ]",
"[ 3/2 → 7/4 | s:chocolat ]",
"[ 7/4 → 2/1 | s:chocolat ]",
"[ 2/1 → 9/4 | s:chocolat ]",
"[ 9/4 → 5/2 | s:chocolat ]",
"[ 5/2 → 11/4 | s:chocolat ]",
"[ 11/4 → 3/1 | s:chocolat ]",
"[ 3/1 → 13/4 | s:chocolat ]",
"[ 13/4 → 7/2 | s:chocolat ]",
"[ 7/2 → 15/4 | s:chocolat ]",
"[ 15/4 → 4/1 | s:chocolat ]",
]
`;
exports[`runs examples > example "saw" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:c3 clip:0.03125 ]",
@@ -4324,96 +3842,96 @@ exports[`runs examples > example "saw" example index 1 1`] = `
exports[`runs examples > example "scale" example index 0 1`] = `
[
"[ 0/1 → 1/6 | note:C3 ]",
"[ 1/6 → 1/3 | note:E3 ]",
"[ 1/3 → 1/2 | note:G3 ]",
"[ 1/2 → 2/3 | note:B3 ]",
"[ 2/3 → 5/6 | note:G3 ]",
"[ 5/6 → 1/1 | note:E3 ]",
"[ 1/1 → 7/6 | note:C3 ]",
"[ 7/6 → 4/3 | note:E3 ]",
"[ 4/3 → 3/2 | note:G3 ]",
"[ 3/2 → 5/3 | note:B3 ]",
"[ 5/3 → 11/6 | note:G3 ]",
"[ 11/6 → 2/1 | note:E3 ]",
"[ 2/1 → 13/6 | note:C3 ]",
"[ 13/6 → 7/3 | note:E3 ]",
"[ 7/3 → 5/2 | note:G3 ]",
"[ 5/2 → 8/3 | note:B3 ]",
"[ 8/3 → 17/6 | note:G3 ]",
"[ 17/6 → 3/1 | note:E3 ]",
"[ 3/1 → 19/6 | note:C3 ]",
"[ 19/6 → 10/3 | note:E3 ]",
"[ 10/3 → 7/2 | note:G3 ]",
"[ 7/2 → 11/3 | note:B3 ]",
"[ 11/3 → 23/6 | note:G3 ]",
"[ 23/6 → 4/1 | note:E3 ]",
"[ 0/1 → 1/6 | n:0 note:C3 ]",
"[ 1/6 → 1/3 | n:2 note:E3 ]",
"[ 1/3 → 1/2 | n:4 note:G3 ]",
"[ 1/2 → 2/3 | n:6 note:B3 ]",
"[ 2/3 → 5/6 | n:4 note:G3 ]",
"[ 5/6 → 1/1 | n:2 note:E3 ]",
"[ 1/1 → 7/6 | n:0 note:C3 ]",
"[ 7/6 → 4/3 | n:2 note:E3 ]",
"[ 4/3 → 3/2 | n:4 note:G3 ]",
"[ 3/2 → 5/3 | n:6 note:B3 ]",
"[ 5/3 → 11/6 | n:4 note:G3 ]",
"[ 11/6 → 2/1 | n:2 note:E3 ]",
"[ 2/1 → 13/6 | n:0 note:C3 ]",
"[ 13/6 → 7/3 | n:2 note:E3 ]",
"[ 7/3 → 5/2 | n:4 note:G3 ]",
"[ 5/2 → 8/3 | n:6 note:B3 ]",
"[ 8/3 → 17/6 | n:4 note:G3 ]",
"[ 17/6 → 3/1 | n:2 note:E3 ]",
"[ 3/1 → 19/6 | n:0 note:C3 ]",
"[ 19/6 → 10/3 | n:2 note:E3 ]",
"[ 10/3 → 7/2 | n:4 note:G3 ]",
"[ 7/2 → 11/3 | n:6 note:B3 ]",
"[ 11/3 → 23/6 | n:4 note:G3 ]",
"[ 23/6 → 4/1 | n:2 note:E3 ]",
]
`;
exports[`runs examples > example "scale" example index 1 1`] = `
[
"[ 0/1 → 1/4 | note:C3 s:piano ]",
"[ 0/1 → 1/4 | note:C4 s:piano ]",
"[ 1/4 → 1/2 | note:G3 s:piano ]",
"[ 1/2 → 3/4 | note:E3 s:piano ]",
"[ 1/2 → 3/4 | note:C4 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 → 3/2 | note:G3 s:piano ]",
"[ 3/2 → 7/4 | note:E3 s:piano ]",
"[ 3/2 → 7/4 | note:C4 s:piano ]",
"[ 7/4 → 2/1 | note:G3 s:piano ]",
"[ 2/1 → 9/4 | note:C3 s:piano ]",
"[ 2/1 → 9/4 | note:C4 s:piano ]",
"[ 9/4 → 5/2 | note:G3 s:piano ]",
"[ 5/2 → 11/4 | note:Eb3 s:piano ]",
"[ 5/2 → 11/4 | note:C4 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 → 7/2 | note:G3 s:piano ]",
"[ 7/2 → 15/4 | note:Eb3 s:piano ]",
"[ 7/2 → 15/4 | note:C4 s:piano ]",
"[ 15/4 → 4/1 | note:G3 s:piano ]",
"[ 0/1 → 1/4 | n:0 note:C3 s:piano ]",
"[ 0/1 → 1/4 | n:7 note:C4 s:piano ]",
"[ 1/4 → 1/2 | n:4 note:G3 s:piano ]",
"[ 1/2 → 3/4 | n:2 note:E3 s:piano ]",
"[ 1/2 → 3/4 | n:7 note:C4 s:piano ]",
"[ 3/4 → 1/1 | n:4 note:G3 s:piano ]",
"[ 1/1 → 5/4 | n:0 note:C3 s:piano ]",
"[ 1/1 → 5/4 | n:7 note:C4 s:piano ]",
"[ 5/4 → 3/2 | n:4 note:G3 s:piano ]",
"[ 3/2 → 7/4 | n:2 note:E3 s:piano ]",
"[ 3/2 → 7/4 | n:7 note:C4 s:piano ]",
"[ 7/4 → 2/1 | n:4 note:G3 s:piano ]",
"[ 2/1 → 9/4 | n:0 note:C3 s:piano ]",
"[ 2/1 → 9/4 | n:7 note:C4 s:piano ]",
"[ 9/4 → 5/2 | n:4 note:G3 s:piano ]",
"[ 5/2 → 11/4 | n:2 note:Eb3 s:piano ]",
"[ 5/2 → 11/4 | n:7 note:C4 s:piano ]",
"[ 11/4 → 3/1 | n:4 note:G3 s:piano ]",
"[ 3/1 → 13/4 | n:0 note:C3 s:piano ]",
"[ 3/1 → 13/4 | n:7 note:C4 s:piano ]",
"[ 13/4 → 7/2 | n:4 note:G3 s:piano ]",
"[ 7/2 → 15/4 | n:2 note:Eb3 s:piano ]",
"[ 7/2 → 15/4 | n:7 note:C4 s:piano ]",
"[ 15/4 → 4/1 | n:4 note:G3 s:piano ]",
]
`;
exports[`runs examples > example "scale" example index 2 1`] = `
[
"[ 0/1 → 1/8 | note:C5 s:folkharp ]",
"[ 1/8 → 1/4 | note:F3 s:folkharp ]",
"[ 1/4 → 3/8 | note:F4 s:folkharp ]",
"[ 3/8 → 1/2 | note:A3 s:folkharp ]",
"[ 1/2 → 5/8 | note:F3 s:folkharp ]",
"[ 5/8 → 3/4 | note:C4 s:folkharp ]",
"[ 3/4 → 7/8 | note:A4 s:folkharp ]",
"[ 7/8 → 1/1 | note:G4 s:folkharp ]",
"[ 1/1 → 9/8 | note:F4 s:folkharp ]",
"[ 9/8 → 5/4 | note:D3 s:folkharp ]",
"[ 5/4 → 11/8 | note:D3 s:folkharp ]",
"[ 11/8 → 3/2 | note:D4 s:folkharp ]",
"[ 3/2 → 13/8 | note:F3 s:folkharp ]",
"[ 13/8 → 7/4 | note:A3 s:folkharp ]",
"[ 7/4 → 15/8 | note:D4 s:folkharp ]",
"[ 15/8 → 2/1 | note:C5 s:folkharp ]",
"[ 2/1 → 17/8 | note:A3 s:folkharp ]",
"[ 17/8 → 9/4 | note:C3 s:folkharp ]",
"[ 9/4 → 19/8 | note:G4 s:folkharp ]",
"[ 19/8 → 5/2 | note:F3 s:folkharp ]",
"[ 5/2 → 21/8 | note:F4 s:folkharp ]",
"[ 21/8 → 11/4 | note:D4 s:folkharp ]",
"[ 11/4 → 23/8 | note:D5 s:folkharp ]",
"[ 23/8 → 3/1 | note:G3 s:folkharp ]",
"[ 3/1 → 25/8 | note:C3 s:folkharp ]",
"[ 25/8 → 13/4 | note:D5 s:folkharp ]",
"[ 13/4 → 27/8 | note:A3 s:folkharp ]",
"[ 27/8 → 7/2 | note:A4 s:folkharp ]",
"[ 7/2 → 29/8 | note:C5 s:folkharp ]",
"[ 29/8 → 15/4 | note:F5 s:folkharp ]",
"[ 15/4 → 31/8 | note:D3 s:folkharp ]",
"[ 31/8 → 4/1 | note:A3 s:folkharp ]",
"[ 0/1 → 1/8 | n:10 note:C5 s:folkharp ]",
"[ 1/8 → 1/4 | n:2 note:F3 s:folkharp ]",
"[ 1/4 → 3/8 | n:7 note:F4 s:folkharp ]",
"[ 3/8 → 1/2 | n:4 note:A3 s:folkharp ]",
"[ 1/2 → 5/8 | n:2 note:F3 s:folkharp ]",
"[ 5/8 → 3/4 | n:5 note:C4 s:folkharp ]",
"[ 3/4 → 7/8 | n:9 note:A4 s:folkharp ]",
"[ 7/8 → 1/1 | n:8 note:G4 s:folkharp ]",
"[ 1/1 → 9/8 | n:7 note:F4 s:folkharp ]",
"[ 9/8 → 5/4 | n:1 note:D3 s:folkharp ]",
"[ 5/4 → 11/8 | n:1 note:D3 s:folkharp ]",
"[ 11/8 → 3/2 | n:6 note:D4 s:folkharp ]",
"[ 3/2 → 13/8 | n:2 note:F3 s:folkharp ]",
"[ 13/8 → 7/4 | n:4 note:A3 s:folkharp ]",
"[ 7/4 → 15/8 | n:6 note:D4 s:folkharp ]",
"[ 15/8 → 2/1 | n:10 note:C5 s:folkharp ]",
"[ 2/1 → 17/8 | n:4 note:A3 s:folkharp ]",
"[ 17/8 → 9/4 | n:0 note:C3 s:folkharp ]",
"[ 9/4 → 19/8 | n:8 note:G4 s:folkharp ]",
"[ 19/8 → 5/2 | n:2 note:F3 s:folkharp ]",
"[ 5/2 → 21/8 | n:7 note:F4 s:folkharp ]",
"[ 21/8 → 11/4 | n:6 note:D4 s:folkharp ]",
"[ 11/4 → 23/8 | n:11 note:D5 s:folkharp ]",
"[ 23/8 → 3/1 | n:3 note:G3 s:folkharp ]",
"[ 3/1 → 25/8 | n:0 note:C3 s:folkharp ]",
"[ 25/8 → 13/4 | n:11 note:D5 s:folkharp ]",
"[ 13/4 → 27/8 | n:4 note:A3 s:folkharp ]",
"[ 27/8 → 7/2 | n:9 note:A4 s:folkharp ]",
"[ 7/2 → 29/8 | n:10 note:C5 s:folkharp ]",
"[ 29/8 → 15/4 | n:12 note:F5 s:folkharp ]",
"[ 15/4 → 31/8 | n:1 note:D3 s:folkharp ]",
"[ 31/8 → 4/1 | n:4 note:A3 s:folkharp ]",
]
`;
@@ -4438,15 +3956,6 @@ exports[`runs examples > example "scaleTranspose" example index 0 1`] = `
]
`;
exports[`runs examples > example "scope" example index 0 1`] = `
[
"[ 0/1 → 1/1 | s:sawtooth analyze:1 ]",
"[ 1/1 → 2/1 | s:sawtooth analyze:1 ]",
"[ 2/1 → 3/1 | s:sawtooth analyze:1 ]",
"[ 3/1 → 4/1 | s:sawtooth analyze:1 ]",
]
`;
exports[`runs examples > example "segment" example index 0 1`] = `
[
"[ 0/1 → 1/24 | note:40.25 ]",
@@ -4884,25 +4393,6 @@ exports[`runs examples > example "square" example index 0 1`] = `
]
`;
exports[`runs examples > example "squeeze" example index 0 1`] = `
[
"[ 0/1 → 1/1 | note:g ]",
"[ 1/1 → 2/1 | note:a ]",
"[ 2/1 → 17/8 | note:f ]",
"[ 17/8 → 9/4 | note:g ]",
"[ 9/4 → 19/8 | note:f ]",
"[ 19/8 → 5/2 | note:g ]",
"[ 5/2 → 21/8 | note:f ]",
"[ 21/8 → 11/4 | note:g ]",
"[ 11/4 → 23/8 | note:f ]",
"[ 23/8 → 3/1 | note:g ]",
"[ 3/1 → 13/4 | note:g ]",
"[ 13/4 → 7/2 | note:a ]",
"[ 7/2 → 15/4 | note:c ]",
"[ 15/4 → 4/1 | note:d ]",
]
`;
exports[`runs examples > example "squiz" example index 0 1`] = `
[
"[ 0/1 → 1/4 | squiz:2 s:bd ]",
@@ -4974,23 +4464,6 @@ exports[`runs examples > example "stack" example index 0 2`] = `
]
`;
exports[`runs examples > example "striate" example index 0 1`] = `
[
"[ 0/1 → 1/3 | s:numbers n:0 begin:0 end:0.16666666666666666 ]",
"[ 1/3 → 2/3 | s:numbers n:1 begin:0 end:0.16666666666666666 ]",
"[ 2/3 → 1/1 | s:numbers n:2 begin:0 end:0.16666666666666666 ]",
"[ 1/1 → 4/3 | s:numbers n:0 begin:0.16666666666666666 end:0.3333333333333333 ]",
"[ 4/3 → 5/3 | s:numbers n:1 begin:0.16666666666666666 end:0.3333333333333333 ]",
"[ 5/3 → 2/1 | s:numbers n:2 begin:0.16666666666666666 end:0.3333333333333333 ]",
"[ 2/1 → 7/3 | s:numbers n:0 begin:0.3333333333333333 end:0.5 ]",
"[ 7/3 → 8/3 | s:numbers n:1 begin:0.3333333333333333 end:0.5 ]",
"[ 8/3 → 3/1 | s:numbers n:2 begin:0.3333333333333333 end:0.5 ]",
"[ 3/1 → 10/3 | s:numbers n:0 begin:0.5 end:0.6666666666666666 ]",
"[ 10/3 → 11/3 | s:numbers n:1 begin:0.5 end:0.6666666666666666 ]",
"[ 11/3 → 4/1 | s:numbers n:2 begin:0.5 end:0.6666666666666666 ]",
]
`;
exports[`runs examples > example "struct" example index 0 1`] = `
[
"[ 0/1 → 1/4 | note:c3 ]",
@@ -5447,51 +4920,6 @@ exports[`runs examples > example "withValue" example index 0 1`] = `
]
`;
exports[`runs examples > example "xfade" example index 0 1`] = `
[
"[ 0/1 → 1/8 | s:hh gain:0 ]",
"[ 0/1 → 1/2 | s:bd gain:1 ]",
"[ 1/8 → 1/4 | s:hh gain:0 ]",
"[ 1/4 → 3/8 | s:hh gain:0 ]",
"[ 3/8 → 1/2 | s:hh gain:0 ]",
"[ 1/2 → 5/8 | s:hh gain:0 ]",
"[ 1/2 → 1/1 | s:bd gain:1 ]",
"[ 5/8 → 3/4 | s:hh gain:0 ]",
"[ 3/4 → 7/8 | s:hh gain:0 ]",
"[ 7/8 → 1/1 | s:hh gain:0 ]",
"[ 1/1 → 9/8 | s:hh gain:0.5 ]",
"[ 1/1 → 3/2 | s:bd gain:1 ]",
"[ 9/8 → 5/4 | s:hh gain:0.5 ]",
"[ 5/4 → 11/8 | s:hh gain:0.5 ]",
"[ 11/8 → 3/2 | s:hh gain:0.5 ]",
"[ 3/2 → 13/8 | s:hh gain:0.5 ]",
"[ 3/2 → 2/1 | s:bd gain:1 ]",
"[ 13/8 → 7/4 | s:hh gain:0.5 ]",
"[ 7/4 → 15/8 | s:hh gain:0.5 ]",
"[ 15/8 → 2/1 | s:hh gain:0.5 ]",
"[ 2/1 → 17/8 | s:hh gain:1 ]",
"[ 2/1 → 5/2 | s:bd gain:1 ]",
"[ 17/8 → 9/4 | s:hh gain:1 ]",
"[ 9/4 → 19/8 | s:hh gain:1 ]",
"[ 19/8 → 5/2 | s:hh gain:1 ]",
"[ 5/2 → 21/8 | s:hh gain:1 ]",
"[ 5/2 → 3/1 | s:bd gain:1 ]",
"[ 21/8 → 11/4 | s:hh gain:1 ]",
"[ 11/4 → 23/8 | s:hh gain:1 ]",
"[ 23/8 → 3/1 | s:hh gain:1 ]",
"[ 3/1 → 25/8 | s:hh gain:1 ]",
"[ 3/1 → 7/2 | s:bd gain:0.5 ]",
"[ 25/8 → 13/4 | s:hh gain:1 ]",
"[ 13/4 → 27/8 | s:hh gain:1 ]",
"[ 27/8 → 7/2 | s:hh gain:1 ]",
"[ 7/2 → 29/8 | s:hh gain:1 ]",
"[ 7/2 → 4/1 | s:bd gain:0.5 ]",
"[ 29/8 → 15/4 | s:hh gain:1 ]",
"[ 15/4 → 31/8 | s:hh gain:1 ]",
"[ 31/8 → 4/1 | s:hh gain:1 ]",
]
`;
exports[`runs examples > example "zoom" example index 0 1`] = `
[
"[ 0/1 → 1/6 | s:hh ]",
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -228,5 +228,5 @@ export const testCycles = {
festivalOfFingers3: 16,
};
// fixed: https://strudel.cc/?DBp75NUfSxIn (missing .note())
// bug: https://strudel.cc/?xHaKTd1kTpCn + https://strudel.cc/?o5LLePbx8kiQ
// fixed: https://strudel.tidalcycles.org/?DBp75NUfSxIn (missing .note())
// bug: https://strudel.tidalcycles.org/?xHaKTd1kTpCn + https://strudel.tidalcycles.org/?o5LLePbx8kiQ
+1 -1
View File
@@ -4,7 +4,7 @@ import data from './dbdump.json';
describe('renders shared tunes', async () => {
data.forEach(({ id, code, hash }) => {
const url = `https://strudel.cc/?${hash}`;
const url = `https://strudel.tidalcycles.org/?${hash}`;
it(`shared tune ${id} ${url}`, async ({ expect }) => {
if (code.includes('import(')) {
console.log('skip', url);
+2 -2
View File
@@ -1,6 +1,6 @@
# Strudel Website
This is the website for Strudel, deployed at [strudel.cc](https://strudel.cc).
This is the website for Strudel, deployed at [strudel.tidalcycles.org](https://strudel.tidalcycles.org/).
It includes the REPL live coding editor and the documentation site.
## Run locally
@@ -56,7 +56,7 @@ All commands are run from the root of the project, from a terminal:
| Command | Action |
| :--------------------- | :----------------------------------------------- |
| `npm install` | Installs dependencies |
| `npm run dev` | Starts local dev server at `localhost:4321` |
| `npm run dev` | Starts local dev server at `localhost:3000` |
| `npm run build` | Build your production site to `./dist/` |
| `npm run preview` | Preview your build locally, before deploying |
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |

Some files were not shown because too many files have changed in this diff Show More